落地系统异常:大模型/TMS/建群故障停住本单并告警,后台可重试失败步骤。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+31
@@ -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,
|
||||
|
||||
+13
@@ -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;
|
||||
|
||||
|
||||
+29
@@ -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();
|
||||
}
|
||||
+16
-1
@@ -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();
|
||||
|
||||
/**
|
||||
* 智能体建单:每次出询价确认都新开一单,相同需求不复用未关闭工单。
|
||||
*/
|
||||
|
||||
+131
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+122
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -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;
|
||||
Reference in New Issue
Block a user