feat: 提交校园智能助手后端工程源码(Spring Boot 2.7.18 + 平台 starter + JPA + Nacos)
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:
lijing
2026-10-12 11:18:29 +08:00
parent 8589afd69a
commit 8303b857e9
58 changed files with 6883 additions and 2 deletions
@@ -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;
}
}