mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 11:58:34 +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;
|
package vip.mate.wiki.controller;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -16,7 +17,9 @@ import vip.mate.workspace.core.annotation.RequireWorkspaceRole;
|
|||||||
import vip.mate.wiki.WikiProperties;
|
import vip.mate.wiki.WikiProperties;
|
||||||
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
|
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
|
||||||
import vip.mate.wiki.model.WikiPageEntity;
|
import vip.mate.wiki.model.WikiPageEntity;
|
||||||
|
import vip.mate.wiki.model.WikiPageTypeProfileEntity;
|
||||||
import vip.mate.wiki.model.WikiRawMaterialEntity;
|
import vip.mate.wiki.model.WikiRawMaterialEntity;
|
||||||
|
import vip.mate.wiki.profile.WikiPageTypeProfileService;
|
||||||
import vip.mate.wiki.service.WikiDirectoryScanService;
|
import vip.mate.wiki.service.WikiDirectoryScanService;
|
||||||
import vip.mate.wiki.service.WikiKnowledgeBaseService;
|
import vip.mate.wiki.service.WikiKnowledgeBaseService;
|
||||||
import vip.mate.wiki.service.WikiLintJobService;
|
import vip.mate.wiki.service.WikiLintJobService;
|
||||||
@ -55,6 +58,8 @@ public class WikiController {
|
|||||||
private final WikiProperties properties;
|
private final WikiProperties properties;
|
||||||
private final WikiProgressBus progressBus;
|
private final WikiProgressBus progressBus;
|
||||||
private final AuditEventService auditEventService;
|
private final AuditEventService auditEventService;
|
||||||
|
private final WikiPageTypeProfileService pageTypeProfileService;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
// ==================== Knowledge Base ====================
|
// ==================== Knowledge Base ====================
|
||||||
|
|
||||||
@ -189,6 +194,77 @@ public class WikiController {
|
|||||||
return R.ok();
|
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 ====================
|
// ==================== Directory Scan ====================
|
||||||
|
|
||||||
@RequireWorkspaceRole("member")
|
@RequireWorkspaceRole("member")
|
||||||
|
|||||||
@ -89,6 +89,98 @@ public class WikiPageTypeProfileService {
|
|||||||
return resolveProfile(kbId).getPageTypes().keySet();
|
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
|
* Normalise a routed/created pageType against the KB profile: a declared
|
||||||
* type is returned as-is (lowercase); an unknown type is downgraded to the
|
* 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