refactor:优化 MeetingSummaryPromptAssembler 和相关逻辑
- 更新 `MeetingSummaryPromptAssembler`,引入新的模板和占位符 - 在 `MeetingSummaryFileServiceImpl` 中重构代码以简化解析逻辑 - 更新 `SysParamKeys`,添加新的系统参数键 - 调整前端 `AiModels` 页面的本地模型验证逻辑 - 注释掉登录页面中的示例账户信息dev_na
parent
065708497a
commit
2a95eca102
|
|
@ -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。 */
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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:保持信息完整和篇幅平衡,覆盖核心过程、结论和待办,不做过度展开。";
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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("第一章"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 总结"
|
||||
|
|
|
|||
Loading…
Reference in New Issue