feat: 完善 Agent 标准交互与安全运行时

- 接入 AG-UI 运行投影、Turn 时间线和审批隔离

- 增加 Agent Skill 冻结绑定与运行时消费闭环

- 增加受控工作区、内置工具和私有 Artifact 生命周期
This commit is contained in:
2026-08-19 22:13:41 +08:00
parent 91d66e636d
commit 4e8640dcaf
241 changed files with 24382 additions and 2777 deletions

View File

@@ -39,6 +39,7 @@ public class ActionLogReporterProperties {
"/images/**",
"/favicon.ico",
"/api/v1/agent/media/**",
"/api/v1/agent/artifacts/*/content",
"/actuator/**",
"*.js",
"*.css",

View File

@@ -6,6 +6,7 @@ import jakarta.servlet.http.HttpServletResponse;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
@@ -26,6 +27,8 @@ import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE;
)
public class ResponseCachingFilter implements Filter {
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
private final ActionLogReporterProperties logProperties;
/**
@@ -121,18 +124,10 @@ public class ResponseCachingFilter implements Filter {
* @return 是否匹配
*/
private boolean match(String path, String pattern) {
if (pattern.equals("/**")) {
return true;
if (pattern.startsWith("/")) {
return PATH_MATCHER.match(pattern, path);
}
if (pattern.endsWith("/**")) {
String prefix = pattern.substring(0, pattern.length() - 3);
return path.startsWith(prefix);
}
if (pattern.endsWith("*")) {
String prefix = pattern.substring(0, pattern.length() - 1);
return path.startsWith(prefix);
}
if (pattern.contains("*") && !pattern.contains("/**")) {
if (pattern.contains("*")) {
// 支持 *.js, *.css
String p = pattern.replace("*", "").replace(".", "\\.");
return path.matches(".*" + p + ".*");

View File

@@ -4,7 +4,10 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.Test;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -32,4 +35,63 @@ public class ResponseCachingFilterTest {
verify(chain).doFilter(request, response);
}
/**
* 验证 Artifact 正文下载保持原始响应,避免异步正文被空缓存提前完成。
*
* @throws Exception Filter 执行失败
*/
@Test
public void agentArtifactContentShouldBypassResponseCaching() throws Exception {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
FilterChain chain = mock(FilterChain.class);
when(request.getMethod()).thenReturn("GET");
when(request.getRequestURI()).thenReturn("/api/v1/agent/artifacts/artifact-1/content");
ResponseCachingFilter filter = new ResponseCachingFilter(new ActionLogReporterProperties());
filter.doFilter(request, response, chain);
verify(chain).doFilter(request, response);
}
/**
* 验证 Artifact 元数据接口仍经过日志正文包装。
*
* @throws Exception Filter 执行失败
*/
@Test
public void agentArtifactMetadataShouldRemainCacheable() throws Exception {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
FilterChain chain = mock(FilterChain.class);
when(request.getMethod()).thenReturn("GET");
when(request.getRequestURI()).thenReturn("/api/v1/agent/artifacts/artifact-1");
ResponseCachingFilter filter = new ResponseCachingFilter(new ActionLogReporterProperties());
filter.doFilter(request, response, chain);
verify(chain).doFilter(
isA(ContentCachingRequestWrapper.class), isA(ContentCachingResponseWrapper.class));
}
/**
* 验证近似路径不会误命中 Artifact 正文下载排除规则。
*
* @throws Exception Filter 执行失败
*/
@Test
public void artifactContentChildPathShouldRemainCacheable() throws Exception {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
FilterChain chain = mock(FilterChain.class);
when(request.getMethod()).thenReturn("GET");
when(request.getRequestURI()).thenReturn("/api/v1/agent/artifacts/artifact-1/content/preview");
ResponseCachingFilter filter = new ResponseCachingFilter(new ActionLogReporterProperties());
filter.doFilter(request, response, chain);
verify(chain).doFilter(
isA(ContentCachingRequestWrapper.class), isA(ContentCachingResponseWrapper.class));
}
}