refactor:优化 MeetingSummaryPromptAssembler 和相关逻辑

- 更新 `MeetingSummaryPromptAssembler`,引入新的模板和占位符
- 在 `MeetingSummaryFileServiceImpl` 中重构代码以简化解析逻辑
- 更新 `SysParamKeys`,添加新的系统参数键
- 调整前端 `AiModels` 页面的本地模型验证逻辑
- 注释掉登录页面中的示例账户信息
dev_na
chenhao 2026-06-30 15:17:18 +08:00
parent 065708497a
commit 2a95eca102
10 changed files with 331 additions and 354 deletions

View File

@ -4,12 +4,18 @@ package com.imeeting.common;
* Key
*/
public final class SysParamKeys {
private SysParamKeys() {}
private SysParamKeys() {
}
/** 登录验证码开关。 */
public static final String CAPTCHA_ENABLED = "security.captcha.enabled";
/** AI 会议总结使用的系统提示词。 */
// 会议总结系统提示词模板,控制总结任务的角色设定和输出约束
public static final String MEETING_SUMMARY_SYSTEM_PROMPT = "meeting.summary.system_prompt";
// 会议章节系统提示词模板,控制章节切分任务的角色设定和输出约束
public static final String MEETING_CHAPTER_SYSTEM_PROMPT = "meeting.chapter.prompt_template";
// 会议总结用户提示词模板,承载会议字段和业务占位符
public static final String MEETING_SUMMARY_USER_TEMPLATE = "meeting.summary.user_template";
// 会议章节用户提示词模板,承载转录分段占位符
public static final String MEETING_CHAPTER_USER_TEMPLATE = "meeting.chapter.user_template";
/** 是否启用 AI 目录。 */
public static final String MEETING_AI_CATALOG_ENABLED = "meeting.ai_catalog.enabled";
/** 会议总结派发模式PARALLEL / SERIAL。 */

View File

@ -9,12 +9,18 @@ public class MeetingSummaryPromptContextVO {
@Schema(description = "提示词协议版本")
private String promptSchemaVersion;
@Schema(description = "系统消息")
@Schema(description = "最终系统消息")
private String systemMessage;
@Schema(description = "带占位符的用户消息模板")
@Schema(description = "系统提示词模板")
private String systemMessageTemplate;
@Schema(description = "最终用户消息模板")
private String userMessageTemplate;
@Schema(description = "用户提示词模板原文")
private String userMessageTemplateRaw;
@Schema(description = "有效模板提示词")
private String effectiveTemplatePrompt;

View File

@ -1253,7 +1253,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
req.put("temperature", llmModel.getTemperature());
req.put("messages", List.of(
Map.of("role", "system", "content", meetingSummaryPromptAssembler.buildSystemMessage(taskRecord.getTaskConfig())),
Map.of("role", "user", "content", meetingSummaryPromptAssembler.buildUserMessage(meeting, summarySource, userPrompt))
Map.of("role", "user", "content", meetingSummaryPromptAssembler.buildUserMessage(taskRecord.getTaskConfig(), meeting, summarySource, userPrompt))
));
taskRecord.setRequestData(req);

View File

@ -184,7 +184,9 @@ public class MeetingQueryServiceImpl implements MeetingQueryService {
MeetingSummaryPromptContextVO context = new MeetingSummaryPromptContextVO();
context.setPromptSchemaVersion(String.valueOf(taskConfig.get("promptSchemaVersion")));
context.setSystemMessage(meetingSummaryPromptAssembler.buildSystemMessage(taskConfig));
context.setUserMessageTemplate(meetingSummaryPromptAssembler.buildUserMessageTemplate(meeting, userPrompt));
context.setSystemMessageTemplate(String.valueOf(taskConfig.get("summaryPromptTemplate")));
context.setUserMessageTemplate(meetingSummaryPromptAssembler.buildUserMessageTemplate(taskConfig, meeting, userPrompt));
context.setUserMessageTemplateRaw(String.valueOf(taskConfig.get("summaryUserTemplate")));
context.setEffectiveTemplatePrompt(stringValue(taskConfig.get("effectiveTemplatePrompt")));
context.setEffectiveUserPrompt(meetingSummaryPromptAssembler.normalizeOptionalText(userPrompt));
context.setSummaryModelId(summaryModelId);

View File

@ -12,172 +12,132 @@ import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Component
@RequiredArgsConstructor
public class MeetingSummaryPromptAssembler {
public static final String PROMPT_SCHEMA_VERSION = "v3";
public static final String PROMPT_SCHEMA_VERSION = "v2";
private static final String SUMMARY_SOURCE_PLACEHOLDER = "{{SUMMARY_SOURCE_TEXT}}";
private static final String SYSTEM_PROMPT_NOT_CONFIGURED_MESSAGE =
"系统提示词未配置,请先维护系统参数 " + 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 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 MEETING_TITLE = "{{MEETING_TITLE}}";
private static final String MEETING_TIME = "{{MEETING_TIME}}";
private static final String PARTICIPANTS = "{{PARTICIPANTS}}";
private static final String USER_PROMPT = "{{USER_PROMPT}}";
private static final String PROMPT_TEMPLATE = "{{PROMPT_TEMPLATE}}";
private static final String SUMMARY_DETAIL_INSTRUCTION = "{{SUMMARY_DETAIL_INSTRUCTION}}";
private static final String SUMMARY_OUTPUT_SCHEMA = "{{SUMMARY_OUTPUT_SCHEMA}}";
private static final String CHAPTER_OUTPUT_SCHEMA = "{{CHAPTER_OUTPUT_SCHEMA}}";
private static final String SUMMARY_SOURCE_TEXT = "{{SUMMARY_SOURCE_TEXT}}";
private static final String SUMMARY_SOURCE_PLACEHOLDER = SUMMARY_SOURCE_TEXT;
private static final String CHAPTER_OUTLINE_TEXT = "{{CHAPTER_OUTLINE_TEXT}}";
private static final String RAW_TRANSCRIPT_TEXT = "{{RAW_TRANSCRIPT_TEXT}}";
private static final String TRANSCRIPT_SEGMENTS_JSON = "{{TRANSCRIPT_SEGMENTS_JSON}}";
private static final String DEFAULT_SUMMARY_SYSTEM_TEMPLATE = """
:{{PROMPT_TEMPLATE}}
{{SUMMARY_DETAIL_INSTRUCTION}}
{{SUMMARY_OUTPUT_SCHEMA}}
""";
private static final String DEFAULT_SUMMARY_USER_TEMPLATE = """
{{MEETING_TITLE}}
{{MEETING_TIME}}
{{PARTICIPANTS}}
{{USER_PROMPT}}
{{CHAPTER_OUTLINE_TEXT}}
{{SUMMARY_SOURCE_TEXT}}
""";
private static final String DEFAULT_CHAPTER_SYSTEM_TEMPLATE = """
transcript
{{CHAPTER_OUTPUT_SCHEMA}}
1.
2. transcript
3.
4. title
5. JSON
""";
private static final String DEFAULT_CHAPTER_USER_TEMPLATE = """
transcript JSON
{{TRANSCRIPT_SEGMENTS_JSON}}
""";
private final PromptTemplateService promptTemplateService;
private final SysParamService sysParamService;
public Map<String, Object> buildTaskConfig(Long summaryModelId, Long promptId, String userPrompt) {
return buildTaskConfig(
summaryModelId,
summaryModelId,
promptId,
userPrompt,
MeetingConstants.SUMMARY_DETAIL_STANDARD
);
}
public Map<String, Object> buildTaskConfig(Long summaryModelId, Long chapterModelId, Long promptId, String userPrompt) {
return buildTaskConfig(
summaryModelId,
chapterModelId,
promptId,
userPrompt,
MeetingConstants.SUMMARY_DETAIL_STANDARD
);
}
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<>();
taskConfig.put("summaryModelId", summaryModelId);
taskConfig.put("chapterModelId", chapterModelId != null ? chapterModelId : summaryModelId);
taskConfig.put("promptSchemaVersion", PROMPT_SCHEMA_VERSION);
taskConfig.put("effectiveSystemPrompt", resolveSystemPrompt());
taskConfig.put("summaryPromptTemplate", resolveSummarySystemTemplate());
taskConfig.put("summaryUserTemplate", resolveSummaryUserTemplate());
taskConfig.put("chapterPromptTemplate", resolveChapterSystemTemplate());
taskConfig.put("chapterUserTemplate", resolveChapterUserTemplate());
taskConfig.put("summaryDetailLevel", normalizeSummaryDetailLevel(summaryDetailLevel));
String templatePrompt = resolveTemplatePrompt(promptId);
taskConfig.put("effectiveTemplatePrompt", templatePrompt);
taskConfig.put("effectiveTemplatePrompt", resolveTemplatePrompt(promptId));
taskConfig.put("userPrompt", normalizeOptionalText(userPrompt));
if (promptId != null) {
taskConfig.put("promptId", promptId);
}
if (StringUtils.hasText(templatePrompt)) {
taskConfig.put("promptContent", templatePrompt);
}
return taskConfig;
}
public String buildSystemMessage(Map<String, Object> taskConfig) {
String systemPrompt = stringValue(taskConfig, "effectiveSystemPrompt");
if (!StringUtils.hasText(systemPrompt)) {
systemPrompt = resolveSystemPrompt();
}
String templatePrompt = firstNonBlank(
stringValue(taskConfig, "effectiveTemplatePrompt"),
stringValue(taskConfig, "promptContent"),
"请输出结构清晰、信息完整、适合直接阅读和导出的正式会议纪要。"
);
String summaryDetailLevel = normalizeSummaryDetailLevel(stringValue(taskConfig, "summaryDetailLevel"));
return String.join("\n\n",
"你是一名擅长中文会议纪要、结构化分析和待办提取的助手。",
"系统提示词(最高优先级):\n" + systemPrompt,
"模板提示词(结构和风格要求):\n" + templatePrompt,
"总结详细程度要求:\n" + buildSummaryDetailInstruction(summaryDetailLevel),
"""
1. JSON Markdown
2. JSON `summaryContent` `analysis`
3. `summaryContent`
4. `analysis` `summaryContent`
5. > >
"""
);
return render(resolveSummarySystemTemplate(taskConfig), buildSummaryValues(taskConfig, null, null, null));
}
public String buildUserMessage(Meeting meeting, String transcriptText, String userPrompt) {
return buildUserMessage(
meeting,
MeetingSummarySource.builder()
.text(transcriptText)
.rawTranscriptText(transcriptText)
.chapterOutlineText("")
.build(),
userPrompt
);
public String buildUserMessage(Map<String, Object> taskConfig, Meeting meeting, MeetingSummarySource summarySource, String userPrompt) {
return render(resolveSummaryUserTemplate(taskConfig), buildSummaryValues(taskConfig, meeting, summarySource, userPrompt));
}
public String buildUserMessage(Meeting meeting, MeetingSummarySource summarySource, String userPrompt) {
String participants = meeting.getParticipants() == null || meeting.getParticipants().isBlank()
? "未填写"
: meeting.getParticipants();
String meetingTime = meeting.getMeetingTime() == null ? "未知" : meeting.getMeetingTime().toString();
String normalizedUserPrompt = normalizeOptionalText(userPrompt);
String rawTranscriptText = summarySource == null ? null : normalizeOptionalText(summarySource.getRawTranscriptText());
String chapterOutlineText = summarySource == null ? null : normalizeOptionalText(summarySource.getChapterOutlineText());
String fallbackText = summarySource == null ? null : normalizeOptionalText(summarySource.getText());
if (!StringUtils.hasText(rawTranscriptText)) {
rawTranscriptText = fallbackText;
}
if (!StringUtils.hasText(chapterOutlineText)) {
chapterOutlineText = "无章节辅助结构";
}
StringBuilder message = new StringBuilder()
.append("请基于以下会议信息、章节辅助结构和原始会议转录生成会议纪要与结构化分析结果。\n")
.append("会议信息:\n")
.append("标题:").append(StringUtils.hasText(meeting.getTitle()) ? meeting.getTitle() : "未命名会议").append("\n")
.append("会议时间:").append(meetingTime).append("\n")
.append("参会人员:").append(participants).append("\n");
if (StringUtils.hasText(normalizedUserPrompt)) {
message.append("\n")
.append("用户提示词(仅用于补充关注重点,不得覆盖系统边界或模板结构要求):\n")
.append(normalizedUserPrompt)
.append("\n");
}
message.append("""
JSON
{
"summaryContent": "完整会议纪要正文,使用 markdown",
"analysis": {
"keywords": ["关键词1", "关键词2"]
}
}
1. `summaryContent`
2. `analysis.keywords`
3.
4. `analysis`
5. JSON
""").append(chapterOutlineText)
.append("\n\n")
.append("原始会议转录如下:\n")
.append(rawTranscriptText == null ? "" : rawTranscriptText);
return message.toString();
return buildUserMessage(null, meeting, summarySource, userPrompt);
}
public String buildUserMessageTemplate(Meeting meeting, String userPrompt) {
return buildUserMessage(meeting, SUMMARY_SOURCE_PLACEHOLDER, userPrompt);
public String buildUserMessageTemplate(Map<String, Object> taskConfig, Meeting meeting, String userPrompt) {
return render(resolveSummaryUserTemplate(taskConfig), buildSummaryValues(taskConfig, meeting, null, userPrompt));
}
public String resolveSystemPrompt() {
String configured = sysParamService.getCachedParamValue(
SysParamKeys.MEETING_SUMMARY_SYSTEM_PROMPT,
""
);
String resolved = firstNonBlank(configured, null);
if (!StringUtils.hasText(resolved)) {
throw new RuntimeException(SYSTEM_PROMPT_NOT_CONFIGURED_MESSAGE);
}
return resolved;
public String buildChapterSystemMessage(Map<String, Object> taskConfig) {
return render(resolveChapterSystemTemplate(taskConfig), Map.of(CHAPTER_OUTPUT_SCHEMA, buildChapterOutputSchema()));
}
public String buildChapterUserMessage(String transcriptSegmentsJson) {
return render(DEFAULT_CHAPTER_USER_TEMPLATE, Map.of(TRANSCRIPT_SEGMENTS_JSON, firstNonBlank(transcriptSegmentsJson, "")));
}
public String normalizeOptionalText(String value) {
return firstNonBlank(value, null);
}
public String resolveTemplatePrompt(Long promptId) {
@ -185,22 +145,112 @@ public class MeetingSummaryPromptAssembler {
return "";
}
PromptTemplate template = promptTemplateService.getById(promptId);
if (template == null) {
return "";
}
return firstNonBlank(template.getPromptContent(), "");
return template == null ? "" : firstNonBlank(template.getPromptContent(), "");
}
public String normalizeOptionalText(String value) {
return firstNonBlank(value, null);
private String resolveSummarySystemTemplate() {
String configured = sysParamService.getCachedParamValue(SUMMARY_SYSTEM_TEMPLATE_KEY, "");
if (StringUtils.hasText(configured)) {
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) {
return firstNonBlank(stringValue(taskConfig, "summaryPromptTemplate"), resolveSummarySystemTemplate());
}
private String resolveSummaryUserTemplate() {
String configured = sysParamService.getCachedParamValue(SUMMARY_USER_TEMPLATE_KEY, "");
return StringUtils.hasText(configured) ? configured.trim() : DEFAULT_SUMMARY_USER_TEMPLATE;
}
private String resolveSummaryUserTemplate(Map<String, Object> taskConfig) {
return firstNonBlank(stringValue(taskConfig, "summaryUserTemplate"), resolveSummaryUserTemplate());
}
private String resolveChapterSystemTemplate() {
String configured = sysParamService.getCachedParamValue(CHAPTER_SYSTEM_TEMPLATE_KEY, "");
return StringUtils.hasText(configured) ? configured.trim() : DEFAULT_CHAPTER_SYSTEM_TEMPLATE;
}
private String resolveChapterSystemTemplate(Map<String, Object> taskConfig) {
return firstNonBlank(stringValue(taskConfig, "chapterPromptTemplate"), resolveChapterSystemTemplate());
}
private String resolveChapterUserTemplate() {
String configured = sysParamService.getCachedParamValue(CHAPTER_USER_TEMPLATE_KEY, "");
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) {
Map<String, String> values = new LinkedHashMap<>();
values.put(MEETING_TITLE, meeting == null ? "" : firstNonBlank(meeting.getTitle(), "未命名会议"));
values.put(MEETING_TIME, meeting == null || meeting.getMeetingTime() == null ? "未知" : meeting.getMeetingTime().toString());
values.put(PARTICIPANTS, meeting == null || !StringUtils.hasText(meeting.getParticipants()) ? "未填写" : meeting.getParticipants().trim());
values.put(USER_PROMPT, normalizeOptionalText(userPrompt) == null ? "" : normalizeOptionalText(userPrompt));
values.put(PROMPT_TEMPLATE, firstNonBlank(stringValue(taskConfig, "effectiveTemplatePrompt"), ""));
values.put(SUMMARY_DETAIL_INSTRUCTION, buildSummaryDetailInstruction(stringValue(taskConfig, "summaryDetailLevel")));
values.put(SUMMARY_OUTPUT_SCHEMA, buildSummaryOutputSchema());
values.put(CHAPTER_OUTPUT_SCHEMA, buildChapterOutputSchema());
values.put(CHAPTER_OUTLINE_TEXT, summarySource == null ? "" : firstNonBlank(summarySource.getChapterOutlineText(), ""));
values.put(SUMMARY_SOURCE_TEXT, summarySource == null ? SUMMARY_SOURCE_PLACEHOLDER : buildSummarySourceText(summarySource));
values.put(RAW_TRANSCRIPT_TEXT, summarySource == null ? "" : firstNonBlank(summarySource.getRawTranscriptText(), summarySource.getText(), ""));
return values;
}
private String buildSummarySourceText(MeetingSummarySource summarySource) {
String chapterOutlineText = firstNonBlank(summarySource.getChapterOutlineText(), "无章节辅助结构");
String rawTranscriptText = firstNonBlank(summarySource.getRawTranscriptText(), summarySource.getText(), "");
return """
%s
%s
""".formatted(chapterOutlineText, rawTranscriptText);
}
private String render(String template, Map<String, String> values) {
String result = firstNonBlank(template, "");
if (values == null || values.isEmpty()) {
return result;
}
for (Map.Entry<String, String> entry : values.entrySet()) {
result = result.replace(entry.getKey(), entry.getValue() == null ? "" : entry.getValue());
}
return result;
}
private String stringValue(Map<String, Object> source, String key) {
if (source == null || key == null) {
return null;
private String buildSummaryOutputSchema() {
return """
{
"summaryContent": "完整会议纪要正文,使用 markdown",
"analysis": {
"keywords": ["关键词"]
}
Object value = source.get(key);
return value == null ? null : normalizeOptionalText(String.valueOf(value));
}
""";
}
private String buildChapterOutputSchema() {
return """
{
"chapters": [
{
"chapterNo": 1,
"title": "章节标题",
"summary": "章节摘要",
"keywords": ["关键词"],
"startTranscriptId": 1,
"endTranscriptId": 2,
"confidence": 0.95
}
]
}
""";
}
private String firstNonBlank(String... values) {
@ -215,26 +265,31 @@ public class MeetingSummaryPromptAssembler {
return null;
}
private String stringValue(Map<String, Object> source, String key) {
if (source == null || key == null) {
return null;
}
Object value = source.get(key);
return value == null ? null : normalizeOptionalText(String.valueOf(value));
}
private String normalizeSummaryDetailLevel(String summaryDetailLevel) {
if (!StringUtils.hasText(summaryDetailLevel)) {
return MeetingConstants.SUMMARY_DETAIL_STANDARD;
}
String normalized = summaryDetailLevel.trim().toUpperCase();
if (MeetingConstants.SUMMARY_DETAIL_DETAILED.equals(normalized)
|| MeetingConstants.SUMMARY_DETAIL_BRIEF.equals(normalized)) {
return normalized;
}
return MeetingConstants.SUMMARY_DETAIL_STANDARD;
return MeetingConstants.SUMMARY_DETAIL_DETAILED.equals(normalized) || MeetingConstants.SUMMARY_DETAIL_BRIEF.equals(normalized)
? normalized
: MeetingConstants.SUMMARY_DETAIL_STANDARD;
}
private String buildSummaryDetailInstruction(String summaryDetailLevel) {
return switch (normalizeSummaryDetailLevel(summaryDetailLevel)) {
case MeetingConstants.SUMMARY_DETAIL_DETAILED ->
"DETAILED(详细):尽量完整覆盖会议背景、讨论过程、分歧、结论、负责人、时间点和风险,适当展开章节内容。";
"DETAILED:尽量完整覆盖会议背景、讨论过程、分歧、结论、负责人、时间点和风险,适当展开章节内容。";
case MeetingConstants.SUMMARY_DETAIL_BRIEF ->
"BRIEF简洁只保留核心结论、关键决策、待办事项和必要上下文避免冗长铺陈。";
default ->
"STANDARD标准保持信息完整和篇幅平衡覆盖核心过程、结论和待办不做过度展开。";
"BRIEF只保留核心结论、关键决策、待办事项和必要上下文避免冗长铺陈。";
default -> "STANDARD保持信息完整和篇幅平衡覆盖核心过程、结论和待办不做过度展开。";
};
}
}

View File

@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imeeting.common.SysParamKeys;
import com.imeeting.dto.biz.AiModelVO;
import com.imeeting.dto.biz.MeetingSummarySource;
import com.imeeting.dto.biz.MeetingTranscriptChapterImportDTO;
@ -21,6 +22,8 @@ import com.imeeting.mapper.biz.MeetingTranscriptChapterVersionMapper;
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
import com.imeeting.service.biz.AiModelService;
import com.imeeting.service.biz.MeetingTranscriptChapterService;
import com.unisbase.service.SysParamService;
import com.unisbase.service.SysParamService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -74,6 +77,8 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
private final MeetingTranscriptChapterMapper chapterMapper;
private final AiTaskMapper aiTaskMapper;
private final ObjectMapper objectMapper;
private final MeetingSummaryPromptAssembler meetingSummaryPromptAssembler;
private final SysParamService sysParamService;
private AiModelService aiModelService;
@ -93,6 +98,27 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
this.aiModelService = aiModelService;
}
private String renderChapterSystemPrompt() {
Map<String, Object> taskConfig = new LinkedHashMap<>();
taskConfig.put("chapterPromptTemplate", sysParamService.getCachedParamValue(SysParamKeys.MEETING_CHAPTER_SYSTEM_PROMPT, ""));
return meetingSummaryPromptAssembler.buildChapterSystemMessage(taskConfig);
}
private String renderChapterUserPrompt(List<MeetingTranscript> transcripts) throws Exception {
List<Map<String, Object>> segments = new ArrayList<>();
for (MeetingTranscript transcript : transcripts) {
Map<String, Object> item = new LinkedHashMap<>();
item.put("transcriptId", transcript.getId());
item.put("sortOrder", transcript.getSortOrder());
item.put("speakerName", transcript.getSpeakerName());
item.put("startTime", transcript.getStartTime());
item.put("endTime", transcript.getEndTime());
item.put("content", transcript.getContent());
segments.add(item);
}
return meetingSummaryPromptAssembler.buildChapterUserMessage(objectMapper.writeValueAsString(segments));
}
@Override
@Transactional(rollbackFor = Exception.class)
public MeetingSummarySource resolveSummarySource(Meeting meeting, AiTask summaryTask) {
@ -308,8 +334,8 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
requestBody.put("model", llmModel.getModelCode());
requestBody.put("temperature", llmModel.getTemperature());
requestBody.put("messages", List.of(
Map.of("role", "system", "content", buildChapterSystemPrompt()),
Map.of("role", "user", "content", buildChapterUserPrompt(transcripts))
Map.of("role", "system", "content", renderChapterSystemPrompt()),
Map.of("role", "user", "content", renderChapterUserPrompt(transcripts))
));
String payload = objectMapper.writeValueAsString(requestBody);
HttpRequest request = HttpRequest.newBuilder()
@ -886,10 +912,13 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
return normalized;
}
int firstBreak = normalized.indexOf('\n');
int lastFence = normalized.lastIndexOf("\n```");
if (firstBreak < 0 || lastFence <= firstBreak) {
if (firstBreak < 0) {
return normalized;
}
int lastFence = normalized.lastIndexOf("\n```");
if (lastFence <= firstBreak) {
return normalized.substring(firstBreak + 1).trim();
}
return normalized.substring(firstBreak + 1, lastFence).trim();
}
@ -983,8 +1012,8 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
requestBody.put("model", llmModel.getModelCode());
requestBody.put("temperature", llmModel.getTemperature());
requestBody.put("messages", List.of(
Map.of("role", "system", "content", buildChapterSystemPrompt()),
Map.of("role", "user", "content", buildChapterUserPrompt(transcripts))
Map.of("role", "system", "content", renderChapterSystemPrompt()),
Map.of("role", "user", "content", renderChapterUserPrompt(transcripts))
));
String payload = objectMapper.writeValueAsString(requestBody);
requestUrl = appendPath(llmModel.getBaseUrl(), nonBlank(llmModel.getApiPath(), "v1/chat/completions"));

View File

@ -13,8 +13,6 @@ import java.time.LocalDateTime;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
@ -23,110 +21,52 @@ import static org.mockito.Mockito.when;
class MeetingSummaryPromptAssemblerTest {
@Test
void buildTaskConfigShouldCaptureEffectivePromptsUserPromptAndDetailLevel() {
void buildTaskConfigShouldIncludePromptTemplates() {
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
SysParamService sysParamService = mock(SysParamService.class);
PromptTemplate template = new PromptTemplate();
template.setPromptContent("模板提示词");
when(promptTemplateService.getById(3L)).thenReturn(template);
when(sysParamService.getCachedParamValue(eq(SysParamKeys.MEETING_SUMMARY_SYSTEM_PROMPT), eq("")))
.thenReturn("系统提示词");
.thenReturn("总结系统模板");
when(sysParamService.getCachedParamValue(eq(SysParamKeys.MEETING_SUMMARY_USER_TEMPLATE), eq("")))
.thenReturn("总结用户模板");
when(sysParamService.getCachedParamValue(eq(SysParamKeys.MEETING_CHAPTER_SYSTEM_PROMPT), eq("")))
.thenReturn("章节系统模板");
when(sysParamService.getCachedParamValue(eq(SysParamKeys.MEETING_CHAPTER_USER_TEMPLATE), eq("")))
.thenReturn("章节用户模板");
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(2L, taskConfig.get("summaryModelId"));
assertEquals(5L, taskConfig.get("chapterModelId"));
assertEquals(3L, taskConfig.get("promptId"));
assertEquals("v2", taskConfig.get("promptSchemaVersion"));
assertEquals("系统提示词", taskConfig.get("effectiveSystemPrompt"));
assertEquals("模板提示词", taskConfig.get("effectiveTemplatePrompt"));
assertEquals("关注风险项", taskConfig.get("userPrompt"));
assertEquals("模板提示词", taskConfig.get("promptContent"));
assertEquals(MeetingConstants.SUMMARY_DETAIL_DETAILED, taskConfig.get("summaryDetailLevel"));
assertEquals("v3", taskConfig.get("promptSchemaVersion"));
assertEquals("总结系统模板", taskConfig.get("summaryPromptTemplate"));
assertEquals("总结用户模板", taskConfig.get("summaryUserTemplate"));
assertEquals("章节系统模板", taskConfig.get("chapterPromptTemplate"));
assertEquals("章节用户模板", taskConfig.get("chapterUserTemplate"));
}
@Test
void buildSystemMessageShouldContainSummaryDetailInstruction() {
SysParamService sysParamService = mock(SysParamService.class);
when(sysParamService.getCachedParamValue(eq(SysParamKeys.MEETING_SUMMARY_SYSTEM_PROMPT), eq("")))
.thenReturn("系统提示词");
MeetingSummaryPromptAssembler assembler = new MeetingSummaryPromptAssembler(
mock(PromptTemplateService.class),
sysParamService
);
String systemMessage = assembler.buildSystemMessage(Map.of(
"promptContent", "旧模板提示词",
"summaryDetailLevel", MeetingConstants.SUMMARY_DETAIL_BRIEF
));
assertTrue(systemMessage.contains("旧模板提示词"));
assertTrue(systemMessage.contains("BRIEF"));
assertTrue(systemMessage.contains("简洁"));
}
@Test
void buildUserMessageShouldOmitUserPromptSectionWhenBlank() {
void buildUserMessageShouldRenderPlaceholders() {
MeetingSummaryPromptAssembler assembler = new MeetingSummaryPromptAssembler(
mock(PromptTemplateService.class),
mock(SysParamService.class)
);
Meeting meeting = new Meeting();
meeting.setTitle("周会");
meeting.setMeetingTime(LocalDateTime.of(2026, 4, 16, 10, 0));
meeting.setParticipants("张三,李四");
String userMessage = assembler.buildUserMessage(meeting, "这里是转录文本", " ");
assertFalse(userMessage.contains("用户提示词"));
assertTrue(userMessage.contains("这里是转录文本"));
}
@Test
void buildUserMessageShouldSeparateChapterOutlineAndRawTranscript() {
MeetingSummaryPromptAssembler assembler = new MeetingSummaryPromptAssembler(
mock(PromptTemplateService.class),
mock(SysParamService.class)
);
Meeting meeting = new Meeting();
meeting.setTitle("预算会");
meeting.setMeetingTime(LocalDateTime.of(2026, 5, 8, 10, 0));
meeting.setParticipants("Alice,Bob");
MeetingSummarySource source = MeetingSummarySource.builder()
.chapterOutlineText("第1章 预算评审\n章节摘要讨论 20 万预算")
.rawTranscriptText("Alice: 今天讨论 320 万预算,时间定在 2026 年 5 月 8 日 15:30。")
.chapterOutlineText("第一章")
.rawTranscriptText("Alice: hello")
.build();
String userMessage = assembler.buildUserMessage(meeting, source, "关注预算结论");
String userMessage = assembler.buildUserMessage(Map.of(
"summaryUserTemplate", "标题:{{MEETING_TITLE}}\n转录{{SUMMARY_SOURCE_TEXT}}"
), meeting, source, "关注风险");
assertTrue(userMessage.contains("章节辅助结构如下"));
assertTrue(userMessage.contains("第1章 预算评审"));
assertTrue(userMessage.contains("原始会议转录如下"));
assertTrue(userMessage.contains("Alice: 今天讨论 320 万预算"));
assertTrue(userMessage.contains("最终不要在 `analysis` 中返回章节列表"));
}
@Test
void resolveSystemPromptShouldFailWhenSystemParamMissing() {
SysParamService sysParamService = mock(SysParamService.class);
when(sysParamService.getCachedParamValue(eq(SysParamKeys.MEETING_SUMMARY_SYSTEM_PROMPT), eq("")))
.thenReturn(" ");
MeetingSummaryPromptAssembler assembler = new MeetingSummaryPromptAssembler(
mock(PromptTemplateService.class),
sysParamService
);
RuntimeException exception = assertThrows(RuntimeException.class, assembler::resolveSystemPrompt);
assertTrue(exception.getMessage().contains(SysParamKeys.MEETING_SUMMARY_SYSTEM_PROMPT));
assertTrue(userMessage.contains("周会"));
assertTrue(userMessage.contains("Alice: hello"));
assertTrue(userMessage.contains("第一章"));
}
}

View File

@ -233,12 +233,12 @@ export default function Login() {
</Form.Item>
</Form>
<div className="login-footer">
<Text type="secondary">
{t("login.demoAccount")} <Text strong className="tabular-nums">admin</Text> / {t("login.password")}{" "}
<Text strong className="tabular-nums">123456</Text>
</Text>
</div>
{/*<div className="login-footer">*/}
{/* <Text type="secondary">*/}
{/* {t("login.demoAccount")} <Text strong className="tabular-nums">admin</Text> / {t("login.password")}{" "}*/}
{/* <Text strong className="tabular-nums">123456</Text>*/}
{/* </Text>*/}
{/*</div>*/}
</div>
</div>
</div>

View File

@ -77,7 +77,7 @@ const AiModels: React.FC = () => {
const provider = Form.useWatch("provider", form);
const isDefaultChecked = Form.useWatch("isDefaultChecked", form);
const isLocalProvider = String(provider || "").toLowerCase() === "custom";
const isLocalProvider = String(provider || "").toLowerCase() === "local";
const isTencentProvider = String(provider || "").toLowerCase() === "tencent";
const isPlatformAdmin = useMemo(() => {
@ -352,15 +352,15 @@ const AiModels: React.FC = () => {
}
const values = await form.validateFields(["provider", "baseUrl"]);
if (String(values.provider || "").toLowerCase() !== "custom") {
if (String(values.provider || "").toLowerCase() !== "local") {
message.warning("仅本地模型支持连通性测试");
return;
}
const { apiKey } = form.getFieldsValue(["apiKey"]);
if (!apiKey) {
message.warning("请先填写 API Key 后再测试连接");
return;
}
// if (!apiKey) {
// message.warning("请先填写 API Key 后再测试连接");
// return;
// }
setConnectivityLoading(true);
try {

View File

@ -1218,7 +1218,6 @@ const MeetingDetail: React.FC = () => {
const [loading, setLoading] = useState(true);
const [editVisible, setEditVisible] = useState(false);
const [summaryVisible, setSummaryVisible] = useState(false);
const [summaryRecordVisible, setSummaryRecordVisible] = useState(false);
const [actionLoading, setActionLoading] = useState(false);
const [downloadLoading, setDownloadLoading] = useState<'pdf' | 'word' | 'transcript' | null>(null);
const [isEditingSummary, setIsEditingSummary] = useState(false);
@ -1252,7 +1251,6 @@ const MeetingDetail: React.FC = () => {
const emptyTranscriptNoticeShownRef = useRef<number | null>(null);
const audioPlaybackErrorShownRef = useRef<string | null>(null);
const summaryPdfRef = useRef<HTMLDivElement>(null);
const transcriptItemRefs = useRef<Record<number, HTMLDivElement | null>>({});
const pendingTranscriptScrollIdRef = useRef<number | null>(null);
const leftColumnRef = useRef<HTMLDivElement>(null);
@ -2562,6 +2560,9 @@ const MeetingDetail: React.FC = () => {
</div>
) : hasSummaryContent ? (
<div className="summary-content-box">
<div className="summary-head">
<div className="summary-title"></div>
</div>
{summaryPanelNotice && (
<Alert
type={summaryPanelNotice.type}
@ -2624,7 +2625,35 @@ const MeetingDetail: React.FC = () => {
</div>
<div className="summary-section">
<div className="summary-section-title"></div>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12
}}>
<div className="summary-section-title" style={{marginBottom: 0}}></div>
{isOwner ? (
isEditingSummary ? (
<Space size={8}>
<Button onClick={() => setIsEditingSummary(false)}></Button>
<Button type="primary" onClick={handleSaveSummary} loading={actionLoading}></Button>
</Space>
) : (
<Button
size="small"
type="primary"
ghost
icon={<EditOutlined/>}
onClick={() => {
setSummaryDraft(meeting.summaryContent || '');
setIsEditingSummary(true);
}}
>
</Button>
)
) : null}
</div>
<div className="markdown-body summary-markdown">
{isEditingSummary ? (
<Input.TextArea
@ -3081,24 +3110,6 @@ const MeetingDetail: React.FC = () => {
font-size: 18px;
font-weight: 800;
}
.summary-head-actions {
display: flex;
align-items: center;
gap: 16px;
}
.summary-head-link {
color: #6e7695;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
transition: color 0.2s;
}
.summary-head-link:hover {
color: #5f51ff;
}
.summary-content-box {
background: #f8faff;
border: 1px solid #eef1f9;
@ -3622,14 +3633,6 @@ const MeetingDetail: React.FC = () => {
visibility: visible;
}
.summary-head-link--static {
cursor: default !important;
color: #9aa0bd !important;
}
.summary-head-link--static:hover {
color: #9aa0bd !important;
}
.summary-inline-edit {
width: 100% !important;
border-radius: 12px !important;
@ -4099,15 +4102,6 @@ const MeetingDetail: React.FC = () => {
box-shadow: 0 4px 12px rgba(110, 117, 255, 0.24);
cursor: pointer;
}
.markdown-shell {
height: 100%;
min-height: 0;
min-width: 0;
width: 100%;
overflow-x: hidden;
padding-bottom: 28px;
box-sizing: border-box;
}
.summary-markdown {
font-size: 14px;
line-height: 1.9;
@ -4233,61 +4227,6 @@ const MeetingDetail: React.FC = () => {
</Modal>
)}
<Drawer
title="总结记录"
width={680}
onClose={() => {
setSummaryRecordVisible(false);
setIsEditingSummary(false);
}}
open={summaryRecordVisible}
extra={
meeting?.summaryContent ? (
isEditingSummary ? (
<Space>
<Button onClick={() => setIsEditingSummary(false)}></Button>
<Button type="primary" onClick={handleSaveSummary} loading={actionLoading}></Button>
</Space>
) : (
isOwner ? (
<Button
type="primary"
ghost
icon={<EditOutlined />}
onClick={() => {
setSummaryDraft(meeting.summaryContent || '');
setIsEditingSummary(true);
}}
>
</Button>
) : null
)
) : null
}
forceRender
>
<div ref={summaryPdfRef} className="markdown-shell">
{meeting?.summaryContent ? (
isEditingSummary ? (
<Input.TextArea
value={summaryDraft}
onChange={(event) => setSummaryDraft(event.target.value)}
style={{ minHeight: 480, resize: 'none' }}
/>
) : (
<div className="markdown-body summary-markdown">
<ReactMarkdown>
{meeting.summaryContent}
</ReactMarkdown>
</div>
)
) : (
<Empty description="暂无总结" />
)}
</div>
</Drawer>
{isOwner && (
<Drawer
title="重新生成 AI 总结"