diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatAppService.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatAppService.java index 539b5221..051e9566 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatAppService.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatAppService.java @@ -14,6 +14,7 @@ import com.iflytek.skillhub.controller.support.MultipartPackageExtractor; import com.iflytek.skillhub.controller.support.ZipPackageExtractor; import com.iflytek.skillhub.domain.audit.AuditLogService; import com.iflytek.skillhub.domain.namespace.NamespaceRole; +import com.iflytek.skillhub.domain.shared.exception.DomainNotFoundException; import com.iflytek.skillhub.domain.skill.SkillVersion; import com.iflytek.skillhub.domain.skill.SkillVisibility; import com.iflytek.skillhub.domain.skill.service.SkillPublishService; @@ -93,11 +94,11 @@ public class ClawHubCompatAppService { String hash, String userId, Map userNsRoles) { - CompatSkillLookupService.CompatSkillContext context = compatSkillLookupService.findByLegacySlug(slug); + SkillCoordinate coord = resolveQueryCoordinate(slug); SkillQueryService.ResolvedVersionDTO resolved = skillQueryService.resolveVersion( - context.namespace().getSlug(), - context.skill().getSlug(), + coord.namespace(), + coord.slug(), "latest".equals(version) ? null : version, "latest".equals(version) ? "latest" : null, hash, @@ -132,10 +133,22 @@ public class ClawHubCompatAppService { } public String downloadLocationByQuery(String slug, String version) { - CompatSkillLookupService.CompatSkillContext context = compatSkillLookupService.findByLegacySlug(slug); + SkillCoordinate coord = resolveQueryCoordinate(slug); return "latest".equals(version) - ? "/api/v1/skills/" + context.namespace().getSlug() + "/" + context.skill().getSlug() + "/download" - : "/api/v1/skills/" + context.namespace().getSlug() + "/" + context.skill().getSlug() + "/versions/" + version + "/download"; + ? "/api/v1/skills/" + coord.namespace() + "/" + coord.slug() + "/download" + : "/api/v1/skills/" + coord.namespace() + "/" + coord.slug() + "/versions/" + version + "/download"; + } + + private SkillCoordinate resolveQueryCoordinate(String slug) { + if (slug != null && slug.contains("--")) { + return mapper.fromCanonical(slug); + } + try { + CompatSkillLookupService.CompatSkillContext context = compatSkillLookupService.findByLegacySlug(slug); + return new SkillCoordinate(context.namespace().getSlug(), context.skill().getSlug()); + } catch (DomainNotFoundException ex) { + return mapper.fromCanonical(slug); + } } public ClawHubSkillListResponse listSkills(int page, diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/compat/ClawHubCompatControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/compat/ClawHubCompatControllerTest.java index c024cf08..421fa1d8 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/compat/ClawHubCompatControllerTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/compat/ClawHubCompatControllerTest.java @@ -22,6 +22,7 @@ import java.util.Set; import java.math.BigDecimal; import java.time.Instant; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @@ -105,6 +106,56 @@ class ClawHubCompatControllerTest { .andExpect(jsonPath("$.latestVersion.version").value("latest")); } + @Test + void resolve_query_with_canonical_slug_returns_correct_downloadUrl() throws Exception { + when(skillQueryService.resolveVersion("team-ai", "my-skill", null, "latest", null, null, java.util.Map.of())) + .thenReturn(new SkillQueryService.ResolvedVersionDTO( + 1L, "team-ai", "my-skill", "latest", 2L, "sha", true, "/api/v1/skills/team-ai/my-skill/download")); + + mockMvc.perform(get("/api/v1/resolve") + .param("slug", "team-ai--my-skill") + .param("version", "latest")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.match.version").value("latest")) + .andExpect(jsonPath("$.latestVersion.version").value("latest")); + + verify(skillQueryService).resolveVersion("team-ai", "my-skill", null, "latest", null, null, java.util.Map.of()); + } + + @Test + void resolve_query_with_legacy_slug_keeps_legacy_lookup_behavior() throws Exception { + when(skillQueryService.resolveVersion("global", "my-skill", null, "latest", null, null, java.util.Map.of())) + .thenReturn(new SkillQueryService.ResolvedVersionDTO( + 1L, "global", "my-skill", "latest", 2L, "sha", true, "/api/v1/skills/global/my-skill/download")); + + mockMvc.perform(get("/api/v1/resolve") + .param("slug", "my-skill") + .param("version", "latest")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.match.version").value("latest")) + .andExpect(jsonPath("$.latestVersion.version").value("latest")); + + verify(skillQueryService).resolveVersion("global", "my-skill", null, "latest", null, null, java.util.Map.of()); + } + + @Test + void download_query_with_canonical_slug_redirects_to_namespace_skill_download() throws Exception { + mockMvc.perform(get("/api/v1/download") + .param("slug", "team-ai--my-skill") + .param("version", "latest")) + .andExpect(status().isFound()) + .andExpect(header().string("Location", "/api/v1/skills/team-ai/my-skill/download")); + } + + @Test + void download_query_with_legacy_slug_keeps_legacy_lookup_behavior() throws Exception { + mockMvc.perform(get("/api/v1/download") + .param("slug", "my-skill") + .param("version", "latest")) + .andExpect(status().isFound()) + .andExpect(header().string("Location", "/api/v1/skills/global/my-skill/download")); + } + @Test void resolve_with_version_returns_specified_version() throws Exception { when(skillQueryService.resolveVersion("global", "my-skill", "1.0.0", null, null, null, java.util.Map.of()))