refactor:优化 MeetingSummaryPromptAssembler 和相关逻辑
- 更新 `MeetingSummaryPromptAssembler`,引入新的模板和占位符 - 在 `MeetingSummaryFileServiceImpl` 中重构代码以简化解析逻辑 - 更新 `SysParamKeys`,添加新的系统参数键 - 调整前端 `AiModels` 页面的本地模型验证逻辑 - 注释掉登录页面中的示例账户信息dev_na
parent
065708497a
commit
2a95eca102
|
|
@ -4,12 +4,18 @@ package com.imeeting.common;
|
||||||
* 系统参数 Key 常量定义。
|
* 系统参数 Key 常量定义。
|
||||||
*/
|
*/
|
||||||
public final class SysParamKeys {
|
public final class SysParamKeys {
|
||||||
private SysParamKeys() {}
|
private SysParamKeys() {
|
||||||
|
}
|
||||||
|
|
||||||
/** 登录验证码开关。 */
|
|
||||||
public static final String CAPTCHA_ENABLED = "security.captcha.enabled";
|
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_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 目录。 */
|
/** 是否启用 AI 目录。 */
|
||||||
public static final String MEETING_AI_CATALOG_ENABLED = "meeting.ai_catalog.enabled";
|
public static final String MEETING_AI_CATALOG_ENABLED = "meeting.ai_catalog.enabled";
|
||||||
/** 会议总结派发模式:PARALLEL / SERIAL。 */
|
/** 会议总结派发模式:PARALLEL / SERIAL。 */
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,18 @@ public class MeetingSummaryPromptContextVO {
|
||||||
@Schema(description = "提示词协议版本")
|
@Schema(description = "提示词协议版本")
|
||||||
private String promptSchemaVersion;
|
private String promptSchemaVersion;
|
||||||
|
|
||||||
@Schema(description = "系统消息")
|
@Schema(description = "最终系统消息")
|
||||||
private String systemMessage;
|
private String systemMessage;
|
||||||
|
|
||||||
@Schema(description = "带占位符的用户消息模板")
|
@Schema(description = "系统提示词模板")
|
||||||
|
private String systemMessageTemplate;
|
||||||
|
|
||||||
|
@Schema(description = "最终用户消息模板")
|
||||||
private String userMessageTemplate;
|
private String userMessageTemplate;
|
||||||
|
|
||||||
|
@Schema(description = "用户提示词模板原文")
|
||||||
|
private String userMessageTemplateRaw;
|
||||||
|
|
||||||
@Schema(description = "有效模板提示词")
|
@Schema(description = "有效模板提示词")
|
||||||
private String effectiveTemplatePrompt;
|
private String effectiveTemplatePrompt;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1253,7 +1253,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
req.put("temperature", llmModel.getTemperature());
|
req.put("temperature", llmModel.getTemperature());
|
||||||
req.put("messages", List.of(
|
req.put("messages", List.of(
|
||||||
Map.of("role", "system", "content", meetingSummaryPromptAssembler.buildSystemMessage(taskRecord.getTaskConfig())),
|
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);
|
taskRecord.setRequestData(req);
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,9 @@ public class MeetingQueryServiceImpl implements MeetingQueryService {
|
||||||
MeetingSummaryPromptContextVO context = new MeetingSummaryPromptContextVO();
|
MeetingSummaryPromptContextVO context = new MeetingSummaryPromptContextVO();
|
||||||
context.setPromptSchemaVersion(String.valueOf(taskConfig.get("promptSchemaVersion")));
|
context.setPromptSchemaVersion(String.valueOf(taskConfig.get("promptSchemaVersion")));
|
||||||
context.setSystemMessage(meetingSummaryPromptAssembler.buildSystemMessage(taskConfig));
|
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.setEffectiveTemplatePrompt(stringValue(taskConfig.get("effectiveTemplatePrompt")));
|
||||||
context.setEffectiveUserPrompt(meetingSummaryPromptAssembler.normalizeOptionalText(userPrompt));
|
context.setEffectiveUserPrompt(meetingSummaryPromptAssembler.normalizeOptionalText(userPrompt));
|
||||||
context.setSummaryModelId(summaryModelId);
|
context.setSummaryModelId(summaryModelId);
|
||||||
|
|
|
||||||
|
|
@ -12,172 +12,132 @@ import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
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 = "v2";
|
private static final String SUMMARY_SYSTEM_TEMPLATE_KEY = SysParamKeys.MEETING_SUMMARY_SYSTEM_PROMPT;
|
||||||
private static final String SUMMARY_SOURCE_PLACEHOLDER = "{{SUMMARY_SOURCE_TEXT}}";
|
private static final String SUMMARY_USER_TEMPLATE_KEY = SysParamKeys.MEETING_SUMMARY_USER_TEMPLATE;
|
||||||
private static final String SYSTEM_PROMPT_NOT_CONFIGURED_MESSAGE =
|
private static final String CHAPTER_SYSTEM_TEMPLATE_KEY = SysParamKeys.MEETING_CHAPTER_SYSTEM_PROMPT;
|
||||||
"系统提示词未配置,请先维护系统参数 " + SysParamKeys.MEETING_SUMMARY_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 PromptTemplateService promptTemplateService;
|
||||||
private final SysParamService sysParamService;
|
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) {
|
public Map<String, Object> buildTaskConfig(Long summaryModelId, Long chapterModelId, Long promptId, String userPrompt, String summaryDetailLevel) {
|
||||||
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) {
|
|
||||||
Map<String, Object> taskConfig = new HashMap<>();
|
Map<String, Object> taskConfig = new HashMap<>();
|
||||||
taskConfig.put("summaryModelId", summaryModelId);
|
taskConfig.put("summaryModelId", summaryModelId);
|
||||||
taskConfig.put("chapterModelId", chapterModelId != null ? chapterModelId : summaryModelId);
|
taskConfig.put("chapterModelId", chapterModelId != null ? chapterModelId : summaryModelId);
|
||||||
taskConfig.put("promptSchemaVersion", PROMPT_SCHEMA_VERSION);
|
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));
|
taskConfig.put("summaryDetailLevel", normalizeSummaryDetailLevel(summaryDetailLevel));
|
||||||
|
taskConfig.put("effectiveTemplatePrompt", resolveTemplatePrompt(promptId));
|
||||||
String templatePrompt = resolveTemplatePrompt(promptId);
|
|
||||||
taskConfig.put("effectiveTemplatePrompt", templatePrompt);
|
|
||||||
taskConfig.put("userPrompt", normalizeOptionalText(userPrompt));
|
taskConfig.put("userPrompt", normalizeOptionalText(userPrompt));
|
||||||
|
|
||||||
if (promptId != null) {
|
if (promptId != null) {
|
||||||
taskConfig.put("promptId", promptId);
|
taskConfig.put("promptId", promptId);
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(templatePrompt)) {
|
|
||||||
taskConfig.put("promptContent", templatePrompt);
|
|
||||||
}
|
|
||||||
return taskConfig;
|
return taskConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String buildSystemMessage(Map<String, Object> taskConfig) {
|
public String buildSystemMessage(Map<String, Object> taskConfig) {
|
||||||
String systemPrompt = stringValue(taskConfig, "effectiveSystemPrompt");
|
return render(resolveSummarySystemTemplate(taskConfig), buildSummaryValues(taskConfig, null, null, null));
|
||||||
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. 如果系统提示词、模板提示词和用户提示词存在冲突,优先级是:系统提示词 > 模板提示词 > 用户提示词。
|
|
||||||
"""
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String buildUserMessage(Meeting meeting, String transcriptText, String userPrompt) {
|
public String buildUserMessage(Map<String, Object> taskConfig, Meeting meeting, MeetingSummarySource summarySource, String userPrompt) {
|
||||||
return buildUserMessage(
|
return render(resolveSummaryUserTemplate(taskConfig), buildSummaryValues(taskConfig, meeting, summarySource, userPrompt));
|
||||||
meeting,
|
|
||||||
MeetingSummarySource.builder()
|
|
||||||
.text(transcriptText)
|
|
||||||
.rawTranscriptText(transcriptText)
|
|
||||||
.chapterOutlineText("")
|
|
||||||
.build(),
|
|
||||||
userPrompt
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String buildUserMessage(Meeting meeting, MeetingSummarySource summarySource, String userPrompt) {
|
public String buildUserMessage(Meeting meeting, MeetingSummarySource summarySource, String userPrompt) {
|
||||||
String participants = meeting.getParticipants() == null || meeting.getParticipants().isBlank()
|
return buildUserMessage(null, meeting, summarySource, userPrompt);
|
||||||
? "未填写"
|
|
||||||
: 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()
|
public String buildUserMessageTemplate(Map<String, Object> taskConfig, Meeting meeting, String userPrompt) {
|
||||||
.append("请基于以下会议信息、章节辅助结构和原始会议转录生成会议纪要与结构化分析结果。\n")
|
return render(resolveSummaryUserTemplate(taskConfig), buildSummaryValues(taskConfig, meeting, null, userPrompt));
|
||||||
.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,格式固定如下:
|
public String buildChapterSystemMessage(Map<String, Object> taskConfig) {
|
||||||
{
|
return render(resolveChapterSystemTemplate(taskConfig), Map.of(CHAPTER_OUTPUT_SCHEMA, buildChapterOutputSchema()));
|
||||||
"summaryContent": "完整会议纪要正文,使用 markdown",
|
|
||||||
"analysis": {
|
|
||||||
"keywords": ["关键词1", "关键词2"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
要求:
|
public String buildChapterUserMessage(String transcriptSegmentsJson) {
|
||||||
1. `summaryContent` 必须优先遵循模板提示词中的结构、标题层级、章节顺序和写作风格。
|
return render(DEFAULT_CHAPTER_USER_TEMPLATE, Map.of(TRANSCRIPT_SEGMENTS_JSON, firstNonBlank(transcriptSegmentsJson, "")));
|
||||||
2. `analysis.keywords` 必须基于完整转录内容生成,不得脱离上下文,并且在原始会议转录中能找到对应依据。
|
|
||||||
3. 章节信息只是辅助结构,不能替代原始转录真值;数字、日期、时间、金额、百分比等必须优先以原始转录为准。
|
|
||||||
4. 最终不要在 `analysis` 中返回章节列表。
|
|
||||||
5. 仅输出 JSON。
|
|
||||||
|
|
||||||
章节辅助结构如下:
|
|
||||||
""").append(chapterOutlineText)
|
|
||||||
.append("\n\n")
|
|
||||||
.append("原始会议转录如下:\n")
|
|
||||||
.append(rawTranscriptText == null ? "" : rawTranscriptText);
|
|
||||||
return message.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String buildUserMessageTemplate(Meeting meeting, String userPrompt) {
|
public String normalizeOptionalText(String value) {
|
||||||
return buildUserMessage(meeting, SUMMARY_SOURCE_PLACEHOLDER, userPrompt);
|
return firstNonBlank(value, null);
|
||||||
}
|
|
||||||
|
|
||||||
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 resolveTemplatePrompt(Long promptId) {
|
public String resolveTemplatePrompt(Long promptId) {
|
||||||
|
|
@ -185,22 +145,112 @@ public class MeetingSummaryPromptAssembler {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
PromptTemplate template = promptTemplateService.getById(promptId);
|
PromptTemplate template = promptTemplateService.getById(promptId);
|
||||||
if (template == null) {
|
return template == null ? "" : firstNonBlank(template.getPromptContent(), "");
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return firstNonBlank(template.getPromptContent(), "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String normalizeOptionalText(String value) {
|
private String resolveSummarySystemTemplate() {
|
||||||
return firstNonBlank(value, null);
|
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 stringValue(Map<String, Object> source, String key) {
|
private String resolveSummarySystemTemplate(Map<String, Object> taskConfig) {
|
||||||
if (source == null || key == null) {
|
return firstNonBlank(stringValue(taskConfig, "summaryPromptTemplate"), resolveSummarySystemTemplate());
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
Object value = source.get(key);
|
|
||||||
return value == null ? null : normalizeOptionalText(String.valueOf(value));
|
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 buildSummaryOutputSchema() {
|
||||||
|
return """
|
||||||
|
{
|
||||||
|
"summaryContent": "完整会议纪要正文,使用 markdown",
|
||||||
|
"analysis": {
|
||||||
|
"keywords": ["关键词"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildChapterOutputSchema() {
|
||||||
|
return """
|
||||||
|
{
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"chapterNo": 1,
|
||||||
|
"title": "章节标题",
|
||||||
|
"summary": "章节摘要",
|
||||||
|
"keywords": ["关键词"],
|
||||||
|
"startTranscriptId": 1,
|
||||||
|
"endTranscriptId": 2,
|
||||||
|
"confidence": 0.95
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""";
|
||||||
}
|
}
|
||||||
|
|
||||||
private String firstNonBlank(String... values) {
|
private String firstNonBlank(String... values) {
|
||||||
|
|
@ -215,26 +265,31 @@ public class MeetingSummaryPromptAssembler {
|
||||||
return null;
|
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) {
|
private String normalizeSummaryDetailLevel(String summaryDetailLevel) {
|
||||||
if (!StringUtils.hasText(summaryDetailLevel)) {
|
if (!StringUtils.hasText(summaryDetailLevel)) {
|
||||||
return MeetingConstants.SUMMARY_DETAIL_STANDARD;
|
return MeetingConstants.SUMMARY_DETAIL_STANDARD;
|
||||||
}
|
}
|
||||||
String normalized = summaryDetailLevel.trim().toUpperCase();
|
String normalized = summaryDetailLevel.trim().toUpperCase();
|
||||||
if (MeetingConstants.SUMMARY_DETAIL_DETAILED.equals(normalized)
|
return MeetingConstants.SUMMARY_DETAIL_DETAILED.equals(normalized) || MeetingConstants.SUMMARY_DETAIL_BRIEF.equals(normalized)
|
||||||
|| MeetingConstants.SUMMARY_DETAIL_BRIEF.equals(normalized)) {
|
? normalized
|
||||||
return normalized;
|
: MeetingConstants.SUMMARY_DETAIL_STANDARD;
|
||||||
}
|
|
||||||
return MeetingConstants.SUMMARY_DETAIL_STANDARD;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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(详细):尽量完整覆盖会议背景、讨论过程、分歧、结论、负责人、时间点和风险,适当展开章节内容。";
|
"DETAILED:尽量完整覆盖会议背景、讨论过程、分歧、结论、负责人、时间点和风险,适当展开章节内容。";
|
||||||
case MeetingConstants.SUMMARY_DETAIL_BRIEF ->
|
case MeetingConstants.SUMMARY_DETAIL_BRIEF ->
|
||||||
"BRIEF(简洁):只保留核心结论、关键决策、待办事项和必要上下文,避免冗长铺陈。";
|
"BRIEF:只保留核心结论、关键决策、待办事项和必要上下文,避免冗长铺陈。";
|
||||||
default ->
|
default -> "STANDARD:保持信息完整和篇幅平衡,覆盖核心过程、结论和待办,不做过度展开。";
|
||||||
"STANDARD(标准):保持信息完整和篇幅平衡,覆盖核心过程、结论和待办,不做过度展开。";
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.imeeting.common.SysParamKeys;
|
||||||
import com.imeeting.dto.biz.AiModelVO;
|
import com.imeeting.dto.biz.AiModelVO;
|
||||||
import com.imeeting.dto.biz.MeetingSummarySource;
|
import com.imeeting.dto.biz.MeetingSummarySource;
|
||||||
import com.imeeting.dto.biz.MeetingTranscriptChapterImportDTO;
|
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.mapper.biz.MeetingTranscriptMapper;
|
||||||
import com.imeeting.service.biz.AiModelService;
|
import com.imeeting.service.biz.AiModelService;
|
||||||
import com.imeeting.service.biz.MeetingTranscriptChapterService;
|
import com.imeeting.service.biz.MeetingTranscriptChapterService;
|
||||||
|
import com.unisbase.service.SysParamService;
|
||||||
|
import com.unisbase.service.SysParamService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
@ -74,6 +77,8 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
|
||||||
private final MeetingTranscriptChapterMapper chapterMapper;
|
private final MeetingTranscriptChapterMapper chapterMapper;
|
||||||
private final AiTaskMapper aiTaskMapper;
|
private final AiTaskMapper aiTaskMapper;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
private final MeetingSummaryPromptAssembler meetingSummaryPromptAssembler;
|
||||||
|
private final SysParamService sysParamService;
|
||||||
|
|
||||||
private AiModelService aiModelService;
|
private AiModelService aiModelService;
|
||||||
|
|
||||||
|
|
@ -93,6 +98,27 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
|
||||||
this.aiModelService = aiModelService;
|
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
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public MeetingSummarySource resolveSummarySource(Meeting meeting, AiTask summaryTask) {
|
public MeetingSummarySource resolveSummarySource(Meeting meeting, AiTask summaryTask) {
|
||||||
|
|
@ -308,8 +334,8 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
|
||||||
requestBody.put("model", llmModel.getModelCode());
|
requestBody.put("model", llmModel.getModelCode());
|
||||||
requestBody.put("temperature", llmModel.getTemperature());
|
requestBody.put("temperature", llmModel.getTemperature());
|
||||||
requestBody.put("messages", List.of(
|
requestBody.put("messages", List.of(
|
||||||
Map.of("role", "system", "content", buildChapterSystemPrompt()),
|
Map.of("role", "system", "content", renderChapterSystemPrompt()),
|
||||||
Map.of("role", "user", "content", buildChapterUserPrompt(transcripts))
|
Map.of("role", "user", "content", renderChapterUserPrompt(transcripts))
|
||||||
));
|
));
|
||||||
String payload = objectMapper.writeValueAsString(requestBody);
|
String payload = objectMapper.writeValueAsString(requestBody);
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
|
@ -886,10 +912,13 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
int firstBreak = normalized.indexOf('\n');
|
int firstBreak = normalized.indexOf('\n');
|
||||||
int lastFence = normalized.lastIndexOf("\n```");
|
if (firstBreak < 0) {
|
||||||
if (firstBreak < 0 || lastFence <= firstBreak) {
|
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
int lastFence = normalized.lastIndexOf("\n```");
|
||||||
|
if (lastFence <= firstBreak) {
|
||||||
|
return normalized.substring(firstBreak + 1).trim();
|
||||||
|
}
|
||||||
return normalized.substring(firstBreak + 1, lastFence).trim();
|
return normalized.substring(firstBreak + 1, lastFence).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -983,8 +1012,8 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
|
||||||
requestBody.put("model", llmModel.getModelCode());
|
requestBody.put("model", llmModel.getModelCode());
|
||||||
requestBody.put("temperature", llmModel.getTemperature());
|
requestBody.put("temperature", llmModel.getTemperature());
|
||||||
requestBody.put("messages", List.of(
|
requestBody.put("messages", List.of(
|
||||||
Map.of("role", "system", "content", buildChapterSystemPrompt()),
|
Map.of("role", "system", "content", renderChapterSystemPrompt()),
|
||||||
Map.of("role", "user", "content", buildChapterUserPrompt(transcripts))
|
Map.of("role", "user", "content", renderChapterUserPrompt(transcripts))
|
||||||
));
|
));
|
||||||
String payload = objectMapper.writeValueAsString(requestBody);
|
String payload = objectMapper.writeValueAsString(requestBody);
|
||||||
requestUrl = appendPath(llmModel.getBaseUrl(), nonBlank(llmModel.getApiPath(), "v1/chat/completions"));
|
requestUrl = appendPath(llmModel.getBaseUrl(), nonBlank(llmModel.getApiPath(), "v1/chat/completions"));
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,6 @@ import java.time.LocalDateTime;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
@ -23,110 +21,52 @@ import static org.mockito.Mockito.when;
|
||||||
class MeetingSummaryPromptAssemblerTest {
|
class MeetingSummaryPromptAssemblerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void buildTaskConfigShouldCaptureEffectivePromptsUserPromptAndDetailLevel() {
|
void buildTaskConfigShouldIncludePromptTemplates() {
|
||||||
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
|
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
|
||||||
SysParamService sysParamService = mock(SysParamService.class);
|
SysParamService sysParamService = mock(SysParamService.class);
|
||||||
PromptTemplate template = new PromptTemplate();
|
PromptTemplate template = new PromptTemplate();
|
||||||
template.setPromptContent("模板提示词");
|
template.setPromptContent("模板提示词");
|
||||||
when(promptTemplateService.getById(3L)).thenReturn(template);
|
when(promptTemplateService.getById(3L)).thenReturn(template);
|
||||||
when(sysParamService.getCachedParamValue(eq(SysParamKeys.MEETING_SUMMARY_SYSTEM_PROMPT), eq("")))
|
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);
|
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(
|
assertEquals("v3", taskConfig.get("promptSchemaVersion"));
|
||||||
2L,
|
assertEquals("总结系统模板", taskConfig.get("summaryPromptTemplate"));
|
||||||
5L,
|
assertEquals("总结用户模板", taskConfig.get("summaryUserTemplate"));
|
||||||
3L,
|
assertEquals("章节系统模板", taskConfig.get("chapterPromptTemplate"));
|
||||||
" 关注风险项 ",
|
assertEquals("章节用户模板", taskConfig.get("chapterUserTemplate"));
|
||||||
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"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void buildSystemMessageShouldContainSummaryDetailInstruction() {
|
void buildUserMessageShouldRenderPlaceholders() {
|
||||||
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() {
|
|
||||||
MeetingSummaryPromptAssembler assembler = new MeetingSummaryPromptAssembler(
|
MeetingSummaryPromptAssembler assembler = new MeetingSummaryPromptAssembler(
|
||||||
mock(PromptTemplateService.class),
|
mock(PromptTemplateService.class),
|
||||||
mock(SysParamService.class)
|
mock(SysParamService.class)
|
||||||
);
|
);
|
||||||
Meeting meeting = new Meeting();
|
Meeting meeting = new Meeting();
|
||||||
meeting.setTitle("周会");
|
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.setMeetingTime(LocalDateTime.of(2026, 5, 8, 10, 0));
|
||||||
meeting.setParticipants("Alice,Bob");
|
meeting.setParticipants("Alice,Bob");
|
||||||
|
|
||||||
MeetingSummarySource source = MeetingSummarySource.builder()
|
MeetingSummarySource source = MeetingSummarySource.builder()
|
||||||
.chapterOutlineText("第1章 预算评审\n章节摘要:讨论 20 万预算")
|
.chapterOutlineText("第一章")
|
||||||
.rawTranscriptText("Alice: 今天讨论 320 万预算,时间定在 2026 年 5 月 8 日 15:30。")
|
.rawTranscriptText("Alice: hello")
|
||||||
.build();
|
.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("周会"));
|
||||||
assertTrue(userMessage.contains("第1章 预算评审"));
|
assertTrue(userMessage.contains("Alice: hello"));
|
||||||
assertTrue(userMessage.contains("原始会议转录如下"));
|
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -233,12 +233,12 @@ export default function Login() {
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<div className="login-footer">
|
{/*<div className="login-footer">*/}
|
||||||
<Text type="secondary">
|
{/* <Text type="secondary">*/}
|
||||||
{t("login.demoAccount")} <Text strong className="tabular-nums">admin</Text> / {t("login.password")}{" "}
|
{/* {t("login.demoAccount")} <Text strong className="tabular-nums">admin</Text> / {t("login.password")}{" "}*/}
|
||||||
<Text strong className="tabular-nums">123456</Text>
|
{/* <Text strong className="tabular-nums">123456</Text>*/}
|
||||||
</Text>
|
{/* </Text>*/}
|
||||||
</div>
|
{/*</div>*/}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ const AiModels: React.FC = () => {
|
||||||
|
|
||||||
const provider = Form.useWatch("provider", form);
|
const provider = Form.useWatch("provider", form);
|
||||||
const isDefaultChecked = Form.useWatch("isDefaultChecked", 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 isTencentProvider = String(provider || "").toLowerCase() === "tencent";
|
||||||
|
|
||||||
const isPlatformAdmin = useMemo(() => {
|
const isPlatformAdmin = useMemo(() => {
|
||||||
|
|
@ -352,15 +352,15 @@ const AiModels: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const values = await form.validateFields(["provider", "baseUrl"]);
|
const values = await form.validateFields(["provider", "baseUrl"]);
|
||||||
if (String(values.provider || "").toLowerCase() !== "custom") {
|
if (String(values.provider || "").toLowerCase() !== "local") {
|
||||||
message.warning("仅本地模型支持连通性测试");
|
message.warning("仅本地模型支持连通性测试");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { apiKey } = form.getFieldsValue(["apiKey"]);
|
const { apiKey } = form.getFieldsValue(["apiKey"]);
|
||||||
if (!apiKey) {
|
// if (!apiKey) {
|
||||||
message.warning("请先填写 API Key 后再测试连接");
|
// message.warning("请先填写 API Key 后再测试连接");
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
setConnectivityLoading(true);
|
setConnectivityLoading(true);
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1218,7 +1218,6 @@ const MeetingDetail: React.FC = () => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [editVisible, setEditVisible] = useState(false);
|
const [editVisible, setEditVisible] = useState(false);
|
||||||
const [summaryVisible, setSummaryVisible] = useState(false);
|
const [summaryVisible, setSummaryVisible] = useState(false);
|
||||||
const [summaryRecordVisible, setSummaryRecordVisible] = useState(false);
|
|
||||||
const [actionLoading, setActionLoading] = useState(false);
|
const [actionLoading, setActionLoading] = useState(false);
|
||||||
const [downloadLoading, setDownloadLoading] = useState<'pdf' | 'word' | 'transcript' | null>(null);
|
const [downloadLoading, setDownloadLoading] = useState<'pdf' | 'word' | 'transcript' | null>(null);
|
||||||
const [isEditingSummary, setIsEditingSummary] = useState(false);
|
const [isEditingSummary, setIsEditingSummary] = useState(false);
|
||||||
|
|
@ -1252,7 +1251,6 @@ const MeetingDetail: React.FC = () => {
|
||||||
const emptyTranscriptNoticeShownRef = useRef<number | null>(null);
|
const emptyTranscriptNoticeShownRef = useRef<number | null>(null);
|
||||||
const audioPlaybackErrorShownRef = useRef<string | null>(null);
|
const audioPlaybackErrorShownRef = useRef<string | null>(null);
|
||||||
|
|
||||||
const summaryPdfRef = useRef<HTMLDivElement>(null);
|
|
||||||
const transcriptItemRefs = useRef<Record<number, HTMLDivElement | null>>({});
|
const transcriptItemRefs = useRef<Record<number, HTMLDivElement | null>>({});
|
||||||
const pendingTranscriptScrollIdRef = useRef<number | null>(null);
|
const pendingTranscriptScrollIdRef = useRef<number | null>(null);
|
||||||
const leftColumnRef = useRef<HTMLDivElement>(null);
|
const leftColumnRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -2562,6 +2560,9 @@ const MeetingDetail: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
) : hasSummaryContent ? (
|
) : hasSummaryContent ? (
|
||||||
<div className="summary-content-box">
|
<div className="summary-content-box">
|
||||||
|
<div className="summary-head">
|
||||||
|
<div className="summary-title">会议总结</div>
|
||||||
|
</div>
|
||||||
{summaryPanelNotice && (
|
{summaryPanelNotice && (
|
||||||
<Alert
|
<Alert
|
||||||
type={summaryPanelNotice.type}
|
type={summaryPanelNotice.type}
|
||||||
|
|
@ -2624,7 +2625,35 @@ const MeetingDetail: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="summary-section">
|
<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">
|
<div className="markdown-body summary-markdown">
|
||||||
{isEditingSummary ? (
|
{isEditingSummary ? (
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
|
|
@ -3081,24 +3110,6 @@ const MeetingDetail: React.FC = () => {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 800;
|
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 {
|
.summary-content-box {
|
||||||
background: #f8faff;
|
background: #f8faff;
|
||||||
border: 1px solid #eef1f9;
|
border: 1px solid #eef1f9;
|
||||||
|
|
@ -3622,14 +3633,6 @@ const MeetingDetail: React.FC = () => {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-head-link--static {
|
|
||||||
cursor: default !important;
|
|
||||||
color: #9aa0bd !important;
|
|
||||||
}
|
|
||||||
.summary-head-link--static:hover {
|
|
||||||
color: #9aa0bd !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-inline-edit {
|
.summary-inline-edit {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
border-radius: 12px !important;
|
border-radius: 12px !important;
|
||||||
|
|
@ -4099,15 +4102,6 @@ const MeetingDetail: React.FC = () => {
|
||||||
box-shadow: 0 4px 12px rgba(110, 117, 255, 0.24);
|
box-shadow: 0 4px 12px rgba(110, 117, 255, 0.24);
|
||||||
cursor: pointer;
|
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 {
|
.summary-markdown {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.9;
|
line-height: 1.9;
|
||||||
|
|
@ -4233,61 +4227,6 @@ const MeetingDetail: React.FC = () => {
|
||||||
</Modal>
|
</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 && (
|
{isOwner && (
|
||||||
<Drawer
|
<Drawer
|
||||||
title="重新生成 AI 总结"
|
title="重新生成 AI 总结"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue