diff --git a/src/main/java/com/yunhai/agent/service/AgentOrchestrator.java b/src/main/java/com/yunhai/agent/service/AgentOrchestrator.java index 7689bb1..2cb8c21 100644 --- a/src/main/java/com/yunhai/agent/service/AgentOrchestrator.java +++ b/src/main/java/com/yunhai/agent/service/AgentOrchestrator.java @@ -28,6 +28,7 @@ import org.springframework.stereotype.Service; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.LinkedHashMap; @@ -761,6 +762,29 @@ public class AgentOrchestrator { form.setMissing(missingLabels); form.setAllFields(def.isFormAll()); + // 「栏目动态字段」(专业发展的栏目自定义字段):构建期枚举不出来,只能把「去哪儿取」 + // 交给前端带用户登录态取回后就地渲染,提交时打包进 extraFieldsParam(contents)。 + if (StringUtils.isNotBlank(def.getExtraFieldsFrom())) { + // 占位符用**模块侧的查询参数名**(如 SerID),映射告诉前端该拿哪个表单字段去填 + BrowserToolRequest.BrowserCall call = + buildReadCall(def.getExtraFieldsFrom(), def.getExtraFieldsDepends().keySet(), caller); + if (call != null) { + FormRequest.ExtraFields extra = new FormRequest.ExtraFields(); + extra.setCall(call); + extra.setDependsOn(new LinkedHashMap<>(def.getExtraFieldsDepends())); + extra.setParam(def.getExtraFieldsParam()); + extra.setBefore(def.getExtraFieldsBefore()); + // 级联字段的区划树(含 {{level}} 占位符,按字段配置取 2 或 3 级) + if (StringUtils.isNotBlank(def.getExtraFieldsAreaFrom())) { + extra.setAreaCall(buildReadCall(def.getExtraFieldsAreaFrom(), + Collections.singletonList("level"), caller)); + } + form.setExtraFields(extra); + } else { + log.warn("栏目动态字段代取不可用:tool={} dict={}", def.getName(), def.getExtraFieldsFrom()); + } + } + // 已从对话里确定的参数 → 表单里只读回显(值翻译成中文,用户能一眼核对) Map preset = new LinkedHashMap<>(); List> recognized = new ArrayList<>(); @@ -836,21 +860,37 @@ public class AgentOrchestrator { * 服务端直调必然 302,所以只能把地址交给前端、由前端带用户登录态去取。 */ private BrowserToolRequest.BrowserCall buildOptionCall(ToolParam p, CallerContext caller) { - if (StringUtils.isBlank(p.getOptionsFrom())) { + return buildReadCall(p.getOptionsFrom(), p.getDependsOn(), caller); + } + + /** + * 生成「浏览器代取」的调用描述(下拉字典与「栏目动态字段」共用)。 + * + *

差别只在取回的东西怎么用:字典取回的是选项(渲染成下拉), + * 栏目字段取回的是**字段定义**(前端就地生成控件)。地址拼装与权限校验完全一致: + * 只读工具 + 角色可见 + 依赖字段留 {@code {{占位符}}} 由前端替换。 + * + * @param toolName 只读工具名(如 leave.classStudents / profession.moduleFields) + * @param dependsOn 需要留占位符的依赖字段名;为空 = 无参调用 + * @return 调用描述;工具不存在 / 非只读 / 越权 / 模块未配地址时返回 {@code null} + */ + private BrowserToolRequest.BrowserCall buildReadCall(String toolName, Collection dependsOn, + CallerContext caller) { + if (StringUtils.isBlank(toolName)) { return null; } try { - ToolDef dict = registry.get(p.getOptionsFrom()); + ToolDef dict = registry.get(toolName); if (dict == null || !dict.isReadOnly()) { return null; } - // 与 fetchDictOptions 同源的角色约束:越权的字典不外泄地址 + // 与 fetchDictOptions 同源的角色约束:越权的字典/字段定义不外泄地址 if (settings.isEnforceRolePermission() && !dict.allowsAny(caller.getRoles())) { return null; } - return executor.prepareOptionCall(dict, p.getDependsOn()); + return executor.prepareOptionCall(dict, dependsOn); } catch (Exception e) { - log.warn("生成代取选项描述失败 {}:{}", p.getOptionsFrom(), e.toString()); + log.warn("生成代取描述失败 {}:{}", toolName, e.toString()); return null; } } diff --git a/src/main/java/com/yunhai/agent/tool/FormRequest.java b/src/main/java/com/yunhai/agent/tool/FormRequest.java index 87571be..3ecc0ee 100644 --- a/src/main/java/com/yunhai/agent/tool/FormRequest.java +++ b/src/main/java/com/yunhai/agent/tool/FormRequest.java @@ -57,4 +57,50 @@ public class FormRequest { /** 补充提示(可选),如「时间格式为 2026-10-11 08:00:00」 */ private String hint; + + /** + * 「栏目动态字段」——由业务模块的栏目配置决定、后端在构建期无法枚举的字段。 + * + *

背景:教师专业发展的每个栏目(教学设计 / 课题研究…)可以挂一组管理员自定义字段 + * (`GET /api/moudule/Getdatelist?SerID=<栏目 id>` 返回 `{name, fields:[{id,fieldName,fieldType,…}]}`), + * 模块页在「发布」表单里把这一组字段渲染在正文之前,提交时打包成 `contents` + * (`JSON.stringify([{fieldId, value}])`)。助手若要真正对齐,就必须在**选好栏目之后** + * 把这组字段取回来渲染 —— 但它既不是工具的固定参数,也不在构建期可知。 + * + *

做法与下拉字典一致:后端只下发「去哪儿取」({@link ExtraFields#call}), + * 前端带用户登录态取回定义、就地渲染、提交时打包进 {@link ExtraFields#param}。 + * 取不到时不渲染该区块(不阻塞发布,只是少填了自定义字段)。 + */ + private ExtraFields extraFields; + + /** 栏目动态字段的代取描述:`call` 给出地址(含 {{占位符}}),`param` 给出打包后的参数名 */ + @Data + public static class ExtraFields { + /** 带占位符的浏览器代取调用(依赖字段有值后由前端替换 {{占位符}} 再发) */ + private com.yunhai.agent.model.BrowserToolRequest.BrowserCall call; + /** + * 占位符 → 表单字段名 的映射(如 {@code SerID → moduleId})。 + * + *

两个用途:① 全部依赖字段有值才发起请求;② 把地址里的 {@code {{SerID}}} + * 换成 {@code moduleId} 的当前值。之所以要映射而不是同名:模块侧查询参数习惯用 + * {@code SerID} 这类老式命名,与表单字段名并不一致。 + */ + private Map dependsOn = new LinkedHashMap<>(); + /** 渲染结果打包后写进哪个提交参数(专业发展是 contents) */ + private String param; + /** 区块标题(模块页叫「栏目动态字段」) */ + private String title = "栏目动态字段"; + /** + * 渲染在哪个字段**之前**(对齐模块页的版式:栏目字段在正文与附件之上)。 + *

为空则追加到末尾;字段不存在时同样追加到末尾。 + */ + private String before; + /** 响应里字段数组所在的键(默认 fields,即 data.fields) */ + private String fieldsKey = "fields"; + /** + * 「省市区」级联字段的区划树代取调用(含 {@code {{level}}} 占位符,由字段配置决定 2 或 3)。 + *

为空表示该表单不支持级联字段。 + */ + private com.yunhai.agent.model.BrowserToolRequest.BrowserCall areaCall; + } } diff --git a/src/main/java/com/yunhai/agent/tool/ToolDef.java b/src/main/java/com/yunhai/agent/tool/ToolDef.java index af37323..9c81ec3 100644 --- a/src/main/java/com/yunhai/agent/tool/ToolDef.java +++ b/src/main/java/com/yunhai/agent/tool/ToolDef.java @@ -128,6 +128,67 @@ public class ToolDef { return this; } + /** + * 「栏目动态字段」的来源(可选)——某些业务模块的发布表单里有一组**由栏目配置决定**、 + * 构建期无法枚举的字段(教师专业发展的 `Getdatelist`)。 + * + *

声明后:{@code buildForm} 会把「去哪儿取定义」下发到 + * {@code FormRequest.extraFields},由前端带用户登录态取回、就地渲染, + * 提交时打包进 {@link #extraFieldsParam} 指定的参数(如 {@code contents})。 + */ + private String extraFieldsFrom; + + /** 栏目动态字段打包后的提交参数名(如 contents) */ + private String extraFieldsParam; + + /** 取栏目字段定义时的「查询参数名 → 表单字段名」映射(如 SerID → moduleId) */ + private Map extraFieldsDepends = new LinkedHashMap<>(); + + /** 「省市区」级联字段的区划树来源(只读工具名,可空 = 该表单不支持级联字段) */ + private String extraFieldsAreaFrom; + + /** 栏目动态字段渲染在哪个字段之前(对齐模块页版式;空 = 追加到末尾) */ + private String extraFieldsBefore; + + /** + * 声明「栏目动态字段」:字段定义从 {@code dictTool} 代取,渲染后打包进 {@code param}。 + * + *

★ {@code dependsOn} 要的是「查询参数名 → 表单字段名」的两边映射,而不是同名: + * 模块侧查询参数习惯用老式命名(专业发展的栏目字段接口是 {@code SerID}), + * 与表单字段名({@code moduleId})并不一致 —— 既要留 {@code {{SerID}}} 占位符, + * 也要知道该用表单里哪个字段的值去填它。 + * + * @param dictTool 提供字段定义的只读工具(GET) + * @param param 打包后的提交参数名(专业发展是 contents) + * @param dependsOn 查询参数名 → 表单字段名(如 SerID → moduleId) + */ + public ToolDef extraFields(String dictTool, String param, Map dependsOn) { + this.extraFieldsFrom = dictTool; + this.extraFieldsParam = param; + if (dependsOn != null) { + this.extraFieldsDepends.putAll(dependsOn); + } + return this; + } + + /** 声明「省市区」级联字段的区划树来源(如 profession.areaTree,地址里带 {{level}} 占位符) */ + public ToolDef extraFieldsArea(String dictTool) { + this.extraFieldsAreaFrom = dictTool; + return this; + } + + /** + * 把「栏目动态字段」渲染在某个字段之前(对齐模块页的版式)。 + *

模块页是「标题 / 年级段学科 / 是否公开 → 栏目动态字段 → 正文 → 附件」, + * 不锚定的话这一组字段会掉在附件后面,用户一眼就能看出与系统不一致。 + * + * @param fieldName 该工具的参数名(如 remark) + */ + public ToolDef extraFieldsBefore(String fieldName) { + this.extraFieldsBefore = fieldName; + return this; + } + public ToolDef() { } diff --git a/src/main/java/com/yunhai/agent/tool/defs/ProfessionTools.java b/src/main/java/com/yunhai/agent/tool/defs/ProfessionTools.java index 6f1ab2b..52e495f 100644 --- a/src/main/java/com/yunhai/agent/tool/defs/ProfessionTools.java +++ b/src/main/java/com/yunhai/agent/tool/defs/ProfessionTools.java @@ -4,10 +4,25 @@ 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。 + * + *

★ 发布表单的字段以模块前端源码为**权威契约**: + * {@code ProfessionView/src/views/profession/RecordEdit.vue}(发布/编辑记录页)。 + * 提交体(模块页 {@code save()} 逐行核对): + *

+ * POST /api/profession/Create   (application/x-www-form-urlencoded)
+ * { moduleId, title, grade, subject, ifOpen, remark,
+ *   contents: JSON.stringify([{fieldId, value}]),   // 栏目动态字段
+ *   fileIds: "11,12" }                              // 逗号串
+ * 
+ * 与助手此前实现的差异(本轮修正):早先只有 moduleId/title/content 三项, + * 既缺「年级段 / 学科 / 是否公开 / 正文(字段名应是 remark 而不是 content)」, + * 也缺栏目动态字段与附件 —— 发出去的记录与在系统里发的天然不同。 */ public final class ProfessionTools { @@ -63,6 +78,38 @@ public final class ProfessionTools { "GET", "/api/moudule/ModuleTree") .readAt("/record/new")); + /* ==================== 发布表单的字典(都必须由前端带登录态代取) ==================== + * 模块的用户态接口只认浏览器 CAS 会话,服务端直取必然 code=302, + * 因此这里只声明「去哪儿取」,由 AgentOrchestrator 下发 optionCall / extraFields.call。 + */ + + // 年级段:模块页 RecordEdit.vue 的 grades(