diff --git a/src/main/java/com/yunhai/agent/service/AgentOrchestrator.java b/src/main/java/com/yunhai/agent/service/AgentOrchestrator.java index 0eec2b8..7689bb1 100644 --- a/src/main/java/com/yunhai/agent/service/AgentOrchestrator.java +++ b/src/main/java/com/yunhai/agent/service/AgentOrchestrator.java @@ -641,6 +641,49 @@ public class AgentOrchestrator { return missing; } + /** + * 模块在站点根下的**路径前缀**(`homeleave` → `/home-leave`)。 + * + *

★ 不能用模块 key 直接拼:key 只是 `agent.modules` 的内部标识,真实路由前缀可能不同 —— + * 请假正是这种情况,key 是 `homeleave` 而前缀是 `/home-leave`(差一个连字符)。 + * 早先附件上传按 `"/" + def.getModule() + "/api/base/FileUpload"` 拼,于是请假的佐证材料 + * POST 到 `/homeleave/api/base/FileUpload`,被网关当成不存在的应用直接 **404** + * (实测:`/home-leave/...` 回 302 走 CAS,`/homeleave/...` 回 caddy 404)。 + * + * @param module 模块标识(`ToolDef.module`) + * @return 路径前缀,以 `/` 开头且不以 `/` 结尾;模块未配置时退回 `/` + */ + private String modulePathPrefix(String module) { + return pathPrefix(module, settings.getModules().get(module)); + } + + /** + * 由模块的**完整地址**推出它在站点根下的路径前缀(`homeleave` + `…/home-leave` → `/home-leave`)。 + * + * @param module 模块标识(仅用于兜底) + * @param moduleBaseUrl `agent.modules` 里配置的地址,如 `http://host/home-leave` + */ + public static String pathPrefix(String module, String moduleBaseUrl) { + if (StringUtils.isBlank(moduleBaseUrl)) { + return "/" + module; + } + String rest = moduleBaseUrl; + int scheme = rest.indexOf("://"); + if (scheme >= 0) { + rest = rest.substring(scheme + 3); + } + int slash = rest.indexOf('/'); + if (slash < 0) { + // 形如 http://host(没有路径部分)⇒ 模块就挂在站点根,退回 key 前缀 + return "/" + module; + } + String path = rest.substring(slash); + while (path.length() > 1 && path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + return path.isEmpty() ? "/" + module : path; + } + /** 由工具定义生成补全表单 */ private FormRequest buildForm(ToolDef def, Map args, List missing, CallerContext caller) { @@ -672,11 +715,12 @@ public class AgentOrchestrator { for (ToolParam p : shown) { FormField f = p.toFormField(); // 附件控件:补上传地址(模块的附件上传只认浏览器 CAS 会话,必须前端带登录态 POST)。 - // 缺省按 「/<模块>/api/base/FileUpload」,个别模块不同(作业是 /api/file/Upload)时由参数覆盖。 + // 缺省按「模块真实前缀 + /api/base/FileUpload」,个别模块上传接口不同(作业是 /api/file/Upload) + // 时由参数覆盖。★ 前缀必须取自 agent.modules 的配置,不能用模块 key 拼(见 modulePathPrefix)。 if ("file".equals(f.getWidget())) { f.setUploadPath(StringUtils.isNotBlank(p.getUploadPathOverride()) ? p.getUploadPathOverride() - : "/" + def.getModule() + "/api/base/FileUpload"); + : modulePathPrefix(def.getModule()) + "/api/base/FileUpload"); } // 字典类字段(如请假类型、班级、学生):优先交给前端用**用户自己的登录态**代取。 // diff --git a/src/test/java/com/yunhai/agent/ModuleFormParityTest.java b/src/test/java/com/yunhai/agent/ModuleFormParityTest.java index f335aa1..c5d324c 100644 --- a/src/test/java/com/yunhai/agent/ModuleFormParityTest.java +++ b/src/test/java/com/yunhai/agent/ModuleFormParityTest.java @@ -2,6 +2,7 @@ package com.yunhai.agent; import com.yunhai.agent.config.AgentSettings; import com.yunhai.agent.model.BrowserToolRequest; +import com.yunhai.agent.service.AgentOrchestrator; import com.yunhai.agent.tool.FormField; import com.yunhai.agent.tool.ToolDef; import com.yunhai.agent.tool.ToolExecutor; @@ -48,7 +49,9 @@ class ModuleFormParityTest { settings.getModules().put("campusnotice", "http://base.yunhaixingtong.com/campusnotice"); settings.getModules().put("dormnotice", "http://base.yunhaixingtong.com/dormnotice"); settings.getModules().put("familyactivity", "http://base.yunhaixingtong.com/familyactivity"); - settings.getModules().put("homeleave", "http://base.yunhaixingtong.com/homeleave"); + // ★ 前缀是 /home-leave(差一个连字符)—— 早先这里跟着代码一起写成 /homeleave, + // 于是「附件上传地址拼错」这个 bug 在测试里也被一并复制、永远测不出来。 + settings.getModules().put("homeleave", "http://base.yunhaixingtong.com/home-leave"); settings.getModules().put("coursehomework", "http://base.yunhaixingtong.com/coursehomework"); registry = new ToolRegistry(settings); registry.init(); @@ -226,7 +229,45 @@ class ModuleFormParityTest { call.getFollowUp().getPathAndQuery(), "第二步地址要带模块前缀,并留 {data} 给首步的 id"); } - /* ==================== ⑥ 附件控件渲染 ==================== */ + /* ==================== ⑥ 附件上传地址:前缀必须取自配置,不能用模块 key 拼 ==================== */ + + /** + * ★ 回归:附件上传地址的前缀必须取自 `agent.modules` 的配置。 + * + *

请假的 key 是 {@code homeleave},真实路由前缀是 {@code /home-leave} —— 差一个连字符。 + * 早先按 {@code "/" + def.getModule() + "/api/base/FileUpload"} 拼,请求打到 + * {@code /homeleave/api/base/FileUpload},被网关当成不存在的应用直接 **404** + * —— 用户上传佐证材料时实际撞到的就是这个。 + */ + @Test + void uploadPathPrefixComesFromConfiguredUrlNotModuleKey() { + assertEquals("/home-leave", + AgentOrchestrator.pathPrefix("homeleave", "http://base.yunhaixingtong.com/home-leave"), + "请假的前缀带连字符,不能拿 key(homeleave) 顶替"); + assertEquals("/home-leave/api/base/FileUpload", + AgentOrchestrator.pathPrefix("homeleave", "http://base.yunhaixingtong.com/home-leave") + + "/api/base/FileUpload"); + assertEquals("/campusnotice", + AgentOrchestrator.pathPrefix("campusnotice", "http://base.yunhaixingtong.com/campusnotice")); + assertEquals("/dormnotice", + AgentOrchestrator.pathPrefix("dormnotice", "http://base.yunhaixingtong.com/dormnotice/"), + "尾部斜杠要吃掉,否则会拼出 //api/base/FileUpload"); + // 地址没有路径部分 / 完全未配置 → 退回 key 前缀,至少不会拼出空串 + assertEquals("/x", AgentOrchestrator.pathPrefix("x", "http://host")); + assertEquals("/y", AgentOrchestrator.pathPrefix("y", null)); + } + + /** 四个走默认上传接口的模块都要能拼出「以 / 开头且无双斜杠」的地址 */ + @Test + void everyModuleResolvesAnAbsoluteUploadPath() { + for (String m : Arrays.asList("campusnotice", "dormnotice", "familyactivity", "homeleave")) { + String p = AgentOrchestrator.pathPrefix(m, settings.getModules().get(m)) + "/api/base/FileUpload"; + assertTrue(p.startsWith("/"), m + " 的上传地址要以 / 开头,实际:" + p); + assertFalse(p.contains("//"), m + " 的上传地址不能出现双斜杠,实际:" + p); + } + } + + /* ==================== ⑦ 附件控件渲染 ==================== */ @Test void scalarValueIsWrappedForArrayParams() {