| | |
| | | package com.smtaiserver.smtaiserver.control; |
| | | |
| | | import cn.hutool.http.HttpUtil; |
| | | import com.smtaiserver.smtaiserver.core.SMTAIServerApp; |
| | | import com.smtaiserver.smtaiserver.vo.MediaVo; |
| | | import com.smtaiserver.smtaiserver.vo.WechatMessageVO; |
| | | import com.smtaiserver.smtaiserver.core.SMTAIServerRequest; |
| | | import com.smtaiserver.smtaiserver.database.SMTDatabase; |
| | | import com.smtaiserver.smtaiserver.database.SMTDatabase.DBRecords; |
| | | import com.smtaiserver.smtaiserver.javaai.ast.ASTDBMap; |
| | | import com.smtaiserver.smtaiserver.javaai.llm.core.SMTLLMConnect; |
| | | import com.smtaiserver.smtaiserver.util.SMTWXSStatic.WeixinuUtil; |
| | | import com.smtservlet.core.SMTRequest; |
| | | import com.smtservlet.util.Json; |
| | | import com.smtservlet.util.SMTJsonWriter; |
| | | import com.smtservlet.util.SMTStatic; |
| | | |
| | | import okhttp3.OkHttpClient; |
| | | import org.apache.logging.log4j.LogManager; |
| | | import org.apache.logging.log4j.Logger; |
| | | import org.dom4j.Document; |
| | | import org.dom4j.DocumentException; |
| | | import org.dom4j.Element; |
| | | import org.dom4j.io.SAXReader; |
| | | import org.springframework.boot.configurationprocessor.json.JSONException; |
| | | import org.springframework.boot.configurationprocessor.json.JSONObject; |
| | | import org.springframework.http.HttpEntity; |
| | | import org.springframework.web.servlet.ModelAndView; |
| | | |
| | | import javax.servlet.ServletInputStream; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.xml.bind.JAXBContext; |
| | | import javax.xml.bind.JAXBException; |
| | | import javax.xml.bind.Marshaller; |
| | | import java.io.IOException; |
| | | import java.io.OutputStream; |
| | | import java.io.StringWriter; |
| | | import java.net.HttpURLConnection; |
| | | import java.net.URI; |
| | | import java.net.URL; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.security.MessageDigest; |
| | | import java.security.NoSuchAlgorithmException; |
| | | import java.util.HashMap; |
| | | import java.util.Iterator; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.sql.Timestamp; |
| | | import java.util.*; |
| | | import java.util.concurrent.CompletableFuture; |
| | | |
| | | import static com.smtaiserver.smtaiserver.util.SMTWXSStatic.WeixinuUtil.*; |
| | | import static java.util.Arrays.sort; |
| | | |
| | | public class SMTAIWeixinControl { |
| | | |
| | | private static final String FROM_USER_NAME = "FromUserName"; |
| | | private static final String TO_USER_NAME = "ToUserName"; |
| | | private static final String CONTENT = "Content"; |
| | | private final String accessToken = |
| | | "89_pBAY9bgHt-7gBy2N8k_s31PMV3KFY1xgy_yCLw8eMHLqpugmHv2RXUgwf-2HC6bq5XPzVloiZp4ccnHDUfumjEOGxeFwFqIp8-gLU3OrPneiKgkv4l0mxd_orTAHNPgAJANTV"; |
| | | |
| | | private static Logger _logger = LogManager.getLogger(SMTAIServerControl.class); |
| | | |
| | | private final Object _lockToken = new Object(); |
| | | private String _tokenValue = null; |
| | | private long _tokenTicket = 0; |
| | | private OkHttpClient _web; |
| | | |
| | | /** 微信验证 */ |
| | | public ModelAndView weChatNotify(SMTRequest tranReq) throws Exception { |
| | | public ModelAndView weChatNotify(SMTAIServerRequest tranReq) throws Exception { |
| | | String method = tranReq.getRequest().getMethod(); |
| | | if (method.equals("GET")) return getModelAndView(tranReq); |
| | | if (method.equals("GET")) return WeixinuUtil.getModelAndView(tranReq); |
| | | return reply(tranReq); |
| | | } |
| | | |
| | | /** 被动回复 */ |
| | | private ModelAndView reply(SMTRequest tranReq) throws Exception { |
| | | private ModelAndView reply(SMTAIServerRequest tranReq) { |
| | | long l = System.currentTimeMillis() / 1000; |
| | | String createTimeStr = String.valueOf(l); |
| | | HttpServletRequest request = tranReq.getRequest(); |
| | | Map<String, String> requestMap = getWechatReqMap(request); |
| | | String fromUserName = requestMap.get(FROM_USER_NAME); |
| | | String toUserName = requestMap.get(TO_USER_NAME); |
| | | if (requestMap.isEmpty()) { |
| | | return null; |
| | | } |
| | | |
| | | String xmltemp = |
| | | "<xml>\n" |
| | | + " <ToUserName><![CDATA[{{{toUser}}}]]></ToUserName>\n" |
| | | + " <FromUserName><![CDATA[{{{fromUser}}}]]></FromUserName>\n" |
| | | + " <CreateTime>{{{CreateTime}}}</CreateTime>\n" |
| | | + " <MsgType><![CDATA[text]]></MsgType>\n" |
| | | + " <Content><![CDATA[我正在思考哦~请稍等……]]></Content>\n" |
| | | + "</xml>"; |
| | | // 替换占位符 |
| | | String result = |
| | | xmltemp |
| | | .replace("{{{toUser}}}", fromUserName) |
| | | .replace("{{{fromUser}}}", toUserName) |
| | | .replace("{{{CreateTime}}}", createTimeStr); |
| | | String reqContent = requestMap.get(CONTENT); |
| | | WechatMessageVO wechatMessage = new WechatMessageVO(); |
| | | wechatMessage.setFromUserName(requestMap.get(TO_USER_NAME)); |
| | | wechatMessage.setToUserName(requestMap.get(FROM_USER_NAME)); |
| | | // 设置消息类型 |
| | | switch (reqContent) { |
| | | case "文字": |
| | | wechatMessage.setMsgType("text"); |
| | | break; |
| | | case "图片": |
| | | wechatMessage.setMsgType("image"); |
| | | break; |
| | | case "语音": |
| | | wechatMessage.setMsgType("voice"); |
| | | break; |
| | | case "视频": |
| | | wechatMessage.setMsgType("video"); |
| | | break; |
| | | case "音乐": |
| | | wechatMessage.setMsgType("music"); |
| | | break; |
| | | case "图文": |
| | | wechatMessage.setMsgType("news"); |
| | | break; |
| | | default: |
| | | wechatMessage.setMsgType("text"); |
| | | break; |
| | | } |
| | | |
| | | wechatMessage.setCreateTime(System.currentTimeMillis() / 1000); |
| | | MediaVo media = new MediaVo(); |
| | | // 设置消息内容 |
| | | switch (reqContent) { |
| | | case "文字": |
| | | wechatMessage.setContent("我正在思考哦~请稍等……"); |
| | | // 异步调用 aiReplyToTheUserASecondTime |
| | | CompletableFuture.runAsync( |
| | | () -> { |
| | | try { |
| | | aiReplyToTheUserASecondTime(tranReq, requestMap.get(FROM_USER_NAME)); |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } |
| | | }); |
| | | break; |
| | | case "图片": |
| | | media.setMediaId("SI7HPwMI5PL1QV_I9M5AFw6K-1ZVMyTGE0-a5jQM4czTmffKTQpHa6zlYDmvIAPX"); |
| | | wechatMessage.setImage(media); |
| | | break; |
| | | case "语音": |
| | | media.setMediaId("c6hRH_X2HGwrOa1MiTQAcg35D7M42Xa4VMhyzSFMk8MA0pWFhly19W4K3W5NaH4b"); |
| | | wechatMessage.setVoice(media); |
| | | break; |
| | | case "视频": |
| | | media.setMediaId("c6hRH_X2HGwrOa1MiTQAcmo7zQjGAIV7uSP1U1S-tsnR0VJXUS0y10Z5FkaueU5Y"); |
| | | media.setTitle("你好"); |
| | | media.setDescription("你好啊"); |
| | | wechatMessage.setVideo(media); |
| | | break; |
| | | case "音乐": |
| | | System.out.println("进入音乐分支"); |
| | | break; |
| | | case "图文": |
| | | System.out.println("进入图文分支"); |
| | | break; |
| | | default: |
| | | System.out.println("进入默认分支"); |
| | | wechatMessage.setContent("我不太认识你的输入哦"); |
| | | break; |
| | | } |
| | | |
| | | // 异步调用 aiReplyToTheUserASecondTime |
| | | CompletableFuture.runAsync( |
| | | () -> { |
| | | try { |
| | | String answer = callAIForAnswerQuestion(reqContent, tranReq); // Ai调用 返回结果 |
| | | aiReplyToTheUserASecondTime(answer, requestMap.get(FROM_USER_NAME)); |
| | | } catch (Exception e) { |
| | | _logger.error("aiReplyToTheUserASecondTime error", e); |
| | | } |
| | | }); |
| | | try { |
| | | // 将 WechatMessageVO 对象转换为 XML |
| | | JAXBContext jaxbContext = JAXBContext.newInstance(WechatMessageVO.class); |
| | | Marshaller marshaller = jaxbContext.createMarshaller(); |
| | | marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); |
| | | StringWriter sw = new StringWriter(); |
| | | marshaller.marshal(wechatMessage, sw); |
| | | _logger.info("微信消息返参:" + sw); |
| | | _logger.info("微信消息返参:" + result); |
| | | // 返回 XML 字符串 |
| | | return tranReq.returnText(sw.toString()); |
| | | } catch (JAXBException e) { |
| | | return tranReq.returnText(result); |
| | | } catch (Exception e) { |
| | | throw new RuntimeException(e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 二次回复 |
| | | * ai回复 |
| | | * |
| | | * @param tranReq |
| | | * @return |
| | | * @throws Exception |
| | | */ |
| | | public ModelAndView aiReplyToTheUserASecondTime(SMTRequest tranReq, String fromUserName) |
| | | private String callAIForAnswerQuestion(String question, SMTAIServerRequest tranReq) |
| | | throws Exception { |
| | | JSONObject jsonObject = new JSONObject(); |
| | | jsonObject.put("touser", "oKAHz7LQhyL2IkcMpl8B7K4OotQk"); |
| | | jsonObject.put("msgtype", "text"); |
| | | JSONObject jsonObject1 = new JSONObject(); |
| | | jsonObject1.put("content", "你好啊"); |
| | | jsonObject.put("text", jsonObject1); |
| | | _logger.info("jsonObject: {}", jsonObject); |
| | | // 构建上传 URL |
| | | String callFunc = |
| | | "query_water_fee:\n" |
| | | + " 功能:\n" |
| | | + " 查询用户用水量和水费信息\n" |
| | | + " 参数:\n" |
| | | + " question:用户问题\n" |
| | | + " user_name:用户名\n" |
| | | + " value_title:'用水量'或'水费'\n" |
| | | + " value_name:用水量:volume, 水费:amount\n" |
| | | + " start_time:查询起始日期,格式:年-月-日\n" |
| | | + " end_time:查询结束时间,格式:年-月-日\n"; |
| | | |
| | | String prompt = |
| | | ((String) SMTAIServerApp.getApp().getGlobalConfig("prompt.agent_tools")) |
| | | .replace("{{{AGENT_TOOL_DEFINE_LIST}}}", callFunc); |
| | | |
| | | SMTLLMConnect llm = SMTAIServerApp.getApp().allocLLMConnect(null); |
| | | String answer = llm.callWithMessage(new String[] {prompt}, question, tranReq); |
| | | tranReq.traceLLMDebug(answer); |
| | | Json ojsonASTList = SMTStatic.convLLMAnswerToJson(answer, true); |
| | | if (ojsonASTList != null && ojsonASTList.isArray()) { |
| | | List<Json> jsonASTList = ojsonASTList.asJsonList(); |
| | | if (jsonASTList.size() > 0) { |
| | | Json jsonAST = jsonASTList.get(0); |
| | | if ("query_water_fee".equals(jsonAST.safeGetStr("call", null))) { |
| | | jsonAST = jsonAST.getJson("args"); |
| | | try (ASTDBMap dbMap = new ASTDBMap()) { |
| | | SMTDatabase db = dbMap.getDatabase("DS_74_CHENGTOU"); |
| | | DBRecords recs = |
| | | db.querySQL( |
| | | " SELECT ROUND(SUM(" |
| | | + jsonAST.getJson("value_name").asString() |
| | | + ")::NUMERIC(10, 2), 2) AS TOTAL" |
| | | + " FROM chengtou_data.bill_data WHERE billing_date BETWEEN ? AND ?", |
| | | new Object[] { |
| | | SMTStatic.toDate(jsonAST.getJson("start_time").asString()), |
| | | SMTStatic.toDate(jsonAST.getJson("end_time").asString()) |
| | | }); |
| | | |
| | | if (recs.getRecord(0).getString(0) == null) |
| | | return "从" |
| | | + jsonAST.getJson("start_time").asString() |
| | | + "到" |
| | | + jsonAST.getJson("end_time").asString() |
| | | + "的" |
| | | + jsonAST.getJson("value_title").asString() |
| | | + "未查到任何数据"; |
| | | |
| | | return "从" |
| | | + jsonAST.getJson("start_time").asString() |
| | | + "到" |
| | | + jsonAST.getJson("end_time").asString() |
| | | + "的" |
| | | + jsonAST.getJson("value_title").asString() |
| | | + "总计" |
| | | + recs.getRecord(0).getString(0) |
| | | + ("volume".equals(jsonAST.getJson("value_name").asString()) ? "吨" : "元"); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | answer = llm.callWithMessage(null, question, tranReq); |
| | | |
| | | return answer; |
| | | } |
| | | |
| | | /** 二次回复 */ |
| | | public ModelAndView aiReplyToTheUserASecondTime(String answer, String fromUserName) |
| | | throws Exception { |
| | | String accessToken = getAccessToken(); |
| | | SMTJsonWriter jsonWr = new SMTJsonWriter(false); |
| | | jsonWr.addKeyValue("touser", fromUserName); |
| | | jsonWr.addKeyValue("msgtype", "text"); |
| | | jsonWr.beginMap("text"); |
| | | { |
| | | jsonWr.addKeyValue("content", answer); |
| | | } |
| | | jsonWr.endMap(); |
| | | String url = |
| | | String.format( |
| | | "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=%s", accessToken); |
| | | // String body = HttpRequest.post(url).form(String.valueOf(jsonObject)).timeout(20000)//超时,毫秒 |
| | | // .execute().body(); |
| | | Map<String, Object> stringObjectMap = jsonObjectToMap(jsonObject); |
| | | String s = sendPost(url, stringObjectMap); |
| | | String s = sendPost(url, jsonWr.getRootJson()); |
| | | _logger.info("上传结果: {}", s); |
| | | return null; |
| | | } |
| | | |
| | | public String getAccessToken() throws Exception { |
| | | synchronized (this._lockToken) { |
| | | SMTDatabase db = SMTAIServerApp.getApp().allocDatabase(); |
| | | try { |
| | | HashMap<String, String> weixinParam = getWeixinParam(); |
| | | long ONE_HOUR_IN_MILLIS = 3600 * 1000; |
| | | long expiresTime = System.currentTimeMillis() + ONE_HOUR_IN_MILLIS; |
| | | Timestamp expiresTimestamp = new Timestamp(expiresTime); |
| | | String appId = weixinParam.get("appId"); |
| | | |
| | | // 查询未过期的 access_token |
| | | DBRecords dbRecord = |
| | | db.querySQL( |
| | | "SELECT app_id, access_token, expires_time " |
| | | + "FROM ai_weixin_token " |
| | | + "WHERE app_id = ? " |
| | | + " AND expires_time > ?", |
| | | new Object[] {appId, expiresTimestamp}); |
| | | |
| | | private static ModelAndView getModelAndView(SMTRequest tranReq) throws Exception { |
| | | String signature = tranReq.convParamToString("signature", true); |
| | | String timestamp = tranReq.convParamToString("timestamp", true); |
| | | String nonce = tranReq.convParamToString("nonce", true); |
| | | String echostr = tranReq.convParamToString("echostr", true); |
| | | // 获取微信请求参数 |
| | | _logger.info( |
| | | "开始校验此次消息是否来自微信服务器,param->signature:{},\ntimestamp:{},\nnonce:{},\nechostr:{}", |
| | | signature, |
| | | timestamp, |
| | | nonce, |
| | | echostr); |
| | | // 需要验证的时候就启用 |
| | | if (checkSignature(signature, timestamp, nonce)) { |
| | | return tranReq.returnText(echostr); |
| | | } |
| | | return tranReq.returnText(""); |
| | | } |
| | | |
| | | public static Map<String, Object> jsonObjectToMap(JSONObject jsonObject) throws JSONException { |
| | | Map<String, Object> map = new HashMap<>(); |
| | | Iterator<String> keys = jsonObject.keys(); |
| | | |
| | | while (keys.hasNext()) { |
| | | String key = keys.next(); |
| | | Object value = jsonObject.get(key); |
| | | |
| | | // 如果值是 JSONObject,递归转换为 Map |
| | | if (value instanceof JSONObject) { |
| | | value = jsonObjectToMap((JSONObject) value); |
| | | // 数据库无记录,从微信服务器获取 access_token |
| | | List<SMTDatabase.DBRecord> records = dbRecord.getRecords(); |
| | | boolean res = false; |
| | | if (dbRecord.getRowCount() != 0) { |
| | | String expires_time = records.get(0).getString("expires_time"); |
| | | long dbExpiresTime = Timestamp.valueOf(expires_time).getTime(); |
| | | res = System.currentTimeMillis() <= dbExpiresTime; |
| | | } |
| | | // 微信取,返回token并且保存或覆盖数据 |
| | | if (dbRecord.getRowCount() == 0 || !res) { |
| | | String accessToken = fetchAccessTokenFromWeixinServer(); // 从微信服务器获取 access_token |
| | | String sql = |
| | | "INSERT INTO ai_weixin_token (app_id, access_token, expires_time) " |
| | | + "VALUES (?, ?, ?) " |
| | | + "ON CONFLICT (app_id) " |
| | | + // 如果 app_id 冲突 |
| | | "DO UPDATE SET " |
| | | + " access_token = EXCLUDED.access_token, " |
| | | + " expires_time = EXCLUDED.expires_time;"; |
| | | db.executeSQL(sql, new Object[] {appId, accessToken, expiresTimestamp}); |
| | | return accessToken; |
| | | } else { // 直接拿数据库accesstoken |
| | | return records.get(0).getString("access_token"); |
| | | } |
| | | } catch (Exception e) { |
| | | throw new Exception("Failed to get access token: " + e); |
| | | } finally { |
| | | if (db != null) { |
| | | db.close(); |
| | | } |
| | | } |
| | | |
| | | map.put(key, value); |
| | | } |
| | | |
| | | return map; |
| | | } |
| | | |
| | | /** |
| | | * 验证签名 |
| | | * |
| | | * @param signature |
| | | * @param timestamp |
| | | * @param nonce |
| | | * @return |
| | | */ |
| | | public static boolean checkSignature(String signature, String timestamp, String nonce) |
| | | throws Exception { |
| | | Object weixinParam = SMTAIServerApp.getApp().getGlobalConfig("weixin_core_param", "false"); |
| | | _logger.info("微信参数:{}", weixinParam); |
| | | JSONObject weixinJson = new JSONObject(weixinParam.toString()); |
| | | Object appId = weixinJson.get("appId"); |
| | | Object secret = weixinJson.get("secret"); |
| | | String token = (String) weixinJson.get("token"); |
| | | |
| | | String[] arr = new String[] {token, timestamp, nonce}; |
| | | // 将token、timestamp、nonce三个参数进行字典序排序 |
| | | // Arrays.sort(arr); |
| | | sort(arr); |
| | | StringBuilder content = new StringBuilder(); |
| | | for (String s : arr) { |
| | | content.append(s); |
| | | } |
| | | MessageDigest md = null; |
| | | String tmpStr = null; |
| | | |
| | | try { |
| | | md = MessageDigest.getInstance("SHA-1"); |
| | | // 将三个参数字符串拼接成一个字符串进行sha1加密 |
| | | byte[] digest = md.digest(content.toString().getBytes()); |
| | | tmpStr = byteToStr(digest); |
| | | } catch (NoSuchAlgorithmException e) { |
| | | _logger.error("签名异常", e); |
| | | } |
| | | content = null; |
| | | // 将sha1加密后的字符串可与signature对比,标识该请求来源于微信 |
| | | |
| | | return tmpStr != null && tmpStr.equals(signature.toUpperCase()); |
| | | } |
| | | public static Map<String, String> getWechatReqMap(HttpServletRequest request) { |
| | | Map<String, String> requestMap = new HashMap<>(); |
| | | SAXReader reader = new SAXReader(); |
| | | try (ServletInputStream inputStream = request.getInputStream()) { |
| | | Document document = reader.read(inputStream); |
| | | Element root = document.getRootElement(); |
| | | List<Element> elementList = root.elements(); |
| | | for (Element e : elementList) { |
| | | requestMap.put(e.getName(), e.getText()); |
| | | } |
| | | } catch (IOException | DocumentException e) { |
| | | throw new RuntimeException(e); |
| | | } |
| | | return requestMap; |
| | | } |
| | | /** |
| | | * 将字节数组转换为十六进制字符串 |
| | | * |
| | | * @param byteArray |
| | | * @return |
| | | */ |
| | | private static String byteToStr(byte[] byteArray) { |
| | | StringBuilder strDigest = new StringBuilder(); |
| | | for (byte b : byteArray) { |
| | | strDigest.append(byteToHexStr(b)); |
| | | } |
| | | return strDigest.toString(); |
| | | } |
| | | |
| | | /** |
| | | * 将字节转换为十六进制字符串 |
| | | * |
| | | * @param mByte |
| | | * @return |
| | | */ |
| | | private static String byteToHexStr(byte mByte) { |
| | | char[] Digit = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; |
| | | char[] tempArr = new char[2]; |
| | | tempArr[0] = Digit[(mByte >>> 4) & 0X0F]; |
| | | tempArr[1] = Digit[mByte & 0X0F]; |
| | | String s = new String(tempArr); |
| | | return s; |
| | | } |
| | | |
| | | public static String sendPost(String urlString, Map<String, Object> params) throws Exception { |
| | | // 将参数转换为 JSON 格式字符串 |
| | | JSONObject jsonParams = new JSONObject(params); |
| | | String payload = jsonParams.toString(); |
| | | // 创建连接 |
| | | URL url = new URL(urlString); |
| | | HttpURLConnection connection = (HttpURLConnection) url.openConnection(); |
| | | connection.setRequestMethod("POST"); |
| | | // 设置请求头 |
| | | connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8"); |
| | | connection.setDoOutput(true); |
| | | // 写入请求体 |
| | | try (OutputStream os = connection.getOutputStream()) { |
| | | byte[] input = payload.getBytes(StandardCharsets.UTF_8); |
| | | os.write(input, 0, input.length); |
| | | } |
| | | // 读取响应 |
| | | try (java.io.InputStream in = connection.getInputStream()) { |
| | | java.util.Scanner scanner = new java.util.Scanner(in).useDelimiter("\\A"); |
| | | return scanner.hasNext() ? scanner.next() : ""; |
| | | private String fetchAccessTokenFromWeixinServer() throws Exception { |
| | | HashMap<String, String> weixinParam = getWeixinParam(); |
| | | String url = |
| | | "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" |
| | | + weixinParam.get("appId") |
| | | + "&secret=" |
| | | + weixinParam.get("secret"); |
| | | String response = HttpUtil.get(url); |
| | | Json json = Json.read(response); |
| | | String accessToken = json.safeGetStr("access_token", null); |
| | | if (accessToken != null) { |
| | | return accessToken; |
| | | } else { |
| | | throw new Exception("can't get weixin token : " + json); |
| | | } |
| | | } |
| | | } |