build / build-push (push) Successful in 1m27s
用户反馈:说「我要发布专业发展」没直接出表单,先被反问「要选哪个栏目」, 答完出来表单,「所属栏目」却还是空的要再选一次 —— 一件事问了两遍。 两处根因: 1) profession.publish 的说明只写了「调用前必须确认所属栏目 moduleId 与标题」, 漏了公告 / 宿舍 / 活动 / 作业四个发布工具都有的那句「缺失就直接调用本工具, 系统会弹出完整表单让用户填写,不要用文字反复追问」—— 模型读到「必须确认」 就照字面先去口头确认了; 2) 「发布专业发展」不在裸意图直达表里,这句必然要过一轮模型。 改动: - 工具说明按另外四个发布工具的同款句式补齐(说明词是模型唯一的施工图); - IntentShortcut 新增「发布专业发展记录」意图(仅教职工),说「我要发布专业发展」 直接弹表单;栏目是学校自建的、各校 id 不同,所以**不预置**, 由用户就在表单里选 —— 这正是「直接出表单选栏目」; - 契约测试 everyPublishToolTellsTheModelNotToKeepAskingInWords:凡是名字以 .publish 结尾的工具,说明里必须带「不要用文字反复追问」(漏写会被测出来); - IntentDirectFormTest 补两条:教职工直达出表单(含栏目下拉与代取地址、且不预置 栏目)、学生落回模型。 配套(前端 86e3a10):即便真的先问了一轮再弹表单,对话里定好的值也会被对齐到 选项取值(数字 71 → "71",名称「数学」→ SUB02),表单里不再重复问。
199 lines
12 KiB
Java
199 lines
12 KiB
Java
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.LinkedHashMap;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
|
||
/**
|
||
* 教师专业发展模块工具 —— 后端 Profession,前缀 /api/profession、/api/moudule、/api/points、/api/stats。
|
||
*
|
||
* <p>★ 发布表单的字段以模块前端源码为**权威契约**:
|
||
* {@code ProfessionView/src/views/profession/RecordEdit.vue}(发布/编辑记录页)。
|
||
* 提交体(模块页 {@code save()} 逐行核对):
|
||
* <pre>
|
||
* POST /api/profession/Create (application/x-www-form-urlencoded)
|
||
* { moduleId, title, grade, subject, ifOpen, remark,
|
||
* contents: JSON.stringify([{fieldId, value}]), // 栏目动态字段
|
||
* fileIds: "11,12" } // 逗号串
|
||
* </pre>
|
||
* 与助手此前实现的差异(本轮修正):早先只有 moduleId/title/content 三项,
|
||
* 既缺「年级段 / 学科 / 是否公开 / 正文(字段名应是 remark 而不是 content)」,
|
||
* 也缺栏目动态字段与附件 —— 发出去的记录与在系统里发的天然不同。
|
||
*/
|
||
public final class ProfessionTools {
|
||
|
||
public static final String MODULE = "profession";
|
||
|
||
private ProfessionTools() {
|
||
}
|
||
|
||
public static List<ToolDef> defs() {
|
||
List<ToolDef> list = new ArrayList<>();
|
||
|
||
list.add(ToolDef.of("profession.homeRecords", MODULE, "栏目最新记录",
|
||
"各栏目最新发布的记录块(专业发展首页内容)。",
|
||
"GET", "/api/profession/HomeModuleRecords")
|
||
.p(ToolParam.optional("size", "integer", "每栏取几条").dft("6"))
|
||
.readAt("/home"));
|
||
|
||
list.add(ToolDef.of("profession.list", MODULE, "专业发展记录列表",
|
||
"后台口径的专业发展记录分页查询,可按栏目/教师/时间筛选。",
|
||
"GET", "/api/profession/ProfessionList")
|
||
.p(ToolParam.optional("pageIndex", "integer", "页码").dft("1"),
|
||
ToolParam.optional("pageSize", "integer", "每页条数").dft("10"),
|
||
ToolParam.optional("keyword", "string", "标题关键词"),
|
||
ToolParam.optional("moduleId", "integer", "栏目 id"),
|
||
ToolParam.optional("starttime", "string", "开始时间 yyyy-MM-dd"),
|
||
ToolParam.optional("endtime", "string", "结束时间 yyyy-MM-dd"))
|
||
.readAt("/module/list"));
|
||
|
||
list.add(ToolDef.of("profession.moduleRecords", MODULE, "栏目文章列表",
|
||
"某个栏目下的文章分页(附同校其他栏目)。",
|
||
"GET", "/api/profession/ModuleRecordList")
|
||
.p(ToolParam.required("moduleId", "integer", "栏目 id"),
|
||
ToolParam.optional("pageIndex", "integer", "页码").dft("1"),
|
||
ToolParam.optional("pageSize", "integer", "每页条数").dft("10"))
|
||
.readAt("/module/list"));
|
||
|
||
list.add(ToolDef.of("profession.myRecords", MODULE, "我的栏目文章",
|
||
"查询我在某栏目下发表的文章。",
|
||
"GET", "/api/profession/MyModuleRecordList")
|
||
.p(ToolParam.required("moduleId", "integer", "栏目 id"),
|
||
ToolParam.optional("pageIndex", "integer", "页码").dft("1"),
|
||
ToolParam.optional("pageSize", "integer", "每页条数").dft("10"))
|
||
.readAt("/timeline"));
|
||
|
||
list.add(ToolDef.of("profession.detail", MODULE, "记录详情",
|
||
"查看一条专业发展记录的详情。",
|
||
"GET", "/api/profession/ProfessionDetail")
|
||
.p(ToolParam.required("SerID", "integer", "记录 id"))
|
||
.readAt("/module/list"));
|
||
|
||
list.add(ToolDef.of("profession.moduleTree", MODULE, "栏目树",
|
||
"专业发展的栏目树(含我的栏目),发布记录时选栏目用。",
|
||
"GET", "/api/moudule/ModuleTree")
|
||
.readAt("/record/new"));
|
||
|
||
/* ==================== 发布表单的字典(都必须由前端带登录态代取) ====================
|
||
* 模块的用户态接口只认浏览器 CAS 会话,服务端直取必然 code=302,
|
||
* 因此这里只声明「去哪儿取」,由 AgentOrchestrator 下发 optionCall / extraFields.call。
|
||
*/
|
||
|
||
// 年级段:模块页 RecordEdit.vue 的 grades(<select>,可空)
|
||
list.add(ToolDef.of("profession.grades", MODULE, "年级段字典",
|
||
"发布记录时的「年级段」下拉选项。属发布表单的组成部分,一般不单独调用。",
|
||
"GET", "/api/profession/GetGradeList")
|
||
.readAt("/record/new"));
|
||
|
||
// 学科:{subId, subName},subId 对齐班级管理的 SUB01 编码
|
||
list.add(ToolDef.of("profession.subjects", MODULE, "学科字典",
|
||
"发布记录时的「学科」下拉选项。属发布表单的组成部分,一般不单独调用。",
|
||
"GET", "/api/profession/GetSubList")
|
||
.readAt("/record/new"));
|
||
|
||
// 栏目的动态字段定义:{name, fields:[{id,fieldName,fieldType,ifRequired,...}]}
|
||
list.add(ToolDef.of("profession.moduleFields", MODULE, "栏目动态字段定义",
|
||
"某个栏目挂了哪些自定义字段(字段名/类型/是否必填/选项)。"
|
||
+ "发布表单据它渲染「栏目动态字段」区块,一般不单独调用。",
|
||
"GET", "/api/moudule/Getdatelist")
|
||
.p(ToolParam.required("SerID", "integer", "栏目 id(moduleId)"))
|
||
.readAt("/record/new"));
|
||
|
||
// 行政区划树:级联字段(province_city)用,level=2 省市 / 3 省市区
|
||
list.add(ToolDef.of("profession.areaTree", MODULE, "行政区划树",
|
||
"省市区三级行政区划树,供发布表单的「省市区」级联字段使用。",
|
||
"GET", "/api/profession/GetAreaTree")
|
||
.p(ToolParam.optional("level", "integer", "展开层级:2 省市 / 3 省市区").dft("3"))
|
||
.readAt("/record/new"));
|
||
|
||
list.add(ToolDef.of("profession.myPoints", MODULE, "我的积分",
|
||
"查询我的专业发展积分与来源明细。",
|
||
"GET", "/api/profession/MyPoints")
|
||
.readAt("/timeline"));
|
||
|
||
list.add(ToolDef.of("profession.pointsRank", MODULE, "积分排行",
|
||
"积分排行榜。",
|
||
"GET", "/api/profession/PointsRank")
|
||
.p(ToolParam.optional("top", "integer", "取前几名").dft("8"))
|
||
.readAt("/home"));
|
||
|
||
/* ==================== 发布记录 ==================== */
|
||
|
||
list.add(ToolDef.of("profession.publish", MODULE, "发布专业发展记录",
|
||
"发布一条专业发展记录(教学设计/听课记录/课题研究/教学反思等)。"
|
||
+ "调用前必须确认:所属栏目 moduleId、标题 title;"
|
||
// ★ 与公告 / 宿舍 / 活动 / 作业四个发布工具完全同款的一句(此前唯独这里漏了):
|
||
// 少了它,模型读到「必须确认」就先去反问用户「你要发到哪个栏目」,
|
||
// 答完才弹表单 —— 一件小事被拆成两轮对话(用户反馈的原话)。
|
||
+ "缺失就直接调用本工具,系统会弹出**完整表单**让用户填写,不要用文字反复追问;"
|
||
+ "正文、年级段、学科、是否公开、附件在表单里补齐,"
|
||
+ "栏目自定义字段由表单按所选栏目动态渲染。",
|
||
"POST_FORM", "/api/profession/Create")
|
||
.p(professionPublishParams())
|
||
.extraFields("profession.moduleFields", "contents", moduleIdAs("SerID"))
|
||
.extraFieldsBefore("remark")
|
||
.extraFieldsArea("profession.areaTree")
|
||
.fullForm()
|
||
.write("/record/new"));
|
||
|
||
return list;
|
||
}
|
||
|
||
/**
|
||
* (Java 8 没有 {@code Map.of})栏目动态字段的依赖映射:查询参数名 → 表单字段名。
|
||
* <p>模块侧的栏目字段接口查询参数叫 {@code SerID},而表单字段叫 {@code moduleId} —— 两边名字不同。
|
||
*/
|
||
private static Map<String, String> moduleIdAs(String queryParam) {
|
||
Map<String, String> m = new LinkedHashMap<>();
|
||
m.put(queryParam, "moduleId");
|
||
return m;
|
||
}
|
||
|
||
/**
|
||
* 「发布 / 编辑记录」页的完整字段。
|
||
*
|
||
* <p>★ 与 {@code ProfessionView/src/views/profession/RecordEdit.vue} 的表单逐项对齐:
|
||
* <pre>
|
||
* 标题(必填,≤200) → 年级段 / 学科(两个可空下拉) → 是否公开(单选 公开/不公开)
|
||
* → 栏目动态字段(按栏目渲染 → contents) → 正文内容(富文本 → remark) → 附件(fileIds)
|
||
* </pre>
|
||
* 唯一多出来的是「所属栏目」——模块页是从栏目列表点「发布」进来的(栏目由入口决定),
|
||
* 助手没有这个上下文,所以必须让用户选一次。
|
||
*/
|
||
private static ToolParam[] professionPublishParams() {
|
||
List<ToolParam> ps = new ArrayList<>();
|
||
ps.add(ToolParam.required("moduleId", "integer", "栏目 id,来自 profession.moduleTree")
|
||
.label("所属栏目")
|
||
.optionsFrom("profession.moduleTree"));
|
||
ps.add(ToolParam.required("title", "string", "记录标题(200 字以内)")
|
||
.label("记录标题").placeholder("请输入记录标题").maxLength(200)
|
||
.eg("2026年秋季公开课教学设计"));
|
||
ps.add(ToolParam.optional("grade", "string", "年级段,来自 profession.grades")
|
||
.label("年级段")
|
||
// 年级段字典是 {value,label}(不是平台通用的 {k,v} / 业务 VO 的 {id,name}),必须指认键名
|
||
.optionsFrom("profession.grades").optionsKeys("value", "label"));
|
||
ps.add(ToolParam.optional("subject", "string", "学科,取值来自 profession.subjects 的 subId")
|
||
.label("学科")
|
||
.optionsFrom("profession.subjects").optionsKeys("subId", "subName"));
|
||
ps.add(ToolParam.optional("ifOpen", "integer", "是否公开:1 公开 / 0 不公开")
|
||
.label("是否公开").as("radio").dft("1")
|
||
.options(new String[]{"1", "0"}, new String[]{"公开", "不公开"}));
|
||
ps.add(ToolParam.optional("remark", "string", "正文内容(对应模块页的富文本编辑器)")
|
||
.label("正文内容").as("textarea").placeholder("请输入正文内容…"));
|
||
// 栏目动态字段的打包结果:由表单取回字段定义、渲染后自行生成,用户不直接填
|
||
ps.add(ToolParam.optional("contents", "string", "栏目动态字段的取值(JSON 数组,由表单自动生成)")
|
||
.label("栏目动态字段").hide());
|
||
ps.add(ToolParam.optional("fileIds", "array", "附件文件 id 列表(由表单的附件上传得到)")
|
||
.label("添加附件").as("file").multiple(true).join(",")
|
||
// ★ 专业发展的附件上传接口与其余模块不同:走 file 域(不是 base/FileUpload)。
|
||
// 只写模块内相对路径,前缀由 AgentOrchestrator.resolveUploadPath 拼 → /profession/api/file/Upload
|
||
// (ProfessionView/src/views/profession/RecordEdit.vue 的 uploadAction = fileUploadUrl)
|
||
.upload("/api/file/Upload"));
|
||
return ps.toArray(new ToolParam[0]);
|
||
}
|
||
}
|