OpController.java
7.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
package com.xly.web;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xly.service.AuditService;
import com.xly.service.ErpClient;
import com.xly.service.OpService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 写操作确认端点(**确定性、不经过 LLM**)—— 人在环写入闭环的执行侧。
*
* <p>用户在对话框点【确认】-> {@code /confirm} 才真正调 ERP 执行;点【取消】-> {@code /cancel}。
* v1 为同步执行(update 很快):直接调 ERP 表单更新接口,回写 ai_op_queue 状态并返回结果。
*/
@RestController
@RequestMapping("/api/agent/op")
public class OpController {
private static final Logger log = LoggerFactory.getLogger(OpController.class);
private final OpService ops;
private final ErpClient erp;
private final AuditService audit;
private final ObjectMapper mapper;
/** true=确认后委托 ERP 侧暂存执行器(/ai/execStaging,§10 生产路径);false=xlyAi 直连 ERP 通用写接口执行。 */
@org.springframework.beans.factory.annotation.Value("${erp.exec-staging.enabled:false}")
private boolean execStagingEnabled;
public OpController(OpService ops, ErpClient erp, AuditService audit, ObjectMapper mapper) {
this.ops = ops;
this.erp = erp;
this.audit = audit;
this.mapper = mapper;
}
/** 会话里最近一条待确认操作(前端每轮结束后轮询,用于渲染确认卡片)。 */
@GetMapping("/pending")
public Map<String, Object> pending(@RequestParam("conversationId") String conversationId) {
Map<String, Object> op = ops.pending(conversationId);
return op == null ? Map.of() : op;
}
/**
* 确认执行:调 ERP 执行暂存的写操作,回写状态。
* <p>{@code Authorization} 头(可空)= 用户浏览器里的 ERP 登录 token,透传给 ERP 使执行以用户身份
* 进行;为空则回退 dev-login。绝不因用户 token 缺失而静默提权(见 ErpClient.canRelogin)。
*/
@PostMapping("/{id}/confirm")
public Map<String, Object> confirm(@PathVariable("id") String id,
@RequestHeader(value = "Authorization", required = false) String authToken) {
Map<String, Object> op = ops.get(id);
if (op == null) {
return result("failed", "找不到该操作", null);
}
if (!"draft".equals(String.valueOf(op.get("sStatus")))) {
return result(String.valueOf(op.get("sStatus")), "该操作已处理过(" + op.get("sStatus") + ")", op);
}
String uid = str(op.get("sUserId"));
String conv = str(op.get("sConversationId"));
String target = str(op.get("sTargetTable")) + "#" + str(op.get("sTargetBillId"));
String detail = str(op.get("sDescription"));
// 生产路径:委托 ERP 侧暂存执行器(它以用户身份执行、事务化并自行回写 ai_op_queue 状态)。
if (execStagingEnabled) {
return confirmViaExecutor(id, op, authToken, uid, conv, target, detail);
}
try {
String opType = str(op.get("sOpType"));
JsonNode r;
if ("create".equals(opType)) {
@SuppressWarnings("unchecked")
Map<String, Object> columns = mapper.readValue(str(op.get("sPayload")), Map.class);
r = erp.createForm(authToken, str(op.get("sTargetTable")), columns);
} else if ("delete".equals(opType)) {
r = erp.deleteForm(authToken, str(op.get("sTargetModuleId")), str(op.get("sTargetTable")), str(op.get("sTargetBillId")));
} else if ("examine".equals(opType)) {
int iFlag = "0".equals(str(op.get("sNewValue"))) ? 0 : 1; // 1=审核 0=反审核
r = erp.examineForm(authToken, str(op.get("sTargetModuleId")), str(op.get("sTargetBillId")), iFlag);
} else {
r = erp.updateForm(authToken,
str(op.get("sTargetModuleId")), str(op.get("sTargetTable")), str(op.get("sTargetBillId")),
str(op.get("sField")), str(op.get("sNewValue")));
}
int code = r.path("code").asInt(0);
if (code == 1) {
ops.setStatus(id, "executed", "操作成功");
audit.log(uid, conv, "confirm", target, detail, true, "executed");
return result("executed", "已执行:" + op.get("sDescription"), op);
}
String msg = r.path("msg").asText("执行失败");
ops.setStatus(id, "failed", msg);
audit.log(uid, conv, "confirm", target, detail, false, msg);
return result("failed", msg, op);
} catch (Exception e) {
log.warn("confirm op {} failed", id, e);
ops.setStatus(id, "failed", e.getMessage());
audit.log(uid, conv, "confirm", target, detail, false, e.getMessage());
return result("failed", "执行异常:" + e.getMessage(), op);
}
}
/** 委托 ERP 侧暂存执行器执行(§10);执行器自行回写 ai_op_queue 状态,这里只映射结果 + 审计。 */
private Map<String, Object> confirmViaExecutor(String id, Map<String, Object> op, String authToken,
String uid, String conv, String target, String detail) {
try {
JsonNode r = erp.execStaging(authToken, id);
String st = r.path("status").asText("failed");
String msg = r.path("msg").asText("");
boolean ok = "executed".equals(st);
audit.log(uid, conv, "confirm", target, detail, ok, "execStaging:" + st + (msg.isEmpty() ? "" : (" " + msg)));
return result(ok ? "executed" : "failed",
ok ? ("已执行:" + op.get("sDescription")) : (msg.isEmpty() ? "执行失败" : msg), op);
} catch (Exception e) {
log.warn("execStaging confirm op {} failed", id, e);
audit.log(uid, conv, "confirm", target, detail, false, e.getMessage());
return result("failed", "执行异常:" + e.getMessage(), op);
}
}
/** 取消。 */
@PostMapping("/{id}/cancel")
public Map<String, Object> cancel(@PathVariable("id") String id) {
Map<String, Object> op = ops.get(id);
if (op != null && "draft".equals(String.valueOf(op.get("sStatus")))) {
ops.setStatus(id, "cancelled", "用户取消");
audit.log(str(op.get("sUserId")), str(op.get("sConversationId")), "cancel",
str(op.get("sTargetTable")) + "#" + str(op.get("sTargetBillId")),
str(op.get("sDescription")), true, "cancelled");
}
return result("cancelled", "已取消", op);
}
private Map<String, Object> result(String status, String msg, Map<String, Object> op) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("status", status);
m.put("msg", msg);
if (op != null) {
m.put("opId", op.get("sId"));
m.put("description", op.get("sDescription"));
}
return m;
}
private static String str(Object o) {
return o == null ? null : o.toString();
}
}