Files
yunhai-agent-service/src/main/java/com/yunhai/agent/tool/defs/NoticeTools.java
T
lijing 9410599008
build / build-push (push) Successful in 1m7s
feat(通知公告): 接入 NoticeFront(模块 notice、工具前缀 notify.*)
★「通知公告」与既有「校园公告」是两个应用,不是同一个:
  · 校园公告 CampusAnnouncement → key=campusnotice、前缀 /campusnotice、按班级下发、带审批流/草稿箱(既有 notice.*)
  · 通知公告 NoticeFront       → key=notice、前缀 /notice、按用户下发,无审批/草稿/推送范围(本次新增 notify.*)
  两者路由与页面名几乎同名(/list、/list/new、已收到/已发送),极易拿错源码,故工具前缀避开已占用的 notice.*。

新增 NoticeTools(9 个工具):收件箱/详情/一键已读、发布/编辑/我发出的/阅读情况、发文部门字典、组织树。
发布表单逐项对齐 NoticeFront/src/views/notice/NewNotice.vue#doAdd:
  title / content / deptId / receiveList / fileIds / timingSendTime
(smsFlag 在页面里没有对应控件、恒为 0,故不放出;receiveList 的「全部用户」哨兵是 all。)

配套:ToolRegistry 注册、RolePolicy 把发布闭环与两个字典工具列为教职工专属、
application.yml 补地址 http://base.yunhaixingtong.com/notice 与默认落地页 /
(AgentSettings 出厂默认表也要补 notice —— 契约测试用的是 new AgentSettings(),不读 yml)。

fix(ToolExecutor): 已按 joinSeparator 拼成逗号串的字段不再被包回数组
  normalizeJsonArgs 的第二个循环「数组参数给了标量 → 包成单元素数组」会作用到
  刚拼好的 receiveList/fileIds 上,把 101,102 变成 [101,102],
  而模块侧 NoticeRequest.receiveList 是 String ⇒ 直接判非法。
  此前一直没暴露:本地 mock 的提交体是原样 JSON.stringify(args)(本来就是数组),
  看不出差异,只有真实后端才走到这一步。现在 join 过的字段直接跳过该循环。

测试:ModuleFormParityTest 新增「通知公告发布字段集不与校园公告互串」+
「代取地址带 /notice 前缀」+「提交体是逗号串、哨兵互斥、提交路径带前缀」;全量 126/126 绿。
2026-10-14 12:27:31 +08:00

