mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(wiki): pageType profile CRUD API and service operations
This commit is contained in:
parent
7cd9971596
commit
bea48bcf89
@ -1,5 +1,6 @@
|
||||
package vip.mate.wiki.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@ -16,7 +17,9 @@ import vip.mate.workspace.core.annotation.RequireWorkspaceRole;
|
||||
import vip.mate.wiki.WikiProperties;
|
||||
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
|
||||
import vip.mate.wiki.model.WikiPageEntity;
|
||||
import vip.mate.wiki.model.WikiPageTypeProfileEntity;
|
||||
import vip.mate.wiki.model.WikiRawMaterialEntity;
|
||||
import vip.mate.wiki.profile.WikiPageTypeProfileService;
|
||||
import vip.mate.wiki.service.WikiDirectoryScanService;
|
||||
import vip.mate.wiki.service.WikiKnowledgeBaseService;
|
||||
import vip.mate.wiki.service.WikiLintJobService;
|
||||
@ -55,6 +58,8 @@ public class WikiController {
|
||||
private final WikiProperties properties;
|
||||
private final WikiProgressBus progressBus;
|
||||
private final AuditEventService auditEventService;
|
||||
private final WikiPageTypeProfileService pageTypeProfileService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
// ==================== Knowledge Base ====================
|
||||
|
||||
@ -189,6 +194,77 @@ public class WikiController {
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
// ==================== PageType Profile ====================
|
||||
|
||||
@RequireWorkspaceRole("viewer")
|
||||
@Operation(summary = "获取知识库 pageType profile(未配置则返回内置默认)")
|
||||
@GetMapping("/knowledge-bases/{id}/page-type-profile")
|
||||
public R<Map<String, Object>> getPageTypeProfile(@PathVariable Long id,
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
||||
verifyKBWorkspace(id, workspaceId);
|
||||
WikiPageTypeProfileEntity row = pageTypeProfileService.findEnabledRow(id);
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
if (row != null) {
|
||||
out.put("name", row.getName());
|
||||
out.put("version", row.getVersion());
|
||||
out.put("config", row.getConfigJson());
|
||||
out.put("builtinDefault", false);
|
||||
} else {
|
||||
String json;
|
||||
try {
|
||||
json = objectMapper.writeValueAsString(pageTypeProfileService.getDefaultProfile());
|
||||
} catch (Exception e) {
|
||||
json = "{}";
|
||||
}
|
||||
out.put("name", "default");
|
||||
out.put("version", 0);
|
||||
out.put("config", json);
|
||||
out.put("builtinDefault", true);
|
||||
}
|
||||
return R.ok(out);
|
||||
}
|
||||
|
||||
@RequireWorkspaceRole("admin")
|
||||
@Operation(summary = "保存知识库 pageType profile")
|
||||
@PutMapping("/knowledge-bases/{id}/page-type-profile")
|
||||
public R<Void> savePageTypeProfile(@PathVariable Long id, @RequestBody Map<String, String> body,
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
||||
verifyKBWorkspace(id, workspaceId);
|
||||
String config = body.get("config");
|
||||
if (config == null || config.isBlank()) {
|
||||
return R.fail(400, "config is required");
|
||||
}
|
||||
try {
|
||||
pageTypeProfileService.saveProfile(id, body.get("name"), config);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return R.fail(400, e.getMessage());
|
||||
}
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@RequireWorkspaceRole("member")
|
||||
@Operation(summary = "校验 pageType profile JSON(不保存)")
|
||||
@PostMapping("/knowledge-bases/{id}/page-type-profile/validate")
|
||||
public R<Map<String, Object>> validatePageTypeProfile(@PathVariable Long id, @RequestBody Map<String, String> body,
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
||||
verifyKBWorkspace(id, workspaceId);
|
||||
List<String> issues = pageTypeProfileService.validateProfileJson(body.getOrDefault("config", ""));
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
out.put("valid", issues.isEmpty());
|
||||
out.put("issues", issues);
|
||||
return R.ok(out);
|
||||
}
|
||||
|
||||
@RequireWorkspaceRole("admin")
|
||||
@Operation(summary = "重置 pageType profile 为内置默认")
|
||||
@PostMapping("/knowledge-bases/{id}/page-type-profile/reset-default")
|
||||
public R<Void> resetPageTypeProfile(@PathVariable Long id,
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
||||
verifyKBWorkspace(id, workspaceId);
|
||||
pageTypeProfileService.resetToDefault(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
// ==================== Directory Scan ====================
|
||||
|
||||
@RequireWorkspaceRole("member")
|
||||
|
||||
@ -89,6 +89,98 @@ public class WikiPageTypeProfileService {
|
||||
return resolveProfile(kbId).getPageTypes().keySet();
|
||||
}
|
||||
|
||||
/** The enabled profile row for a KB, or {@code null} when none configured. */
|
||||
public WikiPageTypeProfileEntity findEnabledRow(Long kbId) {
|
||||
if (kbId == null) {
|
||||
return null;
|
||||
}
|
||||
return profileMapper.selectOne(
|
||||
new LambdaQueryWrapper<WikiPageTypeProfileEntity>()
|
||||
.eq(WikiPageTypeProfileEntity::getKbId, kbId)
|
||||
.eq(WikiPageTypeProfileEntity::getEnabled, 1)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a KB profile. Parses {@code configJson} first (rejecting invalid
|
||||
* JSON), then upserts the KB's single enabled row — updating in place and
|
||||
* bumping its version when one exists, else inserting a new enabled row.
|
||||
*
|
||||
* @throws IllegalArgumentException when {@code configJson} does not parse
|
||||
*/
|
||||
public void saveProfile(Long kbId, String name, String configJson) {
|
||||
try {
|
||||
objectMapper.readValue(configJson, WikiPageTypeProfile.class);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("Invalid profile config JSON: " + e.getMessage());
|
||||
}
|
||||
WikiPageTypeProfileEntity existing = findEnabledRow(kbId);
|
||||
if (existing != null) {
|
||||
existing.setConfigJson(configJson);
|
||||
if (name != null && !name.isBlank()) {
|
||||
existing.setName(name);
|
||||
}
|
||||
existing.setVersion((existing.getVersion() == null ? 1 : existing.getVersion()) + 1);
|
||||
profileMapper.updateById(existing);
|
||||
} else {
|
||||
WikiPageTypeProfileEntity row = new WikiPageTypeProfileEntity();
|
||||
row.setKbId(kbId);
|
||||
row.setName(name == null || name.isBlank() ? "default" : name);
|
||||
row.setVersion(1);
|
||||
row.setConfigJson(configJson);
|
||||
row.setEnabled(1);
|
||||
profileMapper.insert(row);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset a KB to the built-in default by removing its profile rows, so
|
||||
* {@link #resolveProfile} falls back to the default. Logical delete.
|
||||
*/
|
||||
public void resetToDefault(Long kbId) {
|
||||
if (kbId == null) {
|
||||
return;
|
||||
}
|
||||
profileMapper.delete(new LambdaQueryWrapper<WikiPageTypeProfileEntity>()
|
||||
.eq(WikiPageTypeProfileEntity::getKbId, kbId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Structurally validate a profile JSON without persisting it. Returns a
|
||||
* list of human-readable issues; empty means valid.
|
||||
*/
|
||||
public java.util.List<String> validateProfileJson(String configJson) {
|
||||
java.util.List<String> issues = new java.util.ArrayList<>();
|
||||
WikiPageTypeProfile profile;
|
||||
try {
|
||||
profile = objectMapper.readValue(configJson, WikiPageTypeProfile.class);
|
||||
} catch (Exception e) {
|
||||
issues.add("Invalid JSON: " + e.getMessage());
|
||||
return issues;
|
||||
}
|
||||
if (profile.getPageTypes() == null || profile.getPageTypes().isEmpty()) {
|
||||
issues.add("Profile declares no pageTypes");
|
||||
return issues;
|
||||
}
|
||||
java.util.Set<String> validTypes = java.util.Set.of(
|
||||
"string", "number", "boolean", "date", "enum", "string_array");
|
||||
profile.getPageTypes().forEach((typeName, def) -> {
|
||||
if (def.getSchema() == null) {
|
||||
return;
|
||||
}
|
||||
def.getSchema().forEach((fieldName, fieldSchema) -> {
|
||||
String t = fieldSchema.getType();
|
||||
if (t == null || !validTypes.contains(t.trim().toLowerCase())) {
|
||||
issues.add(typeName + "." + fieldName + ": unknown field type '" + t + "'");
|
||||
} else if ("enum".equalsIgnoreCase(t.trim())
|
||||
&& (fieldSchema.getValues() == null || fieldSchema.getValues().isEmpty())) {
|
||||
issues.add(typeName + "." + fieldName + ": enum field declares no values");
|
||||
}
|
||||
});
|
||||
});
|
||||
return issues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise a routed/created pageType against the KB profile: a declared
|
||||
* type is returned as-is (lowercase); an unknown type is downgraded to the
|
||||
|
||||
@ -0,0 +1,104 @@
|
||||
package vip.mate.wiki.profile;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import vip.mate.wiki.model.WikiPageTypeProfileEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link WikiPageTypeProfileService} CRUD against H2:
|
||||
* upsert keeps a single enabled row (no generated-column violation), reset
|
||||
* falls back to the default, and JSON validation reports structural issues.
|
||||
*/
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.NONE,
|
||||
properties = {
|
||||
"spring.flyway.enabled=true",
|
||||
"spring.flyway.locations=classpath:db/migration/h2",
|
||||
"mateclaw.feature-flag.refresh-ms=999999"
|
||||
}
|
||||
)
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
|
||||
class WikiPageTypeProfileServiceE2ETest {
|
||||
|
||||
@Autowired
|
||||
private WikiPageTypeProfileService service;
|
||||
|
||||
private static final String EPISODE_JSON =
|
||||
"{\"version\":1,\"pageTypes\":{\"episode\":{\"label\":\"Episode\"}}}";
|
||||
|
||||
@Test
|
||||
void saveThenResolve_roundTrips() {
|
||||
long kb = 5001L;
|
||||
service.saveProfile(kb, "liquidity", EPISODE_JSON);
|
||||
|
||||
WikiPageTypeProfileEntity row = service.findEnabledRow(kb);
|
||||
assertNotNull(row);
|
||||
assertEquals("liquidity", row.getName());
|
||||
assertEquals(1, row.getVersion());
|
||||
assertTrue(service.resolveProfile(kb).hasPageType("episode"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveTwice_upsertsInPlaceAndBumpsVersion() {
|
||||
long kb = 5002L;
|
||||
service.saveProfile(kb, "v1", EPISODE_JSON);
|
||||
// Saving again must update the single enabled row, not insert a second
|
||||
// (which would violate the one-enabled-per-KB generated-column UNIQUE).
|
||||
service.saveProfile(kb, "v1",
|
||||
"{\"version\":1,\"pageTypes\":{\"episode\":{\"label\":\"E\"},\"pattern\":{\"label\":\"P\"}}}");
|
||||
|
||||
WikiPageTypeProfileEntity row = service.findEnabledRow(kb);
|
||||
assertEquals(2, row.getVersion());
|
||||
assertTrue(service.resolveProfile(kb).hasPageType("pattern"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetToDefault_removesRowAndFallsBack() {
|
||||
long kb = 5003L;
|
||||
service.saveProfile(kb, "custom", EPISODE_JSON);
|
||||
assertNotNull(service.findEnabledRow(kb));
|
||||
|
||||
service.resetToDefault(kb);
|
||||
|
||||
assertNull(service.findEnabledRow(kb));
|
||||
// Resolution now returns the built-in default.
|
||||
assertTrue(service.resolveProfile(kb).hasPageType("concept"));
|
||||
assertFalse(service.resolveProfile(kb).hasPageType("episode"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidConfig_isRejectedOnSave() {
|
||||
try {
|
||||
service.saveProfile(5004L, "bad", "{ not json");
|
||||
org.junit.jupiter.api.Assertions.fail("expected IllegalArgumentException");
|
||||
} catch (IllegalArgumentException expected) {
|
||||
// ok
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateProfileJson_reportsIssues() {
|
||||
assertTrue(service.validateProfileJson(EPISODE_JSON).isEmpty());
|
||||
|
||||
List<String> noTypes = service.validateProfileJson("{\"pageTypes\":{}}");
|
||||
assertFalse(noTypes.isEmpty());
|
||||
|
||||
List<String> badEnum = service.validateProfileJson(
|
||||
"{\"pageTypes\":{\"x\":{\"schema\":{\"f\":{\"type\":\"enum\"}}}}}");
|
||||
assertTrue(badEnum.stream().anyMatch(s -> s.contains("enum")));
|
||||
|
||||
List<String> badType = service.validateProfileJson(
|
||||
"{\"pageTypes\":{\"x\":{\"schema\":{\"f\":{\"type\":\"banana\"}}}}}");
|
||||
assertTrue(badType.stream().anyMatch(s -> s.contains("unknown field type")));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user