feat(form): 发布表单字段与各模块「新建发布」页完全对齐
build / build-push (push) Successful in 54s

原来助手表单只渲染"缺失的必填项",且控件只有文本/单选/时间 —— 与各模块
自己的发布页相比缺了接收对象多选、附件、定时发送、置顶、紧急程度、回执单据、
发布方式等字段,用户没法"填得一样"。

补齐一层共享的表单能力,再把三个发布工具按模块页逐字段重写:

共享能力
- ToolParam: 新增 join(分隔符)/multiple/allOption(哨兵)/scopes(推送范围),
  array 类型有选项来源时自动用多选控件;数组 schema 补 items
- ToolDef: 新增 fullForm() —— 表单渲染全部字段(不再只渲染缺失必填)
- ToolExecutor: POST_JSON 前按 joinSeparator 把数组拼成模块要的逗号串;
  allToken 与具体项互斥(只选具体项→拼串,否则发哨兵)
- AgentOrchestrator: file 控件补 uploadPath(/<模块>/api/base/FileUpload)
- 模板/参数文件同步下发 joinSeparator/uploadPath/multiple/allToken/scopeType

三个发布工具
- notice.publish: 补重要程度/接收班级(pushScopes 对象数组)/置顶/短信/阅读回执/
  附件/定时发送/发布方式;新增 notice.stuTree(组织树)
- dorm.publish: 补紧急程度/生效时间/回执截止/回执单据/接收宿舍(多选,ALL)/
  宿管配合/置顶/短信/附件/定时发送/存草稿
- activity.publish: 补报名截止/回执方式三档/名额上限/家长随行/活动费用/
  接收班级(多选,ALL)/短信/附件/定时发送/存草稿
- leave.submit/register: 连续请假改为可填参数,补连续天数与佐证材料附件

测试:发布工具契约 + 表单字段与模块页一致性断言;90 tests 全绿
This commit is contained in:
lijing
2026-10-12 22:05:09 +08:00
parent b6a8a63b0e
commit 322d589b3e
13 changed files with 456 additions and 64 deletions
@@ -62,30 +62,58 @@ public final class CampusNoticeTools {
.readAt("/list/new"));
list.add(ToolDef.of("notice.publish", MODULE, "发布校园公告",
"发布一条校园公告,**立即生效、全站可见**(是否转审批由学校「应用设置」里的流程绑定决定,"
+ "请求侧无法选择)。调用前必须确认:标题 title、正文 content、发文部门 deptId;"
+ "缺失就直接调用本工具,系统会弹出表单让用户补齐,不要用文字反复追问;"
+ "发文部门必须来自字典,不要编造。",
"发布一条校园公告。字段与校园公告模块的「新建公告」页**完全一致**:"
+ "标题、重要程度、来源部门、接收班级、置顶、短信提醒、学员阅读回执、附件、正文、定时发送、发布方式。"
+ "调用前必须确认:标题 title、正文 content、发文部门 deptId;"
+ "缺失就直接调用本工具,系统会弹出**完整表单**让用户填写,不要用文字反复追问;"
+ "发文部门必须来自字典,不要编造。"
+ "接收班级 pushScopes 默认「全部班级」;要定向到具体班级时由表单多选。"
+ "发布方式 publishType:direct 直接发布(默认)/ draft 存为草稿。"
+ "timingSendTime 非空 = 定时发送。",
"POST_JSON", "/api/notice/Create")
.p(ToolParam.required("title", "string", "公告标题")
.p(ToolParam.required("title", "string", "公告标题(200 字以内)")
.label("公告标题").eg("关于国庆假期安排的通知"),
ToolParam.required("content", "string", "公告正文(纯文本)")
ToolParam.required("content", "string", "公告正文(纯文本,对应模块页的富文本编辑器)")
.label("公告正文").as("textarea").eg("各位老师、同学:经学校研究决定……"),
ToolParam.required("deptId", "string", "发文部门 id,来自 notice.deptDics 的 k 字段")
.label("发文部门")
.optionsFrom("notice.deptDics").optionsKeys("k", "v"),
ToolParam.optional("type", "integer", "重要程度:1 普通 / 2 重要")
.label("重要程度").dft("1")
.options(new String[]{"1", "2"}, new String[]{"普通", "重要"}),
ToolParam.optional("receiptFlag", "integer", "是否需要阅读回执(要回执时收件人需点确认)")
.label("需要阅读回执").dft("0")
ToolParam.required("deptId", "string", "发文部门 id,来自 notice.deptDics 的 k 字段")
.label("发文部门")
.optionsFrom("notice.deptDics").optionsKeys("k", "v"),
ToolParam.optional("pushScopes", "array", "接收班级:留空或含全部 = 全部班级;也可多选具体班级")
.label("接收班级")
.optionsFrom("notice.stuTree").optionsKeys("id", "name")
.scopes(4).allOption("ALL", "全部班级"),
ToolParam.optional("topmost", "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[]{"否", "是"}))
.label("短信提醒").dft("0")
.options(new String[]{"0", "1"}, new String[]{"否", "是"}),
ToolParam.optional("receiptFlag", "integer", "是否需要学员阅读回执(要回执时收件人需点确认)")
.label("学员阅读回执").dft("0")
.options(new String[]{"0", "1"}, new String[]{"否", "是"}),
ToolParam.optional("fileIds", "array", "附件文件 id 列表(由表单的附件上传得到)")
.label("添加附件").as("file").multiple(true).join(","),
ToolParam.optional("timingSendTime", "string", "定时发送时间 yyyy-MM-dd HH:mm:ss(留空=立即发布)")
.label("定时发送").eg("2026-10-13 08:00:00"),
ToolParam.optional("publishType", "string", "发布方式:direct 直接发布 / draft 存为草稿")
.label("发布方式").dft("direct")
.options(new String[]{"direct", "draft"}, new String[]{"直接发布", "存为草稿"}))
.fullForm()
.write("/list/new")
.modId(null));
list.add(ToolDef.of("notice.stuTree", MODULE, "组织树(学员)",
"查询本校组织树(学部 / 年级 / 班级),用于发布公告时选择接收班级。"
+ "仅在用户明确要「按组织范围选人」时使用;"
+ "发布公告时**不要**先调它 —— 表单的「接收班级」多选会自动代取该树。",
"GET", "/api/base/getStuTree")
.fix("toUser", false)
.readAt("/list/new")
.modId(null));
list.add(ToolDef.of("notice.approvalList", MODULE, "待我审批的公告",
"查询待我审批的公告列表(公告审批中心)。",
"GET", "/api/notice/ApprovalList")