189 lines
12 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.yunhai.agent.tool.defs;
import com.yunhai.agent.tool.ToolDef;
import com.yunhai.agent.tool.ToolParam;
import java.util.ArrayList;
import java.util.List;
/**
* 通知公告模块工具 —— 后端 NoticeFront,线上部署前缀 /notice,接口前缀 /api/notice、/api/dict、/api/base。
*
* <p>★ 与同名的「校园公告」({@link CampusNoticeTools},模块 key={@code campusnotice},前缀 /campusnotice)
* <b>不是一个应用</b>:校园公告按「班级」下发、带审批流与草稿箱;本模块(通知公告)按「用户」下发,
* 页面只有 收件箱 / 已发送 / 全部通知 / 发通知 / 详情 / 应用设置,<b>没有</b>审批、草稿、推送范围。
* 两者的发布字段截然不同,改这里时不要照搬 CampusNoticeTools。
*
* <p>发布表单逐项对齐 {@code NoticeFront/src/views/notice/NewNotice.vue#doAdd}:
* {@code title / content / receiveList / deptId / fileIds / smsFlag / timingSendTime}。
* 其中模块页的 {@code smsFlag} 恒为 0(模板里没有对应控件),故这里不放出该字段。
*
* <p>注意:本模块的用户态接口只认浏览器 CAS 会话,服务端直调会拿到 {@code code=302},
* 凡是需要「取字典给下拉」的字段都用 {@code optionsFrom + optionsKeys} 声明,由前端带用户登录态代取
* (见 AgentOrchestrator#buildForm)。
*/
public final class NoticeTools {
public static final String MODULE = "notice";
/** 发文部门字典的 type 值({@code /api/dict/List?type=1},与模块页 {@code noticeApi.getSysDic()} 一致) */
private static final String DICT_TYPE_DEPT = "1";
/** 附件上传地址 —— 模块页 NoticeUpload.vue 用的是 {@code /api/base/FileUpload};
* 这里只写**模块内相对路径**,前缀由 AgentOrchestrator.resolveUploadPath 统一拼。 */
private static final String UPLOAD_PATH = "/api/base/FileUpload";
private NoticeTools() {
}
public static List<ToolDef> defs() {
List<ToolDef> list = new ArrayList<>();
addInbox(list);
addPublishLoop(list);
addDictionaries(list);
return list;
}
/* ==================== 收件箱(所有用户) ==================== */
private static void addInbox(List<ToolDef> list) {
list.add(ToolDef.of("notify.received", MODULE, "我收到的通知",
"分页查询当前用户收到的通知(返回值含未读条数)。"
+ "用户问“最近有什么通知”“有没有未读通知”“我还有几条没看”时用它。",
"GET", "/api/notice/List")
.p(ToolParam.optional("pageIndex", "integer", "页码,从 1 开始").dft("1"),
ToolParam.optional("pageSize", "integer", "每页条数,默认 10").dft("10"),
ToolParam.optional("keyword", "string", "标题关键词"),
ToolParam.optional("readType", "integer", "阅读状态:0 全部 / 1 未读 / 2 已读")
.dft("0"),
ToolParam.optional("orderByTime", "string", "排序:1 时间倒序 / 2 时间正序")
.dft("1"))
.readAt("/"));
list.add(ToolDef.of("notify.detail", MODULE, "通知详情",
"查看一条通知的正文、附件与阅读进度(应读 / 已读人数)。",
"GET", "/api/notice/Detail")
.p(ToolParam.required("serid", "integer", "通知 id")
.label("通知"),
ToolParam.optional("type", "integer", "1 查看并标记已读(默认) / 0 仅查看不标读")
.dft("1"))
.readAt("/"));
list.add(ToolDef.of("notify.initializeRed", MODULE, "一键已读全部通知",
"把我收到的通知一次性全部标记为已读(模块页「已收到」列表上的一键已读)。",
"POST_FORM", "/api/notice/InitializeRed")
.write("/"));
}
/* ==================== 发布闭环(教职工) ==================== */
private static void addPublishLoop(List<ToolDef> list) {
list.add(ToolDef.of("notify.publish", MODULE, "发布通知",
"发布一条通知(可定时发送)。字段与通知公告模块的「发通知」页**完全一致**:"
+ "通知标题、来源部门、通知对象、附件、正文、定时发送。"
+ "调用前必须确认:标题 title、来源部门 deptId、通知对象 receiveList;"
+ "缺失就直接调用本工具,系统会弹出**完整表单**让用户填写,不要用文字反复追问;"
+ "来源部门必须来自字典,不要编造。"
+ "通知对象 receiveList 默认「全部用户」;要只发给部分人时由表单多选(用户 id 逗号串)。"
+ "timingSendTime 非空 = 定时发送。",
"POST_JSON", "/api/notice/Create")
.p(noticePublishParams())
.fullForm()
.write("/list/new"));
list.add(ToolDef.of("notify.editNotice", MODULE, "编辑通知",
"修改一条已发送的通知(标题、正文、来源部门、通知对象、附件、定时发送)。"
+ "调用前须确认 sourceId(要改的那条通知 id,可先用 notify.received / notify.sent 查到)。",
"POST_JSON", "/api/notice/Edit")
.p(ToolParam.required("sourceId", "integer", "要编辑的通知 id")
.label("要编辑的通知"),
ToolParam.required("title", "string", "通知标题(50 字以内)")
.label("通知标题"),
ToolParam.optional("content", "string", "通知正文(纯文本)")
.label("通知正文").as("textarea"),
ToolParam.optional("deptId", "string", "来源部门 id,来自 notify.deptDics 的 k 字段")
.label("来源部门")
.optionsFrom("notify.deptDics").optionsKeys("k", "v"),
ToolParam.optional("receiveList", "array", "通知对象:留空或含全部 = 全部用户")
.label("通知对象")
.optionsFrom("notify.schoolAll").optionsKeys("id", "name")
.join(",").allOption("all", "全部用户"),
ToolParam.optional("fileIds", "array", "附件文件 id 列表")
.label("添加附件").as("file").multiple(true).join(",")
.upload(UPLOAD_PATH),
ToolParam.optional("timingSendTime", "string", "定时发送时间 yyyy-MM-dd HH:mm:ss(留空=立即发送)")
.label("定时发送"))
.write("/list"));
list.add(ToolDef.of("notify.sent", 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", "标题关键词"),
ToolParam.optional("deptId", "string", "来源部门 id"),
ToolParam.optional("startTime", "string", "开始时间 yyyy-MM-dd").eg("2026-10-01"),
ToolParam.optional("endTime", "string", "结束时间 yyyy-MM-dd").eg("2026-10-31"),
ToolParam.optional("orderByTime", "string", "排序:1 时间倒序 / 2 时间正序").dft("1"))
.readAt("/list"));
list.add(ToolDef.of("notify.readDetail", MODULE, "通知阅读情况",
"查询某条通知的阅读情况(按组织分组列出已读 / 未读人员)。",
"GET", "/api/notice/ReadDetail")
.p(ToolParam.required("serid", "integer", "通知 id")
.label("通知"))
.readAt("/list/detail"));
}
/* ==================== 字典 / 组织树(发布表单下拉的来源) ==================== */
private static void addDictionaries(List<ToolDef> list) {
list.add(ToolDef.of("notify.deptDics", MODULE, "发文部门字典",
"查询本校本模块可选的来源部门(发布通知时填 deptId 用)。"
+ "仅在用户明确问「有哪些发文部门」时使用;"
+ "发布通知时**不要**先调它 —— 表单的「来源部门」下拉会自动带出可选值。",
"GET", "/api/dict/List")
.fix("type", DICT_TYPE_DEPT)
.readAt("/list/new"));
list.add(ToolDef.of("notify.schoolAll", MODULE, "组织树(选择通知对象)",
"查询本校组织树(部门 → 教师),用于发布通知时选择接收用户。"
+ "仅在用户明确要「按组织范围选人」时使用;"
+ "发布通知时**不要**先调它 —— 表单的「通知对象」多选会自动代取该树。",
"GET", "/api/base/GetSchoolAll")
.readAt("/list/new"));
}
/**
* 「发通知」页的完整字段(发布用)。
*
* <p>★ 与模块页 {@code NewNotice.vue#doAdd} 构造的 {@code NoticeRequest} 逐项对齐:
* {@code title / content / receiveList / deptId / fileIds / timingSendTime}。
* 模块页不传 {@code deptName}(校园公告那套才传),故这里也不设镜像字段;
* {@code smsFlag} 虽在请求体里,但页面没有对应控件、恒为 0,故不作为表单字段放出。
*/
private static ToolParam[] noticePublishParams() {
List<ToolParam> ps = new ArrayList<>();
ps.add(ToolParam.required("title", "string", "通知标题(50 字以内)")
.label("通知标题").eg("关于本周宿舍熄灯时间调整的通知"));
ps.add(ToolParam.optional("content", "string", "通知正文(纯文本,对应模块页的富文本编辑器)")
.label("通知正文").as("textarea").eg("各位老师:经学校研究决定……"));
ps.add(ToolParam.required("deptId", "string", "来源部门 id,来自 notify.deptDics 的 k 字段")
.label("来源部门")
.optionsFrom("notify.deptDics").optionsKeys("k", "v"));
// 模块页的 UserCheckInput:v-model 值为 "all" 或用户 id 逗号串(NOTICE_REQ.receiveList 是 string)
ps.add(ToolParam.required("receiveList", "array", "通知对象:留空或含全部 = 全部用户;也可多选具体用户")
.label("通知对象")
.optionsFrom("notify.schoolAll").optionsKeys("id", "name")
.join(",").allOption("all", "全部用户"));
ps.add(ToolParam.optional("fileIds", "array", "附件文件 id 列表(由表单的附件上传得到)")
.label("添加附件").as("file").multiple(true).join(",")
.upload(UPLOAD_PATH));
ps.add(ToolParam.optional("timingSendTime", "string", "定时发送时间 yyyy-MM-dd HH:mm:ss(留空=立即发送)")
.label("定时发送").eg("2026-10-15 08:00:00"));
return ps.toArray(new ToolParam[0]);
}
}