2025-09-16 09:38:01 +08:00
|
|
|
|
// 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");
|
2025-09-17 13:15:15 +08:00
|
|
|
|
|
2025-09-16 09:38:01 +08:00
|
|
|
|
const app = express();
|
2025-09-17 13:15:15 +08:00
|
|
|
|
const port = 30009;
|
2025-09-16 09:38:01 +08:00
|
|
|
|
|
2025-09-17 13:15:15 +08:00
|
|
|
|
const mysql = require("mysql2/promise");
|
2025-09-16 09:38:01 +08:00
|
|
|
|
// 初始化数据库连接池
|
|
|
|
|
|
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 字段
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化混元客户端
|
2025-09-17 15:42:09 +08:00
|
|
|
|
// 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/
|
|
|
|
|
|
// 初始化客户端
|
|
|
|
|
|
|
2025-09-16 09:38:01 +08:00
|
|
|
|
const client = new OpenAI({
|
2025-09-17 15:42:09 +08:00
|
|
|
|
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
|
2025-09-16 09:38:01 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-17 13:15:15 +08:00
|
|
|
|
// 查询用户所有会话列表
|
|
|
|
|
|
app.get("/sessions", async (req, res) => {
|
|
|
|
|
|
|
2025-09-17 15:42:09 +08:00
|
|
|
|
console.log("收到的 userId:", req.query.userId);
|
|
|
|
|
|
try {
|
2025-09-17 21:18:51 +08:00
|
|
|
|
const userId = req.query.userId;
|
|
|
|
|
|
if (!userId || userId === "NaN" || isNaN(Number(userId)))
|
|
|
|
|
|
{
|
|
|
|
|
|
console.log("userId:", req.query.userId);
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
code: 300,
|
|
|
|
|
|
error: "http://1.95.137.212/#/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');
|
2025-09-17 15:42:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-17 21:18:51 +08:00
|
|
|
|
// return;
|
2025-09-17 15:42:09 +08:00
|
|
|
|
// 查该用户所有的会话,按最后活跃时间倒序
|
|
|
|
|
|
const [rows] = await pool.query(
|
|
|
|
|
|
`
|
|
|
|
|
|
SELECT session_id, MAX(created_at) AS last_time
|
|
|
|
|
|
FROM ai_chat_messages
|
|
|
|
|
|
WHERE user_id = ?
|
|
|
|
|
|
GROUP BY session_id
|
|
|
|
|
|
ORDER BY last_time DESC
|
|
|
|
|
|
`,
|
|
|
|
|
|
[userId]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
code: 0,
|
|
|
|
|
|
userId,
|
|
|
|
|
|
sessions: rows.map((r) => r.session_id), // 只返回 session_id 列表
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (err)
|
|
|
|
|
|
{
|
|
|
|
|
|
console.error("查询会话列表失败:", err);
|
|
|
|
|
|
res.status(500).json({ code: 1, error: err.message });
|
|
|
|
|
|
}
|
2025-09-17 13:15:15 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-16 09:38:01 +08:00
|
|
|
|
// 查询会话历史
|
|
|
|
|
|
app.get("/history", async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { sessionId } = req.query;
|
|
|
|
|
|
if (!sessionId) {
|
|
|
|
|
|
return res.status(400).json({ code: 1, error: "sessionId 必填" });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查询数据库
|
|
|
|
|
|
const [rows] = await pool.query(
|
|
|
|
|
|
"SELECT user_id, role, content, image_base64, created_at 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 });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ====================== 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 { userPrompt, imageBase64, userId, sessionId } = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果前端没有传 sessionId,生成一个
|
|
|
|
|
|
const sessId = sessionId || Date.now().toString();
|
|
|
|
|
|
|
|
|
|
|
|
// ------------------ 1️⃣ 保存用户提问 ------------------
|
|
|
|
|
|
await pool.query(
|
|
|
|
|
|
"INSERT INTO ai_chat_messages (session_id, user_id, role, content, image_base64) VALUES (?, ?, 'user', ?, ?)",
|
|
|
|
|
|
[sessId, userId || null, userPrompt || "", imageBase64 || null]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// ------------------ 2️⃣ 图像分析 ------------------
|
|
|
|
|
|
const completion = await client.chat.completions.create({
|
2025-09-17 15:42:09 +08:00
|
|
|
|
model: "gpt-4.1",
|
2025-09-16 09:38:01 +08:00
|
|
|
|
stream: true,
|
2025-09-17 15:42:09 +08:00
|
|
|
|
// do_scene: true,
|
|
|
|
|
|
// do_behavior: true,
|
|
|
|
|
|
// llm_model: "hunyuan-turbos-latest",
|
2025-09-16 09:38:01 +08:00
|
|
|
|
messages: [
|
|
|
|
|
|
{
|
|
|
|
|
|
role: "system",
|
|
|
|
|
|
content:
|
2025-09-17 14:27:17 +08:00
|
|
|
|
"",
|
2025-09-16 09:38:01 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
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),
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存图像分析结果到数据库
|
|
|
|
|
|
await pool.query(
|
|
|
|
|
|
"INSERT INTO ai_chat_messages (session_id, user_id, role, content) VALUES (?, ?, 'assistant', ?)",
|
|
|
|
|
|
[sessId, userId || null, sceneText]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// ------------------ 3️⃣ LLM 分析 ------------------
|
2025-09-17 15:42:09 +08:00
|
|
|
|
const llmStream = await client1.chat.completions.create({
|
|
|
|
|
|
model: "gpt-5-mini",
|
2025-09-16 09:38:01 +08:00
|
|
|
|
stream: true,
|
|
|
|
|
|
messages: [
|
|
|
|
|
|
{
|
|
|
|
|
|
role: "system",
|
2025-09-17 14:27:17 +08:00
|
|
|
|
content:""
|
|
|
|
|
|
// content: "你是一名新能源项目的图像分析AI分析师。",
|
2025-09-16 09:38:01 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
role: "user",
|
2025-09-17 14:27:17 +08:00
|
|
|
|
content: `${sceneText}`,
|
|
|
|
|
|
// content: `请基于以下场景分析结果提供部署建议或优化意见:\n${sceneText}`,
|
2025-09-16 09:38:01 +08:00
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存 LLM 输出到数据库
|
|
|
|
|
|
await pool.query(
|
|
|
|
|
|
"INSERT INTO ai_chat_messages (session_id, user_id, role, content) VALUES (?, ?, 'assistant', ?)",
|
|
|
|
|
|
[sessId, userId || null, llmText]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
res.write("data: [DONE]\n\n");
|
|
|
|
|
|
res.end();
|
|
|
|
|
|
|
|
|
|
|
|
// 删除上传文件
|
|
|
|
|
|
if (req.file && req.file.path) {
|
|
|
|
|
|
fs.unlinkSync(req.file.path);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error("❌ 调用失败:", err.response?.data || err.message);
|
|
|
|
|
|
res.status(500).json({ code: 1, error: err.response?.data || err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 静态文件服务
|
|
|
|
|
|
app.use("/uploads", express.static("uploads"));
|
|
|
|
|
|
|
|
|
|
|
|
// 启动服务
|
|
|
|
|
|
app.listen(port, () => {
|
|
|
|
|
|
console.log(`✅ 服务器运行:http://localhost:${port}`);
|
|
|
|
|
|
});
|