This commit is contained in:
van
2026-04-02 20:09:31 +08:00
parent d361e93895
commit 72d5856838
9 changed files with 469 additions and 22 deletions

View File

@@ -0,0 +1,117 @@
package com.ruoyi.jarvis.wecom;
import com.alibaba.fastjson2.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* 调用 wxSend 的企微应用文本主动推送POST /wecom/active-push
*/
@Component
public class WxSendWeComPushClient {
private static final Logger log = LoggerFactory.getLogger(WxSendWeComPushClient.class);
public static final String HEADER_PUSH_SECRET = "X-WxSend-WeCom-Push-Secret";
@Value("${jarvis.wecom.wxsend-base-url:}")
private String wxsendBaseUrl;
@Value("${jarvis.wecom.push-secret:}")
private String pushSecret;
/**
* 在被动回复返回后延迟再发,保证企微侧先出现首条被动消息。
*/
public void scheduleActivePushes(String toUser, List<String> contents) {
if (!StringUtils.hasText(wxsendBaseUrl) || !StringUtils.hasText(pushSecret)
|| !StringUtils.hasText(toUser) || contents == null || contents.isEmpty()) {
return;
}
final String userId = toUser.trim();
final List<String> list = new ArrayList<>(contents);
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(450);
String base = normalizeBase(wxsendBaseUrl);
String url = base + "/wecom/active-push";
for (String c : list) {
if (!StringUtils.hasText(c)) {
continue;
}
postJson(url, userId, c.trim());
Thread.sleep(120);
}
} catch (Exception e) {
log.warn("企微主动推送任务异常 userId={} msg={}", userId, e.toString());
}
});
}
private static String normalizeBase(String base) {
String b = base.trim();
if (b.endsWith("/")) {
return b.substring(0, b.length() - 1);
}
return b;
}
private void postJson(String url, String toUser, String content) {
JSONObject body = new JSONObject();
body.put("toUser", toUser);
body.put("content", content);
byte[] bytes = body.toJSONString().getBytes(StandardCharsets.UTF_8);
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod("POST");
conn.setConnectTimeout(15000);
conn.setReadTimeout(60000);
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
conn.setRequestProperty(HEADER_PUSH_SECRET, pushSecret);
try (OutputStream os = conn.getOutputStream()) {
os.write(bytes);
}
int code = conn.getResponseCode();
InputStream is = code >= 200 && code < 300 ? conn.getInputStream() : conn.getErrorStream();
String resp = readAll(is);
if (code < 200 || code >= 300) {
log.warn("wxSend active-push HTTP {} body={}", code, resp);
} else {
log.debug("wxSend active-push OK http={} resp={}", code, resp);
}
} catch (Exception e) {
log.warn("wxSend active-push 请求失败 url={} err={}", url, e.toString());
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
private static String readAll(InputStream is) throws java.io.IOException {
if (is == null) {
return "";
}
byte[] buf = new byte[4096];
StringBuilder sb = new StringBuilder();
int n;
while ((n = is.read(buf)) >= 0) {
sb.append(new String(buf, 0, n, StandardCharsets.UTF_8));
}
return sb.toString();
}
}

View File

@@ -2,8 +2,10 @@ package com.ruoyi.web.controller.jarvis;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.jarvis.domain.dto.WeComInboundRequest;
import com.ruoyi.jarvis.domain.dto.WeComInboundResult;
import com.ruoyi.jarvis.service.IWeComInboundService;
import com.ruoyi.jarvis.service.IWeComInboundTraceService;
import com.ruoyi.jarvis.wecom.WxSendWeComPushClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
@@ -28,6 +30,8 @@ public class WeComInboundController {
private IWeComInboundService weComInboundService;
@Resource
private IWeComInboundTraceService weComInboundTraceService;
@Resource
private WxSendWeComPushClient wxSendWeComPushClient;
@PostMapping("/inbound")
public AjaxResult inbound(
@@ -37,10 +41,12 @@ public class WeComInboundController {
return AjaxResult.error("拒绝访问");
}
WeComInboundRequest req = body != null ? body : new WeComInboundRequest();
String reply = weComInboundService.handleInbound(req);
weComInboundTraceService.recordInbound(req, reply);
Map<String, Object> data = new HashMap<>(2);
data.put("reply", reply != null ? reply : "");
WeComInboundResult result = weComInboundService.handleInbound(req);
weComInboundTraceService.recordInbound(req, result.toTraceFullText());
Map<String, Object> data = new HashMap<>(4);
data.put("reply", result.getPassiveReply());
data.put("activePushCount", result.getActivePushContents().size());
wxSendWeComPushClient.scheduleActivePushes(req.getFromUserName(), result.getActivePushContents());
return AjaxResult.success(data);
}
}

View File

@@ -210,10 +210,24 @@ jarvis:
# 企微经 wxSend 调用本接口时校验(须与 wxSend 配置一致)
wecom:
inbound-secret: jarvis_wecom_bridge_change_me
# wxSend 根地址(无尾斜杠),用于 F 录单等第二条起主动推送;与 wxSend server.port 一致
wxsend-base-url: http://127.0.0.1:36699
# 须与 wxSend jarvis.wecom.push-secret 一致Header X-WxSend-WeCom-Push-Secret
push-secret: jarvis_wecom_push_change_me
# 多轮会话:与 JDUtil interaction_state 类似TTL 与空闲超时(分钟)
session-ttl-minutes: 30
session-idle-timeout-minutes: 30
session-sweep-ms: 60000
# 企微「开」+ 手机号Jarvis POST 该局域网接口,将响应中的 reply_text 被动回复给用户
phone-forward:
enabled: true
base-url: http://192.168.8.60:18080
path: /v1/forward
connect-timeout-ms: 8000
# wait_reply 时服务端会等多条 Bot 回复,宜适当加大
read-timeout-ms: 120000
wait-reply: true
reply-take-nth: 2
# Ollama 大模型服务(监控健康度调试用)
ollama:
base-url: http://192.168.8.34:11434

View File

@@ -208,9 +208,19 @@ jarvis:
base-url: http://192.168.8.60:5008
wecom:
inbound-secret: jarvis_wecom_bridge_change_me
wxsend-base-url: http://127.0.0.1:36699
push-secret: jarvis_wecom_push_change_me
session-ttl-minutes: 30
session-idle-timeout-minutes: 30
session-sweep-ms: 60000
phone-forward:
enabled: true
base-url: http://192.168.8.60:18080
path: /v1/forward
connect-timeout-ms: 8000
read-timeout-ms: 120000
wait-reply: true
reply-take-nth: 2
# Ollama 大模型服务(监控健康度调试用)
ollama:
base-url: http://192.168.8.34:11434