Files
AI/server2.js
2026-10-15 10:07:42 +08:00

2675 lines
96 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 = `
<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: "https://ai.satabot.com/login.html",
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: "https://ai.satabot.com/login.html"
});
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}`);
});