union-ark-web 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/PROJECT_OVERVIEW.md +405 -0
  2. package/README.md +177 -0
  3. package/ark-control/pom.xml +142 -0
  4. package/ark-control/src/local-mock/java/com/epcc/commons/securityproxy/api/SecurityResult.java +14 -0
  5. package/ark-control/src/local-mock/java/com/epcc/commons/securityproxy/api/SymmetricalSecurityService.java +11 -0
  6. package/ark-control/src/local-mock/java/com/epcc/dubbo/result/Result.java +29 -0
  7. package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/annotation/EnDecryptField.java +16 -0
  8. package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/annotation/EnDecryptFieldLong.java +17 -0
  9. package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/annotation/EnDecryptFieldWithTag.java +16 -0
  10. package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/exception/BaseDataErrorCode.java +26 -0
  11. package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/exception/CheckException.java +33 -0
  12. package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/exception/ErrorCode.java +9 -0
  13. package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/redis/RedisCacheService.java +131 -0
  14. package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/util/Constant.java +24 -0
  15. package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/util/ResultUtil.java +9 -0
  16. package/ark-control/src/local-mock/java/com/union/control/local/sensitive/LocalMockSensitiveProxy.java +40 -0
  17. package/ark-control/src/local-mock/java/com/union/control/local/sensitive/LocalRedisCacheService.java +45 -0
  18. package/ark-control/src/local-mock/java/com/union/control/local/sensitive/LocalSensitiveHostConfiguration.java +27 -0
  19. package/ark-control/src/local-mock/java/com/union/control/local/sensitive/LocalSensitiveProxyConfiguration.java +16 -0
  20. package/ark-control/src/main/java/com/union/control/ArkControlApplication.java +17 -0
  21. package/ark-control/src/main/java/com/union/control/dubbo/ProviderAccessLogFilter.java +45 -0
  22. package/ark-control/src/main/java/com/union/control/mapper/AgentExecutionMapper.java +35 -0
  23. package/ark-control/src/main/java/com/union/control/mapper/ConversationMapper.java +44 -0
  24. package/ark-control/src/main/java/com/union/control/mapper/MemoryStoreMapper.java +41 -0
  25. package/ark-control/src/main/java/com/union/control/mapper/ScheduledTaskMapper.java +96 -0
  26. package/ark-control/src/main/java/com/union/control/mapper/SensitiveAddressBookDemo.java +31 -0
  27. package/ark-control/src/main/java/com/union/control/mapper/SensitiveDataDemoMapper.java +24 -0
  28. package/ark-control/src/main/java/com/union/control/mapper/interceptor/AESInterceptor.java +265 -0
  29. package/ark-control/src/main/java/com/union/control/mapper/interceptor/AddressBookHandler.java +105 -0
  30. package/ark-control/src/main/java/com/union/control/schedule/ScheduledExecutionToken.java +55 -0
  31. package/ark-control/src/main/java/com/union/control/schedule/ScheduledTaskScheduler.java +206 -0
  32. package/ark-control/src/main/java/com/union/control/service/impl/AgentExecutionServiceImpl.java +212 -0
  33. package/ark-control/src/main/java/com/union/control/service/impl/AgentProxyServiceImpl.java +143 -0
  34. package/ark-control/src/main/java/com/union/control/service/impl/ConversationServiceImpl.java +245 -0
  35. package/ark-control/src/main/java/com/union/control/service/impl/MemoryStoreServiceImpl.java +252 -0
  36. package/ark-control/src/main/java/com/union/control/service/impl/RunningAnalysisMockServiceImpl.java +301 -0
  37. package/ark-control/src/main/java/com/union/control/service/impl/ScheduledTaskServiceImpl.java +576 -0
  38. package/ark-control/src/main/java/com/union/control/service/impl/SensitiveDataDemoServiceImpl.java +144 -0
  39. package/ark-control/src/main/java/com/union/control/service/sensitive/RedisRevealTokenStore.java +90 -0
  40. package/ark-control/src/main/java/com/union/control/service/sensitive/SensitiveRevealProcessor.java +319 -0
  41. package/ark-control/src/main/java/com/union/control/service/sensitive/SensitiveRevealServiceImpl.java +72 -0
  42. package/ark-control/src/main/java/com/union/control/utils/AgentSupport.java +103 -0
  43. package/ark-control/src/main/java/com/union/control/utils/security/SymmetricalSecurityUtils.java +160 -0
  44. package/ark-control/src/main/resources/META-INF/dubbo/com.alibaba.dubbo.rpc.Filter +1 -0
  45. package/ark-control/src/main/resources/application.properties +4 -0
  46. package/ark-control/src/main/resources/application.yml +32 -0
  47. package/ark-control/src/main/resources/dubbo-provider.xml +20 -0
  48. package/ark-control/src/main/resources/mapper/AgentExecutionMapper.xml +75 -0
  49. package/ark-control/src/main/resources/mapper/ConversationMapper.xml +110 -0
  50. package/ark-control/src/main/resources/mapper/MemoryStoreMapper.xml +74 -0
  51. package/ark-control/src/main/resources/mapper/ScheduledTaskMapper.xml +286 -0
  52. package/ark-control/src/main/resources/mapper/SensitiveDataDemoMapper.xml +55 -0
  53. package/ark-control/src/main/resources/schema.sql +146 -0
  54. package/ark-control/src/test/java/com/union/control/AgentExecutionServiceTest.java +170 -0
  55. package/ark-control/src/test/java/com/union/control/ArkControlStructureContractTest.java +107 -0
  56. package/ark-control/src/test/java/com/union/control/ConversationServiceTest.java +89 -0
  57. package/ark-control/src/test/java/com/union/control/MemoryStoreServiceTest.java +26 -0
  58. package/ark-control/src/test/java/com/union/control/PydanticAiControlContractTest.java +52 -0
  59. package/ark-control/src/test/java/com/union/control/TestJson.java +45 -0
  60. package/ark-control/src/test/java/com/union/control/dubbo/ProviderAccessLogFilterTest.java +53 -0
  61. package/ark-control/src/test/java/com/union/control/local/sensitive/LocalSensitiveProxyConfigurationTest.java +23 -0
  62. package/ark-control/src/test/java/com/union/control/mapper/interceptor/AESInterceptorCompatibilityTest.java +505 -0
  63. package/ark-control/src/test/java/com/union/control/schedule/ScheduledExecutionTokenTest.java +27 -0
  64. package/ark-control/src/test/java/com/union/control/schedule/ScheduledTaskSchedulerTest.java +51 -0
  65. package/ark-control/src/test/java/com/union/control/service/AgentProxyServiceTest.java +111 -0
  66. package/ark-control/src/test/java/com/union/control/service/ScheduledTaskServiceTest.java +483 -0
  67. package/ark-control/src/test/java/com/union/control/service/impl/SensitiveDataDemoServiceImplTest.java +104 -0
  68. package/ark-control/src/test/java/com/union/control/service/sensitive/RedisRevealTokenStoreTest.java +65 -0
  69. package/ark-control/src/test/java/com/union/control/service/sensitive/SensitiveRevealBoundaryTest.java +99 -0
  70. package/ark-control/src/test/java/com/union/control/service/sensitive/SensitiveRevealProcessorCodecTest.java +111 -0
  71. package/ark-control/src/test/java/com/union/control/service/sensitive/SensitiveRevealProcessorTest.java +111 -0
  72. package/ark-control/src/test/java/com/union/control/utils/security/SymmetricalSecurityUtilsCompatibilityTest.java +87 -0
  73. package/ark-control-facade/pom.xml +16 -0
  74. package/ark-control-facade/src/main/java/com/union/control/service/AgentExecutionService.java +8 -0
  75. package/ark-control-facade/src/main/java/com/union/control/service/AgentProxyService.java +7 -0
  76. package/ark-control-facade/src/main/java/com/union/control/service/AgentResponse.java +22 -0
  77. package/ark-control-facade/src/main/java/com/union/control/service/ConversationService.java +12 -0
  78. package/ark-control-facade/src/main/java/com/union/control/service/MemoryStoreService.java +12 -0
  79. package/ark-control-facade/src/main/java/com/union/control/service/RunningAnalysisMockService.java +10 -0
  80. package/ark-control-facade/src/main/java/com/union/control/service/ScheduledTaskService.java +40 -0
  81. package/ark-control-facade/src/main/java/com/union/control/service/SensitiveDataDemoService.java +13 -0
  82. package/ark-control-facade/src/main/java/com/union/control/service/sensitive/SensitiveRevealService.java +18 -0
  83. package/ark-web/README.md +80 -0
  84. package/ark-web/pom.xml +105 -0
  85. package/ark-web/src/local-mock/java/com/epcc/arkweb/mock/ArkAuthServiceMock.java +105 -0
  86. package/ark-web/src/local-mock/java/com/epcc/arkweb/mock/LocalKaptchaConfiguration.java +26 -0
  87. package/ark-web/src/main/java/com/epcc/arkweb/Application.java +13 -0
  88. package/ark-web/src/main/java/com/epcc/arkweb/config/InterceptorConfig.java +20 -0
  89. package/ark-web/src/main/java/com/epcc/arkweb/config/Realm.java +64 -0
  90. package/ark-web/src/main/java/com/epcc/arkweb/config/ShiroConfig.java +129 -0
  91. package/ark-web/src/main/java/com/epcc/arkweb/filter/LoginFormFilter.java +43 -0
  92. package/ark-web/src/main/java/com/epcc/arkweb/helper/AuthContextHolder.java +39 -0
  93. package/ark-web/src/main/java/com/epcc/arkweb/helper/AuthenticatedRequest.java +91 -0
  94. package/ark-web/src/main/java/com/epcc/arkweb/model/ShiroUser.java +21 -0
  95. package/ark-web/src/main/java/com/epcc/arkweb/utils/ResultMsg.java +19 -0
  96. package/ark-web/src/main/java/com/epcc/arkweb/vo/llm/ScheduledTaskCommandVO.java +30 -0
  97. package/ark-web/src/main/java/com/epcc/arkweb/vo/llm/ScheduledTaskQueryVO.java +20 -0
  98. package/ark-web/src/main/java/com/epcc/arkweb/web/CommonController.java +25 -0
  99. package/ark-web/src/main/java/com/epcc/arkweb/web/llm/AgentAuthorizationInterceptor.java +229 -0
  100. package/ark-web/src/main/java/com/epcc/arkweb/web/llm/AgentController.java +165 -0
  101. package/ark-web/src/main/java/com/epcc/arkweb/web/llm/AgentPermission.java +13 -0
  102. package/ark-web/src/main/java/com/epcc/arkweb/web/llm/LlmController.java +209 -0
  103. package/ark-web/src/main/java/com/epcc/arkweb/web/llm/ScheduledTaskController.java +160 -0
  104. package/ark-web/src/main/java/com/epcc/arkweb/web/sensitive/reveal/SensitiveCaptchaService.java +146 -0
  105. package/ark-web/src/main/java/com/epcc/arkweb/web/sensitive/reveal/SensitiveRevealController.java +163 -0
  106. package/ark-web/src/main/resources/application.properties +1 -0
  107. package/ark-web/src/main/resources/application.yml +23 -0
  108. package/ark-web/src/main/resources/dubbo-consumer.xml +19 -0
  109. package/ark-web/src/production-overlay/java/com/epcc/arkweb/config/CasShiroConfig.java +229 -0
  110. package/ark-web/src/production-overlay/java/com/epcc/arkweb/config/InterceptorConfig.java +35 -0
  111. package/ark-web/src/production-overlay/java/com/epcc/arkweb/config/ShiroConfig.java +161 -0
  112. package/ark-web/src/test/java/com/epcc/arkweb/ArkWebStructureContractTest.java +290 -0
  113. package/ark-web/src/test/java/com/epcc/arkweb/ShiroTestSupport.java +32 -0
  114. package/ark-web/src/test/java/com/epcc/arkweb/config/LocalShiroIntegrationTest.java +121 -0
  115. package/ark-web/src/test/java/com/epcc/arkweb/helper/AuthenticatedRequestTest.java +131 -0
  116. package/ark-web/src/test/java/com/epcc/arkweb/local/LocalArkAuthServiceMockTest.java +17 -0
  117. package/ark-web/src/test/java/com/epcc/arkweb/web/llm/AgentAuthorizationInterceptorTest.java +134 -0
  118. package/ark-web/src/test/java/com/epcc/arkweb/web/llm/AgentControllerAuthorizationTest.java +97 -0
  119. package/ark-web/src/test/java/com/epcc/arkweb/web/llm/LlmControllerTest.java +113 -0
  120. package/ark-web/src/test/java/com/epcc/arkweb/web/sensitive/reveal/SensitiveRevealControllerTest.java +273 -0
  121. package/package.json +18 -0
  122. package/pom.xml +16 -0
