fix:总结返回结构变化

dev_na
chenhao 2026-07-07 17:00:10 +08:00
parent 788a98f28f
commit 6476b23bb6
3 changed files with 251 additions and 87 deletions

View File

@ -100,6 +100,10 @@ public class MeetingSummaryFileServiceImpl implements MeetingSummaryFileService
if (rawContent == null || rawContent.isBlank()) { if (rawContent == null || rawContent.isBlank()) {
return null; return null;
} }
Map<String, Object> xmlBundle = tryParseXmlBundle(rawContent.trim());
if (xmlBundle != null) {
return xmlBundle;
}
Map<String, Object> parsed = tryParseJson(rawContent.trim()); Map<String, Object> parsed = tryParseJson(rawContent.trim());
if (parsed == null) { if (parsed == null) {
return null; return null;
@ -129,6 +133,10 @@ public class MeetingSummaryFileServiceImpl implements MeetingSummaryFileService
if (rawContent == null || rawContent.isBlank()) { if (rawContent == null || rawContent.isBlank()) {
return null; return null;
} }
Map<String, Object> xmlAnalysis = tryParseXmlAnalysis(rawContent.trim());
if (xmlAnalysis != null) {
return xmlAnalysis;
}
Map<String, Object> parsed = tryParseJson(rawContent.trim()); Map<String, Object> parsed = tryParseJson(rawContent.trim());
if (parsed == null) { if (parsed == null) {
return null; return null;
@ -327,6 +335,142 @@ public class MeetingSummaryFileServiceImpl implements MeetingSummaryFileService
return null; return null;
} }
private Map<String, Object> tryParseXmlBundle(String rawContent) {
String text = normalizeXmlCandidate(rawContent);
String summarySection = extractXmlSection(text, "summary");
if (summarySection != null) {
text = summarySection;
}
String summaryContent = extractXmlSection(text, "summaryContent");
String analysisSection = extractXmlSection(text, "analysis");
if (!hasText(summaryContent) && !hasText(analysisSection)) {
return null;
}
Map<String, Object> bundle = new LinkedHashMap<>();
bundle.put("summaryContent", normalizeSummaryMarkdown(summaryContent));
bundle.put("analysis", hasText(analysisSection) ? parseXmlAnalysisSection(analysisSection) : null);
return bundle;
}
private Map<String, Object> tryParseXmlAnalysis(String rawContent) {
String text = normalizeXmlCandidate(rawContent);
String summarySection = extractXmlSection(text, "summary");
if (summarySection != null) {
text = summarySection;
}
String analysisSection = extractXmlSection(text, "analysis");
if (analysisSection != null) {
return parseXmlAnalysisSection(analysisSection);
}
if (containsAnyXmlTag(text, "overview", "keywords", "speakerSummaries", "keyPoints", "todos")) {
return parseXmlAnalysisSection(text);
}
return null;
}
private Map<String, Object> parseXmlAnalysisSection(String analysisSection) {
Map<String, Object> analysis = new LinkedHashMap<>();
analysis.put("overview", extractXmlSection(analysisSection, "overview"));
analysis.put("keywords", extractXmlTextList(firstNonBlank(extractXmlSection(analysisSection, "keywords"), analysisSection), "keyword"));
analysis.put("speakerSummaries", extractXmlItemMaps(analysisSection, "speakerSummaries", "speaker", "summary"));
analysis.put("keyPoints", extractXmlItemMaps(analysisSection, "keyPoints", "title", "summary", "speaker", "time"));
analysis.put("todos", extractXmlTextList(firstNonBlank(extractXmlSection(analysisSection, "todos"), analysisSection), "todo"));
return parseSummaryAnalysisFromMap(analysis);
}
private List<Map<String, Object>> extractXmlItemMaps(String text, String parentTag, String... childTags) {
List<Map<String, Object>> result = new ArrayList<>();
String parent = extractXmlSection(text, parentTag);
if (!hasText(parent)) {
return result;
}
for (String item : extractXmlSections(parent, "item")) {
Map<String, Object> normalized = new LinkedHashMap<>();
for (String childTag : childTags) {
normalized.put(childTag, extractXmlSection(item, childTag));
}
result.add(normalized);
}
return result;
}
private List<String> extractXmlTextList(String text, String tag) {
List<String> result = new ArrayList<>();
for (String item : extractXmlSections(text, tag)) {
if (hasText(item) && !result.contains(item)) {
result.add(item);
}
}
return result;
}
private List<String> extractXmlSections(String text, String tag) {
List<String> result = new ArrayList<>();
if (!hasText(text)) {
return result;
}
String pattern = "(?is)<\\s*" + tag + "(?:\\s+[^>]*)?>(.*?)<\\s*/\\s*" + tag + "\\s*>";
java.util.regex.Matcher matcher = java.util.regex.Pattern.compile(pattern).matcher(text);
while (matcher.find()) {
result.add(normalizeXmlText(matcher.group(1)));
}
return result;
}
private String extractXmlSection(String text, String tag) {
List<String> sections = extractXmlSections(text, tag);
return sections.isEmpty() ? null : sections.get(0);
}
private String normalizeXmlCandidate(String rawContent) {
String fenced = unwrapCodeFence(rawContent);
return fenced == null ? rawContent.trim() : fenced.trim();
}
private String normalizeXmlText(String text) {
String normalized = text == null ? "" : text.trim();
if (normalized.startsWith("<![CDATA[") && normalized.endsWith("]]>")) {
normalized = normalized.substring(9, normalized.length() - 3).trim();
}
return normalized
.replace("&lt;", "<")
.replace("&gt;", ">")
.replace("&amp;", "&")
.replace("&quot;", "\"")
.replace("&apos;", "'");
}
private boolean containsAnyXmlTag(String text, String... tags) {
if (!hasText(text) || tags == null) {
return false;
}
for (String tag : tags) {
if (text.matches("(?is).*<\\s*" + tag + "(?:\\s+[^>]*)?>.*")) {
return true;
}
}
return false;
}
private boolean hasText(String text) {
return text != null && !text.isBlank();
}
private String firstNonBlank(String... values) {
if (values == null) {
return null;
}
for (String value : values) {
if (hasText(value)) {
return value.trim();
}
}
return null;
}
private Map<String, Object> tryParseJson(String text) { private Map<String, Object> tryParseJson(String text) {
Map<String, Object> parsed = tryReadMap(text); Map<String, Object> parsed = tryReadMap(text);
if (parsed != null) { if (parsed != null) {

View File

@ -18,13 +18,12 @@ import java.util.Map;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class MeetingSummaryPromptAssembler { public class MeetingSummaryPromptAssembler {
public static final String PROMPT_SCHEMA_VERSION = "v3"; public static final String PROMPT_SCHEMA_VERSION = "v4";
private static final String SUMMARY_SYSTEM_TEMPLATE_KEY = SysParamKeys.MEETING_SUMMARY_SYSTEM_PROMPT; private static final String SUMMARY_SYSTEM_TEMPLATE_KEY = SysParamKeys.MEETING_SUMMARY_SYSTEM_PROMPT;
private static final String SUMMARY_USER_TEMPLATE_KEY = SysParamKeys.MEETING_SUMMARY_USER_TEMPLATE; private static final String SUMMARY_USER_TEMPLATE_KEY = SysParamKeys.MEETING_SUMMARY_USER_TEMPLATE;
private static final String CHAPTER_SYSTEM_TEMPLATE_KEY = SysParamKeys.MEETING_CHAPTER_SYSTEM_PROMPT; private static final String CHAPTER_SYSTEM_TEMPLATE_KEY = SysParamKeys.MEETING_CHAPTER_SYSTEM_PROMPT;
private static final String CHAPTER_USER_TEMPLATE_KEY = SysParamKeys.MEETING_CHAPTER_USER_TEMPLATE; private static final String CHAPTER_USER_TEMPLATE_KEY = SysParamKeys.MEETING_CHAPTER_USER_TEMPLATE;
private static final String MEETING_TITLE = "{{MEETING_TITLE}}"; private static final String MEETING_TITLE = "{{MEETING_TITLE}}";
private static final String MEETING_TIME = "{{MEETING_TIME}}"; private static final String MEETING_TIME = "{{MEETING_TIME}}";
private static final String PARTICIPANTS = "{{PARTICIPANTS}}"; private static final String PARTICIPANTS = "{{PARTICIPANTS}}";
@ -43,7 +42,7 @@ public class MeetingSummaryPromptAssembler {
:{{PROMPT_TEMPLATE}} {{PROMPT_TEMPLATE}}
{{SUMMARY_DETAIL_INSTRUCTION}} {{SUMMARY_DETAIL_INSTRUCTION}}
@ -71,7 +70,7 @@ public class MeetingSummaryPromptAssembler {
private static final String DEFAULT_CHAPTER_SYSTEM_TEMPLATE = """ private static final String DEFAULT_CHAPTER_SYSTEM_TEMPLATE = """
transcript transcript
{{CHAPTER_OUTPUT_SCHEMA}} {{CHAPTER_OUTPUT_SCHEMA}}
@ -81,7 +80,7 @@ public class MeetingSummaryPromptAssembler {
2. transcript 2. transcript
3. 3.
4. title 4. title
5. JSON 5. JSON
"""; """;
private static final String DEFAULT_CHAPTER_USER_TEMPLATE = """ private static final String DEFAULT_CHAPTER_USER_TEMPLATE = """
@ -92,7 +91,6 @@ public class MeetingSummaryPromptAssembler {
private final PromptTemplateService promptTemplateService; private final PromptTemplateService promptTemplateService;
private final SysParamService sysParamService; private final SysParamService sysParamService;
public Map<String, Object> buildTaskConfig(Long summaryModelId, Long chapterModelId, Long promptId, String userPrompt, String summaryDetailLevel) { public Map<String, Object> buildTaskConfig(Long summaryModelId, Long chapterModelId, Long promptId, String userPrompt, String summaryDetailLevel) {
Map<String, Object> taskConfig = new HashMap<>(); Map<String, Object> taskConfig = new HashMap<>();
taskConfig.put("summaryModelId", summaryModelId); taskConfig.put("summaryModelId", summaryModelId);
@ -127,7 +125,6 @@ public class MeetingSummaryPromptAssembler {
return render(resolveSummaryUserTemplate(taskConfig), buildSummaryValues(taskConfig, meeting, null, userPrompt)); return render(resolveSummaryUserTemplate(taskConfig), buildSummaryValues(taskConfig, meeting, null, userPrompt));
} }
public String buildChapterSystemMessage(Map<String, Object> taskConfig) { public String buildChapterSystemMessage(Map<String, Object> taskConfig) {
return render(resolveChapterSystemTemplate(taskConfig), Map.of(CHAPTER_OUTPUT_SCHEMA, buildChapterOutputSchema())); return render(resolveChapterSystemTemplate(taskConfig), Map.of(CHAPTER_OUTPUT_SCHEMA, buildChapterOutputSchema()));
} }
@ -150,11 +147,7 @@ public class MeetingSummaryPromptAssembler {
private String resolveSummarySystemTemplate() { private String resolveSummarySystemTemplate() {
String configured = sysParamService.getCachedParamValue(SUMMARY_SYSTEM_TEMPLATE_KEY, ""); String configured = sysParamService.getCachedParamValue(SUMMARY_SYSTEM_TEMPLATE_KEY, "");
if (StringUtils.hasText(configured)) { return StringUtils.hasText(configured) ? configured.trim() : DEFAULT_SUMMARY_SYSTEM_TEMPLATE;
return configured.trim();
}
String legacy = sysParamService.getCachedParamValue(SysParamKeys.MEETING_SUMMARY_SYSTEM_PROMPT, "");
return StringUtils.hasText(legacy) ? legacy.trim() : DEFAULT_SUMMARY_SYSTEM_TEMPLATE;
} }
private String resolveSummarySystemTemplate(Map<String, Object> taskConfig) { private String resolveSummarySystemTemplate(Map<String, Object> taskConfig) {
@ -184,7 +177,6 @@ public class MeetingSummaryPromptAssembler {
return StringUtils.hasText(configured) ? configured.trim() : DEFAULT_CHAPTER_USER_TEMPLATE; return StringUtils.hasText(configured) ? configured.trim() : DEFAULT_CHAPTER_USER_TEMPLATE;
} }
private Map<String, String> buildSummaryValues(Map<String, Object> taskConfig, Meeting meeting, MeetingSummarySource summarySource, String userPrompt) { private Map<String, String> buildSummaryValues(Map<String, Object> taskConfig, Meeting meeting, MeetingSummarySource summarySource, String userPrompt) {
Map<String, String> values = new LinkedHashMap<>(); Map<String, String> values = new LinkedHashMap<>();
values.put(MEETING_TITLE, meeting == null ? "" : firstNonBlank(meeting.getTitle(), "未命名会议")); values.put(MEETING_TITLE, meeting == null ? "" : firstNonBlank(meeting.getTitle(), "未命名会议"));
@ -226,13 +218,18 @@ public class MeetingSummaryPromptAssembler {
private String buildSummaryOutputSchema() { private String buildSummaryOutputSchema() {
return """ return """
{ <summary>
"summaryContent": string, <summaryContent><![CDATA[
"analysis": { ##
"keywords": array 使 Markdown
} ]]></summaryContent>
} <analysis>
summaryContent使 markdown,analysis.keywords <keywords>
<keyword></keyword>
</keywords>
</analysis>
</summary>
XML JSONsummaryContent Markdown 使 CDATA
"""; """;
} }
@ -246,7 +243,7 @@ public class MeetingSummaryPromptAssembler {
"summary": string, "summary": string,
"keywords": array, "keywords": array,
"startTranscriptId": number, "startTranscriptId": number,
"endTranscriptId":number, "endTranscriptId": number,
"confidence": number "confidence": number
} }
] ]
@ -287,10 +284,19 @@ public class MeetingSummaryPromptAssembler {
private String buildSummaryDetailInstruction(String summaryDetailLevel) { private String buildSummaryDetailInstruction(String summaryDetailLevel) {
return switch (normalizeSummaryDetailLevel(summaryDetailLevel)) { return switch (normalizeSummaryDetailLevel(summaryDetailLevel)) {
case MeetingConstants.SUMMARY_DETAIL_DETAILED -> case MeetingConstants.SUMMARY_DETAIL_DETAILED ->
"DETAILED尽量完整覆盖会议背景、讨论过程、分歧、结论、负责人、时间点和风险适当展开章节内容。"; "目标:完整覆盖结构化关键信息和转录中的有效事实,适合正式纪要、复盘和归档。\n" +
"信息覆盖:不得因为篇幅原因丢失任何关键事实、数字、时间、责任人、部门、决策、行动项、风险、问题和待确认事项。\n" +
"表达方式:在完整覆盖的基础上展开背景、原因、影响范围、过程关系和后续动作;相近表达可以合并,但合并后必须保留原有事实含义。\n" +
"密度:每个模板小节可以充分展开;重要议题可以分层描述;同一事项可补充上下文和执行影响。";
case MeetingConstants.SUMMARY_DETAIL_BRIEF -> case MeetingConstants.SUMMARY_DETAIL_BRIEF ->
"BRIEF只保留核心结论、关键决策、待办事项和必要上下文避免冗长铺陈。"; "目标:完整覆盖结构化关键信息和转录中的有效事实,同时压缩表达,适合快速扫读和简版纪要。\n" +
default -> "STANDARD保持信息完整和篇幅平衡覆盖核心过程、结论和待办不做过度展开。"; "信息覆盖:不得因为选择简洁档而丢失关键事实、数字、时间、责任人、部门、决策、行动项、风险、问题和待确认事项。\n" +
"表达方式:把多个相关事实合并成短句或紧凑条目;减少背景解释和过程描述,但必须保留事实本身及其关键限定条件。\n" +
"密度:每个模板小节用更短句子表达;同样的信息写得更凝练,不减少信息项。";
default -> "目标:完整覆盖结构化关键信息和转录中的有效事实,适合默认正式纪要。\n" +
"信息覆盖:不得遗漏任何关键事实、数字、时间、责任人、部门、决策、行动项、风险、问题和待确认事项。\n" +
"表达方式:用清晰、紧凑的方式组织信息;相近事项可以合并成一条,但不能省略不同事项、不同指标、不同责任方或不同时间点。\n" +
"密度:每个模板小节保持适中篇幅;少写背景,多写结论、进展、决策和执行安排。";
}; };
} }
} }

View File

@ -39,7 +39,7 @@ class MeetingSummaryPromptAssemblerTest {
MeetingSummaryPromptAssembler assembler = new MeetingSummaryPromptAssembler(promptTemplateService, sysParamService); MeetingSummaryPromptAssembler assembler = new MeetingSummaryPromptAssembler(promptTemplateService, sysParamService);
Map<String, Object> taskConfig = assembler.buildTaskConfig(2L, 5L, 3L, "关注风险", MeetingConstants.SUMMARY_DETAIL_DETAILED); Map<String, Object> taskConfig = assembler.buildTaskConfig(2L, 5L, 3L, "关注风险", MeetingConstants.SUMMARY_DETAIL_DETAILED);
assertEquals("v3", taskConfig.get("promptSchemaVersion")); assertEquals("v4", taskConfig.get("promptSchemaVersion"));
assertEquals("总结系统模板", taskConfig.get("summaryPromptTemplate")); assertEquals("总结系统模板", taskConfig.get("summaryPromptTemplate"));
assertEquals("总结用户模板", taskConfig.get("summaryUserTemplate")); assertEquals("总结用户模板", taskConfig.get("summaryUserTemplate"));
assertEquals("章节系统模板", taskConfig.get("chapterPromptTemplate")); assertEquals("章节系统模板", taskConfig.get("chapterPromptTemplate"));
@ -69,4 +69,18 @@ class MeetingSummaryPromptAssemblerTest {
assertTrue(userMessage.contains("Alice: hello")); assertTrue(userMessage.contains("Alice: hello"));
assertTrue(userMessage.contains("第一章")); assertTrue(userMessage.contains("第一章"));
} }
@Test
void buildSystemMessageShouldRequestXmlSummaryContent() {
MeetingSummaryPromptAssembler assembler = new MeetingSummaryPromptAssembler(
mock(PromptTemplateService.class),
mock(SysParamService.class)
);
String systemMessage = assembler.buildSystemMessage(Map.of());
assertTrue(systemMessage.contains("<summary>"));
assertTrue(systemMessage.contains("<summaryContent>"));
assertTrue(systemMessage.contains("<analysis>"));
}
} }