feat: 完成 Agent MCP 对接
- 增加 MCP 连接类型、环境检测接口和容器运行环境支持 - 将 Agent 编排改为绑定整体 MCP 并编译为 runtime McpSpec - 优化 MCP 工具展示、审批、草稿试运行和画布回显稳定性
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
package tech.easyflow.agent.runtime;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentDefinition;
|
||||
import com.easyagents.agent.runtime.mcp.McpSpec;
|
||||
import com.easyagents.agent.runtime.mcp.McpTransportType;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
import tech.easyflow.agent.enums.AgentToolType;
|
||||
import tech.easyflow.ai.entity.Mcp;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.entity.ModelProvider;
|
||||
import tech.easyflow.ai.service.McpService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Agent MCP 运行时定义编译测试。
|
||||
*/
|
||||
public class AgentDefinitionCompilerMcpTest {
|
||||
|
||||
/**
|
||||
* 验证 Agent 绑定 MCP 后会编译为 runtime 原生 MCP 声明,并按整个 MCP 暴露工具。
|
||||
*
|
||||
* @throws Exception 反射注入依赖失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void compileShouldBuildWholeMcpSpecWithDynamicPrefixAndApproval() throws Exception {
|
||||
BigInteger modelId = BigInteger.valueOf(10L);
|
||||
BigInteger mcpId = BigInteger.valueOf(20L);
|
||||
Model model = model(modelId);
|
||||
Mcp mcp = mcp(mcpId);
|
||||
AgentDefinitionCompiler compiler = new AgentDefinitionCompiler();
|
||||
setField(compiler, "objectMapper", new com.fasterxml.jackson.databind.ObjectMapper());
|
||||
setField(compiler, "modelService", modelService(model));
|
||||
setField(compiler, "mcpService", mcpService(mcp));
|
||||
|
||||
Agent agent = agent(modelId, mcpId);
|
||||
|
||||
AgentRuntimeBundle bundle = compiler.compile(agent);
|
||||
AgentDefinition definition = bundle.getDefinition();
|
||||
|
||||
Assert.assertTrue(definition.getToolSpecs().isEmpty());
|
||||
Assert.assertTrue(bundle.getToolInvokers().isEmpty());
|
||||
Assert.assertEquals(1, definition.getMcpSpecs().size());
|
||||
McpSpec spec = definition.getMcpSpecs().get(0);
|
||||
Assert.assertEquals("mcp_20", spec.getName());
|
||||
Assert.assertEquals(McpTransportType.STDIO, spec.getTransportType());
|
||||
Assert.assertEquals("npx", spec.getCommand());
|
||||
Assert.assertEquals(List.of("-y", "@modelcontextprotocol/server-everything"), spec.getArgs());
|
||||
Assert.assertTrue(spec.isApprovalRequired());
|
||||
Assert.assertEquals("mcp_20_", spec.getToolNamePrefix());
|
||||
Assert.assertTrue(spec.getToolAliases().isEmpty());
|
||||
Assert.assertTrue(spec.getEnableTools().isEmpty());
|
||||
Assert.assertEquals(AgentToolType.MCP.name(), spec.getMetadata().get("toolType"));
|
||||
Assert.assertEquals(String.valueOf(mcpId), spec.getMetadata().get("mcpId"));
|
||||
Assert.assertEquals("everything", spec.getMetadata().get("serverName"));
|
||||
Assert.assertTrue(spec.getToolApprovalRequests().isEmpty());
|
||||
Assert.assertEquals("确认调用 MCP 工具?", spec.getApprovalRequest().getApprovalPrompt());
|
||||
}
|
||||
|
||||
private Agent agent(BigInteger modelId, BigInteger mcpId) {
|
||||
AgentToolBinding binding = new AgentToolBinding();
|
||||
binding.setToolType(AgentToolType.MCP.name());
|
||||
binding.setTargetId(mcpId);
|
||||
binding.setEnabled(true);
|
||||
binding.setHitlEnabled(true);
|
||||
binding.setHitlConfigJson(Map.of("prompt", "确认调用 MCP 工具?"));
|
||||
|
||||
Agent agent = new Agent();
|
||||
agent.setId(BigInteger.valueOf(1L));
|
||||
agent.setName("MCP Agent");
|
||||
agent.setModelId(modelId);
|
||||
agent.setToolBindings(List.of(binding));
|
||||
return agent;
|
||||
}
|
||||
|
||||
private Model model(BigInteger modelId) {
|
||||
ModelProvider provider = new ModelProvider();
|
||||
provider.setProviderType("openai");
|
||||
provider.setProviderName("OpenAI");
|
||||
Model model = new Model();
|
||||
model.setId(modelId);
|
||||
model.setModelProvider(provider);
|
||||
model.setModelName("gpt-test");
|
||||
model.setEndpoint("https://example.com");
|
||||
model.setRequestPath("/v1/chat/completions");
|
||||
model.setApiKey("test-key");
|
||||
return model;
|
||||
}
|
||||
|
||||
private Mcp mcp(BigInteger mcpId) {
|
||||
Mcp mcp = new Mcp();
|
||||
mcp.setId(mcpId);
|
||||
mcp.setTitle("Everything");
|
||||
mcp.setDescription("MCP Everything");
|
||||
mcp.setApprovalRequired(true);
|
||||
mcp.setStatus(true);
|
||||
mcp.setConfigJson("""
|
||||
{
|
||||
"mcpServers": {
|
||||
"everything": {
|
||||
"transport": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-everything"]
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
return mcp;
|
||||
}
|
||||
|
||||
private ModelService modelService(Model model) {
|
||||
return (ModelService) Proxy.newProxyInstance(
|
||||
ModelService.class.getClassLoader(),
|
||||
new Class<?>[]{ModelService.class},
|
||||
(proxy, method, args) -> "getModelInstance".equals(method.getName()) ? model : defaultValue(method.getReturnType()));
|
||||
}
|
||||
|
||||
private McpService mcpService(Mcp mcp) {
|
||||
return (McpService) Proxy.newProxyInstance(
|
||||
McpService.class.getClassLoader(),
|
||||
new Class<?>[]{McpService.class},
|
||||
(proxy, method, args) -> "getById".equals(method.getName()) ? mcp : defaultValue(method.getReturnType()));
|
||||
}
|
||||
|
||||
private Object defaultValue(Class<?> type) {
|
||||
if (type == boolean.class) {
|
||||
return false;
|
||||
}
|
||||
if (type == int.class || type == long.class || type == short.class || type == byte.class) {
|
||||
return 0;
|
||||
}
|
||||
if (type == double.class || type == float.class) {
|
||||
return 0D;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void setField(Object target, String fieldName, Object value) throws Exception {
|
||||
Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,22 @@
|
||||
package tech.easyflow.agent.runtime;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentInitRequest;
|
||||
import com.easyagents.agent.runtime.AgentRuntime;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
||||
import com.easyagents.agent.runtime.message.AgentKnowledgeReference;
|
||||
import com.easyagents.agent.runtime.message.AgentMessage;
|
||||
import com.easyagents.agent.runtime.message.AgentMessageRole;
|
||||
import com.easyagents.agent.runtime.persistence.session.AgentSessionStore;
|
||||
import com.easyagents.agent.runtime.persistence.session.memory.InMemoryAgentSessionStore;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.agent.entity.AgentHitlPending;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
import tech.easyflow.agent.runtime.event.AgentRunEventRecorder;
|
||||
import tech.easyflow.agent.runtime.hitl.AgentHitlPendingService;
|
||||
import tech.easyflow.agent.runtime.lock.AgentRunLock;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
@@ -402,14 +409,150 @@ public class AgentRunServiceDraftAndHitlTest {
|
||||
|
||||
Exception thrown = Assert.assertThrows(Exception.class, () -> invoke(service, "run",
|
||||
new Class<?>[]{Agent.class, String.class, String.class, String.class, String.class,
|
||||
String.class, ChatRuntimeContext.class, boolean.class},
|
||||
agent, "你好", "request-lock", "trace-lock", "session-lock", "AGENT", context, true));
|
||||
String.class, ChatRuntimeContext.class, boolean.class, AgentSessionStore.class},
|
||||
agent, "你好", "request-lock", "trace-lock", "session-lock", "AGENT", context, true,
|
||||
new InMemoryAgentSessionStore()));
|
||||
|
||||
Assert.assertTrue(rootCause(thrown) instanceof BusinessException);
|
||||
Assert.assertEquals(0, chatRuntimeManager.prepareSessionCount);
|
||||
Assert.assertEquals(0, chatRuntimeManager.recordUserMessageCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证草稿运行会使用独立 session store,且不会绑定 MySQL session 元信息。
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void startRuntimeShouldUseDraftSessionStoreWithoutBindingMysqlSession() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
RecordingAgentDefinitionCompiler compiler = new RecordingAgentDefinitionCompiler();
|
||||
RecordingAgentRuntime runtime = new RecordingAgentRuntime();
|
||||
RecordingAgentRuntimeFactory runtimeFactory = new RecordingAgentRuntimeFactory(runtime);
|
||||
AgentSessionStore draftStore = new InMemoryAgentSessionStore();
|
||||
setField(service, "agentDefinitionCompiler", compiler);
|
||||
setField(service, "agentRuntimeFactory", runtimeFactory);
|
||||
setField(service, "agentRunRegistry", new AgentRunRegistry());
|
||||
|
||||
Agent agent = new Agent();
|
||||
agent.setId(BigInteger.valueOf(100));
|
||||
invoke(service, "startRuntime",
|
||||
new Class<?>[]{Agent.class, String.class, String.class, String.class, String.class, String.class,
|
||||
ChatRuntimeContext.class, ChatSseEmitter.class, boolean.class, AgentSessionStore.class,
|
||||
AgentRunLock.Handle.class},
|
||||
agent, "你好", "request-draft", "trace-draft", "agent-draft-100", "AGENT_DRAFT",
|
||||
chatContext(), new RecordingChatSseEmitter(), false, draftStore, null);
|
||||
|
||||
Assert.assertSame(draftStore, runtime.initRequest.getSessionStore());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证草稿事件不会写运行事件表,正式事件仍会记录。
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void handleRuntimeEventShouldOnlyPersistEventsForFormalChat() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
setField(service, "agentRunRegistry", new AgentRunRegistry());
|
||||
RecordingAgentRunEventRecorder recorder = new RecordingAgentRunEventRecorder();
|
||||
setField(service, "agentRunEventRecorder", recorder);
|
||||
AgentRuntimeEvent draftEvent = AgentRuntimeEvent.of(AgentRuntimeEventType.TOOL_CALL);
|
||||
draftEvent.getPayload().put("toolName", "search");
|
||||
|
||||
invoke(service, "handleRuntimeEvent",
|
||||
runtimeEventParameterTypes(),
|
||||
draftEvent, "request-draft", new RecordingChatSseEmitter(), new StringBuilder(),
|
||||
new ChatAssistantAccumulator(), chatContext(), new AtomicBoolean(false), false);
|
||||
|
||||
Assert.assertEquals(0, recorder.recordCount);
|
||||
|
||||
AgentRuntimeEvent formalEvent = AgentRuntimeEvent.of(AgentRuntimeEventType.TOOL_CALL);
|
||||
formalEvent.getPayload().put("toolName", "search");
|
||||
invoke(service, "handleRuntimeEvent",
|
||||
runtimeEventParameterTypes(),
|
||||
formalEvent, "request-formal", new RecordingChatSseEmitter(), new StringBuilder(),
|
||||
new ChatAssistantAccumulator(), chatContext(), new AtomicBoolean(false), true);
|
||||
|
||||
Assert.assertEquals(1, recorder.recordCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证草稿工具审批只注册内存恢复令牌,不写 HITL pending 表。
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void draftToolApprovalShouldNotPersistPending() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
AgentRunRegistry registry = new AgentRunRegistry();
|
||||
RecordingAgentHitlPendingService pendingService = new RecordingAgentHitlPendingService();
|
||||
setField(service, "agentRunRegistry", registry);
|
||||
setField(service, "agentHitlPendingService", pendingService);
|
||||
registry.register(runContext("request-draft", "agent-draft-tool", false));
|
||||
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED);
|
||||
event.getPayload().put("resumeToken", "token-draft");
|
||||
|
||||
invoke(service, "handleRuntimeEvent",
|
||||
runtimeEventParameterTypes(),
|
||||
event, "request-draft", new RecordingChatSseEmitter(), new StringBuilder(),
|
||||
new ChatAssistantAccumulator(), chatContext(), new AtomicBoolean(false), false);
|
||||
|
||||
Assert.assertTrue(registry.containsResumeTarget("request-draft", "token-draft"));
|
||||
Assert.assertEquals(0, pendingService.recordApprovalRequiredCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证草稿审批恢复不执行 pending 表消费,正式审批仍执行。
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void approveShouldSkipPendingConsumeOnlyForDraftRun() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
AgentRunRegistry registry = new AgentRunRegistry();
|
||||
RecordingAgentHitlPendingService pendingService = new RecordingAgentHitlPendingService();
|
||||
setField(service, "agentRunRegistry", registry);
|
||||
setField(service, "agentHitlPendingService", pendingService);
|
||||
|
||||
registry.register(runContext("request-draft-approve", "agent-draft-approve", false));
|
||||
registry.registerResumeToken("request-draft-approve", "token-draft-approve");
|
||||
invoke(service, "approveRuntime",
|
||||
new Class<?>[]{String.class, String.class, BigInteger.class, String.class},
|
||||
"request-draft-approve", "token-draft-approve", BigInteger.ONE, "1");
|
||||
|
||||
Assert.assertEquals(0, pendingService.approveCount);
|
||||
|
||||
registry.register(runContext("request-formal-approve", "session-formal-approve", true));
|
||||
registry.registerResumeToken("request-formal-approve", "token-formal-approve");
|
||||
invoke(service, "approveRuntime",
|
||||
new Class<?>[]{String.class, String.class, BigInteger.class, String.class},
|
||||
"request-formal-approve", "token-formal-approve", BigInteger.ONE, "1");
|
||||
|
||||
Assert.assertEquals(1, pendingService.approveCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证清理草稿会话只清草稿 store,不触碰 MySQL pending 清理。
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void clearDraftSessionShouldOnlyDeleteDraftStore() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
RecordingAgentHitlPendingService pendingService = new RecordingAgentHitlPendingService();
|
||||
RecordingAgentSessionStore draftStore = new RecordingAgentSessionStore();
|
||||
setField(service, "agentRunRegistry", new AgentRunRegistry());
|
||||
setField(service, "agentHitlPendingService", pendingService);
|
||||
setField(service, "draftAgentSessionStore", draftStore);
|
||||
|
||||
invoke(service, "clearDraftSessionInternal",
|
||||
new Class<?>[]{String.class, String.class}, "agent-draft-clear", "1");
|
||||
|
||||
Assert.assertEquals("agent-draft-clear", draftStore.deletedSessionKey);
|
||||
Assert.assertEquals(0, pendingService.deleteByRuntimeSessionIdCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证正式聊天会在会话准备完成后向前端返回真实会话 ID。
|
||||
*
|
||||
@@ -530,6 +673,28 @@ public class AgentRunServiceDraftAndHitlTest {
|
||||
ChatRuntimeContext.class, AtomicBoolean.class, boolean.class};
|
||||
}
|
||||
|
||||
private AgentRunRegistry.AgentRunContext runContext(String requestId, String sessionId, boolean persistChatlog) {
|
||||
return new AgentRunRegistry.AgentRunContext(
|
||||
requestId,
|
||||
sessionId,
|
||||
new RecordingAgentRuntime(),
|
||||
new RecordingChatSseEmitter(),
|
||||
chatContext(),
|
||||
new StringBuilder(),
|
||||
new ChatAssistantAccumulator(),
|
||||
new AtomicBoolean(false),
|
||||
persistChatlog,
|
||||
new AgentRunRegistry.RunOwner("agent-1", sessionId, "1"),
|
||||
null,
|
||||
event -> {
|
||||
},
|
||||
error -> {
|
||||
},
|
||||
() -> {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private ChatRuntimeContext chatContext() {
|
||||
ChatRuntimeContext context = new ChatRuntimeContext();
|
||||
context.setAssistantId(BigInteger.valueOf(100));
|
||||
@@ -598,6 +763,148 @@ public class AgentRunServiceDraftAndHitlTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static class RecordingAgentRuntime implements AgentRuntime {
|
||||
|
||||
private AgentInitRequest initRequest;
|
||||
private int resumeCount;
|
||||
|
||||
@Override
|
||||
public void init(AgentInitRequest request) {
|
||||
initRequest = request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public reactor.core.publisher.Flux<AgentRuntimeEvent> stream(AgentMessage userMessage) {
|
||||
return reactor.core.publisher.Flux.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public reactor.core.publisher.Flux<AgentRuntimeEvent> resume(com.easyagents.agent.runtime.AgentResumeRequest request) {
|
||||
resumeCount++;
|
||||
return reactor.core.publisher.Flux.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static class RecordingAgentRuntimeFactory implements AgentRuntimeFactory {
|
||||
|
||||
private final AgentRuntime runtime;
|
||||
|
||||
private RecordingAgentRuntimeFactory(AgentRuntime runtime) {
|
||||
this.runtime = runtime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentRuntime create() {
|
||||
return runtime;
|
||||
}
|
||||
}
|
||||
|
||||
private static class RecordingAgentDefinitionCompiler extends AgentDefinitionCompiler {
|
||||
|
||||
@Override
|
||||
public AgentRuntimeBundle compile(Agent agent) {
|
||||
AgentRuntimeBundle bundle = new AgentRuntimeBundle();
|
||||
bundle.setDefinition(new com.easyagents.agent.runtime.AgentDefinition());
|
||||
return bundle;
|
||||
}
|
||||
}
|
||||
|
||||
private static class RecordingAgentRunEventRecorder implements AgentRunEventRecorder {
|
||||
|
||||
private int recordCount;
|
||||
|
||||
@Override
|
||||
public void record(String requestId, ChatRuntimeContext chatContext, AgentRuntimeEvent event) {
|
||||
recordCount++;
|
||||
}
|
||||
}
|
||||
|
||||
private static class RecordingAgentHitlPendingService implements AgentHitlPendingService {
|
||||
|
||||
private int recordApprovalRequiredCount;
|
||||
private int approveCount;
|
||||
private int rejectCount;
|
||||
private int cancelByRequestIdCount;
|
||||
private int deleteByRuntimeSessionIdCount;
|
||||
|
||||
@Override
|
||||
public void recordApprovalRequired(String requestId, ChatRuntimeContext chatContext, AgentRuntimeEvent event) {
|
||||
recordApprovalRequiredCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentHitlPending approve(String resumeToken, BigInteger operatorId) {
|
||||
approveCount++;
|
||||
return new AgentHitlPending();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentHitlPending reject(String resumeToken, BigInteger operatorId, String reason) {
|
||||
rejectCount++;
|
||||
return new AgentHitlPending();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelByRequestId(String requestId, String reason) {
|
||||
cancelByRequestIdCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByChatSessionId(BigInteger chatSessionId) {
|
||||
// 测试桩无需处理。
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByRuntimeSessionId(String runtimeSessionId) {
|
||||
deleteByRuntimeSessionIdCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentHitlPending> expirePending(int limit) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private static class RecordingAgentSessionStore implements AgentSessionStore {
|
||||
|
||||
private String deletedSessionKey;
|
||||
|
||||
@Override
|
||||
public void save(String sessionKey, String name, io.agentscope.core.state.State state) {
|
||||
// 测试桩无需处理。
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveList(String sessionKey, String name, List<? extends io.agentscope.core.state.State> states) {
|
||||
// 测试桩无需处理。
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends io.agentscope.core.state.State> java.util.Optional<T> get(String sessionKey, String name, Class<T> type) {
|
||||
return java.util.Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends io.agentscope.core.state.State> List<T> getList(String sessionKey, String name, Class<T> itemType) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(String sessionKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String sessionKey) {
|
||||
deletedSessionKey = sessionKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.Set<String> listSessionKeys() {
|
||||
return java.util.Set.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录 chatlog 写入动作的测试桩。
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user