Files
yunhai-agent-service/src/main/java/com/yunhai/agent/tool/defs/ProfessionTools.java
T
lijing 006003c842
build / build-push (push) Successful in 1m15s
feat(表单): 五个模块正文换成模块页同款富文本,公告/宿舍的接收对象换成自绘控件
彻底对齐各模块「新建 / 发布」页里的两类控件(此前只有通知公告搬到位):

正文 → wangEditor(widget=richtext)
  · 校园公告 content、宿舍通知 content(发布 + 编辑)、家校活动 content(发布 + 编辑)、
    作业 description
  · 教师专业发展 remark:只声明形态、**不声明** .richText(...) —— 该模块 file 域只有
    Upload/Preview/Down(没有 UploadImage),模块页 RecordEdit.vue 也没配 uploadImage,
    所以下发空地址,前端据此把工具栏的「图片」组整组摘掉(模块页就是这个形态)

接收对象 → 自绘控件(widget=scopepicker / roompicker)
  · 校园公告 pushScopes:换成 ClassScopeInput 同款(全部班级 / 指定班级 + 弹窗班级树);
    提交口径不变,仍是 [{scopeType,scopeId,scopeName}]
  · 宿舍通知 receiveList:换成模块页「选择接收宿舍」弹窗同款(左楼栋 / 右按楼层分组的房间);
    v-model 是字符串(房间 id 逗号串或 ALL 哨兵),与模块 receiveList 的真实契约一致

基建
  · ToolParam 新增 richText(upload,preview) / scopePicker(type) / roomPicker() /
    pickerSource(页签名, 模块内相对路径);对应 FormField 四个字段
  · AgentOrchestrator:新增 resolveModulePath 通用形态,richtext 与 pickerSources 的地址
    一律只写模块内相对路径、前缀在这里拼;richtext **没有兜底**(不声明就是空)
  · 带 optionCall 的字段会被改写成 select 的白名单改用 isSelfDrawnPicker()
    (原先是硬编码 userpicker),并放开为 public 供契约测试复用生产口径

测试(139 tests, 0 failures)
  · ModuleFormParityTest 新增两条通用不变量:正文图片地址与弹窗数据源都必须带模块前缀、
    不许自己带前缀、不许双斜杠;另加「专业发展不得拿到兜底上传地址」
  · FormFieldWidgetTest 新增 richtext(含「不声明 = 空」反向断言)与自绘控件透传两条
  · IntentDirectFormTest / DormActivityPublishIntentTest 同步改成新控件口径
2026-10-14 16:56:07 +08:00

204 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[]{"公开", "不公开"}));
// ★ 富文本但**不带图片上传**:模块页 RecordEdit.vue 的 editorConfig 只有 placeholder
// (toolbarConfig = {}),并没有配 MENU_CONF.uploadImage —— 该模块的 file 域只暴露了
// Upload / Preview / Down(见 profession.ts),**没有** UploadImage。
// 所以这里不调 .richText(...):上传地址为空时前端会把工具栏的「图片」组去掉,
// 而不是留一个点了必失败的按钮。
ps.add(ToolParam.optional("remark", "string", "正文内容(富文本 HTML,对应模块页的 wangEditor;该模块不支持正文插图)")
.label("正文内容").as("richtext").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]);
}
}