From 62c9776b0188cfd3d2e9452d09f08665c5147018 Mon Sep 17 00:00:00 2001 From: matevip Date: Tue, 12 May 2026 11:25:50 +0800 Subject: [PATCH] feat(wiki): clean conversational preamble and code fences from transformation output --- .gitignore | 1 - .../service/WikiTransformationExecutor.java | 53 ++++++++++++++++++- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 2e20f10d..dd12de0d 100644 --- a/.gitignore +++ b/.gitignore @@ -98,7 +98,6 @@ CLAUDE.md # Codex CLI local artifacts .codex/ -.superpowers/ # Sync tooling local state (generated each run; report is intentionally tracked) scripts/.*-sync-state.json diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiTransformationExecutor.java b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiTransformationExecutor.java index fa4c16a4..63848986 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiTransformationExecutor.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiTransformationExecutor.java @@ -144,12 +144,16 @@ public class WikiTransformationExecutor { ChatResponse resp = chatModel.call(new Prompt(List.of( new SystemMessage(systemPrompt), new UserMessage(userPrompt)))); - String output = (resp == null || resp.getResult() == null + String rawOutput = (resp == null || resp.getResult() == null || resp.getResult().getOutput() == null) ? null : resp.getResult().getOutput().getText(); - if (output == null || output.isBlank()) { + if (rawOutput == null || rawOutput.isBlank()) { throw new IllegalStateException("LLM returned empty output"); } + String output = cleanLlmOutput(rawOutput); + if (output.isBlank()) { + throw new IllegalStateException("LLM output was empty after cleanup"); + } // Honour a mid-flight cancel: the cancel endpoint flipped the run // row to 'cancelled' while the LLM was still working. Drop the // output and stop here rather than overwrite the cancelled state. @@ -252,6 +256,51 @@ public class WikiTransformationExecutor { return (t == null || t.isBlank()) ? ("raw#" + raw.getId()) : t; } + /** Recognises the conversational openers LLMs sometimes prepend even when + * the system prompt told them not to. Lines matching this pattern at the + * very start of the output are dropped. */ + private static final java.util.regex.Pattern PREAMBLE_PATTERN = + java.util.regex.Pattern.compile( + "^\\s*(以下是|下面是|这是|根据您的要求|Sure(?:!|,)?|Of course[!,]?|Here(?:'s| is| are)|Certainly[!,]?|Got it[!,]?)[^\\n]*[::][^\\n]*\\n+", + java.util.regex.Pattern.CASE_INSENSITIVE); + + /** + * Normalise raw LLM output before persisting: + * + * Conservative — only trims when the heuristic match is unambiguous, + * because over-trimming on a structured response would corrupt content. + */ + static String cleanLlmOutput(String text) { + if (text == null) return ""; + String result = text.trim(); + + // Outer code fence ```lang? ... ``` + if (result.startsWith("```")) { + int firstNewline = result.indexOf('\n'); + if (firstNewline > 0 && result.endsWith("```")) { + String header = result.substring(3, firstNewline).trim(); + // Only strip when the header is empty or looks like a language tag + // (markdown / md / json / yaml / text / plaintext) — never strip + // when the LLM used ``` as actual fenced content inside. + if (header.isEmpty() || header.matches("(?i)markdown|md|text|plaintext|json|yaml|yml|html?")) { + result = result.substring(firstNewline + 1, result.length() - 3).trim(); + } + } + } + + // Conversational opener line ending with a colon, followed by content. + java.util.regex.Matcher m = PREAMBLE_PATTERN.matcher(result); + if (m.find()) { + result = result.substring(m.end()).trim(); + } + + return result; + } + // ==================== Save-as-page ==================== /**