feat: 完善 Agent 标准交互与安全运行时
- 接入 AG-UI 运行投影、Turn 时间线和审批隔离 - 增加 Agent Skill 冻结绑定与运行时消费闭环 - 增加受控工作区、内置工具和私有 Artifact 生命周期
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
package tech.easyflow.admin.controller.agent;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaMode;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||
import tech.easyflow.agent.entity.AgentArtifact;
|
||||
import tech.easyflow.agent.runtime.artifact.AgentArtifactService;
|
||||
import tech.easyflow.agent.runtime.artifact.AgentArtifactView;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Agent Artifact 安全元数据与鉴权下载控制器。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/agent/artifacts")
|
||||
public class AgentArtifactController {
|
||||
|
||||
private final AgentArtifactService artifactService;
|
||||
|
||||
/**
|
||||
* 创建 Artifact 控制器。
|
||||
*
|
||||
* @param artifactService Artifact 服务
|
||||
*/
|
||||
public AgentArtifactController(AgentArtifactService artifactService) {
|
||||
this.artifactService = artifactService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询一个已鉴权 Artifact 的安全元数据。
|
||||
*
|
||||
* @param artifactId 稳定 Artifact ID
|
||||
* @return 安全元数据
|
||||
*/
|
||||
@GetMapping("/{artifactId}")
|
||||
@SaCheckPermission(value = {"/api/v1/agent/session/query", "/api/v1/agent/save"}, mode = SaMode.OR)
|
||||
public Result<AgentArtifactView> metadata(@PathVariable String artifactId,
|
||||
@RequestParam BigInteger agentId,
|
||||
@RequestParam String mode,
|
||||
@RequestParam(required = false) BigInteger sessionId,
|
||||
@RequestParam(required = false) String runtimeSessionId) {
|
||||
AgentArtifact artifact = artifactService.requireDownload(
|
||||
artifactId, requireAccount(), agentId, mode, sessionId, runtimeSessionId);
|
||||
return Result.ok(artifactService.toView(artifact));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过后端鉴权代理流式下载私有 Artifact。
|
||||
*
|
||||
* @param artifactId 稳定 Artifact ID
|
||||
* @return 私有流式响应
|
||||
*/
|
||||
@GetMapping("/{artifactId}/content")
|
||||
@SaCheckPermission(value = {"/api/v1/agent/session/query", "/api/v1/agent/save"}, mode = SaMode.OR)
|
||||
public ResponseEntity<StreamingResponseBody> content(@PathVariable String artifactId,
|
||||
@RequestParam BigInteger agentId,
|
||||
@RequestParam String mode,
|
||||
@RequestParam(required = false) BigInteger sessionId,
|
||||
@RequestParam(required = false) String runtimeSessionId) {
|
||||
AgentArtifact artifact = artifactService.requireDownload(
|
||||
artifactId, requireAccount(), agentId, mode, sessionId, runtimeSessionId);
|
||||
StreamingResponseBody body = output -> {
|
||||
try (InputStream input = artifactService.openDownload(artifact)) {
|
||||
input.transferTo(output);
|
||||
}
|
||||
};
|
||||
String mimeType = artifact.getMimeType() == null
|
||||
? MediaType.APPLICATION_OCTET_STREAM_VALUE : artifact.getMimeType();
|
||||
return ResponseEntity.ok()
|
||||
.cacheControl(CacheControl.noStore())
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment()
|
||||
.filename(artifact.getFileName(), StandardCharsets.UTF_8).build().toString())
|
||||
.header("X-Content-Type-Options", "nosniff")
|
||||
.contentType(MediaType.parseMediaType(mimeType))
|
||||
.contentLength(artifact.getSizeBytes() == null ? 0L : artifact.getSizeBytes())
|
||||
.body(body);
|
||||
}
|
||||
|
||||
private LoginAccount requireAccount() {
|
||||
try {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
if (account == null || account.getId() == null || account.getTenantId() == null) {
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
return account;
|
||||
} catch (BusinessException error) {
|
||||
throw error;
|
||||
} catch (Exception error) {
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaMode;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import io.agentscope.core.agui.model.RunAgentInput;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -12,7 +13,9 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
@@ -27,6 +30,7 @@ import tech.easyflow.agent.publish.AgentPublishAppService;
|
||||
import tech.easyflow.agent.runtime.AgentChatRequest;
|
||||
import tech.easyflow.agent.runtime.AgentDraftChatRequest;
|
||||
import tech.easyflow.agent.runtime.AgentRunService;
|
||||
import tech.easyflow.agent.runtime.agui.AgentAguiHitlResolveRequest;
|
||||
import tech.easyflow.agent.runtime.composer.AgentComposerDraft;
|
||||
import tech.easyflow.agent.runtime.composer.AgentComposerDraftService;
|
||||
import tech.easyflow.agent.runtime.composer.AgentComposerSession;
|
||||
@@ -41,6 +45,7 @@ import tech.easyflow.agent.service.AgentApprovalStateService;
|
||||
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
|
||||
import tech.easyflow.agent.service.AgentOptionQueryService;
|
||||
import tech.easyflow.agent.service.AgentService;
|
||||
import tech.easyflow.agent.service.AgentSkillBindingService;
|
||||
import tech.easyflow.agent.service.AgentToolBindingService;
|
||||
import tech.easyflow.agent.vo.AgentOptionView;
|
||||
import tech.easyflow.agent.vo.AgentResourceOptionsView;
|
||||
@@ -74,6 +79,8 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
@Resource
|
||||
private AgentKnowledgeBindingService agentKnowledgeBindingService;
|
||||
@Resource
|
||||
private AgentSkillBindingService agentSkillBindingService;
|
||||
@Resource
|
||||
private AgentRunService agentRunService;
|
||||
@Resource
|
||||
private AgentPublishAppService agentPublishAppService;
|
||||
@@ -118,10 +125,11 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
* @return Agent 详情
|
||||
*/
|
||||
@GetMapping("/getDetail")
|
||||
public Result<Agent> getDetail(BigInteger id) {
|
||||
public Result<AgentDetailView> getDetail(BigInteger id) {
|
||||
Agent agent = service.getDetail(id);
|
||||
agentApprovalStateService.fillAgentApprovalState(agent);
|
||||
return Result.ok(agent);
|
||||
aiResourceCreatorNameSupport.fillAgentCreatorNames(List.of(agent));
|
||||
return Result.ok(AgentDetailView.from(agent));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,7 +141,8 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
@Override
|
||||
@PostMapping("save")
|
||||
public Result<?> save(@JsonBody Agent agent) {
|
||||
return Result.ok(service.saveDraft(agent));
|
||||
Agent saved = service.saveDraft(agent);
|
||||
return Result.ok(AgentDetailView.from(service.getDetail(saved.getId())));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,7 +154,32 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
@Override
|
||||
@PostMapping("update")
|
||||
public Result<?> update(@JsonBody Agent agent) {
|
||||
return Result.ok(service.updateDraft(agent));
|
||||
Agent saved = service.updateDraft(agent);
|
||||
return Result.ok(AgentDetailView.from(service.getDetail(saved.getId())));
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子保存 Agent 草稿及本次发生变化的绑定组。
|
||||
*
|
||||
* @param request 设计器保存请求
|
||||
* @return 保存后的 Agent 与本次替换的绑定
|
||||
*/
|
||||
@PostMapping("/draft/save")
|
||||
@SaCheckPermission("/api/v1/agent/save")
|
||||
public Result<AgentDetailView> saveDraft(@JsonBody(required = true, skipConvertError = false)
|
||||
AgentDraftSaveRequest request) {
|
||||
if (request == null || request.getAgent() == null) {
|
||||
throw new BusinessException("Agent 草稿不能为空");
|
||||
}
|
||||
Agent saved = service.saveDraftGraph(
|
||||
request.getAgent(),
|
||||
request.getToolBindings(),
|
||||
request.isReplaceToolBindings(),
|
||||
request.getKnowledgeBindings(),
|
||||
request.isReplaceKnowledgeBindings(),
|
||||
request.toSkillBindings(),
|
||||
request.isReplaceSkillBindings());
|
||||
return Result.ok(AgentDetailView.from(saved));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,8 +190,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
*/
|
||||
@PostMapping("visibilityScope/update")
|
||||
@SaCheckPermission("/api/v1/agent/save")
|
||||
public Result<Agent> updateVisibilityScope(@JsonBody Agent agent) {
|
||||
return Result.ok(service.updateVisibilityScope(agent.getId(), agent.getVisibilityScope()));
|
||||
public Result<AgentDetailView> updateVisibilityScope(@JsonBody Agent agent) {
|
||||
return Result.ok(AgentDetailView.from(
|
||||
service.updateVisibilityScope(agent.getId(), agent.getVisibilityScope())));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -258,6 +293,47 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
return agentRunService.chatDraft(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 AG-UI 协议运行正式 Agent 聊天。
|
||||
*
|
||||
* @param agentId URL 中的 Agent ID
|
||||
* @param input AG-UI 运行输入
|
||||
* @return 原生 AG-UI SSE
|
||||
*/
|
||||
@PostMapping("/{agentId}/agui/run")
|
||||
@SaCheckPermission("/api/v1/agent/session/query")
|
||||
public SseEmitter chatAgui(@PathVariable BigInteger agentId,
|
||||
@RequestBody RunAgentInput input) {
|
||||
return agentRunService.chatAgui(agentId, input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 AG-UI 协议运行草稿 Agent 试用。
|
||||
*
|
||||
* @param input AG-UI 运行输入
|
||||
* @return 原生 AG-UI SSE
|
||||
*/
|
||||
@PostMapping("/agui/run/draft")
|
||||
@SaCheckPermission("/api/v1/agent/save")
|
||||
public SseEmitter chatDraftAgui(@RequestBody RunAgentInput input) {
|
||||
return agentRunService.chatDraftAgui(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 AG-UI 自定义 HITL 兼容桥审批。
|
||||
*
|
||||
* @param request 审批请求
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PostMapping("/agui/hitl/resolve")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<Void> resolveAguiApproval(@RequestBody AgentAguiHitlResolveRequest request) {
|
||||
agentRunService.resolveAguiApproval(request);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传一张 Agent 聊天临时图片。
|
||||
*
|
||||
@@ -565,6 +641,26 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
return Result.ok(agentKnowledgeBindingService.replaceBindings(agentId, bindings));
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子替换 Agent 的全部 Skill 草稿绑定。
|
||||
*
|
||||
* @param request 白名单 Skill 引用请求
|
||||
* @return 服务端生成的安全 Skill 摘要
|
||||
*/
|
||||
@PostMapping("/skillBinding/update")
|
||||
@SaCheckPermission("/api/v1/agent/save")
|
||||
public Result<List<AgentDetailView.SkillBindingView>> updateSkillBinding(
|
||||
@JsonBody(required = true, skipConvertError = false) AgentSkillBindingUpdateRequest request) {
|
||||
if (request == null || request.getAgentId() == null) {
|
||||
throw new BusinessException("Agent ID 不能为空");
|
||||
}
|
||||
List<tech.easyflow.agent.entity.AgentSkillBinding> bindings = request.getBindings() == null
|
||||
? List.of()
|
||||
: request.getBindings().stream().map(AgentSkillBindingUpdateRequest.Binding::toEntity).toList();
|
||||
return Result.ok(agentSkillBindingService.replaceBindings(request.getAgentId(), bindings)
|
||||
.stream().map(AgentDetailView.SkillBindingView::from).toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交发布审批。
|
||||
*
|
||||
@@ -650,6 +746,7 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
agent.setPublishedSnapshotJson(Collections.emptyMap());
|
||||
agent.setToolBindings(null);
|
||||
agent.setKnowledgeBindings(null);
|
||||
agent.setSkillBindings(null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package tech.easyflow.admin.controller.agent;
|
||||
|
||||
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.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 管理端 Agent 草稿安全详情。
|
||||
*
|
||||
* <p>该视图明确排除发布快照以及各绑定的内部资源快照。</p>
|
||||
*/
|
||||
public record AgentDetailView(
|
||||
BigInteger id,
|
||||
BigInteger deptId,
|
||||
String name,
|
||||
String description,
|
||||
String avatar,
|
||||
BigInteger categoryId,
|
||||
BigInteger modelId,
|
||||
Map<String, Object> modelConfigJson,
|
||||
Map<String, Object> generationConfigJson,
|
||||
Map<String, Object> promptConfigJson,
|
||||
Map<String, Object> memoryConfigJson,
|
||||
Map<String, Object> executionConfigJson,
|
||||
Map<String, Object> interactionConfigJson,
|
||||
Integer status,
|
||||
String visibilityScope,
|
||||
String publishStatus,
|
||||
BigInteger currentApprovalInstanceId,
|
||||
Date publishedAt,
|
||||
BigInteger publishedBy,
|
||||
Date created,
|
||||
BigInteger createdBy,
|
||||
Date modified,
|
||||
BigInteger modifiedBy,
|
||||
Boolean approvalPending,
|
||||
String currentApprovalActionType,
|
||||
String displayPublishStatus,
|
||||
String createdByName,
|
||||
List<ToolBindingView> toolBindings,
|
||||
List<KnowledgeBindingView> knowledgeBindings,
|
||||
List<SkillBindingView> skillBindings) {
|
||||
|
||||
/**
|
||||
* 从领域实体构造安全详情。
|
||||
*
|
||||
* @param agent Agent 领域实体
|
||||
* @return 安全详情
|
||||
*/
|
||||
public static AgentDetailView from(Agent agent) {
|
||||
return new AgentDetailView(agent.getId(), agent.getDeptId(), agent.getName(), agent.getDescription(),
|
||||
agent.getAvatar(), agent.getCategoryId(), agent.getModelId(), agent.getModelConfigJson(),
|
||||
agent.getGenerationConfigJson(), agent.getPromptConfigJson(), agent.getMemoryConfigJson(),
|
||||
agent.getExecutionConfigJson(), agent.getInteractionConfigJson(), agent.getStatus(),
|
||||
agent.getVisibilityScope(), agent.getPublishStatus(), agent.getCurrentApprovalInstanceId(),
|
||||
agent.getPublishedAt(), agent.getPublishedBy(), agent.getCreated(), agent.getCreatedBy(),
|
||||
agent.getModified(), agent.getModifiedBy(), agent.getApprovalPending(),
|
||||
agent.getCurrentApprovalActionType(), agent.getDisplayPublishStatus(), agent.getCreatedByName(),
|
||||
mapTools(agent.getToolBindings()), mapKnowledges(agent.getKnowledgeBindings()),
|
||||
mapSkills(agent.getSkillBindings()));
|
||||
}
|
||||
|
||||
private static List<ToolBindingView> mapTools(List<AgentToolBinding> bindings) {
|
||||
return bindings == null ? List.of() : bindings.stream().map(ToolBindingView::from).toList();
|
||||
}
|
||||
|
||||
private static List<KnowledgeBindingView> mapKnowledges(List<AgentKnowledgeBinding> bindings) {
|
||||
return bindings == null ? List.of() : bindings.stream().map(KnowledgeBindingView::from).toList();
|
||||
}
|
||||
|
||||
private static List<SkillBindingView> mapSkills(List<AgentSkillBinding> bindings) {
|
||||
return bindings == null ? List.of() : bindings.stream().map(SkillBindingView::from).toList();
|
||||
}
|
||||
|
||||
/** Agent 直接 Tool 草稿绑定。 */
|
||||
public record ToolBindingView(BigInteger id, String toolType, BigInteger targetId, String toolName,
|
||||
Boolean enabled, Boolean hitlEnabled, Map<String, Object> hitlConfigJson,
|
||||
Map<String, Object> optionsJson, Integer sortNo,
|
||||
Map<String, Object> resourceSummary) {
|
||||
/** @param value 实体 @return 安全绑定 */
|
||||
static ToolBindingView from(AgentToolBinding value) {
|
||||
return new ToolBindingView(value.getId(), value.getToolType(), value.getTargetId(), value.getToolName(),
|
||||
value.getEnabled(), value.getHitlEnabled(), value.getHitlConfigJson(), value.getOptionsJson(),
|
||||
value.getSortNo(), value.getResourceSummary());
|
||||
}
|
||||
}
|
||||
|
||||
/** Agent 知识库草稿绑定。 */
|
||||
public record KnowledgeBindingView(BigInteger id, BigInteger knowledgeId, String retrievalMode,
|
||||
Boolean enabled, Map<String, Object> optionsJson, Integer sortNo,
|
||||
Map<String, Object> resourceSummary) {
|
||||
/** @param value 实体 @return 安全绑定 */
|
||||
static KnowledgeBindingView from(AgentKnowledgeBinding value) {
|
||||
return new KnowledgeBindingView(value.getId(), value.getKnowledgeId(), value.getRetrievalMode(),
|
||||
value.getEnabled(), value.getOptionsJson(), value.getSortNo(), value.getResourceSummary());
|
||||
}
|
||||
}
|
||||
|
||||
/** Agent Skill 草稿绑定。 */
|
||||
public record SkillBindingView(BigInteger id, BigInteger skillId, Integer sortNo,
|
||||
Map<String, Object> resourceSummary) {
|
||||
/** @param value 实体 @return 安全绑定 */
|
||||
static SkillBindingView from(AgentSkillBinding value) {
|
||||
return new SkillBindingView(value.getId(), value.getSkillId(), value.getSortNo(),
|
||||
value.getResourceSummary());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package tech.easyflow.admin.controller.agent;
|
||||
|
||||
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.util.List;
|
||||
|
||||
/**
|
||||
* Agent 设计器原子保存请求。
|
||||
*
|
||||
* <p>绑定变更标记由设计器基于加载后的稳定业务字段计算。服务端仍会执行权限、状态与幂等比较,
|
||||
* 标记为未变化的绑定不会进入查询、外部资源校验或整组重写流程。</p>
|
||||
*/
|
||||
public class AgentDraftSaveRequest {
|
||||
|
||||
private Agent agent;
|
||||
private List<AgentToolBinding> toolBindings;
|
||||
private boolean replaceToolBindings;
|
||||
private List<AgentKnowledgeBinding> knowledgeBindings;
|
||||
private boolean replaceKnowledgeBindings;
|
||||
private List<AgentSkillBindingUpdateRequest.Binding> skillBindings;
|
||||
private boolean replaceSkillBindings;
|
||||
|
||||
/** 创建空请求。 */
|
||||
public AgentDraftSaveRequest() {
|
||||
}
|
||||
|
||||
/** @return Agent 草稿 */
|
||||
public Agent getAgent() {
|
||||
return agent;
|
||||
}
|
||||
|
||||
/** @param agent Agent 草稿 */
|
||||
public void setAgent(Agent agent) {
|
||||
this.agent = agent;
|
||||
}
|
||||
|
||||
/** @return 工具绑定 */
|
||||
public List<AgentToolBinding> getToolBindings() {
|
||||
return toolBindings;
|
||||
}
|
||||
|
||||
/** @param toolBindings 工具绑定 */
|
||||
public void setToolBindings(List<AgentToolBinding> toolBindings) {
|
||||
this.toolBindings = toolBindings;
|
||||
}
|
||||
|
||||
/** @return 是否替换工具绑定 */
|
||||
public boolean isReplaceToolBindings() {
|
||||
return replaceToolBindings;
|
||||
}
|
||||
|
||||
/** @param replaceToolBindings 是否替换工具绑定 */
|
||||
public void setReplaceToolBindings(boolean replaceToolBindings) {
|
||||
this.replaceToolBindings = replaceToolBindings;
|
||||
}
|
||||
|
||||
/** @return 知识库绑定 */
|
||||
public List<AgentKnowledgeBinding> getKnowledgeBindings() {
|
||||
return knowledgeBindings;
|
||||
}
|
||||
|
||||
/** @param knowledgeBindings 知识库绑定 */
|
||||
public void setKnowledgeBindings(List<AgentKnowledgeBinding> knowledgeBindings) {
|
||||
this.knowledgeBindings = knowledgeBindings;
|
||||
}
|
||||
|
||||
/** @return 是否替换知识库绑定 */
|
||||
public boolean isReplaceKnowledgeBindings() {
|
||||
return replaceKnowledgeBindings;
|
||||
}
|
||||
|
||||
/** @param replaceKnowledgeBindings 是否替换知识库绑定 */
|
||||
public void setReplaceKnowledgeBindings(boolean replaceKnowledgeBindings) {
|
||||
this.replaceKnowledgeBindings = replaceKnowledgeBindings;
|
||||
}
|
||||
|
||||
/** @return Skill 绑定 */
|
||||
public List<AgentSkillBindingUpdateRequest.Binding> getSkillBindings() {
|
||||
return skillBindings;
|
||||
}
|
||||
|
||||
/** @param skillBindings Skill 绑定 */
|
||||
public void setSkillBindings(List<AgentSkillBindingUpdateRequest.Binding> skillBindings) {
|
||||
this.skillBindings = skillBindings;
|
||||
}
|
||||
|
||||
/** @return 是否替换 Skill 绑定 */
|
||||
public boolean isReplaceSkillBindings() {
|
||||
return replaceSkillBindings;
|
||||
}
|
||||
|
||||
/** @param replaceSkillBindings 是否替换 Skill 绑定 */
|
||||
public void setReplaceSkillBindings(boolean replaceSkillBindings) {
|
||||
this.replaceSkillBindings = replaceSkillBindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Skill 白名单引用转换为领域绑定。
|
||||
*
|
||||
* @return 最小 Skill 绑定列表
|
||||
*/
|
||||
public List<AgentSkillBinding> toSkillBindings() {
|
||||
return skillBindings == null
|
||||
? List.of() : skillBindings.stream().map(AgentSkillBindingUpdateRequest.Binding::toEntity).toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package tech.easyflow.admin.controller.agent;
|
||||
|
||||
import tech.easyflow.agent.entity.AgentSkillBinding;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Agent Skill 整组替换请求。
|
||||
*
|
||||
* <p>使用标准 JavaBean 以兼容 {@code @JsonBody} 的 Fastjson 1 嵌套列表转换。</p>
|
||||
*/
|
||||
public class AgentSkillBindingUpdateRequest {
|
||||
|
||||
private BigInteger agentId;
|
||||
private List<Binding> bindings;
|
||||
|
||||
/** 创建空请求。 */
|
||||
public AgentSkillBindingUpdateRequest() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Agent Skill 绑定请求。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
* @param bindings Skill 引用
|
||||
*/
|
||||
public AgentSkillBindingUpdateRequest(BigInteger agentId, List<Binding> bindings) {
|
||||
this.agentId = agentId;
|
||||
this.bindings = bindings;
|
||||
}
|
||||
|
||||
/** @return Agent ID */
|
||||
public BigInteger getAgentId() { return agentId; }
|
||||
/** @param agentId Agent ID */
|
||||
public void setAgentId(BigInteger agentId) { this.agentId = agentId; }
|
||||
/** @return Skill 引用 */
|
||||
public List<Binding> getBindings() { return bindings; }
|
||||
/** @param bindings Skill 引用 */
|
||||
public void setBindings(List<Binding> bindings) { this.bindings = bindings; }
|
||||
|
||||
/** 客户端允许提交的最小 Skill 引用。 */
|
||||
public static class Binding {
|
||||
|
||||
private BigInteger skillId;
|
||||
private Integer sortNo;
|
||||
|
||||
/** 创建空绑定。 */
|
||||
public Binding() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建最小 Skill 绑定。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param sortNo 排序号
|
||||
*/
|
||||
public Binding(BigInteger skillId, Integer sortNo) {
|
||||
this.skillId = skillId;
|
||||
this.sortNo = sortNo;
|
||||
}
|
||||
|
||||
/** @return Skill ID */
|
||||
public BigInteger getSkillId() { return skillId; }
|
||||
/** @param skillId Skill ID */
|
||||
public void setSkillId(BigInteger skillId) { this.skillId = skillId; }
|
||||
/** @return 排序号 */
|
||||
public Integer getSortNo() { return sortNo; }
|
||||
/** @param sortNo 排序号 */
|
||||
public void setSortNo(Integer sortNo) { this.sortNo = sortNo; }
|
||||
|
||||
/**
|
||||
* 转换为不含任何服务端快照的领域引用。
|
||||
*
|
||||
* @return 最小 Skill 绑定
|
||||
*/
|
||||
public AgentSkillBinding toEntity() {
|
||||
AgentSkillBinding value = new AgentSkillBinding();
|
||||
value.setSkillId(skillId);
|
||||
value.setSortNo(sortNo);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -324,7 +324,12 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
)
|
||||
public Result<Void> resume(@JsonBody(value = "executeId", required = true) String executeId,
|
||||
@JsonBody("confirmParams") Map<String, Object> confirmParams) {
|
||||
chainExecutor.resumeAsync(executeId, confirmParams);
|
||||
if (!chainExecutor.resumeAsyncIfSuspended(executeId, confirmParams)) {
|
||||
throw new BusinessException(
|
||||
409,
|
||||
40901,
|
||||
"当前执行状态不可恢复,仅暂停中的工作流允许恢复");
|
||||
}
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package tech.easyflow.admin.controller.skill;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaMode;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -17,6 +18,7 @@ import tech.easyflow.admin.controller.skill.vo.SkillCopyRequest;
|
||||
import tech.easyflow.admin.controller.skill.vo.SkillDraftRequest;
|
||||
import tech.easyflow.admin.controller.skill.vo.SkillImportBatchResultView;
|
||||
import tech.easyflow.admin.controller.skill.vo.SkillView;
|
||||
import tech.easyflow.admin.controller.skill.vo.SkillToolBindingUpdateRequest;
|
||||
import tech.easyflow.admin.controller.skill.vo.SkillPublishStatusView;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
@@ -41,6 +43,10 @@ import tech.easyflow.skill.publish.SkillPublishAppService;
|
||||
import tech.easyflow.skill.security.SkillVisibilityQueryHelper;
|
||||
import tech.easyflow.skill.service.SkillApprovalStateService;
|
||||
import tech.easyflow.skill.service.SkillService;
|
||||
import tech.easyflow.skill.service.SkillToolBindingService;
|
||||
import tech.easyflow.skill.service.SkillToolOptionQueryService;
|
||||
import tech.easyflow.skill.vo.SkillMcpToolManifestView;
|
||||
import tech.easyflow.skill.vo.SkillToolOptionPage;
|
||||
import tech.easyflow.skill.validation.SkillValidationResult;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
@@ -73,6 +79,8 @@ public class SkillController {
|
||||
private final SkillImportService skillImportService;
|
||||
private final SkillExportService skillExportService;
|
||||
private final SkillFileService skillFileService;
|
||||
private final SkillToolBindingService skillToolBindingService;
|
||||
private final SkillToolOptionQueryService skillToolOptionQueryService;
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
private final CategoryPermissionService categoryPermissionService;
|
||||
private final SkillVisibilityQueryHelper visibilityQueryHelper;
|
||||
@@ -87,6 +95,8 @@ public class SkillController {
|
||||
* @param skillImportService 导入服务
|
||||
* @param skillExportService 导出服务
|
||||
* @param skillFileService 文件服务
|
||||
* @param skillToolBindingService Skill Tool 绑定服务
|
||||
* @param skillToolOptionQueryService Skill Tool 候选查询服务
|
||||
* @param resourceAccessService 资源权限服务
|
||||
* @param categoryPermissionService 分类权限服务
|
||||
* @param visibilityQueryHelper 可见性查询助手
|
||||
@@ -98,6 +108,8 @@ public class SkillController {
|
||||
SkillImportService skillImportService,
|
||||
SkillExportService skillExportService,
|
||||
SkillFileService skillFileService,
|
||||
SkillToolBindingService skillToolBindingService,
|
||||
SkillToolOptionQueryService skillToolOptionQueryService,
|
||||
ResourceAccessService resourceAccessService,
|
||||
CategoryPermissionService categoryPermissionService,
|
||||
SkillVisibilityQueryHelper visibilityQueryHelper,
|
||||
@@ -108,6 +120,8 @@ public class SkillController {
|
||||
this.skillImportService = skillImportService;
|
||||
this.skillExportService = skillExportService;
|
||||
this.skillFileService = skillFileService;
|
||||
this.skillToolBindingService = skillToolBindingService;
|
||||
this.skillToolOptionQueryService = skillToolOptionQueryService;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
this.categoryPermissionService = categoryPermissionService;
|
||||
this.visibilityQueryHelper = visibilityQueryHelper;
|
||||
@@ -179,6 +193,54 @@ public class SkillController {
|
||||
return Result.ok(toView(skill));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Skill 可绑定的 Tool 候选。
|
||||
*
|
||||
* @param keyword 名称或描述关键词
|
||||
* @param toolType 类型过滤
|
||||
* @param pageNum 页码
|
||||
* @param pageSize 每页数量
|
||||
* @return 安全候选分页
|
||||
*/
|
||||
@GetMapping("/toolOptions")
|
||||
@SaCheckPermission(value = {"/api/v1/skill/save", "/api/v1/skill/update"}, mode = SaMode.OR)
|
||||
public Result<SkillToolOptionPage> toolOptions(String keyword, String toolType,
|
||||
Long pageNum, Long pageSize) {
|
||||
return Result.ok(skillToolOptionQueryService.page(keyword, toolType,
|
||||
pageNum == null ? 1 : pageNum, pageSize == null ? 20 : pageSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按需读取指定 MCP 的脱敏 Tool 清单。
|
||||
*
|
||||
* @param mcpId MCP ID
|
||||
* @return MCP Tool 清单
|
||||
*/
|
||||
@GetMapping("/mcpTools")
|
||||
@SaCheckPermission(value = {"/api/v1/skill/save", "/api/v1/skill/update"}, mode = SaMode.OR)
|
||||
public Result<SkillMcpToolManifestView> mcpTools(BigInteger mcpId) {
|
||||
return Result.ok(skillToolOptionQueryService.mcpTools(mcpId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子替换 Skill 的全部平台 Tool 草稿绑定。
|
||||
*
|
||||
* @param request 白名单绑定请求
|
||||
* @return 服务端规范化的安全绑定摘要
|
||||
*/
|
||||
@PostMapping("/toolBinding/update")
|
||||
@SaCheckPermission(value = {"/api/v1/skill/save", "/api/v1/skill/update"}, mode = SaMode.OR)
|
||||
public Result<List<SkillView.ToolBindingView>> updateToolBindings(
|
||||
@JsonBody(required = true, skipConvertError = false) SkillToolBindingUpdateRequest request) {
|
||||
if (request == null || request.getSkillId() == null) {
|
||||
throw new BusinessException("Skill ID 不能为空");
|
||||
}
|
||||
List<tech.easyflow.skill.entity.SkillToolBinding> bindings = request.getBindings() == null
|
||||
? List.of() : request.getBindings().stream().map(SkillToolBindingUpdateRequest.Binding::toEntity).toList();
|
||||
return Result.ok(skillToolBindingService.replaceBindings(request.getSkillId(), bindings)
|
||||
.stream().map(SkillView.ToolBindingView::from).toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Skill 草稿。
|
||||
*
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package tech.easyflow.admin.controller.skill.vo;
|
||||
|
||||
import tech.easyflow.skill.entity.SkillToolBinding;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Skill Tool 整组替换请求。
|
||||
*
|
||||
* <p>{@code @JsonBody} 当前由 Fastjson 1 完成转换,使用标准 JavaBean 可确保嵌套列表元素
|
||||
* 按声明类型转换,避免嵌套 record 被保留为 {@code JSONObject}。</p>
|
||||
*/
|
||||
public class SkillToolBindingUpdateRequest {
|
||||
|
||||
private BigInteger skillId;
|
||||
private List<Binding> bindings;
|
||||
|
||||
/** 创建空请求。 */
|
||||
public SkillToolBindingUpdateRequest() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Skill Tool 绑定请求。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param bindings 绑定引用
|
||||
*/
|
||||
public SkillToolBindingUpdateRequest(BigInteger skillId, List<Binding> bindings) {
|
||||
this.skillId = skillId;
|
||||
this.bindings = bindings;
|
||||
}
|
||||
|
||||
/** @return Skill ID */
|
||||
public BigInteger getSkillId() { return skillId; }
|
||||
/** @param skillId Skill ID */
|
||||
public void setSkillId(BigInteger skillId) { this.skillId = skillId; }
|
||||
/** @return 绑定引用 */
|
||||
public List<Binding> getBindings() { return bindings; }
|
||||
/** @param bindings 绑定引用 */
|
||||
public void setBindings(List<Binding> bindings) { this.bindings = bindings; }
|
||||
|
||||
/** 客户端允许提交的最小绑定字段。 */
|
||||
public static class Binding {
|
||||
|
||||
private String toolType;
|
||||
private BigInteger targetId;
|
||||
private Boolean hitlEnabled;
|
||||
private Integer sortNo;
|
||||
private String mcpToolManifestHash;
|
||||
|
||||
/** 创建空绑定。 */
|
||||
public Binding() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建最小 Tool 绑定。
|
||||
*
|
||||
* @param toolType Tool 类型
|
||||
* @param targetId 目标资源 ID
|
||||
* @param hitlEnabled 是否调用前确认
|
||||
* @param sortNo 排序号
|
||||
* @param mcpToolManifestHash MCP Tool 清单 hash
|
||||
*/
|
||||
public Binding(String toolType, BigInteger targetId, Boolean hitlEnabled,
|
||||
Integer sortNo, String mcpToolManifestHash) {
|
||||
this.toolType = toolType;
|
||||
this.targetId = targetId;
|
||||
this.hitlEnabled = hitlEnabled;
|
||||
this.sortNo = sortNo;
|
||||
this.mcpToolManifestHash = mcpToolManifestHash;
|
||||
}
|
||||
|
||||
/** @return Tool 类型 */
|
||||
public String getToolType() { return toolType; }
|
||||
/** @param toolType Tool 类型 */
|
||||
public void setToolType(String toolType) { this.toolType = toolType; }
|
||||
/** @return 目标资源 ID */
|
||||
public BigInteger getTargetId() { return targetId; }
|
||||
/** @param targetId 目标资源 ID */
|
||||
public void setTargetId(BigInteger targetId) { this.targetId = targetId; }
|
||||
/** @return 是否调用前确认 */
|
||||
public Boolean getHitlEnabled() { return hitlEnabled; }
|
||||
/** @param hitlEnabled 是否调用前确认 */
|
||||
public void setHitlEnabled(Boolean hitlEnabled) { this.hitlEnabled = hitlEnabled; }
|
||||
/** @return 排序号 */
|
||||
public Integer getSortNo() { return sortNo; }
|
||||
/** @param sortNo 排序号 */
|
||||
public void setSortNo(Integer sortNo) { this.sortNo = sortNo; }
|
||||
/** @return MCP Tool 清单 hash */
|
||||
public String getMcpToolManifestHash() { return mcpToolManifestHash; }
|
||||
/** @param mcpToolManifestHash MCP Tool 清单 hash */
|
||||
public void setMcpToolManifestHash(String mcpToolManifestHash) {
|
||||
this.mcpToolManifestHash = mcpToolManifestHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为领域绑定引用。
|
||||
*
|
||||
* @return 最小 Tool 绑定
|
||||
*/
|
||||
public SkillToolBinding toEntity() {
|
||||
SkillToolBinding value = new SkillToolBinding();
|
||||
value.setToolType(toolType);
|
||||
value.setTargetId(targetId);
|
||||
value.setHitlEnabled(hitlEnabled);
|
||||
value.setSortNo(sortNo);
|
||||
value.setMcpToolManifestHash(mcpToolManifestHash);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package tech.easyflow.admin.controller.skill.vo;
|
||||
import com.easyagents.skill.util.SkillResources;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.entity.SkillResource;
|
||||
import tech.easyflow.skill.entity.SkillToolBinding;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
@@ -16,7 +17,6 @@ import java.util.List;
|
||||
* @param name 标准名称
|
||||
* @param displayName 展示名称
|
||||
* @param description 用途描述
|
||||
* @param skillContent SKILL.md 内容
|
||||
* @param visibilityScope 使用范围
|
||||
* @param packageHash 标准包哈希
|
||||
* @param snapshotHash 发布快照哈希
|
||||
@@ -31,13 +31,15 @@ import java.util.List;
|
||||
* @param readable 是否可读
|
||||
* @param manageable 是否可管理
|
||||
* @param resources 资源摘要
|
||||
* @param toolBindings 平台 Tool 草稿绑定摘要
|
||||
* @param toolCount 实际 Tool 数
|
||||
* @param hasToolUpdate Tool 草稿是否与线上快照不同
|
||||
*/
|
||||
public record SkillView(BigInteger id,
|
||||
BigInteger categoryId,
|
||||
String name,
|
||||
String displayName,
|
||||
String description,
|
||||
String skillContent,
|
||||
String visibilityScope,
|
||||
String packageHash,
|
||||
String snapshotHash,
|
||||
@@ -51,7 +53,10 @@ public record SkillView(BigInteger id,
|
||||
String createdByName,
|
||||
boolean readable,
|
||||
boolean manageable,
|
||||
List<ResourceView> resources) {
|
||||
List<ResourceView> resources,
|
||||
List<ToolBindingView> toolBindings,
|
||||
int toolCount,
|
||||
boolean hasToolUpdate) {
|
||||
|
||||
/**
|
||||
* 从领域实体构造管理端视图。
|
||||
@@ -64,11 +69,18 @@ public record SkillView(BigInteger id,
|
||||
public static SkillView from(Skill skill, boolean readable, boolean manageable) {
|
||||
List<ResourceView> resources = skill.getResources() == null ? null
|
||||
: skill.getResources().stream().map(ResourceView::from).toList();
|
||||
List<ToolBindingView> toolBindings = skill.getToolBindings() == null ? null
|
||||
: skill.getToolBindings().stream().map(ToolBindingView::from).toList();
|
||||
int toolCount = skill.getToolBindings() == null ? 0 : skill.getToolBindings().stream()
|
||||
.mapToInt(binding -> "MCP".equalsIgnoreCase(binding.getToolType())
|
||||
? Math.max(0, binding.getMcpToolCount() == null ? 0 : binding.getMcpToolCount()) : 1)
|
||||
.sum();
|
||||
return new SkillView(skill.getId(), skill.getCategoryId(), skill.getName(), skill.getDisplayName(),
|
||||
skill.getDescription(), skill.getSkillContent(), skill.getVisibilityScope(), skill.getPackageHash(),
|
||||
skill.getDescription(), skill.getVisibilityScope(), skill.getPackageHash(),
|
||||
skill.getSnapshotHash(), skill.getPublishStatus(), skill.getCurrentApprovalInstanceId(),
|
||||
skill.getApprovalPending(), skill.getCurrentApprovalActionType(), skill.getDisplayPublishStatus(),
|
||||
skill.getCreated(), skill.getModified(), skill.getCreatedByName(), readable, manageable, resources);
|
||||
skill.getCreated(), skill.getModified(), skill.getCreatedByName(), readable, manageable, resources,
|
||||
toolBindings, toolCount, hasToolUpdate(skill));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,4 +109,44 @@ public record SkillView(BigInteger id,
|
||||
resource.getMediaType(), resource.getIsText(), resource.getContentHash(), resource.getSize());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasToolUpdate(Skill skill) {
|
||||
if (skill.getToolBindings() == null) {
|
||||
return false;
|
||||
}
|
||||
Object published = skill.getPublishedToolBindingsJson() == null
|
||||
? null : skill.getPublishedToolBindingsJson().get("bindings");
|
||||
List<String> currentKeys = skill.getToolBindings().stream().map(SkillView::bindingKey).toList();
|
||||
if (!(published instanceof List<?> list)) {
|
||||
return !currentKeys.isEmpty();
|
||||
}
|
||||
List<String> publishedKeys = list.stream().map(item -> {
|
||||
if (!(item instanceof java.util.Map<?, ?> map)) {
|
||||
return "INVALID";
|
||||
}
|
||||
return String.valueOf(map.get("toolType")) + ":" + map.get("targetId") + ":"
|
||||
+ Boolean.TRUE.equals(map.get("hitlEnabled")) + ":" + map.get("mcpToolManifestHash");
|
||||
}).toList();
|
||||
return !currentKeys.equals(publishedKeys);
|
||||
}
|
||||
|
||||
private static String bindingKey(SkillToolBinding binding) {
|
||||
return binding.getToolType() + ":" + binding.getTargetId() + ":"
|
||||
+ Boolean.TRUE.equals(binding.getHitlEnabled()) + ":" + binding.getMcpToolManifestHash();
|
||||
}
|
||||
|
||||
/**
|
||||
* Skill 平台 Tool 草稿绑定安全摘要。
|
||||
*/
|
||||
public record ToolBindingView(BigInteger id, String toolType, BigInteger targetId,
|
||||
Boolean hitlEnabled, Integer mcpToolCount,
|
||||
String mcpToolManifestHash, Integer sortNo,
|
||||
java.util.Map<String, Object> resourceSummary) {
|
||||
/** @param binding 绑定实体 @return 安全摘要 */
|
||||
public static ToolBindingView from(SkillToolBinding binding) {
|
||||
return new ToolBindingView(binding.getId(), binding.getToolType(), binding.getTargetId(),
|
||||
binding.getHitlEnabled(), binding.getMcpToolCount(), binding.getMcpToolManifestHash(),
|
||||
binding.getSortNo(), binding.getResourceSummary());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,6 +266,9 @@ public class AgentSessionService {
|
||||
if (!Objects.equals(summary.getUserId(), account.getId())) {
|
||||
throw new BusinessException("无权访问该 Agent 会话");
|
||||
}
|
||||
if (!Objects.equals(summary.getTenantId(), account.getTenantId())) {
|
||||
throw new BusinessException("无权访问该 Agent 会话");
|
||||
}
|
||||
}
|
||||
|
||||
private Map<BigInteger, AgentAvailability> resolveAgentAvailability(List<ChatSessionSummary> sessions) {
|
||||
|
||||
@@ -166,8 +166,14 @@ public class ChatWorkspaceService {
|
||||
roundIds.add(record.getRoundId());
|
||||
}
|
||||
}
|
||||
List<ChatMessageRecord> allVariants = new ArrayList<>();
|
||||
for (BigInteger roundId : roundIds) {
|
||||
variantsByRound.put(roundId.toString(), chatRoundOperateService.listVariants(sessionId, roundId));
|
||||
List<ChatMessageRecord> variants = chatRoundOperateService.listVariantsUnprojected(sessionId, roundId);
|
||||
variantsByRound.put(roundId.toString(), variants);
|
||||
allVariants.addAll(variants);
|
||||
}
|
||||
if (!allVariants.isEmpty()) {
|
||||
chatRoundOperateService.projectVariants(sessionId, allVariants);
|
||||
}
|
||||
ChatWorkspaceConversationView view = new ChatWorkspaceConversationView();
|
||||
view.setRecords(records);
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package tech.easyflow.admin.controller.agent;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import org.testng.Assert;
|
||||
import org.testng.annotations.Test;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBodyParser;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Agent Skill 绑定管理端 API 安全契约测试。
|
||||
*/
|
||||
public class AgentSkillBindingContractTest {
|
||||
|
||||
/**
|
||||
* 验证请求 DTO 仅暴露 Agent ID、Skill ID 与排序号。
|
||||
*
|
||||
* @throws Exception 反序列化失败
|
||||
*/
|
||||
@Test
|
||||
public void requestUsesWhitelistFieldsAndDropsServerSnapshot() throws Exception {
|
||||
JSONObject json = JSON.parseObject("""
|
||||
{
|
||||
"agentId": 10,
|
||||
"bindings": [{
|
||||
"skillId": 101,
|
||||
"sortNo": 2,
|
||||
"resourceSnapshot": {"skillContent": "forged"},
|
||||
"resourceSummary": {"displayName": "forged"}
|
||||
}]
|
||||
}
|
||||
""");
|
||||
|
||||
AgentSkillBindingUpdateRequest request = (AgentSkillBindingUpdateRequest) JsonBodyParser.parseJsonBody(
|
||||
json, AgentSkillBindingUpdateRequest.class, AgentSkillBindingUpdateRequest.class, "");
|
||||
|
||||
Assert.assertEquals(request.getAgentId(), BigInteger.TEN);
|
||||
Assert.assertEquals(request.getBindings().get(0).getSkillId(), BigInteger.valueOf(101));
|
||||
Assert.assertEquals(request.getBindings().get(0).getSortNo(), Integer.valueOf(2));
|
||||
Assert.assertTrue(request.getBindings().get(0).toEntity().getResourceSnapshot().isEmpty());
|
||||
Assert.assertTrue(request.getBindings().get(0).toEntity().getResourceSummary().isEmpty());
|
||||
Assert.assertEquals(
|
||||
Arrays.stream(AgentSkillBindingUpdateRequest.Binding.class.getDeclaredFields())
|
||||
.filter(field -> !Modifier.isStatic(field.getModifiers()))
|
||||
.map(field -> field.getName()).toList(),
|
||||
List.of("skillId", "sortNo"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证更新入口使用白名单 DTO 并返回脱敏视图。
|
||||
*
|
||||
* @throws Exception 反射失败
|
||||
*/
|
||||
@Test
|
||||
public void updateEndpointReturnsSafeSkillBindingViews() throws Exception {
|
||||
Method method = AgentController.class.getMethod(
|
||||
"updateSkillBinding", AgentSkillBindingUpdateRequest.class);
|
||||
JsonBody jsonBody = method.getParameters()[0].getAnnotation(JsonBody.class);
|
||||
ParameterizedType resultType = (ParameterizedType) method.getGenericReturnType();
|
||||
ParameterizedType listType = (ParameterizedType) resultType.getActualTypeArguments()[0];
|
||||
|
||||
Assert.assertNotNull(jsonBody);
|
||||
Assert.assertEquals(resultType.getRawType(), Result.class);
|
||||
Assert.assertEquals(listType.getRawType(), List.class);
|
||||
Assert.assertEquals(listType.getActualTypeArguments()[0], AgentDetailView.SkillBindingView.class);
|
||||
Assert.assertFalse(Arrays.stream(AgentDetailView.SkillBindingView.class.getRecordComponents())
|
||||
.anyMatch(component -> "resourceSnapshot".equals(component.getName())));
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||
import tech.easyflow.admin.controller.skill.vo.SkillCopyRequest;
|
||||
import tech.easyflow.admin.controller.skill.vo.SkillDraftRequest;
|
||||
import tech.easyflow.admin.controller.skill.vo.SkillImportBatchResultView;
|
||||
import tech.easyflow.admin.controller.skill.vo.SkillToolBindingUpdateRequest;
|
||||
import tech.easyflow.admin.controller.skill.vo.SkillView;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
@@ -25,11 +26,14 @@ import tech.easyflow.skill.publish.SkillPublishAppService;
|
||||
import tech.easyflow.skill.security.SkillVisibilityQueryHelper;
|
||||
import tech.easyflow.skill.service.SkillApprovalStateService;
|
||||
import tech.easyflow.skill.service.SkillService;
|
||||
import tech.easyflow.skill.service.SkillToolBindingService;
|
||||
import tech.easyflow.skill.service.SkillToolOptionQueryService;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -44,6 +48,40 @@ import static org.mockito.Mockito.when;
|
||||
*/
|
||||
public class SkillControllerContractTest {
|
||||
|
||||
/**
|
||||
* Tool 绑定请求能够把嵌套列表转换为白名单 JavaBean。
|
||||
*
|
||||
* @throws Exception 反序列化失败
|
||||
*/
|
||||
@Test
|
||||
public void jsonBodyParserDeserializesNestedToolBindings() throws Exception {
|
||||
JSONObject json = JSON.parseObject("""
|
||||
{
|
||||
"skillId": 101,
|
||||
"bindings": [{
|
||||
"toolType": "WORKFLOW",
|
||||
"targetId": 202,
|
||||
"hitlEnabled": true,
|
||||
"sortNo": 0,
|
||||
"resourceSnapshot": {"forged": true}
|
||||
}]
|
||||
}
|
||||
""");
|
||||
|
||||
SkillToolBindingUpdateRequest request = (SkillToolBindingUpdateRequest) JsonBodyParser.parseJsonBody(
|
||||
json, SkillToolBindingUpdateRequest.class, SkillToolBindingUpdateRequest.class, "");
|
||||
|
||||
Assert.assertEquals(request.getSkillId(), BigInteger.valueOf(101));
|
||||
Assert.assertEquals(request.getBindings().get(0).getTargetId(), BigInteger.valueOf(202));
|
||||
Assert.assertEquals(request.getBindings().get(0).getToolType(), "WORKFLOW");
|
||||
Assert.assertTrue(request.getBindings().get(0).getHitlEnabled());
|
||||
Assert.assertEquals(
|
||||
Arrays.stream(SkillToolBindingUpdateRequest.Binding.class.getDeclaredFields())
|
||||
.filter(field -> !Modifier.isStatic(field.getModifiers()))
|
||||
.map(field -> field.getName()).toList(),
|
||||
List.of("toolType", "targetId", "hitlEnabled", "sortNo", "mcpToolManifestHash"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 草稿白名单 DTO 只接受标准包治理字段。
|
||||
*
|
||||
@@ -158,6 +196,7 @@ public class SkillControllerContractTest {
|
||||
when(accessService.canAccess(any(), any(), any())).thenReturn(true);
|
||||
return new SkillController(mock(SkillService.class), mock(SkillApprovalStateService.class),
|
||||
publishService, importService, mock(SkillExportService.class), mock(SkillFileService.class),
|
||||
mock(SkillToolBindingService.class), mock(SkillToolOptionQueryService.class),
|
||||
accessService, mock(CategoryPermissionService.class), mock(SkillVisibilityQueryHelper.class),
|
||||
mock(AiResourceCreatorNameSupport.class));
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@ import tech.easyflow.agent.runtime.composer.AgentComposerDraftService;
|
||||
import tech.easyflow.agent.runtime.media.AgentMediaService;
|
||||
import tech.easyflow.agent.service.AgentService;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||
import tech.easyflow.chatlog.service.ChatSessionCommandService;
|
||||
import tech.easyflow.chatlog.service.ChatSessionQueryService;
|
||||
import tech.easyflow.chatlog.support.ChatJsonSupport;
|
||||
@@ -18,6 +21,7 @@ import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
@@ -30,6 +34,7 @@ import static org.mockito.Mockito.when;
|
||||
public class AgentSessionServiceTest {
|
||||
|
||||
private static final BigInteger ACCOUNT_ID = BigInteger.valueOf(7);
|
||||
private static final BigInteger TENANT_ID = BigInteger.valueOf(3);
|
||||
private static final BigInteger SESSION_ID = BigInteger.valueOf(101);
|
||||
|
||||
private ChatSessionQueryService chatSessionQueryService;
|
||||
@@ -63,6 +68,7 @@ public class AgentSessionServiceTest {
|
||||
);
|
||||
account = new LoginAccount();
|
||||
account.setId(ACCOUNT_ID);
|
||||
account.setTenantId(TENANT_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,6 +149,7 @@ public class AgentSessionServiceTest {
|
||||
private ChatSessionSummary buildSession(BigInteger userId, Integer isDeleted, String assistantCode) {
|
||||
ChatSessionSummary summary = new ChatSessionSummary();
|
||||
summary.setId(SESSION_ID);
|
||||
summary.setTenantId(TENANT_ID);
|
||||
summary.setUserId(userId);
|
||||
summary.setIsDeleted(isDeleted);
|
||||
summary.setAssistantCode(assistantCode);
|
||||
|
||||
Reference in New Issue
Block a user