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

@@ -31,6 +31,7 @@ import tech.easyflow.approval.mapper.ApprovalTaskMapper;
import tech.easyflow.approval.service.ApprovalActionFacade;
import tech.easyflow.approval.service.ApprovalAssigneeService;
import tech.easyflow.approval.service.ApprovalQueryService;
import tech.easyflow.approval.support.ApprovalSnapshotProjection;
import tech.easyflow.system.entity.SysAccount;
import tech.easyflow.system.service.CategoryPermissionService;
import tech.easyflow.system.service.SysAccountService;
@@ -171,7 +172,8 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
detail.setApplicantId(instance.getApplicantId());
detail.setSubmittedAt(instance.getSubmittedAt());
detail.setFinishedAt(instance.getFinishedAt());
detail.setSnapshotJson(instance.getSnapshotJson());
detail.setSnapshotJson(ApprovalSnapshotProjection.project(
instance.getResourceType(), instance.getSnapshotJson()));
List<ApprovalLog> logs = approvalLogMapper.selectListByQuery(
QueryWrapper.create().eq(ApprovalLog::getInstanceId, instanceId));

View File

@@ -0,0 +1,276 @@
package tech.easyflow.approval.support;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* 审批详情快照的响应投影。
*
* <p>审批实例仍持久化完整冻结快照,详情响应仅投影审核需要的摘要字段,避免内部运行正文、
* 资源快照和连接凭据通过管理端查询接口泄露。</p>
*/
public final class ApprovalSnapshotProjection {
private static final Set<String> STRICT_RESOURCE_TYPES = Set.of("AGENT", "SKILL");
private static final Set<String> REMOVED_KEYS = Set.of(
"skillcontent",
"content",
"contentref",
"resources",
"resourcecontent",
"publishedsnapshotjson",
"publishedtoolbindingsjson",
"prompt",
"systemprompt",
"instructions",
"instruction",
"script",
"source",
"code",
"body",
"text",
"raw",
"payload",
"tools",
"mcptoolmanifest",
"manifest",
"headers",
"header",
"environment",
"env",
"command",
"args",
"arguments",
"query",
"endpoint",
"url",
"uri",
"credentials",
"credential",
"secret",
"password",
"token",
"apikey",
"privatekey",
"inputschema",
"outputschema",
"schema");
private ApprovalSnapshotProjection() {
}
/**
* 将审批冻结快照转换为可下发的响应副本。
*
* @param resourceType 审批资源类型
* @param snapshot 完整冻结快照
* @return 不包含内部正文与连接配置的响应快照;原快照为空时返回空 Map
*/
public static Map<String, Object> project(String resourceType, Map<String, Object> snapshot) {
if (snapshot == null || snapshot.isEmpty()) {
return Map.of();
}
if (!STRICT_RESOURCE_TYPES.contains(normalizeResourceType(resourceType))) {
return copyMap(snapshot);
}
Map<String, Object> projected = new LinkedHashMap<>();
snapshot.forEach((key, value) -> {
if ("resourceSnapshot".equals(key) && value instanceof Map<?, ?> resourceSnapshot) {
projected.put(key, projectMap(resourceSnapshot));
return;
}
projected.put(key, copyValue(value));
});
return projected;
}
/**
* 递归投影 L21 资源快照。
*
* @param source 待投影 Map
* @return 保持原有顺序的安全摘要 Map
*/
private static Map<String, Object> projectMap(Map<?, ?> source) {
Map<String, Object> projected = new LinkedHashMap<>();
source.forEach((rawKey, value) -> {
String key = String.valueOf(rawKey);
String normalizedKey = normalizeKey(key);
if ("resources".equals(normalizedKey)) {
putCollectionCount(projected, "resourceCount", value);
return;
}
if ("mcptoolmanifest".equals(normalizedKey) || "manifest".equals(normalizedKey)) {
putCollectionCount(projected, "manifestToolCount", value);
return;
}
if ("resourcesnapshot".equals(normalizedKey)) {
return;
}
if (isRemovedKey(normalizedKey)) {
return;
}
if (isConfigKey(normalizedKey)) {
return;
}
projected.put(key, projectValue(value));
});
return projected;
}
/**
* 递归投影 Map、集合和数组值。
*
* @param value 原始值
* @return 安全副本
*/
private static Object projectValue(Object value) {
if (value instanceof Map<?, ?> map) {
return projectMap(map);
}
if (value instanceof Collection<?> collection) {
List<Object> projected = new ArrayList<>(collection.size());
collection.forEach(item -> projected.add(projectValue(item)));
return projected;
}
if (value != null && value.getClass().isArray()) {
int length = Array.getLength(value);
List<Object> projected = new ArrayList<>(length);
for (int index = 0; index < length; index++) {
projected.add(projectValue(Array.get(value, index)));
}
return projected;
}
return value;
}
/**
* 判断字段是否属于连接配置或其他配置正文。
*
* @param normalizedKey 已规范化字段名
* @return 需要移除时为 true
*/
private static boolean isConfigKey(String normalizedKey) {
return normalizedKey.endsWith("config") || normalizedKey.endsWith("configjson")
|| normalizedKey.endsWith("configuration");
}
/**
* 判断字段是否需要从严格响应投影中移除。
*
* @param normalizedKey 已规范化字段名
* @return 需要移除时为 true
*/
private static boolean isRemovedKey(String normalizedKey) {
if (normalizedKey.endsWith("hash") || normalizedKey.endsWith("count")) {
return false;
}
return REMOVED_KEYS.contains(normalizedKey)
|| normalizedKey.endsWith("content")
|| normalizedKey.endsWith("contentref")
|| normalizedKey.endsWith("storagepath")
|| normalizedKey.endsWith("physicalpath")
|| normalizedKey.endsWith("source")
|| normalizedKey.contains("manifest")
|| normalizedKey.contains("credential")
|| normalizedKey.endsWith("secret")
|| normalizedKey.endsWith("password")
|| normalizedKey.endsWith("token")
|| normalizedKey.endsWith("apikey")
|| normalizedKey.endsWith("privatekey")
|| normalizedKey.endsWith("schema")
|| normalizedKey.endsWith("headers")
|| normalizedKey.endsWith("environment")
|| normalizedKey.endsWith("env")
|| normalizedKey.endsWith("command")
|| normalizedKey.endsWith("args")
|| normalizedKey.endsWith("arguments")
|| normalizedKey.endsWith("query")
|| normalizedKey.endsWith("queryparams")
|| normalizedKey.endsWith("queryparameters")
|| normalizedKey.endsWith("endpoint")
|| normalizedKey.endsWith("url")
|| normalizedKey.endsWith("uri");
}
/**
* 在源值可计数时写入摘要数量,并避免覆盖已存在的显式数量。
*
* @param projected 目标 Map
* @param countKey 数量字段名
* @param value 待计数值
*/
private static void putCollectionCount(Map<String, Object> projected, String countKey, Object value) {
if (projected.containsKey(countKey)) {
return;
}
if (value instanceof Collection<?> collection) {
projected.put(countKey, collection.size());
} else if (value != null && value.getClass().isArray()) {
projected.put(countKey, Array.getLength(value));
}
}
/**
* 为不采用严格 L21 投影的历史资源复制快照,避免响应方修改持久化 Map。
*
* @param source 原始 Map
* @return 深复制 Map
*/
private static Map<String, Object> copyMap(Map<?, ?> source) {
Map<String, Object> copy = new LinkedHashMap<>();
source.forEach((key, value) -> copy.put(String.valueOf(key), copyValue(value)));
return copy;
}
/**
* 深复制 Map、集合和数组。
*
* @param value 原始值
* @return 深复制值
*/
private static Object copyValue(Object value) {
if (value instanceof Map<?, ?> map) {
return copyMap(map);
}
if (value instanceof Collection<?> collection) {
List<Object> copy = new ArrayList<>(collection.size());
collection.forEach(item -> copy.add(copyValue(item)));
return copy;
}
if (value != null && value.getClass().isArray()) {
int length = Array.getLength(value);
List<Object> copy = new ArrayList<>(length);
for (int index = 0; index < length; index++) {
copy.add(copyValue(Array.get(value, index)));
}
return copy;
}
return value;
}
/**
* 规范化资源类型。
*
* @param resourceType 原始资源类型
* @return 大写资源类型
*/
private static String normalizeResourceType(String resourceType) {
return resourceType == null ? "" : resourceType.trim().toUpperCase(Locale.ROOT);
}
/**
* 规范化字段名,忽略大小写和分隔符。
*
* @param key 原始字段名
* @return 仅含小写字母和数字的字段名
*/
private static String normalizeKey(String key) {
return key == null ? "" : key.replaceAll("[^A-Za-z0-9]", "").toLowerCase(Locale.ROOT);
}
}