feat: 完善 Agent 标准交互与安全运行时
- 接入 AG-UI 运行投影、Turn 时间线和审批隔离 - 增加 Agent Skill 冻结绑定与运行时消费闭环 - 增加受控工作区、内置工具和私有 Artifact 生命周期
This commit is contained in:
@@ -26,6 +26,8 @@ import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.service.SkillService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Objects;
|
||||
@@ -46,6 +48,7 @@ public class AgentDependencyAccessService {
|
||||
private final AgentCategoryService agentCategoryService;
|
||||
private final CategoryPermissionService categoryPermissionService;
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
private final SkillService skillService;
|
||||
|
||||
/**
|
||||
* 创建 Agent 依赖资源校验服务。
|
||||
@@ -60,6 +63,7 @@ public class AgentDependencyAccessService {
|
||||
* @param agentCategoryService Agent 分类服务
|
||||
* @param categoryPermissionService 分类权限服务
|
||||
* @param resourceAccessService 资源权限服务
|
||||
* @param skillService Skill 服务
|
||||
*/
|
||||
public AgentDependencyAccessService(ModelService modelService,
|
||||
WorkflowService workflowService,
|
||||
@@ -70,7 +74,8 @@ public class AgentDependencyAccessService {
|
||||
DocumentCollectionService documentCollectionService,
|
||||
AgentCategoryService agentCategoryService,
|
||||
CategoryPermissionService categoryPermissionService,
|
||||
ResourceAccessService resourceAccessService) {
|
||||
ResourceAccessService resourceAccessService,
|
||||
SkillService skillService) {
|
||||
this.modelService = modelService;
|
||||
this.workflowService = workflowService;
|
||||
this.pluginItemService = pluginItemService;
|
||||
@@ -81,6 +86,7 @@ public class AgentDependencyAccessService {
|
||||
this.agentCategoryService = agentCategoryService;
|
||||
this.categoryPermissionService = categoryPermissionService;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
this.skillService = skillService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,6 +141,17 @@ public class AgentDependencyAccessService {
|
||||
* @return 插件工具
|
||||
*/
|
||||
public PluginItem requirePluginItem(Agent agent, BigInteger pluginItemId) {
|
||||
return requirePluginResource(agent, pluginItemId).pluginItem();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并锁定插件工具及其父插件,返回同一事务中的完整调用资源。
|
||||
*
|
||||
* @param agent Agent
|
||||
* @param pluginItemId 插件工具 ID
|
||||
* @return 插件项与父插件
|
||||
*/
|
||||
public PluginResource requirePluginResource(Agent agent, BigInteger pluginItemId) {
|
||||
PluginItem current = pluginItemService.getById(pluginItemId);
|
||||
if (current == null || current.getPluginId() == null) {
|
||||
throw new BusinessException("绑定插件不存在");
|
||||
@@ -153,7 +170,7 @@ public class AgentDependencyAccessService {
|
||||
}
|
||||
assertSameTenant(agent, plugin.getTenantId(), "无权限绑定该插件");
|
||||
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), plugin.getId(), "无权限绑定该插件");
|
||||
return pluginItem;
|
||||
return new PluginResource(pluginItem, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,6 +191,30 @@ public class AgentDependencyAccessService {
|
||||
return mcp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并锁定 Agent 可使用的已发布 Skill。
|
||||
*
|
||||
* <p>Skill 发布阶段已经完成底层 Tool 权限与 MCP 清单检测。Agent 保存阶段只消费冻结快照,
|
||||
* 避免在数据库事务中执行外部 MCP I/O;快照内容及组合 hash 由运行投影器继续校验。</p>
|
||||
*
|
||||
* @param agent Agent
|
||||
* @param skillId Skill ID
|
||||
* @return 已发布 Skill
|
||||
*/
|
||||
public Skill requireSkill(Agent agent, BigInteger skillId) {
|
||||
Skill skill = skillService.getOne(QueryWrapper.create()
|
||||
.eq(Skill::getId, skillId)
|
||||
.forUpdate());
|
||||
if (skill == null || PublishStatus.from(skill.getPublishStatus()) != PublishStatus.PUBLISHED
|
||||
|| skill.getPublishedSnapshotJson() == null || skill.getPublishedSnapshotJson().isEmpty()) {
|
||||
throw new BusinessException("绑定 Skill 不存在或未发布");
|
||||
}
|
||||
assertSameTenant(agent, skill.getTenantId(), "无权限绑定该 Skill");
|
||||
resourceAccessService.assertAccess(
|
||||
CategoryResourceType.SKILL, skill, ResourceAction.USE, "无权限绑定该 Skill");
|
||||
return skill;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并锁定知识库。
|
||||
*
|
||||
@@ -233,4 +274,13 @@ public class AgentDependencyAccessService {
|
||||
throw new BusinessException(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件运行依赖聚合。
|
||||
*
|
||||
* @param pluginItem 插件工具
|
||||
* @param plugin 父插件调用配置
|
||||
*/
|
||||
public record PluginResource(PluginItem pluginItem, Plugin plugin) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.security.AgentVisibilityQueryHelper;
|
||||
import tech.easyflow.agent.vo.AgentOptionView;
|
||||
@@ -28,8 +29,12 @@ import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.service.SkillService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -54,9 +59,11 @@ public class AgentOptionQueryService {
|
||||
private final PluginItemService pluginItemService;
|
||||
private final PluginVisibilityService pluginVisibilityService;
|
||||
private final McpService mcpService;
|
||||
private final SkillService skillService;
|
||||
private final AgentVisibilityQueryHelper agentVisibilityQueryHelper;
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
/**
|
||||
* 创建 Agent 安全选项查询服务。
|
||||
@@ -69,6 +76,7 @@ public class AgentOptionQueryService {
|
||||
* @param pluginItemService 插件工具服务
|
||||
* @param pluginVisibilityService 插件可见性服务
|
||||
* @param mcpService MCP 服务
|
||||
* @param skillService Skill 服务
|
||||
* @param agentVisibilityQueryHelper Agent 可见性查询助手
|
||||
* @param resourceAccessService 资源访问服务
|
||||
* @param objectMapper JSON 映射器
|
||||
@@ -81,6 +89,7 @@ public class AgentOptionQueryService {
|
||||
PluginItemService pluginItemService,
|
||||
PluginVisibilityService pluginVisibilityService,
|
||||
McpService mcpService,
|
||||
SkillService skillService,
|
||||
AgentVisibilityQueryHelper agentVisibilityQueryHelper,
|
||||
ResourceAccessService resourceAccessService,
|
||||
ObjectMapper objectMapper) {
|
||||
@@ -92,6 +101,7 @@ public class AgentOptionQueryService {
|
||||
this.pluginItemService = pluginItemService;
|
||||
this.pluginVisibilityService = pluginVisibilityService;
|
||||
this.mcpService = mcpService;
|
||||
this.skillService = skillService;
|
||||
this.agentVisibilityQueryHelper = agentVisibilityQueryHelper;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
this.objectMapper = objectMapper;
|
||||
@@ -133,12 +143,113 @@ public class AgentOptionQueryService {
|
||||
return new AgentResourceOptionsView(
|
||||
listModelOptions(account),
|
||||
listKnowledgeOptions(account),
|
||||
listSkillOptions(account),
|
||||
listWorkflowOptions(account),
|
||||
listPluginToolOptions(account),
|
||||
listMcpOptions(account)
|
||||
listMcpOptions(account),
|
||||
new AgentResourceOptionsView.Capabilities(
|
||||
categoryPermissionService != null && categoryPermissionService.isSuperAdmin(account))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入平台超级管理员判定服务。
|
||||
*
|
||||
* @param categoryPermissionService 分类权限服务
|
||||
*/
|
||||
@Autowired
|
||||
public void setCategoryPermissionService(CategoryPermissionService categoryPermissionService) {
|
||||
this.categoryPermissionService = categoryPermissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前账号可使用的已发布 Skill 安全选项。
|
||||
*
|
||||
* @param account 当前登录账号
|
||||
* @return Skill 选项
|
||||
*/
|
||||
private List<AgentResourceOptionsView.SkillOption> listSkillOptions(LoginAccount account) {
|
||||
return skillService.list(QueryWrapper.create()
|
||||
.eq(Skill::getTenantId, account.getTenantId())
|
||||
.eq(Skill::getPublishStatus, PublishStatus.PUBLISHED.getCode())
|
||||
.orderBy(Skill::getModified, false)
|
||||
.orderBy(Skill::getDisplayName, true))
|
||||
.stream()
|
||||
.filter(skill -> resourceAccessService.canAccess(
|
||||
CategoryResourceType.SKILL, skill, ResourceAction.USE))
|
||||
.map(this::toSkillOption)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Skill 发布数据投影为不含正文、资源内容和连接配置的选择项。
|
||||
*
|
||||
* @param skill Skill
|
||||
* @return 安全选择项
|
||||
*/
|
||||
private AgentResourceOptionsView.SkillOption toSkillOption(Skill skill) {
|
||||
Map<String, Object> publishedSnapshot = skill.getPublishedSnapshotJson();
|
||||
List<?> resources = listValue(publishedSnapshot, "resources");
|
||||
int textCount = 0;
|
||||
int binaryCount = 0;
|
||||
long textBytes = utf8Length(textValue(publishedSnapshot, "skillContent"));
|
||||
for (Object raw : resources) {
|
||||
if (!(raw instanceof Map<?, ?> resource)) {
|
||||
continue;
|
||||
}
|
||||
if (Boolean.TRUE.equals(resource.get("text"))) {
|
||||
textCount++;
|
||||
textBytes = Math.addExact(textBytes,
|
||||
utf8Length(resource.get("textContent") == null
|
||||
? null : String.valueOf(resource.get("textContent"))));
|
||||
} else {
|
||||
binaryCount++;
|
||||
}
|
||||
}
|
||||
int toolCount = 0;
|
||||
for (Object raw : listValue(skill.getPublishedToolBindingsJson(), "bindings")) {
|
||||
if (raw instanceof Map<?, ?> binding && binding.get("toolCount") instanceof Number number) {
|
||||
toolCount += Math.max(0, number.intValue());
|
||||
}
|
||||
}
|
||||
return new AgentResourceOptionsView.SkillOption(
|
||||
skill.getId(),
|
||||
publishedText(publishedSnapshot, "displayName", skill.getDisplayName()),
|
||||
publishedText(publishedSnapshot, "description", skill.getDescription()),
|
||||
publishedText(publishedSnapshot, "visibilityScope", skill.getVisibilityScope()),
|
||||
skill.getSnapshotHash(), toolCount, textBytes, textCount, binaryCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取发布快照中的展示字段,旧快照缺少字段时兼容当前行。
|
||||
*
|
||||
* @param snapshot 发布内容快照
|
||||
* @param key 字段名
|
||||
* @param legacyFallback 历史快照回退值
|
||||
* @return 冻结展示值
|
||||
*/
|
||||
private String publishedText(Map<String, Object> snapshot, String key, String legacyFallback) {
|
||||
if (snapshot == null || !snapshot.containsKey(key)) {
|
||||
return legacyFallback;
|
||||
}
|
||||
Object value = snapshot.get(key);
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
|
||||
private String textValue(Map<String, Object> source, String key) {
|
||||
Object value = source == null ? null : source.get(key);
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
|
||||
private long utf8Length(String value) {
|
||||
return value == null ? 0L : value.getBytes(StandardCharsets.UTF_8).length;
|
||||
}
|
||||
|
||||
private List<?> listValue(Map<String, Object> source, String key) {
|
||||
Object value = source == null ? null : source.get(key);
|
||||
return value instanceof List<?> list ? list : List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前账号可用于 Agent 会话的知识库安全选项。
|
||||
*
|
||||
@@ -228,7 +339,6 @@ public class AgentOptionQueryService {
|
||||
return workflowService.list(QueryWrapper.create()
|
||||
.eq(Workflow::getTenantId, account.getTenantId())
|
||||
.eq(Workflow::getPublishStatus, PublishStatus.PUBLISHED.getCode())
|
||||
.eq(Workflow::getStatus, 1)
|
||||
.orderBy(Workflow::getModified, false))
|
||||
.stream()
|
||||
.filter(item -> resourceAccessService.canAccess(
|
||||
|
||||
@@ -2,8 +2,12 @@ package tech.easyflow.agent.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||
import tech.easyflow.agent.entity.AgentSkillBinding;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -35,6 +39,26 @@ public interface AgentService extends IService<Agent> {
|
||||
*/
|
||||
Agent updateDraft(Agent agent);
|
||||
|
||||
/**
|
||||
* 在一个事务中保存 Agent 草稿及发生变化的资源绑定。
|
||||
*
|
||||
* @param agent Agent 草稿
|
||||
* @param toolBindings 工具绑定
|
||||
* @param replaceToolBindings 是否替换工具绑定
|
||||
* @param knowledgeBindings 知识库绑定
|
||||
* @param replaceKnowledgeBindings 是否替换知识库绑定
|
||||
* @param skillBindings Skill 绑定
|
||||
* @param replaceSkillBindings 是否替换 Skill 绑定
|
||||
* @return 保存后的 Agent 与本次替换的绑定
|
||||
*/
|
||||
Agent saveDraftGraph(Agent agent,
|
||||
List<AgentToolBinding> toolBindings,
|
||||
boolean replaceToolBindings,
|
||||
List<AgentKnowledgeBinding> knowledgeBindings,
|
||||
boolean replaceKnowledgeBindings,
|
||||
List<AgentSkillBinding> skillBindings,
|
||||
boolean replaceSkillBindings);
|
||||
|
||||
/**
|
||||
* 更新 Agent 的可见范围。
|
||||
*
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package tech.easyflow.agent.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.agent.entity.AgentSkillBinding;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Agent Skill 绑定服务。
|
||||
*/
|
||||
public interface AgentSkillBindingService extends IService<AgentSkillBinding> {
|
||||
|
||||
/**
|
||||
* 原子替换 Agent 的全部 Skill 绑定。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
* @param bindings Skill 引用列表
|
||||
* @return 规范化后的脱敏绑定摘要
|
||||
*/
|
||||
List<AgentSkillBinding> replaceBindings(BigInteger agentId, List<AgentSkillBinding> bindings);
|
||||
|
||||
/**
|
||||
* 查询 Agent 的 Skill 草稿绑定。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
* @return 稳定排序的绑定
|
||||
*/
|
||||
List<AgentSkillBinding> listBindings(BigInteger agentId);
|
||||
|
||||
/**
|
||||
* 查询 Agent 的 Skill 脱敏绑定摘要。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
* @return 稳定排序的绑定摘要
|
||||
*/
|
||||
List<AgentSkillBinding> listSummaries(BigInteger agentId);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package tech.easyflow.agent.service.impl;
|
||||
|
||||
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||
import tech.easyflow.agent.entity.AgentSkillBinding;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
import tech.easyflow.agent.enums.AgentToolType;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 比较 Agent 绑定的可持久化业务字段,忽略 ID、审计字段与展示摘要。
|
||||
*/
|
||||
final class AgentBindingSemanticComparator {
|
||||
|
||||
private static final String DEFAULT_RETRIEVAL_MODE = "HYBRID";
|
||||
|
||||
private AgentBindingSemanticComparator() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断工具绑定整组替换是否会产生业务变化。
|
||||
*
|
||||
* @param current 当前持久化绑定
|
||||
* @param requested 客户端请求绑定
|
||||
* @return 业务字段完全一致时返回 {@code true}
|
||||
*/
|
||||
static boolean sameTools(List<AgentToolBinding> current, List<AgentToolBinding> requested) {
|
||||
List<AgentToolBinding> left = safeList(current);
|
||||
List<AgentToolBinding> right = safeList(requested);
|
||||
if (left.size() != right.size()) {
|
||||
return false;
|
||||
}
|
||||
for (int index = 0; index < left.size(); index++) {
|
||||
AgentToolBinding persisted = left.get(index);
|
||||
AgentToolBinding incoming = right.get(index);
|
||||
if (persisted == null || incoming == null
|
||||
|| !Objects.equals(normalizeToolType(persisted.getToolType()), normalizeToolType(incoming.getToolType()))
|
||||
|| !Objects.equals(persisted.getTargetId(), incoming.getTargetId())
|
||||
|| !Objects.equals(text(persisted.getToolName()), text(incoming.getToolName()))
|
||||
|| !Objects.equals(enabled(persisted.getEnabled()), enabled(incoming.getEnabled()))
|
||||
|| !Objects.equals(Boolean.TRUE.equals(persisted.getHitlEnabled()),
|
||||
Boolean.TRUE.equals(incoming.getHitlEnabled()))
|
||||
|| !Objects.equals(map(persisted.getHitlConfigJson()), map(incoming.getHitlConfigJson()))
|
||||
|| !Objects.equals(map(persisted.getOptionsJson()), map(incoming.getOptionsJson()))
|
||||
|| !Objects.equals(persisted.getSortNo(), sortNo(incoming.getSortNo(), index))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断知识库绑定整组替换是否会产生业务变化。
|
||||
*
|
||||
* @param current 当前持久化绑定
|
||||
* @param requested 客户端请求绑定
|
||||
* @return 业务字段完全一致时返回 {@code true}
|
||||
*/
|
||||
static boolean sameKnowledges(List<AgentKnowledgeBinding> current,
|
||||
List<AgentKnowledgeBinding> requested) {
|
||||
List<AgentKnowledgeBinding> left = safeList(current);
|
||||
List<AgentKnowledgeBinding> right = safeList(requested);
|
||||
if (left.size() != right.size()) {
|
||||
return false;
|
||||
}
|
||||
for (int index = 0; index < left.size(); index++) {
|
||||
AgentKnowledgeBinding persisted = left.get(index);
|
||||
AgentKnowledgeBinding incoming = right.get(index);
|
||||
if (persisted == null || incoming == null
|
||||
|| !Objects.equals(persisted.getKnowledgeId(), incoming.getKnowledgeId())
|
||||
|| !Objects.equals(retrievalMode(persisted.getRetrievalMode()),
|
||||
retrievalMode(incoming.getRetrievalMode()))
|
||||
|| !Objects.equals(enabled(persisted.getEnabled()), enabled(incoming.getEnabled()))
|
||||
|| !Objects.equals(map(persisted.getOptionsJson()), map(incoming.getOptionsJson()))
|
||||
|| !Objects.equals(persisted.getSortNo(), sortNo(incoming.getSortNo(), index))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 Skill 绑定顺序是否发生变化。
|
||||
*
|
||||
* @param current 当前持久化绑定
|
||||
* @param requested 客户端请求绑定
|
||||
* @return Skill ID 与稳定顺序完全一致时返回 {@code true}
|
||||
*/
|
||||
static boolean sameSkills(List<AgentSkillBinding> current, List<AgentSkillBinding> requested) {
|
||||
List<AgentSkillBinding> left = safeList(current);
|
||||
List<AgentSkillBinding> right = safeList(requested);
|
||||
if (left.size() != right.size()) {
|
||||
return false;
|
||||
}
|
||||
for (int index = 0; index < left.size(); index++) {
|
||||
AgentSkillBinding persisted = left.get(index);
|
||||
AgentSkillBinding incoming = right.get(index);
|
||||
if (persisted == null || incoming == null
|
||||
|| !Objects.equals(persisted.getSkillId(), incoming.getSkillId())
|
||||
|| !Objects.equals(persisted.getSortNo(), index)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String normalizeToolType(String value) {
|
||||
try {
|
||||
return AgentToolType.from(value).name();
|
||||
} catch (RuntimeException ignored) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
private static String retrievalMode(String value) {
|
||||
return value == null || value.isBlank()
|
||||
? DEFAULT_RETRIEVAL_MODE : value.trim().toUpperCase(java.util.Locale.ROOT);
|
||||
}
|
||||
|
||||
private static String text(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
private static Boolean enabled(Boolean value) {
|
||||
return value == null || value;
|
||||
}
|
||||
|
||||
private static Integer sortNo(Integer value, int index) {
|
||||
return value == null ? index : value;
|
||||
}
|
||||
|
||||
private static Map<String, Object> map(Map<String, Object> value) {
|
||||
return value == null ? Collections.emptyMap() : value;
|
||||
}
|
||||
|
||||
private static <T> List<T> safeList(List<T> value) {
|
||||
return value == null ? Collections.emptyList() : value;
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,10 @@ public class AgentKnowledgeBindingServiceImpl extends ServiceImpl<AgentKnowledge
|
||||
Agent agent = requireAgentForUpdate(agentId);
|
||||
resourceAccessService.assertAccess(
|
||||
CategoryResourceType.AGENT, agent, ResourceAction.MANAGE, "无权限管理该 Agent");
|
||||
List<AgentKnowledgeBinding> current = listAll(agentId);
|
||||
if (AgentBindingSemanticComparator.sameKnowledges(current, bindings)) {
|
||||
return enabledBindings(current);
|
||||
}
|
||||
validateBindings(agent, bindings);
|
||||
remove(QueryWrapper.create().where("agent_id = ?", agentId));
|
||||
if (bindings == null || bindings.isEmpty()) {
|
||||
@@ -66,7 +70,7 @@ public class AgentKnowledgeBindingServiceImpl extends ServiceImpl<AgentKnowledge
|
||||
applyBindingDefaults(agent, bindings.get(i), i);
|
||||
}
|
||||
saveBatch(bindings);
|
||||
return listEnabled(agentId);
|
||||
return enabledBindings(bindings);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -81,6 +85,30 @@ public class AgentKnowledgeBindingServiceImpl extends ServiceImpl<AgentKnowledge
|
||||
.orderBy("sort_no asc, id asc"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Agent 的全部知识库绑定,用于整组语义比较。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
* @return 稳定排序的全部绑定
|
||||
*/
|
||||
private List<AgentKnowledgeBinding> listAll(BigInteger agentId) {
|
||||
return list(QueryWrapper.create()
|
||||
.where("agent_id = ?", agentId)
|
||||
.orderBy("sort_no asc, id asc"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从已加载或已写入的绑定中筛选启用项,避免替换后再次查询。
|
||||
*
|
||||
* @param bindings 知识库绑定
|
||||
* @return 启用绑定
|
||||
*/
|
||||
private List<AgentKnowledgeBinding> enabledBindings(List<AgentKnowledgeBinding> bindings) {
|
||||
return bindings.stream()
|
||||
.filter(binding -> binding != null && binding.getEnabled() != Boolean.FALSE)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 锁定并加载待修改的 Agent。
|
||||
*
|
||||
|
||||
@@ -255,7 +255,29 @@ public class AgentResourceBindingProviderImpl implements AgentResourceBindingPro
|
||||
AgentToolType toolType,
|
||||
BigInteger resourceId) {
|
||||
return snapshotListContains(snapshot, "toolBindings", resourceId, toolType.name())
|
||||
|| snapshotListContains(snapshot, "toolSummaries", resourceId, toolType.name());
|
||||
|| snapshotListContains(snapshot, "toolSummaries", resourceId, toolType.name())
|
||||
|| nestedSkillBindingsContain(snapshot, toolType, resourceId);
|
||||
}
|
||||
|
||||
private boolean nestedSkillBindingsContain(Map<String, Object> snapshot,
|
||||
AgentToolType toolType,
|
||||
BigInteger resourceId) {
|
||||
Object rawBindings = snapshot == null ? null : snapshot.get("skillBindings");
|
||||
if (!(rawBindings instanceof List<?> bindings)) {
|
||||
return false;
|
||||
}
|
||||
for (Object raw : bindings) {
|
||||
if (!(raw instanceof Map<?, ?> binding)
|
||||
|| !(binding.get("resourceSnapshot") instanceof Map<?, ?> resourceSnapshot)) {
|
||||
continue;
|
||||
}
|
||||
Object rawTools = resourceSnapshot.get("toolBindings");
|
||||
if (rawTools instanceof List<?> tools
|
||||
&& tools.stream().anyMatch(item -> matchesResourceBinding(item, resourceId, toolType.name()))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,18 +7,26 @@ import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.easyflow.agent.config.AgentInteractionConfigSupport;
|
||||
import tech.easyflow.agent.config.AgentBuiltinToolsConfigResolver;
|
||||
import tech.easyflow.agent.config.AgentBuiltinToolsConfig;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
import tech.easyflow.agent.entity.AgentSkillBinding;
|
||||
import tech.easyflow.agent.mapper.AgentMapper;
|
||||
import tech.easyflow.agent.runtime.AgentRuntimeCompiler;
|
||||
import tech.easyflow.agent.service.AgentDependencyAccessService;
|
||||
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
|
||||
import tech.easyflow.agent.service.AgentService;
|
||||
import tech.easyflow.agent.service.AgentToolBindingService;
|
||||
import tech.easyflow.agent.service.AgentSkillBindingService;
|
||||
import tech.easyflow.agent.runtime.skill.AgentSkillRuntimeProjector;
|
||||
import tech.easyflow.agent.support.AgentBindingLockExecutor;
|
||||
import tech.easyflow.ai.entity.*;
|
||||
import tech.easyflow.ai.easyagentsflow.repository.AgentWorkflowSnapshotFactory;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.mcp.McpConnectionSnapshotFactory;
|
||||
import tech.easyflow.ai.plugin.PluginConnectionSnapshotFactory;
|
||||
import tech.easyflow.ai.service.*;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
@@ -26,7 +34,9 @@ import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.enums.VisibilityScope;
|
||||
import tech.easyflow.system.entity.SysLog;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
import tech.easyflow.system.service.SysLogService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
@@ -43,12 +53,15 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
|
||||
private static final TypeReference<List<AgentToolBinding>> TOOL_BINDING_LIST_TYPE = new TypeReference<>() {};
|
||||
private static final TypeReference<List<AgentKnowledgeBinding>> KNOWLEDGE_BINDING_LIST_TYPE = new TypeReference<>() {};
|
||||
private static final TypeReference<List<AgentSkillBinding>> SKILL_BINDING_LIST_TYPE = new TypeReference<>() {};
|
||||
|
||||
@Resource
|
||||
private AgentToolBindingService agentToolBindingService;
|
||||
@Resource
|
||||
private AgentKnowledgeBindingService agentKnowledgeBindingService;
|
||||
@Resource
|
||||
private AgentSkillBindingService agentSkillBindingService;
|
||||
@Resource
|
||||
private ModelService modelService;
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
@@ -57,6 +70,10 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
@Resource
|
||||
private McpService mcpService;
|
||||
@Resource
|
||||
private McpConnectionSnapshotFactory mcpConnectionSnapshotFactory;
|
||||
@Resource
|
||||
private PluginConnectionSnapshotFactory pluginConnectionSnapshotFactory;
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
@@ -68,6 +85,14 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
private AgentBindingLockExecutor agentBindingLockExecutor;
|
||||
@Resource
|
||||
private AgentRuntimeCompiler agentRuntimeCompiler;
|
||||
@Resource
|
||||
private AgentSkillRuntimeProjector agentSkillRuntimeProjector;
|
||||
@Resource
|
||||
private AgentWorkflowSnapshotFactory agentWorkflowSnapshotFactory;
|
||||
@Resource
|
||||
private AgentBuiltinToolsConfigResolver agentBuiltinToolsConfigResolver;
|
||||
@Resource
|
||||
private SysLogService sysLogService;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -76,8 +101,11 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
public Agent getDetail(BigInteger id) {
|
||||
Agent agent = requireAgent(id);
|
||||
resourceAccessService.assertAccess(CategoryResourceType.AGENT, agent, ResourceAction.READ, "无权限查看该 Agent");
|
||||
agent.setExecutionConfigJson(
|
||||
agentBuiltinToolsConfigResolver.normalizeForDraftRead(agent.getExecutionConfigJson()));
|
||||
agent.setToolBindings(agentToolBindingService.listEnabled(id));
|
||||
agent.setKnowledgeBindings(agentKnowledgeBindingService.listEnabled(id));
|
||||
agent.setSkillBindings(agentSkillBindingService.listSummaries(id));
|
||||
return agent;
|
||||
}
|
||||
|
||||
@@ -88,9 +116,15 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Agent saveDraft(Agent agent) {
|
||||
applyDraftDefaults(agent);
|
||||
validateDraft(agent);
|
||||
validateDraft(agent, null);
|
||||
boolean shellApprovalDisabled = agentBuiltinToolsConfigResolver
|
||||
.isShellApprovalDisableTransition(agent.getExecutionConfigJson(), null);
|
||||
save(agent);
|
||||
return getDetail(agent.getId());
|
||||
if (shellApprovalDisabled) {
|
||||
recordShellApprovalDisabled(agent.getId(), "saveDraft",
|
||||
AgentBuiltinToolsConfig.newAgentDefaults().shell());
|
||||
}
|
||||
return agent;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,13 +141,48 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
resourceAccessService.assertAccess(
|
||||
CategoryResourceType.AGENT, existing, ResourceAction.MANAGE, "无权限管理该 Agent");
|
||||
agent.setTenantId(existing.getTenantId());
|
||||
validateDraft(agent);
|
||||
Map<String, Object> existingExecutionConfig = existing.getExecutionConfigJson();
|
||||
validateDraft(agent, existingExecutionConfig);
|
||||
boolean shellApprovalDisabled = agentBuiltinToolsConfigResolver
|
||||
.isShellApprovalDisableTransition(agent.getExecutionConfigJson(), existingExecutionConfig);
|
||||
AgentBuiltinToolsConfig.ToolSwitch previousShell = agentBuiltinToolsConfigResolver
|
||||
.resolveDraftRuntime(existingExecutionConfig).shell();
|
||||
applyDraftUpdate(existing, agent);
|
||||
updateById(existing);
|
||||
return getDetail(existing.getId());
|
||||
if (shellApprovalDisabled) {
|
||||
recordShellApprovalDisabled(existing.getId(), "updateDraft", previousShell);
|
||||
}
|
||||
return existing;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Agent saveDraftGraph(Agent agent,
|
||||
List<AgentToolBinding> toolBindings,
|
||||
boolean replaceToolBindings,
|
||||
List<AgentKnowledgeBinding> knowledgeBindings,
|
||||
boolean replaceKnowledgeBindings,
|
||||
List<AgentSkillBinding> skillBindings,
|
||||
boolean replaceSkillBindings) {
|
||||
Agent saved = agent != null && agent.getId() != null
|
||||
? updateDraft(agent) : saveDraft(agent);
|
||||
BigInteger agentId = saved.getId();
|
||||
if (replaceToolBindings) {
|
||||
saved.setToolBindings(agentToolBindingService.replaceBindings(agentId, toolBindings));
|
||||
}
|
||||
if (replaceKnowledgeBindings) {
|
||||
saved.setKnowledgeBindings(agentKnowledgeBindingService.replaceBindings(agentId, knowledgeBindings));
|
||||
}
|
||||
if (replaceSkillBindings) {
|
||||
saved.setSkillBindings(agentSkillBindingService.replaceBindings(agentId, skillBindings));
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@@ -174,7 +243,10 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
CategoryResourceType.AGENT, detail, ResourceAction.MANAGE, "无权限管理该 Agent");
|
||||
detail.setToolBindings(agentToolBindingService.listEnabled(agentId));
|
||||
detail.setKnowledgeBindings(agentKnowledgeBindingService.listEnabled(agentId));
|
||||
validateDraft(detail);
|
||||
detail.setSkillBindings(agentSkillBindingService.listBindings(agentId));
|
||||
validateDraft(detail, detail.getExecutionConfigJson());
|
||||
List<AgentSkillBinding> projectedSkillBindings =
|
||||
agentSkillRuntimeProjector.projectCurrentBindings(detail, detail.getSkillBindings());
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||||
snapshot.put("id", detail.getId());
|
||||
snapshot.put("tenantId", detail.getTenantId());
|
||||
@@ -194,12 +266,15 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
snapshot.put("visibilityScope", detail.getVisibilityScope());
|
||||
snapshot.put("toolBindings", snapshotToolBindings(detail, detail.getToolBindings()));
|
||||
snapshot.put("knowledgeBindings", snapshotKnowledgeBindings(detail, detail.getKnowledgeBindings()));
|
||||
snapshot.put("skillBindings", projectedSkillBindings);
|
||||
snapshot.put("basicSummary", basicSummary(detail));
|
||||
snapshot.put("modelSummary", modelSummary(detail.getModelId()));
|
||||
snapshot.put("parameterSummary", parameterSummary(detail));
|
||||
snapshot.put("promptSummary", promptSummary(detail));
|
||||
snapshot.put("toolSummaries", toolSummaries(detail.getToolBindings()));
|
||||
snapshot.put("knowledgeSummaries", knowledgeSummaries(detail.getKnowledgeBindings()));
|
||||
snapshot.put("skillSummaries", projectedSkillBindings.stream()
|
||||
.map(AgentSkillBinding::getResourceSummary).toList());
|
||||
snapshot.put("snapshotAt", new Date());
|
||||
// 发布前完整编译一次,提前暴露工具运行名冲突和运行定义错误。
|
||||
agentRuntimeCompiler.compile(fromSnapshot(snapshot));
|
||||
@@ -222,10 +297,14 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
agent.setModelId(toBigInteger(snapshot.get("modelId")));
|
||||
agent.setCategoryId(toBigInteger(snapshot.get("categoryId")));
|
||||
agent.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||
agent.setExecutionConfigJson(
|
||||
agentBuiltinToolsConfigResolver.normalizeForPublishedRuntime(agent.getExecutionConfigJson()));
|
||||
agent.setInteractionConfigJson(AgentInteractionConfigSupport.normalize(agent.getInteractionConfigJson()));
|
||||
agent.setPublishedSnapshotJson(snapshot);
|
||||
agent.setToolBindings(objectMapper.convertValue(snapshot.get("toolBindings"), TOOL_BINDING_LIST_TYPE));
|
||||
agent.setKnowledgeBindings(objectMapper.convertValue(snapshot.get("knowledgeBindings"), KNOWLEDGE_BINDING_LIST_TYPE));
|
||||
agent.setSkillBindings(snapshot.get("skillBindings") == null ? List.of()
|
||||
: objectMapper.convertValue(snapshot.get("skillBindings"), SKILL_BINDING_LIST_TYPE));
|
||||
return agent;
|
||||
}
|
||||
|
||||
@@ -253,7 +332,7 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
return agent;
|
||||
}
|
||||
|
||||
private void validateDraft(Agent agent) {
|
||||
private void validateDraft(Agent agent, Map<String, Object> existingExecutionConfig) {
|
||||
if (agent == null) {
|
||||
throw new BusinessException("Agent 不能为空");
|
||||
}
|
||||
@@ -264,7 +343,9 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
agentDependencyAccessService.validateCategory(agent);
|
||||
agent.setVisibilityScope(VisibilityScope.fromOrDefault(agent.getVisibilityScope(), VisibilityScope.PRIVATE).name());
|
||||
agent.setInteractionConfigJson(AgentInteractionConfigSupport.normalize(agent.getInteractionConfigJson()));
|
||||
agent.setExecutionConfigJson(normalizeExecutionConfig(agent.getExecutionConfigJson()));
|
||||
Map<String, Object> executionConfig = normalizeExecutionConfig(agent.getExecutionConfigJson());
|
||||
agent.setExecutionConfigJson(agentBuiltinToolsConfigResolver.normalizeForDraftSave(
|
||||
executionConfig, existingExecutionConfig, requireCurrentLoginAccount()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,6 +437,33 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
existing.setModifiedBy(account.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 持久化 Shell 审批关闭这一高风险配置变更的专用审计记录。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
* @param actionMethod 触发变更的服务方法
|
||||
* @param previousShell 变更前 Shell 配置
|
||||
*/
|
||||
private void recordShellApprovalDisabled(BigInteger agentId,
|
||||
String actionMethod,
|
||||
AgentBuiltinToolsConfig.ToolSwitch previousShell) {
|
||||
LoginAccount account = requireCurrentLoginAccount();
|
||||
SysLog log = new SysLog();
|
||||
log.setAccountId(account.getId());
|
||||
log.setActionName("关闭 Agent Shell 调用审批");
|
||||
log.setActionType("SECURITY_CONFIG_CHANGE");
|
||||
log.setActionClass(AgentServiceImpl.class.getName());
|
||||
log.setActionMethod(actionMethod);
|
||||
log.setActionUrl("/api/v1/agent/" + ("saveDraft".equals(actionMethod) ? "save" : "update"));
|
||||
log.setActionBody("{\"agentId\":\"" + agentId
|
||||
+ "\",\"setting\":\"shell\",\"before\":{\"enabled\":"
|
||||
+ previousShell.enabled() + ",\"approvalRequired\":" + previousShell.approvalRequired()
|
||||
+ "},\"after\":{\"enabled\":true,\"approvalRequired\":false}}");
|
||||
log.setStatus(1);
|
||||
log.setCreated(new Date());
|
||||
sysLogService.save(log);
|
||||
}
|
||||
|
||||
private Map<String, Object> modelSummary(BigInteger modelId) {
|
||||
Model model = modelService.getModelInstance(modelId);
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
@@ -432,14 +540,19 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
private Map<String, Object> toolResourceSnapshot(Agent agent, AgentToolBinding binding) {
|
||||
if ("WORKFLOW".equalsIgnoreCase(binding.getToolType())) {
|
||||
Workflow workflow = agentDependencyAccessService.requireWorkflow(agent, binding.getTargetId());
|
||||
return objectMapper.convertValue(workflow, new TypeReference<Map<String, Object>>() {});
|
||||
return agentWorkflowSnapshotFactory.snapshot(workflow);
|
||||
}
|
||||
if ("PLUGIN".equalsIgnoreCase(binding.getToolType())) {
|
||||
PluginItem pluginItem = agentDependencyAccessService.requirePluginItem(agent, binding.getTargetId());
|
||||
return objectMapper.convertValue(pluginItem, new TypeReference<Map<String, Object>>() {});
|
||||
AgentDependencyAccessService.PluginResource resource =
|
||||
agentDependencyAccessService.requirePluginResource(agent, binding.getTargetId());
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||||
snapshot.put("pluginItem", objectMapper.convertValue(
|
||||
resource.pluginItem(), new TypeReference<Map<String, Object>>() {}));
|
||||
snapshot.put("plugin", pluginConnectionSnapshotFactory.snapshot(resource.plugin()));
|
||||
return snapshot;
|
||||
}
|
||||
Mcp mcp = agentDependencyAccessService.requireMcp(agent, binding.getTargetId());
|
||||
return objectMapper.convertValue(mcp, new TypeReference<Map<String, Object>>() {});
|
||||
return mcpConnectionSnapshotFactory.snapshot(mcp);
|
||||
}
|
||||
|
||||
private List<AgentKnowledgeBinding> snapshotKnowledgeBindings(
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
package tech.easyflow.agent.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.entity.AgentSkillBinding;
|
||||
import tech.easyflow.agent.mapper.AgentMapper;
|
||||
import tech.easyflow.agent.mapper.AgentSkillBindingMapper;
|
||||
import tech.easyflow.agent.runtime.skill.AgentSkillRuntimeProjector;
|
||||
import tech.easyflow.agent.service.AgentSkillBindingService;
|
||||
import tech.easyflow.agent.support.AgentBindingLockExecutor;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.service.SkillService;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Agent Skill 绑定服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class AgentSkillBindingServiceImpl
|
||||
extends ServiceImpl<AgentSkillBindingMapper, AgentSkillBinding>
|
||||
implements AgentSkillBindingService {
|
||||
|
||||
private final AgentMapper agentMapper;
|
||||
private final AgentBindingLockExecutor bindingLockExecutor;
|
||||
private final AgentSkillRuntimeProjector runtimeProjector;
|
||||
private final SkillService skillService;
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
|
||||
/**
|
||||
* 创建 Agent Skill 绑定服务。
|
||||
*
|
||||
* @param agentMapper Agent Mapper
|
||||
* @param bindingLockExecutor Agent 绑定锁执行器
|
||||
* @param runtimeProjector Skill 运行投影器
|
||||
* @param skillService Skill 服务
|
||||
* @param resourceAccessService 资源权限服务
|
||||
*/
|
||||
public AgentSkillBindingServiceImpl(AgentMapper agentMapper,
|
||||
AgentBindingLockExecutor bindingLockExecutor,
|
||||
AgentSkillRuntimeProjector runtimeProjector,
|
||||
SkillService skillService,
|
||||
ResourceAccessService resourceAccessService) {
|
||||
this.agentMapper = agentMapper;
|
||||
this.bindingLockExecutor = bindingLockExecutor;
|
||||
this.runtimeProjector = runtimeProjector;
|
||||
this.skillService = skillService;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<AgentSkillBinding> replaceBindings(BigInteger agentId,
|
||||
List<AgentSkillBinding> bindings) {
|
||||
return bindingLockExecutor.execute(agentId, () -> {
|
||||
Agent agent = requireAgentForUpdate(agentId);
|
||||
resourceAccessService.assertAccess(
|
||||
CategoryResourceType.AGENT, agent, ResourceAction.MANAGE, "无权限管理该 Agent");
|
||||
List<AgentSkillBinding> current = listBindings(agentId);
|
||||
if (AgentBindingSemanticComparator.sameSkills(current, bindings)) {
|
||||
return listSummaries(agentId);
|
||||
}
|
||||
List<AgentSkillBinding> normalized = normalize(agent, bindings);
|
||||
// 在删除旧绑定前完成权限、包完整性、Tool 与 8 MiB 预算校验,失败时保留旧组。
|
||||
List<AgentSkillBinding> projected = runtimeProjector.projectCurrentBindings(agent, normalized);
|
||||
Map<BigInteger, Map<String, Object>> summaries = new HashMap<>();
|
||||
for (AgentSkillBinding binding : projected) {
|
||||
summaries.put(binding.getSkillId(), binding.getResourceSummary());
|
||||
}
|
||||
normalized.forEach(binding -> binding.setResourceSummary(summaries.get(binding.getSkillId())));
|
||||
remove(QueryWrapper.create()
|
||||
.eq(AgentSkillBinding::getTenantId, agent.getTenantId())
|
||||
.eq(AgentSkillBinding::getAgentId, agentId));
|
||||
if (!normalized.isEmpty()) {
|
||||
saveBatch(normalized);
|
||||
}
|
||||
return normalized;
|
||||
});
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public List<AgentSkillBinding> listBindings(BigInteger agentId) {
|
||||
if (agentId == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return list(QueryWrapper.create()
|
||||
.eq(AgentSkillBinding::getAgentId, agentId)
|
||||
.orderBy(AgentSkillBinding::getSortNo, true)
|
||||
.orderBy(AgentSkillBinding::getId, true));
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public List<AgentSkillBinding> listSummaries(BigInteger agentId) {
|
||||
List<AgentSkillBinding> bindings = listBindings(agentId);
|
||||
Agent agent = agentMapper.selectOneById(agentId);
|
||||
Map<BigInteger, String> publishedHashes = publishedRuntimeHashes(agent);
|
||||
for (AgentSkillBinding binding : bindings) {
|
||||
Skill skill = skillService.getById(binding.getSkillId());
|
||||
if (skill == null) {
|
||||
binding.setResourceSummary(Map.of(
|
||||
"skillId", binding.getSkillId(),
|
||||
"displayName", "已失效技能",
|
||||
"available", false));
|
||||
continue;
|
||||
}
|
||||
binding.setResourceSummary(runtimeProjector.currentSummary(
|
||||
skill, publishedHashes.get(binding.getSkillId())));
|
||||
binding.getResourceSummary().put("available", true);
|
||||
}
|
||||
return bindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范客户端绑定并写入服务端归属、排序和审计字段。
|
||||
*
|
||||
* @param agent Agent
|
||||
* @param bindings 客户端绑定
|
||||
* @return 规范绑定
|
||||
*/
|
||||
private List<AgentSkillBinding> normalize(Agent agent, List<AgentSkillBinding> bindings) {
|
||||
if (bindings == null || bindings.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
if (bindings.size() > AgentSkillRuntimeProjector.MAX_SKILL_COUNT) {
|
||||
throw new BusinessException(409, 4092, "单个 Agent 最多可绑定 20 个 Skill");
|
||||
}
|
||||
Set<BigInteger> unique = new HashSet<>();
|
||||
List<AgentSkillBinding> result = new ArrayList<>();
|
||||
LoginAccount account = requireAccount();
|
||||
Date now = new Date();
|
||||
for (int index = 0; index < bindings.size(); index++) {
|
||||
AgentSkillBinding source = bindings.get(index);
|
||||
if (source == null || source.getSkillId() == null) {
|
||||
throw new BusinessException("Agent Skill 绑定参数不完整");
|
||||
}
|
||||
if (!unique.add(source.getSkillId())) {
|
||||
throw new BusinessException(409, 4092, "同一 Skill 不能重复绑定");
|
||||
}
|
||||
AgentSkillBinding binding = new AgentSkillBinding();
|
||||
binding.setTenantId(agent.getTenantId());
|
||||
binding.setAgentId(agent.getId());
|
||||
binding.setSkillId(source.getSkillId());
|
||||
binding.setSortNo(index);
|
||||
binding.setCreated(now);
|
||||
binding.setCreatedBy(account.getId());
|
||||
binding.setModified(now);
|
||||
binding.setModifiedBy(account.getId());
|
||||
result.add(binding);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询并锁定 Agent。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
* @return Agent
|
||||
*/
|
||||
private Agent requireAgentForUpdate(BigInteger agentId) {
|
||||
if (agentId == null) {
|
||||
throw new BusinessException("Agent ID 不能为空");
|
||||
}
|
||||
Agent agent = agentMapper.selectOneByQuery(QueryWrapper.create()
|
||||
.eq(Agent::getId, agentId)
|
||||
.forUpdate());
|
||||
if (agent == null) {
|
||||
throw new BusinessException(404, 404, "Agent 不存在");
|
||||
}
|
||||
return agent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Agent 当前线上冻结的 Skill 组合 hash。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
* @param skillId Skill ID
|
||||
* @return 组合 hash 或 null
|
||||
*/
|
||||
private Map<BigInteger, String> publishedRuntimeHashes(Agent agent) {
|
||||
Map<BigInteger, String> hashes = new HashMap<>();
|
||||
Map<String, Object> snapshot = agent == null ? null : agent.getPublishedSnapshotJson();
|
||||
Object rawBindings = snapshot == null ? null : snapshot.get("skillBindings");
|
||||
if (!(rawBindings instanceof List<?> items)) {
|
||||
return hashes;
|
||||
}
|
||||
for (Object raw : items) {
|
||||
if (!(raw instanceof Map<?, ?> item) || item.get("skillId") == null) {
|
||||
continue;
|
||||
}
|
||||
Object resource = item.get("resourceSnapshot");
|
||||
if (resource instanceof Map<?, ?> resourceMap) {
|
||||
Object hash = resourceMap.get("skillRuntimeSnapshotHash");
|
||||
if (hash != null) {
|
||||
hashes.put(new BigInteger(String.valueOf(item.get("skillId"))), String.valueOf(hash));
|
||||
}
|
||||
}
|
||||
}
|
||||
return hashes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录账号。
|
||||
*
|
||||
* @return 登录账号
|
||||
*/
|
||||
private LoginAccount requireAccount() {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
if (account == null || account.getId() == null) {
|
||||
throw new BusinessException(401, 401, "当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package tech.easyflow.agent.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.entity.AgentSkillBinding;
|
||||
import tech.easyflow.agent.service.AgentService;
|
||||
import tech.easyflow.agent.service.AgentSkillBindingService;
|
||||
import tech.easyflow.skill.service.SkillReferenceProvider;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Agent 草稿和有效发布快照中的 Skill 引用提供者。
|
||||
*/
|
||||
@Component
|
||||
public class AgentSkillReferenceProvider implements SkillReferenceProvider {
|
||||
|
||||
private final AgentService agentService;
|
||||
private final AgentSkillBindingService bindingService;
|
||||
|
||||
/**
|
||||
* 创建引用提供者。
|
||||
*
|
||||
* @param agentService Agent 服务
|
||||
* @param bindingService Agent Skill 绑定服务
|
||||
*/
|
||||
public AgentSkillReferenceProvider(AgentService agentService,
|
||||
AgentSkillBindingService bindingService) {
|
||||
this.agentService = agentService;
|
||||
this.bindingService = bindingService;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public List<String> listReferences(BigInteger skillId) {
|
||||
Set<BigInteger> ids = new LinkedHashSet<>();
|
||||
for (AgentSkillBinding binding : bindingService.list(QueryWrapper.create()
|
||||
.eq(AgentSkillBinding::getSkillId, skillId))) {
|
||||
ids.add(binding.getAgentId());
|
||||
}
|
||||
for (Agent agent : agentService.list(QueryWrapper.create()
|
||||
.select(Agent::getId, Agent::getPublishedSnapshotJson)
|
||||
.isNotNull(Agent::getPublishedSnapshotJson))) {
|
||||
if (containsSkill(agent.getPublishedSnapshotJson(), skillId)) {
|
||||
ids.add(agent.getId());
|
||||
}
|
||||
}
|
||||
List<String> result = new ArrayList<>();
|
||||
for (Agent agent : agentService.listByIds(ids)) {
|
||||
result.add("智能体“" + (agent.getName() == null ? "未命名智能体" : agent.getName()) + "”");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean containsSkill(Map<String, Object> snapshot, BigInteger skillId) {
|
||||
Object raw = snapshot == null ? null : snapshot.get("skillBindings");
|
||||
if (!(raw instanceof List<?> bindings)) {
|
||||
return false;
|
||||
}
|
||||
return bindings.stream().anyMatch(item -> item instanceof Map<?, ?> binding
|
||||
&& skillId.toString().equals(String.valueOf(binding.get("skillId"))));
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,10 @@ public class AgentToolBindingServiceImpl extends ServiceImpl<AgentToolBindingMap
|
||||
Agent agent = requireAgentForUpdate(agentId);
|
||||
resourceAccessService.assertAccess(
|
||||
CategoryResourceType.AGENT, agent, ResourceAction.MANAGE, "无权限管理该 Agent");
|
||||
List<AgentToolBinding> current = listAll(agentId);
|
||||
if (AgentBindingSemanticComparator.sameTools(current, bindings)) {
|
||||
return enabledBindings(current);
|
||||
}
|
||||
validateBindings(agent, bindings);
|
||||
remove(QueryWrapper.create().where("agent_id = ?", agentId));
|
||||
if (bindings == null || bindings.isEmpty()) {
|
||||
@@ -64,7 +68,7 @@ public class AgentToolBindingServiceImpl extends ServiceImpl<AgentToolBindingMap
|
||||
applyBindingDefaults(agent, bindings.get(i), i);
|
||||
}
|
||||
saveBatch(bindings);
|
||||
return listEnabled(agentId);
|
||||
return enabledBindings(bindings);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -79,6 +83,30 @@ public class AgentToolBindingServiceImpl extends ServiceImpl<AgentToolBindingMap
|
||||
.orderBy("sort_no asc, id asc"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Agent 的全部工具绑定,用于整组语义比较。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
* @return 稳定排序的全部绑定
|
||||
*/
|
||||
private List<AgentToolBinding> listAll(BigInteger agentId) {
|
||||
return list(QueryWrapper.create()
|
||||
.where("agent_id = ?", agentId)
|
||||
.orderBy("sort_no asc, id asc"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从已加载或已写入的绑定中筛选启用项,避免替换后再次查询。
|
||||
*
|
||||
* @param bindings 工具绑定
|
||||
* @return 启用绑定
|
||||
*/
|
||||
private List<AgentToolBinding> enabledBindings(List<AgentToolBinding> bindings) {
|
||||
return bindings.stream()
|
||||
.filter(binding -> binding != null && binding.getEnabled() != Boolean.FALSE)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 锁定并加载待修改的 Agent。
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user