diff --git a/src/main/java/com/yunhai/agent/service/AgentOrchestrator.java b/src/main/java/com/yunhai/agent/service/AgentOrchestrator.java
index 2765f46..0eec2b8 100644
--- a/src/main/java/com/yunhai/agent/service/AgentOrchestrator.java
+++ b/src/main/java/com/yunhai/agent/service/AgentOrchestrator.java
@@ -252,8 +252,12 @@ public class AgentOrchestrator {
ToolCallTrace trace = buildTrace(def, args, tr);
resp.getToolCalls().add(trace);
- if (!def.isReadOnly()) {
- resp.setWroteSomething(true);
+ // 写操作 → 去系统里确认结果;只读**失败**(下游 302 / 会话没建起来)→
+ // 同样要给「去系统里自己查看」入口,否则模型嘴上说「点下方按钮」而界面没有按钮。
+ if (!def.isReadOnly() || !tr.isSuccess()) {
+ if (!def.isReadOnly()) {
+ resp.setWroteSomething(true);
+ }
if (StringUtils.isNotBlank(def.getLandingPath())) {
resp.setLandingPath(def.getLandingPath());
}
@@ -361,8 +365,12 @@ public class AgentOrchestrator {
}
auditService.record(session.getSessionId(), caller, def, args, tr);
resp.getToolCalls().add(buildTrace(def, args, tr));
- if (!def.isReadOnly()) {
- resp.setWroteSomething(true);
+ // 写操作 → 去系统里确认结果;只读**失败**(如下游 302 / 会话没建起来)→
+ // 同样要给「去系统里自己查看」入口,否则模型嘴上说「点下方按钮」而界面没有按钮。
+ if (!def.isReadOnly() || !tr.isSuccess()) {
+ if (!def.isReadOnly()) {
+ resp.setWroteSomething(true);
+ }
if (StringUtils.isNotBlank(def.getLandingPath())) {
resp.setLandingPath(def.getLandingPath());
}
diff --git a/src/main/java/com/yunhai/agent/tool/FormField.java b/src/main/java/com/yunhai/agent/tool/FormField.java
index af72755..4691187 100644
--- a/src/main/java/com/yunhai/agent/tool/FormField.java
+++ b/src/main/java/com/yunhai/agent/tool/FormField.java
@@ -18,7 +18,7 @@ public class FormField {
/** 中文标签,如「开始时间」 */
private String label;
- /** 控件类型:text / textarea / number / date / datetime / select / multiselect / file */
+ /** 控件类型:text / textarea / number / date / datetime / select / multiselect / file / scopes / flow / radio / switch */
private String widget;
/** 数据类型:string / integer / number / boolean / array */
@@ -86,6 +86,38 @@ public class FormField {
*/
private boolean fileAsObject;
+ /**
+ * 单选控件({@code widget=radio})在选项就绪后自动选中第一项。
+ *
对齐模块页:{@code LeaveApply.vue} 的「请假类型」一进页面就已选中第一个类型。
+ */
+ private boolean autoPickFirst;
+
+ /**
+ * 选项只有一个时自动填入并隐藏本字段。
+ *
对齐模块页:{@code LeaveApply.vue} 的「班级」用 {@code v-if="myClasses.length > 1"}`,
+ * 单班时不渲染、值直接带上。
+ */
+ private boolean hideWhenSingleOption;
+
+ /**
+ * 快捷时段按钮组:{@code {start: 开始字段名, end: 结束字段名,
+ * items: [{label, start, end}]}};{@code start/end} 为时刻({@code HH:mm:ss})。
+ *
对齐模块页 {@code LeaveTimeSection.quickSet()}:沿用已选日期,只换时分秒。
+ */
+ private Map quickPeriods;
+
+ /** {@code widget=switch} 勾选时提交的值 */
+ private String switchOn;
+
+ /** {@code widget=switch} 未勾选时提交的值 */
+ private String switchOff;
+
+ /** 多行文本框显示行数 */
+ private Integer rows;
+
+ /** 文本最大长度(前端显示字数统计) */
+ private Integer maxLength;
+
private boolean required;
/** 说明(来自工具定义,写清格式与含义) */
@@ -94,6 +126,13 @@ public class FormField {
/** 格式示例,前端用作 placeholder,如 2026-10-11 08:00:00 */
private String example;
+ /**
+ * 显式的占位提示(优先于 {@link #example})。
+ * 用于对齐业务模块页的原文案,如请假事由的「请输入请假原因.....」——
+ * 这里不该塞它的示例值(「发烧就医,需在家休息」),那是给模型看的参考,不是给用户看的提示。
+ */
+ private String placeholder;
+
private String defaultValue;
/**
diff --git a/src/main/java/com/yunhai/agent/tool/ToolExecutor.java b/src/main/java/com/yunhai/agent/tool/ToolExecutor.java
index bf3f014..b6689b5 100644
--- a/src/main/java/com/yunhai/agent/tool/ToolExecutor.java
+++ b/src/main/java/com/yunhai/agent/tool/ToolExecutor.java
@@ -619,9 +619,29 @@ public class ToolExecutor {
*
*
与模块页 {@code LeaveTimeSection.regenerate()} 口径一致(每天一段,时分沿用基准)。
* 放在执行器里做,浏览器代执行与服务端直调共用同一份逻辑。
+ *
+ *
★ 若入参里**已经有** {@code continuousLeaveTime}(助手表单让用户逐天微调过,
+ * 前端会显式发来),则以它为准、不再自行展开 —— 否则用户改好的个别天会被覆盖掉。
*/
private Map expandDailyArgs(ToolDef def, Map args) {
Map extra = new LinkedHashMap<>();
+
+ // ① 前端已经算好逐天时段(用户可单天微调)→ 原样采用,只把基准对齐到第一天
+ Object explicit = args.get("continuousLeaveTime");
+ if (explicit instanceof List && !((List>) explicit).isEmpty()) {
+ Object first = ((List>) explicit).get(0);
+ if (first instanceof Map) {
+ Object s0 = ((Map, ?>) first).get("startDate");
+ Object e0 = ((Map, ?>) first).get("endDate");
+ if (s0 != null && e0 != null) {
+ extra.put("startDate", String.valueOf(s0));
+ extra.put("endDate", String.valueOf(e0));
+ }
+ }
+ return extra;
+ }
+
+ // ② 否则按「基准时间 + 天数」自行展开(模型直接调用 / 没走表单时的路径)
for (ToolParam p : def.getParams()) {
Map spec = p.getDailyExpandSpec();
if (spec == null || spec.isEmpty()) continue;
diff --git a/src/main/java/com/yunhai/agent/tool/ToolParam.java b/src/main/java/com/yunhai/agent/tool/ToolParam.java
index 42a5339..bb28927 100644
--- a/src/main/java/com/yunhai/agent/tool/ToolParam.java
+++ b/src/main/java/com/yunhai/agent/tool/ToolParam.java
@@ -62,6 +62,9 @@ public class ToolParam {
/** 示例值(表单 placeholder,如 2026-10-11 08:00:00),也会进入给模型的描述 */
private String example;
+ /** 显式占位提示(优先于 {@link #example}),用于照搬业务模块页的原文案 */
+ private String placeholderText;
+
/**
* 控件类型:text / textarea / number / date / datetime / select / multiselect / file;
* 缺省按参数名与类型自动推断。
@@ -148,8 +151,52 @@ public class ToolParam {
* 作用有两个:① 生成表单时把依赖名带给前端,前端据此清空/重新拉取选项;
* ② 生成「浏览器代取选项」的地址时,把依赖字段位置留成占位符({@code ?classId={{classId}}})。
*/
+ /**
+ * 依赖的先序字段名(用于联动下拉,如「学生」依赖「班级」)
+ */
private List dependsOn;
+ /**
+ * 单选控件({@code widget=radio})在选项就绪后**自动选中第一项**。
+ *
+ * ★ 对齐模块页行为:HomeLeave 的 {@code LeaveApply.vue} 里「请假类型」是单选按钮组,
+ * 且 {@code watch(leaveTypeOptions, opts => { if (formData.leaveType == null && opts.length)
+ * formData.leaveType = opts[0].id })} —— 一进页面就已经选中第一个类型。
+ * 助手的表单若不自动选中,用户就得比模块页多点一步,且容易漏填导致必填校验卡住。
+ */
+ private boolean autoPickFirst;
+
+ /**
+ * 选项只有一个时**自动填入并隐藏本字段**。
+ *
+ *
★ 对齐模块页行为:{@code LeaveApply.vue} 的「班级」是
+ * {@code v-if="myClasses.length > 1"} —— 教师只有 1 个班时页面上根本没有这个控件,
+ * 值在 {@code onMounted} 里直接带上。助手表单同理:单班自动带上、不显示,
+ * 多班(或选项没取到)才让用户选。
+ */
+ private boolean hideWhenSingleOption;
+
+ /**
+ * 快捷时段按钮组(对齐 {@code LeaveTimeSection.vue} 的全天/上午/下午)。
+ *
+ *
结构:{@code {start: 开始字段名, end: 结束字段名,
+ * items: [{label, start, end}, ...]}};{@code start/end} 是**时刻**({@code HH:mm:ss}),
+ * 点按钮时套用到当前已选日期上(未选日期则用今天)。
+ */
+ private Map quickPeriods;
+
+ /** {@code widget=switch} 的「开」值(勾选时提交的值),如请假 continuousLeave 的 {@code 1} */
+ private String switchOn;
+
+ /** {@code widget=switch} 的「关」值(未勾选时提交的值),如 {@code 0} */
+ private String switchOff;
+
+ /** 多行文本框显示行数(缺省 3,对齐模块页「请假事由」的 4 行) */
+ private Integer rows;
+
+ /** 文本最大长度(对齐模块页 {@code maxlength} + 字数统计) */
+ private Integer maxLength;
+
public ToolParam() {
}
@@ -207,6 +254,15 @@ public class ToolParam {
return this;
}
+ /**
+ * 显式占位提示,照搬业务模块页的原文案(如请假事由的「请输入请假原因.....」)。
+ * 与 {@link #eg} 的区别:{@code eg} 是给模型看的**示例值**,这里纯粹是给用户看的提示。
+ */
+ public ToolParam placeholder(String value) {
+ this.placeholderText = value;
+ return this;
+ }
+
/** 指定控件类型 */
public ToolParam as(String value) {
this.widget = value;
@@ -323,6 +379,88 @@ public class ToolParam {
return this;
}
+ /**
+ * 单选按钮组(对齐模块页的 {@code RadioGroup})。
+ *
取值仍走 {@link #options}/{@link #optionsFrom},只是渲染成平铺的单选按钮而不是下拉。
+ */
+ public ToolParam radio() {
+ this.widget = "radio";
+ return this;
+ }
+
+ /**
+ * 勾选框 / 开关控件。
+ *
+ * @param onValue 勾选时提交的值(如 {@code "1"})
+ * @param offValue 未勾选时提交的值(如 {@code "0"})
+ */
+ public ToolParam switchOf(String onValue, String offValue) {
+ this.widget = "switch";
+ this.switchOn = onValue;
+ this.switchOff = offValue;
+ return this;
+ }
+
+ /** 选项就绪后自动选中第一项(模块页「请假类型」的默认行为) */
+ public ToolParam autoPickFirst() {
+ this.autoPickFirst = true;
+ return this;
+ }
+
+ /** 选项只有一个时自动填入并隐藏本字段(模块页「班级」:只教一个班时不显示) */
+ public ToolParam hideIfSingleOption() {
+ this.hideWhenSingleOption = true;
+ return this;
+ }
+
+ /**
+ * 快捷时段按钮组(全天 / 上午 / 下午 …),点一下同时填好开始与结束**时刻**。
+ *
+ *
+ * ToolParam.required("startDate", "string", "...")
+ * .as("datetime")
+ * .quickPeriods("startDate", "endDate",
+ * "全天", "08:00:00", "18:00:00",
+ * "上午", "08:00:00", "12:00:00",
+ * "下午", "12:00:00", "18:00:00")
+ *
+ *
+ * 三元组按「标签、开始时刻、结束时刻」顺序依次传入(个数必须为 3 的倍数)。
+ * 与模块页 {@code LeaveTimeSection.quickSet()} 口径一致:**沿用已选的日期**,
+ * 只把时分秒换掉;还没选日期就按今天。
+ */
+ public ToolParam quickPeriods(String startField, String endField, String... labelStartEnd) {
+ if (labelStartEnd == null || labelStartEnd.length < 3 || labelStartEnd.length % 3 != 0) {
+ throw new IllegalArgumentException("quickPeriods 需要「标签, 开始时刻, 结束时刻」三元组");
+ }
+ List