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.
- package/PROJECT_OVERVIEW.md +405 -0
- package/README.md +177 -0
- package/ark-control/pom.xml +142 -0
- package/ark-control/src/local-mock/java/com/epcc/commons/securityproxy/api/SecurityResult.java +14 -0
- package/ark-control/src/local-mock/java/com/epcc/commons/securityproxy/api/SymmetricalSecurityService.java +11 -0
- package/ark-control/src/local-mock/java/com/epcc/dubbo/result/Result.java +29 -0
- package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/annotation/EnDecryptField.java +16 -0
- package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/annotation/EnDecryptFieldLong.java +17 -0
- package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/annotation/EnDecryptFieldWithTag.java +16 -0
- package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/exception/BaseDataErrorCode.java +26 -0
- package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/exception/CheckException.java +33 -0
- package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/exception/ErrorCode.java +9 -0
- package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/redis/RedisCacheService.java +131 -0
- package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/util/Constant.java +24 -0
- package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/util/ResultUtil.java +9 -0
- package/ark-control/src/local-mock/java/com/union/control/local/sensitive/LocalMockSensitiveProxy.java +40 -0
- package/ark-control/src/local-mock/java/com/union/control/local/sensitive/LocalRedisCacheService.java +45 -0
- package/ark-control/src/local-mock/java/com/union/control/local/sensitive/LocalSensitiveHostConfiguration.java +27 -0
- package/ark-control/src/local-mock/java/com/union/control/local/sensitive/LocalSensitiveProxyConfiguration.java +16 -0
- package/ark-control/src/main/java/com/union/control/ArkControlApplication.java +17 -0
- package/ark-control/src/main/java/com/union/control/dubbo/ProviderAccessLogFilter.java +45 -0
- package/ark-control/src/main/java/com/union/control/mapper/AgentExecutionMapper.java +35 -0
- package/ark-control/src/main/java/com/union/control/mapper/ConversationMapper.java +44 -0
- package/ark-control/src/main/java/com/union/control/mapper/MemoryStoreMapper.java +41 -0
- package/ark-control/src/main/java/com/union/control/mapper/ScheduledTaskMapper.java +96 -0
- package/ark-control/src/main/java/com/union/control/mapper/SensitiveAddressBookDemo.java +31 -0
- package/ark-control/src/main/java/com/union/control/mapper/SensitiveDataDemoMapper.java +24 -0
- package/ark-control/src/main/java/com/union/control/mapper/interceptor/AESInterceptor.java +265 -0
- package/ark-control/src/main/java/com/union/control/mapper/interceptor/AddressBookHandler.java +105 -0
- package/ark-control/src/main/java/com/union/control/schedule/ScheduledExecutionToken.java +55 -0
- package/ark-control/src/main/java/com/union/control/schedule/ScheduledTaskScheduler.java +206 -0
- package/ark-control/src/main/java/com/union/control/service/impl/AgentExecutionServiceImpl.java +212 -0
- package/ark-control/src/main/java/com/union/control/service/impl/AgentProxyServiceImpl.java +143 -0
- package/ark-control/src/main/java/com/union/control/service/impl/ConversationServiceImpl.java +245 -0
- package/ark-control/src/main/java/com/union/control/service/impl/MemoryStoreServiceImpl.java +252 -0
- package/ark-control/src/main/java/com/union/control/service/impl/RunningAnalysisMockServiceImpl.java +301 -0
- package/ark-control/src/main/java/com/union/control/service/impl/ScheduledTaskServiceImpl.java +576 -0
- package/ark-control/src/main/java/com/union/control/service/impl/SensitiveDataDemoServiceImpl.java +144 -0
- package/ark-control/src/main/java/com/union/control/service/sensitive/RedisRevealTokenStore.java +90 -0
- package/ark-control/src/main/java/com/union/control/service/sensitive/SensitiveRevealProcessor.java +319 -0
- package/ark-control/src/main/java/com/union/control/service/sensitive/SensitiveRevealServiceImpl.java +72 -0
- package/ark-control/src/main/java/com/union/control/utils/AgentSupport.java +103 -0
- package/ark-control/src/main/java/com/union/control/utils/security/SymmetricalSecurityUtils.java +160 -0
- package/ark-control/src/main/resources/META-INF/dubbo/com.alibaba.dubbo.rpc.Filter +1 -0
- package/ark-control/src/main/resources/application.properties +4 -0
- package/ark-control/src/main/resources/application.yml +32 -0
- package/ark-control/src/main/resources/dubbo-provider.xml +20 -0
- package/ark-control/src/main/resources/mapper/AgentExecutionMapper.xml +75 -0
- package/ark-control/src/main/resources/mapper/ConversationMapper.xml +110 -0
- package/ark-control/src/main/resources/mapper/MemoryStoreMapper.xml +74 -0
- package/ark-control/src/main/resources/mapper/ScheduledTaskMapper.xml +286 -0
- package/ark-control/src/main/resources/mapper/SensitiveDataDemoMapper.xml +55 -0
- package/ark-control/src/main/resources/schema.sql +146 -0
- package/ark-control/src/test/java/com/union/control/AgentExecutionServiceTest.java +170 -0
- package/ark-control/src/test/java/com/union/control/ArkControlStructureContractTest.java +107 -0
- package/ark-control/src/test/java/com/union/control/ConversationServiceTest.java +89 -0
- package/ark-control/src/test/java/com/union/control/MemoryStoreServiceTest.java +26 -0
- package/ark-control/src/test/java/com/union/control/PydanticAiControlContractTest.java +52 -0
- package/ark-control/src/test/java/com/union/control/TestJson.java +45 -0
- package/ark-control/src/test/java/com/union/control/dubbo/ProviderAccessLogFilterTest.java +53 -0
- package/ark-control/src/test/java/com/union/control/local/sensitive/LocalSensitiveProxyConfigurationTest.java +23 -0
- package/ark-control/src/test/java/com/union/control/mapper/interceptor/AESInterceptorCompatibilityTest.java +505 -0
- package/ark-control/src/test/java/com/union/control/schedule/ScheduledExecutionTokenTest.java +27 -0
- package/ark-control/src/test/java/com/union/control/schedule/ScheduledTaskSchedulerTest.java +51 -0
- package/ark-control/src/test/java/com/union/control/service/AgentProxyServiceTest.java +111 -0
- package/ark-control/src/test/java/com/union/control/service/ScheduledTaskServiceTest.java +483 -0
- package/ark-control/src/test/java/com/union/control/service/impl/SensitiveDataDemoServiceImplTest.java +104 -0
- package/ark-control/src/test/java/com/union/control/service/sensitive/RedisRevealTokenStoreTest.java +65 -0
- package/ark-control/src/test/java/com/union/control/service/sensitive/SensitiveRevealBoundaryTest.java +99 -0
- package/ark-control/src/test/java/com/union/control/service/sensitive/SensitiveRevealProcessorCodecTest.java +111 -0
- package/ark-control/src/test/java/com/union/control/service/sensitive/SensitiveRevealProcessorTest.java +111 -0
- package/ark-control/src/test/java/com/union/control/utils/security/SymmetricalSecurityUtilsCompatibilityTest.java +87 -0
- package/ark-control-facade/pom.xml +16 -0
- package/ark-control-facade/src/main/java/com/union/control/service/AgentExecutionService.java +8 -0
- package/ark-control-facade/src/main/java/com/union/control/service/AgentProxyService.java +7 -0
- package/ark-control-facade/src/main/java/com/union/control/service/AgentResponse.java +22 -0
- package/ark-control-facade/src/main/java/com/union/control/service/ConversationService.java +12 -0
- package/ark-control-facade/src/main/java/com/union/control/service/MemoryStoreService.java +12 -0
- package/ark-control-facade/src/main/java/com/union/control/service/RunningAnalysisMockService.java +10 -0
- package/ark-control-facade/src/main/java/com/union/control/service/ScheduledTaskService.java +40 -0
- package/ark-control-facade/src/main/java/com/union/control/service/SensitiveDataDemoService.java +13 -0
- package/ark-control-facade/src/main/java/com/union/control/service/sensitive/SensitiveRevealService.java +18 -0
- package/ark-web/README.md +80 -0
- package/ark-web/pom.xml +105 -0
- package/ark-web/src/local-mock/java/com/epcc/arkweb/mock/ArkAuthServiceMock.java +105 -0
- package/ark-web/src/local-mock/java/com/epcc/arkweb/mock/LocalKaptchaConfiguration.java +26 -0
- package/ark-web/src/main/java/com/epcc/arkweb/Application.java +13 -0
- package/ark-web/src/main/java/com/epcc/arkweb/config/InterceptorConfig.java +20 -0
- package/ark-web/src/main/java/com/epcc/arkweb/config/Realm.java +64 -0
- package/ark-web/src/main/java/com/epcc/arkweb/config/ShiroConfig.java +129 -0
- package/ark-web/src/main/java/com/epcc/arkweb/filter/LoginFormFilter.java +43 -0
- package/ark-web/src/main/java/com/epcc/arkweb/helper/AuthContextHolder.java +39 -0
- package/ark-web/src/main/java/com/epcc/arkweb/helper/AuthenticatedRequest.java +91 -0
- package/ark-web/src/main/java/com/epcc/arkweb/model/ShiroUser.java +21 -0
- package/ark-web/src/main/java/com/epcc/arkweb/utils/ResultMsg.java +19 -0
- package/ark-web/src/main/java/com/epcc/arkweb/vo/llm/ScheduledTaskCommandVO.java +30 -0
- package/ark-web/src/main/java/com/epcc/arkweb/vo/llm/ScheduledTaskQueryVO.java +20 -0
- package/ark-web/src/main/java/com/epcc/arkweb/web/CommonController.java +25 -0
- package/ark-web/src/main/java/com/epcc/arkweb/web/llm/AgentAuthorizationInterceptor.java +229 -0
- package/ark-web/src/main/java/com/epcc/arkweb/web/llm/AgentController.java +165 -0
- package/ark-web/src/main/java/com/epcc/arkweb/web/llm/AgentPermission.java +13 -0
- package/ark-web/src/main/java/com/epcc/arkweb/web/llm/LlmController.java +209 -0
- package/ark-web/src/main/java/com/epcc/arkweb/web/llm/ScheduledTaskController.java +160 -0
- package/ark-web/src/main/java/com/epcc/arkweb/web/sensitive/reveal/SensitiveCaptchaService.java +146 -0
- package/ark-web/src/main/java/com/epcc/arkweb/web/sensitive/reveal/SensitiveRevealController.java +163 -0
- package/ark-web/src/main/resources/application.properties +1 -0
- package/ark-web/src/main/resources/application.yml +23 -0
- package/ark-web/src/main/resources/dubbo-consumer.xml +19 -0
- package/ark-web/src/production-overlay/java/com/epcc/arkweb/config/CasShiroConfig.java +229 -0
- package/ark-web/src/production-overlay/java/com/epcc/arkweb/config/InterceptorConfig.java +35 -0
- package/ark-web/src/production-overlay/java/com/epcc/arkweb/config/ShiroConfig.java +161 -0
- package/ark-web/src/test/java/com/epcc/arkweb/ArkWebStructureContractTest.java +290 -0
- package/ark-web/src/test/java/com/epcc/arkweb/ShiroTestSupport.java +32 -0
- package/ark-web/src/test/java/com/epcc/arkweb/config/LocalShiroIntegrationTest.java +121 -0
- package/ark-web/src/test/java/com/epcc/arkweb/helper/AuthenticatedRequestTest.java +131 -0
- package/ark-web/src/test/java/com/epcc/arkweb/local/LocalArkAuthServiceMockTest.java +17 -0
- package/ark-web/src/test/java/com/epcc/arkweb/web/llm/AgentAuthorizationInterceptorTest.java +134 -0
- package/ark-web/src/test/java/com/epcc/arkweb/web/llm/AgentControllerAuthorizationTest.java +97 -0
- package/ark-web/src/test/java/com/epcc/arkweb/web/llm/LlmControllerTest.java +113 -0
- package/ark-web/src/test/java/com/epcc/arkweb/web/sensitive/reveal/SensitiveRevealControllerTest.java +273 -0
- package/package.json +18 -0
- package/pom.xml +16 -0
package/ark-control/src/main/java/com/union/control/mapper/interceptor/AddressBookHandler.java
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
package com.union.control.mapper.interceptor;
|
|
2
|
+
|
|
3
|
+
import com.nucc.channel.ark.common.exception.CheckException;
|
|
4
|
+
import com.union.control.service.sensitive.SensitiveRevealProcessor;
|
|
5
|
+
import org.apache.ibatis.mapping.MappedStatement;
|
|
6
|
+
import org.springframework.beans.factory.annotation.Value;
|
|
7
|
+
import org.springframework.stereotype.Component;
|
|
8
|
+
|
|
9
|
+
import java.lang.reflect.Array;
|
|
10
|
+
import java.util.ArrayList;
|
|
11
|
+
import java.util.Collections;
|
|
12
|
+
import java.util.LinkedHashSet;
|
|
13
|
+
import java.util.List;
|
|
14
|
+
import java.util.Map;
|
|
15
|
+
import java.util.Set;
|
|
16
|
+
import java.util.regex.Pattern;
|
|
17
|
+
import org.apache.ibatis.reflection.MetaObject;
|
|
18
|
+
import org.apache.ibatis.reflection.SystemMetaObject;
|
|
19
|
+
|
|
20
|
+
/** The only statement-specific branch: AddressBook migration and row visibility. */
|
|
21
|
+
@Component
|
|
22
|
+
final class AddressBookHandler {
|
|
23
|
+
private static final Pattern OLD_ADDRESS_BOOK =
|
|
24
|
+
Pattern.compile("(?<!\\S)t_m_announce_address_book(?!_)");
|
|
25
|
+
|
|
26
|
+
private final SensitiveRevealProcessor revealProcessor;
|
|
27
|
+
private final Set<String> plaintextRoles;
|
|
28
|
+
|
|
29
|
+
AddressBookHandler(SensitiveRevealProcessor revealProcessor,
|
|
30
|
+
@Value("${sensitive.address-book.plaintext-roles:}") String configuredRoles) {
|
|
31
|
+
if (revealProcessor == null) throw new IllegalArgumentException("Reveal processor is required");
|
|
32
|
+
this.revealProcessor = revealProcessor;
|
|
33
|
+
this.plaintextRoles = parseRoles(configuredRoles);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
MappedStatement prepare(MappedStatement statement, Object parameter) throws CheckException {
|
|
37
|
+
revealProcessor.encrypt(parameter);
|
|
38
|
+
return AESInterceptor.copy(statement, parameter,
|
|
39
|
+
OLD_ADDRESS_BOOK.matcher(statement.getBoundSql(parameter).getSql())
|
|
40
|
+
.replaceAll("t_m_announce_address_book_new"));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
void processResult(Object result, boolean revealEnabled, boolean strictMode) throws CheckException {
|
|
44
|
+
if (strictMode) {
|
|
45
|
+
revealProcessor.process(result);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!revealEnabled) {
|
|
49
|
+
revealProcessor.decrypt(result);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
List<Object> plaintextRows = new ArrayList<>();
|
|
54
|
+
List<Object> protectedRows = new ArrayList<>();
|
|
55
|
+
for (Object row : rows(result)) {
|
|
56
|
+
if (allowPlaintext(row)) plaintextRows.add(row);
|
|
57
|
+
else protectedRows.add(row);
|
|
58
|
+
}
|
|
59
|
+
revealProcessor.decrypt(plaintextRows);
|
|
60
|
+
revealProcessor.process(protectedRows);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private static List<Object> rows(Object result) {
|
|
64
|
+
if (result == null) return Collections.emptyList();
|
|
65
|
+
List<Object> rows = new ArrayList<>();
|
|
66
|
+
if (result instanceof Iterable<?>) {
|
|
67
|
+
for (Object row : (Iterable<?>) result) rows.add(row);
|
|
68
|
+
} else if (result.getClass().isArray()) {
|
|
69
|
+
for (int i = 0; i < Array.getLength(result); i++) rows.add(Array.get(result, i));
|
|
70
|
+
} else {
|
|
71
|
+
rows.add(result);
|
|
72
|
+
}
|
|
73
|
+
return rows;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private boolean allowPlaintext(Object row) {
|
|
77
|
+
if (row == null) return false;
|
|
78
|
+
try {
|
|
79
|
+
Object value;
|
|
80
|
+
if (row instanceof Map<?, ?>) {
|
|
81
|
+
value = ((Map<?, ?>) row).get("role");
|
|
82
|
+
} else {
|
|
83
|
+
MetaObject metaObject = SystemMetaObject.forObject(row);
|
|
84
|
+
if (!metaObject.hasGetter("role")) return false;
|
|
85
|
+
value = metaObject.getValue("role");
|
|
86
|
+
}
|
|
87
|
+
return value != null && plaintextRoles.contains(value.toString().trim());
|
|
88
|
+
} catch (RuntimeException error) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private static Set<String> parseRoles(String configuredRoles) {
|
|
94
|
+
Set<String> roles = new LinkedHashSet<>();
|
|
95
|
+
if (configuredRoles == null || configuredRoles.trim().isEmpty()) return roles;
|
|
96
|
+
for (String item : configuredRoles.split(",", -1)) {
|
|
97
|
+
String role = item.trim();
|
|
98
|
+
if (!role.matches("[A-Za-z0-9_-]{1,32}")) {
|
|
99
|
+
throw new IllegalArgumentException("Invalid AddressBook plaintext role");
|
|
100
|
+
}
|
|
101
|
+
roles.add(role);
|
|
102
|
+
}
|
|
103
|
+
return roles;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package com.union.control.schedule;
|
|
2
|
+
|
|
3
|
+
import java.nio.charset.StandardCharsets;
|
|
4
|
+
import java.security.MessageDigest;
|
|
5
|
+
import java.security.NoSuchAlgorithmException;
|
|
6
|
+
import java.security.SecureRandom;
|
|
7
|
+
import java.util.Base64;
|
|
8
|
+
|
|
9
|
+
/** One short-lived capability for one scheduled run. The database stores only its hash. */
|
|
10
|
+
public final class ScheduledExecutionToken {
|
|
11
|
+
private static final SecureRandom RANDOM = new SecureRandom();
|
|
12
|
+
private final String value;
|
|
13
|
+
|
|
14
|
+
private ScheduledExecutionToken(String value) {
|
|
15
|
+
this.value = value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public static ScheduledExecutionToken issue() {
|
|
19
|
+
byte[] bytes = new byte[32];
|
|
20
|
+
RANDOM.nextBytes(bytes);
|
|
21
|
+
return new ScheduledExecutionToken(
|
|
22
|
+
Base64.getUrlEncoder().withoutPadding().encodeToString(bytes));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public String authorizationHeader() {
|
|
26
|
+
return "Scheduled " + value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public String hash() {
|
|
30
|
+
return hashSubmitted(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public static String hashSubmitted(String token) {
|
|
34
|
+
if (!valid(token)) throw new IllegalArgumentException("scheduled token 非法");
|
|
35
|
+
try {
|
|
36
|
+
byte[] digest = MessageDigest.getInstance("SHA-256")
|
|
37
|
+
.digest(token.getBytes(StandardCharsets.UTF_8));
|
|
38
|
+
StringBuilder result = new StringBuilder(64);
|
|
39
|
+
for (byte item : digest) result.append(String.format("%02x", item & 0xff));
|
|
40
|
+
return result.toString();
|
|
41
|
+
} catch (NoSuchAlgorithmException impossible) {
|
|
42
|
+
throw new IllegalStateException(impossible);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public static boolean valid(String token) {
|
|
47
|
+
if (token == null || token.length() != 43
|
|
48
|
+
|| !token.matches("[A-Za-z0-9_-]{43}")) return false;
|
|
49
|
+
try {
|
|
50
|
+
return Base64.getUrlDecoder().decode(token).length == 32;
|
|
51
|
+
} catch (IllegalArgumentException error) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
package com.union.control.schedule;
|
|
2
|
+
|
|
3
|
+
import com.fasterxml.jackson.core.type.TypeReference;
|
|
4
|
+
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
5
|
+
import com.union.control.service.AgentProxyService;
|
|
6
|
+
import com.union.control.service.AgentResponse;
|
|
7
|
+
import com.union.control.service.ScheduledTaskService;
|
|
8
|
+
import org.slf4j.Logger;
|
|
9
|
+
import org.slf4j.LoggerFactory;
|
|
10
|
+
import org.springframework.beans.factory.annotation.Value;
|
|
11
|
+
import org.springframework.scheduling.annotation.Scheduled;
|
|
12
|
+
import org.springframework.stereotype.Component;
|
|
13
|
+
|
|
14
|
+
import javax.annotation.PreDestroy;
|
|
15
|
+
import java.time.Instant;
|
|
16
|
+
import java.time.ZoneOffset;
|
|
17
|
+
import java.time.format.DateTimeFormatter;
|
|
18
|
+
import java.util.Collections;
|
|
19
|
+
import java.util.LinkedHashSet;
|
|
20
|
+
import java.util.Map;
|
|
21
|
+
import java.util.Set;
|
|
22
|
+
import java.util.concurrent.ArrayBlockingQueue;
|
|
23
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
24
|
+
import java.util.concurrent.RejectedExecutionException;
|
|
25
|
+
import java.util.concurrent.ThreadFactory;
|
|
26
|
+
import java.util.concurrent.ThreadPoolExecutor;
|
|
27
|
+
import java.util.concurrent.TimeUnit;
|
|
28
|
+
import java.util.concurrent.atomic.AtomicInteger;
|
|
29
|
+
|
|
30
|
+
/** Claims and executes due tasks. Control is trusted and deliberately has no permission check. */
|
|
31
|
+
@Component
|
|
32
|
+
public class ScheduledTaskScheduler {
|
|
33
|
+
private static final Logger LOG = LoggerFactory.getLogger(ScheduledTaskScheduler.class);
|
|
34
|
+
private static final int SCAN_BATCH_SIZE = 20;
|
|
35
|
+
private static final int WORKER_QUEUE_SIZE = 32;
|
|
36
|
+
private final ScheduledTaskService service;
|
|
37
|
+
private final AgentProxyService proxy;
|
|
38
|
+
private final ObjectMapper json;
|
|
39
|
+
private final ThreadPoolExecutor workers;
|
|
40
|
+
private final boolean enabled;
|
|
41
|
+
private final int maxRunSeconds;
|
|
42
|
+
private final int tokenTtlSeconds;
|
|
43
|
+
private final Set<Long> submitted = Collections.newSetFromMap(
|
|
44
|
+
new ConcurrentHashMap<Long, Boolean>());
|
|
45
|
+
|
|
46
|
+
public ScheduledTaskScheduler(
|
|
47
|
+
ScheduledTaskService service,
|
|
48
|
+
AgentProxyService proxy,
|
|
49
|
+
ObjectMapper json,
|
|
50
|
+
@Value("${agent.scheduled-enabled:false}") boolean enabled,
|
|
51
|
+
@Value("${agent.scheduled-worker-threads:2}") int workerThreads,
|
|
52
|
+
@Value("${agent.scheduled-max-run-seconds:930}") int maxRunSeconds,
|
|
53
|
+
@Value("${agent.scheduled-token-ttl-seconds:960}") int tokenTtlSeconds) {
|
|
54
|
+
if (tokenTtlSeconds <= maxRunSeconds || tokenTtlSeconds > 3600)
|
|
55
|
+
throw new IllegalArgumentException(
|
|
56
|
+
"scheduled token TTL must exceed max run time and be <= 3600");
|
|
57
|
+
this.service = service;
|
|
58
|
+
this.proxy = proxy;
|
|
59
|
+
this.json = json;
|
|
60
|
+
this.enabled = enabled;
|
|
61
|
+
this.maxRunSeconds = Math.max(60, maxRunSeconds);
|
|
62
|
+
this.tokenTtlSeconds = tokenTtlSeconds;
|
|
63
|
+
int threads = Math.max(1, Math.min(workerThreads, 16));
|
|
64
|
+
this.workers = new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS,
|
|
65
|
+
new ArrayBlockingQueue<Runnable>(WORKER_QUEUE_SIZE), new NamedThreadFactory(),
|
|
66
|
+
new ThreadPoolExecutor.AbortPolicy());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@Scheduled(initialDelay = 1000, fixedDelay = 5000)
|
|
70
|
+
public void scan() {
|
|
71
|
+
if (!enabled) return;
|
|
72
|
+
service.failStaleRuns(maxRunSeconds);
|
|
73
|
+
Set<Long> ready = new LinkedHashSet<>(service.pendingRunIds(SCAN_BATCH_SIZE));
|
|
74
|
+
while (ready.size() < SCAN_BATCH_SIZE) {
|
|
75
|
+
Long runId = service.claimDueRun();
|
|
76
|
+
if (runId == null) break;
|
|
77
|
+
ready.add(runId);
|
|
78
|
+
}
|
|
79
|
+
for (Long runId : ready) submit(runId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private void submit(final long runId) {
|
|
83
|
+
if (!submitted.add(runId)) return;
|
|
84
|
+
try {
|
|
85
|
+
workers.execute(new Runnable() {
|
|
86
|
+
@Override
|
|
87
|
+
public void run() {
|
|
88
|
+
try {
|
|
89
|
+
try {
|
|
90
|
+
ScheduledExecutionToken token = ScheduledExecutionToken.issue();
|
|
91
|
+
String expiresAt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
|
|
92
|
+
.withZone(ZoneOffset.UTC)
|
|
93
|
+
.format(Instant.now().plusSeconds(tokenTtlSeconds));
|
|
94
|
+
if (!service.beginRun(runId, token.hash(), expiresAt)) return;
|
|
95
|
+
long startedAt = System.nanoTime();
|
|
96
|
+
LOG.info("Scheduled run started run_id={}", runId);
|
|
97
|
+
service.completeFromProxy(runId, execute(token));
|
|
98
|
+
LOG.info("Scheduled run completed run_id={} duration_ms={}", runId,
|
|
99
|
+
(System.nanoTime() - startedAt) / 1_000_000L);
|
|
100
|
+
} catch (ScheduledAgentFailure error) {
|
|
101
|
+
LOG.warn("Scheduled run failed run_id={} error_code={}",
|
|
102
|
+
runId, error.code);
|
|
103
|
+
try {
|
|
104
|
+
service.failRun(runId, error.code, error.userMessage);
|
|
105
|
+
} catch (RuntimeException finishError) {
|
|
106
|
+
LOG.error("Scheduled run failure persistence failed run_id={}", runId);
|
|
107
|
+
}
|
|
108
|
+
} catch (RuntimeException error) {
|
|
109
|
+
LOG.warn("Scheduled run failed run_id={} error_type={}",
|
|
110
|
+
runId, error.getClass().getSimpleName());
|
|
111
|
+
try {
|
|
112
|
+
service.failRun(runId, "scheduled_run_failed", "定时任务执行失败");
|
|
113
|
+
} catch (RuntimeException finishError) {
|
|
114
|
+
LOG.error("Scheduled run failure persistence failed run_id={}", runId);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} finally {
|
|
118
|
+
submitted.remove(runId);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
} catch (RejectedExecutionException ignored) {
|
|
123
|
+
submitted.remove(runId);
|
|
124
|
+
LOG.warn("Scheduled worker queue full run_id={}", runId);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private Map<String, Object> execute(ScheduledExecutionToken token) {
|
|
129
|
+
try {
|
|
130
|
+
AgentResponse response = proxy.scheduled(token.authorizationHeader());
|
|
131
|
+
if (response.getStatus() < 200 || response.getStatus() >= 300)
|
|
132
|
+
throw failure(response);
|
|
133
|
+
String body = response.getBody();
|
|
134
|
+
if (body == null || body.isEmpty() || body.length() > 4000000)
|
|
135
|
+
throw new IllegalStateException("Agent scheduled 返回无效结果");
|
|
136
|
+
Map<String, Object> result = json.readValue(
|
|
137
|
+
body, new TypeReference<Map<String, Object>>() {});
|
|
138
|
+
required(result, "content");
|
|
139
|
+
return result;
|
|
140
|
+
} catch (RuntimeException error) {
|
|
141
|
+
throw error;
|
|
142
|
+
} catch (Exception error) {
|
|
143
|
+
throw new IllegalStateException("Agent scheduled 请求或响应无效", error);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private static String required(Map<String, Object> values, String key) {
|
|
148
|
+
Object value = values.get(key);
|
|
149
|
+
if (!(value instanceof String) || ((String) value).trim().isEmpty())
|
|
150
|
+
throw new IllegalStateException("Scheduled run result 缺少 " + key);
|
|
151
|
+
return (String) value;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private ScheduledAgentFailure failure(AgentResponse response) {
|
|
155
|
+
String code = "agent_http_error";
|
|
156
|
+
String message = "Agent 服务返回 HTTP " + response.getStatus();
|
|
157
|
+
String body = response.getBody();
|
|
158
|
+
if (body != null && !body.isEmpty() && body.length() <= 65536) {
|
|
159
|
+
try {
|
|
160
|
+
Map<String, Object> error = json.readValue(
|
|
161
|
+
body, new TypeReference<Map<String, Object>>() {});
|
|
162
|
+
code = safeCode(error.get("code"), code);
|
|
163
|
+
message = safeMessage(error.get("error"), message);
|
|
164
|
+
} catch (Exception ignored) {}
|
|
165
|
+
}
|
|
166
|
+
return new ScheduledAgentFailure(code, message);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private static String safeCode(Object value, String fallback) {
|
|
170
|
+
if (!(value instanceof String)) return fallback;
|
|
171
|
+
String code = ((String) value).trim();
|
|
172
|
+
return code.matches("[A-Za-z0-9._-]{1,64}") ? code : fallback;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private static String safeMessage(Object value, String fallback) {
|
|
176
|
+
if (!(value instanceof String)) return fallback;
|
|
177
|
+
String message = ((String) value).trim().replaceAll("\\s+", " ");
|
|
178
|
+
if (message.isEmpty()) return fallback;
|
|
179
|
+
return message.substring(0, Math.min(message.length(), 512));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@PreDestroy
|
|
183
|
+
public void close() { workers.shutdownNow(); }
|
|
184
|
+
|
|
185
|
+
private static final class ScheduledAgentFailure extends IllegalStateException {
|
|
186
|
+
private final String code;
|
|
187
|
+
private final String userMessage;
|
|
188
|
+
|
|
189
|
+
private ScheduledAgentFailure(String code, String userMessage) {
|
|
190
|
+
this.code = code;
|
|
191
|
+
this.userMessage = userMessage;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private static class NamedThreadFactory implements ThreadFactory {
|
|
196
|
+
private final AtomicInteger sequence = new AtomicInteger();
|
|
197
|
+
|
|
198
|
+
@Override
|
|
199
|
+
public Thread newThread(Runnable runnable) {
|
|
200
|
+
Thread thread = new Thread(runnable,
|
|
201
|
+
"scheduled-task-" + sequence.incrementAndGet());
|
|
202
|
+
thread.setDaemon(true);
|
|
203
|
+
return thread;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
package/ark-control/src/main/java/com/union/control/service/impl/AgentExecutionServiceImpl.java
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
package com.union.control.service.impl;
|
|
2
|
+
|
|
3
|
+
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
4
|
+
import com.union.control.mapper.AgentExecutionMapper;
|
|
5
|
+
import com.union.control.mapper.ConversationMapper;
|
|
6
|
+
import com.union.control.service.AgentExecutionService;
|
|
7
|
+
import org.springframework.dao.DuplicateKeyException;
|
|
8
|
+
import org.springframework.stereotype.Service;
|
|
9
|
+
import org.springframework.transaction.annotation.Transactional;
|
|
10
|
+
|
|
11
|
+
import java.util.HashMap;
|
|
12
|
+
import java.util.LinkedHashMap;
|
|
13
|
+
import java.util.List;
|
|
14
|
+
import java.util.Map;
|
|
15
|
+
import java.util.NoSuchElementException;
|
|
16
|
+
|
|
17
|
+
import static com.union.control.utils.AgentSupport.*;
|
|
18
|
+
|
|
19
|
+
@Service("agentExecutionService")
|
|
20
|
+
public class AgentExecutionServiceImpl implements AgentExecutionService {
|
|
21
|
+
private final AgentExecutionMapper executionMapper;
|
|
22
|
+
private final ConversationMapper conversationMapper;
|
|
23
|
+
private final ObjectMapper json;
|
|
24
|
+
|
|
25
|
+
public AgentExecutionServiceImpl(
|
|
26
|
+
AgentExecutionMapper executionMapper,
|
|
27
|
+
ConversationMapper conversationMapper,
|
|
28
|
+
ObjectMapper json) {
|
|
29
|
+
this.executionMapper = executionMapper;
|
|
30
|
+
this.conversationMapper = conversationMapper;
|
|
31
|
+
this.json = json;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Transactional
|
|
35
|
+
public Map<String, Object> claimAguiRun(String payload) {
|
|
36
|
+
Map<String, Object> input = request(json, payload);
|
|
37
|
+
String userId = userId(input);
|
|
38
|
+
String conversationId = text(input, "threadId", 64, true);
|
|
39
|
+
String runId = text(input, "runId", 64, true);
|
|
40
|
+
requireId(conversationId);
|
|
41
|
+
requireExecutionToken(runId);
|
|
42
|
+
String title = conversationTitle(input);
|
|
43
|
+
try {
|
|
44
|
+
conversationMapper.insertConversation(conversationId, userId, title);
|
|
45
|
+
} catch (DuplicateKeyException ignored) {
|
|
46
|
+
// Existing active conversation is expected on subsequent turns.
|
|
47
|
+
}
|
|
48
|
+
requireOwnedActive(userId, conversationId);
|
|
49
|
+
executionMapper.insertRootExecution(runId, conversationId, userId);
|
|
50
|
+
return ok(loadExecution(userId, conversationId, runId, false));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Transactional
|
|
54
|
+
public Map<String, Object> completeRun(String input) {
|
|
55
|
+
Map<String, Object> payload = request(json, input);
|
|
56
|
+
String userId = userId(payload);
|
|
57
|
+
String conversationId = text(payload, "conversationId", 64, true);
|
|
58
|
+
String rootRunId = text(payload, "runId", 64, true);
|
|
59
|
+
requireId(conversationId);
|
|
60
|
+
requireExecutionToken(rootRunId);
|
|
61
|
+
if (conversationMapper.requireOwned(conversationId, userId, true) == null)
|
|
62
|
+
throw new NoSuchElementException("会话不存在");
|
|
63
|
+
Map<String, Object> root = loadExecution(
|
|
64
|
+
userId, conversationId, rootRunId, true);
|
|
65
|
+
if (root.get("parentExecutionId") != null)
|
|
66
|
+
throw new IllegalArgumentException("runId 不是 root execution");
|
|
67
|
+
|
|
68
|
+
Object rawExecutions = payload.get("executions");
|
|
69
|
+
if (!(rawExecutions instanceof List) || ((List<?>) rawExecutions).isEmpty())
|
|
70
|
+
throw new IllegalArgumentException("executions 非法");
|
|
71
|
+
Map<String, Map<String, Object>> requested = parseExecutionRows(
|
|
72
|
+
(List<?>) rawExecutions,
|
|
73
|
+
rootRunId);
|
|
74
|
+
Object rawMessages = payload.get("messages");
|
|
75
|
+
if (!(rawMessages instanceof List))
|
|
76
|
+
throw new IllegalArgumentException("messages 非法");
|
|
77
|
+
Map<String, Map<String, Object>> persisted = persistExecutions(
|
|
78
|
+
userId, conversationId, rootRunId, root, requested);
|
|
79
|
+
persistMessages(userId, conversationId, persisted, (List<?>) rawMessages);
|
|
80
|
+
for (Map<String, Object> value : requested.values())
|
|
81
|
+
finishExecution(persisted.get(String.valueOf(value.get("runId"))),
|
|
82
|
+
String.valueOf(value.get("status")), (String) value.get("errorCode"));
|
|
83
|
+
conversationMapper.touchConversation(conversationId, userId);
|
|
84
|
+
return ok(null);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private Map<String, Map<String, Object>> persistExecutions(
|
|
88
|
+
String userId, String conversationId, String rootRunId,
|
|
89
|
+
Map<String, Object> root, Map<String, Map<String, Object>> requested) {
|
|
90
|
+
executionMapper.updateRootAgent(number(root.get("id")),
|
|
91
|
+
String.valueOf(requested.get(rootRunId).get("agentName")));
|
|
92
|
+
Map<String, Map<String, Object>> persisted = new HashMap<>();
|
|
93
|
+
persisted.put(rootRunId, root);
|
|
94
|
+
for (Map<String, Object> value : requested.values()) {
|
|
95
|
+
String runId = String.valueOf(value.get("runId"));
|
|
96
|
+
if (rootRunId.equals(runId)) continue;
|
|
97
|
+
String parentRunId = String.valueOf(value.get("parentRunId"));
|
|
98
|
+
Map<String, Object> parent = persisted.get(parentRunId);
|
|
99
|
+
if (parent == null) throw new IllegalArgumentException("parent execution 不存在");
|
|
100
|
+
executionMapper.insertChildExecution(runId, conversationId, userId,
|
|
101
|
+
number(parent.get("id")), String.valueOf(value.get("agentName")),
|
|
102
|
+
(String) value.get("delegationToolCallId"), (String) value.get("task"));
|
|
103
|
+
persisted.put(runId, loadExecution(userId, conversationId, runId, true));
|
|
104
|
+
}
|
|
105
|
+
return persisted;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private void persistMessages(
|
|
109
|
+
String userId, String conversationId,
|
|
110
|
+
Map<String, Map<String, Object>> executions, List<?> messages) {
|
|
111
|
+
Long current = conversationMapper.currentMessageSequence(conversationId);
|
|
112
|
+
long sequence = current == null ? 0 : current;
|
|
113
|
+
for (Object raw : messages) {
|
|
114
|
+
if (!(raw instanceof Map)) throw new IllegalArgumentException("AG-UI message 必须是对象");
|
|
115
|
+
Map<?, ?> envelope = (Map<?, ?>) raw;
|
|
116
|
+
String messageRunId = text(envelope, "runId", 64, true);
|
|
117
|
+
Object rawMessage = envelope.get("message");
|
|
118
|
+
if (!(rawMessage instanceof Map)) throw new IllegalArgumentException("message 非法");
|
|
119
|
+
@SuppressWarnings("unchecked") Map<String, Object> message =
|
|
120
|
+
(Map<String, Object>) rawMessage;
|
|
121
|
+
String messageId = text(message, "id", 128, true);
|
|
122
|
+
String role = text(message, "role", 32, true);
|
|
123
|
+
Map<String, Object> execution = executions.get(messageRunId);
|
|
124
|
+
if (execution == null)
|
|
125
|
+
throw new IllegalArgumentException("message execution 不存在");
|
|
126
|
+
Map<String, Object> messagePayload = new LinkedHashMap<>(message);
|
|
127
|
+
messagePayload.remove("id");
|
|
128
|
+
messagePayload.remove("role");
|
|
129
|
+
String messageJson;
|
|
130
|
+
try {
|
|
131
|
+
messageJson = json.writeValueAsString(messagePayload);
|
|
132
|
+
} catch (Exception error) {
|
|
133
|
+
throw new IllegalArgumentException("AG-UI message 不是有效 JSON");
|
|
134
|
+
}
|
|
135
|
+
conversationMapper.insertMessage(conversationId, userId, messageId,
|
|
136
|
+
number(execution.get("id")), role, ++sequence, messageJson);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private Map<String, Object> findExecution(
|
|
141
|
+
String userId, String conversationId, String runId, boolean lock) {
|
|
142
|
+
List<Map<String, Object>> rows = executionMapper.findExecution(
|
|
143
|
+
userId, conversationId, runId, lock);
|
|
144
|
+
return rows.isEmpty() ? null : rows.get(0);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private Map<String, Object> loadExecution(
|
|
148
|
+
String userId, String conversationId, String runId, boolean lock) {
|
|
149
|
+
requireId(conversationId);
|
|
150
|
+
requireExecutionToken(runId);
|
|
151
|
+
Map<String, Object> execution = findExecution(userId, conversationId, runId, lock);
|
|
152
|
+
if (execution == null) throw new IllegalStateException("execution 已失效");
|
|
153
|
+
return execution;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private void finishExecution(
|
|
157
|
+
Map<String, Object> execution, String status, String errorCode) {
|
|
158
|
+
int updated = executionMapper.finishExecution(number(execution.get("id")), status, errorCode);
|
|
159
|
+
if (updated != 1) throw new IllegalStateException("execution 已失效");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private Map<String, Map<String, Object>> parseExecutionRows(
|
|
163
|
+
List<?> values, String rootRunId) {
|
|
164
|
+
Map<String, Map<String, Object>> result = new LinkedHashMap<>();
|
|
165
|
+
for (Object raw : values) {
|
|
166
|
+
if (!(raw instanceof Map)) throw new IllegalArgumentException("execution 非法");
|
|
167
|
+
Map<?, ?> value = (Map<?, ?>) raw;
|
|
168
|
+
String runId = text(value, "runId", 64, true);
|
|
169
|
+
requireExecutionToken(runId);
|
|
170
|
+
String parentRunId = text(value, "parentRunId", 64, false);
|
|
171
|
+
String agentName = text(value, "agentName", 128, true);
|
|
172
|
+
String delegationCallId = text(value, "delegationToolCallId", 128, false);
|
|
173
|
+
String task = text(value, "task", 1000, false);
|
|
174
|
+
String status = text(value, "status", 32, true);
|
|
175
|
+
String errorCode = text(value, "errorCode", 64, false);
|
|
176
|
+
Map<String, Object> normalized = new LinkedHashMap<>();
|
|
177
|
+
normalized.put("runId", runId);
|
|
178
|
+
normalized.put("parentRunId", parentRunId);
|
|
179
|
+
normalized.put("agentName", agentName);
|
|
180
|
+
normalized.put("delegationToolCallId", delegationCallId);
|
|
181
|
+
normalized.put("task", task);
|
|
182
|
+
normalized.put("status", status);
|
|
183
|
+
normalized.put("errorCode", errorCode);
|
|
184
|
+
if (result.put(runId, normalized) != null)
|
|
185
|
+
throw new IllegalArgumentException("execution runId 重复");
|
|
186
|
+
}
|
|
187
|
+
if (!result.containsKey(rootRunId))
|
|
188
|
+
throw new IllegalArgumentException("executions 缺少 root");
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private void requireOwnedActive(String userId, String conversationId) {
|
|
193
|
+
if (conversationMapper.requireOwnedActive(conversationId, userId) == null) {
|
|
194
|
+
throw new NoSuchElementException("会话不存在或未激活");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
private static String conversationTitle(Map<String, Object> input) {
|
|
201
|
+
Object raw = input.get("messages");
|
|
202
|
+
if (!(raw instanceof List) || ((List<?>) raw).isEmpty()) return "新会话";
|
|
203
|
+
Object item = ((List<?>) raw).get(((List<?>) raw).size() - 1);
|
|
204
|
+
if (!(item instanceof Map)) return "新会话";
|
|
205
|
+
Map<?, ?> message = (Map<?, ?>) item;
|
|
206
|
+
if (!(message.get("content") instanceof String)) return "新会话";
|
|
207
|
+
String value = ((String) message.get("content")).trim();
|
|
208
|
+
if (value.isEmpty()) return "新会话";
|
|
209
|
+
return value.substring(0, Math.min(value.length(), 255));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
}
|