@@ -0,0 +1,209 @@
1
+ package com.epcc.arkweb.web.llm;
2
+
3
+ import com.union.control.service.AgentProxyService;
4
+ import com.union.control.service.AgentResponse;
5
+ import com.union.control.service.AgentExecutionService;
6
+ import com.union.control.service.ConversationService;
7
+ import com.epcc.arkweb.helper.AuthenticatedRequest;
8
+ import org.apache.shiro.authz.annotation.RequiresPermissions;
9
+
10
+ import org.springframework.beans.factory.annotation.Autowired;
11
+ import org.springframework.beans.factory.annotation.Value;
12
+ import org.springframework.http.ResponseEntity;
13
+ import org.springframework.http.HttpStatus;
14
+ import org.springframework.http.HttpMethod;
15
+ import org.springframework.http.MediaType;
16
+ import org.springframework.http.client.ClientHttpResponse;
17
+ import org.springframework.http.client.SimpleClientHttpRequestFactory;
18
+ import org.springframework.stereotype.Controller;
19
+ import org.springframework.web.client.ResponseErrorHandler;
20
+ import org.springframework.web.client.RestTemplate;
21
+ import org.springframework.web.bind.annotation.GetMapping;
22
+ import org.springframework.web.bind.annotation.PostMapping;
23
+ import org.springframework.web.bind.annotation.RequestBody;
24
+ import org.springframework.web.bind.annotation.RequestParam;
25
+ import org.springframework.web.bind.annotation.RequestHeader;
26
+ import org.springframework.web.bind.annotation.RequestMapping;
27
+ import org.springframework.http.HttpHeaders;
28
+ import org.springframework.web.bind.annotation.ResponseBody;
29
+
30
+ import javax.servlet.http.HttpServletResponse;
31
+ import java.io.IOException;
32
+ import java.io.InputStream;
33
+ import java.nio.charset.StandardCharsets;
34
+ import java.util.Map;
35
+ import java.util.concurrent.atomic.AtomicBoolean;
36
+
37
+ /** Browser-facing LLM API. Only Agent runs cross the Python boundary. */
38
+ @Controller
39
+ @RequestMapping(value = {"llm", "union-op/llm"})
40
+ public class LlmController {
41
+ private final ConversationService conversations;
42
+ private final AgentExecutionService executions;
43
+ private final AgentProxyService gateway;
44
+ private final AuthenticatedRequest request;
45
+ private final String pyAppBaseUrl;
46
+ private final RestTemplate http;
47
+
48
+ @Autowired
49
+ public LlmController(
50
+ ConversationService conversations,
51
+ AgentExecutionService executions,
52
+ AgentProxyService gateway,
53
+ AuthenticatedRequest request,
54
+ @Value("${agent.py-app-base-url}") String pyAppBaseUrl,
55
+ @Value("${AGENT_MAX_RUN_SECONDS:900}") double maxRunSeconds) {
56
+ this(conversations, executions, gateway, request, pyAppBaseUrl, new RestTemplate());
57
+ double millis = Math.ceil(maxRunSeconds * 1000);
58
+ if (!Double.isFinite(millis) || millis < 1 || millis > Integer.MAX_VALUE)
59
+ throw new IllegalArgumentException("AGENT_MAX_RUN_SECONDS is out of range");
60
+ SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
61
+ factory.setConnectTimeout((int) millis);
62
+ factory.setReadTimeout((int) millis);
63
+ http.setRequestFactory(factory);
64
+ }
65
+
66
+ LlmController(
67
+ ConversationService conversations,
68
+ AgentExecutionService executions,
69
+ AgentProxyService gateway,
70
+ AuthenticatedRequest request,
71
+ String pyAppBaseUrl,
72
+ RestTemplate http) {
73
+ this.conversations = conversations;
74
+ this.executions = executions;
75
+ this.gateway = gateway;
76
+ this.request = request;
77
+ this.pyAppBaseUrl = pyAppBaseUrl.replaceAll("/+$", "");
78
+ this.http = http;
79
+ this.http.setErrorHandler(new ResponseErrorHandler() {
80
+ @Override
81
+ public boolean hasError(ClientHttpResponse response) {
82
+ return false;
83
+ }
84
+
85
+ @Override
86
+ public void handleError(ClientHttpResponse response) {}
87
+ });
88
+ }
89
+
90
+ @GetMapping("/conversationList")
91
+ @RequiresPermissions(value = "/assistantManager/page")
92
+ @ResponseBody
93
+ public Map<String, Object> conversationList(
94
+ @RequestParam(required = false, defaultValue = "100") int limit) {
95
+ return conversations.conversations(request.json("limit", limit));
96
+ }
97
+
98
+ @GetMapping("/conversationDetails")
99
+ @RequiresPermissions(value = "/assistantManager/page")
100
+ @ResponseBody
101
+ public Map<String, Object> conversationDetails(
102
+ @RequestParam String conversationId) {
103
+ return conversations.conversation(request.json("conversationId", conversationId));
104
+ }
105
+
106
+ @PostMapping("/conversationTitle")
107
+ @RequiresPermissions(value = "/assistantManager/page")
108
+ @ResponseBody
109
+ public Map<String, Object> conversationTitle(
110
+ @RequestBody Map<String, Object> payload) {
111
+ return conversations.rename(request.json(payload));
112
+ }
113
+
114
+ @PostMapping("/conversationDelete")
115
+ @RequiresPermissions(value = "/assistantManager/page")
116
+ @ResponseBody
117
+ public Map<String, Object> conversationDelete(
118
+ @RequestBody Map<String, Object> payload) {
119
+ return conversations.deleteConversation(request.json(payload));
120
+ }
121
+
122
+ @PostMapping("/executionCancel")
123
+ @RequiresPermissions(value = "/assistantManager/page")
124
+ @ResponseBody
125
+ public ResponseEntity<String> executionCancel(
126
+ @RequestHeader(value = HttpHeaders.COOKIE, required = false) String cookie,
127
+ @RequestBody Map<String, Object> payload) {
128
+ Object conversationId = payload.get("conversationId");
129
+ Object runId = payload.get("runId");
130
+ if (!(conversationId instanceof String) || !(runId instanceof String))
131
+ throw new IllegalArgumentException("缺少 execution 标识");
132
+ return response(gateway.cancel(cookie, (String) conversationId, (String) runId));
133
+ }
134
+
135
+ @PostMapping(value = "/chatMessage", produces = "text/event-stream")
136
+ @RequiresPermissions(value = "/assistantManager/page")
137
+ public void chatMessage(
138
+ @RequestHeader(value = HttpHeaders.COOKIE, required = false) String cookie,
139
+ @RequestBody byte[] payload, HttpServletResponse response) {
140
+ byte[] clientPayload = request.clientPayload(payload);
141
+ Map<String, Object> claimed = executions.claimAguiRun(request.json(clientPayload));
142
+ @SuppressWarnings("unchecked")
143
+ Map<String, Object> execution = (Map<String, Object>) claimed.get("data");
144
+ String conversationId = String.valueOf(execution.get("conversationId"));
145
+ String runId = String.valueOf(execution.get("runId"));
146
+ AtomicBoolean disconnected = new AtomicBoolean();
147
+ try {
148
+ http.execute(pyAppBaseUrl + "/agent/v1/runs", HttpMethod.POST,
149
+ upstreamRequest -> {
150
+ upstreamRequest.getHeaders().setContentType(MediaType.APPLICATION_JSON);
151
+ upstreamRequest.getHeaders().setAccept(
152
+ java.util.Collections.singletonList(MediaType.TEXT_EVENT_STREAM));
153
+ upstreamRequest.getHeaders().set(HttpHeaders.COOKIE, cookie);
154
+ upstreamRequest.getBody().write(clientPayload);
155
+ }, upstream -> {
156
+ response.setStatus(upstream.getRawStatusCode());
157
+ copyHeader(upstream, response, HttpHeaders.CONTENT_TYPE);
158
+ copyHeader(upstream, response, HttpHeaders.CACHE_CONTROL);
159
+ response.setHeader("X-Accel-Buffering", "no");
160
+ InputStream input = upstream.getBody();
161
+ byte[] buffer = new byte[4096];
162
+ int read;
163
+ while ((read = input.read(buffer)) != -1) {
164
+ try {
165
+ response.getOutputStream().write(buffer, 0, read);
166
+ response.getOutputStream().flush();
167
+ } catch (IOException error) {
168
+ disconnected.set(true);
169
+ throw error;
170
+ }
171
+ }
172
+ try {
173
+ response.flushBuffer();
174
+ } catch (IOException error) {
175
+ disconnected.set(true);
176
+ throw error;
177
+ }
178
+ return null;
179
+ }
180
+ );
181
+ } catch (RuntimeException error) {
182
+ if (!disconnected.get()) throw error;
183
+ gateway.cancel(cookie, conversationId, runId);
184
+ }
185
+ }
186
+
187
+ @PostMapping("/chatMessageSync")
188
+ @RequiresPermissions(value = "/assistantManager/page")
189
+ @ResponseBody
190
+ public ResponseEntity<String> chatMessageSync(
191
+ @RequestHeader(value = HttpHeaders.COOKIE, required = false) String cookie,
192
+ @RequestBody byte[] payload) {
193
+ return response(gateway.sync(cookie,
194
+ new String(request.clientPayload(payload), StandardCharsets.UTF_8)));
195
+ }
196
+
197
+ private static ResponseEntity<String> response(AgentResponse response) {
198
+ return ResponseEntity.status(HttpStatus.valueOf(response.getStatus()))
199
+ .contentType(MediaType.APPLICATION_JSON)
200
+ .body(response.getBody());
201
+ }
202
+
203
+ private static void copyHeader(
204
+ ClientHttpResponse upstream, HttpServletResponse response, String name) {
205
+ String value = upstream.getHeaders().getFirst(name);
206
+ if (value != null) response.setHeader(name, value);
207
+ }
208
+
209
+ }
@@ -0,0 +1,160 @@
1
+ package com.epcc.arkweb.web.llm;
2
+
3
+ import com.epcc.arkweb.helper.AuthenticatedRequest;
4
+ import com.union.control.service.ScheduledTaskService;
5
+ import com.epcc.arkweb.utils.ResultMsg;
6
+ import com.epcc.arkweb.vo.llm.ScheduledTaskCommandVO;
7
+ import com.epcc.arkweb.vo.llm.ScheduledTaskQueryVO;
8
+ import org.apache.shiro.authz.annotation.RequiresPermissions;
9
+ import org.springframework.beans.factory.annotation.Autowired;
10
+ import org.springframework.stereotype.Controller;
11
+ import org.springframework.web.bind.annotation.RequestBody;
12
+ import org.springframework.web.bind.annotation.RequestMapping;
13
+ import org.springframework.web.bind.annotation.RequestMethod;
14
+ import org.springframework.web.bind.annotation.RequestParam;
15
+ import org.springframework.web.bind.annotation.ResponseBody;
16
+
17
+ import java.util.LinkedHashMap;
18
+ import java.util.Map;
19
+
20
+ @Controller
21
+ @RequestMapping(value = {"llm", "union-op/llm"})
22
+ public class ScheduledTaskController {
23
+
24
+ @Autowired
25
+ private ScheduledTaskService scheduledTaskService;
26
+
27
+ @Autowired
28
+ private AuthenticatedRequest request;
29
+
30
+ @RequestMapping(value = "/scheduledTaskCreate", method = RequestMethod.POST)
31
+ @RequiresPermissions(value = "/assistantManager/page")
32
+ @ResponseBody
33
+ public ResultMsg create(@RequestBody ScheduledTaskCommandVO command) {
34
+ return response(scheduledTaskService.create(request.json(command(command))));
35
+ }
36
+
37
+ @RequestMapping(value = "/scheduledTaskUpdate", method = RequestMethod.POST)
38
+ @RequiresPermissions(value = "/assistantManager/page")
39
+ @ResponseBody
40
+ public ResultMsg update(@RequestBody ScheduledTaskCommandVO command) {
41
+ return response(scheduledTaskService.update(request.json(command(command))));
42
+ }
43
+
44
+ @RequestMapping(value = "/scheduledTaskList", method = {RequestMethod.GET, RequestMethod.POST})
45
+ @RequiresPermissions(value = "/assistantManager/page")
46
+ @ResponseBody
47
+ public ResultMsg list(ScheduledTaskQueryVO query) {
48
+ return response(scheduledTaskService.list(request.json(
49
+ "keyword", query.getKeyword(), "status", query.getStatus(),
50
+ "page", page(query.getPage()), "pageSize", pageSize(query.getPageSize()))));
51
+ }
52
+
53
+ @RequestMapping(value = "/scheduledTaskDetail", method = RequestMethod.GET)
54
+ @RequiresPermissions(value = "/assistantManager/page")
55
+ @ResponseBody
56
+ public ResultMsg detail(@RequestParam Long taskId) {
57
+ return response(scheduledTaskService.detail(request.json(
58
+ "taskId", positive(taskId, "taskId"))));
59
+ }
60
+
61
+ @RequestMapping(value = "/scheduledTaskRunList", method = {RequestMethod.GET, RequestMethod.POST})
62
+ @RequiresPermissions(value = "/assistantManager/page")
63
+ @ResponseBody
64
+ public ResultMsg runList(ScheduledTaskQueryVO query) {
65
+ query.setTaskId(positive(query.getTaskId(), "taskId"));
66
+ return response(scheduledTaskService.runs(request.json(
67
+ "taskId", query.getTaskId(), "page", page(query.getPage()),
68
+ "pageSize", pageSize(query.getPageSize()))));
69
+ }
70
+
71
+ @RequestMapping(value = "/scheduledTaskRunDetail", method = RequestMethod.GET)
72
+ @RequiresPermissions(value = "/assistantManager/page")
73
+ @ResponseBody
74
+ public ResultMsg runDetail(@RequestParam Long runId) {
75
+ return response(scheduledTaskService.runDetail(request.json(
76
+ "runId", positive(runId, "runId"))));
77
+ }
78
+
79
+ @RequestMapping(value = "/scheduledTaskUnread", method = RequestMethod.GET)
80
+ @RequiresPermissions(value = "/assistantManager/page")
81
+ @ResponseBody
82
+ public ResultMsg unread() {
83
+ return response(scheduledTaskService.unread(request.json()));
84
+ }
85
+
86
+ @RequestMapping(value = "/scheduledTaskStart", method = RequestMethod.POST)
87
+ @RequiresPermissions(value = "/assistantManager/page")
88
+ @ResponseBody
89
+ public ResultMsg start(@RequestBody IdCommand command) {
90
+ return response(scheduledTaskService.start(request.json(
91
+ "taskId", positive(command.getTaskId(), "taskId"))));
92
+ }
93
+
94
+ @RequestMapping(value = "/scheduledTaskPause", method = RequestMethod.POST)
95
+ @RequiresPermissions(value = "/assistantManager/page")
96
+ @ResponseBody
97
+ public ResultMsg pause(@RequestBody IdCommand command) {
98
+ return response(scheduledTaskService.pause(request.json(
99
+ "taskId", positive(command.getTaskId(), "taskId"))));
100
+ }
101
+
102
+ @RequestMapping(value = "/scheduledTaskDiscard", method = RequestMethod.POST)
103
+ @RequiresPermissions(value = "/assistantManager/page")
104
+ @ResponseBody
105
+ public ResultMsg discard(@RequestBody IdCommand command) {
106
+ return response(scheduledTaskService.discard(request.json(
107
+ "taskId", positive(command.getTaskId(), "taskId"))));
108
+ }
109
+
110
+ @RequestMapping(value = "/scheduledTaskRunOpen", method = RequestMethod.POST)
111
+ @RequiresPermissions(value = "/assistantManager/page")
112
+ @ResponseBody
113
+ public ResultMsg open(@RequestBody IdCommand command) {
114
+ return response(scheduledTaskService.open(request.json(
115
+ "runId", positive(command.getRunId(), "runId"))));
116
+ }
117
+
118
+ private static Long positive(Long value, String name) {
119
+ if (value == null || value <= 0L) {
120
+ throw new IllegalArgumentException(name + " 非法");
121
+ }
122
+ return value;
123
+ }
124
+
125
+ private static int page(Integer value) { return value == null ? 1 : value; }
126
+ private static int pageSize(Integer value) { return value == null ? 20 : value; }
127
+
128
+ private static Map<String, Object> command(ScheduledTaskCommandVO value) {
129
+ if (value == null) throw new IllegalArgumentException("请求参数非法");
130
+ Map<String, Object> command = new LinkedHashMap<>();
131
+ command.put("taskId", value.getTaskId());
132
+ command.put("title", value.getTitle());
133
+ command.put("prompt", value.getPrompt());
134
+ command.put("scheduleType", value.getScheduleType());
135
+ command.put("runAt", value.getRunAt());
136
+ command.put("cronExpression", value.getCronExpression());
137
+ command.put("intervalSeconds", value.getIntervalSeconds());
138
+ command.put("timezone", value.getTimezone());
139
+ return command;
140
+ }
141
+
142
+ private static ResultMsg response(Map<String, Object> result) {
143
+ if (result == null || !Boolean.TRUE.equals(result.get("success"))) {
144
+ return ResultMsg.fail(result == null ? "定时任务服务不可用"
145
+ : String.valueOf(result.get("errorMsg")));
146
+ }
147
+ return ResultMsg.ok(result.get("data"));
148
+ }
149
+
150
+ public static class IdCommand {
151
+ private Long taskId;
152
+ private Long runId;
153
+
154
+ public Long getTaskId() { return taskId; }
155
+ public void setTaskId(Long taskId) { this.taskId = taskId; }
156
+ public Long getRunId() { return runId; }
157
+ public void setRunId(Long runId) { this.runId = runId; }
158
+ }
159
+
160
+ }
@@ -0,0 +1,146 @@
1
+ package com.epcc.arkweb.web.sensitive.reveal;
2
+
3
+ import com.google.code.kaptcha.Producer;
4
+ import org.apache.shiro.session.Session;
5
+ import org.slf4j.Logger;
6
+ import org.slf4j.LoggerFactory;
7
+ import org.springframework.beans.factory.annotation.Autowired;
8
+ import org.springframework.http.HttpStatus;
9
+ import org.springframework.stereotype.Service;
10
+
11
+ import javax.imageio.ImageIO;
12
+ import java.io.ByteArrayOutputStream;
13
+ import java.io.IOException;
14
+ import java.io.Serializable;
15
+ import java.nio.charset.StandardCharsets;
16
+ import java.security.MessageDigest;
17
+ import java.security.NoSuchAlgorithmException;
18
+ import java.time.Clock;
19
+ import java.util.Base64;
20
+ import java.util.UUID;
21
+ import java.util.regex.Pattern;
22
+
23
+ /** Web-only challenge; uses the existing login session, never the reveal token store. */
24
+ @Service
25
+ public class SensitiveCaptchaService {
26
+ private static final Logger LOG = LoggerFactory.getLogger(SensitiveCaptchaService.class);
27
+ private static final String SESSION_KEY = SensitiveCaptchaService.class.getName() + ".challenge";
28
+ private static final long TTL_MILLIS = 120000L;
29
+ private static final Pattern TOKEN = Pattern.compile("rt_[A-Za-z0-9_-]{43}");
30
+ private final Producer producer;
31
+ private final Clock clock;
32
+ // ponytail: short session updates share a JVM lock; multi-node one-use needs sticky sessions.
33
+ private final Object sessionLock = new Object();
34
+
35
+ @Autowired
36
+ public SensitiveCaptchaService(Producer producer) {
37
+ this(producer, Clock.systemUTC());
38
+ }
39
+
40
+ SensitiveCaptchaService(Producer producer, Clock clock) {
41
+ this.producer = producer;
42
+ this.clock = clock;
43
+ }
44
+
45
+ public byte[] generate(Session session, String userId, String token) {
46
+ long started = System.nanoTime();
47
+ String sessionRef = sessionRef(session);
48
+ if (token == null || !TOKEN.matcher(token).matches()) {
49
+ LOG.info("Sensitive captcha generation rejected session_ref={} reason=invalid_token", sessionRef);
50
+ throw new CaptchaException("INVALID_TOKEN", HttpStatus.BAD_REQUEST);
51
+ }
52
+ if (session == null) {
53
+ LOG.info("Sensitive captcha generation rejected session_ref=none reason=no_session");
54
+ throw new CaptchaException("CAPTCHA_REQUIRED", HttpStatus.FORBIDDEN);
55
+ }
56
+ synchronized (sessionLock) {
57
+ session.removeAttribute(SESSION_KEY);
58
+ }
59
+ String answer;
60
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
61
+ try {
62
+ answer = producer.createText();
63
+ if (!ImageIO.write(producer.createImage(answer), "png", output)) {
64
+ throw new IOException("PNG encoder unavailable");
65
+ }
66
+ } catch (IOException | RuntimeException error) {
67
+ LOG.info("Sensitive captcha generation failed session_ref={} error_type={}",
68
+ sessionRef, error.getClass().getSimpleName());
69
+ throw new CaptchaException("CAPTCHA_UNAVAILABLE", HttpStatus.SERVICE_UNAVAILABLE);
70
+ }
71
+ Challenge challenge = new Challenge(userId, token, answer, clock.millis() + TTL_MILLIS);
72
+ synchronized (sessionLock) {
73
+ session.setAttribute(SESSION_KEY, challenge);
74
+ }
75
+ LOG.info("Sensitive captcha generated session_ref={} challenge_ref={} ttl_seconds=120 elapsed_ms={}",
76
+ sessionRef, challenge.id, (System.nanoTime() - started) / 1000000);
77
+ return output.toByteArray();
78
+ }
79
+
80
+ public void verify(Session session, String userId, String token, String answer) {
81
+ String sessionRef = sessionRef(session);
82
+ Object stored;
83
+ synchronized (sessionLock) {
84
+ stored = session == null ? null : session.removeAttribute(SESSION_KEY);
85
+ }
86
+ if (!(stored instanceof Challenge)) {
87
+ LOG.info("Sensitive captcha verification rejected session_ref={} reason={}",
88
+ sessionRef, session == null ? "no_session" : "challenge_missing");
89
+ throw new CaptchaException("CAPTCHA_REQUIRED", HttpStatus.FORBIDDEN);
90
+ }
91
+ Challenge challenge = (Challenge) stored;
92
+ if (clock.millis() >= challenge.expiresAt) {
93
+ LOG.info("Sensitive captcha verification rejected session_ref={} challenge_ref={} reason=expired",
94
+ sessionRef, challenge.id);
95
+ throw new CaptchaException("CAPTCHA_EXPIRED", HttpStatus.FORBIDDEN);
96
+ }
97
+ String reason = !challenge.userId.equals(userId) ? "user_mismatch"
98
+ : !challenge.token.equals(token) ? "token_mismatch"
99
+ : answer == null || answer.trim().isEmpty() ? "answer_missing"
100
+ : answer.length() > 32 || !challenge.answer.equalsIgnoreCase(answer.trim()) ? "answer_incorrect" : null;
101
+ if (reason != null) {
102
+ LOG.info("Sensitive captcha verification rejected session_ref={} challenge_ref={} reason={}",
103
+ sessionRef, challenge.id, reason);
104
+ throw new CaptchaException("CAPTCHA_INVALID", HttpStatus.FORBIDDEN);
105
+ }
106
+ LOG.info("Sensitive captcha verification passed session_ref={} challenge_ref={}", sessionRef, challenge.id);
107
+ }
108
+
109
+ /** A non-credential correlation value; never log the login session ID itself. */
110
+ static String sessionRef(Session session) {
111
+ if (session == null || session.getId() == null) return "none";
112
+ try {
113
+ byte[] digest = MessageDigest.getInstance("SHA-256")
114
+ .digest(session.getId().toString().getBytes(StandardCharsets.UTF_8));
115
+ return Base64.getUrlEncoder().withoutPadding().encodeToString(digest).substring(0, 16);
116
+ } catch (NoSuchAlgorithmException impossible) {
117
+ throw new IllegalStateException(impossible);
118
+ }
119
+ }
120
+
121
+ private static final class Challenge implements Serializable {
122
+ private static final long serialVersionUID = 1L;
123
+ final String id = UUID.randomUUID().toString();
124
+ final String userId;
125
+ final String token;
126
+ final String answer;
127
+ final long expiresAt;
128
+
129
+ Challenge(String userId, String token, String answer, long expiresAt) {
130
+ this.userId = userId;
131
+ this.token = token;
132
+ this.answer = answer;
133
+ this.expiresAt = expiresAt;
134
+ }
135
+ }
136
+
137
+ public static final class CaptchaException extends RuntimeException {
138
+ private static final long serialVersionUID = 1L;
139
+ final HttpStatus status;
140
+
141
+ CaptchaException(String code, HttpStatus status) {
142
+ super(code);
143
+ this.status = status;
144
+ }
145
+ }
146
+ }
@@ -0,0 +1,163 @@
1
+ package com.epcc.arkweb.web.sensitive.reveal;
2
+
3
+ import com.epcc.arkweb.helper.AuthenticatedRequest;
4
+ import com.union.control.service.SensitiveDataDemoService;
5
+ import com.union.control.service.sensitive.SensitiveRevealService;
6
+ import org.apache.shiro.SecurityUtils;
7
+ import org.apache.shiro.authz.annotation.RequiresPermissions;
8
+ import org.slf4j.Logger;
9
+ import org.slf4j.LoggerFactory;
10
+ import org.springframework.beans.factory.annotation.Value;
11
+ import org.springframework.http.CacheControl;
12
+ import org.springframework.http.HttpHeaders;
13
+ import org.springframework.http.HttpStatus;
14
+ import org.springframework.http.MediaType;
15
+ import org.springframework.http.ResponseEntity;
16
+ import org.springframework.web.bind.annotation.ExceptionHandler;
17
+ import org.springframework.web.bind.annotation.GetMapping;
18
+ import org.springframework.web.bind.annotation.PostMapping;
19
+ import org.springframework.web.bind.annotation.RequestBody;
20
+ import org.springframework.web.bind.annotation.RequestMapping;
21
+ import org.springframework.web.bind.annotation.RestController;
22
+
23
+ import java.util.Collections;
24
+ import java.util.List;
25
+ import java.util.Map;
26
+ import java.util.LinkedHashMap;
27
+ import java.util.UUID;
28
+
29
+ @RestController
30
+ @RequestMapping("/api/sensitive/reveal")
31
+ public class SensitiveRevealController {
32
+ private static final Logger LOG = LoggerFactory.getLogger(SensitiveRevealController.class);
33
+ private final SensitiveRevealService service;
34
+ private final SensitiveDataDemoService demoService;
35
+ private final AuthenticatedRequest request;
36
+ private final SensitiveCaptchaService captcha;
37
+ private final boolean strictMode;
38
+
39
+ public SensitiveRevealController(SensitiveRevealService service,
40
+ SensitiveDataDemoService demoService, AuthenticatedRequest request,
41
+ SensitiveCaptchaService captcha,
42
+ @Value("${sensitive.reveal.strict_mode:false}") boolean strictMode) {
43
+ this.service = service;
44
+ this.demoService = demoService;
45
+ this.request = request;
46
+ this.captcha = captcha;
47
+ this.strictMode = strictMode;
48
+ }
49
+
50
+ @GetMapping("/options")
51
+ @RequiresPermissions(value = "/assistantManager/page")
52
+ public ResponseEntity<Map<String, Boolean>> options() {
53
+ request.actor();
54
+ return ResponseEntity.ok().cacheControl(CacheControl.noStore())
55
+ .body(Collections.singletonMap("strictMode", strictMode));
56
+ }
57
+
58
+ @PostMapping("/captcha")
59
+ @RequiresPermissions(value = "/assistantManager/page")
60
+ public ResponseEntity<byte[]> captcha(@RequestBody Map<String, Object> payload) {
61
+ String userId = request.actor().getLoginName();
62
+ if (!strictMode) {
63
+ LOG.info("Sensitive captcha request rejected reason=strict_mode_disabled");
64
+ throw new SensitiveCaptchaService.CaptchaException("STRICT_MODE_DISABLED", HttpStatus.CONFLICT);
65
+ }
66
+ byte[] png = captcha.generate(SecurityUtils.getSubject().getSession(false),
67
+ userId, string(payload, "token"));
68
+ return ResponseEntity.ok().cacheControl(CacheControl.noStore())
69
+ .header(HttpHeaders.PRAGMA, "no-cache").contentType(MediaType.IMAGE_PNG).body(png);
70
+ }
71
+
72
+ @PostMapping
73
+ @RequiresPermissions(value = "/assistantManager/page")
74
+ public ResponseEntity<Map<String, String>> reveal(@RequestBody Map<String, Object> payload) {
75
+ String requestId = UUID.randomUUID().toString();
76
+ long started = System.nanoTime();
77
+ String stage = "captcha";
78
+ LOG.info("Sensitive reveal received request_id={} strict_mode={} session_ref={}", requestId, strictMode,
79
+ SensitiveCaptchaService.sessionRef(SecurityUtils.getSubject().getSession(false)));
80
+ try {
81
+ if (strictMode) {
82
+ captcha.verify(SecurityUtils.getSubject().getSession(false), request.actor().getLoginName(),
83
+ string(payload, "token"), string(payload, "captchaCode"));
84
+ }
85
+ stage = "control";
86
+ LOG.info("Sensitive reveal calling Control request_id={}", requestId);
87
+ // Only token and server-generated trace metadata cross Dubbo; the answer stays in Web.
88
+ Map<String, Object> input = new LinkedHashMap<>();
89
+ input.put("token", payload.get("token"));
90
+ input.put("revealRequestId", requestId);
91
+ String plaintext = service.reveal(request.json(input));
92
+ LOG.info("Sensitive reveal completed request_id={} elapsed_ms={}",
93
+ requestId, (System.nanoTime() - started) / 1000000);
94
+ return ResponseEntity.ok().cacheControl(CacheControl.noStore())
95
+ .header(HttpHeaders.PRAGMA, "no-cache")
96
+ .body(Collections.singletonMap("value", plaintext));
97
+ } catch (RuntimeException error) {
98
+ String reason = error instanceof SensitiveCaptchaService.CaptchaException
99
+ ? error.getMessage() : error.getClass().getSimpleName();
100
+ LOG.info("Sensitive reveal rejected request_id={} stage={} reason={} elapsed_ms={}",
101
+ requestId, stage, reason, (System.nanoTime() - started) / 1000000);
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ @ExceptionHandler(SensitiveCaptchaService.CaptchaException.class)
107
+ public ResponseEntity<Map<String, String>> captchaError(SensitiveCaptchaService.CaptchaException error) {
108
+ return ResponseEntity.status(error.status).cacheControl(CacheControl.noStore())
109
+ .header(HttpHeaders.PRAGMA, "no-cache").contentType(MediaType.APPLICATION_JSON)
110
+ .body(Collections.singletonMap("code", error.getMessage()));
111
+ }
112
+
113
+ private static String string(Map<String, Object> payload, String key) {
114
+ Object value = payload.get(key);
115
+ return value instanceof String ? (String) value : null;
116
+ }
117
+
118
+ @PostMapping("/demo/save")
119
+ @RequiresPermissions(value = "/assistantManager/page")
120
+ public ResponseEntity<Map<String, Object>> saveDemo(@RequestBody Map<String, Object> payload) {
121
+ return ResponseEntity.ok().cacheControl(CacheControl.noStore())
122
+ .body(demoService.save(request.json(payload)));
123
+ }
124
+
125
+ @PostMapping("/demo/address-book/save")
126
+ @RequiresPermissions(value = "/assistantManager/page")
127
+ public ResponseEntity<Map<String, Object>> saveAddressBookDemo(@RequestBody Map<String, Object> payload) {
128
+ return ResponseEntity.ok().cacheControl(CacheControl.noStore())
129
+ .body(demoService.saveAddressBook(request.json(payload)));
130
+ }
131
+
132
+ @ExceptionHandler(SensitiveRevealService.ExpiredTokenException.class)
133
+ public ResponseEntity<Map<String, String>> expiredToken(SensitiveRevealService.ExpiredTokenException error) {
134
+ return ResponseEntity.status(HttpStatus.GONE).cacheControl(CacheControl.noStore())
135
+ .body(Collections.singletonMap("code", "TOKEN_EXPIRED"));
136
+ }
137
+
138
+ @PostMapping("/demo/insert")
139
+ @RequiresPermissions(value = "/assistantManager/page")
140
+ public Map<String, Integer> insertDemo(@RequestBody Map<String, Object> payload) {
141
+ return Collections.singletonMap("affectedRows", demoService.insert(request.json(payload)));
142
+ }
143
+
144
+ @GetMapping("/demo/query")
145
+ @RequiresPermissions(value = "/assistantManager/page")
146
+ public List<Map<String, Object>> queryDemo() {
147
+ return demoService.query(request.json());
148
+ }
149
+
150
+ @PostMapping("/demo/address-book/insert")
151
+ @RequiresPermissions(value = "/assistantManager/page")
152
+ public Map<String, Integer> insertAddressBookDemo(
153
+ @RequestBody Map<String, Object> payload) {
154
+ return Collections.singletonMap("affectedRows",
155
+ demoService.insertAddressBook(request.json(payload)));
156
+ }
157
+
158
+ @GetMapping("/demo/address-book/query")
159
+ @RequiresPermissions(value = "/assistantManager/page")
160
+ public List<Map<String, Object>> queryAddressBookDemo() {
161
+ return demoService.queryAddressBook(request.json());
162
+ }
163
+ }
@@ -0,0 +1 @@
1
+ sensitive.reveal.strict_mode=${SENSITIVE_REVEAL_STRICT_MODE:true}