diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/profile/WikiPageTypeProfileService.java b/mateclaw-server/src/main/java/vip/mate/wiki/profile/WikiPageTypeProfileService.java index a9427cd8..b286be29 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/profile/WikiPageTypeProfileService.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/profile/WikiPageTypeProfileService.java @@ -90,6 +90,56 @@ public class WikiPageTypeProfileService { return resolveProfile(kbId).getPageTypes().keySet(); } + /** + * The per-stage LLM instruction declared for a pageType, or empty string. + * {@code stage} is {@code route} / {@code create} / {@code merge}. Used to + * inject type-specific guidance into the corresponding stage prompt. + */ + public String stageInstruction(Long kbId, String pageType, String stage) { + WikiPageTypeDef def = resolveProfile(kbId).get(pageType); + if (def == null || stage == null) { + return ""; + } + WikiPageTypeDef.StageInstructions si = switch (stage) { + case "route" -> def.getRoute(); + case "create" -> def.getCreate(); + case "merge" -> def.getMerge(); + default -> null; + }; + return (si != null && si.getInstructions() != null) ? si.getInstructions().trim() : ""; + } + + /** + * Render the per-type Markdown templates as a prompt block for the + * multi-page batch-create stage (where one call generates pages of several + * types). Only types that declare a template are listed; empty when none. + */ + public String describeTemplatesForPrompt(Long kbId) { + WikiPageTypeProfile profile = resolveProfile(kbId); + StringBuilder sb = new StringBuilder(); + profile.getPageTypes().forEach((name, def) -> { + if (def != null && def.getTemplate() != null + && def.getTemplate().getMarkdown() != null + && !def.getTemplate().getMarkdown().isBlank()) { + sb.append("### ").append(name).append(" 骨架\n") + .append(def.getTemplate().getMarkdown().trim()).append("\n\n"); + } + }); + return sb.toString().trim(); + } + + /** + * The Markdown content template for a pageType, or empty string. Injected + * into the generation prompt so the page follows the declared skeleton. + */ + public String templateMarkdown(Long kbId, String pageType) { + WikiPageTypeDef def = resolveProfile(kbId).get(pageType); + if (def == null || def.getTemplate() == null || def.getTemplate().getMarkdown() == null) { + return ""; + } + return def.getTemplate().getMarkdown().trim(); + } + /** * Render the KB's allowed page types as a prompt fragment, one per line * with description and required-metadata hints, e.g. diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiProcessingService.java b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiProcessingService.java index e8186092..eaae6985 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiProcessingService.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiProcessingService.java @@ -813,7 +813,8 @@ public class WikiProcessingService { // this picks up changes from sequential chunks without an extra DB hit when nothing changed. String freshIndex = buildExistingPagesIndex(kbId); - String routeSystem = PromptLoader.loadPrompt("wiki/route-system"); + String routeSystem = PromptLoader.loadPrompt("wiki/route-system") + .replace("{allowed_page_types}", allowedTypesFragment(kbId)); String routeUserTemplate = PromptLoader.loadPrompt("wiki/route-user"); String documentMapSection = buildDocumentMapSection(documentMap); String routeUser = routeUserTemplate @@ -1082,10 +1083,13 @@ public class WikiProcessingService { // the profile recognises. Default-profile KBs get the same list // as the previous hardcoded enum, so behaviour is unchanged. batchSystem = batchSystem.replace("{allowed_page_types}", - pageTypeProfileService.describeForPrompt(kbId)); + pageTypeProfileService.describeForPrompt(kbId)) + .replace("{page_type_templates}", + emptyOr(pageTypeProfileService.describeTemplatesForPrompt(kbId))); } else { batchSystem = batchSystem.replace("{allowed_page_types}", - "concept / person / place / event / technology / organization / product / term / process / other"); + "concept / person / place / event / technology / organization / product / term / process / other") + .replace("{page_type_templates}", "(无)"); } String batchUserTemplate = PromptLoader.loadPrompt("wiki/batch-create-user"); String docMapSection = buildDocumentMapSection(documentMap); @@ -1264,7 +1268,9 @@ public class WikiProcessingService { String title = pageMeta.path("title").asText(""); String summary = pageMeta.path("summary").asText(""); String configContent = kb.getConfigContent() != null ? kb.getConfigContent() : ""; - String createSystem = PromptLoader.loadPrompt("wiki/create-page-system"); + String createSystem = PromptLoader.loadPrompt("wiki/create-page-system") + .replace("{page_type_instructions}", + typeGuidance(kb.getId(), pageMeta.path("page_type").asText(""), "create")); String createUserTemplate = PromptLoader.loadPrompt("wiki/create-page-user"); String createUser = createUserTemplate .replace("{config}", configContent) @@ -1492,6 +1498,38 @@ public class WikiProcessingService { } } + private String emptyOr(String s) { + return (s == null || s.isBlank()) ? "(无)" : s; + } + + /** Allowed page types fragment for prompt injection (profile-driven; legacy fallback). */ + private String allowedTypesFragment(Long kbId) { + return pageTypeProfileService != null + ? pageTypeProfileService.describeForPrompt(kbId) + : "concept / person / place / event / technology / organization / product / term / process / other"; + } + + /** + * Per-type guidance for the create / merge prompts: the stage instruction + * plus, for the create stage, the Markdown template skeleton. Empty-safe. + */ + private String typeGuidance(Long kbId, String pageType, String stage) { + if (pageTypeProfileService == null || pageType == null || pageType.isBlank()) { + return "(无特定指引)"; + } + String instr = pageTypeProfileService.stageInstruction(kbId, pageType, stage); + String tpl = "create".equals(stage) ? pageTypeProfileService.templateMarkdown(kbId, pageType) : ""; + StringBuilder sb = new StringBuilder(); + if (instr != null && !instr.isBlank()) { + sb.append(instr); + } + if (tpl != null && !tpl.isBlank()) { + if (sb.length() > 0) sb.append("\n\n"); + sb.append("请按以下 Markdown 骨架组织正文:\n").append(tpl); + } + return sb.length() == 0 ? "(无特定指引)" : sb.toString(); + } + private void applyValidatedMetadata(Long pageId, Long kbId, String pageType, JsonNode metadataNode) { if (pageId == null || pageTypeProfileService == null || metadataValidator == null) { @@ -1546,7 +1584,9 @@ public class WikiProcessingService { } String configContent = kb.getConfigContent() != null ? kb.getConfigContent() : ""; - String mergeSystem = PromptLoader.loadPrompt("wiki/merge-page-system"); + String mergeSystem = PromptLoader.loadPrompt("wiki/merge-page-system") + .replace("{page_type_merge_instruction}", + typeGuidance(kbId, existing.getPageType(), "merge")); // Trim existing content to prevent context overflow on small models (qwen-turbo: 4096 tokens). // Merging a 3000-char page + 30K chunk blows past the limit → truncated JSON → parse failure. // 1800 chars ≈ ~600 tokens, leaving ample room for the chunk and response. @@ -2348,7 +2388,9 @@ public class WikiProcessingService { // Use existing two-phase single-page create logic String existingPagesIndex = buildExistingPagesIndex(kb.getId()); String configContent = kb.getConfigContent() != null ? kb.getConfigContent() : ""; - String createSystem = PromptLoader.loadPrompt("wiki/create-page-system"); + String createSystem = PromptLoader.loadPrompt("wiki/create-page-system") + .replace("{page_type_instructions}", + typeGuidance(kb.getId(), page.getPageType(), "create")); String createUserTemplate = PromptLoader.loadPrompt("wiki/create-page-user"); String createUser = createUserTemplate .replace("{config}", configContent) diff --git a/mateclaw-server/src/main/resources/prompts/wiki/batch-create-system.txt b/mateclaw-server/src/main/resources/prompts/wiki/batch-create-system.txt index 67be8f0b..6d0a1612 100644 --- a/mateclaw-server/src/main/resources/prompts/wiki/batch-create-system.txt +++ b/mateclaw-server/src/main/resources/prompts/wiki/batch-create-system.txt @@ -55,3 +55,6 @@ 允许的页面类型: {allowed_page_types} + +各类型内容模板(若某页的 page_type 有对应骨架,请按骨架组织正文;无则自由组织): +{page_type_templates} diff --git a/mateclaw-server/src/main/resources/prompts/wiki/create-page-system.txt b/mateclaw-server/src/main/resources/prompts/wiki/create-page-system.txt index 940637b4..96a1db25 100644 --- a/mateclaw-server/src/main/resources/prompts/wiki/create-page-system.txt +++ b/mateclaw-server/src/main/resources/prompts/wiki/create-page-system.txt @@ -55,3 +55,6 @@ - `title`:通常与 metadata 一致;如有更精确的描述可微调 - `content`:完整 markdown,开头一段摘要,后面分章节 - `summary`:一段话简短摘要(可与 metadata.summary 一致或精炼) + +## 内容指引(本知识库 / 该页类型) +{page_type_instructions} diff --git a/mateclaw-server/src/main/resources/prompts/wiki/merge-page-system.txt b/mateclaw-server/src/main/resources/prompts/wiki/merge-page-system.txt index 868f077b..f1527769 100644 --- a/mateclaw-server/src/main/resources/prompts/wiki/merge-page-system.txt +++ b/mateclaw-server/src/main/resources/prompts/wiki/merge-page-system.txt @@ -47,3 +47,6 @@ - `title`:通常保持不变;只有当新材料明确改变了页面主题时才改 - `content`:完整 markdown,包含原内容中仍然有效的部分 + 来自新材料的新增信息 - `summary`:更新后的一段话摘要 + +## 该类型的合并策略(本知识库) +{page_type_merge_instruction} diff --git a/mateclaw-server/src/main/resources/prompts/wiki/route-system.txt b/mateclaw-server/src/main/resources/prompts/wiki/route-system.txt index 96174a5f..554855b1 100644 --- a/mateclaw-server/src/main/resources/prompts/wiki/route-system.txt +++ b/mateclaw-server/src/main/resources/prompts/wiki/route-system.txt @@ -68,6 +68,12 @@ - `update`:已存在但需要根据本材料合并更新的页面,**只列 slug 字符串数组,最多 5 条** - 两个数组合计通常至少 1 条(材料完全无价值——如空白页、纯目录——才允许全空) +## 允许的页面类型(本知识库) + +为每个 create 项判断最贴切的类型;不确定时按默认处理。可在 create 项加 `page_type` 字段。 + +{allowed_page_types} + ## 关键纪律 - 输出体积应该是几百到几千字,**不要超过几 KB**。如果你发现自己在写正文,立刻停下 —— 那是下一阶段的工作。 diff --git a/mateclaw-server/src/test/java/vip/mate/wiki/profile/WikiPageTypeProfileServiceTest.java b/mateclaw-server/src/test/java/vip/mate/wiki/profile/WikiPageTypeProfileServiceTest.java index 01c30f2b..9a0c72f8 100644 --- a/mateclaw-server/src/test/java/vip/mate/wiki/profile/WikiPageTypeProfileServiceTest.java +++ b/mateclaw-server/src/test/java/vip/mate/wiki/profile/WikiPageTypeProfileServiceTest.java @@ -89,6 +89,27 @@ class WikiPageTypeProfileServiceTest { assertTrue(fragment.contains("- other"), fragment); } + @Test + void stageInstructionAndTemplate_areResolvedPerType() { + WikiPageTypeProfileEntity row = new WikiPageTypeProfileEntity(); + row.setKbId(1L); + row.setEnabled(1); + row.setConfigJson("{\"pageTypes\":{\"episode\":{" + + "\"route\":{\"instructions\":\"仅当有明确日期时路由为 episode\"}," + + "\"merge\":{\"instructions\":\"保留已审阅分析,追加新证据\"}," + + "\"create\":{\"instructions\":\"抽取 event_date\"}," + + "\"template\":{\"markdown\":\"## Summary\\n{{summary}}\\n## 事件\"}}}}"); + when(mapper.selectOne(any())).thenReturn(row); + + assertTrue(service.stageInstruction(1L, "episode", "route").contains("明确日期")); + assertTrue(service.stageInstruction(1L, "episode", "merge").contains("已审阅")); + assertTrue(service.stageInstruction(1L, "episode", "create").contains("event_date")); + assertTrue(service.templateMarkdown(1L, "episode").contains("## Summary")); + // unknown type / stage → empty, never null + assertEquals("", service.stageInstruction(1L, "nope", "route")); + assertEquals("", service.templateMarkdown(1L, "nope")); + } + @Test void describeForPrompt_customProfile_showsRequiredMetadata() { WikiPageTypeProfileEntity row = new WikiPageTypeProfileEntity();