加入思考过程
This commit is contained in:
882
server2.js
882
server2.js
@@ -1037,338 +1037,608 @@ app.post("/refreshAiChatNum", async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
// app.post("/analyze", upload.single("file"), async (req, res) => {
|
||||
// const { userId } = req.body;
|
||||
// try {
|
||||
// const exists = await hasQuota(userId);
|
||||
// if (!exists) {
|
||||
// const [userRemainChatNum] = await pool.query(
|
||||
// "SELECT ai_chat_num from sys_user where user_id= ?",
|
||||
// [userId]
|
||||
// );
|
||||
// if (userRemainChatNum.length > 0) {
|
||||
// const quota = userRemainChatNum[0].ai_chat_num;
|
||||
// if (quota <= 0 && quota !== -1) {
|
||||
// return res.status(400).json({ error: "对话次数已用完,请完成账号注册" });
|
||||
// }
|
||||
// await setQuota(userId, quota);
|
||||
// }
|
||||
// } else {
|
||||
// const quota = await getQuota(userId);
|
||||
// if (quota <= 0 && quota !== -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 = await getQuota(userId);
|
||||
// // if(userChatNum!==-1){
|
||||
// // userChatNum = Math.max(0, userChatNum - 1); // 防止变成负数
|
||||
// // await 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 });
|
||||
// }
|
||||
// finally {
|
||||
// let userChatNum = await getQuota(userId);
|
||||
// if (userChatNum !== -1 && userChatNum !== undefined) {
|
||||
// userChatNum = Math.max(0, userChatNum - 1); // 防止变成负数
|
||||
// await setQuota(userId, userChatNum);
|
||||
// // 更新数据库
|
||||
// await pool.query(
|
||||
// "update sys_user set ai_chat_num = ? where user_id= ?",
|
||||
// [userChatNum, userId]
|
||||
// );
|
||||
// console.log("更新用户对话次数:", userId, userChatNum);
|
||||
// }
|
||||
|
||||
// }
|
||||
// });
|
||||
|
||||
// const app = express();
|
||||
// const upload = require('multer')({ dest: 'uploads/' });
|
||||
// const fs = require('fs');
|
||||
// 替换为你的工具模块
|
||||
// const { pool, client, client1, hasQuota, getQuota, setQuota, stopFlags } = require('./your-utils');
|
||||
|
||||
app.post("/analyze", upload.single("file"), async (req, res) => {
|
||||
const { userId } = req.body;
|
||||
try {
|
||||
const exists = await hasQuota(userId);
|
||||
if (!exists) {
|
||||
const [userRemainChatNum] = await pool.query(
|
||||
"SELECT ai_chat_num from sys_user where user_id= ?",
|
||||
[userId]
|
||||
);
|
||||
if (userRemainChatNum.length > 0) {
|
||||
const quota = userRemainChatNum[0].ai_chat_num;
|
||||
if (quota <= 0 && quota !== -1) {
|
||||
return res.status(400).json({ error: "对话次数已用完,请完成账号注册" });
|
||||
}
|
||||
await setQuota(userId, quota);
|
||||
}
|
||||
} else {
|
||||
const quota = await getQuota(userId);
|
||||
if (quota <= 0 && quota !== -1) {
|
||||
return res.status(400).json({ error: "对话次数已用完,请完成账号注册" });
|
||||
}
|
||||
}
|
||||
const { userId } = req.body;
|
||||
try {
|
||||
const exists = await hasQuota(userId);
|
||||
if (!exists) {
|
||||
const [userRemainChatNum] = await pool.query(
|
||||
"SELECT ai_chat_num from sys_user where user_id= ?",
|
||||
[userId]
|
||||
);
|
||||
if (userRemainChatNum.length > 0) {
|
||||
const quota = userRemainChatNum[0].ai_chat_num;
|
||||
if (quota <= 0 && quota !== -1) {
|
||||
return res.status(400).json({ error: "对话次数已用完,请完成账号注册" });
|
||||
}
|
||||
await setQuota(userId, quota);
|
||||
}
|
||||
} else {
|
||||
const quota = await getQuota(userId);
|
||||
if (quota <= 0 && quota !== -1) {
|
||||
return res.status(400).json({ error: "对话次数已用完,请完成账号注册" });
|
||||
}
|
||||
}
|
||||
|
||||
const { requestId } = req.body;
|
||||
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;
|
||||
});
|
||||
// 统一 SSE 响应头
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/event-stream; charset=utf-8",
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"Transfer-Encoding": "chunked"
|
||||
});
|
||||
res.socket.setNoDelay(true); // 禁用缓冲,实时推送
|
||||
|
||||
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.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)
|
||||
if (!req.body?.userId || !req.body?.sessionId) {
|
||||
console.log("缺少必要参数");
|
||||
return res.write(`data: ${JSON.stringify({ type: "error", content: "缺少必要参数" })}\n\ndata: [DONE]\n\n`), res.end();
|
||||
}
|
||||
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;
|
||||
console.log(existingTitle, rows?.[0], 9999, rows);
|
||||
|
||||
// 如果前端没有传 sessionId,生成一个
|
||||
const sessId = sessionId || Date.now().toString();
|
||||
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 [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 => ({
|
||||
role: row.role,
|
||||
content: [
|
||||
row.content ? { type: "text", text: row.content } : null,
|
||||
row.image_base64 ? {
|
||||
type: "image_url",
|
||||
image_url: { url: row.image_base64.startsWith("data:") ? row.image_base64 : `data:image/png;base64,${row.image_base64}` }
|
||||
} : null
|
||||
].filter(Boolean)
|
||||
}));
|
||||
|
||||
// 构建历史消息数组
|
||||
const historyMessages = historyRows.map(row => {
|
||||
const message = {
|
||||
role: row.role,
|
||||
content: []
|
||||
};
|
||||
// ------------------ 保存用户提问 ------------------
|
||||
await pool.query(
|
||||
"INSERT INTO ai_chat_messages (session_id, user_id, role, content, image_base64) VALUES (?, ?, 'user', ?, ?)",
|
||||
[sessId, userId || null, userPrompt || "", imageBase64 || null]
|
||||
);
|
||||
|
||||
// 添加文本内容
|
||||
if (row.content) {
|
||||
message.content.push({ type: "text", text: row.content });
|
||||
}
|
||||
// ------------------ 1. 图像分析(GPT 直接返回思考过程 + 结果) ------------------
|
||||
console.log("====== 开始图像分析... ======");
|
||||
const imageAnalysisMessages = [
|
||||
{
|
||||
role: "system",
|
||||
content: `你是专注于「新能源运维+智能设备」领域的图像分析专家,同时具备行业信息解答能力,严格按以下规则输出,流式返回且格式不可错乱:
|
||||
1. 思考过程:第一人称推导不需要明确显示是第一人称,先判断用户需求类型(是图像分析还是信息咨询/设备相关问题),再拆解核心诉求,结合行业经验梳理推理逻辑——若为信息咨询,需关联场景(如光伏运维)补充背景;若为图像问题,需按「元素识别→状态判断→需求匹配」推导,全程贴合用户提问重点,不冗余无关信息。
|
||||
2. 分隔符:必须用「---分析结果---」清晰分割思考过程与最终内容,分隔符单独成行,前后无多余文字。
|
||||
3. 最终分析结果:结构简洁,先明确核心结论,再补充关键细节(如企业业务、产品特性、场景适配性),结尾预留延伸空间(引导用户补充信息以获取更精准内容),语言专业且落地,符合光伏运维等实际场景需求。
|
||||
|
||||
// 添加图片内容
|
||||
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}`,
|
||||
}
|
||||
});
|
||||
}
|
||||
要求:思考过程体现「需求判断→逻辑拆解→信息整合」的完整链路,必须按照思考过程-分析结果-最终分析结果的格式排版。最终结果聚焦用户核心诉求,不偏离主题,适配后续LLM深度分析的衔接。`
|
||||
},
|
||||
...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)
|
||||
}
|
||||
];
|
||||
|
||||
return message;
|
||||
});
|
||||
const sceneCompletion = await client.chat.completions.create({
|
||||
model: "gpt-4.1",
|
||||
stream: true,
|
||||
messages: imageAnalysisMessages,
|
||||
// temperature: 0.3 // 降低随机性,保证格式稳定
|
||||
});
|
||||
|
||||
// 流式解析:拆分思考过程和分析结果
|
||||
let sceneState = "thinking"; // thinking: 思考过程阶段, result: 分析结果阶段
|
||||
let sceneThinkingText = "", sceneText = "";
|
||||
const SCENE_SPLITTER = "---分析结果---";
|
||||
|
||||
// ------------------ 保存用户提问 ------------------
|
||||
await pool.query(
|
||||
"INSERT INTO ai_chat_messages (session_id, user_id, role, content, image_base64) VALUES (?, ?, 'user', ?, ?)",
|
||||
[sessId, userId || null, userPrompt || "", imageBase64 || null]
|
||||
);
|
||||
for await (const chunk of sceneCompletion) {
|
||||
if (stopFlags.get(requestId) || aborted) {
|
||||
console.log("⛔ 图像分析被中断");
|
||||
return res.end(), stopFlags.delete(requestId);
|
||||
}
|
||||
const delta = chunk.choices[0]?.delta?.content || "";
|
||||
if (!delta) continue;
|
||||
|
||||
// console.log("userPrompt: ",userPrompt);
|
||||
// console.log("imageBase64: ",imageBase64);
|
||||
// console.log("userId: ",userId);
|
||||
// console.log("sessionId: ",sessionId);
|
||||
// 状态切换:遇到分隔符则切换到结果阶段
|
||||
if (sceneState === "thinking" && delta.includes(SCENE_SPLITTER)) {
|
||||
const [thinkingPart, resultPart] = delta.split(SCENE_SPLITTER);
|
||||
sceneThinkingText += thinkingPart;
|
||||
// 推送思考过程
|
||||
res.write(`data: ${JSON.stringify({ type: "scene_thinking", content: thinkingPart })}\n\n`);
|
||||
sceneState = "result";
|
||||
// 推送分隔符后的结果片段
|
||||
if (resultPart) {
|
||||
sceneText += resultPart;
|
||||
res.write(`data: ${JSON.stringify({ type: "scene", content: resultPart })}\n\n`);
|
||||
}
|
||||
} else if (sceneState === "thinking") {
|
||||
// 持续推送思考过程
|
||||
sceneThinkingText += delta;
|
||||
res.write(`data: ${JSON.stringify({ type: "scene_thinking", content: delta })}\n\n`);
|
||||
} else {
|
||||
// 持续推送分析结果
|
||||
sceneText += delta;
|
||||
res.write(`data: ${JSON.stringify({ type: "scene", content: delta })}\n\n`);
|
||||
}
|
||||
}
|
||||
console.log("====== 图像分析思考过程 ======\n", sceneThinkingText);
|
||||
console.log("====== 图像分析结果 ======\n", sceneText);
|
||||
console.log("====== 图像分析结束 ======");
|
||||
|
||||
// ------------------ 图像分析 ------------------
|
||||
// 构建包含上下文的消息数组
|
||||
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),
|
||||
},
|
||||
];
|
||||
// ------------------ 2. LLM 深度分析(GPT 直接返回思考过程 + 结果) ------------------
|
||||
console.log("====== 开始深度思考... ======");
|
||||
const llmMessages = [
|
||||
{
|
||||
role: "system",
|
||||
content: `你是新能源(光伏/风电)场站运维专家,擅长结合智能设备(如割草机器人)给出可落地的标准化方案,严格按以下规则输出,流式返回且格式不可错乱:
|
||||
1. 思考过程:第一人称推导,先拆解用户核心需求(如设备选型、部署落地、运维优化),再搭建分析框架(按「目标确认→资料收集→现场勘察→试点验证→落地推广」逻辑),明确核心评估维度(安全兼容性、自动化可靠性、运维便利性、经济性),说明方案优先级原则(如「可操作性优先、风险可控」),全程紧扣用户提及的设备品牌(如飒踏智能),推导逻辑与后续方案强关联。
|
||||
2. 分隔符:必须用「---方案结果---」清晰分割思考过程与最终内容,分隔符单独成行,前后无多余文字。
|
||||
3. 最终运维方案:按「时间节点明确、步骤可落地、指标可量化」原则,拆分各阶段任务(含时间周期、核心动作、输出物),补充标准化SOP、KPI设定、风险防控要点,兼顾安全性(对光伏设备无损伤)、实用性(适配场站场景)、经济性(成本可控),方案结构清晰,可直接用于场站作业指导或采购决策。
|
||||
|
||||
const completion = await client.chat.completions.create({
|
||||
model: "gpt-4.1",
|
||||
stream: true,
|
||||
messages: imageAnalysisMessages
|
||||
});
|
||||
要求:思考过程不跳跃,每个分析维度对应方案中的具体环节;最终方案避免空泛,补充可量化指标、资料清单、勘察要点等细节,同时预留灵活调整空间(适配不同场站规模、设备型号)。`
|
||||
},
|
||||
...historyMessages,
|
||||
{ role: "user", content: userPrompt || "请基于图片分析提供建议" },
|
||||
{ role: "assistant", content: sceneText }
|
||||
];
|
||||
|
||||
console.log("====== 开始图像分析... ======")
|
||||
let sceneText = "";
|
||||
const llmStream = await client1.chat.completions.create({
|
||||
model: "gpt-5-mini",
|
||||
stream: true,
|
||||
messages: llmMessages,
|
||||
// temperature: 0.4
|
||||
});
|
||||
|
||||
/* TLS 加密缓冲,小块数据可能先缓存在服务器端,不立即发送,会导致图像分析模型的结果不能立即返回 */
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
});
|
||||
res.socket.setNoDelay(true);
|
||||
// 流式解析:拆分LLM思考过程和方案结果
|
||||
let llmState = "thinking";
|
||||
let llmThinkingText = "", llmText = "";
|
||||
const LLM_SPLITTER = "---方案结果---";
|
||||
|
||||
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;
|
||||
for await (const chunk of llmStream) {
|
||||
if (stopFlags.get(requestId) || aborted) {
|
||||
console.log("⛔ 深度分析被中断");
|
||||
return res.end(), stopFlags.delete(requestId);
|
||||
}
|
||||
const delta = chunk.choices[0]?.delta?.content || "";
|
||||
if (!delta) continue;
|
||||
|
||||
if (delta) {
|
||||
res.write(
|
||||
`data: ${JSON.stringify({ type: "scene", content: delta })}\n\n`
|
||||
);
|
||||
}
|
||||
if (llmState === "thinking" && delta.includes(LLM_SPLITTER)) {
|
||||
const [thinkingPart, resultPart] = delta.split(LLM_SPLITTER);
|
||||
llmThinkingText += thinkingPart;
|
||||
res.write(`data: ${JSON.stringify({ type: "llm_thinking", content: thinkingPart })}\n\n`);
|
||||
llmState = "result";
|
||||
if (resultPart) {
|
||||
llmText += resultPart;
|
||||
res.write(`data: ${JSON.stringify({ type: "llm", content: resultPart })}\n\n`);
|
||||
}
|
||||
} else if (llmState === "thinking") {
|
||||
llmThinkingText += delta;
|
||||
res.write(`data: ${JSON.stringify({ type: "llm_thinking", content: delta })}\n\n`);
|
||||
} else {
|
||||
llmText += delta;
|
||||
res.write(`data: ${JSON.stringify({ type: "llm", content: delta })}\n\n`);
|
||||
}
|
||||
}
|
||||
console.log("====== LLM思考过程 ======\n", llmThinkingText);
|
||||
console.log("====== LLM方案结果 ======\n", llmText);
|
||||
console.log("====== 深度思考结束 ======");
|
||||
|
||||
// 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("====== 图像分析结束 ======")
|
||||
// ------------------ 保存结果 & 生成标题 ------------------
|
||||
if (existingTitle) {
|
||||
await pool.query(
|
||||
"INSERT INTO ai_chat_messages (session_id, user_id, role, content) VALUES (?, ?, 'assistant', ?)",
|
||||
[sessId, userId || null, llmText]
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
const titleResp = await client1.chat.completions.create({
|
||||
model: "gpt-5-mini",
|
||||
messages: [
|
||||
{ role: "system", content: "只返回15字以内的标题,无解释" },
|
||||
{ role: "user", content: `用户问题: ${userPrompt}\nAI回答: ${llmText}` }
|
||||
]
|
||||
});
|
||||
const title = titleResp.choices[0]?.message?.content?.trim() || "未命名会话";
|
||||
res.write(`data: ${JSON.stringify({ type: "title", content: title })}\n\n`);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存图像分析结果到数据库
|
||||
// await pool.query(
|
||||
// "INSERT INTO ai_chat_messages (session_id, user_id, role, content) VALUES (?, ?, 'assistant', ?)",
|
||||
// [sessId, userId || null, sceneText]
|
||||
// );
|
||||
// ------------------ 结束流程 ------------------
|
||||
res.write("data: [DONE]\n\n");
|
||||
res.end();
|
||||
if (req.file?.path) fs.unlinkSync(req.file.path);
|
||||
console.log("====== 分析完成 ======");
|
||||
console.timeEnd("analyze");
|
||||
|
||||
// ------------------ 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 = await getQuota(userId);
|
||||
// if(userChatNum!==-1){
|
||||
// userChatNum = Math.max(0, userChatNum - 1); // 防止变成负数
|
||||
// await 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 });
|
||||
}
|
||||
finally {
|
||||
let userChatNum = await getQuota(userId);
|
||||
if (userChatNum !== -1 && userChatNum !== undefined) {
|
||||
userChatNum = Math.max(0, userChatNum - 1); // 防止变成负数
|
||||
await setQuota(userId, userChatNum);
|
||||
// 更新数据库
|
||||
await pool.query(
|
||||
"update sys_user set ai_chat_num = ? where user_id= ?",
|
||||
[userChatNum, userId]
|
||||
);
|
||||
console.log("更新用户对话次数:", userId, userChatNum);
|
||||
}
|
||||
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ 调用失败:", err.response?.data || err.message);
|
||||
res.write(`data: ${JSON.stringify({ type: "error", content: err.response?.data || err.message })}\n\n`);
|
||||
res.write("data: [DONE]\n\n");
|
||||
res.end();
|
||||
} finally {
|
||||
let userChatNum = await getQuota(userId);
|
||||
if (userChatNum !== -1 && userChatNum !== undefined) {
|
||||
userChatNum = Math.max(0, userChatNum - 1);
|
||||
await setQuota(userId, userChatNum);
|
||||
await pool.query("update sys_user set ai_chat_num = ? where user_id= ?", [userChatNum, userId]);
|
||||
console.log("更新用户对话次数:", userId, userChatNum);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
|
||||
app.post("/stop", express.json(), (req, res) => {
|
||||
const { requestId } = req.body;
|
||||
console.log("🔴 收到停止信号:", requestId);
|
||||
|
||||
Reference in New Issue
Block a user