package com.yunhai.agent.tool; import com.alibaba.fastjson.JSONArray; import com.yunhai.agent.auth.AgentRole; import com.yunhai.agent.auth.RolePolicy; import com.yunhai.agent.config.AgentSettings; import com.yunhai.agent.tool.defs.ActivityTools; import com.yunhai.agent.tool.defs.CampusNoticeTools; import com.yunhai.agent.tool.defs.DormNoticeTools; import com.yunhai.agent.tool.defs.HomeworkTools; import com.yunhai.agent.tool.defs.LeaveTools; import com.yunhai.agent.tool.defs.ProfessionTools; import com.yunhai.agent.tool.defs.ResourceTools; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * 工具注册表:所有可被大模型调用的业务动作都在这里汇总。 * *

新增一个模块的接入方式:写一个 {@code XxxTools#defs()} 返回 {@code List}, * 在下面的 {@link #build()} 里加一行。执行、鉴权、schema 生成全部复用通用实现。 * *

可见性由两个维度共同决定: *

    *
  1. {@code modId} —— 平台菜单授权({@code agent.filter-tools-by-authorized-module=true} 时生效);
  2. *
  3. {@code roles} —— 学生 / 教师 / 管理员角色(「分学生、教师权限」的落点)。
  4. *
*/ @Slf4j @Component public class ToolRegistry { private final AgentSettings settings; /** name → ToolDef(保持插入顺序,便于目录展示) */ private final Map tools = new LinkedHashMap<>(); /** * 大模型侧工具名(点换下划线)→ 内部点分名。 * *

大模型对 {@code function.name} 只接受 {@code ^[a-zA-Z0-9_-]+$},点号非法, * 所以下发时用 {@link ToolDef#wireName()},模型回传的也是这个名字,这里负责还原。 */ private final Map wireAliases = new LinkedHashMap<>(); public ToolRegistry(AgentSettings settings) { this.settings = settings; } @PostConstruct public void init() { build(); log.info("智能体工具注册完成,共 {} 个:{}", tools.size(), String.join(", ", tools.keySet())); // 校验:工具引用的模块必须在 agent.modules 里配了地址,否则运行期必然失败 for (ToolDef d : tools.values()) { if (!settings.getModules().containsKey(d.getModule())) { log.warn("工具 {} 引用了未配置地址的模块「{}」,请检查 agent.modules", d.getName(), d.getModule()); } } } private void build() { register(LeaveTools.defs()); register(CampusNoticeTools.defs()); register(DormNoticeTools.defs()); register(ActivityTools.defs()); register(HomeworkTools.defs()); register(ResourceTools.defs()); register(ProfessionTools.defs()); // 套用「学生 / 教师」角色能力策略(未显式声明角色的工具在此落定) RolePolicy.apply(tools.values()); } private void register(List defs) { for (ToolDef d : defs) { if (tools.containsKey(d.getName())) { throw new IllegalStateException("工具名重复:" + d.getName()); } String wire = d.wireName(); // wire 名撞车会导致模型回传无法还原(点→下划线是单向压缩),注册期就拦下来 if (tools.containsKey(wire)) { throw new IllegalStateException("工具名冲突:" + d.getName() + " 的模型侧名字「" + wire + "」与已有工具同名,请改名"); } if (wire != null && !wire.equals(d.getName())) { String prev = wireAliases.put(wire, d.getName()); if (prev != null) { throw new IllegalStateException("模型侧工具名冲突:" + d.getName() + " 与 " + prev + " 换成模型可用名后都是「" + wire + "」,请改名"); } } // ★ 深链兜底:工具没声明落地页时,取该模块的默认页面(agent.module-landing)。 // 这样"查询失败 / 越权拦截"也能带着一个真实可打开的模块地址回到前端。 if (StringUtils.isBlank(d.getLandingPath())) { d.setLandingPath(settings.getModuleLanding().get(d.getModule())); } tools.put(d.getName(), d); } } /** * 按名字取工具:**同时兼容内部点分名与大模型侧下划线名**。 * *

模型回传的 {@code function.name} 是 {@link ToolDef#wireName()}(点已换成下划线), * 而前端表单回传、字典工具引用用的是内部点分名,两种都要能命中。 */ public ToolDef get(String name) { if (name == null) { return null; } ToolDef d = tools.get(name); if (d != null) { return d; } String real = wireAliases.get(name); return real == null ? null : tools.get(real); } public Collection all() { return tools.values(); } /** * 按「已授权模块 + 角色」过滤后,转成给模型看的 tools schema。 * * @param authorizedModIds 当前用户已授权的 modId 集合;为 null 表示不按 modId 过滤 * @param roles 当前用户角色集合;为 null 表示不按角色过滤(排障用) */ public JSONArray toOpenAiTools(Set authorizedModIds, Set roles) { JSONArray arr = new JSONArray(); for (ToolDef d : tools.values()) { if (!visible(d, authorizedModIds, roles)) { continue; } arr.add(d.toOpenAiTool()); } return arr; } /** 供前端「我能用这个智能体做什么」的目录展示 */ public List visibleTools(Set authorizedModIds, Set roles) { return tools.values().stream() .filter(d -> visible(d, authorizedModIds, roles)) .collect(Collectors.toList()); } private boolean visible(ToolDef d, Set authorizedModIds, Set roles) { // 1) modId 维度:仅在开启「按授权模块过滤」时才过滤(modId 为 null 的工具不参与该维度) if (d.getModuleId() != null && authorizedModIds != null && !authorizedModIds.contains(d.getModuleId())) { return false; } // 2) 角色维度:未声明角色的工具人人可用 return d.allowsAny(roles); } /** 按模块分组统计(目录接口用) */ public Map> groupByModule(Set authorizedModIds, Set roles) { Map> map = new LinkedHashMap<>(); for (ToolDef d : visibleTools(authorizedModIds, roles)) { map.computeIfAbsent(d.getModule(), k -> new ArrayList<>()).add(d); } return map; } }