From 60b1bcf8078a57f7d7f47a78f9bb0feb9c802406 Mon Sep 17 00:00:00 2001 From: geyun <1556375442@qq.com> Date: Sat, 24 Jan 2026 16:46:45 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E6=80=9D=E8=80=83=E8=BF=87?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server2.js | 882 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 576 insertions(+), 306 deletions(-) diff --git a/server2.js b/server2.js index 0aedcde..8a8f7bd 100755 --- a/server2.js +++ b/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);