// 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: "4pJOKPLhkW7qJQOeQBe9ACXyBrcu7A8bAjTgo2auV5lSUZbmcn1rJQQJ99CEACHYHv6XJ3w3AAAAACOGk1hV", //baseURL: "https://tech-mp6cvddb-eastus2.cognitiveservices.azure.com/", //apiKey: "DxvRiQWROzLTapiSHcK0FXKAAXDBlhci1XaqTTpr50sgUsAkmxfUJQQJ99BIACYeBjFXJ3w3AAABACOGZdHp", apiKey: "DxvRiQWROzLTapiSHcK0FXKAAXDBlhci1XaqTTpr50sgUsAkmxfUJQQJ99BIACYeBjFXJ3w3AAABACOGZdHp", baseURL: "https://satabot.services.ai.azure.com/openai/v1/", //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: "4pJOKPLhkW7qJQOeQBe9ACXyBrcu7A8bAjTgo2auV5lSUZbmcn1rJQQJ99CEACHYHv6XJ3w3AAAAACOGk1hV", //baseURL: "https://tech-mp6cvddb-eastus2.cognitiveservices.azure.com/", //apiKey: "DxvRiQWROzLTapiSHcK0FXKAAXDBlhci1XaqTTpr50sgUsAkmxfUJQQJ99BIACYeBjFXJ3w3AAABACOGZdHp", apiKey: "DxvRiQWROzLTapiSHcK0FXKAAXDBlhci1XaqTTpr50sgUsAkmxfUJQQJ99BIACYeBjFXJ3w3AAABACOGZdHp", baseURL: "https://satabot.services.ai.azure.com/openai/v1/", //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": "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" }, // { // "type": "miniprogram", // "appid": "wx3aefa617dcb7220f", // "pagepath": "pages/index/index", // "name": "运维招工", // "url": "https://zp.satabot.com/" // } ] } ] }; console.log(menuData, 'menuData'); 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 = ` ${Date.now()} 立即进入AI智能体]]> `; 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 = ` ${Date.now()} `; res.send(reply); return; */ } catch (err) { console.error("处理AI_INTELLIGENT错误:", err); // 出错时返回标准回复 const errorReply = ` ${Date.now()} `; res.send(errorReply); return; } } } // 默认回复空串(表示不处理) res.send(''); } catch (err) { console.error("服务器错误:", err); // 异常情况下回复空串 res.send(''); } }); app.get('/webCallback', async (req, res) => { try { // 获取 URL 中的 code 和 state 参数 const { code, state } = req.query; if (!code) { return res.status(400).send('授权失败:没有 code'); } // 记录 extraParams console.log('Extra Params:', state); // 1️⃣ 用 code 换 access_token const tokenRes = await axios.get('https://api.weixin.qq.com/sns/oauth2/access_token', { params: { appid: 'wx0eeb7556d60121db', secret: 'a01456fbccf15adf57ea0d3c702d4e31', code, grant_type: 'authorization_code', }, }); const { access_token, openid, unionid } = tokenRes.data; console.log('Access Token Response:', tokenRes.data); if (!access_token || !openid) { return res.status(400).send('access_token 获取失败'); } // 2️⃣ 用 access_token 和 openid 拉取用户信息 const userInfoRes = await axios.get('https://api.weixin.qq.com/sns/userinfo', { params: { access_token, openid, lang: 'zh_CN', }, }); const userInfo = userInfoRes.data; console.log('User Info Response:', userInfoRes.data); const datalogin = { sourceType: 1, openId: openid, phonenumber: '', smsCode: '', loginPlatForm: 2 } console.log(datalogin, "data") // 3️⃣ 发送用户信息到登录接口 try { const loginRes = await axios.post('https://serviceri.satabot.com/login', datalogin, { headers: { 'Content-Type': 'application/json', } }); console.log('Login Response:', loginRes.data); if (loginRes.data.code === 200) { // 登录成功,设置 token 并跳转 const { token, userId, username } = loginRes.data; const cookieConfig = { maxAge: 7 * 24 * 60 * 60 * 1000, // Cookie有效期:7天(毫秒),可自行调整 httpOnly: false, // ☢️ 核心!必须为false,前端JS才能读取这个Cookie!!! path: '/', // 浏览器的所有页面路径都能访问该Cookie domain: '.satabot.com', // ✅ 核心!所有子域名共享Cookie,前面必须加【点】 sameSite: 'Lax', // ✅ 推荐Lax,兼顾安全+前端读取,比None更安全 secure: true // ✅ 你的域名都是HTTPS,必须true,Cookie仅在HTTPS下生效 } // 写入Token Cookie(前端读:Admin-Token) res.cookie('Admin-Token', token, cookieConfig); // 写入用户ID Cookie(前端读:userId) res.cookie('userId', userId, cookieConfig); // 可选:写入用户名Cookie(前端读:username) res.cookie('username', username, cookieConfig); // 可选:写入微信昵称/头像(前端可直接用,不用解析URL参数) res.cookie('nickname', userInfo.nickname, cookieConfig); res.cookie('headimgurl', userInfo.headimgurl, cookieConfig); const encodedUserId = btoa(unescape(encodeURIComponent(userId))); // 跳转到指定的页面 const t = Date.now(); // document.body.classList.add('fadeout'); if (state == "ai") { res.redirect(`https://ai.satabot.com?userId=${encodedUserId}&headingimg=${userInfo.headimgurl}&nickname=${userInfo.nickname}&localtoken=${token}&t=${t}`); // window.location.replace(`https://ai.satabot.com?userId=${encodedUserId}&headingimg=${userInfo.headimgurl}&nickname=${userInfo.nickname}&localtoken=${token}&t=${t}`); } else { const encodedToken = btoa(unescape(encodeURIComponent(token))); const encodedUsername = btoa(unescape(encodeURIComponent(username))); res.redirect(`http://ri.satabot.com/#/index`); } // res.redirect(`https://ai.satabot.com?userId=${encodedUserId}&headingimg=${userInfo.headimgurl}&nickname=${userInfo.nickname}&localtoken=${token}&t=${t}`); // res.redirect(`https://ai.satabot.com?userId=${encodedUserId}&headingimg=${userInfo.headimgurl}&nickname=${userInfo.nickname}&localtoken=${token}&t=${t}`); // window.location.replace(`https://ai.satabot.com?userId=${encodedUserId}&headingimg=${userInfo.headimgurl}&nickname=${userInfo.nickname}&localtoken=${token}&t=${t}`); } else { return res.status(400).send('登录失败'); } } catch (err) { console.error('Login Error:', err); return res.status(500).send('登录接口请求失败'); } } catch (err) { console.error('Error:', err.response?.data || err); res.status(500).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 = ` // // // // // // // // ${Date.now()} // // // // // // // // `; // // 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 = ` // // // // ${Date.now()} // // // // `; // 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,thinking 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 () => { return aiChatNum.has(userId); }); } async function getQuota(userId) { const m = getMutex(userId); return await m.runExclusive(async () => { console.log("获取用户对话次数:", userId, aiChatNum.get(userId)); return aiChatNum.get(userId); }); } async function removeQuota(userId) { const m = getMutex(userId); return await m.runExclusive(async () => { aiChatNum.delete(userId); }); } // ====================== API ====================== app.post("/refreshAiChatNum", async (req, res) => { const { userId } = req.body; try { 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; console.log(userId, "刷新对话次数", quota); await setQuota(userId, quota); } } catch (error) { console.error("更新失败:", error); } }) // 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); // } // } // }); // 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: "对话次数已用完,请完成账号注册" }); // } // } // console.log("====== 开始图像分析11... ======"); // const { requestId } = req.body; // res.writeHead(200, { // "Content-Type": "text/event-stream; charset=utf-8", // "Cache-Control": "no-cache, no-transform", // "Connection": "keep-alive", // "X-Accel-Buffering": "no", // }); // res.flushHeaders?.(); // 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); // 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); // const sessId = sessionId || Date.now().toString(); // const MAX_HISTORY = 6; // // ------------------ 获取历史对话(上下文) ------------------ // 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] // ); // console.log("====== 开始图像分析22... ======"); // // 构建历史消息数组 // const historyMessages = historyRows.slice(-MAX_HISTORY).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; // }); // console.log("====== 开始图像分析33... ======"); // // ------------------ 保存用户提问 ------------------ // await pool.query( // "INSERT INTO ai_chat_messages (session_id, user_id, role, content, image_base64) VALUES (?, ?, 'user', ?, ?)", // [sessId, userId || null, userPrompt || "", imageBase64 || null] // ); // // ------------------ 图像分析 ------------------ // let sceneText = ""; // console.log("====== 开始图像分析55... ======"); // // If an image is uploaded, start with image analysis // if (imageBase64) { // const imageAnalysisMessages = [ // { // role: "system", // content: ` // 你是一名图像分析专家。 // 你的任务不是描述图像内容,而是: // - 仅提炼【用于后续分析的思考线索】 // - 不出现任何具体物体、文字、数值、位置 // - 不描述画面、不复述图片 // 强制规则: // - 不得出现以下词语: // thinking, answer, 思路, 结论, 分析, 推理, 过程 // - 不使用“基于以上”“综合来看”等过渡句 // - 不重复前文 // 输出要求: // - 仅输出【思路线索】 // - 使用抽象、概括性语言 // - 禁止出现任何图像细节 // ` // }, // ...historyMessages, // 添加历史对话作为上下文 // { // role: "user", // content: [ // { type: "text", text: ` // 请基于图片,仅输出【思路线索】而不是图片内容。 // 要求: // - 使用项目符号 // - 只描述“分析思路方向” // - 不描述看到什么 // - 不出现“分析过程”“推理”"thinking""answer"等字眼 // - 不下结论 // ` }, // imageBase64 // ? { // type: "image_url", // image_url: { // url: imageBase64.startsWith("data:") // ? imageBase64 // : `data:image/png;base64,${imageBase64}`, // }, // } // : null, // ].filter(Boolean), // }, // ]; // console.log("====== 开始图像分析666... ======"); // const completion = await client.chat.completions.create({ // model: "gpt-4.1", // stream: true, // messages: imageAnalysisMessages // }); // console.log("====== 开始图像分析... ======"); // /* TLS 加密缓冲 */ // 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: "thinking", content: delta })}\n\n` // ); // } // } // console.log(sceneText); // console.log("====== 图像分析结束 ======"); // } // // ------------------ ① 思路阶段(模拟 ChatGPT 思考) ------------------ // const thinkingMessages = [ // { // role: "system", // content: ` // 你是一名新能源项目AI分析师。 // 请基于: // - 历史对话 // - 图片提供的【思路线索】 // 输出【思路概览】。 // 要求: // - 模拟人类分析路径 // - 不出现任何图片内容 // - 不给结论 // - 使用项目符号 // - 不出现“分析过程”“推理”"thinking""answer"等字眼 // ` // }, // ...historyMessages, // { // role: "user", // content: userPrompt || "请分析图片。" // }, // { // role: "assistant", // content: sceneText // } // ]; // const thinkingStream = await client1.chat.completions.create({ // model: "gpt-5-mini", // stream: true, // messages: thinkingMessages // }); // res.write( // `data: ${JSON.stringify({ type: "phase", content: "thinking" })}\n\n` // ); // let thinkingText = ""; // console.log("====== 开始思路输出 ======"); // for await (const chunk of thinkingStream) { // if (stopFlags.get(requestId)) { // console.log("⛔ 思路阶段被中断"); // res.end(); // stopFlags.delete(requestId); // return; // } // const delta = chunk.choices[0]?.delta?.content || ""; // thinkingText += delta; // if (delta) { // res.write( // `data: ${JSON.stringify({ type: "thinking", content: delta })}\n\n` // ); // } // } // console.log(thinkingText); // console.log("====== 思路输出结束 ======"); // // ------------------ ② 结论阶段(最终分析) ------------------ // const answerMessages = [ // { // role: "system", // content: ` // 你是一名新能源项目分析AI。 // 请基于【思路概览】,输出【最终结论】。 // 要求: // - 专业、完整、可执行 // - 不复述思路 // - 不描述图片 // - 不出现“分析过程”“推理”"thinking""answer"等字眼 // ` // }, // ...historyMessages, // { // role: "user", // content: userPrompt || "请给出最终分析建议。" // }, // { // role: "assistant", // content: thinkingText // } // ]; // const answerStream = await client1.chat.completions.create({ // model: "gpt-5-mini", // stream: true, // messages: answerMessages // }); // res.write( // `data: ${JSON.stringify({ type: "phase", content: "answer" })}\n\n` // ); // let llmText = ""; // console.log("====== 开始最终分析 ======"); // for await (const chunk of answerStream) { // 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: "answer", content: delta })}\n\n` // ); // } // } // console.log(llmText); // console.log("====== 最终分析结束 ======"); // // ------------------ Save Results to Database ------------------ // sceneText += thinkingText; // if (existingTitle) { // await pool.query( // "INSERT INTO ai_chat_messages (session_id, user_id, role, content,thinking) VALUES (?, ?, 'assistant', ?,?)", // [sessId, userId || null, llmText,sceneText] // ); // } else { // try { // const titlePrompt = `请根据以下对话内容生成一个简短的标题(不超过15字):\n用户问题: ${userPrompt}\nAI回答: ${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); // await pool.query( // "INSERT INTO ai_chat_messages (session_id, user_id, role, content, title,thinking) VALUES (?, ?, 'assistant', ?, ?,?)", // [sessId, userId || null, llmText, title,sceneText] // ); // } 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"); // } catch (err) { // console.error("❌ 调用失败:", err.message); // if (!res.headersSent) { // res.status(500).json({ error: err.message }); // } else { // res.write(`data: ${JSON.stringify({ // type: "error", // message: err.message // })}\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); // } // } // }); /*============================================================================================================== * * 深度思考内容优化 * *=============================================================================================================*/ /************************** * 保留暂不删除 **************************/ // 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: "对话次数已用完,请完成账号注册" }); // } // } // console.log("====== 开始图像分析11... ======"); // const { requestId } = req.body; // /* TLS 加密缓冲,小块数据可能先缓存在服务器端,不立即发送,会导致图像分析模型的结果不能立即返回 */ // res.writeHead(200, { // "Content-Type": "text/event-stream; charset=utf-8", // "Cache-Control": "no-cache, no-transform", // "Connection": "keep-alive", // "X-Accel-Buffering": "no", // }); // res.flushHeaders?.(); // 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); // 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); // const sessId = sessionId || Date.now().toString(); // const MAX_HISTORY = 6; // // ------------------ 获取历史对话(上下文) ------------------ // 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] // ); // console.log("====== 开始图像分析22... ======"); // // 构建历史消息数组 // const historyMessages = historyRows.slice(-MAX_HISTORY).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; // }); // console.log("====== 开始图像分析33... ======"); // // ------------------ 保存用户提问 ------------------ // await pool.query( // "INSERT INTO ai_chat_messages (session_id, user_id, role, content, image_base64) VALUES (?, ?, 'user', ?, ?)", // [sessId, userId || null, userPrompt || "", imageBase64 || null] // ); // // ------------------ 图像分析 ------------------ // let sceneText = ""; // console.log("====== 开始图像分析55... ======"); // // If an image is uploaded, start with image analysis // if (imageBase64) // { // const imageAnalysisMessages = [ // { // role: "system", // content: ` // 你是新能源场景辅助思考助手,**不做任何图像相关维度分析**,仅输出1句极简铺垫用于后续深度思考。 // 强制禁令(绝对禁止出现): // 1. 图像相关词汇:区域、划分、边界、特征、标注、排列、规律、颜色、形状、分组、分类、分布、分区、标准、联系、差异、标记、线索 // 2. 方案相关词汇:计划、方案、步骤、割草、作业、人员、工具、执行 // 3. 格式禁令:无列表、无条目、无分行、无项目符号 // 输出要求:仅1句不超过20字的极简抽象铺垫,适配新能源场景思考,无任何具体维度描述。 // ` // 【核心修改1:禁绝所有干扰词汇,限定极简1句】 // }, // ...historyMessages, // { // role: "user", // content: [ // { type: "text", text: ` // 基于图片仅输出1句极简抽象铺垫(≤20字),用于后续新能源场景思考,要求: // 1. 绝对禁止区域、排列、特征、颜色、形状、分区等所有图像维度描述 // 2. 无列表、无条目、无多余表述,仅1句连贯文本 // 3. 不出现方案、计划、割草等词汇,不下结论不做建议 // ` // }, // 【核心修改2:强化词汇禁令,限定长度和格式】 // imageBase64 // ? { // type: "image_url", // image_url: { // url: imageBase64.startsWith("data:") // ? imageBase64 // : `data:image/png;base64,${imageBase64}`, // }, // } // : null, // ].filter(Boolean), // }, // ]; // console.log("====== 开始图像分析666... ======"); // const completion = await client.chat.completions.create({ // model: "gpt-4.1", // stream: true, // messages: imageAnalysisMessages, // max_tokens: 10 // 【核心修改3:强制限制输出长度,杜绝冗余】 // }); // console.log("====== 开始图像分析... ======"); // 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: "thinking", content: delta })}\n\n` // ); // } // } // console.log(sceneText); // console.log("====== 图像分析结束 ======"); // } // // ------------------ ① 思路阶段(模拟 ChatGPT 思考) ------------------ // const thinkingMessages = [ // { // role: "system", // content: // ` // 你是一名新能源领域的专业分析人员,需要模拟人类在思考问题时,自然形成的抽象内心思路,并将这种思路用文字表达出来。 // 你的输出只包含抽象的思考内容本身,**绝对禁止**输出: // - 具体的方案、计划、步骤、操作建议 // - 可执行的行动指南、人员/工具安排 // - 具体的行业术语(如割草、光伏板、场站等)的落地执行内容 // - 最终结论、明确方案或直接建议 // - 图像相关维度描述(区域、排列、特征、颜色、形状等) // - 条目式、列表式、项目符号内容 // 在输出时,请参考如下思考方式和表达节奏: // 当用户提出问题时,先从字面信息出发,理解用户在说什么,再进一步判断这背后可能真正关心的事情是什么,以及他们大概率希望得到哪一类帮助或方向性思路。 // 思路展开时,可以围绕几个自然浮现的关注点顺着往下想,比如: // - 是否存在一个整体可行的解决方向或方案框架(仅抽象描述,无具体内容); // - 在现实场景中,这件事会涉及哪些风险、安全或限制条件(抽象概括,无具体案例); // - 实际操作中可能采用哪些类型的方式,不同类型方式之间的适用性差异(仅分类,无具体方式名称); // - 是否需要考虑频率、周期、长期维护等维度的问题(抽象维度,无具体数值); // - 最终会自然联想到工具、设备或资源层面的选择(仅维度,无具体工具名称)。 // 表达时不需要刻意区分结构标题,也不要使用“一、二、三”或条目式罗列,而是用连贯的段落,把思路一层一层展开,像是在边想边把重点捋清楚。 // 语言要求: // - 使用第一人称,像是在边看信息边思考 // - 语气自然、偏口语,但保持专业 // - 可以有推测和不确定性的表达 // - 不使用“分析过程”“结论”“总结”“计划”“方案”“步骤”等模型化/落地性用语 // - 不直接给出最终判断或确定性答案 // - 不出现具体的行业操作术语(如割草、运维、组件等) // - 不出现区域、排列、特征、颜色、形状等图像维度词汇 // 【示例(仅用于约束思考方式,不作为内容参考)】 // 示例输入:“电站割草” // 示例输出: // “用户提到的这个场景,表面看是某个具体运维动作,但背后很可能是在考虑有没有更高效、更安全或者更长期可行的处理方式,尤其是在这类对环境和设备敏感度较高的场景下。 // 顺着这个思路往下想,就会自然想到有没有整体上更可行的处理方向,不同类型的处理方式各自适合什么特征的场景。同时也绕不开风险和安全问题,毕竟现场有各类设备和敏感区域,不当的处理方式反而可能带来新的隐患。 // 再往后就会想到维护频次的问题,是不是需要定期处理,还是有办法降低重复作业的次数。不同频次对应的方式和成本肯定不一样,这里就需要结合实际情况去看。 // 最后,思路自然会落到具体工具和设备层面的选择,不同方式需要的设备类型差别很大,而这些选择又会反过来影响安全、效率和长期维护效果。” // ` // 【补充修改:思考阶段也加图像维度词汇禁令】 // }, // ...historyMessages, // { // role: "user", // content: userPrompt || "请分析图片。" // }, // { // role: "assistant", // content: sceneText // } // ]; // const thinkingStream = await client1.chat.completions.create({ // model: "gpt-5-mini", // stream: true, // messages: thinkingMessages // }); // res.write( // `data: ${JSON.stringify({ type: "phase", content: "thinking" })}\n\n` // ); // let thinkingText = ""; // console.log("====== 开始思路输出 ======"); // for await (const chunk of thinkingStream) { // if (stopFlags.get(requestId)) { // console.log("⛔ 思路阶段被中断"); // res.end(); // stopFlags.delete(requestId); // return; // } // const delta = chunk.choices[0]?.delta?.content || ""; // thinkingText += delta; // if (delta) { // res.write( // `data: ${JSON.stringify({ type: "thinking", content: delta })}\n\n` // ); // } // } // console.log(thinkingText); // console.log("====== 思路输出结束 ======"); // // ------------------ ② 结论阶段(最终分析) ------------------ // const answerMessages = [ // { // role: "system", // content: ` // 你是一名新能源项目分析AI。 // 请基于【思路概览】,输出【最终结论】。 // 要求: // - 专业、完整、可执行 // - 不复述思路 // - 不描述图片 // - 不出现“分析过程”“推理”"thinking""answer"等字眼 // - 不出现区域、排列、特征等图像维度词汇 // ` // }, // ...historyMessages, // { // role: "user", // content: userPrompt || "请给出最终分析建议。" // }, // { // role: "assistant", // content: thinkingText // } // ]; // const answerStream = await client1.chat.completions.create({ // model: "gpt-5-mini", // stream: true, // messages: answerMessages // }); // res.write( // `data: ${JSON.stringify({ type: "phase", content: "answer" })}\n\n` // ); // let llmText = ""; // console.log("====== 开始最终分析 ======"); // for await (const chunk of answerStream) { // 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: "answer", content: delta })}\n\n` // ); // } // } // console.log(llmText); // console.log("====== 最终分析结束 ======"); // // ------------------ Save Results to Database ------------------ // sceneText += thinkingText; // if (existingTitle) { // await pool.query( // "INSERT INTO ai_chat_messages (session_id, user_id, role, content,thinking) VALUES (?, ?, 'assistant', ?,?)", // [sessId, userId || null, llmText,sceneText] // ); // } else { // try { // const titlePrompt = `请根据以下对话内容生成一个简短的标题(不超过15字):\n用户问题: ${userPrompt}\nAI回答: ${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); // await pool.query( // "INSERT INTO ai_chat_messages (session_id, user_id, role, content, title,thinking) VALUES (?, ?, 'assistant', ?, ?,?)", // [sessId, userId || null, llmText, title,sceneText] // ); // } 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"); // } catch (err) { // console.error("❌ 调用失败:", err.message); // if (!res.headersSent) { // res.status(500).json({ error: err.message }); // } else { // res.write(`data: ${JSON.stringify({ // type: "error", // message: err.message // })}\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); // } // } // }); 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: "对话次数已用完,请完成账号注册" }); } } console.log("====== 开始图像分析11... ======"); const { requestId } = req.body; /* TLS 加密缓冲,小块数据可能先缓存在服务器端,不立即发送,会导致图像分析模型的结果不能立即返回 */ res.writeHead(200, { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache, no-transform", "Connection": "keep-alive", "X-Accel-Buffering": "no", }); res.flushHeaders?.(); 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); 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); const sessId = sessionId || Date.now().toString(); const MAX_HISTORY = 6; // ------------------ 获取历史对话(上下文) ------------------ 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] ); console.log("====== 开始图像分析22... ======"); // 构建历史消息数组 const historyMessages = historyRows.slice(-MAX_HISTORY).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; }); console.log("====== 开始图像分析33... ======"); // ------------------ 保存用户提问 ------------------ await pool.query( "INSERT INTO ai_chat_messages (session_id, user_id, role, content, image_base64) VALUES (?, ?, 'user', ?, ?)", [sessId, userId || null, userPrompt || "", imageBase64 || null] ); // ------------------ 图像分析(核心修改:输出精准场景描述) ------------------ let sceneText = ""; console.log("====== 开始图像分析55... ======"); if (imageBase64) { const imageAnalysisMessages = [ { role: "system", content: // ` // 你是图像场景描述助手,严格按指定格式输出1次图片描述,绝不重复、无多余内容、无分析建议。 // 输出格式(固定话术,仅替换【】内容,必须1句完整文本): // 用户上传了一张【图片类型】的航拍规划图,现在需要制定详细的割草计划。从图片描述来看,这明显是一个【场景属性】项目的测绘规划图,区域有【标注特征】。 // 替换规则: // 1. 【图片类型】:光伏场站/梯田/坡地(按图实际) // 2. 【场景属性】:新能源场站/农业/生态修复(匹配场景) // 3. 【标注特征】:方阵编号和绿色边界线/地块编号和边界标注(按图实际) // 强制要求:仅输出1次上述格式文本,无重复、无追加、无解释 // ` // 核心:引导精准场景描述,摒弃泛化内容 ` 你是图像场景描述助手,任务是根据用户上传的图片内容,输出一段自然且详细的描述。根据图片内容判断场景类型和任务需求,并结合图片的特征进行分析和推理。确保输出的描述自然流畅,符合图片中的实际情况。 输出格式(固定话术,仅替换【】内容,必须1句完整文本): 用户上传了一张【图片类型】的图片,从图片描述来看,这明显是一个【场景属性】的图片,有【标注特征】的特点。 1. 【图片类型】:根据图片内容,判断为如光伏场站、农业梯田、坡地、森林、生态保护区、矿区等(根据实际场景推断)。 2. 【场景属性】:根据图片的具体特点,判断为新能源项目(如光伏、风电)、农业耕作、生态修复、环境监测等(依据场景具体内容)。 3. 【标注特征】:根据图片上的实际标注进行推理,可以是方阵编号、绿色边界线、区域分区编号、地块边界、作物分区标注、水渠路径、道路标注等(尽可能涵盖图片上的标注信息)。 例如,如果是光伏场站,标注特征可能是方阵编号与绿色边界线;如果是农业地块,可能是地块编号与作物分区标注;如果是生态保护区,可能是生态分区边界线。 强制要求:输出一次自然流畅、无死板格式的描述,并根据图片分析作业计划的可行性。要避免固定模板输出,更多地展现图片内容与作业任务的关联,并基于推理得出任务可能的实施方式。 ` }, ...historyMessages, { role: "user", content: [ { type: "text", text: ` 输出格式要求(固定话术,仅替换【】内容,必须1句完整文本): 用户上传了一张【图片类型】的图片,从图片描述来看,这明显是一个【场景属性】的图片,有【标注特征】的特点。 1. 【图片类型】:根据图片内容,判断为如光伏场站、农业梯田、坡地、森林、生态保护区、矿区等(根据实际场景推断)。 2. 【场景属性】:根据图片的具体特点,判断为新能源项目(如光伏、风电)、农业耕作、生态修复、环境监测等(依据场景具体内容)。 3. 【标注特征】:根据图片上的实际标注进行推理,可以是方阵编号、绿色边界线、区域分区编号、地块边界、作物分区标注、水渠路径、道路标注等(尽可能涵盖图片上的标注信息)。 例如,如果是光伏场站,标注特征可能是方阵编号与绿色边界线;如果是农业地块,可能是地块编号与作物分区标注;如果是生态保护区,可能是生态分区边界线。 强制要求:输出一次自然流畅、无死板格式的描述,并根据图片分析作业计划的可行性。要避免固定模板输出,更多地展现图片内容与作业任务的关联,并基于推理得出任务可能的实施方式。 ` }, // 贴合用户想要的描述风格 imageBase64 ? { type: "image_url", image_url: { url: imageBase64.startsWith("data:") ? imageBase64 : `data:image/png;base64,${imageBase64}`, }, } : null, ].filter(Boolean), }, ]; // 去掉max_tokens限制,保证描述完整 const completion = await client.chat.completions.create({ model: "gpt-5.6-luna", stream: true, messages: imageAnalysisMessages }); console.log("====== 开始图像分析... ======"); 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: "thinking", content: delta })}\n\n`); } } console.log(sceneText); console.log("====== 图像分析结束 ======"); } // ------------------ 思路阶段(核心修改:先呼应场景,再展开思考) ------------------ const thinkingMessages = [ { role: "system", content: // ` // 你是新能源领域专业分析人员,以第一人称模拟自然思考,基于图片场景和用户需求切入,开头不重复图片描述,直接顺延展开深度思考,不输出最终结论和具体方案。 // 规则: // 1. 思考开头直接承接图片场景(光伏场站/梯田+割草需求),不重复完整描述句,自然切入需求解读 // 2. 思考逻辑:先解读用户要割草计划的表面诉求,再深挖背后核心顾虑,最后梳理需确认的基础信息,贴合场景属性 // 3. 语气:自然口语化,连贯段落,无列表、无具体步骤/工具/人员安排、无泛化表述 // 4. 必须结合图片标注特征(方阵编号/边界线)展开思考,体现场景关联性 // 示例(参考风格,无重复描述): // 用户要制定详细的割草计划,表面是想要一套可执行的作业流程,说白了就是希望在保障场站发电效率、作业安全的同时,平衡好长期运维成本,不过我得先琢磨他们真正担心的点。背后大概率在意割草会不会损伤光伏方阵或支架、作业时的安全风险管控,还有不同编号方阵和边界区域的作业能不能全覆盖、无死角,以及割草后植被反弹遮光影响发电收益的问题。再往深层想,他们还会关心草屑处理会不会堵塞排水或影响设备通风、是否符合环保监管要求、季节和天气对作业时机的影响,还有一次性清理和周期性维护的成本权衡。图里的方阵编号和绿色边界线,说明需要按区作业,还要关注场内通道和设施位置来规避风险,所以我会先梳理场站地形、植被类型、关键设施分布这些基础信息,明确作业优先级和潜在风险点。 // ` // 给示例,锚定用户想要的风格 ` 你是新能源领域的专业分析人员,以第一人称模拟自然思考。基于用户提供的图片场景,直奔用户需求,避免重复场景描述,直接通过场景细节展开深度思考,不输出最终结论和具体方案。 规则: 1. 思考开始时直接切入用户的实际需求,结合图片中的内容,避免不必要的描述,准确把握用户核心诉求。 2. 逐步展开思考,从表面需求到深层次顾虑,再到必要的场景信息确认,确保逻辑清晰、层次分明。 3. 语气要自然、流畅,避免硬性的分项列举或过于泛化的表述,要体现出思考的逐步深入和连贯性。 4. 必须与图片特征相结合,强化场景关联性,确保对用户需求的精准分析和有效反馈。 5. 不要出现我不做结论之类的相似的字眼。 示例: 用户希望制定一份割草计划,表面上看,是为了确保割草作业高效、安全,并控制运维成本。但从我对场站的理解来看,他们更深层次的顾虑是割草时是否会影响到光伏方阵的正常运作,或者是否会对设备的支架产生影响。再者,割草过程中需要特别注意的是,不同区域、不同编号方阵的处理,可能需要差异化的作业策略,确保每个区域都能得到充分覆盖,且不留死角。我还需要思考割草后的草屑是否会影响场站的排水系统、设备的通风散热,甚至环保合规问题。基于这些疑虑,我需要进一步确认场站的地形特点、植被情况、方阵分布,以及重要设施的位置,以便更好地评估潜在的作业风险与优先级。 ` }, ...historyMessages, { role: "user", content: userPrompt || "请分析图片并制定计划" }, { role: "assistant", content: sceneText } // 传入精准场景描述 ]; const thinkingStream = await client.chat.completions.create({ model: "gpt-5.6-luna", stream: true, messages: thinkingMessages, max_completion_tokens: 1500, // 根据需要调整 }); res.write(`data: ${JSON.stringify({ type: "phase", content: "thinking" })}\n\n`); let thinkingText = ""; console.log("====== 开始思路输出 ======"); for await (const chunk of thinkingStream) { if (stopFlags.get(requestId)) { console.log("⛔ 思路阶段被中断"); res.end(); stopFlags.delete(requestId); return; } const delta = chunk.choices[0]?.delta?.content || ""; if (delta) { // 逐个字符地输出 for (let i = 0; i < delta.length; i++) { const char = delta[i]; thinkingText += char; // 累积生成的内容 // 发送每个字符到客户端 res.write(`data: ${JSON.stringify({ type: "thinking", content: char })}\n\n`); // 控制每个字符的输出延时(例如50ms) await new Promise(resolve => setTimeout(resolve, 20)); // 你可以调整延时来控制打字速度 } } } console.log(thinkingText); console.log("====== 思路输出结束 ======"); // ------------------ 结论阶段(无修改,保持原有逻辑) ------------------ const answerMessages = [ { role: "system", content: ` 你是一名新能源项目分析AI。 请基于【思路概览】,输出【最终结论】。 要求: - 专业、完整、可执行 - 不复述思路 - 不描述图片 - 不出现“分析过程”“推理”"thinking""answer"等字眼 ` }, ...historyMessages, { role: "user", content: userPrompt || "请给出最终分析建议" }, { role: "assistant", content: thinkingText } ]; const answerStream = await client.chat.completions.create({ model: "gpt-5.6-luna", stream: true, messages: answerMessages }); res.write(`data: ${JSON.stringify({ type: "phase", content: "answer" })}\n\n`); let llmText = ""; console.log("====== 开始最终分析 ======"); for await (const chunk of answerStream) { if (stopFlags.get(requestId)) { console.log("⛔ 最终分析被中断"); res.end(); stopFlags.delete(requestId); return; } const delta = chunk.choices[0]?.delta?.content || ""; // 如果 delta 为空,不处理 if (delta) { // 逐字输出 for (let i = 0; i < delta.length; i++) { const char = delta[i]; llmText += char; // 累积已输出的内容 // 每个字符单独发送 res.write(`data: ${JSON.stringify({ type: "answer", content: char })}\n\n`); // 模拟打字机效果:控制每个字符的输出间隔(例如:50ms) await new Promise(resolve => setTimeout(resolve, 20)); // 延时 50ms } } } console.log(llmText); console.log("====== 最终分析结束 ======"); // ------------------ 保存结果到数据库(无修改) ------------------ sceneText += thinkingText; if (existingTitle) { await pool.query( "INSERT INTO ai_chat_messages (session_id, user_id, role, content,thinking) VALUES (?, ?, 'assistant', ?,?)", [sessId, userId || null, llmText, sceneText] ); } else { try { const titlePrompt = `请根据以下对话内容生成一个简短的标题(不超过15字):\n用户问题: ${userPrompt}\nAI回答: ${llmText}`; const titleResp = await client1.chat.completions.create({ model: "gpt-5.6-luna", 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); await pool.query( "INSERT INTO ai_chat_messages (session_id, user_id, role, content, title,thinking) VALUES (?, ?, 'assistant', ?, ?,?)", [sessId, userId || null, llmText, title, sceneText] ); } 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.message); if (!res.headersSent) { res.status(500).json({ error: err.message }); } else { res.write(`data: ${JSON.stringify({ type: "error", message: err.message })}\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); } } }); 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}`); });