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,119 @@
package com.yunhai.agent;
import com.yunhai.agent.tool.FormField;
import com.yunhai.agent.tool.ToolDef;
import com.yunhai.agent.tool.ToolParam;
import com.yunhai.agent.tool.defs.LeaveTools;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* 表单字段「控件形态」的回归断言(纯单元,不依赖 Spring)。
*
* <p>背景(2026-10-10 截图实测):请假表单里「请假类型」渲染成了普通输入框,
* 占位文字是「请假类型编号,来自 leave.types」。根因有两层:
* <ol>
* <li>它在工具定义里是 {@code integer},本应落到数字控件,但前端没有 {@code number} 分支;</li>
* <li>它的可选项来自字典工具 {@code leave.types},字典没取到时不该静默退化 ——
* 必须把 {@link FormField#getOptionsFrom()} 一并下发,前端才能提示「可先手填编号」。</li>
* </ol>
*/
class FormFieldWidgetTest {
/** ① 整数参数 → number 控件;字典来源必须一并下发(前端据此在字典失败时给提示) */
@Test
void integerParamWithDictSourceRendersNumberAndCarriesOptionsFrom() {
ToolParam p = ToolParam.required("leaveType", "integer", "请假类型编号,来自 leave.types")
.label("请假类型").optionsFrom("leave.types");
FormField f = p.toFormField();
assertEquals("number", f.getWidget(), "integer 参数应渲染成数字控件");
assertEquals("leave.types", f.getOptionsFrom(), "动态选项来源必须一并下发");
assertTrue(f.getOptions().isEmpty(), "未取到字典时 options 应为空(前端据此提示)");
}
/** ② 静态枚举 → select,且不应带 optionsFrom(否则前端会误报「字典没取到」) */
@Test
void enumParamRendersSelectWithoutOptionsFrom() {
ToolParam p = ToolParam.required("level", "string", "级别")
.label("级别").options(new String[]{"1", "2"}, new String[]{"普通", "重要"});
FormField f = p.toFormField();
assertEquals("select", f.getWidget());
assertNull(f.getOptionsFrom(), "静态枚举没有动态来源,optionsFrom 应为 null");
assertEquals(2, f.getOptions().size());
}
/** ③ 日期与长文本仍按参数名推断(防止改动上面两条时把推断逻辑带歪) */
@Test
void nameBasedWidgetInferenceStillWorks() {
assertEquals("datetime",
ToolParam.required("startDate", "string", "开始时间").toFormField().getWidget());
assertEquals("textarea",
ToolParam.required("remarks", "string", "请假事由").toFormField().getWidget());
}
/**
* ④ 内置兜底:声明了字典来源、而实时字典取不到时,仍应是下拉框,而不是让人填编号的数字框。
* <p>这正是 2026-10-10 反馈「请假类型怎么变成数字框了,还是 1 这种」的修复点 ——
* 光有选项来源不够,还得有可用的默认选项,控件才立得住。
*/
@Test
void fallbackOptionsKeepSelectWidgetWhenDictUnavailable() {
ToolParam p = ToolParam.required("leaveType", "integer", "请假类型")
.label("请假类型")
.fallbackOptions(new String[]{"1", "2"}, new String[]{"病假", "事假及其他"})
.optionsFrom("leave.types");
FormField f = p.toFormField();
assertEquals("select", f.getWidget(), "有兜底选项时必须渲染成下拉框");
assertEquals(2, f.getOptions().size());
assertEquals("1", f.getOptions().get(0).get("value"));
assertEquals("病假", f.getOptions().get(0).get("label"));
assertEquals("事假及其他", f.getOptions().get(1).get("label"));
assertTrue(f.isOptionsFallback(), "必须标记为兜底,前端才能提示「自定义项没取到」");
assertEquals("leave.types", f.getOptionsFrom(), "实时字典来源要保留,取到后覆盖兜底");
}
/** ⑤ 权威枚举优先于兜底:两者都配时兜底不生效,也不应被误标为兜底 */
@Test
void explicitEnumWinsOverFallback() {
ToolParam p = ToolParam.required("status", "integer", "审批结论")
.options(new String[]{"1"}, new String[]{"通过"})
.fallbackOptions(new String[]{"9"}, new String[]{"兜底项"});
FormField f = p.toFormField();
assertEquals(1, f.getOptions().size());
assertEquals("通过", f.getOptions().get(0).get("label"));
assertFalse(f.isOptionsFallback());
}
/** ⑥ 真实工具定义回归:请假类型必须是带中文选项的下拉框,不能退回数字框 */
@Test
void leaveSubmitTypeIsASelectInRealToolDefinition() {
ToolDef def = LeaveTools.defs().stream()
.filter(d -> "leave.submit".equals(d.getName()))
.findFirst()
.orElseThrow(() -> new AssertionError("leave.submit 工具定义丢失"));
ToolParam leaveType = def.getParams().stream()
.filter(p -> "leaveType".equals(p.getName()))
.findFirst()
.orElseThrow(() -> new AssertionError("leaveType 参数丢失"));
FormField f = leaveType.toFormField();
assertEquals("select", f.getWidget());
assertEquals(2, f.getOptions().size());
assertEquals("病假", f.getOptions().get(0).get("label"));
assertEquals("事假及其他", f.getOptions().get(1).get("label"));
}
}