mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-11-02 14:11:49 +08:00
fix(agent): continue unfinished work after finalization review
This commit is contained in:
parent
2efd519904
commit
da67c5e8b9
@ -46,13 +46,18 @@ public final class TaskFinalizationCoordinator {
|
|||||||
Endorse recovery only for canonical supportedRecoveryCallIds; an empty list means none is established.
|
Endorse recovery only for canonical supportedRecoveryCallIds; an empty list means none is established.
|
||||||
Do not invent numeric results or reconstruct omitted results.
|
Do not invent numeric results or reconstruct omitted results.
|
||||||
Unsupported candidate prose requires REVISE with empty recoveryClaims and a specific correction.
|
Unsupported candidate prose requires REVISE with empty recoveryClaims and a specific correction.
|
||||||
Unfinished executable objective work requires CONTINUE; do not automatically replay failed calls.
|
Unfinished executable objective work or unsatisfied requested state constraints require CONTINUE, not prose-only REVISE.
|
||||||
|
Explicitly requested pending records are not unfinished executable work; compare them to the original constraints.
|
||||||
|
If a record requested unchanged was modified, request its authorized restoration with CONTINUE; do not merely revise the narrative.
|
||||||
|
Do not automatically replay failed calls.
|
||||||
An empty recoveryClaims does not excuse unsupported candidate prose. Inspect every prose, table and JSON
|
An empty recoveryClaims does not excuse unsupported candidate prose. Inspect every prose, table and JSON
|
||||||
claim independently. If the candidate implies an unsupported retry, parameter-fix success or original-call
|
claim independently. If the candidate implies an unsupported retry, parameter-fix success or original-call
|
||||||
recovery, return REVISE with empty recoveryClaims, even if its task-level recovery flag is valid.
|
recovery, return REVISE with empty recoveryClaims, even if its task-level recovery flag is valid.
|
||||||
Return ONLY one JSON object with exactly these six fields. No markdown fences, preamble, trailing text, or extra explanations.
|
Return ONLY one JSON object with exactly these six fields. No markdown fences, preamble, trailing text, or extra explanations.
|
||||||
Decision choices: ACCEPT, REVISE, CONTINUE. Choose exactly one value, never a combined value.
|
Decision choices: ACCEPT, REVISE, CONTINUE. Choose exactly one value, never a combined value.
|
||||||
The reason must be at most 256 characters: one short evidence-based sentence, without per-stage calculations or receipt lists.
|
The reason must be at most 256 characters: one short evidence-based sentence, without per-stage calculations or receipt lists.
|
||||||
|
For REVISE, quote the short offending clause and give a concrete grounded replacement in that same reason.
|
||||||
|
Distinguish a false factual claim from truthful negation or explicitly scoped later successes; do not reject those merely for containing recovery words.
|
||||||
Valid ACCEPT example (replace values with the actual verified facts):
|
Valid ACCEPT example (replace values with the actual verified facts):
|
||||||
{"decision":"ACCEPT","taskComplete":true,"remainingWork":false,
|
{"decision":"ACCEPT","taskComplete":true,"remainingWork":false,
|
||||||
"failedCallCount":0,"recoveryClaims":[],"reason":"All requested stages and aggregate match canonical outputs."}
|
"failedCallCount":0,"recoveryClaims":[],"reason":"All requested stages and aggregate match canonical outputs."}
|
||||||
|
|||||||
@ -151,7 +151,11 @@ public final class TaskFinalizationGate {
|
|||||||
}
|
}
|
||||||
if (verdict.decision() == Outcome.ACCEPT) return new Result(Outcome.ACCEPT, verdict.reason());
|
if (verdict.decision() == Outcome.ACCEPT) return new Result(Outcome.ACCEPT, verdict.reason());
|
||||||
if (attempts >= MAX_CORRECTIVE_ATTEMPTS || !canContinue) return incomplete("Correction allowance exhausted");
|
if (attempts >= MAX_CORRECTIVE_ATTEMPTS || !canContinue) return incomplete("Correction allowance exhausted");
|
||||||
return new Result(verdict.decision(), verdict.reason());
|
// Work-state facts take precedence over a model's prose-only repair label.
|
||||||
|
// This still consumes the same bounded correction allowance and never accepts a draft.
|
||||||
|
Outcome next = verdict.decision() == Outcome.REVISE && !verdict.taskComplete() && verdict.remainingWork()
|
||||||
|
? Outcome.CONTINUE : verdict.decision();
|
||||||
|
return new Result(next, verdict.reason());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Result incomplete(String reason) { return new Result(Outcome.INCOMPLETE, reason); }
|
private static Result incomplete(String reason) { return new Result(Outcome.INCOMPLETE, reason); }
|
||||||
|
|||||||
@ -26,6 +26,16 @@ class TaskFinalizationCoordinatorTest {
|
|||||||
static String json(String decision,String claims){return "{\"decision\":\""+decision+"\",\"taskComplete\":true,\"remainingWork\":false,\"failedCallCount\":1,\"recoveryClaims\":"+claims+",\"reason\":\"checked\"}";}
|
static String json(String decision,String claims){return "{\"decision\":\""+decision+"\",\"taskComplete\":true,\"remainingWork\":false,\"failedCallCount\":1,\"recoveryClaims\":"+claims+",\"reason\":\"checked\"}";}
|
||||||
NodeStreamingChatHelper.StreamResult result(String text){return new NodeStreamingChatHelper.StreamResult(text,"checker thinking",null,List.of(),false,7,3,false,null,NodeStreamingChatHelper.ErrorType.NONE,false,2,1,1);}
|
NodeStreamingChatHelper.StreamResult result(String text){return new NodeStreamingChatHelper.StreamResult(text,"checker thinking",null,List.of(),false,7,3,false,null,NodeStreamingChatHelper.ErrorType.NONE,false,2,1,1);}
|
||||||
TaskFinalizationCoordinator.Review review(int window,int attempts,int used){return coordinator.review(model,"c","Calculate all stages","candidate",ledger,"untrusted progress",window,attempts,10,used,true,new RunState());}
|
TaskFinalizationCoordinator.Review review(int window,int attempts,int used){return coordinator.review(model,"c","Calculate all stages","candidate",ledger,"untrusted progress",window,attempts,10,used,true,new RunState());}
|
||||||
|
@Test void revisionFeedbackRequestsSpecificOffendingClauseAndGroundedReplacement() {
|
||||||
|
when(helper.streamCallSilent(any(),any(),anyString(),anyString())).thenAnswer(call->{
|
||||||
|
String prompt=((Prompt)call.getArgument(1)).getContents();
|
||||||
|
assertTrue(prompt.contains("quote the short offending clause"));
|
||||||
|
assertTrue(prompt.contains("give a concrete grounded replacement"));
|
||||||
|
assertTrue(prompt.contains("at most 256 characters"));
|
||||||
|
return result(json("REVISE","[]"));
|
||||||
|
});
|
||||||
|
assertEquals(TaskFinalizationGate.Outcome.REVISE,review(16000,0,0).result().outcome());
|
||||||
|
}
|
||||||
@Test void latestAggregateAfterPreviewIsGroundedFromExactRetainedResponse() {
|
@Test void latestAggregateAfterPreviewIsGroundedFromExactRetainedResponse() {
|
||||||
String output="x".repeat(600)+" totalSquareSum=2094116873200";
|
String output="x".repeat(600)+" totalSquareSum=2094116873200";
|
||||||
var receipts=ActionExecutionLedger.fromEvents(List.of(GraphEventPublisher.toolComplete("aggregate","execute_code",output,true)));
|
var receipts=ActionExecutionLedger.fromEvents(List.of(GraphEventPublisher.toolComplete("aggregate","execute_code",output,true)));
|
||||||
|
|||||||
@ -113,10 +113,21 @@ class TaskFinalizationGateTest {
|
|||||||
assertEquals(TaskFinalizationGate.Outcome.REVISE,gate.evaluate(revision,ActionExecutionLedger.empty(),0,false,true).outcome());
|
assertEquals(TaskFinalizationGate.Outcome.REVISE,gate.evaluate(revision,ActionExecutionLedger.empty(),0,false,true).outcome());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test void unfinishedRevisionRoutesToExecutionRatherThanProseOnlyRepair() {
|
||||||
|
var verdict=gate.parse(json("REVISE",false,true,0,"[]")).orElseThrow();
|
||||||
|
assertEquals(TaskFinalizationGate.Outcome.CONTINUE,
|
||||||
|
gate.evaluate(verdict,ActionExecutionLedger.empty(),0,false,true).outcome());
|
||||||
|
for(int attempts:List.of(0,1)) {
|
||||||
|
assertEquals(TaskFinalizationGate.Outcome.INCOMPLETE,
|
||||||
|
gate.evaluate(verdict,ActionExecutionLedger.empty(),attempts,false,false).outcome());
|
||||||
|
}
|
||||||
|
assertEquals(TaskFinalizationGate.Outcome.INCOMPLETE,
|
||||||
|
gate.evaluate(verdict,ActionExecutionLedger.empty(),2,false,true).outcome());
|
||||||
|
}
|
||||||
@Test void attemptsAreTotalAndTerminalStateTakesPrecedence() {
|
@Test void attemptsAreTotalAndTerminalStateTakesPrecedence() {
|
||||||
for(String decision:List.of("CONTINUE","REVISE")) {
|
for(String decision:List.of("CONTINUE","REVISE")) {
|
||||||
var verdict=gate.parse(json(decision,false,true,0,"[]")).orElseThrow();
|
var verdict=gate.parse(json(decision,false,true,0,"[]")).orElseThrow();
|
||||||
assertEquals(TaskFinalizationGate.Outcome.valueOf(decision),gate.evaluate(verdict,ActionExecutionLedger.empty(),1,false,true).outcome());
|
assertEquals(TaskFinalizationGate.Outcome.CONTINUE,gate.evaluate(verdict,ActionExecutionLedger.empty(),1,false,true).outcome());
|
||||||
assertEquals(TaskFinalizationGate.Outcome.INCOMPLETE,gate.evaluate(verdict,ActionExecutionLedger.empty(),2,false,true).outcome());
|
assertEquals(TaskFinalizationGate.Outcome.INCOMPLETE,gate.evaluate(verdict,ActionExecutionLedger.empty(),2,false,true).outcome());
|
||||||
assertEquals(TaskFinalizationGate.Outcome.INCOMPLETE,gate.evaluate(verdict,ActionExecutionLedger.empty(),0,true,true).outcome());
|
assertEquals(TaskFinalizationGate.Outcome.INCOMPLETE,gate.evaluate(verdict,ActionExecutionLedger.empty(),0,true,true).outcome());
|
||||||
assertEquals(TaskFinalizationGate.Outcome.INCOMPLETE,gate.evaluate(verdict,ActionExecutionLedger.empty(),0,false,false).outcome());
|
assertEquals(TaskFinalizationGate.Outcome.INCOMPLETE,gate.evaluate(verdict,ActionExecutionLedger.empty(),0,false,false).outcome());
|
||||||
|
|||||||
@ -0,0 +1,137 @@
|
|||||||
|
package vip.mate.agent.graph.node;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.sun.net.httpserver.HttpServer;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
import org.springframework.ai.chat.messages.*;
|
||||||
|
import vip.mate.agent.AgentToolSet;
|
||||||
|
import vip.mate.agent.context.PrefixBudgetPlan;
|
||||||
|
import vip.mate.agent.graph.NodeStreamingChatHelper;
|
||||||
|
import vip.mate.agent.graph.PayloadReduction;
|
||||||
|
import vip.mate.agent.runtime.state.RunState;
|
||||||
|
import vip.mate.channel.web.ChatStreamTracker;
|
||||||
|
import vip.mate.llm.chatmodel.OpenAiCompatibleChatModelBuilder;
|
||||||
|
import vip.mate.llm.model.ModelConfigEntity;
|
||||||
|
import vip.mate.llm.model.ModelProviderEntity;
|
||||||
|
import vip.mate.llm.service.ModelProviderService;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
import static vip.mate.agent.graph.state.MateClawStateKeys.*;
|
||||||
|
|
||||||
|
/** Actual HTTP bodies/statuses through the production provider and reasoning recovery. */
|
||||||
|
class PayloadWireIntegrationTest {
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings={"recover", "mandatory", "stillTooLarge", "reviewTooLarge", "unfinishedRevision"})
|
||||||
|
void boundsWireRetriesAndPreservesMandatoryUtf8Input(String mode) throws Exception {
|
||||||
|
var bodies = Collections.synchronizedList(new ArrayList<byte[]>());
|
||||||
|
var server = HttpServer.create(new InetSocketAddress("127.0.0.1",0),0);
|
||||||
|
server.createContext("/", exchange -> {
|
||||||
|
byte[] request = exchange.getRequestBody().readAllBytes();
|
||||||
|
bodies.add(request);
|
||||||
|
boolean reject = mode.equals("reviewTooLarge") ? bodies.size()>1
|
||||||
|
: !mode.equals("unfinishedRevision") && request.length > (mode.equals("stillTooLarge") ? 64 : 40_000);
|
||||||
|
String response = reject
|
||||||
|
? "{\"error\":{\"message\":\"body limit exceeded\",\"type\":\"invalid_request_error\"}}"
|
||||||
|
: "data: {\"id\":\"r\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"fixture\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"verified result\"},\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n";
|
||||||
|
if(mode.equals("unfinishedRevision") && bodies.size()>1) {
|
||||||
|
String verdict="{\"decision\":\"REVISE\",\"taskComplete\":false,\"remainingWork\":true,\"failedCallCount\":1,\"recoveryClaims\":[],\"reason\":\"Restore the requested pending checkpoint before answering\"}";
|
||||||
|
response=response.replace("\"verified result\"",new ObjectMapper().writeValueAsString(verdict));
|
||||||
|
}
|
||||||
|
byte[] bytes=response.getBytes(StandardCharsets.UTF_8);
|
||||||
|
exchange.getResponseHeaders().set("Content-Type",reject?"application/json":"text/event-stream");
|
||||||
|
exchange.sendResponseHeaders(reject?413:200,bytes.length);
|
||||||
|
exchange.getResponseBody().write(bytes);
|
||||||
|
exchange.close();
|
||||||
|
});
|
||||||
|
server.start();
|
||||||
|
try {
|
||||||
|
var service=mock(ModelProviderService.class);
|
||||||
|
when(service.isProviderConfigured(anyString())).thenReturn(true);
|
||||||
|
when(service.readProviderGenerateKwargs(any())).thenReturn(Map.of());
|
||||||
|
var config=new ModelConfigEntity(); config.setModelName("fixture"); config.setRequestTimeoutSeconds(3);
|
||||||
|
var provider=new ModelProviderEntity(); provider.setProviderId("fixture"); provider.setRequireApiKey(false);
|
||||||
|
provider.setBaseUrl("http://127.0.0.1:"+server.getAddress().getPort());
|
||||||
|
var model=new OpenAiCompatibleChatModelBuilder(service,null,null,null).build(config,provider,null);
|
||||||
|
String schema="{\"type\":\"object\",\"properties\":{\"language\":{\"type\":\"string\"},\"code\":{\"type\":\"string\"},\"args\":{\"type\":\"string\"}},\"required\":[\"language\",\"code\"],\"additionalProperties\":false}";
|
||||||
|
var callback=new org.springframework.ai.tool.ToolCallback() {
|
||||||
|
public org.springframework.ai.tool.definition.ToolDefinition getToolDefinition() {
|
||||||
|
return org.springframework.ai.tool.definition.ToolDefinition.builder().name("execute_code")
|
||||||
|
.description("Synthetic fixture only").inputSchema(schema).build();
|
||||||
|
}
|
||||||
|
public String call(String arguments) { throw new AssertionError("History recovery must not replay tools"); }
|
||||||
|
};
|
||||||
|
var node=new ReasoningNode(model,AgentToolSet.fromCallbacks(List.of(),List.of(callback)),null,false,
|
||||||
|
new NodeStreamingChatHelper(mock(ChatStreamTracker.class)),null,null,1024,null,null,null,null);
|
||||||
|
node.setPrefixBudgetPlan(new PrefixBudgetPlan(true,1_000_000,PrefixBudgetPlan.Profile.NORMAL,0,0,0,0,0,0,0));
|
||||||
|
String objective="必要输入:保持原文与引号 \"参数\" 🧪";
|
||||||
|
if (mode.equals("mandatory")) objective = objective.repeat(2000);
|
||||||
|
String originalArguments=new ObjectMapper().writeValueAsString(Map.of(
|
||||||
|
"language","python","code","print(\"历史🧪\")","args","[\"空 格\",\"back\\\\slash\"]"));
|
||||||
|
List<Message> canonical=new ArrayList<>(); canonical.add(new UserMessage(objective));
|
||||||
|
if (!mode.equals("mandatory")) for(int i=0;i<20;i++) {
|
||||||
|
String id="wire-"+i;
|
||||||
|
canonical.add(AssistantMessage.builder().content("").toolCalls(List.of(
|
||||||
|
new AssistantMessage.ToolCall(id,"function","execute_code",originalArguments))).build());
|
||||||
|
canonical.add(ToolResponseMessage.builder().responses(List.of(
|
||||||
|
new ToolResponseMessage.ToolResponse(id,"execute_code","历史🧪".repeat(500)))).build());
|
||||||
|
}
|
||||||
|
int originalSize=canonical.size();
|
||||||
|
String originalHistory=canonical.toString();
|
||||||
|
var values=new HashMap<String,Object>();
|
||||||
|
boolean guardedReview=mode.equals("reviewTooLarge") || mode.equals("unfinishedRevision");
|
||||||
|
if(guardedReview) {
|
||||||
|
values.put(CURRENT_ITERATION,2);
|
||||||
|
var events=List.of(vip.mate.agent.GraphEventPublisher.toolComplete("f","calculate","missing language",false),
|
||||||
|
vip.mate.agent.GraphEventPublisher.toolComplete("s","calculate","42",true));
|
||||||
|
values.put(PENDING_EVENTS,events);
|
||||||
|
values.put(ACTION_EXECUTION_LEDGER,vip.mate.agent.graph.state.ActionExecutionLedger.fromEvents(events));
|
||||||
|
node.setFinalizationCoordinator(new vip.mate.agent.graph.guard.TaskFinalizationCoordinator(
|
||||||
|
new NodeStreamingChatHelper(mock(ChatStreamTracker.class)),null));
|
||||||
|
}
|
||||||
|
values.putAll(Map.of(CONVERSATION_ID,"wire-"+mode,SYSTEM_PROMPT,"MANDATORY_SKILL",
|
||||||
|
USER_MESSAGE,objective,MESSAGES,canonical,MAX_ITERATIONS,100));
|
||||||
|
var output=node.apply(new RunState(values));
|
||||||
|
int expected=mode.equals("mandatory")?1:2;
|
||||||
|
assertEquals(expected,bodies.size(),"no SDK/helper/fallback or unchanged payload retries");
|
||||||
|
assertEquals(expected,output.get(LLM_CALL_COUNT));
|
||||||
|
assertEquals(originalSize,canonical.size(),"canonical history must survive local reduction");
|
||||||
|
assertEquals(originalHistory,canonical.toString(),"history contents and arguments must remain intact");
|
||||||
|
if(expected==2 && !guardedReview) assertTrue(bodies.get(1).length<bodies.get(0).length,"serialized UTF-8 request must shrink");
|
||||||
|
var json=new ObjectMapper();
|
||||||
|
for(byte[] body:bodies) {
|
||||||
|
JsonNode messages=json.readTree(body).get("messages");
|
||||||
|
boolean hasObjective=false,hasSkill=false;
|
||||||
|
Set<String> calls=new HashSet<>(),responses=new HashSet<>();
|
||||||
|
for(JsonNode message:messages) {
|
||||||
|
hasObjective |= message.path("content").asText().contains(objective);
|
||||||
|
hasSkill |= message.path("content").asText().contains("MANDATORY_SKILL");
|
||||||
|
for(JsonNode call:message.path("tool_calls")) {
|
||||||
|
calls.add(call.path("id").asText());
|
||||||
|
assertEquals(originalArguments,call.at("/function/arguments").asText());
|
||||||
|
}
|
||||||
|
if(message.path("role").asText().equals("tool")) responses.add(message.path("tool_call_id").asText());
|
||||||
|
}
|
||||||
|
assertTrue(hasObjective);
|
||||||
|
if(!guardedReview || body==bodies.get(0)) assertTrue(hasSkill);
|
||||||
|
assertEquals(calls,responses);
|
||||||
|
if(!guardedReview || body==bodies.get(0))
|
||||||
|
assertEquals(json.readTree(schema),json.readTree(body).at("/tools/0/function/parameters"));
|
||||||
|
}
|
||||||
|
if(guardedReview) {
|
||||||
|
if(mode.equals("unfinishedRevision")) {
|
||||||
|
assertEquals(true,output.get(CONTINUE_REASONING));
|
||||||
|
assertTrue(String.valueOf(output.get(MESSAGES)).contains("Requested outcome: CONTINUE"));
|
||||||
|
assertEquals(1,output.get(FINALIZATION_ATTEMPTS));
|
||||||
|
} else assertEquals("incomplete",output.get(FINISH_REASON));
|
||||||
|
assertFalse(String.valueOf(output.get(FINAL_ANSWER)).contains("verified result"));
|
||||||
|
assertEquals(false,output.get(CONTENT_STREAMED));
|
||||||
|
assertFalse(json.readTree(bodies.get(1)).has("tools"),"review must not replay tools");
|
||||||
|
} else assertEquals(mode.equals("recover")?"verified result":"[错误] "+PayloadReduction.INCOMPLETE,output.get(FINAL_ANSWER));
|
||||||
|
} finally { server.stop(0); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -95,12 +95,11 @@ class SchedulingConfigPausedShutdownTest {
|
|||||||
private static void awaitActiveWorkers(ThreadPoolTaskScheduler scheduler, int expected)
|
private static void awaitActiveWorkers(ThreadPoolTaskScheduler scheduler, int expected)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2);
|
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2);
|
||||||
while (scheduler.getScheduledThreadPoolExecutor().getActiveCount() < expected
|
while (System.nanoTime() < deadline) {
|
||||||
&& System.nanoTime() < deadline) {
|
// Keep the successful observation: a second sample can race a worker transition.
|
||||||
|
if (scheduler.getScheduledThreadPoolExecutor().getActiveCount() >= expected) return;
|
||||||
Thread.sleep(5);
|
Thread.sleep(5);
|
||||||
}
|
}
|
||||||
assertTrue(
|
fail("pending tick must reach the lifecycle pause gate");
|
||||||
scheduler.getScheduledThreadPoolExecutor().getActiveCount() >= expected,
|
|
||||||
"pending tick must reach the lifecycle pause gate");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,10 +69,9 @@ class ServletJsonContractTest {
|
|||||||
try (var context =
|
try (var context =
|
||||||
new SpringApplicationBuilder(Application.class)
|
new SpringApplicationBuilder(Application.class)
|
||||||
.properties(
|
.properties(
|
||||||
"server.port=0",
|
|
||||||
"spring.main.banner-mode=off",
|
"spring.main.banner-mode=off",
|
||||||
"spring.http.converters.preferred-json-mapper=jackson2")
|
"spring.http.converters.preferred-json-mapper=jackson2")
|
||||||
.run()) {
|
.run("--server.port=0")) {
|
||||||
int port = ((WebServerApplicationContext) context).getWebServer().getPort();
|
int port = ((WebServerApplicationContext) context).getWebServer().getPort();
|
||||||
var response =
|
var response =
|
||||||
HttpClient.newHttpClient()
|
HttpClient.newHttpClient()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user