|
|
@@ -11,6 +11,7 @@ import com.zsjz.ai.module.agent.WorkspaceManagerFactory;
|
|
|
import com.zsjz.ai.module.agent.config.EtlProperties;
|
|
|
import com.zsjz.ai.module.agent.config.SkillRepositoryConfigEntry;
|
|
|
import com.zsjz.ai.module.agent.config.SkillRepositorySupport;
|
|
|
+import com.zsjz.ai.module.agent.dto.SaveExpertDTO;
|
|
|
import com.zsjz.ai.module.agent.entity.AgentEntity;
|
|
|
import com.zsjz.ai.module.agent.entity.AgentModel;
|
|
|
import com.zsjz.ai.module.agent.followup.FollowupMiddleware;
|
|
|
@@ -27,10 +28,12 @@ import com.zsjz.ai.module.agent.rag.RagSchemaService;
|
|
|
import com.zsjz.ai.module.agent.scaffold.WorkspaceScaffolder;
|
|
|
import com.zsjz.ai.module.agent.artifact.ArtifactService;
|
|
|
import com.zsjz.ai.module.agent.tools.*;
|
|
|
+import com.zsjz.ai.module.agent.vo.ExpertVO;
|
|
|
import com.zsjz.ai.module.plat.mapper.CaseInfoMapper;
|
|
|
import com.zsjz.ai.module.plat.mapper.TableFieldMapper;
|
|
|
import com.zsjz.ai.module.plat.mapper.TableInfoMapper;
|
|
|
import io.agentscope.core.model.Model;
|
|
|
+import io.agentscope.core.skill.SkillFilter;
|
|
|
import io.agentscope.core.skill.repository.FileSystemSkillRepository;
|
|
|
import io.agentscope.core.state.AgentStateStore;
|
|
|
import io.agentscope.core.state.InMemoryAgentStateStore;
|
|
|
@@ -144,7 +147,7 @@ public class AgentService extends ServiceImpl<AgentMapper, AgentEntity> {
|
|
|
return agentPool.computeIfAbsent(key, k -> {
|
|
|
AgentEntity entry = getById(agentRowId);
|
|
|
if (entry == null) {
|
|
|
- throw new ServerException(404, "Agent 配置不存在: " + agentRowId);
|
|
|
+ throw new ServerException(404, "智能体不存在或已删除: " + agentRowId);
|
|
|
}
|
|
|
AgentModel agentModelConfig = modelMapper.selectById(modelId);
|
|
|
if (agentModelConfig == null) {
|
|
|
@@ -220,7 +223,18 @@ public class AgentService extends ServiceImpl<AgentMapper, AgentEntity> {
|
|
|
// 默认只装备 person 组,其余组由模型调用 reset_equipped_tools 按需切换,避免每轮塞几十份 schema。
|
|
|
Toolkit toolkit = new Toolkit();
|
|
|
registerInfraTools(toolkit);
|
|
|
- agentToolRegistry.registerBusinessTools(toolkit);
|
|
|
+ // 业务工具组:专家配置了 tools_allow_json → 只注册并装备这些组
|
|
|
+ // (组外工具不注册 = 模型不可见、reset_equipped_tools 也切不过去,构成真实限制);
|
|
|
+ // 未配置 → 全组注册、默认装备 person(原口径不变)
|
|
|
+ List<String> expertGroups = readStringList(entry.getToolsAllowJson());
|
|
|
+ if (expertGroups == null || expertGroups.isEmpty()) {
|
|
|
+ agentToolRegistry.registerBusinessTools(toolkit);
|
|
|
+ } else {
|
|
|
+ // enabled = 专家启用的组(限制集);初始装备仍按默认口径(person,省 token),
|
|
|
+ // 专家没启用 person 时重载会自动回落到「启用的组」
|
|
|
+ agentToolRegistry.registerBusinessTools(toolkit, expertGroups,
|
|
|
+ AgentToolRegistry.DEFAULT_ACTIVE_GROUPS);
|
|
|
+ }
|
|
|
b.toolkit(toolkit);
|
|
|
// 注册 reset_equipped_tools 元工具(内部会调 toolkit.registerMetaTool()),
|
|
|
// 模型据此在对话中自行装备/卸载业务工具组;必须放在 toolkit(...) 之后、build() 之前。
|
|
|
@@ -257,6 +271,12 @@ public class AgentService extends ServiceImpl<AgentMapper, AgentEntity> {
|
|
|
if (Files.isDirectory(userSkills)) {
|
|
|
b.skillRepository(new FileSystemSkillRepository(userSkills));
|
|
|
}
|
|
|
+ // 技能白名单:专家配置了 skills_allow_json → 系统提示目录只列这些技能
|
|
|
+ // (SkillFilter.only 作用于「所有技能仓库合并后」,与用户停用的 overlay 正确叠加)
|
|
|
+ List<String> expertSkills = readStringList(entry.getSkillsAllowJson());
|
|
|
+ if (expertSkills != null && !expertSkills.isEmpty()) {
|
|
|
+ b.skillFilter(SkillFilter.only(expertSkills.toArray(new String[0])));
|
|
|
+ }
|
|
|
|
|
|
b.stateStore(agentStateStore);
|
|
|
b.filesystem(new LocalFilesystemSpec().isolationScope(IsolationScope.USER));
|
|
|
@@ -289,6 +309,172 @@ public class AgentService extends ServiceImpl<AgentMapper, AgentEntity> {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+ // ==================== 专家(智能体)管理 ====================
|
|
|
+
|
|
|
+ /** 技能名合法字符(与 SkillStore 校验口径一致:中英文/数字/._-,1~64) */
|
|
|
+ private static final String SKILL_NAME_RE = "^[A-Za-z0-9一-龥._-]{1,64}$";
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 专家列表:内置(全局可见)+ 当前用户自建,内置排前。
|
|
|
+ *
|
|
|
+ * <p>按用户隔离是<b>后端</b>过滤(内置 OR 属于我),不是前端隐藏。
|
|
|
+ */
|
|
|
+ public List<ExpertVO> listExperts(Long userId) {
|
|
|
+ // agent 是极小的表(专家数量级),全量取回后在 Java 侧过滤:
|
|
|
+ // 隔离规则单测可覆盖(loadAllExperts 是测试缝),语义也更直白
|
|
|
+ return loadAllExperts().stream()
|
|
|
+ .filter(e -> isBuiltin(e) || (userId != null && userId.equals(e.getUserId())))
|
|
|
+ .map(e -> toExpertVO(e, userId))
|
|
|
+ .toList();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 查询全部专家(内置在前、自建按创建顺序);包级可见,单测可 stub */
|
|
|
+ List<AgentEntity> loadAllExperts() {
|
|
|
+ return lambdaQuery()
|
|
|
+ .orderByDesc(AgentEntity::getIsBuiltin)
|
|
|
+ .orderByAsc(AgentEntity::getRowId)
|
|
|
+ .list();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean isBuiltin(AgentEntity e) {
|
|
|
+ return e.getIsBuiltin() != null && e.getIsBuiltin() == 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 新建自建专家(user_id=当前用户,is_builtin=0) */
|
|
|
+ public ExpertVO createExpert(Long userId, SaveExpertDTO dto) {
|
|
|
+ validateExpertConfig(userId, dto);
|
|
|
+ AgentEntity entity = new AgentEntity();
|
|
|
+ entity.setUserId(userId);
|
|
|
+ entity.setIsBuiltin(0);
|
|
|
+ applyExpertConfig(entity, dto);
|
|
|
+ save(entity);
|
|
|
+ return toExpertVO(getById(entity.getRowId()), userId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 编辑专家:内置拒绝;自建必须属于当前用户;保存后驱逐池化实例 */
|
|
|
+ public ExpertVO updateExpert(Long userId, Long rowId, SaveExpertDTO dto) {
|
|
|
+ requireOwnedExpert(userId, rowId, "编辑");
|
|
|
+ validateExpertConfig(userId, dto);
|
|
|
+ AgentEntity entity = getById(rowId);
|
|
|
+ applyExpertConfig(entity, dto);
|
|
|
+ updateById(entity);
|
|
|
+ evictAgentPool(rowId);
|
|
|
+ return toExpertVO(getById(rowId), userId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除专家:内置拒绝;绑定它的会话继续对话时由 {@link #getOrCreateAgent} 返回明确错误。
|
|
|
+ */
|
|
|
+ public void deleteExpert(Long userId, Long rowId) {
|
|
|
+ AgentEntity entity = requireOwnedExpert(userId, rowId, "删除");
|
|
|
+ removeById(rowId);
|
|
|
+ evictAgentPool(rowId);
|
|
|
+ log.info("删除专家: rowId={}, name={}, userId={}", rowId, entity.getName(), userId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 驱逐某智能体的全部池化实例。池 key = w{ws}-a{rowId}-m{model}-u{user},
|
|
|
+ * 配置(提示词/工具组/技能)变更后必须调用,否则旧实例继续生效。
|
|
|
+ */
|
|
|
+ public void evictAgentPool(Long agentRowId) {
|
|
|
+ String marker = "-a" + agentRowId + "-";
|
|
|
+ int removed = 0;
|
|
|
+ for (String key : agentPool.keySet()) {
|
|
|
+ if (key.contains(marker) && agentPool.remove(key) != null) {
|
|
|
+ removed++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (removed > 0) {
|
|
|
+ log.info("驱逐 agent 实例池: agentRowId={}, removed={}", agentRowId, removed);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 内置专家不可编辑/删除;自建必须属于当前用户 */
|
|
|
+ private AgentEntity requireOwnedExpert(Long userId, Long rowId, String action) {
|
|
|
+ AgentEntity entity = getById(rowId);
|
|
|
+ if (entity == null) {
|
|
|
+ throw new ServerException(404, "智能体不存在或已删除");
|
|
|
+ }
|
|
|
+ if (entity.getIsBuiltin() != null && entity.getIsBuiltin() == 1) {
|
|
|
+ throw new ServerException(400, "内置专家不可" + action);
|
|
|
+ }
|
|
|
+ if (userId == null || !userId.equals(entity.getUserId())) {
|
|
|
+ throw new ServerException(400, "无权操作该专家");
|
|
|
+ }
|
|
|
+ return entity;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 入参校验:工具组必须是已注册组;技能名格式合法且在当前用户技能目录中存在 */
|
|
|
+ private void validateExpertConfig(Long userId, SaveExpertDTO dto) {
|
|
|
+ if (dto.getToolGroups() != null) {
|
|
|
+ List<String> known = AgentToolRegistry.registeredGroupNames();
|
|
|
+ for (String group : dto.getToolGroups()) {
|
|
|
+ if (group == null || !known.contains(group)) {
|
|
|
+ throw new ServerException(400, "未知工具组: " + group);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ List<String> skills = dto.getSkillNames();
|
|
|
+ if (skills == null || skills.isEmpty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Path cwd = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
|
|
|
+ Path skillsRoot = workspaceManagerFactory.resolveUserSkillsPath(cwd, userId);
|
|
|
+ boolean rootExists = Files.isDirectory(skillsRoot);
|
|
|
+ for (String name : skills) {
|
|
|
+ if (name == null || !name.matches(SKILL_NAME_RE)) {
|
|
|
+ throw new ServerException(400, "非法技能名: " + name);
|
|
|
+ }
|
|
|
+ // 未安装时拒绝写入白名单(SkillFilter.only 全不命中 = 该专家技能全被藏起来)
|
|
|
+ if (!rootExists || !Files.isDirectory(skillsRoot.resolve(name))) {
|
|
|
+ throw new ServerException(400, "技能不存在或未安装: " + name);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void applyExpertConfig(AgentEntity entity, SaveExpertDTO dto) {
|
|
|
+ entity.setName(dto.getName().trim());
|
|
|
+ entity.setDescription(dto.getDescription());
|
|
|
+ entity.setSysPrompt(dto.getSysPrompt().trim());
|
|
|
+ entity.setToolsAllowJson(writeStringList(dto.getToolGroups()));
|
|
|
+ entity.setSkillsAllowJson(writeStringList(dto.getSkillNames()));
|
|
|
+ }
|
|
|
+
|
|
|
+ private ExpertVO toExpertVO(AgentEntity e, Long userId) {
|
|
|
+ ExpertVO vo = new ExpertVO();
|
|
|
+ vo.setId(String.valueOf(e.getRowId()));
|
|
|
+ vo.setName(e.getName());
|
|
|
+ vo.setDescription(e.getDescription());
|
|
|
+ vo.setSysPrompt(e.getSysPrompt());
|
|
|
+ vo.setBuiltin(isBuiltin(e));
|
|
|
+ vo.setMine(!vo.isBuiltin() && userId != null && userId.equals(e.getUserId()));
|
|
|
+ vo.setToolGroups(readStringList(e.getToolsAllowJson()));
|
|
|
+ vo.setSkillNames(readStringList(e.getSkillsAllowJson()));
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析 JSON 字符串数组(tools_allow_json / skills_allow_json 通用);
|
|
|
+ * null/空/坏 JSON 一律返回 null —— 等同「未配置,走默认口径」。
|
|
|
+ */
|
|
|
+ public static List<String> readStringList(String json) {
|
|
|
+ if (json == null || json.isBlank()) return null;
|
|
|
+ try {
|
|
|
+ return Json.objectMapper().readValue(json, new TypeReference<>() {
|
|
|
+ });
|
|
|
+ } catch (JsonProcessingException ex) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 序列化字符串数组;null/空写 null(让 buildAgent 走默认口径而不是空白名单) */
|
|
|
+ private static String writeStringList(List<String> list) {
|
|
|
+ if (list == null || list.isEmpty()) return null;
|
|
|
+ List<String> distinct = list.stream().filter(v -> v != null && !v.isBlank()).distinct().toList();
|
|
|
+ return distinct.isEmpty() ? null : Json.toStr(distinct);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 在用户配置的 sysPrompt 后追加 RAG 检索指引(Agentic RAG:Agent 自行决定何时检索)
|
|
|
*/
|