messages = session.getMessages();
if (messages.isEmpty()) {
messages.add(ChatMessage.system(buildSystemPrompt(userDesc, authorizedModIds, roles)));
@@ -398,6 +407,54 @@ public class AgentOrchestrator {
/* ==================== 表单:缺参拦截 与 直提交 ==================== */
+ /**
+ * 裸意图直达:命中「只有意图、没有细节」的句子(如「我要请假」)时,
+ * 按当前身份直接构建表单返回 —— 学生拿「请假申请」,教职工拿「请假登记(帮学生请假)」。
+ *
+ * 判据与能力目录 / 模型侧过滤**完全一致**({@code visibleTools}),
+ * 因此不存在「直达出一个本身份无权使用的表单」的可能;不命中或不适用就返回 {@code null},
+ * 照常交给大模型处理。
+ */
+ private ChatResponse directForm(String message, CallerContext caller, Set authorizedModIds,
+ Set roles, SessionStore.SessionState session, EventSink out) {
+ IntentShortcut.Intent intent = intentShortcut.match(message);
+ if (intent == null) {
+ return null;
+ }
+ boolean staff = roles != null
+ && (roles.contains(AgentRole.TEACHER) || roles.contains(AgentRole.ADMIN));
+ String toolName = staff ? intent.getStaffTool() : intent.getStudentTool();
+ ToolDef def = toolName == null ? null : registry.get(toolName);
+ if (def == null) {
+ return null;
+ }
+ boolean visible = registry.visibleTools(authorizedModIds, roles).stream()
+ .anyMatch(t -> t.getName().equals(def.getName()));
+ if (!visible) {
+ return null;
+ }
+
+ Map empty = new LinkedHashMap<>();
+ List missing = missingRequired(def, empty);
+ if (missing.isEmpty()) {
+ return null;
+ }
+
+ String reply = staff ? intent.getStaffReply() : intent.getStudentReply();
+ FormRequest form = buildForm(def, empty, missing, caller);
+ form.setDescription(reply);
+
+ ChatResponse resp = new ChatResponse();
+ resp.setSessionId(session.getSessionId());
+ resp.setForm(form);
+ resp.setReply(reply);
+ out.emit(AgentEvent.of("form", "需要补充信息:" + String.join("、", form.getMissing())));
+ log.info("意图直达弹表单:session={} 身份={} 工具={}", session.getSessionId(),
+ staff ? "教职工" : "学生", def.getName());
+ finish(resp, session, 0, 0, out);
+ return resp;
+ }
+
/**
* 预检本轮的写操作工具:若有必填参数缺失,则撤销刚加入的 assistant 消息并返回一张补全表单。
*
@@ -592,10 +649,21 @@ public class AgentOrchestrator {
List missingLabels = new ArrayList<>();
for (ToolParam p : missing) {
FormField f = p.toFormField();
- // 字典类字段(如请假类型):先跑一次它的来源工具,把可选项直接摆到下拉框里
- // 判据是「声明了来源」而不是「当前没有选项」—— 参数可能已带内置兜底,
- // 实时字典取到就覆盖兜底,取不到则保留兜底(下拉框不会退化成数字框)。
- if (StringUtils.isNotBlank(p.getOptionsFrom())) {
+ // 字典类字段(如请假类型、班级、学生):优先交给前端用**用户自己的登录态**代取。
+ //
+ // ★ 为什么不在这里直接执行字典工具:业务模块的用户态接口只认浏览器 CAS 会话,
+ // 服务端直调必然 code=302(README 第 11 节)。以前"服务端取一次、失败就退化成
+ // 数字输入框",结果教师登记请假时得手填 classId/stuId 编号——根本没法用。
+ // 现在把「去哪儿取」以 optionCall 下发(依赖字段留 {{占位符}}),
+ // 前端在学生选完班级后自己拉名单,下拉框才是真的下拉框。
+ BrowserToolRequest.BrowserCall optionCall = buildOptionCall(p, caller);
+ if (optionCall != null) {
+ f.setOptionCall(optionCall);
+ f.setWidget("select");
+ // 有内置兜底的(如请假类型 病假/事假)先摆上,前端取到学校实时字典后覆盖
+ f.setOptionsFallback(p.isUsingFallbackOptions());
+ } else if (StringUtils.isNotBlank(p.getOptionsFrom())) {
+ // 没有代取通道(模块未配地址 / 越权)→ 退回服务端取一次,取不到就保留原控件让用户手填编号
List