2652 lines
94 KiB
JavaScript
Executable File
2652 lines
94 KiB
JavaScript
Executable File
// 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: "DxvRiQWROzLTapiSHcK0FXKAAXDBlhci1XaqTTpr50sgUsAkmxfUJQQJ99BIACYeBjFXJ3w3AAABACOGZdHp",
|
||
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: "DxvRiQWROzLTapiSHcK0FXKAAXDBlhci1XaqTTpr50sgUsAkmxfUJQQJ99BIACYeBjFXJ3w3AAABACOGZdHp",
|
||
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 = `
|
||
<xml>
|
||
<ToUserName><![CDATA[${fromUser}]]></ToUserName>
|
||
<FromUserName><![CDATA[${toUser}]]></FromUserName>
|
||
<CreateTime>${Date.now()}</CreateTime>
|
||
<MsgType><![CDATA[text]]></MsgType>
|
||
<Content><![CDATA[欢迎使用AI智能体!正在为您准备服务...<a href="https://ai.satabot.com?userId=${fromUser}&from=weixin&ts=${timestamp}" target="_blank">立即进入AI智能体</a>]]></Content>
|
||
</xml>
|
||
`;
|
||
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 = `
|
||
<xml>
|
||
<ToUserName><![CDATA[${fromUser}]]></ToUserName>
|
||
<FromUserName><![CDATA[${toUser}]]></FromUserName>
|
||
<CreateTime>${Date.now()}</CreateTime>
|
||
<MsgType><![CDATA[text]]></MsgType>
|
||
<Content><![CDATA[你好,${user.nickname || '用户'}!欢迎使用AI智能体]]></Content>
|
||
</xml>
|
||
`;
|
||
res.send(reply);
|
||
return;
|
||
*/
|
||
} catch (err) {
|
||
console.error("处理AI_INTELLIGENT错误:", err);
|
||
// 出错时返回标准回复
|
||
const errorReply = `
|
||
<xml>
|
||
<ToUserName><![CDATA[${fromUser}]]></ToUserName>
|
||
<FromUserName><![CDATA[${toUser}]]></FromUserName>
|
||
<CreateTime>${Date.now()}</CreateTime>
|
||
<MsgType><![CDATA[text]]></MsgType>
|
||
<Content><![CDATA[服务暂时不可用,请稍后再试]]></Content>
|
||
</xml>
|
||
`;
|
||
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 = `
|
||
// // <xml>
|
||
// // <ToUserName><![CDATA[${fromUser}]]></ToUserName>
|
||
// // <FromUserName><![CDATA[${message.ToUserName}]]></FromUserName>
|
||
// // <CreateTime>${Date.now()}</CreateTime>
|
||
// // <MsgType><![CDATA[text]]></MsgType>
|
||
// // <Content><![CDATA[请点击以下链接授权获取昵称:${oauthUrl}]]></Content>
|
||
// // </xml>
|
||
// // `;
|
||
// // 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 = `
|
||
// <xml>
|
||
// <ToUserName><![CDATA[${fromUser}]]></ToUserName>
|
||
// <FromUserName><![CDATA[${message.fromUser}]]></FromUserName>
|
||
// <CreateTime>${Date.now()}</CreateTime>
|
||
// <MsgType><![CDATA[text]]></MsgType>
|
||
// <Content><![CDATA[你好,${user.nickname}!欢迎使用 AI智能体]]></Content>
|
||
// </xml>
|
||
// `;
|
||
// 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次上述格式文本,无重复、无追加、无解释
|
||
` // 核心:引导精准场景描述,摒弃泛化内容
|
||
},
|
||
...historyMessages,
|
||
{
|
||
role: "user",
|
||
content: [
|
||
{ type: "text", text:
|
||
`
|
||
请严格按以下固定格式输出1句图片描述,不重复、不增删、不解释、无多余内容:
|
||
用户上传了一张【图片类型】的航拍规划图,现在需要制定详细的割草计划。从图片描述来看,这明显是一个【场景属性】项目的测绘规划图,区域有【标注特征】。
|
||
替换要求:
|
||
1. 【图片类型】填光伏场站/梯田/坡地;【场景属性】填新能源场站/农业;【标注特征】填方阵编号和绿色边界线
|
||
2. 仅输出1次完整句子,杜绝任何重复表述
|
||
`
|
||
}, // 贴合用户想要的描述风格
|
||
imageBase64 ? {
|
||
type: "image_url",
|
||
image_url: {
|
||
url: imageBase64.startsWith("data:") ? imageBase64 : `data:image/png;base64,${imageBase64}`,
|
||
},
|
||
} : null,
|
||
].filter(Boolean),
|
||
},
|
||
];
|
||
|
||
console.log("====== 开始图像分析666... ======");
|
||
// 去掉max_tokens限制,保证描述完整
|
||
const completion = await client.chat.completions.create({
|
||
model: "gpt-4.1",
|
||
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. 必须结合图片标注特征(方阵编号/边界线)展开思考,体现场景关联性
|
||
示例(参考风格,无重复描述):
|
||
用户要制定详细的割草计划,表面是想要一套可执行的作业流程,说白了就是希望在保障场站发电效率、作业安全的同时,平衡好长期运维成本,不过我得先琢磨他们真正担心的点。背后大概率在意割草会不会损伤光伏方阵或支架、作业时的安全风险管控,还有不同编号方阵和边界区域的作业能不能全覆盖、无死角,以及割草后植被反弹遮光影响发电收益的问题。再往深层想,他们还会关心草屑处理会不会堵塞排水或影响设备通风、是否符合环保监管要求、季节和天气对作业时机的影响,还有一次性清理和周期性维护的成本权衡。图里的方阵编号和绿色边界线,说明需要按区作业,还要关注场内通道和设施位置来规避风险,所以我会先梳理场站地形、植被类型、关键设施分布这些基础信息,明确作业优先级和潜在风险点。
|
||
` // 给示例,锚定用户想要的风格
|
||
},
|
||
...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("====== 最终分析结束 ======");
|
||
|
||
// ------------------ 保存结果到数据库(无修改) ------------------
|
||
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("/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.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}`);
|
||
});
|