落地系统异常:大模型/TMS/建群故障停住本单并告警,后台可重试失败步骤。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-21 14:01:49 +08:00
co-authored by Cursor
parent a5e73c01ae
commit 4381966c4e
26 changed files with 2098 additions and 72 deletions
@@ -204,6 +204,37 @@ public class InquiryAgentController {
return Result.OK(rendered);
}
@ApiOperation("智能体写入系统异常两列(已关闭拒绝,不改六态)")
@PostMapping("/ticket/markSystemException")
public Result<Map<String, Object>> markSystemException(@RequestBody Map<String, Object> body,
HttpServletRequest http) {
if (!authed(http)) {
return Result.error(401, "unauthorized");
}
return Result.OK(workOrderService.markSystemException(body));
}
@ApiOperation("智能体清除系统异常两列(重试成功后)")
@PostMapping("/ticket/clearSystemException")
public Result<Map<String, Object>> clearSystemException(@RequestBody Map<String, Object> body,
HttpServletRequest http) {
if (!authed(http)) {
return Result.error(401, "unauthorized");
}
return Result.OK(workOrderService.clearSystemException(firstStr(body, "workOrderNo", "work_order_no")));
}
@ApiOperation("系统异常告警接收人:角色勾了通知且账号已绑企微")
@PostMapping("/account/listExceptionNotify")
public Result<Map<String, Object>> listExceptionNotify(HttpServletRequest http) {
if (!authed(http)) {
return Result.error(401, "unauthorized");
}
Map<String, Object> out = new java.util.LinkedHashMap<>();
out.put("staff", workOrderService.listExceptionNotifyUsers());
return Result.OK(out);
}
@ApiOperation("经主账查真 TMS(有价回报价 / 无价回暂无报价 / 技术失败)")
@PostMapping("/tms/query")
public Result<Map<String, Object>> tmsQuery(@RequestBody Map<String, Object> body,
@@ -122,6 +122,19 @@ public class InquiryTicket implements Serializable {
@ApiModelProperty("系统异常原因")
private String systemExceptionReason;
/**
* 后台点重试时重跑哪一步:tms_quote / tms_cabin / llm / group。
* 不改六态;成功后与两列一并清空。
*/
@ApiModelProperty("系统异常步骤")
private String systemExceptionStep;
/**
* 重试上下文 JSON:会话目标、未加上的人、业务线等。
*/
@ApiModelProperty("系统异常重试上下文")
private String systemExceptionPayload;
@ApiModelProperty("关单原因")
private String closeReason;
@@ -0,0 +1,29 @@
package org.jeecg.modules.inquiry.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
/**
* 系统异常告警接收人:后台账号角色勾了「系统异常消息通知」,且工号(企微 userid)非空。
* <p>
* 不按海陆空过滤。inquiry 模块不依赖 system-biz,直接读同库 Jeecg 表。
*/
@Mapper
public interface InquiryExceptionNotifyMapper {
/**
* 启用账号 + 角色 description 含系统异常通知稳定码 + work_no 已绑企微。
*/
@Select("SELECT DISTINCT u.work_no AS wecomId, u.realname AS name "
+ "FROM sys_role r "
+ "INNER JOIN sys_user_role ur ON ur.role_id = r.id "
+ "INNER JOIN sys_user u ON u.id = ur.user_id "
+ "WHERE u.del_flag = 0 AND u.status = 1 "
+ "AND u.work_no IS NOT NULL AND TRIM(u.work_no) <> '' "
+ "AND (r.description LIKE '%系统异常·消息通知%' "
+ "OR r.description LIKE '%系统异常消息通知%')")
List<Map<String, Object>> listBoundAccounts();
}
@@ -41,10 +41,25 @@ public interface IInquiryWorkOrderService extends IService<InquiryTicket> {
void handleAbnormal(TicketAbnormalRequest req, String operator);
/**
* 系统异常重试:累加 retry_count,记日志;真正重跑任务由 Worker 后续认领(本期只落主账状态)。
* 系统异常重试:已关闭拒绝;记下重试意图后唤醒智能体(快回,不在本线程重跑)。
*/
void retryAbnormal(TicketAbnormalRequest req, String operator);
/**
* 智能体写入系统异常两列与重试上下文。已关闭拒绝。不改六态。
*/
Map<String, Object> markSystemException(Map<String, Object> body);
/**
* 重试成功后清两列。
*/
Map<String, Object> clearSystemException(String workOrderNo);
/**
* 后台账号:角色勾了系统异常消息通知,且工号已绑企微。
*/
java.util.List<java.util.Map<String, Object>> listExceptionNotifyUsers();
/**
* 智能体建单:每次出询价确认都新开一单,相同需求不复用未关闭工单。
*/
@@ -20,8 +20,10 @@ import org.jeecg.modules.inquiry.mapper.InquiryAttachmentMapper;
import org.jeecg.modules.inquiry.mapper.InquiryExceptionCaseMapper;
import org.jeecg.modules.inquiry.mapper.InquiryQuoteMapper;
import org.jeecg.modules.inquiry.mapper.InquiryStaffRoleMapper;
import org.jeecg.modules.inquiry.mapper.InquiryExceptionNotifyMapper;
import org.jeecg.modules.inquiry.mapper.InquiryTicketMapper;
import org.jeecg.modules.inquiry.mapper.InquiryTicketStatusLogMapper;
import org.jeecg.modules.inquiry.support.AgentSystemExceptionRetryClient;
import org.jeecg.modules.inquiry.service.IInquiryWorkOrderService;
import org.jeecg.modules.inquiry.support.InquiryQuoteDisplaySupport;
import org.jeecg.modules.inquiry.support.InquiryTicketDisplaySupport;
@@ -69,6 +71,10 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
private InquiryTmsQueryService tmsQueryService;
@Autowired
private InquiryStaffRoleMapper staffRoleMapper;
@Autowired
private InquiryExceptionNotifyMapper exceptionNotifyMapper;
@Autowired
private AgentSystemExceptionRetryClient exceptionRetryClient;
@Override
public IPage<InquiryTicket> pageView(Page<InquiryTicket> page, Map<String, String> filters) {
@@ -300,6 +306,8 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
ticket.setEventException(null);
ticket.setSystemException(null);
ticket.setSystemExceptionReason(null);
ticket.setSystemExceptionStep(null);
ticket.setSystemExceptionPayload(null);
ticket.setUpdateBy(operator);
ticket.setUpdateTime(now);
updateById(ticket);
@@ -310,6 +318,12 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
@Transactional(rollbackFor = Exception.class)
public void retryAbnormal(TicketAbnormalRequest req, String operator) {
InquiryTicket ticket = requireTicket(req);
if (TicketStatuses.CLOSED.equals(ticket.getStatus())) {
throw new IllegalArgumentException("已关闭工单不能重试");
}
if (StringUtils.isBlank(ticket.getSystemException())) {
throw new IllegalArgumentException("当前没有系统异常");
}
List<InquiryExceptionCase> openCases = exceptionCaseMapper.selectList(new LambdaQueryWrapper<InquiryExceptionCase>()
.eq(InquiryExceptionCase::getWorkOrderNo, ticket.getWorkOrderNo())
.eq(InquiryExceptionCase::getCaseType, "SYSTEM")
@@ -339,6 +353,110 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
}
}
appendLog(ticket, ticket.getStatus(), ticket.getStatus(), "retry", operator, req.getRemark());
String wakeErr = exceptionRetryClient.enqueueRetry(ticket.getWorkOrderNo());
if (StringUtils.isNotBlank(wakeErr)) {
throw new IllegalArgumentException(wakeErr);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> markSystemException(Map<String, Object> body) {
Map<String, Object> out = new LinkedHashMap<>();
String no = firstBody(body, "workOrderNo", "work_order_no");
InquiryTicket ticket = findByWorkOrderNo(no);
if (ticket == null) {
out.put("ok", false);
out.put("error", "not_found");
return out;
}
if (TicketStatuses.CLOSED.equals(ticket.getStatus())) {
out.put("ok", false);
out.put("error", "closed");
return out;
}
String type = firstBody(body, "systemException", "system_exception");
String reason = firstBody(body, "systemExceptionReason", "system_exception_reason");
String step = firstBody(body, "step", "systemExceptionStep", "system_exception_step");
Object payloadObj = body == null ? null : body.get("payload");
if (payloadObj == null && body != null) {
payloadObj = body.get("systemExceptionPayload");
}
String payloadJson = payloadObj == null ? null
: (payloadObj instanceof String ? (String) payloadObj : JSON.toJSONString(payloadObj));
ticket.setSystemException(type);
ticket.setSystemExceptionReason(reason);
ticket.setSystemExceptionStep(step);
ticket.setSystemExceptionPayload(payloadJson);
ticket.setUpdateTime(new Date());
updateById(ticket);
InquiryExceptionCase created = new InquiryExceptionCase();
created.setTicketId(ticket.getId());
created.setWorkOrderNo(ticket.getWorkOrderNo());
created.setCaseType("SYSTEM");
created.setTitle(StringUtils.defaultIfBlank(type, "系统异常"));
created.setReason(reason);
created.setStatus("OPEN");
created.setRetryCount(0);
created.setCreateTime(new Date());
created.setUpdateTime(new Date());
exceptionCaseMapper.insert(created);
appendLog(ticket, ticket.getStatus(), ticket.getStatus(), "system_exception", "agent", reason);
out.put("ok", true);
out.put("status", ticket.getStatus());
return out;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> clearSystemException(String workOrderNo) {
Map<String, Object> out = new LinkedHashMap<>();
InquiryTicket ticket = findByWorkOrderNo(StringUtils.trimToEmpty(workOrderNo));
if (ticket == null) {
out.put("ok", false);
out.put("error", "not_found");
return out;
}
ticket.setSystemException(null);
ticket.setSystemExceptionReason(null);
ticket.setSystemExceptionStep(null);
ticket.setSystemExceptionPayload(null);
ticket.setUpdateTime(new Date());
updateById(ticket);
List<InquiryExceptionCase> openCases = exceptionCaseMapper.selectList(new LambdaQueryWrapper<InquiryExceptionCase>()
.eq(InquiryExceptionCase::getWorkOrderNo, ticket.getWorkOrderNo())
.eq(InquiryExceptionCase::getCaseType, "SYSTEM")
.in(InquiryExceptionCase::getStatus, "OPEN", "RETRYING"));
Date now = new Date();
for (InquiryExceptionCase c : openCases) {
c.setStatus("HANDLED");
c.setHandledBy("agent");
c.setHandledAt(now);
c.setUpdateTime(now);
exceptionCaseMapper.updateById(c);
}
appendLog(ticket, ticket.getStatus(), ticket.getStatus(), "system_exception_clear", "agent", "重试成功");
out.put("ok", true);
return out;
}
@Override
public List<Map<String, Object>> listExceptionNotifyUsers() {
List<Map<String, Object>> rows = exceptionNotifyMapper.listBoundAccounts();
return rows == null ? Collections.emptyList() : rows;
}
private static String firstBody(Map<String, Object> body, String... keys) {
if (body == null) {
return "";
}
for (String key : keys) {
Object v = body.get(key);
if (v != null && StringUtils.isNotBlank(String.valueOf(v))) {
return String.valueOf(v).trim();
}
}
return "";
}
/**
@@ -699,6 +817,19 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
}
out.put("collab_facts", collabFacts);
out.put("collab_member_ids", parseMemberIds(ticket.getCollabMemberIds()));
out.put("system_exception", StringUtils.defaultString(ticket.getSystemException()));
out.put("systemException", StringUtils.defaultString(ticket.getSystemException()));
out.put("system_exception_reason", StringUtils.defaultString(ticket.getSystemExceptionReason()));
out.put("systemExceptionReason", StringUtils.defaultString(ticket.getSystemExceptionReason()));
out.put("system_exception_step", StringUtils.defaultString(ticket.getSystemExceptionStep()));
Map<String, Object> excPayload = new LinkedHashMap<>();
if (StringUtils.isNotBlank(ticket.getSystemExceptionPayload())) {
JSONObject parsed = JSON.parseObject(ticket.getSystemExceptionPayload());
if (parsed != null) {
excPayload.putAll(parsed);
}
}
out.put("system_exception_payload", excPayload);
return out;
}
@@ -0,0 +1,122 @@
package org.jeecg.modules.inquiry.support;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 后台点「重试」后唤醒智能体:只入队,不等 TMS / 大模型 / 建群跑完。
* <p>
* 打 AGENT_HTTP_BASE_URL /internal/bridge/wake,kind=system_exception_retry。
* 禁止在本类同步等 Python Graph。
*/
@Slf4j
@Component
public class AgentSystemExceptionRetryClient {
@Value("${inquiry.agent.base-url:${AGENT_HTTP_BASE_URL:http://127.0.0.1:8910}}")
private String agentBaseUrl;
@Value("${inquiry.agent.callback-token:${AGENT_CALLBACK_TOKEN:}}")
private String agentToken;
@Value("${inquiry.agent.retry-timeout-ms:8000}")
private int timeoutMs;
/**
* 入队系统异常重试。返回 null 表示智能体已收下;非空为失败原因。
*/
public String enqueueRetry(String workOrderNo) {
String no = StringUtils.trimToEmpty(workOrderNo);
if (StringUtils.isBlank(no)) {
return "工单号为空";
}
String base = StringUtils.removeEnd(StringUtils.defaultIfBlank(agentBaseUrl, ""), "/");
if (StringUtils.isBlank(base)) {
return "智能体地址未配置(AGENT_HTTP_BASE_URL)";
}
String url = base + "/internal/bridge/wake";
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod("POST");
conn.setConnectTimeout(Math.max(2000, timeoutMs));
conn.setReadTimeout(Math.max(2000, timeoutMs));
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
conn.setRequestProperty("Accept", "application/json");
if (StringUtils.isNotBlank(agentToken)) {
conn.setRequestProperty("X-Agent-Callback-Token", agentToken);
}
Map<String, Object> body = new LinkedHashMap<>();
body.put("thread_id", no);
body.put("inquiry_no", no);
body.put("kind", "system_exception_retry");
body.put("quote_version", 0);
body.put("wait_version", 0);
byte[] bytes = JSON.toJSONBytes(body);
try (OutputStream out = conn.getOutputStream()) {
out.write(bytes);
}
int code = conn.getResponseCode();
String respText = readQuietly(conn, code >= 200 && code < 300);
if (code == 202 || code == 200) {
JSONObject resp = safeJson(respText);
if (resp != null && "accepted".equals(resp.getString("status"))) {
return null;
}
if (resp != null && StringUtils.isNotBlank(resp.getString("entry_id"))) {
return null;
}
return "智能体未确认入队";
}
return "无法提交重试: HTTP " + code;
} catch (Exception e) {
log.warn("系统异常重试唤醒失败 wo={} err={}", no, e.getMessage());
return "无法提交重试: " + e.getMessage();
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
private static String readQuietly(HttpURLConnection conn, boolean ok) {
try {
java.io.InputStream in = ok ? conn.getInputStream() : conn.getErrorStream();
if (in == null) {
return "";
}
java.io.ByteArrayOutputStream buf = new java.io.ByteArrayOutputStream();
byte[] chunk = new byte[512];
int n;
while ((n = in.read(chunk)) > 0) {
buf.write(chunk, 0, n);
}
return new String(buf.toByteArray(), StandardCharsets.UTF_8);
} catch (Exception e) {
return "";
}
}
private static JSONObject safeJson(String text) {
if (StringUtils.isBlank(text)) {
return null;
}
try {
return JSON.parseObject(text);
} catch (Exception e) {
return null;
}
}
}
@@ -0,0 +1,33 @@
-- 系统异常重试上下文:步骤 + JSON。仅改 inquiry_robot。
-- 两列展示字段 system_exception / system_exception_reason 已存在。
-- 可重复执行:已有列则跳过。
SET NAMES utf8mb4;
SET @db := DATABASE();
SET @exists_step := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'inq_ticket' AND COLUMN_NAME = 'system_exception_step'
);
SET @sql_step := IF(
@exists_step = 0,
'ALTER TABLE `inq_ticket` ADD COLUMN `system_exception_step` varchar(64) NULL COMMENT ''系统异常步骤:tms_quote/tms_cabin/llm/group'' AFTER `system_exception_reason`',
'SELECT 1'
);
PREPARE stmt_step FROM @sql_step;
EXECUTE stmt_step;
DEALLOCATE PREPARE stmt_step;
SET @exists_payload := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'inq_ticket' AND COLUMN_NAME = 'system_exception_payload'
);
SET @sql_payload := IF(
@exists_payload = 0,
'ALTER TABLE `inq_ticket` ADD COLUMN `system_exception_payload` text NULL COMMENT ''系统异常重试上下文 JSON'' AFTER `system_exception_step`',
'SELECT 1'
);
PREPARE stmt_payload FROM @sql_payload;
EXECUTE stmt_payload;
DEALLOCATE PREPARE stmt_payload;