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

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

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

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

View File

@@ -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("当前登录状态失效,请重新登录后再试");
}
}
}

View File

@@ -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);
}
/**

View File

@@ -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());
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}

View File

@@ -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 草稿。
*

View File

@@ -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;
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -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) {

View File

@@ -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);