feat: 添加外部总结编排触发和失败处理功能
- 新增 `MeetingSummaryOrchestrationTriggerResultVO` 数据传输对象 - 实现 `MeetingExternalSummaryWebhookTrigger` 服务,用于触发外部 n8n 总结编排 - 在 `MeetingCommandServiceImpl` 中添加 `triggerExternalSummaryOrchestration` 和 `markExternalSummaryOrchestrationFailed` 方法 - 更新 `MeetingCommandService` 接口以支持新的方法 - 在 `AiTaskServiceImpl` 中添加 `triggerExternalSummaryWebhook` 方法 - 在 `MeetingController` 中添加手动触发外部 n8n 总结编排的 API - 新增 `MeetingMarkdownBundleMcpToolProvider` 以提供会议 Markdown 包工具dev_na
parent
3469884bca
commit
51190f330c
|
|
@ -5,6 +5,7 @@ import com.imeeting.common.RedisKeys;
|
|||
import com.imeeting.dto.biz.CreateMeetingCommand;
|
||||
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
|
||||
import com.imeeting.dto.biz.MeetingResummaryDTO;
|
||||
import com.imeeting.dto.biz.MeetingSummaryOrchestrationTriggerResultVO;
|
||||
import com.imeeting.dto.biz.MeetingSpeakerUpdateDTO;
|
||||
import com.imeeting.dto.biz.MeetingSummaryExportResult;
|
||||
import com.imeeting.dto.biz.MeetingTranscriptExportResult;
|
||||
|
|
@ -417,6 +418,17 @@ public class MeetingController {
|
|||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "手动触发外部 n8n 总结编排")
|
||||
@PostMapping("/{id}/summary/orchestration/trigger")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<MeetingSummaryOrchestrationTriggerResultVO> triggerExternalSummaryOrchestration(@PathVariable Long id,
|
||||
@RequestParam(defaultValue = "false") boolean force) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
||||
return ApiResponse.ok(meetingCommandService.triggerExternalSummaryOrchestration(id, force));
|
||||
}
|
||||
|
||||
@Operation(summary = "重试音频转写")
|
||||
@PostMapping("/{id}/transcripts/regenerate")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.imeeting.controller.biz;
|
||||
|
||||
import com.imeeting.dto.biz.MeetingSummaryFinalizeDTO;
|
||||
import com.imeeting.dto.biz.MeetingExternalWorkflowFailureDTO;
|
||||
import com.imeeting.dto.biz.MeetingSummaryPromptContextRequestDTO;
|
||||
import com.imeeting.dto.biz.MeetingSummaryPromptContextVO;
|
||||
import com.imeeting.dto.biz.MeetingTranscriptChapterImportDTO;
|
||||
|
|
@ -102,6 +103,22 @@ public class MeetingInternalWorkflowController {
|
|||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "回写外部编排失败")
|
||||
@PostMapping("/{meetingId}/summary/fail")
|
||||
public ApiResponse<Boolean> failSummary(HttpServletRequest request,
|
||||
@PathVariable Long meetingId,
|
||||
@Valid @RequestBody MeetingExternalWorkflowFailureDTO command) {
|
||||
if (!isExternalModeEnabled()) {
|
||||
return ApiResponse.error("External n8n summary orchestration is disabled");
|
||||
}
|
||||
if (!isInternalSecretValid(request)) {
|
||||
return ApiResponse.error("Invalid internal secret");
|
||||
}
|
||||
command.setMeetingId(meetingId);
|
||||
meetingCommandService.markExternalSummaryOrchestrationFailed(command);
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
private boolean isExternalModeEnabled() {
|
||||
return "EXTERNAL_N8N".equalsIgnoreCase(summaryOrchestrationMode);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
package com.imeeting.dto.biz;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "外部总结编排失败回写请求")
|
||||
public class MeetingExternalWorkflowFailureDTO {
|
||||
|
||||
@Schema(description = "会议ID")
|
||||
private Long meetingId;
|
||||
|
||||
@Schema(description = "总结任务ID")
|
||||
private Long summaryTaskId;
|
||||
|
||||
@Schema(description = "章节任务ID")
|
||||
private Long chapterTaskId;
|
||||
|
||||
@NotBlank(message = "stage must not be blank")
|
||||
@Schema(description = "失败阶段,建议值:CHAPTER / SUMMARY / WORKFLOW")
|
||||
private String stage;
|
||||
|
||||
@NotBlank(message = "errorMessage must not be blank")
|
||||
@Schema(description = "失败错误信息")
|
||||
private String errorMessage;
|
||||
|
||||
@Schema(description = "失败详情或原始错误")
|
||||
private String rawError;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.imeeting.dto.biz;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "会议外部总结编排触发结果")
|
||||
public class MeetingSummaryOrchestrationTriggerResultVO {
|
||||
|
||||
@Schema(description = "会议ID")
|
||||
private Long meetingId;
|
||||
|
||||
@Schema(description = "总结任务ID")
|
||||
private Long summaryTaskId;
|
||||
|
||||
@Schema(description = "触发来源")
|
||||
private String triggerSource;
|
||||
|
||||
@Schema(description = "触发状态")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "是否实际发起了 webhook 调用")
|
||||
private Boolean triggered;
|
||||
|
||||
@Schema(description = "是否因为已触发而跳过")
|
||||
private Boolean skipped;
|
||||
|
||||
@Schema(description = "HTTP 状态码")
|
||||
private Integer httpStatus;
|
||||
|
||||
@Schema(description = "结果说明")
|
||||
private String message;
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package com.imeeting.mcp;
|
||||
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingPreviewResult;
|
||||
import com.imeeting.service.mcp.MeetingMcpToolService;
|
||||
import com.unisbase.llm.tools.support.AbstractMcpToolProvider;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MeetingDetailPreviewMcpToolProvider extends AbstractMcpToolProvider {
|
||||
|
||||
private final MeetingMcpToolService meetingMcpToolService;
|
||||
|
||||
@Override
|
||||
protected String getToolName() {
|
||||
return "imeeting_get_meeting_detail_preview";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getToolDescription() {
|
||||
return "获取当前 bot 用户可见的指定会议详情,返回结构与现有会议预览接口保持一致。";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildInputSchema() {
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
properties.put("meetingId", integerProperty("会议 ID"));
|
||||
return objectSchema(properties, "meetingId");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object handle(Map<String, Object> params) {
|
||||
Long meetingId = requireLong(params, "meetingId");
|
||||
LegacyMeetingPreviewResult result = meetingMcpToolService.getMeetingPreview(meetingId);
|
||||
Map<String, Object> detailData = meetingMcpToolService.getMeetingRichDetail(meetingId);
|
||||
|
||||
Map<String, Object> query = new LinkedHashMap<>();
|
||||
query.put("meetingId", meetingId);
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("code", result.getCode());
|
||||
data.put("message", result.getMessage());
|
||||
data.put("data", detailData);
|
||||
return response(mapOf("tool", getToolName()), query, data);
|
||||
}
|
||||
|
||||
private Map<String, Object> integerProperty(String description) {
|
||||
Map<String, Object> property = new LinkedHashMap<>();
|
||||
property.put("type", "integer");
|
||||
property.put("description", description);
|
||||
return property;
|
||||
}
|
||||
|
||||
private Long requireLong(Map<String, Object> params, String key) {
|
||||
Object value = params == null ? null : params.get(key);
|
||||
if (value == null) {
|
||||
throw new IllegalArgumentException(key + " is required");
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(String.valueOf(value).trim());
|
||||
} catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException(key + " must be numeric");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package com.imeeting.mcp;
|
||||
|
||||
import com.imeeting.service.mcp.MeetingMcpToolService;
|
||||
import com.unisbase.llm.tools.support.AbstractMcpToolProvider;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MeetingMarkdownBundleMcpToolProvider extends AbstractMcpToolProvider {
|
||||
|
||||
private final MeetingMcpToolService meetingMcpToolService;
|
||||
|
||||
@Override
|
||||
protected String getToolName() {
|
||||
return "imeeting_get_meeting_markdown_bundle";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getToolDescription() {
|
||||
return "按会议 ID 直接获取会议总结 Markdown、会议转录 Markdown 和章节 Markdown。";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildInputSchema() {
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
properties.put("meetingId", integerProperty("会议 ID"));
|
||||
return objectSchema(properties, "meetingId");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object handle(Map<String, Object> params) {
|
||||
Long meetingId = requireLong(params, "meetingId");
|
||||
Map<String, Object> result = meetingMcpToolService.getMeetingMarkdownBundle(meetingId);
|
||||
|
||||
Map<String, Object> query = new LinkedHashMap<>();
|
||||
query.put("meetingId", meetingId);
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("code", "200");
|
||||
data.put("message", "success");
|
||||
data.put("data", result);
|
||||
return response(mapOf("tool", getToolName()), query, data);
|
||||
}
|
||||
|
||||
private Map<String, Object> integerProperty(String description) {
|
||||
Map<String, Object> property = new LinkedHashMap<>();
|
||||
property.put("type", "integer");
|
||||
property.put("description", description);
|
||||
return property;
|
||||
}
|
||||
|
||||
private Long requireLong(Map<String, Object> params, String key) {
|
||||
Object value = params == null ? null : params.get(key);
|
||||
if (value == null) {
|
||||
throw new IllegalArgumentException(key + " is required");
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(String.valueOf(value).trim());
|
||||
} catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException(key + " must be numeric");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.imeeting.mcp;
|
||||
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingListResponse;
|
||||
import com.imeeting.service.mcp.MeetingMcpToolService;
|
||||
import com.unisbase.llm.tools.support.AbstractMcpToolProvider;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MeetingRecordingListMcpToolProvider extends AbstractMcpToolProvider {
|
||||
|
||||
private final MeetingMcpToolService meetingMcpToolService;
|
||||
|
||||
@Override
|
||||
protected String getToolName() {
|
||||
return "imeeting_list_my_meeting_recordings";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getToolDescription() {
|
||||
return "获取当前 bot 用户可见的个人会议录音列表,返回结构与现有兼容会议列表保持一致。";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildInputSchema() {
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
properties.put("page", integerProperty("页码,从 1 开始,默认 1"));
|
||||
properties.put("pageSize", integerProperty("每页数量,默认 10"));
|
||||
properties.put("title", stringProperty("可选,按会议标题模糊过滤"));
|
||||
return objectSchema(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object handle(Map<String, Object> params) {
|
||||
Integer page = optionalInteger(params, "page", 1);
|
||||
Integer pageSize = optionalInteger(params, "pageSize", 10);
|
||||
String title = getString(params, "title");
|
||||
LegacyMeetingListResponse result = meetingMcpToolService.listCurrentUserMeetings(page, pageSize, title);
|
||||
|
||||
Map<String, Object> query = new LinkedHashMap<>();
|
||||
query.put("page", page);
|
||||
query.put("pageSize", pageSize);
|
||||
if (!isBlank(title)) {
|
||||
query.put("title", title);
|
||||
}
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("code", "200");
|
||||
data.put("message", "success");
|
||||
data.put("data", result);
|
||||
return response(mapOf("tool", getToolName()), query, data);
|
||||
}
|
||||
|
||||
private Map<String, Object> integerProperty(String description) {
|
||||
Map<String, Object> property = new LinkedHashMap<>();
|
||||
property.put("type", "integer");
|
||||
property.put("description", description);
|
||||
return property;
|
||||
}
|
||||
|
||||
private Integer optionalInteger(Map<String, Object> params, String key, int defaultValue) {
|
||||
Object value = params == null ? null : params.get(key);
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(String.valueOf(value).trim());
|
||||
} catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException(key + " must be numeric");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ package com.imeeting.service.biz;
|
|||
|
||||
import com.imeeting.dto.biz.CreateMeetingCommand;
|
||||
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
|
||||
import com.imeeting.dto.biz.MeetingExternalWorkflowFailureDTO;
|
||||
import com.imeeting.dto.biz.MeetingSummaryOrchestrationTriggerResultVO;
|
||||
import com.imeeting.dto.biz.MeetingSummaryFinalizeDTO;
|
||||
import com.imeeting.dto.biz.MeetingTranscriptChapterImportDTO;
|
||||
import com.imeeting.dto.biz.MeetingTranscriptChapterImportResultVO;
|
||||
|
|
@ -42,4 +44,8 @@ public interface MeetingCommandService {
|
|||
MeetingTranscriptChapterImportResultVO importTranscriptChapters(MeetingTranscriptChapterImportDTO command);
|
||||
|
||||
void finalizeSummary(MeetingSummaryFinalizeDTO command);
|
||||
|
||||
MeetingSummaryOrchestrationTriggerResultVO triggerExternalSummaryOrchestration(Long meetingId, boolean force);
|
||||
|
||||
void markExternalSummaryOrchestrationFailed(MeetingExternalWorkflowFailureDTO command);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
private final MeetingTranscriptChapterService meetingTranscriptChapterService;
|
||||
private final MeetingSummaryPromptAssembler meetingSummaryPromptAssembler;
|
||||
private final TaskSecurityContextRunner taskSecurityContextRunner;
|
||||
private final MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger;
|
||||
|
||||
@Value("${unisbase.app.server-base-url}")
|
||||
private String serverBaseUrl;
|
||||
|
|
@ -170,7 +171,9 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
return;
|
||||
}
|
||||
if (isExternalSummaryModeEnabled()) {
|
||||
updateProgress(meetingId, 95, "等待外部章节与总结编排...", 0);
|
||||
AiTask chapterTask = findLatestTask(meetingId, "CHAPTER");
|
||||
AiTask sumTask = findLatestTask(meetingId, "SUMMARY");
|
||||
triggerExternalSummaryWebhook(meeting, sumTask, chapterTask, "AUTO_SUMMARY_DISPATCH", false);
|
||||
return;
|
||||
}
|
||||
AiTask chapterTask = findLatestTask(meetingId, "CHAPTER");
|
||||
|
|
@ -642,8 +645,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
|
||||
private void executeSummaryFlow(Meeting meeting, AiTask sumTask, AiTask chapterTask) throws Exception {
|
||||
if (isExternalSummaryModeEnabled()) {
|
||||
updateMeetingStatus(meeting.getId(), 2);
|
||||
updateProgress(meeting.getId(), 95, "等待外部章节与总结编排...", 0);
|
||||
triggerExternalSummaryWebhook(meeting, sumTask, chapterTask, "AUTO_AFTER_TRANSCRIPT_READY", false);
|
||||
return;
|
||||
}
|
||||
String summaryLockKey = RedisKeys.meetingSummaryLockKey(meeting.getId());
|
||||
|
|
@ -678,6 +680,30 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
return "EXTERNAL_N8N".equalsIgnoreCase(summaryOrchestrationMode);
|
||||
}
|
||||
|
||||
private void triggerExternalSummaryWebhook(Meeting meeting,
|
||||
AiTask summaryTask,
|
||||
AiTask chapterTask,
|
||||
String triggerSource,
|
||||
boolean force) {
|
||||
if (meeting == null || meeting.getId() == null) {
|
||||
return;
|
||||
}
|
||||
if (summaryTask == null) {
|
||||
updateProgress(meeting.getId(), -1, "缺少总结任务,无法触发外部 n8n 编排", 0);
|
||||
return;
|
||||
}
|
||||
updateMeetingStatus(meeting.getId(), 2);
|
||||
try {
|
||||
var result = meetingExternalSummaryWebhookTrigger.trigger(meeting, summaryTask, chapterTask, triggerSource, force);
|
||||
this.updateById(summaryTask);
|
||||
updateProgress(meeting.getId(), 95, result.getMessage(), 0);
|
||||
} catch (Exception ex) {
|
||||
this.updateById(summaryTask);
|
||||
updateProgress(meeting.getId(), -1, "触发外部 n8n 编排失败: " + ex.getMessage(), 0);
|
||||
log.error("Failed to trigger external n8n webhook for meeting {}", meeting.getId(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean canExecuteTask(AiTask task) {
|
||||
return task != null
|
||||
&& !Integer.valueOf(2).equals(task.getStatus())
|
||||
|
|
@ -701,6 +727,9 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
}
|
||||
|
||||
private void updateProgress(Long meetingId, int percent, String msg, int eta) {
|
||||
if (meetingId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> progress = new HashMap<>();
|
||||
progress.put("percent", percent);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ import com.imeeting.common.MeetingConstants;
|
|||
import com.imeeting.common.RedisKeys;
|
||||
import com.imeeting.dto.biz.CreateMeetingCommand;
|
||||
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
|
||||
import com.imeeting.dto.biz.MeetingExternalWorkflowFailureDTO;
|
||||
import com.imeeting.dto.biz.MeetingSummaryFinalizeDTO;
|
||||
import com.imeeting.dto.biz.MeetingSummaryOrchestrationTriggerResultVO;
|
||||
import com.imeeting.dto.biz.MeetingTranscriptChapterImportDTO;
|
||||
import com.imeeting.dto.biz.MeetingTranscriptChapterImportResultVO;
|
||||
import com.imeeting.dto.biz.MeetingVO;
|
||||
|
|
@ -70,6 +72,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
private final RealtimeMeetingAudioStorageService realtimeMeetingAudioStorageService;
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final MeetingExternalSummaryWebhookTrigger meetingExternalSummaryWebhookTrigger;
|
||||
|
||||
@Value("${imeeting.summary-orchestration.mode:INTERNAL_BUILTIN}")
|
||||
private String summaryOrchestrationMode;
|
||||
|
|
@ -706,6 +709,98 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
updateMeetingProgress(meeting.getId(), 100, "外部总结回填完成", 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public MeetingSummaryOrchestrationTriggerResultVO triggerExternalSummaryOrchestration(Long meetingId, boolean force) {
|
||||
ensureExternalSummaryModeEnabled();
|
||||
Meeting meeting = meetingService.getById(meetingId);
|
||||
if (meeting == null) {
|
||||
throw new RuntimeException("会议不存在");
|
||||
}
|
||||
|
||||
AiTask summaryTask = aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
||||
.eq(AiTask::getMeetingId, meetingId)
|
||||
.eq(AiTask::getTaskType, "SUMMARY")
|
||||
.orderByDesc(AiTask::getId)
|
||||
.last("LIMIT 1"));
|
||||
if (summaryTask == null) {
|
||||
throw new RuntimeException("缺少可用的总结任务,无法触发外部编排");
|
||||
}
|
||||
AiTask chapterTask = aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
||||
.eq(AiTask::getMeetingId, meetingId)
|
||||
.eq(AiTask::getTaskType, "CHAPTER")
|
||||
.orderByDesc(AiTask::getId)
|
||||
.last("LIMIT 1"));
|
||||
|
||||
try {
|
||||
MeetingSummaryOrchestrationTriggerResultVO result = meetingExternalSummaryWebhookTrigger.trigger(
|
||||
meeting,
|
||||
summaryTask,
|
||||
chapterTask,
|
||||
"MANUAL_API",
|
||||
force
|
||||
);
|
||||
aiTaskService.updateById(summaryTask);
|
||||
updateMeetingProgress(meetingId, 95, result.getMessage(), 0);
|
||||
return result;
|
||||
} catch (Exception ex) {
|
||||
aiTaskService.updateById(summaryTask);
|
||||
updateMeetingProgress(meetingId, -1, "触发外部 n8n 编排失败: " + ex.getMessage(), 0);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void markExternalSummaryOrchestrationFailed(MeetingExternalWorkflowFailureDTO command) {
|
||||
ensureExternalSummaryModeEnabled();
|
||||
if (command == null || command.getMeetingId() == null) {
|
||||
throw new RuntimeException("缺少会议ID,无法回写外部编排失败");
|
||||
}
|
||||
String stage = command.getStage() == null ? "WORKFLOW" : command.getStage().trim().toUpperCase();
|
||||
String errorMessage = command.getErrorMessage() == null ? "" : command.getErrorMessage().trim();
|
||||
if (errorMessage.isEmpty()) {
|
||||
throw new RuntimeException("缺少失败错误信息,无法回写外部编排失败");
|
||||
}
|
||||
|
||||
Meeting meeting = meetingService.getById(command.getMeetingId());
|
||||
if (meeting == null) {
|
||||
throw new RuntimeException("会议不存在");
|
||||
}
|
||||
|
||||
AiTask chapterTask = resolveOwnedTask(command.getChapterTaskId(), meeting.getId(), "CHAPTER");
|
||||
AiTask summaryTask = resolveOwnedTask(command.getSummaryTaskId(), meeting.getId(), "SUMMARY");
|
||||
|
||||
if (chapterTask == null) {
|
||||
chapterTask = aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
||||
.eq(AiTask::getMeetingId, meeting.getId())
|
||||
.eq(AiTask::getTaskType, "CHAPTER")
|
||||
.orderByDesc(AiTask::getId)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
if (summaryTask == null) {
|
||||
summaryTask = aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
||||
.eq(AiTask::getMeetingId, meeting.getId())
|
||||
.eq(AiTask::getTaskType, "SUMMARY")
|
||||
.orderByDesc(AiTask::getId)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
if ("CHAPTER".equals(stage)) {
|
||||
markTaskFailed(chapterTask, "外部章节编排失败: " + errorMessage, command.getRawError());
|
||||
markTaskFailed(summaryTask, "外部章节编排失败,无法继续总结: " + errorMessage, command.getRawError());
|
||||
} else {
|
||||
markTaskFailed(summaryTask, "外部总结编排失败: " + errorMessage, command.getRawError());
|
||||
}
|
||||
|
||||
if (summaryTask != null) {
|
||||
meeting.setLatestSummaryTaskId(summaryTask.getId());
|
||||
}
|
||||
meeting.setStatus(4);
|
||||
meetingService.updateById(meeting);
|
||||
updateMeetingProgress(meeting.getId(), -1, errorMessage, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void reSummary(Long meetingId, Long summaryModelId, Long chapterModelId, Long promptId, String userPrompt) {
|
||||
|
|
@ -867,6 +962,36 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
|
||||
private AiTask resolveOwnedTask(Long taskId, Long meetingId, String expectedType) {
|
||||
if (taskId == null) {
|
||||
return null;
|
||||
}
|
||||
AiTask task = aiTaskService.getById(taskId);
|
||||
if (task == null || !Objects.equals(task.getMeetingId(), meetingId) || !expectedType.equals(task.getTaskType())) {
|
||||
throw new RuntimeException(expectedType + "任务不存在或不属于当前会议");
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
private void markTaskFailed(AiTask task, String message, String rawError) {
|
||||
if (task == null || Integer.valueOf(2).equals(task.getStatus())) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> responseData = task.getResponseData() == null
|
||||
? new HashMap<>()
|
||||
: new HashMap<>(task.getResponseData());
|
||||
Map<String, Object> failureData = new HashMap<>();
|
||||
failureData.put("message", message);
|
||||
failureData.put("rawError", rawError);
|
||||
failureData.put("failedAt", java.time.LocalDateTime.now().toString());
|
||||
responseData.put("externalWorkflowFailure", failureData);
|
||||
task.setResponseData(responseData);
|
||||
task.setStatus(3);
|
||||
task.setErrorMsg(message);
|
||||
task.setCompletedAt(java.time.LocalDateTime.now());
|
||||
aiTaskService.updateById(task);
|
||||
}
|
||||
|
||||
private void dispatchSummaryTaskAfterCommit(Long meetingId, Long tenantId, Long userId) {
|
||||
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
aiTaskService.dispatchSummaryTask(meetingId, tenantId, userId);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,357 @@
|
|||
package com.imeeting.service.biz.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.imeeting.dto.biz.AiModelVO;
|
||||
import com.imeeting.dto.biz.MeetingSummaryOrchestrationTriggerResultVO;
|
||||
import com.imeeting.entity.biz.AiTask;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.service.biz.AiModelService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MeetingExternalSummaryWebhookTrigger {
|
||||
|
||||
private static final String TRIGGER_STATUS_TRIGGERED = "TRIGGERED";
|
||||
private static final String TRIGGER_STATUS_SKIPPED = "SKIPPED";
|
||||
private static final String TRIGGER_STATUS_FAILED = "FAILED";
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final AiModelService aiModelService;
|
||||
|
||||
@Value("${unisbase.app.server-base-url:}")
|
||||
private String serverBaseUrl;
|
||||
|
||||
@Value("${unisbase.internal-auth.header-name:X-Internal-Secret}")
|
||||
private String internalAuthHeaderName;
|
||||
|
||||
@Value("${imeeting.summary-orchestration.external-n8n.webhook-url:}")
|
||||
private String webhookUrl;
|
||||
|
||||
@Value("${imeeting.summary-orchestration.external-n8n.auth-header-name:}")
|
||||
private String webhookAuthHeaderName;
|
||||
|
||||
@Value("${imeeting.summary-orchestration.external-n8n.auth-header-value:}")
|
||||
private String webhookAuthHeaderValue;
|
||||
|
||||
@Value("${imeeting.summary-orchestration.external-n8n.connect-timeout-seconds:10}")
|
||||
private Integer connectTimeoutSeconds;
|
||||
|
||||
@Value("${imeeting.summary-orchestration.external-n8n.read-timeout-seconds:30}")
|
||||
private Integer readTimeoutSeconds;
|
||||
|
||||
public MeetingSummaryOrchestrationTriggerResultVO trigger(Meeting meeting,
|
||||
AiTask summaryTask,
|
||||
AiTask chapterTask,
|
||||
String triggerSource,
|
||||
boolean force) {
|
||||
if (meeting == null || meeting.getId() == null) {
|
||||
throw new RuntimeException("缺少会议上下文,无法触发外部总结编排");
|
||||
}
|
||||
if (summaryTask == null || summaryTask.getId() == null || !"SUMMARY".equals(summaryTask.getTaskType())) {
|
||||
throw new RuntimeException("缺少可用的总结任务,无法触发外部总结编排");
|
||||
}
|
||||
if (webhookUrl == null || webhookUrl.isBlank()) {
|
||||
markTriggerFailed(summaryTask, triggerSource, null, "未配置 n8n webhook-url");
|
||||
throw new RuntimeException("未配置 n8n webhook-url,无法触发外部总结编排");
|
||||
}
|
||||
|
||||
if (!force && wasTriggered(summaryTask)) {
|
||||
return buildResult(
|
||||
meeting.getId(),
|
||||
summaryTask.getId(),
|
||||
triggerSource,
|
||||
TRIGGER_STATUS_SKIPPED,
|
||||
false,
|
||||
true,
|
||||
extractPreviousHttpStatus(summaryTask),
|
||||
"当前总结任务已触发过 n8n webhook,已跳过重复触发"
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, Object> payload;
|
||||
try {
|
||||
payload = buildPayload(meeting, summaryTask, chapterTask, triggerSource, force);
|
||||
} catch (Exception ex) {
|
||||
markTriggerFailed(summaryTask, triggerSource, null, ex.getMessage());
|
||||
throw ex;
|
||||
}
|
||||
Map<String, Object> requestData = copyMap(summaryTask.getRequestData());
|
||||
requestData.put("externalOrchestrationTriggerPayload", payload);
|
||||
summaryTask.setRequestData(requestData);
|
||||
|
||||
HttpResponse<String> response;
|
||||
try {
|
||||
HttpClient client = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(safeSeconds(connectTimeoutSeconds, 10)))
|
||||
.build();
|
||||
HttpRequest.Builder builder = HttpRequest.newBuilder()
|
||||
.uri(URI.create(webhookUrl.trim()))
|
||||
.timeout(Duration.ofSeconds(safeSeconds(readTimeoutSeconds, 30)))
|
||||
.header("Content-Type", "application/json; charset=UTF-8")
|
||||
.header("Accept", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(payload), StandardCharsets.UTF_8));
|
||||
if (webhookAuthHeaderName != null && !webhookAuthHeaderName.isBlank()
|
||||
&& webhookAuthHeaderValue != null && !webhookAuthHeaderValue.isBlank()) {
|
||||
builder.header(webhookAuthHeaderName.trim(), webhookAuthHeaderValue);
|
||||
}
|
||||
response = client.send(builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
} catch (Exception ex) {
|
||||
markTriggerFailed(summaryTask, triggerSource, null, "触发 n8n webhook 异常: " + ex.getMessage());
|
||||
throw new RuntimeException("触发 n8n webhook 异常: " + ex.getMessage(), ex);
|
||||
}
|
||||
|
||||
int httpStatus = response.statusCode();
|
||||
if (httpStatus >= 200 && httpStatus < 300) {
|
||||
markTriggered(summaryTask, triggerSource, force, httpStatus, response.body());
|
||||
return buildResult(
|
||||
meeting.getId(),
|
||||
summaryTask.getId(),
|
||||
triggerSource,
|
||||
TRIGGER_STATUS_TRIGGERED,
|
||||
true,
|
||||
false,
|
||||
httpStatus,
|
||||
"已触发 n8n webhook"
|
||||
);
|
||||
}
|
||||
|
||||
markTriggerFailed(summaryTask, triggerSource, httpStatus, "触发 n8n webhook 失败: HTTP " + httpStatus);
|
||||
throw new RuntimeException("触发 n8n webhook 失败: HTTP " + httpStatus + ", body=" + clip(response.body(), 500));
|
||||
}
|
||||
|
||||
private Map<String, Object> buildPayload(Meeting meeting,
|
||||
AiTask summaryTask,
|
||||
AiTask chapterTask,
|
||||
String triggerSource,
|
||||
boolean force) {
|
||||
String normalizedBaseUrl = normalizeBaseUrl(serverBaseUrl);
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("meetingId", meeting.getId());
|
||||
payload.put("meetingTitle", meeting.getTitle());
|
||||
payload.put("meetingType", meeting.getMeetingType());
|
||||
payload.put("meetingStatus", meeting.getStatus());
|
||||
payload.put("tenantId", meeting.getTenantId());
|
||||
payload.put("creatorId", meeting.getCreatorId());
|
||||
payload.put("summaryTaskId", summaryTask.getId());
|
||||
payload.put("summaryTaskStatus", summaryTask.getStatus());
|
||||
payload.put("chapterTaskId", chapterTask == null ? null : chapterTask.getId());
|
||||
payload.put("chapterTaskStatus", chapterTask == null ? null : chapterTask.getStatus());
|
||||
payload.put("triggerSource", triggerSource);
|
||||
payload.put("force", force);
|
||||
payload.put("triggeredAt", LocalDateTime.now().toString());
|
||||
payload.put("summaryOrchestrationMode", "EXTERNAL_N8N");
|
||||
payload.put("summaryTaskConfig", copyMap(summaryTask.getTaskConfig()));
|
||||
payload.put("modelConfig", buildModelConfig(summaryTask));
|
||||
|
||||
Map<String, Object> internalApi = new LinkedHashMap<>();
|
||||
internalApi.put("baseUrl", normalizedBaseUrl);
|
||||
internalApi.put("internalAuthHeaderName", internalAuthHeaderName);
|
||||
internalApi.put("transcriptSourceUrl", normalizedBaseUrl + "/sys/internal/meetings/" + meeting.getId() + "/transcript-source");
|
||||
internalApi.put("chaptersImportUrl", normalizedBaseUrl + "/sys/internal/meetings/" + meeting.getId() + "/chapters/import");
|
||||
internalApi.put("summaryPromptContextUrl", normalizedBaseUrl + "/sys/internal/meetings/" + meeting.getId() + "/summary-prompt-context");
|
||||
internalApi.put("summaryFinalizeUrl", normalizedBaseUrl + "/sys/internal/meetings/" + meeting.getId() + "/summary/finalize");
|
||||
internalApi.put("summaryFailUrl", normalizedBaseUrl + "/sys/internal/meetings/" + meeting.getId() + "/summary/fail");
|
||||
payload.put("internalApi", internalApi);
|
||||
return payload;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildModelConfig(AiTask summaryTask) {
|
||||
Map<String, Object> config = new LinkedHashMap<>();
|
||||
config.put("chapter", resolveLlmExecutionConfig(longValue(summaryTask, "chapterModelId")));
|
||||
config.put("summary", resolveLlmExecutionConfig(longValue(summaryTask, "summaryModelId")));
|
||||
return config;
|
||||
}
|
||||
|
||||
private Map<String, Object> resolveLlmExecutionConfig(Long modelId) {
|
||||
if (modelId == null) {
|
||||
return null;
|
||||
}
|
||||
AiModelVO model = aiModelService.getModelById(modelId, "LLM");
|
||||
if (model == null) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Object> config = new LinkedHashMap<>();
|
||||
config.put("id", model.getId());
|
||||
config.put("provider", model.getProvider());
|
||||
config.put("baseUrl", model.getBaseUrl());
|
||||
config.put("apiPath", model.getApiPath());
|
||||
config.put("resolvedUrl", appendPath(model.getBaseUrl(),
|
||||
model.getApiPath() == null || model.getApiPath().isBlank()
|
||||
? "v1/chat/completions"
|
||||
: model.getApiPath()));
|
||||
config.put("apiKey", model.getApiKey());
|
||||
config.put("authHeaderName", "Authorization");
|
||||
config.put("authHeaderValue", buildBearer(model.getApiKey()));
|
||||
config.put("modelCode", model.getModelCode());
|
||||
config.put("temperature", model.getTemperature());
|
||||
config.put("topP", model.getTopP());
|
||||
return config;
|
||||
}
|
||||
|
||||
private void markTriggered(AiTask summaryTask,
|
||||
String triggerSource,
|
||||
boolean force,
|
||||
Integer httpStatus,
|
||||
String responseBody) {
|
||||
Map<String, Object> responseData = copyMap(summaryTask.getResponseData());
|
||||
Map<String, Object> triggerState = new LinkedHashMap<>();
|
||||
triggerState.put("status", TRIGGER_STATUS_TRIGGERED);
|
||||
triggerState.put("triggerSource", triggerSource);
|
||||
triggerState.put("triggeredAt", LocalDateTime.now().toString());
|
||||
triggerState.put("force", force);
|
||||
triggerState.put("httpStatus", httpStatus);
|
||||
triggerState.put("responsePreview", clip(responseBody, 1000));
|
||||
responseData.put("externalOrchestrationTrigger", triggerState);
|
||||
summaryTask.setResponseData(responseData);
|
||||
summaryTask.setErrorMsg(null);
|
||||
}
|
||||
|
||||
private void markTriggerFailed(AiTask summaryTask,
|
||||
String triggerSource,
|
||||
Integer httpStatus,
|
||||
String message) {
|
||||
if (summaryTask == null) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> responseData = copyMap(summaryTask.getResponseData());
|
||||
Map<String, Object> triggerState = new LinkedHashMap<>();
|
||||
triggerState.put("status", TRIGGER_STATUS_FAILED);
|
||||
triggerState.put("triggerSource", triggerSource);
|
||||
triggerState.put("triggeredAt", LocalDateTime.now().toString());
|
||||
triggerState.put("httpStatus", httpStatus);
|
||||
triggerState.put("message", message);
|
||||
responseData.put("externalOrchestrationTrigger", triggerState);
|
||||
summaryTask.setResponseData(responseData);
|
||||
summaryTask.setErrorMsg(message);
|
||||
}
|
||||
|
||||
private boolean wasTriggered(AiTask summaryTask) {
|
||||
if (summaryTask == null || summaryTask.getResponseData() == null) {
|
||||
return false;
|
||||
}
|
||||
Object triggerState = summaryTask.getResponseData().get("externalOrchestrationTrigger");
|
||||
if (!(triggerState instanceof Map<?, ?> map)) {
|
||||
return false;
|
||||
}
|
||||
Object status = map.get("status");
|
||||
return TRIGGER_STATUS_TRIGGERED.equals(String.valueOf(status));
|
||||
}
|
||||
|
||||
private Integer extractPreviousHttpStatus(AiTask summaryTask) {
|
||||
if (summaryTask == null || summaryTask.getResponseData() == null) {
|
||||
return null;
|
||||
}
|
||||
Object triggerState = summaryTask.getResponseData().get("externalOrchestrationTrigger");
|
||||
if (!(triggerState instanceof Map<?, ?> map)) {
|
||||
return null;
|
||||
}
|
||||
Object status = map.get("httpStatus");
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(String.valueOf(status));
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private MeetingSummaryOrchestrationTriggerResultVO buildResult(Long meetingId,
|
||||
Long summaryTaskId,
|
||||
String triggerSource,
|
||||
String status,
|
||||
boolean triggered,
|
||||
boolean skipped,
|
||||
Integer httpStatus,
|
||||
String message) {
|
||||
MeetingSummaryOrchestrationTriggerResultVO result = new MeetingSummaryOrchestrationTriggerResultVO();
|
||||
result.setMeetingId(meetingId);
|
||||
result.setSummaryTaskId(summaryTaskId);
|
||||
result.setTriggerSource(triggerSource);
|
||||
result.setStatus(status);
|
||||
result.setTriggered(triggered);
|
||||
result.setSkipped(skipped);
|
||||
result.setHttpStatus(httpStatus);
|
||||
result.setMessage(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, Object> copyMap(Map<String, Object> source) {
|
||||
return source == null ? new LinkedHashMap<>() : new LinkedHashMap<>(source);
|
||||
}
|
||||
|
||||
private String normalizeBaseUrl(String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
throw new RuntimeException("未配置 unisbase.app.server-base-url,无法生成 n8n 回调地址");
|
||||
}
|
||||
String normalized = raw.trim();
|
||||
while (normalized.endsWith("/")) {
|
||||
normalized = normalized.substring(0, normalized.length() - 1);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private Long longValue(AiTask summaryTask, String key) {
|
||||
if (summaryTask == null || summaryTask.getTaskConfig() == null || key == null) {
|
||||
return null;
|
||||
}
|
||||
Object value = summaryTask.getTaskConfig().get(key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(String.valueOf(value).trim());
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String appendPath(String baseUrl, String path) {
|
||||
String normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
||||
String normalizedPath = path == null ? "" : path.trim();
|
||||
while (normalizedPath.startsWith("/")) {
|
||||
normalizedPath = normalizedPath.substring(1);
|
||||
}
|
||||
if (normalizedPath.isEmpty()) {
|
||||
return normalizedBaseUrl;
|
||||
}
|
||||
return normalizedBaseUrl + "/" + normalizedPath;
|
||||
}
|
||||
|
||||
private String buildBearer(String apiKey) {
|
||||
if (apiKey == null || apiKey.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
return apiKey.startsWith("Bearer ") ? apiKey : "Bearer " + apiKey;
|
||||
}
|
||||
|
||||
private int safeSeconds(Integer value, int fallback) {
|
||||
return value == null || value <= 0 ? fallback : value;
|
||||
}
|
||||
|
||||
private String clip(String value, int maxLength) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String normalized = value.trim();
|
||||
if (normalized.length() <= maxLength) {
|
||||
return normalized;
|
||||
}
|
||||
return normalized.substring(0, maxLength);
|
||||
}
|
||||
}
|
||||
|
|
@ -87,6 +87,12 @@ unisbase:
|
|||
imeeting:
|
||||
summary-orchestration:
|
||||
mode: INTERNAL_BUILTIN
|
||||
external-n8n:
|
||||
webhook-url: ${IMEETING_EXTERNAL_N8N_WEBHOOK_URL:https://n8n.oa.unissense.tech/webhook/imeeting-summary-external-template}
|
||||
auth-header-name: ${IMEETING_EXTERNAL_N8N_AUTH_HEADER_NAME:test}
|
||||
auth-header-value: ${IMEETING_EXTERNAL_N8N_AUTH_HEADER_VALUE:123456}
|
||||
connect-timeout-seconds: ${IMEETING_EXTERNAL_N8N_CONNECT_TIMEOUT_SECONDS:10}
|
||||
read-timeout-seconds: ${IMEETING_EXTERNAL_N8N_READ_TIMEOUT_SECONDS:1200}
|
||||
realtime:
|
||||
resume-window-minutes: 30
|
||||
empty-session-retention-minutes: 720
|
||||
|
|
|
|||
|
|
@ -296,7 +296,8 @@ class AiTaskServiceImplTest {
|
|||
revisionService,
|
||||
chapterService,
|
||||
mock(MeetingSummaryPromptAssembler.class),
|
||||
taskSecurityContextRunner
|
||||
taskSecurityContextRunner,
|
||||
mock(MeetingExternalSummaryWebhookTrigger.class)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue