feat: 增加技能管理模块试验性功能,等待优化
This commit is contained in:
@@ -12,6 +12,7 @@ public enum ApprovalResourceType {
|
||||
|
||||
BOT("BOT"),
|
||||
AGENT("AGENT"),
|
||||
SKILL("SKILL"),
|
||||
WORKFLOW("WORKFLOW"),
|
||||
KNOWLEDGE("KNOWLEDGE");
|
||||
|
||||
|
||||
@@ -7,4 +7,5 @@ tech.easyflow.chatlog.config.ChatlogModuleConfig
|
||||
tech.easyflow.datacenter.config.DatacenterModuleConfig
|
||||
tech.easyflow.job.config.JobModuleConfig
|
||||
tech.easyflow.log.config.LogModuleConfig
|
||||
tech.easyflow.skill.config.SkillModuleConfig
|
||||
tech.easyflow.system.config.SysModuleConfig
|
||||
|
||||
59
easyflow-modules/easyflow-module-skill/pom.xml
Normal file
59
easyflow-modules/easyflow-module-skill/pom.xml
Normal file
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-modules</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<name>easyflow-module-skill</name>
|
||||
<artifactId>easyflow-module-skill</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-ai</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-approval</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-system</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-common-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-common-satoken</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-common-file-storage</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mybatis-flex</groupId>
|
||||
<artifactId>mybatis-flex-spring-boot3-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-skill</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,14 @@
|
||||
package tech.easyflow.skill.config;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
|
||||
/**
|
||||
* Skill 模块自动配置。
|
||||
*/
|
||||
@MapperScan("tech.easyflow.skill.mapper")
|
||||
@ComponentScan("tech.easyflow.skill")
|
||||
@AutoConfiguration
|
||||
public class SkillModuleConfig {
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package tech.easyflow.skill.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||
import tech.easyflow.common.entity.DateEntity;
|
||||
import tech.easyflow.system.permission.resource.VisibilityResource;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Skill 主实体。
|
||||
*/
|
||||
@Table("tb_skill")
|
||||
public class Skill extends DateEntity implements VisibilityResource, Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||
private BigInteger id;
|
||||
@Column(tenantId = true)
|
||||
private BigInteger tenantId;
|
||||
private BigInteger deptId;
|
||||
private BigInteger categoryId;
|
||||
private String name;
|
||||
private String displayName;
|
||||
private String description;
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> metadataJson = new LinkedHashMap<>();
|
||||
private String skillContent;
|
||||
private Boolean enabled;
|
||||
private String visibilityScope;
|
||||
private String sourceType;
|
||||
private String packageHash;
|
||||
private Integer referenceCount;
|
||||
private Integer scriptCount;
|
||||
private Integer assetCount;
|
||||
private String publishStatus;
|
||||
private BigInteger currentApprovalInstanceId;
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> publishedSnapshotJson = new LinkedHashMap<>();
|
||||
private Date publishedAt;
|
||||
private BigInteger publishedBy;
|
||||
private Date created;
|
||||
private BigInteger createdBy;
|
||||
private Date modified;
|
||||
private BigInteger modifiedBy;
|
||||
|
||||
@Column(ignore = true)
|
||||
private Boolean approvalPending;
|
||||
@Column(ignore = true)
|
||||
private String currentApprovalActionType;
|
||||
@Column(ignore = true)
|
||||
private String displayPublishStatus;
|
||||
@Column(ignore = true)
|
||||
private String createdByName;
|
||||
@Column(ignore = true)
|
||||
private List<SkillReference> references;
|
||||
@Column(ignore = true)
|
||||
private List<SkillScript> scripts;
|
||||
@Column(ignore = true)
|
||||
private List<SkillAsset> assets;
|
||||
|
||||
public BigInteger getId() { return id; }
|
||||
public void setId(BigInteger id) { this.id = id; }
|
||||
public BigInteger getTenantId() { return tenantId; }
|
||||
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||
public BigInteger getDeptId() { return deptId; }
|
||||
public void setDeptId(BigInteger deptId) { this.deptId = deptId; }
|
||||
public BigInteger getCategoryId() { return categoryId; }
|
||||
public void setCategoryId(BigInteger categoryId) { this.categoryId = categoryId; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public String getDisplayName() { return displayName; }
|
||||
public void setDisplayName(String displayName) { this.displayName = displayName; }
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
public Map<String, Object> getMetadataJson() { return metadataJson; }
|
||||
public void setMetadataJson(Map<String, Object> metadataJson) { this.metadataJson = metadataJson == null ? new LinkedHashMap<>() : metadataJson; }
|
||||
public String getSkillContent() { return skillContent; }
|
||||
public void setSkillContent(String skillContent) { this.skillContent = skillContent; }
|
||||
public Boolean getEnabled() { return enabled; }
|
||||
public void setEnabled(Boolean enabled) { this.enabled = enabled; }
|
||||
public String getVisibilityScope() { return visibilityScope; }
|
||||
public void setVisibilityScope(String visibilityScope) { this.visibilityScope = visibilityScope; }
|
||||
public String getSourceType() { return sourceType; }
|
||||
public void setSourceType(String sourceType) { this.sourceType = sourceType; }
|
||||
public String getPackageHash() { return packageHash; }
|
||||
public void setPackageHash(String packageHash) { this.packageHash = packageHash; }
|
||||
public Integer getReferenceCount() { return referenceCount; }
|
||||
public void setReferenceCount(Integer referenceCount) { this.referenceCount = referenceCount; }
|
||||
public Integer getScriptCount() { return scriptCount; }
|
||||
public void setScriptCount(Integer scriptCount) { this.scriptCount = scriptCount; }
|
||||
public Integer getAssetCount() { return assetCount; }
|
||||
public void setAssetCount(Integer assetCount) { this.assetCount = assetCount; }
|
||||
public String getPublishStatus() { return publishStatus; }
|
||||
public void setPublishStatus(String publishStatus) { this.publishStatus = publishStatus; }
|
||||
public BigInteger getCurrentApprovalInstanceId() { return currentApprovalInstanceId; }
|
||||
public void setCurrentApprovalInstanceId(BigInteger currentApprovalInstanceId) { this.currentApprovalInstanceId = currentApprovalInstanceId; }
|
||||
public Map<String, Object> getPublishedSnapshotJson() { return publishedSnapshotJson; }
|
||||
public void setPublishedSnapshotJson(Map<String, Object> publishedSnapshotJson) { this.publishedSnapshotJson = publishedSnapshotJson == null ? new LinkedHashMap<>() : publishedSnapshotJson; }
|
||||
public Date getPublishedAt() { return publishedAt; }
|
||||
public void setPublishedAt(Date publishedAt) { this.publishedAt = publishedAt; }
|
||||
public BigInteger getPublishedBy() { return publishedBy; }
|
||||
public void setPublishedBy(BigInteger publishedBy) { this.publishedBy = publishedBy; }
|
||||
@Override public Date getCreated() { return created; }
|
||||
@Override public void setCreated(Date created) { this.created = created; }
|
||||
public BigInteger getCreatedBy() { return createdBy; }
|
||||
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
|
||||
@Override public Date getModified() { return modified; }
|
||||
@Override public void setModified(Date modified) { this.modified = modified; }
|
||||
public BigInteger getModifiedBy() { return modifiedBy; }
|
||||
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
|
||||
public Boolean getApprovalPending() { return approvalPending; }
|
||||
public void setApprovalPending(Boolean approvalPending) { this.approvalPending = approvalPending; }
|
||||
public String getCurrentApprovalActionType() { return currentApprovalActionType; }
|
||||
public void setCurrentApprovalActionType(String currentApprovalActionType) { this.currentApprovalActionType = currentApprovalActionType; }
|
||||
public String getDisplayPublishStatus() { return displayPublishStatus; }
|
||||
public void setDisplayPublishStatus(String displayPublishStatus) { this.displayPublishStatus = displayPublishStatus; }
|
||||
public String getCreatedByName() { return createdByName; }
|
||||
public void setCreatedByName(String createdByName) { this.createdByName = createdByName; }
|
||||
public List<SkillReference> getReferences() { return references; }
|
||||
public void setReferences(List<SkillReference> references) { this.references = references; }
|
||||
public List<SkillScript> getScripts() { return scripts; }
|
||||
public void setScripts(List<SkillScript> scripts) { this.scripts = scripts; }
|
||||
public List<SkillAsset> getAssets() { return assets; }
|
||||
public void setAssets(List<SkillAsset> assets) { this.assets = assets; }
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package tech.easyflow.skill.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Skill asset 实体。
|
||||
*/
|
||||
@Table("tb_skill_asset")
|
||||
public class SkillAsset implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||
private BigInteger id;
|
||||
@Column(tenantId = true)
|
||||
private BigInteger tenantId;
|
||||
private BigInteger skillId;
|
||||
private String path;
|
||||
private String name;
|
||||
private String mediaType;
|
||||
private String contentRef;
|
||||
private String contentHash;
|
||||
private Long size;
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> metadataJson = new LinkedHashMap<>();
|
||||
|
||||
public BigInteger getId() { return id; }
|
||||
public void setId(BigInteger id) { this.id = id; }
|
||||
public BigInteger getTenantId() { return tenantId; }
|
||||
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||
public BigInteger getSkillId() { return skillId; }
|
||||
public void setSkillId(BigInteger skillId) { this.skillId = skillId; }
|
||||
public String getPath() { return path; }
|
||||
public void setPath(String path) { this.path = path; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public String getMediaType() { return mediaType; }
|
||||
public void setMediaType(String mediaType) { this.mediaType = mediaType; }
|
||||
public String getContentRef() { return contentRef; }
|
||||
public void setContentRef(String contentRef) { this.contentRef = contentRef; }
|
||||
public String getContentHash() { return contentHash; }
|
||||
public void setContentHash(String contentHash) { this.contentHash = contentHash; }
|
||||
public Long getSize() { return size; }
|
||||
public void setSize(Long size) { this.size = size; }
|
||||
public Map<String, Object> getMetadataJson() { return metadataJson; }
|
||||
public void setMetadataJson(Map<String, Object> metadataJson) { this.metadataJson = metadataJson == null ? new LinkedHashMap<>() : metadataJson; }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package tech.easyflow.skill.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Skill asset 内容索引实体。
|
||||
*/
|
||||
@Table("tb_skill_asset_content")
|
||||
public class SkillAssetContent implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
private String contentRef;
|
||||
private String contentHash;
|
||||
private String filePath;
|
||||
private String mediaType;
|
||||
private Long size;
|
||||
private Integer refCount;
|
||||
private Date created;
|
||||
private Date modified;
|
||||
|
||||
public String getContentRef() { return contentRef; }
|
||||
public void setContentRef(String contentRef) { this.contentRef = contentRef; }
|
||||
public String getContentHash() { return contentHash; }
|
||||
public void setContentHash(String contentHash) { this.contentHash = contentHash; }
|
||||
public String getFilePath() { return filePath; }
|
||||
public void setFilePath(String filePath) { this.filePath = filePath; }
|
||||
public String getMediaType() { return mediaType; }
|
||||
public void setMediaType(String mediaType) { this.mediaType = mediaType; }
|
||||
public Long getSize() { return size; }
|
||||
public void setSize(Long size) { this.size = size; }
|
||||
public Integer getRefCount() { return refCount; }
|
||||
public void setRefCount(Integer refCount) { this.refCount = refCount; }
|
||||
public Date getCreated() { return created; }
|
||||
public void setCreated(Date created) { this.created = created; }
|
||||
public Date getModified() { return modified; }
|
||||
public void setModified(Date modified) { this.modified = modified; }
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package tech.easyflow.skill.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import tech.easyflow.common.entity.DateEntity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Skill 分类实体。
|
||||
*/
|
||||
@Table("tb_skill_category")
|
||||
public class SkillCategory extends DateEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||
private BigInteger id;
|
||||
@Column(tenantId = true)
|
||||
private BigInteger tenantId;
|
||||
private BigInteger parentId;
|
||||
private String categoryName;
|
||||
private Integer levelNo;
|
||||
private String ancestors;
|
||||
private Integer sortNo;
|
||||
private Integer status;
|
||||
private Date created;
|
||||
private BigInteger createdBy;
|
||||
private Date modified;
|
||||
private BigInteger modifiedBy;
|
||||
|
||||
public BigInteger getId() { return id; }
|
||||
public void setId(BigInteger id) { this.id = id; }
|
||||
public BigInteger getTenantId() { return tenantId; }
|
||||
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||
public BigInteger getParentId() { return parentId; }
|
||||
public void setParentId(BigInteger parentId) { this.parentId = parentId; }
|
||||
public String getCategoryName() { return categoryName; }
|
||||
public void setCategoryName(String categoryName) { this.categoryName = categoryName; }
|
||||
public Integer getLevelNo() { return levelNo; }
|
||||
public void setLevelNo(Integer levelNo) { this.levelNo = levelNo; }
|
||||
public String getAncestors() { return ancestors; }
|
||||
public void setAncestors(String ancestors) { this.ancestors = ancestors; }
|
||||
public Integer getSortNo() { return sortNo; }
|
||||
public void setSortNo(Integer sortNo) { this.sortNo = sortNo; }
|
||||
public Integer getStatus() { return status; }
|
||||
public void setStatus(Integer status) { this.status = status; }
|
||||
@Override public Date getCreated() { return created; }
|
||||
@Override public void setCreated(Date created) { this.created = created; }
|
||||
public BigInteger getCreatedBy() { return createdBy; }
|
||||
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
|
||||
@Override public Date getModified() { return modified; }
|
||||
@Override public void setModified(Date modified) { this.modified = modified; }
|
||||
public BigInteger getModifiedBy() { return modifiedBy; }
|
||||
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package tech.easyflow.skill.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Skill reference 文档实体。
|
||||
*/
|
||||
@Table("tb_skill_reference")
|
||||
public class SkillReference implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||
private BigInteger id;
|
||||
@Column(tenantId = true)
|
||||
private BigInteger tenantId;
|
||||
private BigInteger skillId;
|
||||
private String path;
|
||||
private String name;
|
||||
private String content;
|
||||
private String contentHash;
|
||||
private Long size;
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> metadataJson = new LinkedHashMap<>();
|
||||
|
||||
public BigInteger getId() { return id; }
|
||||
public void setId(BigInteger id) { this.id = id; }
|
||||
public BigInteger getTenantId() { return tenantId; }
|
||||
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||
public BigInteger getSkillId() { return skillId; }
|
||||
public void setSkillId(BigInteger skillId) { this.skillId = skillId; }
|
||||
public String getPath() { return path; }
|
||||
public void setPath(String path) { this.path = path; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
public String getContentHash() { return contentHash; }
|
||||
public void setContentHash(String contentHash) { this.contentHash = contentHash; }
|
||||
public Long getSize() { return size; }
|
||||
public void setSize(Long size) { this.size = size; }
|
||||
public Map<String, Object> getMetadataJson() { return metadataJson; }
|
||||
public void setMetadataJson(Map<String, Object> metadataJson) { this.metadataJson = metadataJson == null ? new LinkedHashMap<>() : metadataJson; }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package tech.easyflow.skill.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Skill script 实体。
|
||||
*/
|
||||
@Table("tb_skill_script")
|
||||
public class SkillScript implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||
private BigInteger id;
|
||||
@Column(tenantId = true)
|
||||
private BigInteger tenantId;
|
||||
private BigInteger skillId;
|
||||
private String path;
|
||||
private String language;
|
||||
private String content;
|
||||
private String contentHash;
|
||||
private Long size;
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> metadataJson = new LinkedHashMap<>();
|
||||
|
||||
public BigInteger getId() { return id; }
|
||||
public void setId(BigInteger id) { this.id = id; }
|
||||
public BigInteger getTenantId() { return tenantId; }
|
||||
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||
public BigInteger getSkillId() { return skillId; }
|
||||
public void setSkillId(BigInteger skillId) { this.skillId = skillId; }
|
||||
public String getPath() { return path; }
|
||||
public void setPath(String path) { this.path = path; }
|
||||
public String getLanguage() { return language; }
|
||||
public void setLanguage(String language) { this.language = language; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
public String getContentHash() { return contentHash; }
|
||||
public void setContentHash(String contentHash) { this.contentHash = contentHash; }
|
||||
public Long getSize() { return size; }
|
||||
public void setSize(Long size) { this.size = size; }
|
||||
public Map<String, Object> getMetadataJson() { return metadataJson; }
|
||||
public void setMetadataJson(Map<String, Object> metadataJson) { this.metadataJson = metadataJson == null ? new LinkedHashMap<>() : metadataJson; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package tech.easyflow.skill.file;
|
||||
|
||||
/**
|
||||
* Skill 逻辑文件内容。
|
||||
*/
|
||||
public class SkillFileContent {
|
||||
|
||||
private String path;
|
||||
private String type;
|
||||
private String content;
|
||||
private String language;
|
||||
private String mediaType;
|
||||
private Long size;
|
||||
private String downloadUrl;
|
||||
|
||||
public String getPath() { return path; }
|
||||
public void setPath(String path) { this.path = path; }
|
||||
public String getType() { return type; }
|
||||
public void setType(String type) { this.type = type; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
public String getLanguage() { return language; }
|
||||
public void setLanguage(String language) { this.language = language; }
|
||||
public String getMediaType() { return mediaType; }
|
||||
public void setMediaType(String mediaType) { this.mediaType = mediaType; }
|
||||
public Long getSize() { return size; }
|
||||
public void setSize(Long size) { this.size = size; }
|
||||
public String getDownloadUrl() { return downloadUrl; }
|
||||
public void setDownloadUrl(String downloadUrl) { this.downloadUrl = downloadUrl; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package tech.easyflow.skill.file;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Skill 逻辑文件树节点。
|
||||
*/
|
||||
public class SkillFileNode {
|
||||
|
||||
private String key;
|
||||
private String path;
|
||||
private String name;
|
||||
private String type;
|
||||
private String language;
|
||||
private String mediaType;
|
||||
private Long size;
|
||||
private List<SkillFileNode> children = new ArrayList<>();
|
||||
|
||||
public String getKey() { return key; }
|
||||
public void setKey(String key) { this.key = key; }
|
||||
public String getPath() { return path; }
|
||||
public void setPath(String path) { this.path = path; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public String getType() { return type; }
|
||||
public void setType(String type) { this.type = type; }
|
||||
public String getLanguage() { return language; }
|
||||
public void setLanguage(String language) { this.language = language; }
|
||||
public String getMediaType() { return mediaType; }
|
||||
public void setMediaType(String mediaType) { this.mediaType = mediaType; }
|
||||
public Long getSize() { return size; }
|
||||
public void setSize(Long size) { this.size = size; }
|
||||
public List<SkillFileNode> getChildren() { return children; }
|
||||
public void setChildren(List<SkillFileNode> children) { this.children = children == null ? new ArrayList<>() : children; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package tech.easyflow.skill.file;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* Skill 文本文件保存请求。
|
||||
*/
|
||||
public class SkillFileSaveRequest {
|
||||
|
||||
private BigInteger skillId;
|
||||
private String path;
|
||||
private String content;
|
||||
|
||||
public BigInteger getSkillId() { return skillId; }
|
||||
public void setSkillId(BigInteger skillId) { this.skillId = skillId; }
|
||||
public String getPath() { return path; }
|
||||
public void setPath(String path) { this.path = path; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package tech.easyflow.skill.file;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Skill 逻辑文件服务。
|
||||
*/
|
||||
public interface SkillFileService {
|
||||
|
||||
/**
|
||||
* 获取 Skill 文件树。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @return 文件树
|
||||
*/
|
||||
List<SkillFileNode> tree(BigInteger skillId);
|
||||
|
||||
/**
|
||||
* 获取逻辑文件内容。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param path 逻辑路径
|
||||
* @return 文件内容
|
||||
*/
|
||||
SkillFileContent getContent(BigInteger skillId, String path);
|
||||
|
||||
/**
|
||||
* 保存文本文件内容。
|
||||
*
|
||||
* @param request 保存请求
|
||||
* @return 保存后的文件内容
|
||||
*/
|
||||
SkillFileContent saveContent(SkillFileSaveRequest request);
|
||||
|
||||
/**
|
||||
* 删除逻辑文件。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param path 逻辑路径
|
||||
*/
|
||||
void deleteFile(BigInteger skillId, String path);
|
||||
|
||||
/**
|
||||
* 上传 asset 文件。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param path asset 逻辑路径
|
||||
* @param file 上传文件
|
||||
* @return asset 文件内容
|
||||
*/
|
||||
SkillFileContent uploadAsset(BigInteger skillId, String path, MultipartFile file);
|
||||
|
||||
/**
|
||||
* 打开 asset 输入流。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param path asset 逻辑路径
|
||||
* @return asset 输入流
|
||||
*/
|
||||
InputStream openAsset(BigInteger skillId, String path);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,460 @@
|
||||
package tech.easyflow.skill.file;
|
||||
|
||||
import com.easyagents.skill.model.SkillScriptLanguage;
|
||||
import com.easyagents.skill.util.SkillHashes;
|
||||
import com.easyagents.skill.util.SkillPaths;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.entity.SkillAsset;
|
||||
import tech.easyflow.skill.entity.SkillReference;
|
||||
import tech.easyflow.skill.entity.SkillScript;
|
||||
import tech.easyflow.skill.service.SkillAssetService;
|
||||
import tech.easyflow.skill.service.SkillReferenceService;
|
||||
import tech.easyflow.skill.service.SkillScriptService;
|
||||
import tech.easyflow.skill.service.SkillService;
|
||||
import tech.easyflow.skill.store.DBSkillContentStore;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Skill 逻辑文件服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class SkillFileServiceImpl implements SkillFileService {
|
||||
|
||||
private static final String DEFAULT_MEDIA_TYPE = "application/octet-stream";
|
||||
|
||||
private final SkillService skillService;
|
||||
private final SkillReferenceService skillReferenceService;
|
||||
private final SkillScriptService skillScriptService;
|
||||
private final SkillAssetService skillAssetService;
|
||||
private final DBSkillContentStore contentStore;
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
|
||||
/**
|
||||
* 创建 Skill 逻辑文件服务。
|
||||
*
|
||||
* @param skillService Skill 服务
|
||||
* @param skillReferenceService reference 服务
|
||||
* @param skillScriptService script 服务
|
||||
* @param skillAssetService asset 服务
|
||||
* @param contentStore asset 内容存储
|
||||
* @param resourceAccessService 资源访问服务
|
||||
*/
|
||||
public SkillFileServiceImpl(SkillService skillService,
|
||||
SkillReferenceService skillReferenceService,
|
||||
SkillScriptService skillScriptService,
|
||||
SkillAssetService skillAssetService,
|
||||
DBSkillContentStore contentStore,
|
||||
ResourceAccessService resourceAccessService) {
|
||||
this.skillService = skillService;
|
||||
this.skillReferenceService = skillReferenceService;
|
||||
this.skillScriptService = skillScriptService;
|
||||
this.skillAssetService = skillAssetService;
|
||||
this.contentStore = contentStore;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public List<SkillFileNode> tree(BigInteger skillId) {
|
||||
Skill skill = requireReadableSkill(skillId);
|
||||
List<SkillFileNode> roots = new ArrayList<>();
|
||||
roots.add(fileNode(SkillPaths.SKILL_FILE, "SKILL.md", SkillFileType.SKILL.name()));
|
||||
roots.add(directoryNode(SkillPaths.REFERENCES_DIR, skill.getReferences().stream()
|
||||
.map(item -> fileNode(item.getPath(), SkillPaths.fileName(item.getPath()), SkillFileType.REFERENCE.name()))
|
||||
.toList()));
|
||||
roots.add(directoryNode(SkillPaths.SCRIPTS_DIR, skill.getScripts().stream()
|
||||
.map(item -> {
|
||||
SkillFileNode node = fileNode(item.getPath(), SkillPaths.fileName(item.getPath()), SkillFileType.SCRIPT.name());
|
||||
node.setLanguage(item.getLanguage());
|
||||
return node;
|
||||
})
|
||||
.toList()));
|
||||
roots.add(directoryNode(SkillPaths.ASSETS_DIR, skill.getAssets().stream()
|
||||
.map(item -> {
|
||||
SkillFileNode node = fileNode(item.getPath(), SkillPaths.fileName(item.getPath()), SkillFileType.ASSET.name());
|
||||
node.setMediaType(item.getMediaType());
|
||||
node.setSize(item.getSize());
|
||||
return node;
|
||||
})
|
||||
.toList()));
|
||||
return roots;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public SkillFileContent getContent(BigInteger skillId, String path) {
|
||||
Skill skill = requireReadableSkill(skillId);
|
||||
String normalizedPath = SkillPaths.normalize(path);
|
||||
if (SkillPaths.SKILL_FILE.equals(normalizedPath)) {
|
||||
SkillFileContent content = new SkillFileContent();
|
||||
content.setPath(SkillPaths.SKILL_FILE);
|
||||
content.setType(SkillFileType.SKILL.name());
|
||||
content.setContent(skill.getSkillContent());
|
||||
content.setSize((long) bytes(skill.getSkillContent()).length);
|
||||
return content;
|
||||
}
|
||||
String topDir = SkillPaths.firstSegment(normalizedPath);
|
||||
if (SkillPaths.REFERENCES_DIR.equals(topDir)) {
|
||||
SkillReference reference = requireReference(skillId, normalizedPath);
|
||||
SkillFileContent content = new SkillFileContent();
|
||||
content.setPath(reference.getPath());
|
||||
content.setType(SkillFileType.REFERENCE.name());
|
||||
content.setContent(reference.getContent());
|
||||
content.setSize(reference.getSize());
|
||||
return content;
|
||||
}
|
||||
if (SkillPaths.SCRIPTS_DIR.equals(topDir)) {
|
||||
SkillScript script = requireScript(skillId, normalizedPath);
|
||||
SkillFileContent content = new SkillFileContent();
|
||||
content.setPath(script.getPath());
|
||||
content.setType(SkillFileType.SCRIPT.name());
|
||||
content.setContent(script.getContent());
|
||||
content.setLanguage(script.getLanguage());
|
||||
content.setSize(script.getSize());
|
||||
return content;
|
||||
}
|
||||
if (SkillPaths.ASSETS_DIR.equals(topDir)) {
|
||||
SkillAsset asset = requireAsset(skillId, normalizedPath);
|
||||
SkillFileContent content = new SkillFileContent();
|
||||
content.setPath(asset.getPath());
|
||||
content.setType(SkillFileType.ASSET.name());
|
||||
content.setMediaType(asset.getMediaType());
|
||||
content.setSize(asset.getSize());
|
||||
content.setDownloadUrl("/api/v1/skill/file/asset");
|
||||
return content;
|
||||
}
|
||||
throw new BusinessException("不支持的 Skill 文件路径");
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public SkillFileContent saveContent(SkillFileSaveRequest request) {
|
||||
if (request == null || request.getSkillId() == null) {
|
||||
throw new BusinessException("Skill ID 不能为空");
|
||||
}
|
||||
Skill skill = requireManageSkill(request.getSkillId());
|
||||
String normalizedPath = SkillPaths.normalize(request.getPath());
|
||||
String content = request.getContent() == null ? "" : request.getContent();
|
||||
if (SkillPaths.SKILL_FILE.equals(normalizedPath)) {
|
||||
skill.setSkillContent(content);
|
||||
skillService.updateDraft(skill);
|
||||
return getContent(skill.getId(), SkillPaths.SKILL_FILE);
|
||||
}
|
||||
String topDir = SkillPaths.firstSegment(normalizedPath);
|
||||
if (SkillPaths.REFERENCES_DIR.equals(topDir)) {
|
||||
saveReference(skill, normalizedPath, content);
|
||||
refreshSkillCounts(skill.getId());
|
||||
return getContent(skill.getId(), normalizedPath);
|
||||
}
|
||||
if (SkillPaths.SCRIPTS_DIR.equals(topDir)) {
|
||||
saveScript(skill, normalizedPath, content);
|
||||
refreshSkillCounts(skill.getId());
|
||||
return getContent(skill.getId(), normalizedPath);
|
||||
}
|
||||
throw new BusinessException("仅支持保存 SKILL.md、references/*.md 和 scripts/*.py|*.js|*.sh");
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteFile(BigInteger skillId, String path) {
|
||||
requireManageSkill(skillId);
|
||||
String normalizedPath = SkillPaths.normalize(path);
|
||||
if (SkillPaths.SKILL_FILE.equals(normalizedPath)) {
|
||||
throw new BusinessException("SKILL.md 不允许删除");
|
||||
}
|
||||
String topDir = SkillPaths.firstSegment(normalizedPath);
|
||||
if (SkillPaths.REFERENCES_DIR.equals(topDir)) {
|
||||
skillReferenceService.remove(QueryWrapper.create().eq(SkillReference::getSkillId, skillId).eq(SkillReference::getPath, normalizedPath));
|
||||
} else if (SkillPaths.SCRIPTS_DIR.equals(topDir)) {
|
||||
skillScriptService.remove(QueryWrapper.create().eq(SkillScript::getSkillId, skillId).eq(SkillScript::getPath, normalizedPath));
|
||||
} else if (SkillPaths.ASSETS_DIR.equals(topDir)) {
|
||||
skillAssetService.remove(QueryWrapper.create().eq(SkillAsset::getSkillId, skillId).eq(SkillAsset::getPath, normalizedPath));
|
||||
} else {
|
||||
throw new BusinessException("不支持的 Skill 文件路径");
|
||||
}
|
||||
refreshSkillCounts(skillId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public SkillFileContent uploadAsset(BigInteger skillId, String path, MultipartFile file) {
|
||||
Skill skill = requireManageSkill(skillId);
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new BusinessException("asset 文件不能为空");
|
||||
}
|
||||
String normalizedPath = normalizeAssetPath(path, file.getOriginalFilename());
|
||||
try {
|
||||
String contentRef = contentStore.put(file.getBytes());
|
||||
String hash = contentRef.substring("sha256:".length());
|
||||
SkillAsset asset = findAsset(skillId, normalizedPath);
|
||||
if (asset == null) {
|
||||
asset = new SkillAsset();
|
||||
asset.setTenantId(skill.getTenantId());
|
||||
asset.setSkillId(skillId);
|
||||
asset.setPath(normalizedPath);
|
||||
}
|
||||
asset.setName(SkillPaths.fileName(normalizedPath));
|
||||
asset.setMediaType(detectMediaType(normalizedPath));
|
||||
asset.setContentRef(contentRef);
|
||||
asset.setContentHash(hash);
|
||||
asset.setSize(file.getSize());
|
||||
if (asset.getId() == null) {
|
||||
skillAssetService.save(asset);
|
||||
} else {
|
||||
skillAssetService.updateById(asset);
|
||||
}
|
||||
refreshSkillCounts(skillId);
|
||||
return getContent(skillId, normalizedPath);
|
||||
} catch (IOException e) {
|
||||
throw new BusinessException("读取 asset 文件失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public InputStream openAsset(BigInteger skillId, String path) {
|
||||
requireReadableSkill(skillId);
|
||||
SkillAsset asset = requireAsset(skillId, SkillPaths.normalize(path));
|
||||
return contentStore.open(asset.getContentRef());
|
||||
}
|
||||
|
||||
private Skill requireReadableSkill(BigInteger skillId) {
|
||||
Skill skill = requireSkill(skillId);
|
||||
resourceAccessService.assertAccess(CategoryResourceType.SKILL, skill, ResourceAction.READ, "无权限查看该 Skill");
|
||||
return skillService.getDetail(skillId);
|
||||
}
|
||||
|
||||
private Skill requireManageSkill(BigInteger skillId) {
|
||||
Skill skill = skillService.getDetail(skillId);
|
||||
resourceAccessService.assertAccess(CategoryResourceType.SKILL, skill, ResourceAction.MANAGE, "无权限管理该 Skill");
|
||||
return skill;
|
||||
}
|
||||
|
||||
private Skill requireSkill(BigInteger skillId) {
|
||||
if (skillId == null) {
|
||||
throw new BusinessException("Skill ID 不能为空");
|
||||
}
|
||||
Skill skill = skillService.getById(skillId);
|
||||
if (skill == null) {
|
||||
throw new BusinessException("Skill 不存在");
|
||||
}
|
||||
return skill;
|
||||
}
|
||||
|
||||
private void saveReference(Skill skill, String path, String content) {
|
||||
if (!SkillPaths.hasExtension(path, ".md")) {
|
||||
throw new BusinessException("reference 仅支持 .md 文件");
|
||||
}
|
||||
SkillReference reference = findReference(skill.getId(), path);
|
||||
if (reference == null) {
|
||||
reference = new SkillReference();
|
||||
reference.setTenantId(skill.getTenantId());
|
||||
reference.setSkillId(skill.getId());
|
||||
reference.setPath(path);
|
||||
}
|
||||
byte[] bytes = bytes(content);
|
||||
reference.setName(SkillPaths.fileName(path));
|
||||
reference.setContent(content);
|
||||
reference.setContentHash(SkillHashes.sha256Hex(bytes));
|
||||
reference.setSize((long) bytes.length);
|
||||
if (reference.getId() == null) {
|
||||
skillReferenceService.save(reference);
|
||||
} else {
|
||||
skillReferenceService.updateById(reference);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveScript(Skill skill, String path, String content) {
|
||||
SkillScriptLanguage language = SkillScriptLanguage.fromPath(path);
|
||||
if (language == SkillScriptLanguage.UNKNOWN) {
|
||||
throw new BusinessException("script 仅支持 .py、.js、.sh 文件");
|
||||
}
|
||||
SkillScript script = findScript(skill.getId(), path);
|
||||
if (script == null) {
|
||||
script = new SkillScript();
|
||||
script.setTenantId(skill.getTenantId());
|
||||
script.setSkillId(skill.getId());
|
||||
script.setPath(path);
|
||||
}
|
||||
byte[] bytes = bytes(content);
|
||||
script.setLanguage(language.name());
|
||||
script.setContent(content);
|
||||
script.setContentHash(SkillHashes.sha256Hex(bytes));
|
||||
script.setSize((long) bytes.length);
|
||||
if (script.getId() == null) {
|
||||
skillScriptService.save(script);
|
||||
} else {
|
||||
skillScriptService.updateById(script);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshSkillCounts(BigInteger skillId) {
|
||||
Skill skill = skillService.getDetail(skillId);
|
||||
Skill update = new Skill();
|
||||
update.setId(skillId);
|
||||
update.setReferenceCount(skill.getReferences() == null ? 0 : skill.getReferences().size());
|
||||
update.setScriptCount(skill.getScripts() == null ? 0 : skill.getScripts().size());
|
||||
update.setAssetCount(skill.getAssets() == null ? 0 : skill.getAssets().size());
|
||||
skillService.updateById(update);
|
||||
}
|
||||
|
||||
private SkillReference findReference(BigInteger skillId, String path) {
|
||||
List<SkillReference> records = skillReferenceService.list(QueryWrapper.create()
|
||||
.eq(SkillReference::getSkillId, skillId)
|
||||
.eq(SkillReference::getPath, path));
|
||||
return records.isEmpty() ? null : records.get(0);
|
||||
}
|
||||
|
||||
private SkillReference requireReference(BigInteger skillId, String path) {
|
||||
SkillReference reference = findReference(skillId, path);
|
||||
if (reference == null) {
|
||||
throw new BusinessException("reference 文件不存在");
|
||||
}
|
||||
return reference;
|
||||
}
|
||||
|
||||
private SkillScript findScript(BigInteger skillId, String path) {
|
||||
List<SkillScript> records = skillScriptService.list(QueryWrapper.create()
|
||||
.eq(SkillScript::getSkillId, skillId)
|
||||
.eq(SkillScript::getPath, path));
|
||||
return records.isEmpty() ? null : records.get(0);
|
||||
}
|
||||
|
||||
private SkillScript requireScript(BigInteger skillId, String path) {
|
||||
SkillScript script = findScript(skillId, path);
|
||||
if (script == null) {
|
||||
throw new BusinessException("script 文件不存在");
|
||||
}
|
||||
return script;
|
||||
}
|
||||
|
||||
private SkillAsset findAsset(BigInteger skillId, String path) {
|
||||
List<SkillAsset> records = skillAssetService.list(QueryWrapper.create()
|
||||
.eq(SkillAsset::getSkillId, skillId)
|
||||
.eq(SkillAsset::getPath, path));
|
||||
return records.isEmpty() ? null : records.get(0);
|
||||
}
|
||||
|
||||
private SkillAsset requireAsset(BigInteger skillId, String path) {
|
||||
SkillAsset asset = findAsset(skillId, path);
|
||||
if (asset == null) {
|
||||
throw new BusinessException("asset 文件不存在");
|
||||
}
|
||||
return asset;
|
||||
}
|
||||
|
||||
private String normalizeAssetPath(String path, String originalFilename) {
|
||||
String effectivePath = path;
|
||||
if (effectivePath == null || effectivePath.isBlank()) {
|
||||
effectivePath = SkillPaths.ASSETS_DIR + "/" + (originalFilename == null ? "asset.bin" : originalFilename);
|
||||
}
|
||||
String normalized = SkillPaths.normalize(effectivePath);
|
||||
if (!SkillPaths.ASSETS_DIR.equals(SkillPaths.firstSegment(normalized))) {
|
||||
throw new BusinessException("asset 必须位于 assets/ 目录下");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private SkillFileNode directoryNode(String name, List<SkillFileNode> flatChildren) {
|
||||
SkillFileNode node = new SkillFileNode();
|
||||
node.setKey(name);
|
||||
node.setPath(name);
|
||||
node.setName(name + "/");
|
||||
node.setType("DIRECTORY");
|
||||
node.setChildren(toNestedChildren(name, flatChildren));
|
||||
return node;
|
||||
}
|
||||
|
||||
private List<SkillFileNode> toNestedChildren(String root, List<SkillFileNode> flatChildren) {
|
||||
Map<String, SkillFileNode> nodes = new LinkedHashMap<>();
|
||||
for (SkillFileNode child : flatChildren) {
|
||||
String[] segments = child.getPath().substring(root.length() + 1).split("/");
|
||||
String currentPath = root;
|
||||
for (int i = 0; i < segments.length; i++) {
|
||||
currentPath = currentPath + "/" + segments[i];
|
||||
boolean leaf = i == segments.length - 1;
|
||||
if (leaf) {
|
||||
nodes.put(currentPath, child);
|
||||
} else {
|
||||
String segmentName = segments[i];
|
||||
nodes.computeIfAbsent(currentPath, key -> {
|
||||
SkillFileNode directory = new SkillFileNode();
|
||||
directory.setKey(key);
|
||||
directory.setPath(key);
|
||||
directory.setName(segmentName + "/");
|
||||
directory.setType("DIRECTORY");
|
||||
return directory;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
List<SkillFileNode> roots = new ArrayList<>();
|
||||
for (SkillFileNode node : nodes.values()) {
|
||||
String parentPath = parentPath(node.getPath());
|
||||
if (root.equals(parentPath)) {
|
||||
roots.add(node);
|
||||
} else {
|
||||
SkillFileNode parent = nodes.get(parentPath);
|
||||
if (parent != null) {
|
||||
parent.getChildren().add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
return roots;
|
||||
}
|
||||
|
||||
private SkillFileNode fileNode(String path, String name, String type) {
|
||||
SkillFileNode node = new SkillFileNode();
|
||||
node.setKey(path);
|
||||
node.setPath(path);
|
||||
node.setName(name);
|
||||
node.setType(type);
|
||||
return node;
|
||||
}
|
||||
|
||||
private String parentPath(String path) {
|
||||
int index = path.lastIndexOf('/');
|
||||
return index < 0 ? "" : path.substring(0, index);
|
||||
}
|
||||
|
||||
private byte[] bytes(String content) {
|
||||
return (content == null ? "" : content).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private String detectMediaType(String path) {
|
||||
String mediaType = URLConnection.guessContentTypeFromName(path);
|
||||
return mediaType == null ? DEFAULT_MEDIA_TYPE : mediaType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package tech.easyflow.skill.file;
|
||||
|
||||
/**
|
||||
* Skill 逻辑文件类型。
|
||||
*/
|
||||
public enum SkillFileType {
|
||||
|
||||
/**
|
||||
* SKILL.md 主文件。
|
||||
*/
|
||||
SKILL,
|
||||
|
||||
/**
|
||||
* references/ 下的 Markdown 文档。
|
||||
*/
|
||||
REFERENCE,
|
||||
|
||||
/**
|
||||
* scripts/ 下的脚本文件。
|
||||
*/
|
||||
SCRIPT,
|
||||
|
||||
/**
|
||||
* assets/ 下的静态资产。
|
||||
*/
|
||||
ASSET
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package tech.easyflow.skill.imports;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Skill zip 导出服务。
|
||||
*/
|
||||
public interface SkillExportService {
|
||||
|
||||
/**
|
||||
* 导出一个或多个 Skill 为标准 zip 包。
|
||||
*
|
||||
* @param skillIds Skill ID 集合
|
||||
* @param outputStream zip 输出流
|
||||
*/
|
||||
void exportZip(Collection<BigInteger> skillIds, OutputStream outputStream);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package tech.easyflow.skill.imports;
|
||||
|
||||
import com.easyagents.skill.util.SkillPaths;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.entity.SkillAsset;
|
||||
import tech.easyflow.skill.entity.SkillReference;
|
||||
import tech.easyflow.skill.entity.SkillScript;
|
||||
import tech.easyflow.skill.service.SkillService;
|
||||
import tech.easyflow.skill.store.DBSkillContentStore;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
/**
|
||||
* Skill zip 导出服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class SkillExportServiceImpl implements SkillExportService {
|
||||
|
||||
private final SkillService skillService;
|
||||
private final DBSkillContentStore contentStore;
|
||||
|
||||
/**
|
||||
* 创建 Skill 导出服务。
|
||||
*
|
||||
* @param skillService Skill 服务
|
||||
* @param contentStore Skill asset 内容存储
|
||||
*/
|
||||
public SkillExportServiceImpl(SkillService skillService, DBSkillContentStore contentStore) {
|
||||
this.skillService = skillService;
|
||||
this.contentStore = contentStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void exportZip(Collection<BigInteger> skillIds, OutputStream outputStream) {
|
||||
if (skillIds == null || skillIds.isEmpty()) {
|
||||
throw new BusinessException("请选择要导出的 Skill");
|
||||
}
|
||||
try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream, StandardCharsets.UTF_8)) {
|
||||
Set<String> folderNames = new LinkedHashSet<>();
|
||||
for (BigInteger skillId : skillIds) {
|
||||
Skill skill = skillService.getDetail(skillId);
|
||||
writeSkill(zipOutputStream, folderNames, skill);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new BusinessException("导出 Skill 失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void writeSkill(ZipOutputStream zipOutputStream, Set<String> folderNames, Skill skill) throws IOException {
|
||||
String folder = uniqueFolderName(folderNames, skill.getName());
|
||||
writeText(zipOutputStream, folder + "/" + SkillPaths.SKILL_FILE, skill.getSkillContent());
|
||||
if (skill.getReferences() != null) {
|
||||
for (SkillReference reference : skill.getReferences()) {
|
||||
writeText(zipOutputStream, folder + "/" + reference.getPath(), reference.getContent());
|
||||
}
|
||||
}
|
||||
if (skill.getScripts() != null) {
|
||||
for (SkillScript script : skill.getScripts()) {
|
||||
writeText(zipOutputStream, folder + "/" + script.getPath(), script.getContent());
|
||||
}
|
||||
}
|
||||
if (skill.getAssets() != null) {
|
||||
for (SkillAsset asset : skill.getAssets()) {
|
||||
zipOutputStream.putNextEntry(new ZipEntry(folder + "/" + asset.getPath()));
|
||||
zipOutputStream.write(contentStore.readAllBytes(asset.getContentRef()));
|
||||
zipOutputStream.closeEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeText(ZipOutputStream zipOutputStream, String path, String content) throws IOException {
|
||||
zipOutputStream.putNextEntry(new ZipEntry(path));
|
||||
zipOutputStream.write((content == null ? "" : content).getBytes(StandardCharsets.UTF_8));
|
||||
zipOutputStream.closeEntry();
|
||||
}
|
||||
|
||||
private String uniqueFolderName(Set<String> folderNames, String name) {
|
||||
String base = sanitizeFolderName(name);
|
||||
String candidate = base;
|
||||
int index = 2;
|
||||
while (!folderNames.add(candidate)) {
|
||||
candidate = base + "-" + index++;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private String sanitizeFolderName(String value) {
|
||||
String sanitized = value == null ? "skill" : value.trim().replaceAll("[\\\\/:*?\"<>|\\s]+", "-");
|
||||
sanitized = sanitized.replaceAll("^-+", "").replaceAll("-+$", "");
|
||||
return sanitized.isBlank() ? "skill" : sanitized;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package tech.easyflow.skill.imports;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Skill 导入预览结果。
|
||||
*/
|
||||
public class SkillImportPreview {
|
||||
|
||||
private List<SkillImportPreviewItem> skills = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 获取导入 Skill 预览项。
|
||||
*
|
||||
* @return 预览项列表
|
||||
*/
|
||||
public List<SkillImportPreviewItem> getSkills() {
|
||||
return skills;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置导入 Skill 预览项。
|
||||
*
|
||||
* @param skills 预览项列表
|
||||
*/
|
||||
public void setSkills(List<SkillImportPreviewItem> skills) {
|
||||
this.skills = skills == null ? new ArrayList<>() : skills;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package tech.easyflow.skill.imports;
|
||||
|
||||
/**
|
||||
* Skill 导入预览项。
|
||||
*/
|
||||
public class SkillImportPreviewItem {
|
||||
|
||||
private String packageId;
|
||||
private String name;
|
||||
private String description;
|
||||
private int referenceCount;
|
||||
private int scriptCount;
|
||||
private int assetCount;
|
||||
private boolean conflict;
|
||||
|
||||
/**
|
||||
* 获取包内 Skill ID。
|
||||
*
|
||||
* @return 包内 Skill ID
|
||||
*/
|
||||
public String getPackageId() {
|
||||
return packageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置包内 Skill ID。
|
||||
*
|
||||
* @param packageId 包内 Skill ID
|
||||
*/
|
||||
public void setPackageId(String packageId) {
|
||||
this.packageId = packageId;
|
||||
}
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
public int getReferenceCount() { return referenceCount; }
|
||||
public void setReferenceCount(int referenceCount) { this.referenceCount = referenceCount; }
|
||||
public int getScriptCount() { return scriptCount; }
|
||||
public void setScriptCount(int scriptCount) { this.scriptCount = scriptCount; }
|
||||
public int getAssetCount() { return assetCount; }
|
||||
public void setAssetCount(int assetCount) { this.assetCount = assetCount; }
|
||||
public boolean isConflict() { return conflict; }
|
||||
public void setConflict(boolean conflict) { this.conflict = conflict; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package tech.easyflow.skill.imports;
|
||||
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Skill zip 导入服务。
|
||||
*/
|
||||
public interface SkillImportService {
|
||||
|
||||
/**
|
||||
* 预览 zip 中的 Skill 包。
|
||||
*
|
||||
* @param inputStream zip 输入流
|
||||
* @return 导入预览
|
||||
*/
|
||||
SkillImportPreview preview(InputStream inputStream);
|
||||
|
||||
/**
|
||||
* 确认导入 zip 中的 Skill 包。
|
||||
*
|
||||
* @param inputStream zip 输入流
|
||||
* @param categoryId 目标分类 ID,可为空
|
||||
* @param overwriteDraft 是否覆盖同名草稿
|
||||
* @return 已保存 Skill 列表
|
||||
*/
|
||||
List<Skill> importZip(InputStream inputStream, BigInteger categoryId, boolean overwriteDraft);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package tech.easyflow.skill.imports;
|
||||
|
||||
import com.easyagents.skill.codec.ZipSkillPackageCodec;
|
||||
import com.easyagents.skill.util.SkillHashes;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.service.SkillService;
|
||||
import tech.easyflow.skill.store.DBSkillContentStore;
|
||||
import tech.easyflow.skill.support.SkillModelConverter;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Skill zip 导入服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class SkillImportServiceImpl implements SkillImportService {
|
||||
|
||||
private final SkillService skillService;
|
||||
private final DBSkillContentStore contentStore;
|
||||
|
||||
/**
|
||||
* 创建 Skill 导入服务。
|
||||
*
|
||||
* @param skillService Skill 服务
|
||||
* @param contentStore Skill asset 内容存储
|
||||
*/
|
||||
public SkillImportServiceImpl(SkillService skillService, DBSkillContentStore contentStore) {
|
||||
this.skillService = skillService;
|
||||
this.contentStore = contentStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public SkillImportPreview preview(InputStream inputStream) {
|
||||
List<com.easyagents.skill.model.Skill> importedSkills = new ZipSkillPackageCodec().importZip(inputStream);
|
||||
SkillImportPreview preview = new SkillImportPreview();
|
||||
preview.setSkills(importedSkills.stream().map(this::toPreviewItem).toList());
|
||||
return preview;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<Skill> importZip(InputStream inputStream, BigInteger categoryId, boolean overwriteDraft) {
|
||||
List<com.easyagents.skill.model.Skill> importedSkills = new ZipSkillPackageCodec(contentStore).importZip(inputStream);
|
||||
List<Skill> savedSkills = new ArrayList<>();
|
||||
for (com.easyagents.skill.model.Skill imported : importedSkills) {
|
||||
Skill existing = findByName(imported.getName());
|
||||
if (existing != null) {
|
||||
if (!overwriteDraft || !PublishStatus.DRAFT.getCode().equals(existing.getPublishStatus())) {
|
||||
throw new BusinessException("Skill 已存在,且不允许覆盖:" + imported.getName());
|
||||
}
|
||||
Skill replacement = SkillModelConverter.fromAgentSkill(imported);
|
||||
replacement.setId(existing.getId());
|
||||
replacement.setCategoryId(categoryId);
|
||||
replacement.setSourceType("ZIP");
|
||||
replacement.setPackageHash(SkillHashes.sha256Hex(imported.getSkillContent().getBytes(StandardCharsets.UTF_8)));
|
||||
savedSkills.add(skillService.updateDraft(replacement));
|
||||
} else {
|
||||
Skill skill = SkillModelConverter.fromAgentSkill(imported);
|
||||
skill.setCategoryId(categoryId);
|
||||
skill.setSourceType("ZIP");
|
||||
skill.setPackageHash(SkillHashes.sha256Hex(imported.getSkillContent().getBytes(StandardCharsets.UTF_8)));
|
||||
savedSkills.add(skillService.saveDraft(skill));
|
||||
}
|
||||
}
|
||||
return savedSkills;
|
||||
}
|
||||
|
||||
private SkillImportPreviewItem toPreviewItem(com.easyagents.skill.model.Skill skill) {
|
||||
SkillImportPreviewItem item = new SkillImportPreviewItem();
|
||||
item.setPackageId(skill.getId());
|
||||
item.setName(skill.getName());
|
||||
item.setDescription(skill.getDescription());
|
||||
item.setReferenceCount(skill.getReferences().size());
|
||||
item.setScriptCount(skill.getScripts().size());
|
||||
item.setAssetCount(skill.getAssets().size());
|
||||
item.setConflict(findByName(skill.getName()) != null);
|
||||
return item;
|
||||
}
|
||||
|
||||
private Skill findByName(String name) {
|
||||
if (name == null || name.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
List<Skill> skills = skillService.list(QueryWrapper.create().eq(Skill::getName, name));
|
||||
return skills.isEmpty() ? null : skills.get(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.skill.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.skill.entity.SkillAssetContent;
|
||||
|
||||
/**
|
||||
* Skill asset 内容索引 Mapper。
|
||||
*/
|
||||
public interface SkillAssetContentMapper extends BaseMapper<SkillAssetContent> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.skill.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.skill.entity.SkillAsset;
|
||||
|
||||
/**
|
||||
* Skill asset Mapper。
|
||||
*/
|
||||
public interface SkillAssetMapper extends BaseMapper<SkillAsset> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.skill.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.skill.entity.SkillCategory;
|
||||
|
||||
/**
|
||||
* Skill 分类 Mapper。
|
||||
*/
|
||||
public interface SkillCategoryMapper extends BaseMapper<SkillCategory> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.skill.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
|
||||
/**
|
||||
* Skill Mapper。
|
||||
*/
|
||||
public interface SkillMapper extends BaseMapper<Skill> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.skill.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.skill.entity.SkillReference;
|
||||
|
||||
/**
|
||||
* Skill reference Mapper。
|
||||
*/
|
||||
public interface SkillReferenceMapper extends BaseMapper<SkillReference> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.skill.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.skill.entity.SkillScript;
|
||||
|
||||
/**
|
||||
* Skill script Mapper。
|
||||
*/
|
||||
public interface SkillScriptMapper extends BaseMapper<SkillScript> {
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package tech.easyflow.skill.publish;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.publish.AbstractAiResourceLifecycleHandler;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||
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.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Skill 审批资源处理器。
|
||||
*/
|
||||
@Component
|
||||
public class SkillApprovalSubjectHandler extends AbstractAiResourceLifecycleHandler<Skill> {
|
||||
|
||||
private final SkillService skillService;
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
|
||||
/**
|
||||
* 创建 Skill 审批资源处理器。
|
||||
*
|
||||
* @param approvalInstanceService 审批实例服务
|
||||
* @param objectMapper JSON 映射器
|
||||
* @param skillService Skill 服务
|
||||
* @param resourceAccessService 资源访问服务
|
||||
*/
|
||||
public SkillApprovalSubjectHandler(ApprovalInstanceService approvalInstanceService,
|
||||
ObjectMapper objectMapper,
|
||||
SkillService skillService,
|
||||
ResourceAccessService resourceAccessService) {
|
||||
super(approvalInstanceService, objectMapper);
|
||||
this.skillService = skillService;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String resourceType() {
|
||||
return ApprovalResourceType.SKILL.getCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void assertPublishedAccess(Object identifier, String denyMessage) {
|
||||
Skill skill = skillService.getById(String.valueOf(identifier));
|
||||
if (skill == null || !PublishStatus.from(skill.getPublishStatus()).isExternallyVisible()
|
||||
|| skill.getPublishedSnapshotJson() == null || skill.getPublishedSnapshotJson().isEmpty()) {
|
||||
throw new BusinessException(denyMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skill requireResource(BigInteger resourceId) {
|
||||
Skill skill = skillService.getById(resourceId);
|
||||
if (skill == null) {
|
||||
throw new BusinessException("Skill 不存在");
|
||||
}
|
||||
return skill;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void assertManagePermission(Skill resource) {
|
||||
resourceAccessService.assertAccess(CategoryResourceType.SKILL, resource, ResourceAction.MANAGE, "无权限管理该 Skill");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BigInteger getCategoryId(Skill resource) {
|
||||
return resource.getCategoryId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BigInteger getDeptId(Skill resource) {
|
||||
return resource.getDeptId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTitle(Skill resource) {
|
||||
return resource.getDisplayName() == null || resource.getDisplayName().isBlank()
|
||||
? resource.getName()
|
||||
: resource.getDisplayName();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PublishStatus getCurrentStatus(Skill resource) {
|
||||
return PublishStatus.from(resource.getPublishStatus());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> getPublishedSnapshot(Skill resource) {
|
||||
return resource.getPublishedSnapshotJson();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildResourceSnapshot(Skill resource) {
|
||||
return skillService.buildPublishSnapshot(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) {
|
||||
Skill skill = new Skill();
|
||||
skill.setId(resourceId);
|
||||
skill.setPublishStatus(publishStatus.getCode());
|
||||
skill.setCurrentApprovalInstanceId(currentApprovalInstanceId);
|
||||
skillService.updateById(skill);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void publishResource(BigInteger resourceId, Map<String, Object> resourceSnapshot, BigInteger operatorId) {
|
||||
Skill skill = new Skill();
|
||||
skill.setId(resourceId);
|
||||
skill.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||
skill.setPublishedSnapshotJson(resourceSnapshot);
|
||||
skill.setPublishedAt(new Date());
|
||||
skill.setPublishedBy(operatorId);
|
||||
skill.setCurrentApprovalInstanceId(null);
|
||||
skillService.updateById(skill);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void markResourceOffline(BigInteger resourceId) {
|
||||
Skill skill = new Skill();
|
||||
skill.setId(resourceId);
|
||||
skill.setPublishStatus(PublishStatus.OFFLINE.getCode());
|
||||
skill.setCurrentApprovalInstanceId(null);
|
||||
skillService.updateById(skill);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void removeResource(BigInteger resourceId) {
|
||||
skillService.removeAggregate(resourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String resourceLabel() {
|
||||
return "Skill";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package tech.easyflow.skill.publish;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.ai.publish.AiResourceLifecycleService;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* Skill 发布生命周期应用服务。
|
||||
*/
|
||||
@Service
|
||||
public class SkillPublishAppService {
|
||||
|
||||
private final AiResourceLifecycleService aiResourceLifecycleService;
|
||||
|
||||
/**
|
||||
* 创建 Skill 发布应用服务。
|
||||
*
|
||||
* @param aiResourceLifecycleService AI 资源生命周期服务
|
||||
*/
|
||||
public SkillPublishAppService(AiResourceLifecycleService aiResourceLifecycleService) {
|
||||
this.aiResourceLifecycleService = aiResourceLifecycleService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交 Skill 发布审批。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @return 审批动作结果
|
||||
*/
|
||||
public ApprovalActionResult submitPublishApproval(BigInteger id) {
|
||||
return submit(id, ApprovalActionType.PUBLISH);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交 Skill 下线审批。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @return 审批动作结果
|
||||
*/
|
||||
public ApprovalActionResult submitOfflineApproval(BigInteger id) {
|
||||
return submit(id, ApprovalActionType.OFFLINE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交 Skill 删除审批。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @return 审批动作结果
|
||||
*/
|
||||
public ApprovalActionResult submitDeleteApproval(BigInteger id) {
|
||||
return submit(id, ApprovalActionType.DELETE);
|
||||
}
|
||||
|
||||
private ApprovalActionResult submit(BigInteger id, ApprovalActionType actionType) {
|
||||
if (id == null) {
|
||||
throw new BusinessException("Skill 审批时资源ID不能为空");
|
||||
}
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.SKILL.getCode(),
|
||||
id,
|
||||
actionType.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package tech.easyflow.skill.repository;
|
||||
|
||||
import com.easyagents.skill.model.SkillDescriptor;
|
||||
import com.easyagents.skill.repository.SkillRepository;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.service.SkillService;
|
||||
import tech.easyflow.skill.support.SkillModelConverter;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 基于数据库的 easy-agents-skill 仓储适配器。
|
||||
*/
|
||||
@Repository
|
||||
public class DBSkillRepository implements SkillRepository {
|
||||
|
||||
private final SkillService skillService;
|
||||
|
||||
/**
|
||||
* 创建数据库 Skill 仓储。
|
||||
*
|
||||
* @param skillService Skill 服务
|
||||
*/
|
||||
public DBSkillRepository(SkillService skillService) {
|
||||
this.skillService = skillService;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void save(com.easyagents.skill.model.Skill skill) {
|
||||
Skill entity = SkillModelConverter.fromAgentSkill(skill);
|
||||
BigInteger parsedId = tryParseId(skill.getId());
|
||||
if (parsedId != null && skillService.getById(parsedId) != null) {
|
||||
entity.setId(parsedId);
|
||||
skillService.updateDraft(entity);
|
||||
return;
|
||||
}
|
||||
skillService.saveDraft(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Optional<com.easyagents.skill.model.Skill> get(String skillId) {
|
||||
BigInteger id = parseId(skillId);
|
||||
Skill skill = skillService.getDetail(id);
|
||||
return Optional.of(SkillModelConverter.toAgentSkill(skill));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Optional<SkillDescriptor> getDescriptor(String skillId) {
|
||||
BigInteger id = parseId(skillId);
|
||||
Skill skill = skillService.getById(id);
|
||||
if (skill == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(new SkillDescriptor(String.valueOf(skill.getId()), skill.getName(), skill.getDescription(),
|
||||
new com.easyagents.skill.model.SkillMetadata(skill.getMetadataJson())));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public List<SkillDescriptor> listDescriptors() {
|
||||
return skillService.list().stream()
|
||||
.map(skill -> new SkillDescriptor(String.valueOf(skill.getId()), skill.getName(), skill.getDescription(),
|
||||
new com.easyagents.skill.model.SkillMetadata(skill.getMetadataJson())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void delete(String skillId) {
|
||||
skillService.removeAggregate(parseId(skillId));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean exists(String skillId) {
|
||||
BigInteger id = parseId(skillId);
|
||||
return skillService.count(QueryWrapper.create().eq(Skill::getId, id)) > 0;
|
||||
}
|
||||
|
||||
private BigInteger parseId(String skillId) {
|
||||
if (skillId == null || skillId.isBlank()) {
|
||||
throw new BusinessException("Skill ID 不能为空");
|
||||
}
|
||||
return new BigInteger(skillId);
|
||||
}
|
||||
|
||||
private BigInteger tryParseId(String skillId) {
|
||||
if (skillId == null || skillId.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new BigInteger(skillId);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package tech.easyflow.skill.service;
|
||||
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Skill 审批展示态派生服务。
|
||||
*/
|
||||
public interface SkillApprovalStateService {
|
||||
|
||||
/**
|
||||
* 填充单个 Skill 审批展示态。
|
||||
*
|
||||
* @param skill Skill
|
||||
*/
|
||||
void fillSkillApprovalState(Skill skill);
|
||||
|
||||
/**
|
||||
* 批量填充 Skill 审批展示态。
|
||||
*
|
||||
* @param skills Skill 集合
|
||||
*/
|
||||
void fillSkillApprovalState(Collection<Skill> skills);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.skill.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.skill.entity.SkillAssetContent;
|
||||
|
||||
/**
|
||||
* Skill asset 内容索引服务。
|
||||
*/
|
||||
public interface SkillAssetContentService extends IService<SkillAssetContent> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.skill.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.skill.entity.SkillAsset;
|
||||
|
||||
/**
|
||||
* Skill asset 服务。
|
||||
*/
|
||||
public interface SkillAssetService extends IService<SkillAsset> {
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package tech.easyflow.skill.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.skill.entity.SkillCategory;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* Skill 分类服务。
|
||||
*/
|
||||
public interface SkillCategoryService extends IService<SkillCategory> {
|
||||
|
||||
/**
|
||||
* 校验目标分类可作为 Skill 分类。
|
||||
*
|
||||
* @param categoryId 分类 ID,可为空
|
||||
*/
|
||||
void validateUsableCategory(BigInteger categoryId);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.skill.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.skill.entity.SkillReference;
|
||||
|
||||
/**
|
||||
* Skill reference 服务。
|
||||
*/
|
||||
public interface SkillReferenceService extends IService<SkillReference> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.skill.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.skill.entity.SkillScript;
|
||||
|
||||
/**
|
||||
* Skill script 服务。
|
||||
*/
|
||||
public interface SkillScriptService extends IService<SkillScript> {
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package tech.easyflow.skill.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Skill 业务服务。
|
||||
*/
|
||||
public interface SkillService extends IService<Skill> {
|
||||
|
||||
/**
|
||||
* 获取 Skill 详情。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @return Skill 详情
|
||||
*/
|
||||
Skill getDetail(BigInteger id);
|
||||
|
||||
/**
|
||||
* 保存 Skill 草稿。
|
||||
*
|
||||
* @param skill Skill 草稿
|
||||
* @return 保存后的 Skill
|
||||
*/
|
||||
Skill saveDraft(Skill skill);
|
||||
|
||||
/**
|
||||
* 更新 Skill 草稿。
|
||||
*
|
||||
* @param skill Skill 草稿
|
||||
* @return 更新后的 Skill
|
||||
*/
|
||||
Skill updateDraft(Skill skill);
|
||||
|
||||
/**
|
||||
* 构建发布快照。
|
||||
*
|
||||
* @param skill Skill 草稿
|
||||
* @return 发布快照
|
||||
*/
|
||||
Map<String, Object> buildPublishSnapshot(Skill skill);
|
||||
|
||||
/**
|
||||
* 从发布快照还原 Skill。
|
||||
*
|
||||
* @param snapshot 发布快照
|
||||
* @return Skill
|
||||
*/
|
||||
Skill fromSnapshot(Map<String, Object> snapshot);
|
||||
|
||||
/**
|
||||
* 删除 Skill 聚合。
|
||||
*
|
||||
* @param id Skill ID
|
||||
*/
|
||||
void removeAggregate(BigInteger id);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package tech.easyflow.skill.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.enums.ApprovalInstanceStatus;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.approval.mapper.ApprovalInstanceMapper;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.service.SkillApprovalStateService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Skill 审批展示态派生服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class SkillApprovalStateServiceImpl implements SkillApprovalStateService {
|
||||
|
||||
private final ApprovalInstanceMapper approvalInstanceMapper;
|
||||
|
||||
/**
|
||||
* 创建 Skill 审批展示态派生服务。
|
||||
*
|
||||
* @param approvalInstanceMapper 审批实例 Mapper
|
||||
*/
|
||||
public SkillApprovalStateServiceImpl(ApprovalInstanceMapper approvalInstanceMapper) {
|
||||
this.approvalInstanceMapper = approvalInstanceMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void fillSkillApprovalState(Skill skill) {
|
||||
fillSkillApprovalState(skill == null ? List.of() : List.of(skill));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void fillSkillApprovalState(Collection<Skill> skills) {
|
||||
if (CollectionUtils.isEmpty(skills)) {
|
||||
return;
|
||||
}
|
||||
List<Skill> validSkills = skills.stream().filter(Objects::nonNull).toList();
|
||||
if (validSkills.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<BigInteger, ApprovalInstance> instanceMap = loadInstanceMap(validSkills);
|
||||
for (Skill skill : validSkills) {
|
||||
fillOne(skill, instanceMap.get(skill.getCurrentApprovalInstanceId()));
|
||||
}
|
||||
}
|
||||
|
||||
private void fillOne(Skill skill, ApprovalInstance instance) {
|
||||
PublishStatus currentStatus = PublishStatus.from(skill.getPublishStatus());
|
||||
if (!isValidCurrentInstance(instance)) {
|
||||
skill.setApprovalPending(false);
|
||||
skill.setCurrentApprovalActionType(null);
|
||||
skill.setDisplayPublishStatus(currentStatus.getCode());
|
||||
return;
|
||||
}
|
||||
ApprovalInstanceStatus instanceStatus = ApprovalInstanceStatus.from(instance.getStatus());
|
||||
if (instanceStatus.isFinished()) {
|
||||
skill.setApprovalPending(false);
|
||||
skill.setCurrentApprovalActionType(null);
|
||||
skill.setDisplayPublishStatus(currentStatus.getCode());
|
||||
return;
|
||||
}
|
||||
ApprovalActionType actionType = ApprovalActionType.from(instance.getActionType());
|
||||
skill.setApprovalPending(true);
|
||||
skill.setCurrentApprovalActionType(actionType.getCode());
|
||||
skill.setDisplayPublishStatus(resolveDisplayStatusWithActiveInstance(currentStatus, actionType).getCode());
|
||||
}
|
||||
|
||||
private Map<BigInteger, ApprovalInstance> loadInstanceMap(Collection<Skill> skills) {
|
||||
Set<BigInteger> instanceIds = skills.stream()
|
||||
.map(Skill::getCurrentApprovalInstanceId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
if (instanceIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<ApprovalInstance> instances = approvalInstanceMapper.selectListByQuery(
|
||||
QueryWrapper.create().in(ApprovalInstance::getId, instanceIds)
|
||||
);
|
||||
return instances.stream().collect(Collectors.toMap(ApprovalInstance::getId, Function.identity()));
|
||||
}
|
||||
|
||||
private boolean isValidCurrentInstance(ApprovalInstance instance) {
|
||||
return instance != null && ApprovalResourceType.SKILL.getCode().equals(instance.getResourceType());
|
||||
}
|
||||
|
||||
private PublishStatus resolveDisplayStatusWithActiveInstance(PublishStatus currentStatus,
|
||||
ApprovalActionType actionType) {
|
||||
if (currentStatus == PublishStatus.PUBLISH_PENDING
|
||||
|| currentStatus == PublishStatus.OFFLINE_PENDING
|
||||
|| currentStatus == PublishStatus.DELETE_PENDING) {
|
||||
return currentStatus;
|
||||
}
|
||||
return switch (actionType) {
|
||||
case PUBLISH -> PublishStatus.PUBLISH_PENDING;
|
||||
case OFFLINE -> PublishStatus.OFFLINE_PENDING;
|
||||
case DELETE -> PublishStatus.DELETE_PENDING;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package tech.easyflow.skill.service.impl;
|
||||
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.skill.entity.SkillAssetContent;
|
||||
import tech.easyflow.skill.mapper.SkillAssetContentMapper;
|
||||
import tech.easyflow.skill.service.SkillAssetContentService;
|
||||
|
||||
/**
|
||||
* Skill asset 内容索引服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class SkillAssetContentServiceImpl extends ServiceImpl<SkillAssetContentMapper, SkillAssetContent> implements SkillAssetContentService {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package tech.easyflow.skill.service.impl;
|
||||
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.skill.entity.SkillAsset;
|
||||
import tech.easyflow.skill.mapper.SkillAssetMapper;
|
||||
import tech.easyflow.skill.service.SkillAssetService;
|
||||
|
||||
/**
|
||||
* Skill asset 服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class SkillAssetServiceImpl extends ServiceImpl<SkillAssetMapper, SkillAsset> implements SkillAssetService {
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package tech.easyflow.skill.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
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.SkillCategory;
|
||||
import tech.easyflow.skill.mapper.SkillCategoryMapper;
|
||||
import tech.easyflow.skill.service.SkillCategoryService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Skill 分类服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class SkillCategoryServiceImpl extends ServiceImpl<SkillCategoryMapper, SkillCategory> implements SkillCategoryService {
|
||||
|
||||
private static final int MAX_LEVEL = 3;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void validateUsableCategory(BigInteger categoryId) {
|
||||
if (categoryId == null) {
|
||||
return;
|
||||
}
|
||||
SkillCategory category = getById(categoryId);
|
||||
if (category == null) {
|
||||
throw new BusinessException("Skill 分类不存在");
|
||||
}
|
||||
if (category.getLevelNo() != null && category.getLevelNo() > MAX_LEVEL) {
|
||||
throw new BusinessException("Skill 分类最多支持三级");
|
||||
}
|
||||
if (category.getStatus() != null && category.getStatus() != 1) {
|
||||
throw new BusinessException("Skill 分类不可用");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean save(SkillCategory entity) {
|
||||
applyCategoryFields(entity);
|
||||
return super.save(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean updateById(SkillCategory entity) {
|
||||
applyCategoryFields(entity);
|
||||
return super.updateById(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断分类下是否存在子分类。
|
||||
*
|
||||
* @param categoryId 分类 ID
|
||||
* @return 存在子分类时返回 true
|
||||
*/
|
||||
public boolean hasChildren(BigInteger categoryId) {
|
||||
if (categoryId == null) {
|
||||
return false;
|
||||
}
|
||||
return count(QueryWrapper.create().eq(SkillCategory::getParentId, categoryId)) > 0;
|
||||
}
|
||||
|
||||
private void applyCategoryFields(SkillCategory category) {
|
||||
if (category == null) {
|
||||
throw new BusinessException("Skill 分类不能为空");
|
||||
}
|
||||
if (category.getCategoryName() == null || category.getCategoryName().isBlank()) {
|
||||
throw new BusinessException("Skill 分类名称不能为空");
|
||||
}
|
||||
SkillCategory parent = null;
|
||||
if (category.getParentId() != null) {
|
||||
parent = getById(category.getParentId());
|
||||
if (parent == null) {
|
||||
throw new BusinessException("父级 Skill 分类不存在");
|
||||
}
|
||||
if (category.getId() != null && category.getId().equals(category.getParentId())) {
|
||||
throw new BusinessException("父级分类不能是自身");
|
||||
}
|
||||
}
|
||||
int level = parent == null ? 1 : (parent.getLevelNo() == null ? 1 : parent.getLevelNo()) + 1;
|
||||
if (level > MAX_LEVEL) {
|
||||
throw new BusinessException("Skill 分类最多支持三级");
|
||||
}
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
Date now = new Date();
|
||||
category.setLevelNo(level);
|
||||
category.setAncestors(parent == null ? "" : appendAncestor(parent));
|
||||
category.setStatus(category.getStatus() == null ? 1 : category.getStatus());
|
||||
category.setSortNo(category.getSortNo() == null ? 0 : category.getSortNo());
|
||||
if (category.getId() == null) {
|
||||
category.setTenantId(account.getTenantId());
|
||||
category.setCreated(now);
|
||||
category.setCreatedBy(account.getId());
|
||||
}
|
||||
category.setModified(now);
|
||||
category.setModifiedBy(account.getId());
|
||||
}
|
||||
|
||||
private String appendAncestor(SkillCategory parent) {
|
||||
String ancestors = parent.getAncestors() == null || parent.getAncestors().isBlank()
|
||||
? String.valueOf(parent.getId())
|
||||
: parent.getAncestors() + "," + parent.getId();
|
||||
if (ancestors.length() > 512) {
|
||||
throw new BusinessException("Skill 分类层级路径过长");
|
||||
}
|
||||
return ancestors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package tech.easyflow.skill.service.impl;
|
||||
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.skill.entity.SkillReference;
|
||||
import tech.easyflow.skill.mapper.SkillReferenceMapper;
|
||||
import tech.easyflow.skill.service.SkillReferenceService;
|
||||
|
||||
/**
|
||||
* Skill reference 服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class SkillReferenceServiceImpl extends ServiceImpl<SkillReferenceMapper, SkillReference> implements SkillReferenceService {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package tech.easyflow.skill.service.impl;
|
||||
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.skill.entity.SkillScript;
|
||||
import tech.easyflow.skill.mapper.SkillScriptMapper;
|
||||
import tech.easyflow.skill.service.SkillScriptService;
|
||||
|
||||
/**
|
||||
* Skill script 服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class SkillScriptServiceImpl extends ServiceImpl<SkillScriptMapper, SkillScript> implements SkillScriptService {
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
package tech.easyflow.skill.service.impl;
|
||||
|
||||
import com.easyagents.skill.exception.SkillException;
|
||||
import com.easyagents.skill.factory.SkillFactory;
|
||||
import com.easyagents.skill.validation.defaults.DefaultSkillValidator;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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.ai.enums.PublishStatus;
|
||||
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.entity.SkillAsset;
|
||||
import tech.easyflow.skill.entity.SkillReference;
|
||||
import tech.easyflow.skill.entity.SkillScript;
|
||||
import tech.easyflow.skill.mapper.SkillMapper;
|
||||
import tech.easyflow.skill.service.*;
|
||||
import tech.easyflow.skill.support.SkillModelConverter;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.enums.VisibilityScope;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Skill 业务服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class SkillServiceImpl extends ServiceImpl<SkillMapper, Skill> implements SkillService {
|
||||
|
||||
private final DefaultSkillValidator skillValidator = new DefaultSkillValidator();
|
||||
|
||||
@Resource
|
||||
private SkillCategoryService skillCategoryService;
|
||||
@Resource
|
||||
private SkillReferenceService skillReferenceService;
|
||||
@Resource
|
||||
private SkillScriptService skillScriptService;
|
||||
@Resource
|
||||
private SkillAssetService skillAssetService;
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Skill getDetail(BigInteger id) {
|
||||
Skill skill = requireSkill(id);
|
||||
resourceAccessService.assertAccess(CategoryResourceType.SKILL, skill, ResourceAction.READ, "无权限查看该 Skill");
|
||||
fillResources(skill);
|
||||
return skill;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Skill saveDraft(Skill skill) {
|
||||
validateDraft(skill);
|
||||
applyDraftDefaults(skill);
|
||||
save(skill);
|
||||
replaceResources(skill);
|
||||
return getDetail(skill.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Skill updateDraft(Skill skill) {
|
||||
if (skill == null || skill.getId() == null) {
|
||||
throw new BusinessException("Skill ID 不能为空");
|
||||
}
|
||||
Skill existing = requireSkill(skill.getId());
|
||||
resourceAccessService.assertAccess(CategoryResourceType.SKILL, existing, ResourceAction.MANAGE, "无权限管理该 Skill");
|
||||
validateDraft(skill);
|
||||
applyDraftUpdate(existing, skill);
|
||||
updateById(existing);
|
||||
replaceResources(existing);
|
||||
return getDetail(existing.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> buildPublishSnapshot(Skill skill) {
|
||||
Skill detail = getDetail(skill.getId());
|
||||
com.easyagents.skill.model.Skill agentSkill = SkillModelConverter.toAgentSkill(detail);
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||||
snapshot.put("id", detail.getId());
|
||||
snapshot.put("tenantId", detail.getTenantId());
|
||||
snapshot.put("deptId", detail.getDeptId());
|
||||
snapshot.put("createdBy", detail.getCreatedBy());
|
||||
snapshot.put("categoryId", detail.getCategoryId());
|
||||
snapshot.put("name", detail.getName());
|
||||
snapshot.put("displayName", detail.getDisplayName());
|
||||
snapshot.put("description", detail.getDescription());
|
||||
snapshot.put("metadataJson", detail.getMetadataJson());
|
||||
snapshot.put("skillContent", detail.getSkillContent());
|
||||
snapshot.put("enabled", detail.getEnabled());
|
||||
snapshot.put("visibilityScope", detail.getVisibilityScope());
|
||||
snapshot.put("sourceType", detail.getSourceType());
|
||||
snapshot.put("packageHash", detail.getPackageHash());
|
||||
snapshot.put("references", agentSkill.getReferences());
|
||||
snapshot.put("scripts", agentSkill.getScripts());
|
||||
snapshot.put("assets", agentSkill.getAssets());
|
||||
snapshot.put("snapshotAt", new Date());
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Skill fromSnapshot(Map<String, Object> snapshot) {
|
||||
if (snapshot == null || snapshot.isEmpty()) {
|
||||
throw new BusinessException("Skill 发布快照为空");
|
||||
}
|
||||
Skill skill = objectMapper.convertValue(snapshot, Skill.class);
|
||||
skill.setId(toBigInteger(snapshot.get("id")));
|
||||
skill.setTenantId(toBigInteger(snapshot.get("tenantId")));
|
||||
skill.setDeptId(toBigInteger(snapshot.get("deptId")));
|
||||
skill.setCreatedBy(toBigInteger(snapshot.get("createdBy")));
|
||||
skill.setCategoryId(toBigInteger(snapshot.get("categoryId")));
|
||||
skill.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||
skill.setPublishedSnapshotJson(snapshot);
|
||||
return skill;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void removeAggregate(BigInteger id) {
|
||||
if (id == null) {
|
||||
return;
|
||||
}
|
||||
removeResources(id);
|
||||
removeById(id);
|
||||
}
|
||||
|
||||
private Skill requireSkill(BigInteger id) {
|
||||
Skill skill = getById(id);
|
||||
if (skill == null) {
|
||||
throw new BusinessException("Skill 不存在");
|
||||
}
|
||||
return skill;
|
||||
}
|
||||
|
||||
private void validateDraft(Skill skill) {
|
||||
if (skill == null) {
|
||||
throw new BusinessException("Skill 不能为空");
|
||||
}
|
||||
normalizeFromSkillContent(skill);
|
||||
if (skill.getName() == null || skill.getName().isBlank()) {
|
||||
throw new BusinessException("Skill 名称不能为空");
|
||||
}
|
||||
if (skill.getDescription() == null || skill.getDescription().isBlank()) {
|
||||
throw new BusinessException("Skill 描述不能为空");
|
||||
}
|
||||
if (skill.getSkillContent() == null || skill.getSkillContent().isBlank()) {
|
||||
throw new BusinessException("SKILL.md 内容不能为空");
|
||||
}
|
||||
skillCategoryService.validateUsableCategory(skill.getCategoryId());
|
||||
validateTargetCategoryVisible(skill.getCategoryId());
|
||||
skill.setVisibilityScope(VisibilityScope.fromOrDefault(skill.getVisibilityScope(), VisibilityScope.PRIVATE).name());
|
||||
validateSkillPackage(skill);
|
||||
}
|
||||
|
||||
private void validateTargetCategoryVisible(BigInteger categoryId) {
|
||||
if (categoryId == null) {
|
||||
return;
|
||||
}
|
||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess(CategoryResourceType.SKILL.getCode());
|
||||
if (access.isRestricted() && !access.getCategoryIds().contains(categoryId)) {
|
||||
throw new BusinessException("无权限使用该 Skill 分类");
|
||||
}
|
||||
}
|
||||
|
||||
private void applyDraftDefaults(Skill skill) {
|
||||
LoginAccount account = requireCurrentLoginAccount();
|
||||
Date now = new Date();
|
||||
skill.setTenantId(account.getTenantId());
|
||||
skill.setDeptId(account.getDeptId());
|
||||
skill.setCreated(now);
|
||||
skill.setCreatedBy(account.getId());
|
||||
skill.setModified(now);
|
||||
skill.setModifiedBy(account.getId());
|
||||
skill.setEnabled(skill.getEnabled() == null || skill.getEnabled());
|
||||
skill.setSourceType(skill.getSourceType() == null ? "MANUAL" : skill.getSourceType());
|
||||
skill.setPublishStatus(PublishStatus.DRAFT.getCode());
|
||||
syncCounts(skill);
|
||||
}
|
||||
|
||||
private void applyDraftUpdate(Skill existing, Skill incoming) {
|
||||
LoginAccount account = requireCurrentLoginAccount();
|
||||
existing.setCategoryId(incoming.getCategoryId());
|
||||
existing.setName(incoming.getName());
|
||||
existing.setDisplayName(incoming.getDisplayName());
|
||||
existing.setDescription(incoming.getDescription());
|
||||
existing.setMetadataJson(incoming.getMetadataJson());
|
||||
existing.setSkillContent(incoming.getSkillContent());
|
||||
existing.setEnabled(incoming.getEnabled() == null || incoming.getEnabled());
|
||||
existing.setVisibilityScope(incoming.getVisibilityScope());
|
||||
existing.setSourceType(incoming.getSourceType());
|
||||
existing.setPackageHash(incoming.getPackageHash());
|
||||
existing.setReferences(incoming.getReferences());
|
||||
existing.setScripts(incoming.getScripts());
|
||||
existing.setAssets(incoming.getAssets());
|
||||
syncCounts(existing);
|
||||
existing.setModified(new Date());
|
||||
existing.setModifiedBy(account.getId());
|
||||
}
|
||||
|
||||
private void syncCounts(Skill skill) {
|
||||
skill.setReferenceCount(skill.getReferences() == null ? 0 : skill.getReferences().size());
|
||||
skill.setScriptCount(skill.getScripts() == null ? 0 : skill.getScripts().size());
|
||||
skill.setAssetCount(skill.getAssets() == null ? 0 : skill.getAssets().size());
|
||||
}
|
||||
|
||||
private void fillResources(Skill skill) {
|
||||
skill.setReferences(skillReferenceService.list(QueryWrapper.create().eq(SkillReference::getSkillId, skill.getId()).orderBy("path asc")));
|
||||
skill.setScripts(skillScriptService.list(QueryWrapper.create().eq(SkillScript::getSkillId, skill.getId()).orderBy("path asc")));
|
||||
skill.setAssets(skillAssetService.list(QueryWrapper.create().eq(SkillAsset::getSkillId, skill.getId()).orderBy("path asc")));
|
||||
}
|
||||
|
||||
private void replaceResources(Skill skill) {
|
||||
removeResources(skill.getId());
|
||||
BigInteger tenantId = skill.getTenantId();
|
||||
BigInteger skillId = skill.getId();
|
||||
if (skill.getReferences() != null) {
|
||||
for (SkillReference reference : skill.getReferences()) {
|
||||
reference.setTenantId(tenantId);
|
||||
reference.setSkillId(skillId);
|
||||
skillReferenceService.save(reference);
|
||||
}
|
||||
}
|
||||
if (skill.getScripts() != null) {
|
||||
for (SkillScript script : skill.getScripts()) {
|
||||
script.setTenantId(tenantId);
|
||||
script.setSkillId(skillId);
|
||||
skillScriptService.save(script);
|
||||
}
|
||||
}
|
||||
if (skill.getAssets() != null) {
|
||||
for (SkillAsset asset : skill.getAssets()) {
|
||||
asset.setTenantId(tenantId);
|
||||
asset.setSkillId(skillId);
|
||||
skillAssetService.save(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeResources(BigInteger skillId) {
|
||||
skillReferenceService.remove(QueryWrapper.create().eq(SkillReference::getSkillId, skillId));
|
||||
skillScriptService.remove(QueryWrapper.create().eq(SkillScript::getSkillId, skillId));
|
||||
skillAssetService.remove(QueryWrapper.create().eq(SkillAsset::getSkillId, skillId));
|
||||
}
|
||||
|
||||
private void normalizeFromSkillContent(Skill skill) {
|
||||
try {
|
||||
com.easyagents.skill.model.Skill parsed = SkillFactory.create(
|
||||
skill.getId() == null ? "draft" : String.valueOf(skill.getId()),
|
||||
skill.getSkillContent(),
|
||||
SkillModelConverter.toAgentReferences(skill.getReferences()),
|
||||
SkillModelConverter.toAgentScripts(skill.getScripts()),
|
||||
SkillModelConverter.toAgentAssets(skill.getAssets())
|
||||
);
|
||||
skill.setName(parsed.getName());
|
||||
if (skill.getDisplayName() == null || skill.getDisplayName().isBlank()) {
|
||||
skill.setDisplayName(parsed.getName());
|
||||
}
|
||||
skill.setDescription(parsed.getDescription());
|
||||
skill.setMetadataJson(parsed.getMetadata().getValues());
|
||||
} catch (SkillException e) {
|
||||
throw new BusinessException("SKILL.md frontmatter 不合法:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void validateSkillPackage(Skill skill) {
|
||||
try {
|
||||
skillValidator.validate(SkillModelConverter.toAgentSkill(skill));
|
||||
} catch (SkillException e) {
|
||||
throw new BusinessException("Skill 包校验失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private LoginAccount requireCurrentLoginAccount() {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
if (account == null || account.getId() == null) {
|
||||
throw new BusinessException("未登录或登录态无效");
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
private BigInteger toBigInteger(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof BigInteger bigInteger) {
|
||||
return bigInteger;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return BigInteger.valueOf(number.longValue());
|
||||
}
|
||||
return new BigInteger(String.valueOf(value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package tech.easyflow.skill.store;
|
||||
|
||||
import com.easyagents.skill.store.SkillContentStore;
|
||||
import com.easyagents.skill.util.SkillHashes;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import tech.easyflow.common.filestorage.FileStorageService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.skill.entity.SkillAssetContent;
|
||||
import tech.easyflow.skill.service.SkillAssetContentService;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 基于数据库索引和现有文件存储的 Skill 内容存储。
|
||||
*/
|
||||
@Component
|
||||
public class DBSkillContentStore implements SkillContentStore {
|
||||
|
||||
private static final String DEFAULT_MEDIA_TYPE = "application/octet-stream";
|
||||
|
||||
private final SkillAssetContentService skillAssetContentService;
|
||||
private final FileStorageService fileStorageService;
|
||||
|
||||
/**
|
||||
* 创建数据库 Skill 内容存储。
|
||||
*
|
||||
* @param skillAssetContentService asset 内容索引服务
|
||||
* @param fileStorageService 文件存储服务
|
||||
*/
|
||||
public DBSkillContentStore(SkillAssetContentService skillAssetContentService,
|
||||
@Qualifier("default") FileStorageService fileStorageService) {
|
||||
this.skillAssetContentService = skillAssetContentService;
|
||||
this.fileStorageService = fileStorageService;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String put(byte[] bytes) {
|
||||
String contentRef = SkillHashes.sha256Ref(bytes);
|
||||
SkillAssetContent existing = skillAssetContentService.getById(contentRef);
|
||||
if (existing != null) {
|
||||
existing.setRefCount((existing.getRefCount() == null ? 0 : existing.getRefCount()) + 1);
|
||||
existing.setModified(new Date());
|
||||
skillAssetContentService.updateById(existing);
|
||||
return contentRef;
|
||||
}
|
||||
String hex = contentRef.substring("sha256:".length());
|
||||
String prePath = "skill-assets/" + hex.substring(0, Math.min(2, hex.length()));
|
||||
MultipartFile file = new ByteArrayMultipartFile(bytes, contentRef + ".bin", DEFAULT_MEDIA_TYPE);
|
||||
String filePath = fileStorageService.save(file, prePath);
|
||||
SkillAssetContent content = new SkillAssetContent();
|
||||
content.setContentRef(contentRef);
|
||||
content.setContentHash(hex);
|
||||
content.setFilePath(filePath);
|
||||
content.setMediaType(DEFAULT_MEDIA_TYPE);
|
||||
content.setSize((long) (bytes == null ? 0 : bytes.length));
|
||||
content.setRefCount(1);
|
||||
content.setCreated(new Date());
|
||||
content.setModified(new Date());
|
||||
skillAssetContentService.save(content);
|
||||
return contentRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public InputStream open(String contentRef) {
|
||||
SkillAssetContent content = requireContent(contentRef);
|
||||
try {
|
||||
return fileStorageService.readStream(content.getFilePath());
|
||||
} catch (IOException e) {
|
||||
throw new BusinessException("读取 Skill asset 失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public byte[] readAllBytes(String contentRef) {
|
||||
try (InputStream inputStream = open(contentRef)) {
|
||||
return inputStream.readAllBytes();
|
||||
} catch (IOException e) {
|
||||
throw new BusinessException("读取 Skill asset 失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean exists(String contentRef) {
|
||||
if (contentRef == null || contentRef.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
return skillAssetContentService.count(QueryWrapper.create().eq(SkillAssetContent::getContentRef, contentRef)) > 0;
|
||||
}
|
||||
|
||||
private SkillAssetContent requireContent(String contentRef) {
|
||||
SkillAssetContent content = skillAssetContentService.getById(contentRef);
|
||||
if (content == null) {
|
||||
throw new BusinessException("Skill asset 内容不存在");
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
private static class ByteArrayMultipartFile implements MultipartFile {
|
||||
|
||||
private final byte[] bytes;
|
||||
private final String filename;
|
||||
private final String contentType;
|
||||
|
||||
private ByteArrayMultipartFile(byte[] bytes, String filename, String contentType) {
|
||||
this.bytes = bytes == null ? new byte[0] : bytes;
|
||||
this.filename = filename;
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
@Override public String getName() { return "file"; }
|
||||
@Override public String getOriginalFilename() { return filename; }
|
||||
@Override public String getContentType() { return contentType; }
|
||||
@Override public boolean isEmpty() { return bytes.length == 0; }
|
||||
@Override public long getSize() { return bytes.length; }
|
||||
@Override public byte[] getBytes() { return bytes; }
|
||||
@Override public InputStream getInputStream() { return new ByteArrayInputStream(bytes); }
|
||||
@Override public void transferTo(File dest) throws IOException { org.springframework.util.FileCopyUtils.copy(bytes, dest); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package tech.easyflow.skill.support;
|
||||
|
||||
import com.easyagents.skill.factory.SkillFactory;
|
||||
import com.easyagents.skill.model.SkillMetadata;
|
||||
import com.easyagents.skill.model.SkillScriptLanguage;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.entity.SkillAsset;
|
||||
import tech.easyflow.skill.entity.SkillReference;
|
||||
import tech.easyflow.skill.entity.SkillScript;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* EasyFlow Skill 实体与 easy-agents-skill 模型转换器。
|
||||
*/
|
||||
public final class SkillModelConverter {
|
||||
|
||||
private SkillModelConverter() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 easy-agents-skill 聚合。
|
||||
*
|
||||
* @param skill Skill 主实体
|
||||
* @return easy-agents-skill 聚合
|
||||
*/
|
||||
public static com.easyagents.skill.model.Skill toAgentSkill(Skill skill) {
|
||||
return SkillFactory.create(
|
||||
String.valueOf(skill.getId()),
|
||||
skill.getSkillContent(),
|
||||
toAgentReferences(skill.getReferences()),
|
||||
toAgentScripts(skill.getScripts()),
|
||||
toAgentAssets(skill.getAssets())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换导入模型到主实体。
|
||||
*
|
||||
* @param imported 导入 Skill
|
||||
* @return Skill 主实体
|
||||
*/
|
||||
public static Skill fromAgentSkill(com.easyagents.skill.model.Skill imported) {
|
||||
Skill skill = new Skill();
|
||||
skill.setName(imported.getName());
|
||||
skill.setDisplayName(imported.getName());
|
||||
skill.setDescription(imported.getDescription());
|
||||
skill.setMetadataJson(imported.getMetadata().getValues());
|
||||
skill.setSkillContent(imported.getSkillContent());
|
||||
skill.setReferences(imported.getReferences().stream()
|
||||
.map(item -> fromAgentReference(null, null, item))
|
||||
.toList());
|
||||
skill.setScripts(imported.getScripts().stream()
|
||||
.map(item -> fromAgentScript(null, null, item))
|
||||
.toList());
|
||||
skill.setAssets(imported.getAssets().stream()
|
||||
.map(item -> fromAgentAsset(null, null, item))
|
||||
.toList());
|
||||
skill.setReferenceCount(imported.getReferences().size());
|
||||
skill.setScriptCount(imported.getScripts().size());
|
||||
skill.setAssetCount(imported.getAssets().size());
|
||||
return skill;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换 reference 列表。
|
||||
*
|
||||
* @param references reference 实体
|
||||
* @return easy-agents-skill reference
|
||||
*/
|
||||
public static List<com.easyagents.skill.model.SkillReference> toAgentReferences(List<SkillReference> references) {
|
||||
return references == null ? List.of() : references.stream().map(item -> {
|
||||
com.easyagents.skill.model.SkillReference target = new com.easyagents.skill.model.SkillReference();
|
||||
target.setPath(item.getPath());
|
||||
target.setName(item.getName());
|
||||
target.setContent(item.getContent());
|
||||
target.setContentHash(item.getContentHash());
|
||||
target.setSize(item.getSize() == null ? 0L : item.getSize());
|
||||
target.setMetadata(new SkillMetadata(item.getMetadataJson()));
|
||||
return target;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换 script 列表。
|
||||
*
|
||||
* @param scripts script 实体
|
||||
* @return easy-agents-skill script
|
||||
*/
|
||||
public static List<com.easyagents.skill.model.SkillScript> toAgentScripts(List<SkillScript> scripts) {
|
||||
return scripts == null ? List.of() : scripts.stream().map(item -> {
|
||||
com.easyagents.skill.model.SkillScript target = new com.easyagents.skill.model.SkillScript();
|
||||
target.setPath(item.getPath());
|
||||
target.setLanguage(parseLanguage(item.getLanguage()));
|
||||
target.setContent(item.getContent());
|
||||
target.setContentHash(item.getContentHash());
|
||||
target.setSize(item.getSize() == null ? 0L : item.getSize());
|
||||
target.setMetadata(new SkillMetadata(item.getMetadataJson()));
|
||||
return target;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换 asset 列表。
|
||||
*
|
||||
* @param assets asset 实体
|
||||
* @return easy-agents-skill asset
|
||||
*/
|
||||
public static List<com.easyagents.skill.model.SkillAsset> toAgentAssets(List<SkillAsset> assets) {
|
||||
return assets == null ? List.of() : assets.stream().map(item -> {
|
||||
com.easyagents.skill.model.SkillAsset target = new com.easyagents.skill.model.SkillAsset();
|
||||
target.setPath(item.getPath());
|
||||
target.setName(item.getName());
|
||||
target.setMediaType(item.getMediaType());
|
||||
target.setContentRef(item.getContentRef());
|
||||
target.setContentHash(item.getContentHash());
|
||||
target.setSize(item.getSize() == null ? 0L : item.getSize());
|
||||
target.setMetadata(new SkillMetadata(item.getMetadataJson()));
|
||||
return target;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换导入 reference。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param tenantId 租户 ID
|
||||
* @param source 导入模型
|
||||
* @return reference 实体
|
||||
*/
|
||||
public static SkillReference fromAgentReference(BigInteger skillId, BigInteger tenantId,
|
||||
com.easyagents.skill.model.SkillReference source) {
|
||||
SkillReference target = new SkillReference();
|
||||
target.setTenantId(tenantId);
|
||||
target.setSkillId(skillId);
|
||||
target.setPath(source.getPath());
|
||||
target.setName(source.getName());
|
||||
target.setContent(source.getContent());
|
||||
target.setContentHash(source.getContentHash());
|
||||
target.setSize(source.getSize());
|
||||
target.setMetadataJson(source.getMetadata().getValues());
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换导入 script。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param tenantId 租户 ID
|
||||
* @param source 导入模型
|
||||
* @return script 实体
|
||||
*/
|
||||
public static SkillScript fromAgentScript(BigInteger skillId, BigInteger tenantId,
|
||||
com.easyagents.skill.model.SkillScript source) {
|
||||
SkillScript target = new SkillScript();
|
||||
target.setTenantId(tenantId);
|
||||
target.setSkillId(skillId);
|
||||
target.setPath(source.getPath());
|
||||
target.setLanguage(source.getLanguage().name());
|
||||
target.setContent(source.getContent());
|
||||
target.setContentHash(source.getContentHash());
|
||||
target.setSize(source.getSize());
|
||||
target.setMetadataJson(source.getMetadata().getValues());
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换导入 asset。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param tenantId 租户 ID
|
||||
* @param source 导入模型
|
||||
* @return asset 实体
|
||||
*/
|
||||
public static SkillAsset fromAgentAsset(BigInteger skillId, BigInteger tenantId,
|
||||
com.easyagents.skill.model.SkillAsset source) {
|
||||
SkillAsset target = new SkillAsset();
|
||||
target.setTenantId(tenantId);
|
||||
target.setSkillId(skillId);
|
||||
target.setPath(source.getPath());
|
||||
target.setName(source.getName());
|
||||
target.setMediaType(source.getMediaType());
|
||||
target.setContentRef(source.getContentRef());
|
||||
target.setContentHash(source.getContentHash());
|
||||
target.setSize(source.getSize());
|
||||
target.setMetadataJson(source.getMetadata().getValues());
|
||||
return target;
|
||||
}
|
||||
|
||||
private static SkillScriptLanguage parseLanguage(String language) {
|
||||
if (language == null || language.isBlank()) {
|
||||
return SkillScriptLanguage.UNKNOWN;
|
||||
}
|
||||
try {
|
||||
return SkillScriptLanguage.valueOf(language);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return SkillScriptLanguage.UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ public enum CategoryResourceType {
|
||||
|
||||
BOT("BOT"),
|
||||
AGENT("AGENT"),
|
||||
SKILL("SKILL"),
|
||||
PLUGIN("PLUGIN"),
|
||||
WORKFLOW("WORKFLOW"),
|
||||
KNOWLEDGE("KNOWLEDGE"),
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<module>easyflow-module-chatlog</module>
|
||||
<module>easyflow-module-ai</module>
|
||||
<module>easyflow-module-agent</module>
|
||||
<module>easyflow-module-skill</module>
|
||||
<module>easyflow-module-job</module>
|
||||
<module>easyflow-module-datacenter</module>
|
||||
</modules>
|
||||
|
||||
Reference in New Issue
Block a user