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,576 @@
1
+ package com.union.control.service.impl;
2
+
3
+ import com.fasterxml.jackson.core.type.TypeReference;
4
+ import com.fasterxml.jackson.databind.ObjectMapper;
5
+ import com.union.control.mapper.ScheduledTaskMapper;
6
+ import com.union.control.service.ScheduledTaskService;
7
+ import com.union.control.service.ScheduledTaskService.ConflictException;
8
+ import com.union.control.service.ScheduledTaskService.DataCorruptionException;
9
+ import com.union.control.service.ScheduledTaskService.NotFoundException;
10
+ import com.union.control.utils.AgentSupport;
11
+ import com.union.control.schedule.ScheduledExecutionToken;
12
+ import org.springframework.beans.factory.annotation.Autowired;
13
+ import org.springframework.scheduling.support.CronSequenceGenerator;
14
+ import org.springframework.stereotype.Service;
15
+ import org.springframework.transaction.annotation.Isolation;
16
+ import org.springframework.transaction.annotation.Transactional;
17
+
18
+ import java.sql.Timestamp;
19
+ import java.time.Duration;
20
+ import java.time.Instant;
21
+ import java.time.LocalDateTime;
22
+ import java.time.OffsetDateTime;
23
+ import java.time.ZoneId;
24
+ import java.time.ZoneOffset;
25
+ import java.time.format.DateTimeFormatter;
26
+ import java.time.format.DateTimeParseException;
27
+ import java.util.ArrayList;
28
+ import java.util.Arrays;
29
+ import java.util.Collections;
30
+ import java.util.Date;
31
+ import java.util.LinkedHashMap;
32
+ import java.util.List;
33
+ import java.util.Locale;
34
+ import java.util.Map;
35
+ import java.util.TimeZone;
36
+
37
+ @Service("scheduledTaskService")
38
+ public class ScheduledTaskServiceImpl implements ScheduledTaskService {
39
+ private static final int MAX_USER_TASKS = 100;
40
+ private static final int CRON_INTERVAL_CHECKS = 32;
41
+ private static final long MINIMUM_INTERVAL_SECONDS = 60;
42
+ private static final List<String> TYPES = Arrays.asList("ONCE", "CRON", "INTERVAL");
43
+ private static final List<String> TASK_STATUSES = Arrays.asList("ACTIVE", "PAUSED", "COMPLETED");
44
+ private final ScheduledTaskMapper mapper;
45
+ private final ObjectMapper json;
46
+ private final ConversationServiceImpl conversationService;
47
+
48
+ @Autowired
49
+ public ScheduledTaskServiceImpl(
50
+ ScheduledTaskMapper mapper,
51
+ ObjectMapper json,
52
+ ConversationServiceImpl conversationService) {
53
+ this.mapper = mapper;
54
+ this.json = json;
55
+ this.conversationService = conversationService;
56
+ }
57
+
58
+ @Transactional(isolation = Isolation.REPEATABLE_READ)
59
+ public Map<String, Object> create(String input) {
60
+ Map<String, Object> payload = AgentSupport.request(json, input);
61
+ String userId = AgentSupport.userId(payload);
62
+ String orgCode = AgentSupport.identity(payload, "orgCode");
63
+ String roleId = AgentSupport.identity(payload, "roleId");
64
+ mapper.lockUserTasks(userId);
65
+ if (mapper.countRunnableTasks(userId) >= MAX_USER_TASKS)
66
+ throw new ConflictException("每个用户最多保留 100 个可运行任务");
67
+ Map<String, Object> task = taskDefinition(payload, userId);
68
+ task.put("orgCode", orgCode);
69
+ task.put("roleId", roleId);
70
+ mapper.insertTask(task);
71
+ return detailForUser(number(task.get("id")), userId);
72
+ }
73
+
74
+ @Transactional
75
+ public Map<String, Object> update(String input) {
76
+ Map<String, Object> payload = AgentSupport.request(json, input);
77
+ String userId = AgentSupport.userId(payload);
78
+ long taskId = positiveLong(payload == null ? null : payload.get("taskId"), "taskId");
79
+ requireTaskOwner(taskId, userId, true);
80
+ Map<String, Object> task = taskDefinition(payload, userId);
81
+ task.put("taskId", taskId);
82
+ task.put("orgCode", AgentSupport.identity(payload, "orgCode"));
83
+ task.put("roleId", AgentSupport.identity(payload, "roleId"));
84
+ if (mapper.updateTask(task) != 1) throw new NotFoundException("定时任务不存在");
85
+ return detailForUser(taskId, userId);
86
+ }
87
+
88
+ public Map<String, Object> list(String input) {
89
+ Map<String, Object> payload = AgentSupport.request(json, input);
90
+ String userId = AgentSupport.userId(payload);
91
+ String keyword = optionalText(payload, "keyword", 255, null);
92
+ String status = optionalText(payload, "status", 32, null);
93
+ int page = AgentSupport.integer(payload, "page", 1, Integer.MAX_VALUE);
94
+ int pageSize = AgentSupport.integer(payload, "pageSize", 1, 100);
95
+ page(page, pageSize);
96
+ String normalizedStatus = status == null || status.trim().isEmpty()
97
+ ? null : upper(status.trim());
98
+ if (normalizedStatus != null && !TASK_STATUSES.contains(normalizedStatus))
99
+ throw new IllegalArgumentException("status 非法");
100
+ String normalizedKeyword = keyword == null ? "" : keyword.trim();
101
+ if (normalizedKeyword.length() > 255) throw new IllegalArgumentException("keyword 非法");
102
+ String like = normalizedKeyword.isEmpty()
103
+ ? null : "%" + escapeLike(normalizedKeyword) + "%";
104
+ long total = mapper.countTasks(userId, like, normalizedStatus);
105
+ List<Map<String, Object>> items = mapper.findTasks(
106
+ userId, like, normalizedStatus, pageSize, (page - 1) * pageSize);
107
+ return ok(pageData(items, total, page, pageSize));
108
+ }
109
+
110
+ public Map<String, Object> detail(String input) {
111
+ Map<String, Object> payload = AgentSupport.request(json, input);
112
+ return detailForUser(positiveLong(payload.get("taskId"), "taskId"), AgentSupport.userId(payload));
113
+ }
114
+
115
+ private Map<String, Object> detailForUser(long taskId, String userId) {
116
+ Map<String, Object> task = mapper.findTaskDetail(taskId, userId);
117
+ if (task == null) throw new NotFoundException("定时任务不存在");
118
+ return ok(task);
119
+ }
120
+
121
+ public Map<String, Object> runs(String input) {
122
+ Map<String, Object> payload = AgentSupport.request(json, input);
123
+ String userId = AgentSupport.userId(payload);
124
+ long taskId = positiveLong(payload.get("taskId"), "taskId");
125
+ int page = AgentSupport.integer(payload, "page", 1, Integer.MAX_VALUE);
126
+ int pageSize = AgentSupport.integer(payload, "pageSize", 1, 100);
127
+ page(page, pageSize);
128
+ requireTaskOwner(taskId, userId, false);
129
+ long total = mapper.countRuns(taskId, userId);
130
+ List<Map<String, Object>> rows = mapper.findRuns(
131
+ taskId, userId, pageSize, (page - 1) * pageSize);
132
+ return ok(pageData(publicRuns(rows), total, page, pageSize));
133
+ }
134
+
135
+ public Map<String, Object> runDetail(String input) {
136
+ Map<String, Object> payload = AgentSupport.request(json, input);
137
+ String userId = AgentSupport.userId(payload);
138
+ long runId = positiveLong(payload.get("runId"), "runId");
139
+ Map<String, Object> row = ownedRun(runId, userId, false);
140
+ if (row == null) throw new NotFoundException("运行记录不存在");
141
+ return ok(publicRun(row, true));
142
+ }
143
+
144
+ public Map<String, Object> unread(String input) {
145
+ String userId = AgentSupport.userId(AgentSupport.request(json, input));
146
+ long total = mapper.countUnread(userId);
147
+ List<Map<String, Object>> rows = mapper.findUnread(userId);
148
+ List<Map<String, Object>> items = publicRuns(rows);
149
+ Map<String, Object> data = new LinkedHashMap<>();
150
+ data.put("items", items);
151
+ data.put("count", total);
152
+ return ok(data);
153
+ }
154
+
155
+ @Transactional
156
+ public Map<String, Object> start(String input) {
157
+ Map<String, Object> payload = AgentSupport.request(json, input);
158
+ String userId = AgentSupport.userId(payload);
159
+ long taskId = positiveLong(payload.get("taskId"), "taskId");
160
+ Map<String, Object> task = requireTaskOwner(taskId, userId, true);
161
+ if (!"PAUSED".equals(string(task, "status")))
162
+ throw new ConflictException("只有暂停任务可以启动");
163
+ Instant now = Instant.now();
164
+ Instant next = nextForTask(task, now);
165
+ mapper.activateTask(taskId, userId, databaseDateTime(next));
166
+ return detailForUser(taskId, userId);
167
+ }
168
+
169
+ @Transactional
170
+ public Map<String, Object> pause(String input) {
171
+ Map<String, Object> payload = AgentSupport.request(json, input);
172
+ String userId = AgentSupport.userId(payload);
173
+ long taskId = positiveLong(payload.get("taskId"), "taskId");
174
+ requireTaskOwner(taskId, userId, true);
175
+ int changed = mapper.pauseTask(taskId, userId);
176
+ if (changed != 1) throw new ConflictException("只有运行中的任务可以暂停");
177
+ return detailForUser(taskId, userId);
178
+ }
179
+
180
+ @Transactional
181
+ public Map<String, Object> discard(String input) {
182
+ Map<String, Object> payload = AgentSupport.request(json, input);
183
+ String userId = AgentSupport.userId(payload);
184
+ long taskId = positiveLong(payload.get("taskId"), "taskId");
185
+ requireTaskOwner(taskId, userId, true);
186
+ mapper.discardTask(taskId, userId);
187
+ return ok(null);
188
+ }
189
+
190
+ @Transactional(isolation = Isolation.REPEATABLE_READ)
191
+ public Long claimDueRun() {
192
+ Map<String, Object> task = mapper.findDueTask();
193
+ if (task == null) return null;
194
+ long taskId = number(task.get("id"));
195
+ Instant scheduledAt = instant(task.get("nextRunAt"));
196
+ Map<String, Object> run = new LinkedHashMap<>();
197
+ run.put("taskId", taskId);
198
+ run.put("scheduledAt", databaseDateTime(scheduledAt));
199
+ mapper.insertRun(run);
200
+ String type = string(task, "scheduleType");
201
+ if ("ONCE".equals(type)) {
202
+ mapper.completeOnceTask(taskId);
203
+ } else {
204
+ Instant next = nextForTask(task, Instant.now());
205
+ mapper.updateNextRun(taskId, databaseDateTime(next));
206
+ }
207
+ return number(run.get("id"));
208
+ }
209
+
210
+ public List<Long> pendingRunIds(int limit) {
211
+ return mapper.findPendingRunIds(limit);
212
+ }
213
+
214
+ public boolean beginRun(long runId, String tokenHash, String expiresAt) {
215
+ Map<String, Object> context = mapper.findPendingRunContext(runId);
216
+ if (context == null) return false;
217
+ identityValue(string(context, "userId"), "userId");
218
+ identityValue(string(context, "orgCode"), "orgCode");
219
+ identityValue(string(context, "roleId"), "roleId");
220
+ return mapper.beginRun(runId, identityValue(tokenHash, "tokenHash"), expiresAt) == 1;
221
+ }
222
+
223
+ @Transactional
224
+ public void completeFromProxy(long runId, Map<String, Object> result) {
225
+ Map<String, Object> run = lockRun(runId);
226
+ if (terminal(string(run, "status"))) return;
227
+ if (!"RUNNING".equals(string(run, "status")))
228
+ throw new ConflictException("运行状态不可完成");
229
+ String content = rawText(result, "content", 1000000, true);
230
+ Map<String, Object> stored = new LinkedHashMap<>();
231
+ stored.put("content", content);
232
+ stored.put("agentName", optionalText(
233
+ result, "agentName", 128, "ScheduledTaskAgent"));
234
+ try {
235
+ String value = json.writeValueAsString(stored);
236
+ if (value.length() > 4000000) throw new IllegalArgumentException("运行结果过大");
237
+ mapper.completeRun(runId, value);
238
+ completeOnceTask(number(run.get("taskId")));
239
+ } catch (Exception error) {
240
+ if (error instanceof RuntimeException) throw (RuntimeException) error;
241
+ throw new IllegalArgumentException("运行结果无法保存");
242
+ }
243
+ }
244
+
245
+ @Transactional
246
+ public void failRun(long runId, String errorCode, String errorMessage) {
247
+ Map<String, Object> run = lockRun(runId);
248
+ if (terminal(string(run, "status"))) return;
249
+ mapper.failRun(runId,
250
+ safeError(errorCode, "scheduled_run_failed", 64),
251
+ safeError(errorMessage, "定时任务执行失败", 512));
252
+ completeOnceTask(number(run.get("taskId")));
253
+ }
254
+
255
+ public int failStaleRuns(int maxRunSeconds) {
256
+ return mapper.failStaleRuns(maxRunSeconds);
257
+ }
258
+
259
+ public Map<String, Object> resolveScheduledToken(String rawToken) {
260
+ return mapper.resolveScheduledToken(ScheduledExecutionToken.hashSubmitted(rawToken));
261
+ }
262
+
263
+ @Transactional
264
+ public Map<String, Object> open(String input) {
265
+ Map<String, Object> payload = AgentSupport.request(json, input);
266
+ String userId = AgentSupport.userId(payload);
267
+ long runId = positiveLong(payload.get("runId"), "runId");
268
+ Map<String, Object> run = ownedRun(runId, userId, true);
269
+ if (run == null) throw new NotFoundException("运行记录不存在");
270
+ Object existing = run.get("resultConversationId");
271
+ if (existing != null && !String.valueOf(existing).isEmpty()) {
272
+ mapper.markRunRead(runId);
273
+ return ok(Collections.<String, Object>singletonMap(
274
+ "conversationId", String.valueOf(existing)));
275
+ }
276
+ String status = string(run, "status");
277
+ if (!terminal(status)) throw new ConflictException("运行尚未结束");
278
+ Map<String, Object> result = result(run.get("resultPayload"));
279
+ String content = "SUCCEEDED".equals(status)
280
+ ? rawText(result, "content", 1000000, true)
281
+ : safeError(string(run, "errorMessage"), "定时任务执行失败", 512);
282
+ String conversationId = conversationService.materializeCompletedConversation(
283
+ userId, string(run, "title"), string(run, "prompt"), content,
284
+ optionalText(result, "agentName", 128, "ScheduledTaskAgent"));
285
+ mapper.attachConversation(runId, conversationId);
286
+ Map<String, Object> opened = new LinkedHashMap<>();
287
+ opened.put("conversationId", conversationId);
288
+ return ok(opened);
289
+ }
290
+
291
+ private ScheduleValues scheduleValues(
292
+ Map<String, Object> payload, String type, ZoneId zone, Instant now) {
293
+ Instant runAt = parseInstant(text(payload, "runAt", 64, false), zone);
294
+ String cron = text(payload, "cronExpression", 128, false);
295
+ Long interval = optionalLong(payload.get("intervalSeconds"));
296
+ if ("ONCE".equals(type)) {
297
+ if (runAt == null || !runAt.isAfter(now) || cron != null || interval != null)
298
+ throw new IllegalArgumentException("ONCE 调度字段非法");
299
+ return new ScheduleValues(runAt, null, null, runAt);
300
+ }
301
+ if ("CRON".equals(type)) {
302
+ if (cron == null || runAt != null || interval != null)
303
+ throw new IllegalArgumentException("CRON 调度字段非法");
304
+ validateCron(cron, zone);
305
+ return new ScheduleValues(null, cron, null, nextCron(cron, zone, now));
306
+ }
307
+ if (runAt == null || interval == null || cron != null)
308
+ throw new IllegalArgumentException("INTERVAL 调度字段非法");
309
+ return new ScheduleValues(runAt, null, interval,
310
+ nextInterval(runAt, interval, now));
311
+ }
312
+
313
+ private Map<String, Object> taskDefinition(Map<String, Object> payload, String userId) {
314
+ String title = text(payload, "title", 255, true);
315
+ String prompt = text(payload, "prompt", 4096, true);
316
+ String type = upper(text(payload, "scheduleType", 16, true));
317
+ if (!TYPES.contains(type)) throw new IllegalArgumentException("scheduleType 非法");
318
+ ZoneId zone = zone(text(payload, "timezone", 64, true));
319
+ ScheduleValues values = scheduleValues(payload, type, zone, Instant.now());
320
+ Map<String, Object> task = new LinkedHashMap<>();
321
+ task.put("userId", userId);
322
+ task.put("title", title);
323
+ task.put("prompt", prompt);
324
+ task.put("scheduleType", type);
325
+ task.put("runAt", databaseDateTime(values.runAt));
326
+ task.put("cronExpression", values.cronExpression);
327
+ task.put("intervalSeconds", values.intervalSeconds);
328
+ task.put("timezone", zone.getId());
329
+ task.put("nextRunAt", databaseDateTime(values.nextRunAt));
330
+ return task;
331
+ }
332
+
333
+ private static String identityValue(String value, String field) {
334
+ if (value == null || value.trim().isEmpty() || value.length() > 64)
335
+ throw new IllegalArgumentException(field + " 非法");
336
+ return value;
337
+ }
338
+
339
+ private Instant nextForTask(Map<String, Object> task, Instant now) {
340
+ String type = string(task, "scheduleType");
341
+ Instant anchor = instant(task.get("runAt"));
342
+ if ("ONCE".equals(type)) return anchor.isAfter(now) ? anchor : now;
343
+ if ("CRON".equals(type)) return nextCron(
344
+ string(task, "cronExpression"), zone(string(task, "timezone")), now);
345
+ long interval = number(task.get("intervalSeconds"));
346
+ return nextInterval(anchor, interval, now);
347
+ }
348
+
349
+ void validateCron(String expression, ZoneId timezone) {
350
+ CronSequenceGenerator generator;
351
+ try {
352
+ generator = new CronSequenceGenerator(
353
+ expression, TimeZone.getTimeZone(timezone));
354
+ } catch (IllegalArgumentException error) {
355
+ throw new IllegalArgumentException("cronExpression 非法");
356
+ }
357
+ Date previous = generator.next(new Date());
358
+ for (int index = 0; index < CRON_INTERVAL_CHECKS; index++) {
359
+ Date next = generator.next(previous);
360
+ if (Duration.between(previous.toInstant(), next.toInstant()).getSeconds()
361
+ < MINIMUM_INTERVAL_SECONDS)
362
+ throw new IllegalArgumentException(
363
+ "cronExpression 触发间隔不能小于 " + MINIMUM_INTERVAL_SECONDS + " 秒");
364
+ previous = next;
365
+ }
366
+ }
367
+
368
+ Instant nextCron(String expression, ZoneId timezone, Instant after) {
369
+ validateCron(expression, timezone);
370
+ return new CronSequenceGenerator(expression, TimeZone.getTimeZone(timezone))
371
+ .next(Date.from(after)).toInstant();
372
+ }
373
+
374
+ Instant nextInterval(Instant anchor, long intervalSeconds, Instant after) {
375
+ if (intervalSeconds < MINIMUM_INTERVAL_SECONDS)
376
+ throw new IllegalArgumentException(
377
+ "intervalSeconds 不能小于 " + MINIMUM_INTERVAL_SECONDS);
378
+ if (anchor.isAfter(after)) return anchor;
379
+ long intervalMillis = Math.multiplyExact(intervalSeconds, 1000L);
380
+ long elapsedMillis = Duration.between(anchor, after).toMillis();
381
+ long steps = elapsedMillis / intervalMillis + 1;
382
+ return anchor.plusMillis(Math.multiplyExact(steps, intervalMillis));
383
+ }
384
+
385
+ private Map<String, Object> requireTaskOwner(long taskId, String userId, boolean lock) {
386
+ Map<String, Object> task = mapper.findTaskOwner(taskId, userId, lock);
387
+ if (task == null) throw new NotFoundException("定时任务不存在");
388
+ return task;
389
+ }
390
+
391
+ private Map<String, Object> ownedRun(long runId, String userId, boolean lock) {
392
+ return mapper.findOwnedRun(runId, userId, lock);
393
+ }
394
+
395
+ private Map<String, Object> lockRun(long runId) {
396
+ Map<String, Object> run = mapper.findRunForUpdate(runId);
397
+ if (run == null) throw new NotFoundException("运行记录不存在");
398
+ return run;
399
+ }
400
+
401
+ private void completeOnceTask(long taskId) {
402
+ mapper.completeOnceTask(taskId);
403
+ }
404
+
405
+ private List<Map<String, Object>> publicRuns(List<Map<String, Object>> rows) {
406
+ List<Map<String, Object>> result = new ArrayList<>();
407
+ for (Map<String, Object> row : rows) result.add(publicRun(row, false));
408
+ return result;
409
+ }
410
+
411
+ private Map<String, Object> publicRun(Map<String, Object> row, boolean includeResultContent) {
412
+ Map<String, Object> value = new LinkedHashMap<>(row);
413
+ Object raw = value.remove("resultPayload");
414
+ if (includeResultContent) value.put("resultContent", result(raw).get("content"));
415
+ value.put("readFlag", numberOrZero(value.get("readFlag")) == 1);
416
+ return value;
417
+ }
418
+
419
+ private Map<String, Object> result(Object raw) {
420
+ if (raw == null) return Collections.emptyMap();
421
+ try {
422
+ return json.readValue(String.valueOf(raw), new TypeReference<Map<String, Object>>() {});
423
+ } catch (Exception error) {
424
+ throw new DataCorruptionException();
425
+ }
426
+ }
427
+
428
+ private static Map<String, Object> pageData(
429
+ List<Map<String, Object>> items, long total, int page, int pageSize) {
430
+ Map<String, Object> data = new LinkedHashMap<>();
431
+ data.put("items", items);
432
+ data.put("total", total);
433
+ data.put("page", page);
434
+ data.put("pageSize", pageSize);
435
+ return data;
436
+ }
437
+
438
+ private static void page(int page, int pageSize) {
439
+ if (page < 1 || page > 100000) throw new IllegalArgumentException("page 非法");
440
+ if (pageSize < 1 || pageSize > 100) throw new IllegalArgumentException("pageSize 非法");
441
+ }
442
+
443
+ private static String text(Map<?, ?> values, String key, int max, boolean required) {
444
+ Object raw = values == null ? null : values.get(key);
445
+ if (raw != null && !(raw instanceof String)) throw new IllegalArgumentException(key + " 非法");
446
+ String value = raw == null ? null : ((String) raw).trim();
447
+ if (value != null && value.isEmpty()) value = null;
448
+ if ((required && value == null) || (value != null && value.length() > max))
449
+ throw new IllegalArgumentException(key + " 非法");
450
+ return value;
451
+ }
452
+
453
+ private static String rawText(Map<?, ?> values, String key, int max, boolean required) {
454
+ Object raw = values == null ? null : values.get(key);
455
+ if (raw != null && !(raw instanceof String)) throw new IllegalArgumentException(key + " 非法");
456
+ String value = raw == null ? null : (String) raw;
457
+ if ((required && (value == null || value.trim().isEmpty())) ||
458
+ (value != null && value.length() > max))
459
+ throw new IllegalArgumentException(key + " 非法");
460
+ return value;
461
+ }
462
+
463
+ private static String optionalText(
464
+ Map<String, Object> values, String key, int max, String fallback) {
465
+ String value = text(values, key, max, false);
466
+ return value == null ? fallback : value;
467
+ }
468
+
469
+ private static String string(Map<String, Object> values, String key) {
470
+ Object value = values.get(key);
471
+ if (value == null) value = values.get(key.toUpperCase(Locale.ROOT));
472
+ return value == null ? null : String.valueOf(value);
473
+ }
474
+
475
+ private static String upper(String value) {
476
+ return value.toUpperCase(Locale.ROOT);
477
+ }
478
+
479
+ static ZoneId zone(String value) {
480
+ if (value == null || !ZoneId.getAvailableZoneIds().contains(value))
481
+ throw new IllegalArgumentException("timezone 非法");
482
+ return ZoneId.of(value);
483
+ }
484
+
485
+ private static Instant parseInstant(String value, ZoneId zone) {
486
+ if (value == null) return null;
487
+ try {
488
+ return Instant.parse(value);
489
+ } catch (DateTimeParseException ignored) {
490
+ try {
491
+ return OffsetDateTime.parse(value).toInstant();
492
+ } catch (DateTimeParseException ignoredAgain) {
493
+ try {
494
+ return LocalDateTime.parse(value).atZone(zone).toInstant();
495
+ } catch (DateTimeParseException error) {
496
+ throw new IllegalArgumentException("runAt 非法");
497
+ }
498
+ }
499
+ }
500
+ }
501
+
502
+ static Instant instant(Object value) {
503
+ if (value == null) return null;
504
+ if (value instanceof Timestamp) return ((Timestamp) value).toInstant();
505
+ if (value instanceof java.util.Date) return ((java.util.Date) value).toInstant();
506
+ if (value instanceof LocalDateTime)
507
+ return ((LocalDateTime) value).toInstant(ZoneOffset.UTC);
508
+ return Instant.parse(String.valueOf(value));
509
+ }
510
+
511
+ static String databaseDateTime(Instant value) {
512
+ return value == null ? null : DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
513
+ .withZone(ZoneOffset.UTC)
514
+ .format(value);
515
+ }
516
+
517
+ private static Long optionalLong(Object value) {
518
+ if (value == null) return null;
519
+ if (!(value instanceof Number)) throw new IllegalArgumentException("intervalSeconds 非法");
520
+ long result = ((Number) value).longValue();
521
+ if (result <= 0) throw new IllegalArgumentException("intervalSeconds 非法");
522
+ return result;
523
+ }
524
+
525
+ private static long positiveLong(Object value, String name) {
526
+ if (!(value instanceof Number) || ((Number) value).longValue() <= 0)
527
+ throw new IllegalArgumentException(name + " 非法");
528
+ return ((Number) value).longValue();
529
+ }
530
+
531
+ private static long number(Object value) {
532
+ return value instanceof Number ? ((Number) value).longValue() : Long.parseLong(String.valueOf(value));
533
+ }
534
+
535
+ private static long numberOrZero(Object value) {
536
+ if (value instanceof Boolean) return (Boolean) value ? 1 : 0;
537
+ return value == null ? 0 : number(value);
538
+ }
539
+
540
+ private static String escapeLike(String value) {
541
+ return value.replace("!", "!!").replace("%", "!%").replace("_", "!_");
542
+ }
543
+
544
+ private static boolean terminal(String status) {
545
+ return "SUCCEEDED".equals(status) || "FAILED".equals(status);
546
+ }
547
+
548
+ private static String safeError(String value, String fallback, int max) {
549
+ if (value == null || value.trim().isEmpty()) return fallback;
550
+ String normalized = value.trim();
551
+ return normalized.substring(0, Math.min(normalized.length(), max));
552
+ }
553
+
554
+ private static Map<String, Object> ok(Object data) {
555
+ Map<String, Object> response = new LinkedHashMap<>();
556
+ response.put("success", true);
557
+ response.put("data", data);
558
+ return response;
559
+ }
560
+
561
+ private static class ScheduleValues {
562
+ private final Instant runAt;
563
+ private final String cronExpression;
564
+ private final Long intervalSeconds;
565
+ private final Instant nextRunAt;
566
+
567
+ private ScheduleValues(Instant runAt, String cronExpression,
568
+ Long intervalSeconds, Instant nextRunAt) {
569
+ this.runAt = runAt;
570
+ this.cronExpression = cronExpression;
571
+ this.intervalSeconds = intervalSeconds;
572
+ this.nextRunAt = nextRunAt;
573
+ }
574
+ }
575
+
576
+ }