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,171 @@
package com.yunhai.agent.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 智能体配置:大模型 + 各业务模块基地址 + 角色权限映射。
*
* <p>全部落在 {@code application.yml} 的 {@code agent.*} 下,便于运维按环境改配置而不动代码。
*/
@Data
@Component
@ConfigurationProperties(prefix = "agent")
public class AgentSettings {
/** 大模型配置 */
private Llm llm = new Llm();
/** 编排参数 */
private Loop loop = new Loop();
/** 角色权限映射(学生 / 教师 / 管理员 的角色号) */
private Roles roles = new Roles();
/**
* 业务模块基地址表:key = 模块标识(工具定义里引用),value = 该模块的根地址(不含 /api)。
* <p>例:{@code homeleave: http://base.yunhaixingtong.com/home-leave}
* <p>本地联调把 value 改成 {@code http://localhost:8106} 之类即可。
*/
private Map<String, String> modules = new LinkedHashMap<>();
/**
* 各业务模块的「默认落地页」——即该模块前端的一个路由(如 {@code /my})。
*
* <p><b>★ 深链兜底(2026-10 新增)</b>:业务模块的用户态接口 {@code /<模块>/api/*} 只认
* <b>浏览器 CAS 会话</b>,服务端调用拿不到,必然回 {@code code=302}(详见 README 第 11 节)。
* 此时智能体不再只丢一句报错,而是把<b>模块页面地址</b>随轨迹回传前端,
* 让用户用<b>自己的浏览器登录态</b>一键打开真实页面看数据 —— 这比"死等打通"务实得多。
*
* <p>取值已按各模块前端 {@code src/router/routes.ts} 的真实路由核对(2026-10-10):
* <pre>
* homeleave → /my 我的请假
* campusnotice → / 我的公告(收件箱)
* dormnotice → /par/notices 宿舍通知箱
* familyactivity → /par/activities 活动广场
* coursehomework → /stu/today 今日作业
* resourcelibrary → /resource 资源管理
* profession → /home 发展首页
* </pre>
* 运维可在 {@code application.yml} 用 {@code agent.module-landing.<模块>=<路由>} 覆盖单个模块;
* 工具自身声明的 {@code landingPath}({@code write(...)} / {@code readAt(...)})优先级更高。
*/
private Map<String, String> moduleLanding = defaultModuleLanding();
/** 模块默认落地页的出厂值(见 {@link #moduleLanding}) */
private static Map<String, String> defaultModuleLanding() {
Map<String, String> m = new LinkedHashMap<>();
m.put("homeleave", "/my");
m.put("campusnotice", "/");
m.put("dormnotice", "/par/notices");
m.put("familyactivity", "/par/activities");
m.put("coursehomework", "/stu/today");
m.put("resourcelibrary", "/resource");
m.put("profession", "/home");
return m;
}
/**
* 是否按「用户已授权模块」过滤工具(modId 维度)。
* <p>默认 false:不向模型隐藏工具,越权与否由各业务模块按透传身份自行裁决。
* <p>置 true 时:用平台 GetUserMod 的 modId 与 {@code ToolDef.moduleId} 比对,未授权模块的工具不暴露。
*/
private boolean filterToolsByAuthorizedModule = false;
/**
* 是否开启「角色强校验」。
* <p>默认 true:模型若调用了当前角色无权使用的工具,服务端直接拒绝执行(不发真实请求),
* 并在轨迹里留下 FORBIDDEN 记录 —— 这是「分学生、教师权限」的硬约束。
* <p>置 false 可临时放开(排障用),此时仅按角色过滤列表,不做执行拦截。
*/
private boolean enforceRolePermission = true;
/**
* 浏览器代执行(方案 A,2026-10-12 新增)。
*
* <p>业务模块的用户态接口只认<b>浏览器 CAS 会话</b>,服务端调用必然 {@code code=302}
* (README 第 11 节的四条实测证据)。开启后,模块类工具不再由服务端发请求,而是:
* <ol>
* <li>模型决定调用 → 服务端挂起,把「接口描述(方法/路径/参数)」下发前端;</li>
* <li>前端用<b>用户自己的登录态</b>调用模块接口(同站部署 ⇒ 同源,Cookie 自动携带);</li>
* <li>结果回传服务端 → 喂回模型 → 生成最终回答。</li>
* </ol>
* 本地 dev(localhost:8081)因跨域取不到 base 域会话,代执行会失败并自动落回
* 「去系统里查看」的深链兜底;同站部署后即真实生效。
*/
private BrowserExec browserExec = new BrowserExec();
@Data
public static class BrowserExec {
/** 是否启用浏览器代执行(false = 维持服务端直调的旧行为) */
private boolean enabled = true;
}
@Data
public static class Llm {
/** OpenAI 兼容的 chat/completions 地址(DeepSeek 官方:https://api.deepseek.com/chat/completions) */
private String host = "https://api.deepseek.com/chat/completions";
/** 多模态/推理兼用的兜底地址(可选,默认同上) */
private String fallbackHost;
private String apiKey;
/** 默认模型:deepseek-flash(DeepSeek-V4.1-Flash,支持 function calling)。
* 以官方实时清单为准:GET https://api.deepseek.com/models(2026-10-10:deepseek-flash / deepseek-v4-pro) */
private String model = "deepseek-flash";
private int maxTokens = 2048;
private double temperature = 0.3;
/** 单次 HTTP 超时(毫秒)。⚠️ 实际出站超时已由平台统一客户端接管,见 {@code yh.http.read-timeout-ms};本字段保留仅作文档参考 */
private int timeout = 60000;
/** 是否开启深度思考(旧的 deepseek-reasoner 独立模型已下线;V4 系列改为在请求里带 effort 档位,本开关暂未接线) */
private boolean thinking = false;
}
@Data
public static class Loop {
/** 单轮对话内最多允许的「模型思考 → 调工具」循环次数,防死循环 */
private int maxRounds = 6;
/** 每个工具调用结果塞回模型时的最大字符数(防撑爆上下文) */
private int toolResultMaxChars = 6000;
/** 会话上下文最多保留的历史消息条数 */
private int historyMaxMessages = 20;
/** 会话空闲过期时间(分钟) */
private int sessionTtlMinutes = 120;
}
/**
* 角色映射:UserBase 的角色号(roleId)→ 智能体角色。
* <p><b>★ 上线前必须按本校 UserBase 的角色体系校准</b>:
* 平台 {@code BoaUser.getType() == 1} 恒为超管/校管;其余按 {@code roleIds} 命中下面的集合判定。
* 一个用户可同时命中多个(如既是教师又是管理员),能力取并集。
*/
@Data
public static class Roles {
/** 学生角色号(默认 1;请按 UserBase 校准) */
private List<Integer> student = new ArrayList<>(Arrays.asList(1));
/** 教师角色号(默认 2=教师、3=班主任;请按 UserBase 校准) */
private List<Integer> teacher = new ArrayList<>(Arrays.asList(2, 3));
/** 管理员角色号(默认 4=校管、5=分管领导、6=教务、7=校长;请按 UserBase 校准) */
private List<Integer> admin = new ArrayList<>(Arrays.asList(4, 5, 6, 7));
/** 管理员是否天然拥有教师能力(默认 true:校管/领导可用教师侧工具) */
private boolean adminAlsoTeacher = true;
}
}