Files
yunhai-agent-service/src/main/java/com/yunhai/agent/tool/defs/ProfessionTools.java
T
lijing c3477c6588
build / build-push (push) Successful in 1m27s
fix(专业发展): 「发布专业发展」直达表单,所属栏目不再问两遍
用户反馈:说「我要发布专业发展」没直接出表单,先被反问「要选哪个栏目」,
答完出来表单,「所属栏目」却还是空的要再选一次 —— 一件事问了两遍。

两处根因:
1) profession.publish 的说明只写了「调用前必须确认所属栏目 moduleId 与标题」,
   漏了公告 / 宿舍 / 活动 / 作业四个发布工具都有的那句「缺失就直接调用本工具,
   系统会弹出完整表单让用户填写,不要用文字反复追问」—— 模型读到「必须确认」
   就照字面先去口头确认了;
2) 「发布专业发展」不在裸意图直达表里,这句必然要过一轮模型。

改动:
- 工具说明按另外四个发布工具的同款句式补齐(说明词是模型唯一的施工图);
- IntentShortcut 新增「发布专业发展记录」意图(仅教职工),说「我要发布专业发展」
  直接弹表单;栏目是学校自建的、各校 id 不同,所以**不预置**,
  由用户就在表单里选 —— 这正是「直接出表单选栏目」;
- 契约测试 everyPublishToolTellsTheModelNotToKeepAskingInWords:凡是名字以
  .publish 结尾的工具,说明里必须带「不要用文字反复追问」(漏写会被测出来);
- IntentDirectFormTest 补两条:教职工直达出表单(含栏目下拉与代取地址、且不预置
  栏目)、学生落回模型。

配套(前端 86e3a10):即便真的先问了一轮再弹表单,对话里定好的值也会被对齐到
选项取值(数字 71 → "71",名称「数学」→ SUB02),表单里不再重复问。
2026-10-14 11:35:43 +08:00

199 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.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]);
}
}