两个模块与「校园公告」同构(发布 → 阅读情况 → 回执闭环),原先只接了「读列表 + 我提交」 一层,价值最高的 发布/盯回执/催办/统计 全空。本轮补齐。 宿舍通知(9 → 35 个工具) - 发布闭环:dorm.publish(立即发布、默认发给全部宿舍)/ dorm.editNotice / dorm.sendList(改指 /api/notice/SendList,我发布的)/ dorm.readDetail / dorm.statistics; 字典:dorm.noticeTypeDic(type=noticeType)/ dorm.deptDic(type=dept)/ dorm.formTemplates。 - 回执闭环:dorm.receipts / dorm.receiptDetail / dorm.reviewReceipt / dorm.remind / dorm.expedite / dorm.remindStudent / dorm.markRead / dorm.pull / dorm.top; 家长侧补 dorm.initializeRed(一键已读)。 - 宿舍特有的床位台账(模块自带、此前完全没接):dorm.pendingStudents → dorm.assign → dorm.removeStudent / dorm.transfer / dorm.setMonitor,以及楼栋房间增改 (dorm.addBuilding / dorm.editBuilding / dorm.addRoom / dorm.editRoom)。 家校活动(7 → 22 个工具) - 发布闭环:activity.publish / activity.edit / activity.sent / activity.readDetail / activity.statistics;字典:activity.deptDic(type=1)/ activity.categoryDic(type=2)。 - 报名闭环:activity.signups / activity.signupDetail / activity.review / activity.remind / activity.expedite / activity.pull / activity.top; 家长侧补 activity.initializeRed。 裸意图直达(IntentShortcut) - 新增 DORM_PUBLISH(仅教职工)与 ACTIVITY_PUBLISH(仅教职工),学生落回模型如实说明。 - ★ 修跨模块歧义:「发通知 / 发布通知」不带限定时校园公告与宿舍通知都成立, 已从公告短语集移除,改由模型按上下文选;只认带「公告/校园」或「宿舍」「活动」限定的说法。 角色(RolePolicy) - 发布/编辑/阅读情况/回执处理/催办/统计/字典/床位台账 → STAFF_ONLY; 家长侧(通知箱、回执提交、活动广场、报名、一键已读)保持不限。 表单下拉复用请假的 optionCall + valueKeys + dependsOn 通道(模块 /api/* 只认浏览器会话, 服务端必然 302)。发布表单的接收对象固定为「全部」(receiveType/receiveList 用 fix 写死), 避免表单里出现难用的房间/班级 id 输入框。 测试:新增 DormActivityPublishIntentTest(10);IntentDirectFormTest +4(教师得表单且不过大模型、 学生落回模型);NoticeIntentFormTest 增加「发通知不再被公告劫持」; RolePermissionTest 补新工具的可见性断言。全量 86/86 通过。 README:4.2 能力划分更新 + 新增「宿舍通知/家校活动补全了哪些能力」;5.1.0 意图对照表补三行。
This commit is contained in:
@@ -122,7 +122,7 @@ AgentService/
|
|||||||
|
|
||||||
| 名单 | 工具 | 说明 |
|
| 名单 | 工具 | 说明 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 教职工(教师+管理员) | `leave.register` `leave.audit` `leave.batchPass` `leave.stats` `leave.classStudents`、`notice.publish` `notice.approve` `notice.reject` `notice.sent` `notice.deptDics` `notice.approvalList`、`dorm.buildings` `dorm.rooms` `dorm.roomStudents` `dorm.dormTree`、`activity.classTree`、`profession.*`(全部) | 请假登记/审批、发布、统计、宿管、专业发展 |
|
| 教职工(教师+管理员) | `leave.register` `leave.audit` `leave.batchPass` `leave.stats` `leave.classStudents`、`notice.publish` `notice.approve` `notice.reject` `notice.sent` `notice.deptDics` `notice.approvalList`、`dorm.*`(发布/编辑/我发布的/阅读情况/回执名单与处理/催办/催回执/代标记/撤回/置顶/统计/字典模板 **+ 楼栋房间床位台账**;家长侧 5 个除外)、`activity.*`(发布/编辑/我发布的/阅读情况/报名名单与处理/催办/上报/撤回/置顶/统计/字典 + 选班级;家长侧除外)、`profession.*`(全部) | 请假登记/审批、发布公告、**宿舍通知发布与回执管理 + 床位台账**、**活动发布与报名处理**、专业发展 |
|
||||||
| 学生 | `leave.submit` `leave.reportBack`、`homework.*`(全部)、`activity.signup` `activity.cancelSignup` | 请假申请、交作业、报名活动、销假 |
|
| 学生 | `leave.submit` `leave.reportBack`、`homework.*`(全部)、`activity.signup` `activity.cancelSignup` | 请假申请、交作业、报名活动、销假 |
|
||||||
| 学生+教职工共用 | `leave.withdraw` | 撤销请假(学生=申请撤销,教师=直接作废) |
|
| 学生+教职工共用 | `leave.withdraw` | 撤销请假(学生=申请撤销,教师=直接作废) |
|
||||||
| 不限(所有已识别角色) | 各模块的只读查询(列表/详情/字典等) | 数据范围由下游模块按身份收敛 |
|
| 不限(所有已识别角色) | 各模块的只读查询(列表/详情/字典等) | 数据范围由下游模块按身份收敛 |
|
||||||
@@ -134,6 +134,22 @@ AgentService/
|
|||||||
> 拆成 `leave.submit`(学生)与 `leave.register`(教职工)后,角色过滤天然把错的工具挡在模型视野外,
|
> 拆成 `leave.submit`(学生)与 `leave.register`(教职工)后,角色过滤天然把错的工具挡在模型视野外,
|
||||||
> 表单字段由各自必填参数推导,教师表单自动带出「班级 → 学生」联动下拉。
|
> 表单字段由各自必填参数推导,教师表单自动带出「班级 → 学生」联动下拉。
|
||||||
|
|
||||||
|
> ★ **宿舍通知 / 家校活动补全了哪些能力**:这两个模块与「校园公告」是**同一套骨架**
|
||||||
|
> —— *发布 → 阅读情况 → 回执闭环*,原先只接了「读列表 + 家长提交」这一层,
|
||||||
|
> 价值最高的 *发布 / 盯回执 / 催办 / 统计* 全空。本轮补齐:
|
||||||
|
> - **发布闭环**:`dorm.publish` / `activity.publish`(+ `edit` / 我发布的 / 阅读情况 / 统计)、
|
||||||
|
> 发布表单下拉所需的字典工具(`dorm.noticeTypeDic` `dorm.deptDic` `activity.deptDic` `activity.categoryDic`);
|
||||||
|
> - **回执闭环**:`dorm.receipts` / `dorm.reviewReceipt` / `dorm.remind` / `activity.signups` / `activity.review` / `activity.remind`
|
||||||
|
> —— 回答「还有谁没交 / 没回执」,并支持确认、取消、催办;
|
||||||
|
> - **宿舍特有(模块自带但一直没接)的床位台账**:`dorm.pendingStudents` → `dorm.assign`(安排入住)
|
||||||
|
> → `dorm.removeStudent`(退宿)/ `dorm.transfer`(调宿)/ `dorm.setMonitor`(设宿舍长),
|
||||||
|
> 以及楼栋房间的增改(`dorm.addBuilding` `dorm.editBuilding` `dorm.addRoom` `dorm.editRoom`)。
|
||||||
|
>
|
||||||
|
> 写操作**不需要新传输层**:统一走已跑通的浏览器代执行(前端带用户登录态调 `/api/*`),
|
||||||
|
> 表单下拉复用请假的 `optionCall + valueKeys + dependsOn` 通道。
|
||||||
|
> ⚠️ **删除类(`/api/notice/Delete`、`/api/activity/Delete`)暂未接入**:它们的请求体是
|
||||||
|
> **JSON 数组**(`@RequestBody List<Integer>`),而工具执行器只构造对象体;且删除破坏性强,留待需要时单列。
|
||||||
|
|
||||||
### 4.3 两道闸门
|
### 4.3 两道闸门
|
||||||
|
|
||||||
1. **模型侧过滤**(软):`ToolRegistry.toOpenAiTools(modIds, roles)` 只把该角色可用的工具放进大模型的 `tools`,
|
1. **模型侧过滤**(软):`ToolRegistry.toOpenAiTools(modIds, roles)` 只把该角色可用的工具放进大模型的 `tools`,
|
||||||
@@ -180,9 +196,15 @@ AgentService/
|
|||||||
| 请假 | 教职工 | `leave.register` | **班级 → 学生**(联动下拉)/ 请假类型 / 起止时间 / 事由 | `type=1` 免审直接生效 |
|
| 请假 | 教职工 | `leave.register` | **班级 → 学生**(联动下拉)/ 请假类型 / 起止时间 / 事由 | `type=1` 免审直接生效 |
|
||||||
| 发布校园公告 | 教职工 | `notice.publish` | 公告标题 / 公告正文 / **发文部门**(下拉) | **立即发布、全站可见** |
|
| 发布校园公告 | 教职工 | `notice.publish` | 公告标题 / 公告正文 / **发文部门**(下拉) | **立即发布、全站可见** |
|
||||||
| 发布校园公告 | 学生 | —(置 null) | — | 落回模型,如实说明需要教师/管理员身份 |
|
| 发布校园公告 | 学生 | —(置 null) | — | 落回模型,如实说明需要教师/管理员身份 |
|
||||||
|
| 发布宿舍通知 | 教职工 | `dorm.publish` | 通知标题 / 通知正文 / **通知类型 + 发文部门**(下拉)/ 回执方式 | 发给**全部宿舍**的家长(房间级定向到模块页面勾选) |
|
||||||
|
| 发布宿舍通知 | 学生 | —(置 null) | — | 落回模型,如实说明需要教师/管理员(宿管)身份 |
|
||||||
|
| 发布家校活动 | 教职工 | `activity.publish` | 活动标题 / 说明 / **活动分类 + 主办部门**(下拉)/ 地点 / 起止时间 / 报名方式 | 发给**全部班级**的家长 |
|
||||||
|
| 发布家校活动 | 学生 | —(置 null) | — | 落回模型,如实说明需要教师/管理员身份 |
|
||||||
|
|
||||||
⚠️ **发布公告只认带「发/发布」动词的说法**(`发布公告`/`发个公告`/`我要发布校园公告`/`发通知`…):
|
⚠️ **发布类意图必须带模块限定**。2026-10 起宿舍通知也接上了发布工具,因此
|
||||||
光说「公告」「通知」更像**查询**,误判成"发布"会很突兀,故不进短语表。
|
`发通知`/`发布通知` 成了**跨模块歧义**(校园公告 / 宿舍通知都可能),已**移出**公告短语集;
|
||||||
|
现在只认 `发布公告`/`发个公告`/`我要发布校园公告`/`发布校园通知`(带「公告」或「校园」限定)
|
||||||
|
与 `发布宿舍通知`/`发宿舍通知`(带「宿舍」限定)。光说「公告」「通知」更像**查询**,本就不进短语表。
|
||||||
|
|
||||||
⚠️ **不要再给 `notice.publish` 加「发布方式」参数**:后端 `/api/notice/Create` 就是**直接发布**
|
⚠️ **不要再给 `notice.publish` 加「发布方式」参数**:后端 `/api/notice/Create` 就是**直接发布**
|
||||||
(是否转审批由学校「应用设置」里的流程绑定决定,请求侧绕不过),并不存在 `publishType` 字段 ——
|
(是否转审批由学校「应用设置」里的流程绑定决定,请求侧绕不过),并不存在 `publishType` 字段 ——
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import java.util.Set;
|
|||||||
*
|
*
|
||||||
* <p>名单:
|
* <p>名单:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>{@link #STAFF_ONLY} —— 教职工侧(教师 + 管理员):请假登记/审批/统计、公告发布审批、宿管、专业发展;</li>
|
* <li>{@link #STAFF_ONLY} —— 教职工侧(教师 + 管理员):请假登记/审批/统计、公告发布审批、
|
||||||
|
* 宿舍通知发布/回执处理/床位台账、家校活动发布/报名处理、专业发展;</li>
|
||||||
* <li>{@link #STUDENT_ONLY} —— 学生侧:请假申请、交作业、销假;</li>
|
* <li>{@link #STUDENT_ONLY} —— 学生侧:请假申请、交作业、销假;</li>
|
||||||
* <li>{@link #STUDENT_OR_PARENT} —— 家校活动报名:家长与学生都可发起;</li>
|
* <li>{@link #STUDENT_OR_PARENT} —— 家校活动报名:家长与学生都可发起;</li>
|
||||||
* <li>{@link #SHARED} —— 学生与教职工都可能发起的:撤销请假。</li>
|
* <li>{@link #SHARED} —— 学生与教职工都可能发起的:撤销请假。</li>
|
||||||
@@ -35,10 +36,23 @@ public final class RolePolicy {
|
|||||||
// 公告:发布与审批
|
// 公告:发布与审批
|
||||||
"notice.sent", "notice.deptDics", "notice.publish",
|
"notice.sent", "notice.deptDics", "notice.publish",
|
||||||
"notice.approvalList", "notice.approve", "notice.reject",
|
"notice.approvalList", "notice.approve", "notice.reject",
|
||||||
// 宿舍通知:楼栋/房间/住宿生管理侧
|
// 宿舍通知:发布 / 编辑 / 阅读情况 / 回执处理 / 催办 / 统计
|
||||||
|
"dorm.publish", "dorm.editNotice", "dorm.sendList",
|
||||||
|
"dorm.readDetail", "dorm.receipts", "dorm.receiptDetail", "dorm.reviewReceipt",
|
||||||
|
"dorm.remind", "dorm.expedite", "dorm.remindStudent", "dorm.markRead",
|
||||||
|
"dorm.pull", "dorm.top", "dorm.statistics",
|
||||||
|
// 宿舍通知:发布表单的字典与单据模板
|
||||||
|
"dorm.deptDic", "dorm.noticeTypeDic", "dorm.formTemplates",
|
||||||
|
// 宿舍通知:楼栋 / 房间 / 床位台账(宿管日常)
|
||||||
"dorm.buildings", "dorm.rooms", "dorm.roomStudents", "dorm.dormTree",
|
"dorm.buildings", "dorm.rooms", "dorm.roomStudents", "dorm.dormTree",
|
||||||
// 家校活动:发布时选班级
|
"dorm.pendingStudents", "dorm.assign", "dorm.removeStudent", "dorm.transfer",
|
||||||
"activity.classTree"
|
"dorm.setMonitor", "dorm.addBuilding", "dorm.editBuilding", "dorm.addRoom", "dorm.editRoom",
|
||||||
|
// 家校活动:发布 / 编辑 / 阅读情况 / 报名处理 / 催办 / 统计
|
||||||
|
"activity.publish", "activity.edit", "activity.sent",
|
||||||
|
"activity.readDetail", "activity.signups", "activity.signupDetail", "activity.review",
|
||||||
|
"activity.remind", "activity.expedite", "activity.pull", "activity.top", "activity.statistics",
|
||||||
|
// 家校活动:发布表单的字典与选班级
|
||||||
|
"activity.deptDic", "activity.categoryDic", "activity.classTree"
|
||||||
));
|
));
|
||||||
|
|
||||||
/** 学生专属 */
|
/** 学生专属 */
|
||||||
|
|||||||
@@ -1013,6 +1013,13 @@ public class AgentOrchestrator {
|
|||||||
.append("缺哪项就照常调用工具让系统弹表单,不要自己编造发文部门,也不要用文字反复追问。")
|
.append("缺哪项就照常调用工具让系统弹表单,不要自己编造发文部门,也不要用文字反复追问。")
|
||||||
.append("该工具只对教职工下发(学生身份看不到),学生要求发布公告时如实说明身份不够。\n");
|
.append("该工具只对教职工下发(学生身份看不到),学生要求发布公告时如实说明身份不够。\n");
|
||||||
|
|
||||||
|
sb.append("11. ★ 「发布宿舍通知」与「发布校园公告」是**两个不同模块**,不要混用:")
|
||||||
|
.append("宿舍通知发给**家长**、可要求家长回执(回执方式:无需/确认/填报),发布时要填通知类型与发文部门;")
|
||||||
|
.append("校园公告是全校范围的公告。用户说「发通知」而没指明模块时,先问清楚是校园公告还是宿舍通知。\n");
|
||||||
|
|
||||||
|
sb.append("12. ★ 「发布家校活动」要拿到标题、说明、活动分类、主办部门、活动地点与起止时间;")
|
||||||
|
.append("活动默认要求家长报名。缺项就照常调用工具让系统弹表单,分类与主办部门必须来自字典,不要编造。\n");
|
||||||
|
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import java.util.Set;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 「裸意图」直达:把只有意图、没有任何细节的一句话(典型:<b>「我要请假」</b>、
|
* 「裸意图」直达:把只有意图、没有任何细节的一句话(典型:<b>「我要请假」</b>、
|
||||||
* <b>「发布校园公告」</b>)确定性地映射到对应表单,<b>不经过大模型决策</b>。
|
* <b>「发布校园公告」</b>、<b>「发布宿舍通知」</b>、<b>「发布活动」</b>)
|
||||||
|
* 确定性地映射到对应表单,<b>不经过大模型决策</b>。
|
||||||
*
|
*
|
||||||
* <h3>为什么需要它</h3>
|
* <h3>为什么需要它</h3>
|
||||||
* 同一句「我要请假」,模型可能给出三种都不合预期的反应:
|
* 同一句「我要请假」,模型可能给出三种都不合预期的反应:
|
||||||
@@ -122,6 +123,29 @@ public class IntentShortcut {
|
|||||||
"好的,下面是**发布校园公告**表单:填好公告标题和正文,选好发文部门,"
|
"好的,下面是**发布校园公告**表单:填好公告标题和正文,选好发文部门,"
|
||||||
+ "点「确认提交」我就直接发布(立即生效,全站可见)。");
|
+ "点「确认提交」我就直接发布(立即生效,全站可见)。");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布宿舍通知意图:<b>仅教职工</b>(宿管 / 管理员)。
|
||||||
|
*
|
||||||
|
* <p>与公告不同,宿舍通知强调「给家长发、要回执」,所以表单里带「回执方式」;
|
||||||
|
* 接收对象默认全部宿舍(房间级定向请到模块页面里勾选)。
|
||||||
|
*/
|
||||||
|
private static final Intent DORM_PUBLISH = new Intent("dorm.publish",
|
||||||
|
null,
|
||||||
|
"dorm.publish",
|
||||||
|
"发布宿舍通知需要教师或管理员(宿管)身份;你可以先看看「我的宿舍通知箱」,或换个身份账号再来发布。",
|
||||||
|
"好的,下面是**发布宿舍通知**表单:填好标题和正文,选好通知类型与发文部门,"
|
||||||
|
+ "点「确认提交」我就发给全部宿舍的家长(要家长回执的话,在「回执方式」里选一种)。");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布家校活动意图:<b>仅教职工</b>。活动需要地点与起止时间,故表单字段比公告多。
|
||||||
|
*/
|
||||||
|
private static final Intent ACTIVITY_PUBLISH = new Intent("activity.publish",
|
||||||
|
null,
|
||||||
|
"activity.publish",
|
||||||
|
"发布家校活动需要教师或管理员身份;你可以先看看「活动广场」,或换个身份账号再来发布。",
|
||||||
|
"好的,下面是**发布家校活动**表单:填好活动标题、说明、分类、地点和起止时间,"
|
||||||
|
+ "点「确认提交」我就发给全部班级的家长(需要家长报名的话,报名方式保持默认即可)。");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请假的标准说法全集(已剥掉语气词与标点后的形态)。
|
* 请假的标准说法全集(已剥掉语气词与标点后的形态)。
|
||||||
*/
|
*/
|
||||||
@@ -137,16 +161,33 @@ public class IntentShortcut {
|
|||||||
* (「公告」「通知」单独一句,用户多半是想看看有什么),误判成"发布"会很突兀。
|
* (「公告」「通知」单独一句,用户多半是想看看有什么),误判成"发布"会很突兀。
|
||||||
* 只认带「发/发布」动词的说法,与「读公告」明确区分开。
|
* 只认带「发/发布」动词的说法,与「读公告」明确区分开。
|
||||||
*
|
*
|
||||||
* <p>⚠️ 现在收「发通知」是安全的:本平台里 {@code campusnotice} 就是「校园公告(校园通知)」,
|
* <p>★ 2026-10 起「发通知 / 发布通知」这类**不带模块限定**的说法已从本组移除:
|
||||||
* 而宿舍通知目前**没有**发布工具({@code dormnotice} 只有家长侧回执)。等将来接上
|
* 宿舍通知也接上了发布工具({@code dorm.publish}),同一句「发通知」会变成跨模块歧义
|
||||||
* 宿舍通知发布后,「发通知」会变成跨模块歧义(猜错模块比不直达更糟)——
|
* (猜错模块比不直达更糟)。现在只认带「公告」或「校园」限定的说法,其余交给模型按上下文选。
|
||||||
* 届时要么把本组里的「通知」说法摘掉只留「公告」,要么交给模型按上下文选。
|
|
||||||
*/
|
*/
|
||||||
private static final Set<String> NOTICE_PUBLISH_BARE = new HashSet<>(Arrays.asList(
|
private static final Set<String> NOTICE_PUBLISH_BARE = new HashSet<>(Arrays.asList(
|
||||||
"发布公告", "发公告", "发个公告", "发布校园公告", "校园公告发布",
|
"发布公告", "发公告", "发个公告", "发布校园公告", "校园公告发布",
|
||||||
"发布一条公告", "发一条公告", "发布一篇公告", "发一篇公告",
|
"发布一条公告", "发一条公告", "发布一篇公告", "发一篇公告",
|
||||||
"发布通知", "发通知", "发个通知", "发布校园通知", "校园通知发布",
|
"发布校园通知", "校园通知发布"
|
||||||
"发布一条通知", "发一条通知"
|
));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布宿舍通知的标准说法全集 —— 必须带「宿舍」限定(否则与校园公告混淆)。
|
||||||
|
*/
|
||||||
|
private static final Set<String> DORM_PUBLISH_BARE = new HashSet<>(Arrays.asList(
|
||||||
|
"发布宿舍通知", "发宿舍通知", "发个宿舍通知", "发一条宿舍通知", "发布一条宿舍通知",
|
||||||
|
"宿舍通知发布", "发布宿舍公告", "发宿舍公告", "宿舍公告发布", "给宿舍发通知"
|
||||||
|
));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布家校活动的标准说法全集。
|
||||||
|
*
|
||||||
|
* <p>⚠️ 不收「报名活动」—— 那是家长侧的另一件事({@code activity.signup}),不属发布意图。
|
||||||
|
*/
|
||||||
|
private static final Set<String> ACTIVITY_PUBLISH_BARE = new HashSet<>(Arrays.asList(
|
||||||
|
"发布活动", "发活动", "发个活动", "发起活动", "活动发布",
|
||||||
|
"发布一个活动", "发布一条活动", "发一条活动",
|
||||||
|
"发布家校活动", "发家校活动", "发布校园活动", "发校园活动", "发布亲子活动"
|
||||||
));
|
));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,6 +213,8 @@ public class IntentShortcut {
|
|||||||
static {
|
static {
|
||||||
RULES.add(new Rule(LEAVE_BARE, LEAVE));
|
RULES.add(new Rule(LEAVE_BARE, LEAVE));
|
||||||
RULES.add(new Rule(NOTICE_PUBLISH_BARE, NOTICE_PUBLISH));
|
RULES.add(new Rule(NOTICE_PUBLISH_BARE, NOTICE_PUBLISH));
|
||||||
|
RULES.add(new Rule(DORM_PUBLISH_BARE, DORM_PUBLISH));
|
||||||
|
RULES.add(new Rule(ACTIVITY_PUBLISH_BARE, ACTIVITY_PUBLISH));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,20 +7,36 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 家校活动模块工具 —— 后端 FamilyActivity,前缀 /api/activity、/api/base。
|
* 家校活动模块工具 —— 后端 FamilyActivity,前缀 /api/activity、/api/dict、/api/base。
|
||||||
*
|
*
|
||||||
* <p>核心闭环:活动广场 → 家长报名 → 学校确认。
|
* <p>核心闭环:<b>学校发活动 → 家长报名 → 学校确认报名 / 盯报名情况</b>。
|
||||||
|
* 与「宿舍通知」同构(发布 → 阅读情况 → 回执闭环),只是回执换成了「报名」。
|
||||||
*/
|
*/
|
||||||
public final class ActivityTools {
|
public final class ActivityTools {
|
||||||
|
|
||||||
public static final String MODULE = "familyactivity";
|
public static final String MODULE = "familyactivity";
|
||||||
|
|
||||||
|
/** 主办部门字典的 type 值(/api/dict/List?type=1) */
|
||||||
|
private static final Object DICT_TYPE_DEPT = 1;
|
||||||
|
|
||||||
|
/** 活动分类字典的 type 值(/api/dict/List?type=2) */
|
||||||
|
private static final Object DICT_TYPE_CATEGORY = 2;
|
||||||
|
|
||||||
private ActivityTools() {
|
private ActivityTools() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<ToolDef> defs() {
|
public static List<ToolDef> defs() {
|
||||||
List<ToolDef> list = new ArrayList<>();
|
List<ToolDef> list = new ArrayList<>();
|
||||||
|
addParentSide(list);
|
||||||
|
addPublishLoop(list);
|
||||||
|
addSignupLoop(list);
|
||||||
|
addDictionaries(list);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 家长 / 学生侧 ==================== */
|
||||||
|
|
||||||
|
private static void addParentSide(List<ToolDef> list) {
|
||||||
list.add(ToolDef.of("activity.plaza", MODULE, "活动广场",
|
list.add(ToolDef.of("activity.plaza", MODULE, "活动广场",
|
||||||
"当前家长/学生视角的活动广场(含我的报名状态与分桶计数)。用户问“最近有什么活动”“我能报什么”时用它。",
|
"当前家长/学生视角的活动广场(含我的报名状态与分桶计数)。用户问“最近有什么活动”“我能报什么”时用它。",
|
||||||
"GET", "/api/activity/MyActivities")
|
"GET", "/api/activity/MyActivities")
|
||||||
@@ -72,11 +88,185 @@ public final class ActivityTools {
|
|||||||
.label("活动"))
|
.label("活动"))
|
||||||
.write("/par/mine"));
|
.write("/par/mine"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("activity.initializeRed", MODULE, "一键已读全部活动",
|
||||||
|
"把家长端收到的活动一次性全部标记为已读。",
|
||||||
|
"POST_FORM", "/api/activity/InitializeRed")
|
||||||
|
.write("/par/activities"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 发布闭环(教职工) ==================== */
|
||||||
|
|
||||||
|
private static void addPublishLoop(List<ToolDef> list) {
|
||||||
|
list.add(ToolDef.of("activity.publish", MODULE, "发布家校活动",
|
||||||
|
"发布一条家校活动(可要求家长报名),默认发给**全部班级**的家长。"
|
||||||
|
+ "调用前必须确认:标题 title、正文 content、活动分类 category、主办部门 deptId、"
|
||||||
|
+ "活动地点 venue、开始/结束时间 startTime/endTime;"
|
||||||
|
+ "缺失就直接调用本工具,系统会弹出表单让用户补齐,不要用文字反复追问;"
|
||||||
|
+ "活动分类与主办部门必须来自字典,不要编造。默认立即发布;如需先存草稿,把 draft 置 true。",
|
||||||
|
"POST_JSON", "/api/activity/Create")
|
||||||
|
.p(ToolParam.required("title", "string", "活动标题")
|
||||||
|
.label("活动标题").eg("秋季亲子运动会"),
|
||||||
|
ToolParam.required("content", "string", "活动说明(纯文本)")
|
||||||
|
.label("活动说明").as("textarea").eg("为增进家校互动,学校拟举办秋季亲子运动会……"),
|
||||||
|
ToolParam.required("category", "string", "活动分类,来自 activity.categoryDic 的 k 字段")
|
||||||
|
.label("活动分类")
|
||||||
|
.optionsFrom("activity.categoryDic").optionsKeys("k", "v"),
|
||||||
|
ToolParam.required("deptId", "string", "主办部门 id,来自 activity.deptDic 的 k 字段")
|
||||||
|
.label("主办部门")
|
||||||
|
.optionsFrom("activity.deptDic").optionsKeys("k", "v"),
|
||||||
|
ToolParam.required("venue", "string", "活动地点")
|
||||||
|
.label("活动地点").eg("学校操场"),
|
||||||
|
ToolParam.required("startTime", "string", "活动开始时间 yyyy-MM-dd HH:mm:ss")
|
||||||
|
.label("开始时间").eg("2026-10-20 09:00:00"),
|
||||||
|
ToolParam.required("endTime", "string", "活动结束时间 yyyy-MM-dd HH:mm:ss")
|
||||||
|
.label("结束时间").eg("2026-10-20 11:30:00"),
|
||||||
|
ToolParam.required("receiptMode", "string", "报名方式:signup 需要家长报名 / none 无需报名")
|
||||||
|
.label("报名方式").dft("signup")
|
||||||
|
.options(new String[]{"signup", "none"}, new String[]{"需要家长报名", "无需报名"}),
|
||||||
|
ToolParam.optional("signupDeadline", "string", "报名截止时间 yyyy-MM-dd HH:mm:ss(留空=不限)")
|
||||||
|
.label("报名截止时间").eg("2026-10-18 18:00:00"),
|
||||||
|
ToolParam.optional("quota", "integer", "名额上限(0 或留空 = 不限)")
|
||||||
|
.label("名额上限").dft("0"),
|
||||||
|
ToolParam.optional("allowAccompany", "integer", "是否允许家长随行")
|
||||||
|
.label("家长随行").dft("1")
|
||||||
|
.options(new String[]{"1", "0"}, new String[]{"允许", "不允许"}),
|
||||||
|
ToolParam.optional("cost", "number", "活动费用(元,留空=免费)")
|
||||||
|
.label("活动费用"),
|
||||||
|
ToolParam.optional("smsFlag", "integer", "是否同时发短信提醒家长")
|
||||||
|
.label("同时发短信").dft("0")
|
||||||
|
.options(new String[]{"0", "1"}, new String[]{"否", "是"}),
|
||||||
|
ToolParam.optional("topmost", "integer", "是否置顶")
|
||||||
|
.label("置顶").dft("0")
|
||||||
|
.options(new String[]{"0", "1"}, new String[]{"否", "是"}),
|
||||||
|
ToolParam.optional("draft", "string", "是否存草稿:true 只保存不发布")
|
||||||
|
.label("存草稿").dft("false")
|
||||||
|
.options(new String[]{"false", "true"}, new String[]{"立即发布", "存草稿"}))
|
||||||
|
// 接收对象:默认全部班级(班级级定向请到模块页面里勾选)
|
||||||
|
.fix("receiveType", "class")
|
||||||
|
.fix("receiveList", "ALL")
|
||||||
|
.write("/list/new"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("activity.edit", MODULE, "编辑家校活动",
|
||||||
|
"修改一条已发布(或草稿)的活动。调用前须确认 sourceId(要改的那条活动 id)。",
|
||||||
|
"POST_JSON", "/api/activity/Edit")
|
||||||
|
.p(ToolParam.required("sourceId", "integer", "要编辑的活动 id")
|
||||||
|
.label("活动"),
|
||||||
|
ToolParam.required("title", "string", "活动标题")
|
||||||
|
.label("活动标题"),
|
||||||
|
ToolParam.required("content", "string", "活动说明")
|
||||||
|
.label("活动说明").as("textarea"))
|
||||||
|
.fix("receiveType", "class")
|
||||||
|
.fix("receiveList", "ALL")
|
||||||
|
.write("/list"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("activity.sent", MODULE, "我发布的活动",
|
||||||
|
"查询我发布的家校活动列表(可按关键词筛选)。用户问“我发过哪些活动”“那个活动发了没”时用它。",
|
||||||
|
"GET", "/api/activity/SendList")
|
||||||
|
.p(ToolParam.optional("pageIndex", "integer", "页码").dft("1"),
|
||||||
|
ToolParam.optional("pageSize", "integer", "每页条数").dft("10"),
|
||||||
|
ToolParam.optional("scope", "string", "传 user 只看自己发布的"),
|
||||||
|
ToolParam.optional("keyword", "string", "标题关键词"))
|
||||||
|
.readAt("/list"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("activity.readDetail", MODULE, "活动阅读情况",
|
||||||
|
"查询某个活动的阅读情况(已读 / 未读分组,按年级·班级)。",
|
||||||
|
"GET", "/api/activity/ReadDetail")
|
||||||
|
.p(ToolParam.required("serid", "integer", "活动 id"))
|
||||||
|
.readAt("/signups"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("activity.pull", MODULE, "撤回活动",
|
||||||
|
"撤回一条已发布的活动(撤回后家长端不可见)。",
|
||||||
|
"POST_FORM", "/api/activity/UserPull")
|
||||||
|
.p(ToolParam.required("serid", "integer", "活动 id")
|
||||||
|
.label("活动"))
|
||||||
|
.write("/list"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("activity.top", MODULE, "置顶 / 取消置顶活动",
|
||||||
|
"把一个活动置顶或取消置顶。",
|
||||||
|
"POST_FORM", "/api/activity/SetTopmost")
|
||||||
|
.p(ToolParam.required("serid", "integer", "活动 id")
|
||||||
|
.label("活动"),
|
||||||
|
ToolParam.optional("topmost", "integer", "1 置顶(默认) / 0 取消置顶")
|
||||||
|
.label("操作").dft("1")
|
||||||
|
.options(new String[]{"1", "0"}, new String[]{"置顶", "取消置顶"}))
|
||||||
|
.write("/list"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("activity.expedite", MODULE, "催办未读(活动)",
|
||||||
|
"对一个活动的未读人员统一催办(短信 / 站内提醒)。",
|
||||||
|
"POST_FORM", "/api/activity/Expedite")
|
||||||
|
.p(ToolParam.required("serid", "integer", "活动 id")
|
||||||
|
.label("活动"))
|
||||||
|
.write("/signups"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("activity.statistics", MODULE, "活动统计",
|
||||||
|
"查询活动的发布 / 阅读 / 报名统计(可按时间段)。",
|
||||||
|
"GET", "/api/activity/Statis")
|
||||||
|
.p(ToolParam.optional("startTime", "string", "开始时间 yyyy-MM-dd").eg("2026-10-01"),
|
||||||
|
ToolParam.optional("endTime", "string", "结束时间 yyyy-MM-dd").eg("2026-10-31"))
|
||||||
|
.readAt("/stats"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 报名闭环(教职工) ==================== */
|
||||||
|
|
||||||
|
private static void addSignupLoop(List<ToolDef> list) {
|
||||||
|
list.add(ToolDef.of("activity.signups", MODULE, "活动报名名单",
|
||||||
|
"查询某个活动的报名 / 回执名单(按班级分组 + 汇总)。"
|
||||||
|
+ "用户问“还有谁没报名”“报名情况怎么样”时用它。",
|
||||||
|
"GET", "/api/activity/Signups")
|
||||||
|
.p(ToolParam.required("serid", "integer", "活动 id")
|
||||||
|
.label("活动"),
|
||||||
|
ToolParam.optional("signupStatus", "string",
|
||||||
|
"筛选:all 全部 / signed 已报名 / confirmed 已确认 / canceled 已取消 / none 未报名")
|
||||||
|
.dft("all"))
|
||||||
|
.readAt("/signups"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("activity.signupDetail", MODULE, "单条报名详情",
|
||||||
|
"查看某条报名的明细(家长填写的参加人数 / 联系人 / 备注)。",
|
||||||
|
"GET", "/api/activity/SignupDetail")
|
||||||
|
.p(ToolParam.required("signupId", "integer", "报名 id"))
|
||||||
|
.readAt("/signups"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("activity.review", MODULE, "处理活动报名",
|
||||||
|
"对一条报名执行确认 / 取消 / 候补操作。",
|
||||||
|
"POST_JSON", "/api/activity/Review")
|
||||||
|
.p(ToolParam.required("signupId", "integer", "报名 id")
|
||||||
|
.label("报名"),
|
||||||
|
ToolParam.required("action", "string", "处理动作:confirm 通过 / cancel 取消 / waitlist 转候补")
|
||||||
|
.label("处理动作")
|
||||||
|
.options(new String[]{"confirm", "cancel", "waitlist"},
|
||||||
|
new String[]{"通过", "取消", "转候补"}))
|
||||||
|
.write("/signups"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("activity.remind", MODULE, "催报名(活动)",
|
||||||
|
"向未报名的家长发送报名提醒。",
|
||||||
|
"POST_FORM", "/api/activity/Remind")
|
||||||
|
.p(ToolParam.required("serid", "integer", "活动 id")
|
||||||
|
.label("活动"))
|
||||||
|
.write("/signups"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 字典(发布表单下拉的来源) ==================== */
|
||||||
|
|
||||||
|
private static void addDictionaries(List<ToolDef> list) {
|
||||||
|
list.add(ToolDef.of("activity.deptDic", MODULE, "主办部门字典",
|
||||||
|
"查询本校可选的主办部门(发布活动时填 deptId 用)。"
|
||||||
|
+ "仅在用户明确问「有哪些主办部门」时使用;"
|
||||||
|
+ "发布活动时**不要**先调它 —— 表单的「主办部门」下拉会自动带出可选值。",
|
||||||
|
"GET", "/api/dict/List")
|
||||||
|
.fix("type", DICT_TYPE_DEPT)
|
||||||
|
.readAt("/list/new"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("activity.categoryDic", MODULE, "活动分类字典",
|
||||||
|
"查询本校可选的活动分类(发布活动时填 category 用)。"
|
||||||
|
+ "仅在用户明确问「有哪些活动分类」时使用;"
|
||||||
|
+ "发布活动时**不要**先调它 —— 表单的「活动分类」下拉会自动带出可选值。",
|
||||||
|
"GET", "/api/dict/List")
|
||||||
|
.fix("type", DICT_TYPE_CATEGORY)
|
||||||
|
.readAt("/list/new"));
|
||||||
|
|
||||||
list.add(ToolDef.of("activity.classTree", MODULE, "年级班级树",
|
list.add(ToolDef.of("activity.classTree", MODULE, "年级班级树",
|
||||||
"年级 → 班级 树(发布活动时选接收班级用)。",
|
"年级 → 班级 树(发布活动时选接收班级用)。",
|
||||||
"GET", "/api/base/GetClassTree")
|
"GET", "/api/base/GetClassTree")
|
||||||
.readAt("/list/new"));
|
.readAt("/list/new"));
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,20 +7,44 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 宿舍通知模块工具 —— 后端 DormNotice,前缀 /api/notice、/api/dorm、/api/base。
|
* 宿舍通知模块工具 —— 后端 DormNotice,前缀 /api/notice、/api/dorm、/api/dict、/api/base。
|
||||||
*
|
*
|
||||||
* <p>家校闭环:宿管发通知 → 家长查看 → 家长回执/填报。
|
* <p>两条主线:
|
||||||
|
* <ul>
|
||||||
|
* <li><b>家校闭环</b>:宿管发通知 → 家长查看 → 家长回执/填报 → 宿管盯回执、催办。</li>
|
||||||
|
* <li><b>床位台账</b>:待安排池 → 安排入住 → 退宿 → 调宿 → 设宿舍长(宿管每天的活)。</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>注意:本模块与「校园公告」一样,服务端直调用户态接口会拿到 {@code code=302}(只认浏览器 CAS 会话),
|
||||||
|
* 凡是需要「取字典给下拉」的字段都用 {@code optionsFrom + optionsKeys} 声明,由前端带用户登录态代取
|
||||||
|
* (见 AgentOrchestrator#buildForm)。
|
||||||
*/
|
*/
|
||||||
public final class DormNoticeTools {
|
public final class DormNoticeTools {
|
||||||
|
|
||||||
public static final String MODULE = "dormnotice";
|
public static final String MODULE = "dormnotice";
|
||||||
|
|
||||||
|
/** 通知类型字典的 type 值(/api/dict/List?type=noticeType) */
|
||||||
|
private static final String DICT_TYPE_NOTICE = "noticeType";
|
||||||
|
|
||||||
|
/** 发文部门字典的 type 值(/api/dict/List?type=dept) */
|
||||||
|
private static final String DICT_TYPE_DEPT = "dept";
|
||||||
|
|
||||||
private DormNoticeTools() {
|
private DormNoticeTools() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<ToolDef> defs() {
|
public static List<ToolDef> defs() {
|
||||||
List<ToolDef> list = new ArrayList<>();
|
List<ToolDef> list = new ArrayList<>();
|
||||||
|
addParentSide(list);
|
||||||
|
addPublishLoop(list);
|
||||||
|
addReceiptLoop(list);
|
||||||
|
addDictionaries(list);
|
||||||
|
addDormLedger(list);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 家长 / 学生侧 ==================== */
|
||||||
|
|
||||||
|
private static void addParentSide(List<ToolDef> list) {
|
||||||
list.add(ToolDef.of("dorm.myNotices", MODULE, "我的宿舍通知箱",
|
list.add(ToolDef.of("dorm.myNotices", MODULE, "我的宿舍通知箱",
|
||||||
"当前家长/学生视角的宿舍通知箱(含我的回执状态与各分桶计数)。用户问“宿舍有什么通知”“我要回执”时用它。",
|
"当前家长/学生视角的宿舍通知箱(含我的回执状态与各分桶计数)。用户问“宿舍有什么通知”“我要回执”时用它。",
|
||||||
"GET", "/api/notice/MyNotices")
|
"GET", "/api/notice/MyNotices")
|
||||||
@@ -35,14 +59,6 @@ public final class DormNoticeTools {
|
|||||||
.p(ToolParam.required("serid", "integer", "通知 id"))
|
.p(ToolParam.required("serid", "integer", "通知 id"))
|
||||||
.readAt("/par/notices"));
|
.readAt("/par/notices"));
|
||||||
|
|
||||||
list.add(ToolDef.of("dorm.sendList", MODULE, "宿舍通知发布列表",
|
|
||||||
"查询已发布的宿舍通知(家长端收件箱 / 管理端全部)。",
|
|
||||||
"GET", "/api/notice/List")
|
|
||||||
.p(ToolParam.optional("pageIndex", "integer", "页码").dft("1"),
|
|
||||||
ToolParam.optional("pageSize", "integer", "每页条数").dft("10"),
|
|
||||||
ToolParam.optional("keyword", "string", "标题关键词"))
|
|
||||||
.readAt("/list"));
|
|
||||||
|
|
||||||
list.add(ToolDef.of("dorm.submitReceipt", MODULE, "提交宿舍通知回执",
|
list.add(ToolDef.of("dorm.submitReceipt", MODULE, "提交宿舍通知回执",
|
||||||
"家长提交某条宿舍通知的回执 / 填报。调用前须确认 serid;如该通知需填报,还需填 fillData 内容。",
|
"家长提交某条宿舍通知的回执 / 填报。调用前须确认 serid;如该通知需填报,还需填 fillData 内容。",
|
||||||
"POST_JSON", "/api/notice/SubmitReceipt")
|
"POST_JSON", "/api/notice/SubmitReceipt")
|
||||||
@@ -67,6 +83,198 @@ public final class DormNoticeTools {
|
|||||||
.label("宿舍通知"))
|
.label("宿舍通知"))
|
||||||
.write("/par/mine"));
|
.write("/par/mine"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.initializeRed", MODULE, "一键已读全部宿舍通知",
|
||||||
|
"把我收到的宿舍通知一次性全部标记为已读。",
|
||||||
|
"POST_FORM", "/api/notice/InitializeRed")
|
||||||
|
.write("/par/notices"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 发布闭环(教职工) ==================== */
|
||||||
|
|
||||||
|
private static void addPublishLoop(List<ToolDef> list) {
|
||||||
|
list.add(ToolDef.of("dorm.publish", MODULE, "发布宿舍通知",
|
||||||
|
"发布一条宿舍通知(可要求家长回执 / 填报),默认发给**全部宿舍**的家长。"
|
||||||
|
+ "调用前必须确认:标题 title、正文 content、通知类型 noticeType、发文部门 deptId;"
|
||||||
|
+ "缺失就直接调用本工具,系统会弹出表单让用户补齐,不要用文字反复追问;"
|
||||||
|
+ "通知类型与发文部门必须来自字典,不要编造。默认立即发布;如需先存草稿,把 draft 置 true。",
|
||||||
|
"POST_JSON", "/api/notice/Create")
|
||||||
|
.p(ToolParam.required("title", "string", "通知标题")
|
||||||
|
.label("通知标题").eg("关于宿舍熄灯时间调整的通知"),
|
||||||
|
ToolParam.required("content", "string", "通知正文(纯文本)")
|
||||||
|
.label("通知正文").as("textarea").eg("各位家长:自本周起,宿舍熄灯时间调整为 22:30……"),
|
||||||
|
ToolParam.required("noticeType", "string", "通知类型,来自 dorm.noticeTypeDic 的 k 字段")
|
||||||
|
.label("通知类型")
|
||||||
|
.optionsFrom("dorm.noticeTypeDic").optionsKeys("k", "v"),
|
||||||
|
ToolParam.required("deptId", "string", "发文部门 id,来自 dorm.deptDic 的 k 字段")
|
||||||
|
.label("发文部门")
|
||||||
|
.optionsFrom("dorm.deptDic").optionsKeys("k", "v"),
|
||||||
|
ToolParam.required("receiptMode", "string", "回执方式:none 无需回执 / confirm 确认回执 / fill 填报回执")
|
||||||
|
.label("回执方式").dft("none")
|
||||||
|
.options(new String[]{"none", "confirm", "fill"},
|
||||||
|
new String[]{"无需回执", "需要确认回执", "需要填报回执"}),
|
||||||
|
ToolParam.optional("deadlineTime", "string", "回执截止时间 yyyy-MM-dd HH:mm:ss(留空=不限)")
|
||||||
|
.label("回执截止时间").eg("2026-10-20 18:00:00"),
|
||||||
|
ToolParam.optional("needSupervisor", "integer", "是否需要宿管到场配合执行")
|
||||||
|
.label("宿管到场配合").dft("0")
|
||||||
|
.options(new String[]{"0", "1"}, new String[]{"否", "是"}),
|
||||||
|
ToolParam.optional("smsFlag", "integer", "是否同时发短信提醒家长")
|
||||||
|
.label("同时发短信").dft("0")
|
||||||
|
.options(new String[]{"0", "1"}, new String[]{"否", "是"}),
|
||||||
|
ToolParam.optional("topmost", "integer", "是否置顶")
|
||||||
|
.label("置顶").dft("0")
|
||||||
|
.options(new String[]{"0", "1"}, new String[]{"否", "是"}),
|
||||||
|
ToolParam.optional("draft", "string", "是否存草稿:true 只保存不发布")
|
||||||
|
.label("存草稿").dft("false")
|
||||||
|
.options(new String[]{"false", "true"}, new String[]{"立即发布", "存草稿"}))
|
||||||
|
// 接收对象:默认全部宿舍(房间级定向请到模块页面里勾选)
|
||||||
|
.fix("receiveType", "room")
|
||||||
|
.fix("receiveList", "ALL")
|
||||||
|
.write("/list/new"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.editNotice", MODULE, "编辑宿舍通知",
|
||||||
|
"修改一条已发布(或草稿)的宿舍通知。调用前须确认 sourceId(要改的那条通知 id)。",
|
||||||
|
"POST_JSON", "/api/notice/Edit")
|
||||||
|
.p(ToolParam.required("sourceId", "integer", "要编辑的通知 id")
|
||||||
|
.label("通知"),
|
||||||
|
ToolParam.required("title", "string", "通知标题")
|
||||||
|
.label("通知标题"),
|
||||||
|
ToolParam.required("content", "string", "通知正文")
|
||||||
|
.label("通知正文").as("textarea"))
|
||||||
|
.fix("receiveType", "room")
|
||||||
|
.fix("receiveList", "ALL")
|
||||||
|
.write("/list"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.sendList", MODULE, "我发布的宿舍通知",
|
||||||
|
"查询我发布的宿舍通知列表(可按关键词 / 状态筛选)。用户问“我发过哪些宿舍通知”“那条通知发了没”时用它。",
|
||||||
|
"GET", "/api/notice/SendList")
|
||||||
|
.p(ToolParam.optional("pageIndex", "integer", "页码").dft("1"),
|
||||||
|
ToolParam.optional("pageSize", "integer", "每页条数").dft("10"),
|
||||||
|
ToolParam.optional("scope", "string", "传 user 只看自己发布的"),
|
||||||
|
ToolParam.optional("keyword", "string", "标题关键词"))
|
||||||
|
.readAt("/list"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.readDetail", MODULE, "宿舍通知阅读情况",
|
||||||
|
"查询某条宿舍通知的阅读情况(已读 / 未读分组,按楼栋·房间)。",
|
||||||
|
"GET", "/api/notice/ReadDetail")
|
||||||
|
.p(ToolParam.required("serid", "integer", "通知 id"))
|
||||||
|
.readAt("/receipts"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.pull", MODULE, "撤回宿舍通知",
|
||||||
|
"撤回一条已发布的宿舍通知(撤回后家长端不可见)。",
|
||||||
|
"POST_FORM", "/api/notice/UserPull")
|
||||||
|
.p(ToolParam.required("serid", "integer", "通知 id")
|
||||||
|
.label("宿舍通知"))
|
||||||
|
.write("/list"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.top", MODULE, "置顶 / 取消置顶宿舍通知",
|
||||||
|
"把一条宿舍通知置顶或取消置顶。",
|
||||||
|
"POST_FORM", "/api/notice/SetTopmost")
|
||||||
|
.p(ToolParam.required("serid", "integer", "通知 id")
|
||||||
|
.label("宿舍通知"),
|
||||||
|
ToolParam.optional("topmost", "integer", "1 置顶(默认) / 0 取消置顶")
|
||||||
|
.label("操作").dft("1")
|
||||||
|
.options(new String[]{"1", "0"}, new String[]{"置顶", "取消置顶"}))
|
||||||
|
.write("/list"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.expedite", MODULE, "催办未读(宿舍通知)",
|
||||||
|
"对一条宿舍通知的未读人员统一催办(短信 / 站内提醒)。",
|
||||||
|
"POST_FORM", "/api/notice/Expedite")
|
||||||
|
.p(ToolParam.required("serid", "integer", "通知 id")
|
||||||
|
.label("宿舍通知"))
|
||||||
|
.write("/receipts"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.remindStudent", MODULE, "向某学生家长催办(宿舍通知)",
|
||||||
|
"针对某条宿舍通知,单独向某个学生的家长发送提醒。",
|
||||||
|
"POST_FORM", "/api/notice/RemindStudent")
|
||||||
|
.p(ToolParam.required("serid", "integer", "通知 id")
|
||||||
|
.label("宿舍通知"),
|
||||||
|
ToolParam.required("studentId", "integer", "学生 id")
|
||||||
|
.label("学生"))
|
||||||
|
.write("/receipts"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.markRead", MODULE, "代标记某学生已读(宿舍通知)",
|
||||||
|
"管理员代为把某条宿舍通知标记为某学生已读。",
|
||||||
|
"POST_FORM", "/api/notice/MarkRead")
|
||||||
|
.p(ToolParam.required("serid", "integer", "通知 id")
|
||||||
|
.label("宿舍通知"),
|
||||||
|
ToolParam.required("studentId", "integer", "学生 id")
|
||||||
|
.label("学生"))
|
||||||
|
.write("/receipts"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.statistics", MODULE, "宿舍通知统计",
|
||||||
|
"查询宿舍通知的发送 / 阅读 / 回执统计(可按时间段)。",
|
||||||
|
"GET", "/api/notice/Statis")
|
||||||
|
.p(ToolParam.optional("startTime", "string", "开始时间 yyyy-MM-dd").eg("2026-10-01"),
|
||||||
|
ToolParam.optional("endTime", "string", "结束时间 yyyy-MM-dd").eg("2026-10-31"))
|
||||||
|
.readAt("/stats"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 回执闭环(教职工) ==================== */
|
||||||
|
|
||||||
|
private static void addReceiptLoop(List<ToolDef> list) {
|
||||||
|
list.add(ToolDef.of("dorm.receipts", MODULE, "宿舍通知回执名单",
|
||||||
|
"查询某条宿舍通知的回执 / 填报名单(按楼栋·楼层分组 + 汇总)。"
|
||||||
|
+ "用户问“还有谁没回执”“回执情况怎么样”时用它。",
|
||||||
|
"GET", "/api/notice/Receipts")
|
||||||
|
.p(ToolParam.required("serid", "integer", "通知 id")
|
||||||
|
.label("宿舍通知"),
|
||||||
|
ToolParam.optional("status", "string", "筛选:all 全部 / confirmed 已确认 / filled 已填报 / canceled 已取消 / none 未回执")
|
||||||
|
.dft("all"))
|
||||||
|
.readAt("/receipts"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.receiptDetail", MODULE, "单条回执详情",
|
||||||
|
"查看某条回执的明细(家长填写的确认 / 填报内容)。",
|
||||||
|
"GET", "/api/notice/ReceiptDetail")
|
||||||
|
.p(ToolParam.required("receiptId", "integer", "回执 id"))
|
||||||
|
.readAt("/receipts"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.reviewReceipt", MODULE, "处理宿舍通知回执",
|
||||||
|
"对一条回执执行确认 / 取消 / 重置操作。",
|
||||||
|
"POST_JSON", "/api/notice/Review")
|
||||||
|
.p(ToolParam.required("receiptId", "integer", "回执 id")
|
||||||
|
.label("回执"),
|
||||||
|
ToolParam.required("action", "string", "处理动作:confirm 通过确认 / cancel 取消 / reset 重置为待处理")
|
||||||
|
.label("处理动作")
|
||||||
|
.options(new String[]{"confirm", "cancel", "reset"},
|
||||||
|
new String[]{"通过确认", "取消", "重置为待处理"}))
|
||||||
|
.write("/receipts"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.remind", MODULE, "催回执(宿舍通知)",
|
||||||
|
"向未回执的家长发送回执提醒。",
|
||||||
|
"POST_FORM", "/api/notice/Remind")
|
||||||
|
.p(ToolParam.required("serid", "integer", "通知 id")
|
||||||
|
.label("宿舍通知"))
|
||||||
|
.write("/receipts"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 字典 / 模板(发布表单下拉的来源) ==================== */
|
||||||
|
|
||||||
|
private static void addDictionaries(List<ToolDef> list) {
|
||||||
|
list.add(ToolDef.of("dorm.noticeTypeDic", MODULE, "宿舍通知类型字典",
|
||||||
|
"查询本校可选的宿舍通知类型(发布宿舍通知时填 noticeType 用)。"
|
||||||
|
+ "仅在用户明确问「有哪些通知类型」时使用;"
|
||||||
|
+ "发布宿舍通知时**不要**先调它 —— 表单的「通知类型」下拉会自动带出可选值。",
|
||||||
|
"GET", "/api/dict/List")
|
||||||
|
.fix("type", DICT_TYPE_NOTICE)
|
||||||
|
.readAt("/list/new"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.deptDic", MODULE, "宿舍通知发文部门字典",
|
||||||
|
"查询本校可选的发文部门(发布宿舍通知时填 deptId 用)。"
|
||||||
|
+ "仅在用户明确问「有哪些发文部门」时使用;"
|
||||||
|
+ "发布宿舍通知时**不要**先调它 —— 表单的「发文部门」下拉会自动带出可选值。",
|
||||||
|
"GET", "/api/dict/List")
|
||||||
|
.fix("type", DICT_TYPE_DEPT)
|
||||||
|
.readAt("/list/new"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.formTemplates", MODULE, "回执单据模板",
|
||||||
|
"查询「需填报」类宿舍通知可用的单据模板(模板字段由后端预置,发布人只选类型)。",
|
||||||
|
"GET", "/api/notice/FormTemplates")
|
||||||
|
.readAt("/list/new"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 宿舍床位台账(宿管日常) ==================== */
|
||||||
|
|
||||||
|
private static void addDormLedger(List<ToolDef> list) {
|
||||||
list.add(ToolDef.of("dorm.buildings", MODULE, "宿舍楼栋列表",
|
list.add(ToolDef.of("dorm.buildings", MODULE, "宿舍楼栋列表",
|
||||||
"查询宿舍楼栋列表(含房间数/床位数/入住数)。",
|
"查询宿舍楼栋列表(含房间数/床位数/入住数)。",
|
||||||
"GET", "/api/dorm/BuildingList")
|
"GET", "/api/dorm/BuildingList")
|
||||||
@@ -86,10 +294,104 @@ public final class DormNoticeTools {
|
|||||||
.readAt("/setting"));
|
.readAt("/setting"));
|
||||||
|
|
||||||
list.add(ToolDef.of("dorm.dormTree", MODULE, "宿舍楼栋房间树",
|
list.add(ToolDef.of("dorm.dormTree", MODULE, "宿舍楼栋房间树",
|
||||||
"楼栋 → 房间 的完整树(发布宿舍通知时选接收房间用)。",
|
"楼栋 → 房间 的完整树(发布宿舍通知、选择房间时用)。",
|
||||||
"GET", "/api/base/GetDormTree")
|
"GET", "/api/base/GetDormTree")
|
||||||
.readAt("/list/new"));
|
.readAt("/list/new"));
|
||||||
|
|
||||||
return list;
|
list.add(ToolDef.of("dorm.pendingStudents", MODULE, "待安排床位学生池",
|
||||||
|
"查询尚未安排床位的学生名单(「安排入住」的数据源)。",
|
||||||
|
"GET", "/api/dorm/PendingStudents")
|
||||||
|
.p(ToolParam.optional("keyword", "string", "姓名关键词"),
|
||||||
|
ToolParam.optional("gender", "string", "性别筛选"))
|
||||||
|
.readAt("/setting"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.assign", MODULE, "安排入住(分配床位)",
|
||||||
|
"把待安排的学生调入某个房间(按空床位顺序自动落位)。"
|
||||||
|
+ "调用前须确认 roomId 与学生 id(可多个,逗号分隔)。",
|
||||||
|
"POST_FORM", "/api/dorm/AssignStudent")
|
||||||
|
.p(ToolParam.required("roomId", "integer", "目标房间 id")
|
||||||
|
.label("房间"),
|
||||||
|
ToolParam.required("studentIds", "string", "学生 id,多个用英文逗号分隔")
|
||||||
|
.label("学生"))
|
||||||
|
.write("/setting"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.removeStudent", MODULE, "退宿(移出房间)",
|
||||||
|
"把某学生移出所在房间,退回待安排池。",
|
||||||
|
"POST_FORM", "/api/dorm/RemoveStudent")
|
||||||
|
.p(ToolParam.required("roomId", "integer", "房间 id")
|
||||||
|
.label("房间"),
|
||||||
|
ToolParam.required("studentId", "integer", "学生 id")
|
||||||
|
.label("学生"))
|
||||||
|
.write("/setting"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.transfer", MODULE, "调宿(换房间)",
|
||||||
|
"把房间内某学生转到另一间房。",
|
||||||
|
"POST_FORM", "/api/dorm/TransferStudent")
|
||||||
|
.p(ToolParam.required("studentId", "integer", "学生 id")
|
||||||
|
.label("学生"),
|
||||||
|
ToolParam.required("fromRoomId", "integer", "原房间 id")
|
||||||
|
.label("原房间"),
|
||||||
|
ToolParam.required("toRoomId", "integer", "目标房间 id")
|
||||||
|
.label("目标房间"))
|
||||||
|
.write("/setting"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.setMonitor", MODULE, "设置 / 取消宿舍长",
|
||||||
|
"把某房间内的一名学生设为宿舍长;studentId 传 0 表示取消宿舍长。",
|
||||||
|
"POST_FORM", "/api/dorm/SetMonitor")
|
||||||
|
.p(ToolParam.required("roomId", "integer", "房间 id")
|
||||||
|
.label("房间"),
|
||||||
|
ToolParam.required("studentId", "integer", "学生 id(传 0 取消宿舍长)")
|
||||||
|
.label("学生"))
|
||||||
|
.write("/setting"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.addBuilding", MODULE, "新增宿舍楼栋",
|
||||||
|
"新增一栋宿舍楼。",
|
||||||
|
"POST_FORM", "/api/dorm/AddBuilding")
|
||||||
|
.p(ToolParam.required("name", "string", "楼栋名称")
|
||||||
|
.label("楼栋名称").eg("1 号楼"),
|
||||||
|
ToolParam.optional("floors", "integer", "楼层数")
|
||||||
|
.label("楼层数"),
|
||||||
|
ToolParam.optional("remark", "string", "备注")
|
||||||
|
.label("备注").as("textarea"))
|
||||||
|
.write("/setting"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.editBuilding", MODULE, "编辑宿舍楼栋",
|
||||||
|
"修改宿舍楼栋信息(名称 / 楼层数 / 备注)。",
|
||||||
|
"POST_FORM", "/api/dorm/EditBuilding")
|
||||||
|
.p(ToolParam.required("buildingId", "integer", "楼栋 id")
|
||||||
|
.label("楼栋"),
|
||||||
|
ToolParam.required("name", "string", "楼栋名称")
|
||||||
|
.label("楼栋名称"),
|
||||||
|
ToolParam.optional("floors", "integer", "楼层数")
|
||||||
|
.label("楼层数"),
|
||||||
|
ToolParam.optional("remark", "string", "备注")
|
||||||
|
.label("备注").as("textarea"))
|
||||||
|
.write("/setting"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.addRoom", MODULE, "新增宿舍房间",
|
||||||
|
"在某楼栋下新增房间(可按 roomCount 批量生成连续房间号)。",
|
||||||
|
"POST_FORM", "/api/dorm/AddRoom")
|
||||||
|
.p(ToolParam.required("buildingId", "integer", "所属楼栋 id")
|
||||||
|
.label("楼栋"),
|
||||||
|
ToolParam.required("roomNo", "string", "房间号")
|
||||||
|
.label("房间号").eg("301"),
|
||||||
|
ToolParam.optional("bedCount", "integer", "床位数")
|
||||||
|
.label("床位数").dft("6"),
|
||||||
|
ToolParam.optional("roomCount", "integer", "批量生成房间数量(留空=只建一间)")
|
||||||
|
.label("批量数量"))
|
||||||
|
.write("/setting"));
|
||||||
|
|
||||||
|
list.add(ToolDef.of("dorm.editRoom", MODULE, "编辑宿舍房间",
|
||||||
|
"修改房间信息(房间号 / 床位数 / 宿管)。",
|
||||||
|
"POST_FORM", "/api/dorm/EditRoom")
|
||||||
|
.p(ToolParam.required("roomId", "integer", "房间 id")
|
||||||
|
.label("房间"),
|
||||||
|
ToolParam.optional("roomNo", "string", "房间号")
|
||||||
|
.label("房间号"),
|
||||||
|
ToolParam.optional("bedCount", "integer", "床位数")
|
||||||
|
.label("床位数"),
|
||||||
|
ToolParam.optional("supervisor", "string", "宿管姓名")
|
||||||
|
.label("宿管"))
|
||||||
|
.write("/setting"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
package com.yunhai.agent;
|
||||||
|
|
||||||
|
import com.yunhai.agent.config.AgentSettings;
|
||||||
|
import com.yunhai.agent.model.BrowserToolRequest;
|
||||||
|
import com.yunhai.agent.service.IntentShortcut;
|
||||||
|
import com.yunhai.agent.tool.ToolDef;
|
||||||
|
import com.yunhai.agent.tool.ToolExecutor;
|
||||||
|
import com.yunhai.agent.tool.ToolParam;
|
||||||
|
import com.yunhai.agent.tool.ToolRegistry;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 「发布宿舍通知」「发布家校活动」两条链路的断言:裸意图识别 + 发布工具的真实契约 + 字典下拉的代取地址。
|
||||||
|
*
|
||||||
|
* <p>刻意的约束:
|
||||||
|
* <ol>
|
||||||
|
* <li><b>必须带模块限定</b>:宿舍通知只认「宿舍」字样、活动只认「活动」字样。
|
||||||
|
* 本轮起「发通知 / 发布通知」这类不带限定的说法<b>不再直达</b> —— 校园公告与宿舍通知
|
||||||
|
* 都有发布工具,同一句「发通知」是跨模块歧义(猜错模块比不直达更糟),交给模型按上下文选。</li>
|
||||||
|
* <li><b>仅教职工</b>:两个发布工具都归 STAFF_ONLY,学生身份没有落点 → 落回模型如实说明。</li>
|
||||||
|
* <li><b>接收对象固定为「全部」</b>:{@code receiveType}/{@code receiveList} 由 {@code fix} 写死,
|
||||||
|
* 房间/班级级定向请到模块页面里勾选,避免表单里出现难用的 id 输入框。</li>
|
||||||
|
* </ol>
|
||||||
|
*/
|
||||||
|
class DormActivityPublishIntentTest {
|
||||||
|
|
||||||
|
private IntentShortcut shortcut;
|
||||||
|
private ToolRegistry registry;
|
||||||
|
private AgentSettings settings;
|
||||||
|
private ToolExecutor executor;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
shortcut = new IntentShortcut();
|
||||||
|
settings = new AgentSettings();
|
||||||
|
settings.getModules().put("dormnotice", "http://base.yunhaixingtong.com/dormnotice");
|
||||||
|
settings.getModules().put("familyactivity", "http://base.yunhaixingtong.com/familyactivity");
|
||||||
|
registry = new ToolRegistry(settings);
|
||||||
|
registry.init();
|
||||||
|
executor = new ToolExecutor(settings, new RestTemplate());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 裸意图规则 ==================== */
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bareDormPublishIntentIsRecognized() {
|
||||||
|
for (String text : Arrays.asList(
|
||||||
|
"发布宿舍通知", "我要发布宿舍通知", "发宿舍通知", "发个宿舍通知",
|
||||||
|
"帮我发一条宿舍通知", "宿舍通知发布", "发布宿舍公告")) {
|
||||||
|
IntentShortcut.Intent it = shortcut.match(text);
|
||||||
|
assertNotNull(it, "「" + text + "」应被识别为发布宿舍通知意图");
|
||||||
|
assertEquals("dorm.publish", it.getStaffTool(), "「" + text + "」应落到发布宿舍通知工具");
|
||||||
|
assertNull(it.getStudentTool(), "学生没有发布宿舍通知能力");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bareActivityPublishIntentIsRecognized() {
|
||||||
|
for (String text : Arrays.asList(
|
||||||
|
"发布活动", "我要发布活动", "发活动", "发个活动",
|
||||||
|
"发布家校活动", "帮我发布一条活动", "活动发布", "发布亲子活动")) {
|
||||||
|
IntentShortcut.Intent it = shortcut.match(text);
|
||||||
|
assertNotNull(it, "「" + text + "」应被识别为发布活动意图");
|
||||||
|
assertEquals("activity.publish", it.getStaffTool(), "「" + text + "」应落到发布活动工具");
|
||||||
|
assertNull(it.getStudentTool(), "学生没有发布活动能力");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void genericSendNoticeIsNoLongerHijacked() {
|
||||||
|
// 「发通知」不带模块限定 → 校园公告与宿舍通知都成立,属跨模块歧义,交给模型按上下文选
|
||||||
|
for (String text : Arrays.asList(
|
||||||
|
"发通知", "发布通知", "发个通知", "发一条通知")) {
|
||||||
|
assertNull(shortcut.match(text),
|
||||||
|
"「" + text + "」跨模块歧义(公告 / 宿舍通知),不应被直达规则命中");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readingAndSignupAreNotHijacked() {
|
||||||
|
for (String text : Arrays.asList(
|
||||||
|
"宿舍有什么通知", "我有哪些未读的宿舍通知", "这周有什么活动",
|
||||||
|
"看看活动", "报名活动", "我要报名活动", "活动报名情况",
|
||||||
|
"发一条关于宿舍卫生检查的通知,本周五下午查")) {
|
||||||
|
assertNull(shortcut.match(text), "「" + text + "」不是发布意图,不应被直达规则命中");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 三个发布意图不能互相串味 */
|
||||||
|
@Test
|
||||||
|
void publishRulesDoNotOverlap() {
|
||||||
|
assertEquals("notice.publish", shortcut.match("我要发布校园公告").getStaffTool());
|
||||||
|
assertEquals("dorm.publish", shortcut.match("我要发布宿舍通知").getStaffTool());
|
||||||
|
assertEquals("activity.publish", shortcut.match("我要发布活动").getStaffTool());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 宿舍通知发布工具契约 ==================== */
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dormPublishToolContract() {
|
||||||
|
ToolDef publish = registry.get("dorm.publish");
|
||||||
|
assertNotNull(publish);
|
||||||
|
assertFalse(publish.isReadOnly(), "发布宿舍通知是写操作");
|
||||||
|
assertEquals("/api/notice/Create", publish.getPath());
|
||||||
|
|
||||||
|
List<String> required = requiredNames(publish);
|
||||||
|
assertTrue(required.contains("title"));
|
||||||
|
assertTrue(required.contains("content"));
|
||||||
|
assertTrue(required.contains("noticeType"), "通知类型是发布表单必填项");
|
||||||
|
assertTrue(required.contains("deptId"), "发文部门是发布表单必填项");
|
||||||
|
|
||||||
|
// 接收对象由 fix 写死(避免表单里出现房间 id 输入框)
|
||||||
|
assertEquals("room", publish.getFixed().get("receiveType"));
|
||||||
|
assertEquals("ALL", publish.getFixed().get("receiveList"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dormDictsUsePlatformKeyShape() {
|
||||||
|
ToolParam noticeType = param(registry.get("dorm.publish"), "noticeType");
|
||||||
|
assertEquals("dorm.noticeTypeDic", noticeType.getOptionsFrom());
|
||||||
|
assertEquals("k", noticeType.getOptionValueKey(), "/api/dict/List 返回 {k,v},必须按键取值");
|
||||||
|
assertEquals("v", noticeType.getOptionLabelKey());
|
||||||
|
|
||||||
|
ToolParam deptId = param(registry.get("dorm.publish"), "deptId");
|
||||||
|
assertEquals("dorm.deptDic", deptId.getOptionsFrom());
|
||||||
|
assertEquals("k", deptId.getOptionValueKey());
|
||||||
|
assertEquals("v", deptId.getOptionLabelKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dormDictOptionCallCarriesFixedTypeParam() {
|
||||||
|
BrowserToolRequest.BrowserCall noticeTypeCall =
|
||||||
|
executor.prepareOptionCall(registry.get("dorm.noticeTypeDic"), null);
|
||||||
|
assertNotNull(noticeTypeCall);
|
||||||
|
assertEquals("/dormnotice/api/dict/List?type=noticeType", noticeTypeCall.getPathAndQuery());
|
||||||
|
|
||||||
|
BrowserToolRequest.BrowserCall deptCall =
|
||||||
|
executor.prepareOptionCall(registry.get("dorm.deptDic"), null);
|
||||||
|
assertNotNull(deptCall);
|
||||||
|
assertEquals("/dormnotice/api/dict/List?type=dept", deptCall.getPathAndQuery());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 家校活动发布工具契约 ==================== */
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void activityPublishToolContract() {
|
||||||
|
ToolDef publish = registry.get("activity.publish");
|
||||||
|
assertNotNull(publish);
|
||||||
|
assertFalse(publish.isReadOnly(), "发布活动是写操作");
|
||||||
|
assertEquals("/api/activity/Create", publish.getPath());
|
||||||
|
|
||||||
|
List<String> required = requiredNames(publish);
|
||||||
|
for (String k : Arrays.asList("title", "content", "category", "deptId", "venue", "startTime", "endTime")) {
|
||||||
|
assertTrue(required.contains(k), "发布活动表单缺少必填项:" + k);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("class", publish.getFixed().get("receiveType"));
|
||||||
|
assertEquals("ALL", publish.getFixed().get("receiveList"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void activityDictOptionCallsUseExpectedTypeIds() {
|
||||||
|
BrowserToolRequest.BrowserCall dept =
|
||||||
|
executor.prepareOptionCall(registry.get("activity.deptDic"), null);
|
||||||
|
assertNotNull(dept);
|
||||||
|
assertEquals("/familyactivity/api/dict/List?type=1", dept.getPathAndQuery(),
|
||||||
|
"主办部门字典是 type=1");
|
||||||
|
|
||||||
|
BrowserToolRequest.BrowserCall category =
|
||||||
|
executor.prepareOptionCall(registry.get("activity.categoryDic"), null);
|
||||||
|
assertNotNull(category);
|
||||||
|
assertEquals("/familyactivity/api/dict/List?type=2", category.getPathAndQuery(),
|
||||||
|
"活动分类字典是 type=2");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 辅助 ==================== */
|
||||||
|
|
||||||
|
private List<String> requiredNames(ToolDef def) {
|
||||||
|
List<String> names = new ArrayList<>();
|
||||||
|
for (ToolParam p : def.getParams()) {
|
||||||
|
if (p.isRequired()) {
|
||||||
|
names.add(p.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ToolParam param(ToolDef def, String name) {
|
||||||
|
for (ToolParam p : def.getParams()) {
|
||||||
|
if (name.equals(p.getName())) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AssertionError("工具 " + def.getName() + " 缺少参数 " + name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,6 +77,8 @@ class IntentDirectFormTest {
|
|||||||
settings = new AgentSettings();
|
settings = new AgentSettings();
|
||||||
settings.getModules().put("homeleave", "http://base.yunhaixingtong.com/homeleave");
|
settings.getModules().put("homeleave", "http://base.yunhaixingtong.com/homeleave");
|
||||||
settings.getModules().put("campusnotice", "http://base.yunhaixingtong.com/campusnotice");
|
settings.getModules().put("campusnotice", "http://base.yunhaixingtong.com/campusnotice");
|
||||||
|
settings.getModules().put("dormnotice", "http://base.yunhaixingtong.com/dormnotice");
|
||||||
|
settings.getModules().put("familyactivity", "http://base.yunhaixingtong.com/familyactivity");
|
||||||
registry = new ToolRegistry(settings);
|
registry = new ToolRegistry(settings);
|
||||||
registry.init();
|
registry.init();
|
||||||
|
|
||||||
@@ -206,6 +208,79 @@ class IntentDirectFormTest {
|
|||||||
assertEquals("(模型回复)", resp.getReply());
|
assertEquals("(模型回复)", resp.getReply());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==================== 教师:发布宿舍通知 ==================== */
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void teacherSayingPublishDormNoticeGetsPublishFormWithoutCallingLlm() {
|
||||||
|
ChatResponse resp = chat(EXPLODING_LLM, "我要发布宿舍通知", caller(AgentRole.TEACHER));
|
||||||
|
|
||||||
|
assertNotNull(resp.getForm(), "应直接下发表单");
|
||||||
|
assertEquals("dorm.publish", resp.getForm().getTool());
|
||||||
|
assertTrue(resp.getForm().isWrite(), "是写操作,前端要二次确认");
|
||||||
|
assertEquals("/list/new", resp.getForm().getLandingPath());
|
||||||
|
|
||||||
|
// 标题 + 正文 + 通知类型(下拉)+ 发文部门(下拉)
|
||||||
|
assertEquals("text", field(resp, "title").getWidget());
|
||||||
|
assertEquals("textarea", field(resp, "content").getWidget());
|
||||||
|
|
||||||
|
FormField noticeType = field(resp, "noticeType");
|
||||||
|
assertEquals("select", noticeType.getWidget(), "通知类型应是下拉,不该让用户手填编号");
|
||||||
|
assertNotNull(noticeType.getOptionCall());
|
||||||
|
assertEquals("/dormnotice/api/dict/List?type=noticeType",
|
||||||
|
noticeType.getOptionCall().getPathAndQuery(), "字典地址必须带 type=noticeType");
|
||||||
|
|
||||||
|
FormField deptId = field(resp, "deptId");
|
||||||
|
assertEquals("select", deptId.getWidget());
|
||||||
|
assertNotNull(deptId.getOptionCall());
|
||||||
|
assertEquals("/dormnotice/api/dict/List?type=dept", deptId.getOptionCall().getPathAndQuery());
|
||||||
|
assertEquals("k", deptId.getValueKey());
|
||||||
|
assertEquals("v", deptId.getLabelKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void studentSayingPublishDormNoticeFallsThroughToModel() {
|
||||||
|
ChatResponse resp = chat(ECHO_LLM, "我要发布宿舍通知", caller(AgentRole.STUDENT));
|
||||||
|
assertNull(resp.getForm(), "学生不该拿到发布宿舍通知表单");
|
||||||
|
assertEquals("(模型回复)", resp.getReply());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 教师:发布家校活动 ==================== */
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void teacherSayingPublishActivityGetsPublishFormWithoutCallingLlm() {
|
||||||
|
ChatResponse resp = chat(EXPLODING_LLM, "我要发布活动", caller(AgentRole.TEACHER));
|
||||||
|
|
||||||
|
assertNotNull(resp.getForm(), "应直接下发表单");
|
||||||
|
assertEquals("activity.publish", resp.getForm().getTool());
|
||||||
|
assertTrue(resp.getForm().isWrite());
|
||||||
|
assertEquals("/list/new", resp.getForm().getLandingPath());
|
||||||
|
|
||||||
|
// 活动表单:标题 + 说明(多行)+ 分类(下拉)+ 主办部门(下拉)+ 地点 + 起止时间
|
||||||
|
assertEquals("text", field(resp, "title").getWidget());
|
||||||
|
assertEquals("textarea", field(resp, "content").getWidget());
|
||||||
|
|
||||||
|
FormField category = field(resp, "category");
|
||||||
|
assertEquals("select", category.getWidget());
|
||||||
|
assertNotNull(category.getOptionCall());
|
||||||
|
assertEquals("/familyactivity/api/dict/List?type=2", category.getOptionCall().getPathAndQuery());
|
||||||
|
|
||||||
|
FormField deptId = field(resp, "deptId");
|
||||||
|
assertEquals("select", deptId.getWidget());
|
||||||
|
assertNotNull(deptId.getOptionCall());
|
||||||
|
assertEquals("/familyactivity/api/dict/List?type=1", deptId.getOptionCall().getPathAndQuery());
|
||||||
|
|
||||||
|
assertNotNull(field(resp, "venue"));
|
||||||
|
assertNotNull(field(resp, "startTime"));
|
||||||
|
assertNotNull(field(resp, "endTime"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void studentSayingPublishActivityFallsThroughToModel() {
|
||||||
|
ChatResponse resp = chat(ECHO_LLM, "我要发布活动", caller(AgentRole.STUDENT));
|
||||||
|
assertNull(resp.getForm(), "学生不该拿到发布活动表单");
|
||||||
|
assertEquals("(模型回复)", resp.getReply());
|
||||||
|
}
|
||||||
|
|
||||||
/* ==================== 不该被劫持的情况 ==================== */
|
/* ==================== 不该被劫持的情况 ==================== */
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class NoticeIntentFormTest {
|
|||||||
for (String text : Arrays.asList(
|
for (String text : Arrays.asList(
|
||||||
"发布公告", "我要发布公告", "发布校园公告", "我要发布校园公告",
|
"发布公告", "我要发布公告", "发布校园公告", "我要发布校园公告",
|
||||||
"发公告", "发个公告", "帮我发公告", "我要发一条公告",
|
"发公告", "发个公告", "帮我发公告", "我要发一条公告",
|
||||||
"发布通知", "发通知", "发布校园通知", "麻烦发个通知吧")) {
|
"发布校园通知", "麻烦帮我发布校园通知")) {
|
||||||
IntentShortcut.Intent it = shortcut.match(text);
|
IntentShortcut.Intent it = shortcut.match(text);
|
||||||
assertNotNull(it, "「" + text + "」应被识别为裸发布公告意图");
|
assertNotNull(it, "「" + text + "」应被识别为裸发布公告意图");
|
||||||
assertEquals("notice.publish", it.getStaffTool(),
|
assertEquals("notice.publish", it.getStaffTool(),
|
||||||
@@ -66,6 +66,19 @@ class NoticeIntentFormTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2026-10 起宿舍通知也接上了发布工具 → 「发通知 / 发布通知」变成跨模块歧义,
|
||||||
|
* 已从公告短语集移除(改由模型按上下文选),这里把「不再劫持」固化下来。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void genericSendNoticeIsNotHijackedByCampusNotice() {
|
||||||
|
for (String text : Arrays.asList(
|
||||||
|
"发通知", "发布通知", "发个通知", "发一条通知")) {
|
||||||
|
assertNull(shortcut.match(text),
|
||||||
|
"「" + text + "」跨模块歧义(校园公告 / 宿舍通知),不应被公告直达规则命中");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void noticePublishHasNoStudentLanding() {
|
void noticePublishHasNoStudentLanding() {
|
||||||
IntentShortcut.Intent it = shortcut.match("发布公告");
|
IntentShortcut.Intent it = shortcut.match("发布公告");
|
||||||
|
|||||||
@@ -180,6 +180,10 @@ class RolePermissionTest {
|
|||||||
assertFalse(visible(visible, "notice.publish"), "学生不应看到 notice.publish");
|
assertFalse(visible(visible, "notice.publish"), "学生不应看到 notice.publish");
|
||||||
assertFalse(visible(visible, "profession.myPoints"), "学生不应看到 profession.*");
|
assertFalse(visible(visible, "profession.myPoints"), "学生不应看到 profession.*");
|
||||||
assertFalse(visible(visible, "dorm.buildings"), "学生不应看到宿舍管理侧");
|
assertFalse(visible(visible, "dorm.buildings"), "学生不应看到宿舍管理侧");
|
||||||
|
assertFalse(visible(visible, "dorm.publish"), "学生不应看到发布宿舍通知");
|
||||||
|
assertFalse(visible(visible, "dorm.assign"), "学生不应看到床位安排(宿管侧)");
|
||||||
|
assertFalse(visible(visible, "activity.publish"), "学生不应看到发布活动");
|
||||||
|
assertFalse(visible(visible, "activity.signups"), "学生不应看到活动报名名单(教师侧)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -202,6 +206,9 @@ class RolePermissionTest {
|
|||||||
assertFalse(visible(visible, "leave.submit"), "家长不应看到请假申请");
|
assertFalse(visible(visible, "leave.submit"), "家长不应看到请假申请");
|
||||||
assertFalse(visible(visible, "leave.audit"), "家长不应看到审批");
|
assertFalse(visible(visible, "leave.audit"), "家长不应看到审批");
|
||||||
assertFalse(visible(visible, "notice.publish"), "家长不应看到发布公告");
|
assertFalse(visible(visible, "notice.publish"), "家长不应看到发布公告");
|
||||||
|
assertFalse(visible(visible, "dorm.publish"), "家长不应看到发布宿舍通知");
|
||||||
|
assertFalse(visible(visible, "activity.publish"), "家长不应看到发布活动");
|
||||||
|
assertFalse(visible(visible, "activity.signups"), "家长不应看到活动报名名单(教师侧)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -216,6 +223,12 @@ class RolePermissionTest {
|
|||||||
assertTrue(visible(visible, "notice.publish"), "教师应可用 notice.publish");
|
assertTrue(visible(visible, "notice.publish"), "教师应可用 notice.publish");
|
||||||
assertTrue(visible(visible, "profession.myPoints"), "教师应可用 profession.*");
|
assertTrue(visible(visible, "profession.myPoints"), "教师应可用 profession.*");
|
||||||
assertTrue(visible(visible, "leave.stats"), "教师应可用 leave.stats");
|
assertTrue(visible(visible, "leave.stats"), "教师应可用 leave.stats");
|
||||||
|
// 教师能做:发布宿舍通知 / 盯回执 / 床位台账 / 发布活动 / 处理报名
|
||||||
|
assertTrue(visible(visible, "dorm.publish"), "教师应可用发布宿舍通知");
|
||||||
|
assertTrue(visible(visible, "dorm.receipts"), "教师应可用宿舍通知回执名单");
|
||||||
|
assertTrue(visible(visible, "dorm.assign"), "教师(宿管)应可用床位安排");
|
||||||
|
assertTrue(visible(visible, "activity.publish"), "教师应可用发布活动");
|
||||||
|
assertTrue(visible(visible, "activity.review"), "教师应可用处理活动报名");
|
||||||
// 教师不做:学生交作业、报名活动、学生自己的请假申请
|
// 教师不做:学生交作业、报名活动、学生自己的请假申请
|
||||||
assertFalse(visible(visible, "leave.submit"), "教师不应看到学生侧的「提交请假申请」");
|
assertFalse(visible(visible, "leave.submit"), "教师不应看到学生侧的「提交请假申请」");
|
||||||
assertFalse(visible(visible, "homework.submit"), "教师不应看到 homework.submit");
|
assertFalse(visible(visible, "homework.submit"), "教师不应看到 homework.submit");
|
||||||
|
|||||||
Reference in New Issue
Block a user