feat: 提交校园智能助手后端工程源码(Spring Boot 2.7.18 + 平台 starter + JPA + Nacos)
build / build-push (push) Successful in 40s
build / build-push (push) Successful in 40s
- com.yunhai.agent,端口 8093,context-path /agent,制品 target/agent.jar;与 NoticeService 同构 - CAS 单点登录 + 三层角色权限(学生/教师/管理员/访客)+ 会话与工具调用审计落库 - DeepSeek 大模型编排 + 7 个业务模块工具(请假/公告/宿舍/活动/作业/资源库/教师发展) - 浏览器代执行协议(服务端挂起 → 前端用登录态代调 → toolResult 续跑)+ 调用失败深链兜底 - 登出修复:服务端真正 session.invalidate() + 前端两步登出 + logout-guard - 保留 projman 注入的 .gitea/workflows/build.yml 与 docker/Dockerfile;docker/application-prod.yml、run-app.sh 随工程入库
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
package com.yunhai.agent.tool;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.Data;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 工具参数定义。一份声明,三处复用:
|
||||
*
|
||||
* <ol>
|
||||
* <li>生成给大模型的 JSON Schema({@link #toSchema()})</li>
|
||||
* <li>入参必填校验</li>
|
||||
* <li><b>生成让用户直接填写的表单</b>({@link #toFormField()})—— 信息不全时不再靠模型口头追问,
|
||||
* 而是把字段的中文名、格式示例、可选值直接摆到界面上</li>
|
||||
* </ol>
|
||||
*/
|
||||
@Data
|
||||
public class ToolParam {
|
||||
|
||||
/** 参数名(与后端接口参数名严格一致,大小写敏感) */
|
||||
private String name;
|
||||
|
||||
/** JSON Schema 类型:string / integer / number / boolean / array */
|
||||
private String type = "string";
|
||||
|
||||
/** 给模型看的说明(越具体越好,写明格式与取值范围) */
|
||||
private String description;
|
||||
|
||||
private boolean required;
|
||||
|
||||
/** 枚举取值(非空时会在描述与 schema 里同时体现,帮助模型少犯错) */
|
||||
private List<String> enumValues;
|
||||
|
||||
/** 枚举的中文名,与 enumValues 一一对应(表单下拉展示用;缺省时直接显示值) */
|
||||
private List<String> enumLabels;
|
||||
|
||||
/**
|
||||
* 内置兜底枚举:只在「学校可自定义的字典」上使用,是 {@link #optionsFrom} 取不到实时值时的顶替品。
|
||||
* <p>有了它,下游字典接口不通时下拉框仍然是下拉框,而不是退化成让用户填「请假类型编号」的数字框。
|
||||
* 与 {@link #enumValues} 的区别:enumValues 是权威定义(如审批结论),本字段是「大概是这样,以实时字典为准」。
|
||||
*/
|
||||
private List<String> fallbackValues;
|
||||
|
||||
/** 内置兜底枚举的中文名,与 fallbackValues 一一对应 */
|
||||
private List<String> fallbackLabels;
|
||||
|
||||
/** 默认值(模型没给时兜底) */
|
||||
private String defaultValue;
|
||||
|
||||
/* ==================== 表单渲染元数据 ==================== */
|
||||
|
||||
/** 字段中文标签(表单里显示,如「开始时间」);缺省回退到 name */
|
||||
private String label;
|
||||
|
||||
/** 示例值(表单 placeholder,如 2026-10-11 08:00:00),也会进入给模型的描述 */
|
||||
private String example;
|
||||
|
||||
/** 控件类型:text / textarea / number / date / datetime / select;缺省按参数名与类型自动推断 */
|
||||
private String widget;
|
||||
|
||||
/** 是否在表单里隐藏(如由系统固定填入的参数,用户无需看见) */
|
||||
private boolean hidden;
|
||||
|
||||
/**
|
||||
* 该字段的下拉选项来自哪个只读工具(如 {@code leave.types})。
|
||||
* <p>服务端在生成表单时会先执行这个工具,把返回的字典项填成下拉选项 ——
|
||||
* 用户就不用去猜「病假是几号」了。执行失败则回退为普通输入框,不影响主流程。
|
||||
*/
|
||||
private String optionsFrom;
|
||||
|
||||
public ToolParam() {
|
||||
}
|
||||
|
||||
public ToolParam(String name, String type, String description, boolean required) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.description = description;
|
||||
this.required = required;
|
||||
}
|
||||
|
||||
public static ToolParam required(String name, String type, String description) {
|
||||
return new ToolParam(name, type, description, true);
|
||||
}
|
||||
|
||||
public static ToolParam optional(String name, String type, String description) {
|
||||
return new ToolParam(name, type, description, false);
|
||||
}
|
||||
|
||||
public ToolParam enums(String... values) {
|
||||
this.enumValues = Arrays.asList(values);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 枚举值 + 中文名(一一对应),表单会渲染成带中文选项的下拉框 */
|
||||
public ToolParam options(String[] values, String[] labels) {
|
||||
this.enumValues = Arrays.asList(values);
|
||||
this.enumLabels = Arrays.asList(labels);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 内置兜底选项:配合 {@link #optionsFrom} 使用。
|
||||
* <p>实时字典取到 → 用实时值;取不到 → 用这份兜底,下拉框不至于消失。
|
||||
*/
|
||||
public ToolParam fallbackOptions(String[] values, String[] labels) {
|
||||
this.fallbackValues = Arrays.asList(values);
|
||||
this.fallbackLabels = Arrays.asList(labels);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ToolParam dft(String value) {
|
||||
this.defaultValue = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 字段中文标签 */
|
||||
public ToolParam label(String value) {
|
||||
this.label = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 示例值(帮助用户按格式填写) */
|
||||
public ToolParam eg(String value) {
|
||||
this.example = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 指定控件类型 */
|
||||
public ToolParam as(String value) {
|
||||
this.widget = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ToolParam hide() {
|
||||
this.hidden = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 下拉选项来自某个只读字典工具 */
|
||||
public ToolParam optionsFrom(String tool) {
|
||||
this.optionsFrom = tool;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 表单里显示的名字 */
|
||||
public String labelOf() {
|
||||
return StringUtils.isNotBlank(label) ? label : name;
|
||||
}
|
||||
|
||||
/** 当前生效的枚举值:权威枚举优先,其次内置兜底 */
|
||||
private List<String> effectiveValues() {
|
||||
if (enumValues != null && !enumValues.isEmpty()) return enumValues;
|
||||
return fallbackValues;
|
||||
}
|
||||
|
||||
/** 当前生效的枚举中文名,与 {@link #effectiveValues()} 对应 */
|
||||
private List<String> effectiveLabels() {
|
||||
if (enumValues != null && !enumValues.isEmpty()) return enumLabels;
|
||||
return fallbackLabels;
|
||||
}
|
||||
|
||||
/** 当前选项是否来自内置兜底(即实时字典没取到,用的是「大概是这样」的值) */
|
||||
public boolean isUsingFallbackOptions() {
|
||||
return (enumValues == null || enumValues.isEmpty())
|
||||
&& fallbackValues != null && !fallbackValues.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 推断控件类型:显式指定优先,其次枚举→下拉,然后按类型与参数名语义判断。
|
||||
* <p>参数名里的 date/time 是强信号(startDate / endDate / leaveTime),
|
||||
* 文本类词根(remarks / reason / content)则渲染成多行输入。
|
||||
*/
|
||||
public String resolveWidget() {
|
||||
if (StringUtils.isNotBlank(widget)) return widget;
|
||||
List<String> vals = effectiveValues();
|
||||
if (vals != null && !vals.isEmpty()) return "select";
|
||||
|
||||
String t = StringUtils.trimToEmpty(type).toLowerCase();
|
||||
if ("integer".equals(t) || "number".equals(t)) return "number";
|
||||
if ("boolean".equals(t)) return "select";
|
||||
|
||||
String n = StringUtils.trimToEmpty(name).toLowerCase();
|
||||
if (n.contains("date") || n.contains("time")) return "datetime";
|
||||
if (n.contains("remark") || n.contains("reason") || n.contains("desc") || n.contains("memo")
|
||||
|| n.contains("content") || n.contains("address") || n.contains("text")) {
|
||||
return "textarea";
|
||||
}
|
||||
return "text";
|
||||
}
|
||||
|
||||
/** 枚举 → 前端下拉项 [{value,label}](权威枚举优先,其次内置兜底) */
|
||||
public List<Map<String, String>> optionList() {
|
||||
List<Map<String, String>> list = new ArrayList<>();
|
||||
List<String> vals = effectiveValues();
|
||||
List<String> labs = effectiveLabels();
|
||||
if (vals == null) return list;
|
||||
for (int i = 0; i < vals.size(); i++) {
|
||||
Map<String, String> o = new LinkedHashMap<>();
|
||||
o.put("value", vals.get(i));
|
||||
String lab = (labs != null && i < labs.size()) ? labs.get(i) : null;
|
||||
o.put("label", StringUtils.isNotBlank(lab) ? lab : vals.get(i));
|
||||
list.add(o);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 转成前端渲染用的表单字段 */
|
||||
public FormField toFormField() {
|
||||
FormField f = new FormField();
|
||||
f.setName(name);
|
||||
f.setLabel(labelOf());
|
||||
f.setWidget(resolveWidget());
|
||||
f.setType(type);
|
||||
f.setRequired(required);
|
||||
f.setDescription(description);
|
||||
f.setExample(example);
|
||||
f.setDefaultValue(defaultValue);
|
||||
f.setOptions(optionList());
|
||||
// 把「动态选项来源」一并下发:前端据此区分「本来就没有选项」和「字典没取到」
|
||||
f.setOptionsFrom(optionsFrom);
|
||||
// 选项是内置兜底(不是学校实时字典)时如实告知,前端可提示用户
|
||||
f.setOptionsFallback(isUsingFallbackOptions());
|
||||
return f;
|
||||
}
|
||||
|
||||
/** 转成 JSON Schema 的属性节点(给模型看) */
|
||||
public JSONObject toSchema() {
|
||||
JSONObject o = new JSONObject();
|
||||
o.put("type", type);
|
||||
String desc = description == null ? "" : description;
|
||||
List<String> vals = effectiveValues();
|
||||
if (vals != null && !vals.isEmpty()) {
|
||||
desc = desc + (StringUtils.isBlank(desc) ? "" : ";") + "可选值:" + String.join(" / ", vals);
|
||||
o.put("enum", new JSONArray() {{
|
||||
addAll(vals);
|
||||
}});
|
||||
}
|
||||
if (StringUtils.isNotBlank(example)) {
|
||||
desc = desc + (StringUtils.isBlank(desc) ? "" : ";") + "示例:" + example;
|
||||
}
|
||||
o.put("description", desc);
|
||||
return o;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user