diff --git a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresSearchRebuildService.java b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresSearchRebuildService.java index 97e2d560..20d1ecf9 100644 --- a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresSearchRebuildService.java +++ b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresSearchRebuildService.java @@ -14,6 +14,7 @@ import com.iflytek.skillhub.domain.skill.SkillRepository; import com.iflytek.skillhub.domain.skill.SkillStatus; import com.iflytek.skillhub.domain.skill.SkillVersion; import com.iflytek.skillhub.domain.skill.SkillVersionRepository; +import com.iflytek.skillhub.domain.skill.metadata.ComplianceMetadataService; import com.iflytek.skillhub.search.SearchIndexService; import com.iflytek.skillhub.search.SearchRebuildService; import com.iflytek.skillhub.search.SearchTextTokenizer; @@ -127,9 +128,13 @@ public class PostgresSearchRebuildService implements SearchRebuildService { Set keywords = new TreeSet<>(); resolveLatestVersion(skill) .map(this::extractParsedMetadata) - .map(metadata -> metadata.get("frontmatter")) - .map(this::asMap) - .ifPresent(frontmatter -> appendFrontmatter(frontmatter, keywords, searchParts)); + .ifPresent(metadata -> { + appendFrontmatter(asMap(metadata.get("frontmatter")), keywords, searchParts); + appendComplianceSnapshot( + asMap(metadata.get(ComplianceMetadataService.SNAPSHOT_FIELD_NAME)), + keywords, + searchParts); + }); appendLabelKeywords(skill.getId(), keywords); return new SearchIndexPayload( @@ -197,6 +202,29 @@ public class PostgresSearchRebuildService implements SearchRebuildService { } } + private void appendComplianceSnapshot(Map snapshot, + Set keywords, + List searchParts) { + Object rawItems = snapshot.get("items"); + if (!(rawItems instanceof Collection items)) { + return; + } + for (Object rawItem : items) { + Map item = asMap(rawItem); + appendComplianceSearchValue(item.get("standard"), keywords, searchParts); + appendComplianceSearchValue(item.get("version"), keywords, searchParts); + appendComplianceSearchValue(item.get("controlId"), keywords, searchParts); + appendComplianceSearchValue(item.get("title"), keywords, searchParts); + } + } + + private void appendComplianceSearchValue(Object value, Set keywords, List searchParts) { + flattenToStrings(value).forEach(text -> { + addKeyword(keywords, text); + addPart(searchParts, text); + }); + } + private List flattenToStrings(Object value) { if (value == null) { return List.of(); diff --git a/server/skillhub-search/src/test/java/com/iflytek/skillhub/search/postgres/PostgresSearchRebuildServiceTest.java b/server/skillhub-search/src/test/java/com/iflytek/skillhub/search/postgres/PostgresSearchRebuildServiceTest.java index 4abd1e63..e8fcd88a 100644 --- a/server/skillhub-search/src/test/java/com/iflytek/skillhub/search/postgres/PostgresSearchRebuildServiceTest.java +++ b/server/skillhub-search/src/test/java/com/iflytek/skillhub/search/postgres/PostgresSearchRebuildServiceTest.java @@ -158,6 +158,75 @@ class PostgresSearchRebuildServiceTest { assertThat(document.searchText()).doesNotContain("智能体"); } + @Test + void rebuildBySkill_shouldAppendComplianceSnapshotMappingsIntoSearchDocument() { + SkillRepository skillRepository = mock(SkillRepository.class); + NamespaceRepository namespaceRepository = mock(NamespaceRepository.class); + SkillVersionRepository skillVersionRepository = mock(SkillVersionRepository.class); + SearchIndexService searchIndexService = mock(SearchIndexService.class); + + Skill skill = new Skill(7L, "terminal-agent", "owner-1", SkillVisibility.PUBLIC); + skill.setDisplayName("Terminal Agent"); + skill.setSummary("Runs shell workflows safely"); + skill.setLatestVersionId(104L); + + Namespace namespace = new Namespace("team-ai", "Team AI", "owner-1"); + SkillVersion version = new SkillVersion(1L, "1.7.0", "owner-1"); + version.setParsedMetadataJson(""" + { + "frontmatter": { + "keywords": ["shell"] + }, + "complianceSnapshot": { + "schemaVersion": "1", + "digest": "sha256:abc123", + "items": [ + { + "standard": "mitre-attack", + "version": "15", + "controlId": "T1059", + "title": "Command and Scripting Interpreter", + "evidence": [ + { + "type": "packaged-file", + "path": "docs/security.md", + "sha256": "sha256:def456" + } + ] + } + ] + } + } + """); + + when(skillRepository.findById(1L)).thenReturn(Optional.of(skill)); + when(namespaceRepository.findById(7L)).thenReturn(Optional.of(namespace)); + when(skillVersionRepository.findById(104L)).thenReturn(Optional.of(version)); + + PostgresSearchRebuildService service = newService( + skillRepository, + namespaceRepository, + skillVersionRepository, + searchIndexService + ); + + service.rebuildBySkill(1L); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SkillSearchDocument.class); + verify(searchIndexService).index(captor.capture()); + + SkillSearchDocument document = captor.getValue(); + assertThat(document.keywords()).contains("shell"); + assertThat(document.keywords()).contains("mitre-attack"); + assertThat(document.keywords()).contains("T1059"); + assertThat(document.keywords()).contains("Command and Scripting Interpreter"); + assertThat(document.searchText()).contains("mitre-attack"); + assertThat(document.searchText()).contains("T1059"); + assertThat(document.searchText()).contains("Command and Scripting Interpreter"); + assertThat(document.searchText()).doesNotContain("docs/security.md"); + assertThat(document.searchText()).doesNotContain("sha256:def456"); + } + @Test void rebuildBySkill_shouldIgnoreNullFrontmatterValues() { SkillRepository skillRepository = mock(SkillRepository.class);