feat(intent): 「我要发布通知公告」裸意图直达(通知公告模块补齐)
build / build-push (push) Successful in 57s

通知公告(key=notice、前缀 /notice)此前只有 mock 里有裸意图分支,
真实后端 IntentShortcut 缺这一条,前后端行为不一致。

- IntentShortcut: 新增 NOTIFY_PUBLISH 意图 + NOTIFY_PUBLISH_BARE 短语集
  ★ 短语一律带「通知公告」全称限定 —— 它与「校园公告」(campusnotice)
    是两个独立应用(按用户下发 vs 按班级下发),光说「发通知/发布通知」
    两边都成立,猜错模块比不直达更糟;「发通知」继续保持不直达。
- 新增 NotifyIntentFormTest(7):规则识别 / 与校园公告的边界 /
  「发通知」不劫持 / 发布工具为写操作且仅教职工 / 代取地址带 /notice 前缀。
- IntentDirectFormTest:补「发布通知公告」端到端两条(教职工直出表单、
  学生落回模型),并断言表单里不出现校园公告独有字段。
- DormActivityPublishIntentTest:四个发布意图互不串味(补 notify)。

mvn -o test: 135/135 BUILD SUCCESS
This commit is contained in:
lijing
2026-10-14 12:40:46 +08:00
parent 9410599008
commit 8f0f8a04b3
4 changed files with 263 additions and 4 deletions
@@ -40,10 +40,11 @@ import static org.mockito.Mockito.when;
/**
* 裸意图在编排层的端到端断言:**同一句话、不同身份 → 不同表单**,且**不经过大模型**。
*
* <p>覆盖两条已落地的意图:
* <p>覆盖已落地的意图:
* <ul>
* <li>「我要请假」→ 学生拿请假申请、教职工拿请假登记(帮学生请假);</li>
* <li>「我要发布校园公告」→ 教职工拿发布表单,学生落回模型。</li>
* <li>「我要发布校园公告」→ 教职工拿发布表单,学生落回模型;</li>
* <li>「我要发布通知公告」→ 同上,但落到**另一个应用**(通知公告,按用户下发)。</li>
* </ul>
*
* <p>这是本次修复的核心承诺:用户说「我要请假」应当**直接**看到表单,
@@ -77,6 +78,7 @@ class IntentDirectFormTest {
settings = new AgentSettings();
settings.getModules().put("homeleave", "http://base.yunhaixingtong.com/homeleave");
settings.getModules().put("campusnotice", "http://base.yunhaixingtong.com/campusnotice");
settings.getModules().put("notice", "http://base.yunhaixingtong.com/notice");
settings.getModules().put("dormnotice", "http://base.yunhaixingtong.com/dormnotice");
settings.getModules().put("familyactivity", "http://base.yunhaixingtong.com/familyactivity");
settings.getModules().put("profession", "http://base.yunhaixingtong.com/profession");
@@ -209,6 +211,58 @@ class IntentDirectFormTest {
assertEquals("(模型回复)", resp.getReply());
}
/* ==================== 教师:发布通知(通知公告模块) ==================== */
/**
* 「通知公告」(key={@code notice})与「校园公告」({@code campusnotice})是**两个独立应用**:
* 前者按「用户」下发、后者按「班级」下发。同一句「我要发布通知公告」必须落到前者的发布表单,
* 且表单上**不能**出现只有校园公告才有的字段(推送范围 / 回执 / 发布方式)。
*/
@Test
void teacherSayingPublishNotifyGetsPublishFormWithoutCallingLlm() {
ChatResponse resp = chat(EXPLODING_LLM, "我要发布通知公告", caller(AgentRole.TEACHER));
assertNotNull(resp.getForm(), "应直接下发表单");
assertEquals("notify.publish", resp.getForm().getTool());
assertTrue(resp.getForm().isWrite(), "是写操作,前端要二次确认");
assertEquals("/list/new", resp.getForm().getLandingPath());
assertTrue(resp.getReply().contains("发布通知"), "引导语应说明这是发布通知,实际:" + resp.getReply());
// 标题(单行)+ 正文(多行)
assertEquals("text", field(resp, "title").getWidget());
assertEquals("textarea", field(resp, "content").getWidget());
// 来源部门:下拉 + 代取地址(带上模块前缀与固定字典种类)
FormField deptId = field(resp, "deptId");
assertEquals("select", deptId.getWidget(), "来源部门应是下拉,不该让用户手填部门编号");
assertNotNull(deptId.getOptionCall(), "来源部门下拉应带上「代取地址」");
assertEquals("/notice/api/dict/List?type=1", deptId.getOptionCall().getPathAndQuery(),
"字典地址必须带模块前缀 /notice 与 type=1,否则取不回来源部门");
assertEquals("k", deptId.getValueKey());
assertEquals("v", deptId.getLabelKey());
// 通知对象:按用户多选(与校园公告的「推送范围」按班级完全不同)
FormField receiveList = field(resp, "receiveList");
assertEquals("multiselect", receiveList.getWidget());
assertNotNull(receiveList.getOptionCall(), "通知对象应带上组织树代取地址");
assertEquals("/notice/api/base/GetSchoolAll", receiveList.getOptionCall().getPathAndQuery());
// 校园公告独有的字段绝不能串到通知公告的表单上
for (String foreign : new String[]{"pushScopes", "publishType", "receiptFlag", "topmost", "type"}) {
for (FormField f : resp.getForm().getFields()) {
assertFalse(foreign.equals(f.getName()),
"通知公告表单不该出现校园公告字段:" + foreign);
}
}
}
@Test
void studentSayingPublishNotifyFallsThroughToModel() {
ChatResponse resp = chat(ECHO_LLM, "我要发布通知公告", caller(AgentRole.STUDENT));
assertNull(resp.getForm(), "学生不该拿到发布通知表单");
assertEquals("(模型回复)", resp.getReply());
}
/* ==================== 教师:发布宿舍通知 ==================== */
@Test