feat: 优化ASR任务接管逻辑和前端多语言支持
- 在后端实现ASR任务接管逻辑,支持恢复未完成的任务 - 添加前端多语言支持,引入 `react-i18next` 库 -dev_na
parent
11ab76f2ed
commit
0ccf0aa87d
|
|
@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
|
|
@ -60,30 +61,27 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
@Override
|
@Override
|
||||||
@Async
|
@Async
|
||||||
public void dispatchTasks(Long meetingId) {
|
public void dispatchTasks(Long meetingId) {
|
||||||
// 尝试获取轮询锁,防止并发执行
|
|
||||||
String lockKey = RedisKeys.meetingPollingLockKey(meetingId);
|
String lockKey = RedisKeys.meetingPollingLockKey(meetingId);
|
||||||
Boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 30, TimeUnit.MINUTES);
|
Boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 30, TimeUnit.MINUTES);
|
||||||
if (Boolean.FALSE.equals(acquired)) {
|
if (Boolean.FALSE.equals(acquired)) {
|
||||||
log.warn("Meeting {} task is already being processed by another thread", meetingId);
|
log.warn("Meeting {} is already being processed", meetingId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log.info("Starting real AI processing for meeting ID: {}", meetingId);
|
|
||||||
Meeting meeting = meetingMapper.selectById(meetingId);
|
Meeting meeting = meetingMapper.selectById(meetingId);
|
||||||
if (meeting == null) return;
|
if (meeting == null) return;
|
||||||
|
|
||||||
// 1. 执行 ASR 识别
|
// 1. 执行 ASR 识别 (含接管逻辑)
|
||||||
String asrText = processAsrTask(meeting);
|
String asrText = processAsrTask(meeting);
|
||||||
|
|
||||||
// 2. 执行 LLM 总结
|
// 2. 执行 LLM 总结
|
||||||
processSummaryTask(meeting, asrText);
|
processSummaryTask(meeting, asrText);
|
||||||
|
|
||||||
// 完成后清除进度
|
|
||||||
redisTemplate.delete(RedisKeys.meetingProgressKey(meetingId));
|
redisTemplate.delete(RedisKeys.meetingProgressKey(meetingId));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Meeting {} AI Task Flow failed", meetingId, e);
|
log.error("Meeting {} AI Task Flow failed", meetingId, e);
|
||||||
updateMeetingStatus(meetingId, 4); // Overall Failed
|
updateMeetingStatus(meetingId, 4);
|
||||||
updateProgress(meetingId, -1, "分析失败: " + e.getMessage(), 0);
|
updateProgress(meetingId, -1, "分析失败: " + e.getMessage(), 0);
|
||||||
} finally {
|
} finally {
|
||||||
redisTemplate.delete(lockKey);
|
redisTemplate.delete(lockKey);
|
||||||
|
|
@ -95,13 +93,15 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
public void dispatchSummaryTask(Long meetingId) {
|
public void dispatchSummaryTask(Long meetingId) {
|
||||||
Meeting meeting = meetingMapper.selectById(meetingId);
|
Meeting meeting = meetingMapper.selectById(meetingId);
|
||||||
if (meeting == null) return;
|
if (meeting == null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取已有转录全文
|
|
||||||
List<MeetingTranscript> transcripts = transcriptMapper.selectList(new LambdaQueryWrapper<MeetingTranscript>()
|
List<MeetingTranscript> transcripts = transcriptMapper.selectList(new LambdaQueryWrapper<MeetingTranscript>()
|
||||||
.eq(MeetingTranscript::getMeetingId, meetingId)
|
.eq(MeetingTranscript::getMeetingId, meetingId)
|
||||||
.orderByAsc(MeetingTranscript::getStartTime));
|
.orderByAsc(MeetingTranscript::getStartTime));
|
||||||
|
|
||||||
|
if (transcripts.isEmpty()) {
|
||||||
|
throw new RuntimeException("没有找到可用的转录文本,无法生成总结");
|
||||||
|
}
|
||||||
|
|
||||||
String asrText = transcripts.stream()
|
String asrText = transcripts.stream()
|
||||||
.map(t -> (t.getSpeakerName() != null ? t.getSpeakerName() : t.getSpeakerId()) + ": " + t.getContent())
|
.map(t -> (t.getSpeakerName() != null ? t.getSpeakerName() : t.getSpeakerId()) + ": " + t.getContent())
|
||||||
.collect(Collectors.joining("\n"));
|
.collect(Collectors.joining("\n"));
|
||||||
|
|
@ -110,64 +110,73 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Re-summary failed for meeting {}", meetingId, e);
|
log.error("Re-summary failed for meeting {}", meetingId, e);
|
||||||
updateMeetingStatus(meetingId, 4);
|
updateMeetingStatus(meetingId, 4);
|
||||||
|
updateProgress(meetingId, -1, "总结失败: " + e.getMessage(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String processAsrTask(Meeting meeting) throws Exception {
|
private String processAsrTask(Meeting meeting) throws Exception {
|
||||||
updateMeetingStatus(meeting.getId(), 1); // 识别中
|
updateMeetingStatus(meeting.getId(), 1);
|
||||||
updateProgress(meeting.getId(), 5, "已提交识别请求...", 0);
|
|
||||||
|
|
||||||
AiModel asrModel = aiModelService.getById(meeting.getAsrModelId());
|
AiModel asrModel = aiModelService.getById(meeting.getAsrModelId());
|
||||||
if (asrModel == null) throw new RuntimeException("ASR Model config not found");
|
if (asrModel == null) throw new RuntimeException("ASR模型配置不存在");
|
||||||
|
|
||||||
// 构建请求参数并转码
|
String submitUrl = asrModel.getBaseUrl().endsWith("/") ? asrModel.getBaseUrl() + "api/tasks/recognition" : asrModel.getBaseUrl() + "/api/tasks/recognition";
|
||||||
Map<String, Object> req = new HashMap<>();
|
String taskId = null;
|
||||||
String rawAudioUrl = meeting.getAudioUrl();
|
AiTask taskRecord = null;
|
||||||
String encodedAudioUrl = Arrays.stream(rawAudioUrl.split("/"))
|
|
||||||
.map(part -> {
|
|
||||||
try {
|
|
||||||
return URLEncoder.encode(part, StandardCharsets.UTF_8).replace("+", "%20");
|
|
||||||
} catch (Exception e) { return part; }
|
|
||||||
})
|
|
||||||
.collect(Collectors.joining("/"));
|
|
||||||
String fullAudioUrl = serverBaseUrl + (encodedAudioUrl.startsWith("/") ? "" : "/") + encodedAudioUrl;
|
|
||||||
|
|
||||||
req.put("file_path", fullAudioUrl);
|
// --- 核心:接管逻辑 ---
|
||||||
req.put("use_spk_id", meeting.getUseSpkId() != null && meeting.getUseSpkId() == 1);
|
List<AiTask> existingTasks = this.list(new LambdaQueryWrapper<AiTask>()
|
||||||
|
.eq(AiTask::getMeetingId, meeting.getId())
|
||||||
|
.eq(AiTask::getTaskType, "ASR")
|
||||||
|
.orderByDesc(AiTask::getStartedAt));
|
||||||
|
|
||||||
// 处理热词权重
|
if (!existingTasks.isEmpty()) {
|
||||||
List<Map<String, Object>> formattedHotwords = new ArrayList<>();
|
AiTask lastTask = existingTasks.get(0);
|
||||||
if (meeting.getHotWords() != null && !meeting.getHotWords().isEmpty()) {
|
if (lastTask.getResponseData() != null && lastTask.getResponseData().get("task_id") != null) {
|
||||||
List<HotWord> entities = hotWordService.list(new LambdaQueryWrapper<HotWord>()
|
String potentialTaskId = lastTask.getResponseData().get("task_id").toString();
|
||||||
.eq(HotWord::getTenantId, meeting.getTenantId())
|
log.info("Attempting to resume ASR task {} for meeting {}", potentialTaskId, meeting.getId());
|
||||||
.in(HotWord::getWord, meeting.getHotWords()));
|
|
||||||
Map<String, Integer> weightMap = entities.stream().collect(Collectors.toMap(HotWord::getWord, HotWord::getWeight, (v1, v2) -> v1));
|
// 验证旧 taskId 是否依然有效
|
||||||
for (String w : meeting.getHotWords()) {
|
String checkUrl = asrModel.getBaseUrl().endsWith("/") ? asrModel.getBaseUrl() + "api/tasks/" + potentialTaskId : asrModel.getBaseUrl() + "/api/tasks/" + potentialTaskId;
|
||||||
formattedHotwords.add(Map.of("hotword", w, "weight", weightMap.getOrDefault(w, 10) / 10.0));
|
try {
|
||||||
|
String checkResp = get(checkUrl);
|
||||||
|
JsonNode node = objectMapper.readTree(checkResp);
|
||||||
|
if (node.path("code").asInt() == 200) {
|
||||||
|
taskId = potentialTaskId;
|
||||||
|
taskRecord = lastTask;
|
||||||
|
log.info("Successfully resumed ASR task {}", taskId);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Old task ID {} is invalid, will start a new one", potentialTaskId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
req.put("hotwords", formattedHotwords);
|
|
||||||
|
|
||||||
AiTask taskRecord = createAiTask(meeting.getId(), "ASR", req);
|
// 如果没有可接管的任务,则发起新任务
|
||||||
|
if (taskId == null) {
|
||||||
|
updateProgress(meeting.getId(), 5, "正在提交识别请求...", 0);
|
||||||
|
Map<String, Object> req = buildAsrRequest(meeting);
|
||||||
|
taskRecord = createAiTask(meeting.getId(), "ASR", req);
|
||||||
|
|
||||||
// 提交
|
String respBody = postJson(submitUrl, req);
|
||||||
String submitUrl = asrModel.getBaseUrl().endsWith("/") ? asrModel.getBaseUrl() + "api/tasks/recognition" : asrModel.getBaseUrl() + "/api/tasks/recognition";
|
JsonNode submitNode = objectMapper.readTree(respBody);
|
||||||
String respBody = postJson(submitUrl, req);
|
if (submitNode.path("code").asInt() != 200) {
|
||||||
JsonNode submitNode = objectMapper.readTree(respBody);
|
updateAiTaskFail(taskRecord, "Submission Failed: " + respBody);
|
||||||
if (submitNode.path("code").asInt() != 200) {
|
throw new RuntimeException("ASR引擎拒绝请求: " + submitNode.path("msg").asText());
|
||||||
updateAiTaskFail(taskRecord, "Submission Failed: " + respBody);
|
}
|
||||||
throw new RuntimeException("ASR引擎拒绝请求: " + submitNode.path("msg").asText());
|
taskId = submitNode.path("data").path("task_id").asText();
|
||||||
|
taskRecord.setResponseData(Map.of("task_id", taskId));
|
||||||
|
this.updateById(taskRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
String taskId = submitNode.path("data").path("task_id").asText();
|
|
||||||
String queryUrl = asrModel.getBaseUrl().endsWith("/") ? asrModel.getBaseUrl() + "api/tasks/" + taskId : asrModel.getBaseUrl() + "/api/tasks/" + taskId;
|
String queryUrl = asrModel.getBaseUrl().endsWith("/") ? asrModel.getBaseUrl() + "api/tasks/" + taskId : asrModel.getBaseUrl() + "/api/tasks/" + taskId;
|
||||||
|
|
||||||
// 轮询带防护
|
// 轮询逻辑 (带防卡死防护)
|
||||||
JsonNode resultNode = null;
|
JsonNode resultNode = null;
|
||||||
int lastPercent = -1;
|
int lastPercent = -1;
|
||||||
int unchangedCount = 0;
|
int unchangedCount = 0;
|
||||||
|
|
||||||
for (int i = 0; i < 600; i++) { // Max 20 minutes
|
for (int i = 0; i < 600; i++) {
|
||||||
Thread.sleep(2000);
|
Thread.sleep(2000);
|
||||||
String queryResp = get(queryUrl);
|
String queryResp = get(queryUrl);
|
||||||
JsonNode statusNode = objectMapper.readTree(queryResp);
|
JsonNode statusNode = objectMapper.readTree(queryResp);
|
||||||
|
|
@ -177,25 +186,17 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
if ("completed".equalsIgnoreCase(status)) {
|
if ("completed".equalsIgnoreCase(status)) {
|
||||||
resultNode = data.path("result");
|
resultNode = data.path("result");
|
||||||
updateAiTaskSuccess(taskRecord, statusNode);
|
updateAiTaskSuccess(taskRecord, statusNode);
|
||||||
updateProgress(meeting.getId(), 85, "语音转录完成,准备进行总结...", 0);
|
|
||||||
break;
|
break;
|
||||||
} else if ("failed".equalsIgnoreCase(status)) {
|
} else if ("failed".equalsIgnoreCase(status)) {
|
||||||
updateAiTaskFail(taskRecord, "ASR reported failure: " + queryResp);
|
updateAiTaskFail(taskRecord, "ASR engine reported failure: " + queryResp);
|
||||||
throw new RuntimeException("ASR处理失败: " + data.path("message").asText());
|
throw new RuntimeException("ASR引擎处理失败: " + data.path("message").asText());
|
||||||
} else {
|
} else {
|
||||||
// 处理中:同步进度到 Redis
|
|
||||||
int currentPercent = data.path("percentage").asInt();
|
int currentPercent = data.path("percentage").asInt();
|
||||||
String message = data.path("message").asText();
|
|
||||||
int eta = data.path("eta").asInt(0);
|
int eta = data.path("eta").asInt(0);
|
||||||
|
updateProgress(meeting.getId(), (int)(currentPercent * 0.85), data.path("message").asText(), eta);
|
||||||
|
|
||||||
// 缩放到 0-85% 范围
|
|
||||||
int scaledPercent = (int)(currentPercent * 0.85);
|
|
||||||
updateProgress(meeting.getId(), Math.max(5, scaledPercent), message, eta);
|
|
||||||
|
|
||||||
// 防死循环逻辑:如果进度长时间不动且不是 0
|
|
||||||
if (currentPercent > 0 && currentPercent == lastPercent) {
|
if (currentPercent > 0 && currentPercent == lastPercent) {
|
||||||
unchangedCount++;
|
if (++unchangedCount > 45) throw new RuntimeException("识别任务长时间无进度增长,自动强制超时");
|
||||||
if (unchangedCount > 40) throw new RuntimeException("ASR处理停滞,自动超时");
|
|
||||||
} else {
|
} else {
|
||||||
unchangedCount = 0;
|
unchangedCount = 0;
|
||||||
}
|
}
|
||||||
|
|
@ -205,7 +206,40 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
|
|
||||||
if (resultNode == null) throw new RuntimeException("ASR轮询超时");
|
if (resultNode == null) throw new RuntimeException("ASR轮询超时");
|
||||||
|
|
||||||
// 解析并入库 (适配新 Speaker 格式)
|
// 解析并入库 (防御性清理旧数据)
|
||||||
|
return saveTranscripts(meeting, resultNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> buildAsrRequest(Meeting meeting) {
|
||||||
|
Map<String, Object> req = new HashMap<>();
|
||||||
|
String rawAudioUrl = meeting.getAudioUrl();
|
||||||
|
String encodedAudioUrl = Arrays.stream(rawAudioUrl.split("/"))
|
||||||
|
.map(part -> {
|
||||||
|
try { return URLEncoder.encode(part, StandardCharsets.UTF_8).replace("+", "%20"); }
|
||||||
|
catch (Exception e) { return part; }
|
||||||
|
})
|
||||||
|
.collect(Collectors.joining("/"));
|
||||||
|
req.put("file_path", serverBaseUrl + (encodedAudioUrl.startsWith("/") ? "" : "/") + encodedAudioUrl);
|
||||||
|
req.put("use_spk_id", meeting.getUseSpkId() != null && meeting.getUseSpkId() == 1);
|
||||||
|
|
||||||
|
List<Map<String, Object>> hotwords = new ArrayList<>();
|
||||||
|
if (meeting.getHotWords() != null && !meeting.getHotWords().isEmpty()) {
|
||||||
|
List<HotWord> entities = hotWordService.list(new LambdaQueryWrapper<HotWord>()
|
||||||
|
.eq(HotWord::getTenantId, meeting.getTenantId()).in(HotWord::getWord, meeting.getHotWords()));
|
||||||
|
Map<String, Integer> weightMap = entities.stream().collect(Collectors.toMap(HotWord::getWord, HotWord::getWeight, (v1, v2) -> v1));
|
||||||
|
for (String w : meeting.getHotWords()) {
|
||||||
|
hotwords.add(Map.of("hotword", w, "weight", weightMap.getOrDefault(w, 10) / 10.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.put("hotwords", hotwords);
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
protected String saveTranscripts(Meeting meeting, JsonNode resultNode) {
|
||||||
|
// 关键:入库前清理旧记录,防止恢复任务导致数据重复
|
||||||
|
transcriptMapper.delete(new LambdaQueryWrapper<MeetingTranscript>().eq(MeetingTranscript::getMeetingId, meeting.getId()));
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
JsonNode segments = resultNode.path("segments");
|
JsonNode segments = resultNode.path("segments");
|
||||||
if (segments.isArray()) {
|
if (segments.isArray()) {
|
||||||
|
|
@ -214,12 +248,10 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
MeetingTranscript mt = new MeetingTranscript();
|
MeetingTranscript mt = new MeetingTranscript();
|
||||||
mt.setMeetingId(meeting.getId());
|
mt.setMeetingId(meeting.getId());
|
||||||
|
|
||||||
// 解析 Speaker 对象
|
|
||||||
JsonNode spkNode = seg.path("speaker");
|
JsonNode spkNode = seg.path("speaker");
|
||||||
String spkId = spkNode.path("user_id").asText("spk_0");
|
String spkId = spkNode.path("user_id").asText("spk_0");
|
||||||
String spkName = spkNode.path("name").asText(spkId);
|
String spkName = spkNode.path("name").asText(spkId);
|
||||||
|
|
||||||
// 用户名称转换逻辑
|
|
||||||
if (spkId.matches("\\d+")) {
|
if (spkId.matches("\\d+")) {
|
||||||
SysUser user = sysUserMapper.selectById(Long.parseLong(spkId));
|
SysUser user = sysUserMapper.selectById(Long.parseLong(spkId));
|
||||||
if (user != null) spkName = user.getDisplayName() != null ? user.getDisplayName() : user.getUsername();
|
if (user != null) spkName = user.getDisplayName() != null ? user.getDisplayName() : user.getUsername();
|
||||||
|
|
@ -241,8 +273,8 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processSummaryTask(Meeting meeting, String asrText) throws Exception {
|
private void processSummaryTask(Meeting meeting, String asrText) throws Exception {
|
||||||
updateMeetingStatus(meeting.getId(), 2); // 总结中
|
updateMeetingStatus(meeting.getId(), 2);
|
||||||
updateProgress(meeting.getId(), 90, "正在进行 AI 智能总结...", 0);
|
updateProgress(meeting.getId(), 90, "正在生成智能总结纪要...", 0);
|
||||||
|
|
||||||
AiModel llmModel = aiModelService.getById(meeting.getSummaryModelId());
|
AiModel llmModel = aiModelService.getById(meeting.getSummaryModelId());
|
||||||
if (llmModel == null) return;
|
if (llmModel == null) return;
|
||||||
|
|
@ -252,7 +284,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", meeting.getPromptContent()),
|
Map.of("role", "system", "content", meeting.getPromptContent()),
|
||||||
Map.of("role", "user", "content", "请总结以下内容:\n" + asrText)
|
Map.of("role", "user", "content", "请总结以下会议内容:\n" + asrText)
|
||||||
));
|
));
|
||||||
|
|
||||||
AiTask taskRecord = createAiTask(meeting.getId(), "SUMMARY", req);
|
AiTask taskRecord = createAiTask(meeting.getId(), "SUMMARY", req);
|
||||||
|
|
@ -271,13 +303,13 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
if (response.statusCode() == 200 && respNode.has("choices")) {
|
if (response.statusCode() == 200 && respNode.has("choices")) {
|
||||||
String content = respNode.path("choices").path(0).path("message").path("content").asText();
|
String content = respNode.path("choices").path(0).path("message").path("content").asText();
|
||||||
meeting.setSummaryContent(content);
|
meeting.setSummaryContent(content);
|
||||||
meeting.setStatus(3); // Finished
|
meeting.setStatus(3);
|
||||||
meetingMapper.updateById(meeting);
|
meetingMapper.updateById(meeting);
|
||||||
updateAiTaskSuccess(taskRecord, respNode);
|
updateAiTaskSuccess(taskRecord, respNode);
|
||||||
updateProgress(meeting.getId(), 100, "分析已完成", 0);
|
updateProgress(meeting.getId(), 100, "全流程分析完成", 0);
|
||||||
} else {
|
} else {
|
||||||
updateAiTaskFail(taskRecord, "LLM failed: " + response.body());
|
updateAiTaskFail(taskRecord, "LLM Summary failed: " + response.body());
|
||||||
throw new RuntimeException("AI总结生成失败");
|
throw new RuntimeException("AI总结生成异常");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,53 +323,38 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
redisTemplate.opsForValue().set(RedisKeys.meetingProgressKey(meetingId),
|
redisTemplate.opsForValue().set(RedisKeys.meetingProgressKey(meetingId),
|
||||||
objectMapper.writeValueAsString(progress), 1, TimeUnit.HOURS);
|
objectMapper.writeValueAsString(progress), 1, TimeUnit.HOURS);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Update progress to redis failed", e);
|
log.error("Redis progress update error", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String postJson(String url, Object body) throws Exception {
|
private String postJson(String url, Object body) throws Exception {
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
return httpClient.send(HttpRequest.newBuilder().uri(URI.create(url)).header("Content-Type", "application/json")
|
||||||
.uri(URI.create(url))
|
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body))).build(),
|
||||||
.header("Content-Type", "application/json")
|
HttpResponse.BodyHandlers.ofString()).body();
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))
|
|
||||||
.build();
|
|
||||||
return httpClient.send(request, HttpResponse.BodyHandlers.ofString()).body();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String get(String url) throws Exception {
|
private String get(String url) throws Exception {
|
||||||
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build();
|
return httpClient.send(HttpRequest.newBuilder().uri(URI.create(url)).GET().build(), HttpResponse.BodyHandlers.ofString()).body();
|
||||||
return httpClient.send(request, HttpResponse.BodyHandlers.ofString()).body();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMeetingStatus(Long id, int status) {
|
private void updateMeetingStatus(Long id, int status) {
|
||||||
Meeting m = new Meeting();
|
Meeting m = new Meeting(); m.setId(id); m.setStatus(status); meetingMapper.updateById(m);
|
||||||
m.setId(id);
|
|
||||||
m.setStatus(status);
|
|
||||||
meetingMapper.updateById(m);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private AiTask createAiTask(Long meetingId, String type, Map<String, Object> req) {
|
private AiTask createAiTask(Long meetingId, String type, Map<String, Object> req) {
|
||||||
AiTask task = new AiTask();
|
AiTask task = new AiTask();
|
||||||
task.setMeetingId(meetingId);
|
task.setMeetingId(meetingId); task.setTaskType(type); task.setStatus(1);
|
||||||
task.setTaskType(type);
|
task.setRequestData(req); task.setStartedAt(LocalDateTime.now());
|
||||||
task.setStatus(1);
|
this.save(task); return task;
|
||||||
task.setRequestData(req);
|
|
||||||
task.setStartedAt(LocalDateTime.now());
|
|
||||||
this.save(task);
|
|
||||||
return task;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateAiTaskSuccess(AiTask task, JsonNode resp) {
|
private void updateAiTaskSuccess(AiTask task, JsonNode resp) {
|
||||||
task.setStatus(2);
|
task.setStatus(2); task.setResponseData(objectMapper.convertValue(resp, Map.class));
|
||||||
task.setResponseData(objectMapper.convertValue(resp, Map.class));
|
task.setCompletedAt(LocalDateTime.now()); this.updateById(task);
|
||||||
task.setCompletedAt(LocalDateTime.now());
|
|
||||||
this.updateById(task);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateAiTaskFail(AiTask task, String error) {
|
private void updateAiTaskFail(AiTask task, String error) {
|
||||||
task.setStatus(3);
|
task.setStatus(3); task.setErrorMsg(error);
|
||||||
task.setErrorMsg(error);
|
task.setCompletedAt(LocalDateTime.now()); this.updateById(task);
|
||||||
task.setCompletedAt(LocalDateTime.now());
|
|
||||||
this.updateById(task);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined,
|
PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined,
|
||||||
UserOutlined, GlobalOutlined
|
UserOutlined, GlobalOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDict } from '../../hooks/useDict';
|
import { useDict } from '../../hooks/useDict';
|
||||||
import {
|
import {
|
||||||
getHotWordPage,
|
getHotWordPage,
|
||||||
|
|
@ -23,6 +24,7 @@ const { Option } = Select;
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
const HotWords: React.FC = () => {
|
const HotWords: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const { items: categories, loading: dictLoading } = useDict('biz_hotword_category');
|
const { items: categories, loading: dictLoading } = useDict('biz_hotword_category');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ const MeetingProgressDisplay: React.FC<{ meetingId: number; onComplete: () => vo
|
||||||
|
|
||||||
// 格式化剩余时间 (ETA)
|
// 格式化剩余时间 (ETA)
|
||||||
const formatETA = (seconds?: number) => {
|
const formatETA = (seconds?: number) => {
|
||||||
if (!seconds || seconds <= 0) return '即将完成';
|
if (!seconds || seconds <= 0) return '正在分析中';
|
||||||
if (seconds < 60) return `${seconds}秒`;
|
if (seconds < 60) return `${seconds}秒`;
|
||||||
const m = Math.floor(seconds / 60);
|
const m = Math.floor(seconds / 60);
|
||||||
const s = seconds % 60;
|
const s = seconds % 60;
|
||||||
|
|
@ -67,7 +67,7 @@ const MeetingProgressDisplay: React.FC<{ meetingId: number; onComplete: () => vo
|
||||||
<Text strong style={{ fontSize: 18, color: isError ? '#ff4d4f' : '#1890ff', display: 'block', marginBottom: 8 }}>
|
<Text strong style={{ fontSize: 18, color: isError ? '#ff4d4f' : '#1890ff', display: 'block', marginBottom: 8 }}>
|
||||||
{progress?.message || '正在准备计算资源...'}
|
{progress?.message || '正在准备计算资源...'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="secondary">分析过程大约需要 1-3 分钟,请耐心等待,您可以先去处理其他工作</Text>
|
<Text type="secondary">分析过程中,请耐心等待,您可以先去处理其他工作</Text>
|
||||||
</div>
|
</div>
|
||||||
<Divider style={{ margin: '32px 0' }} />
|
<Divider style={{ margin: '32px 0' }} />
|
||||||
<Row gutter={24}>
|
<Row gutter={24}>
|
||||||
|
|
@ -304,7 +304,28 @@ const MeetingDetail: React.FC = () => {
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<Space>
|
<Space>
|
||||||
{isOwner && <Button icon={<SyncOutlined />} type="primary" ghost onClick={() => setSummaryVisible(true)}>重新总结</Button>}
|
{isOwner && meeting.status === 3 && (
|
||||||
|
<Button
|
||||||
|
icon={<SyncOutlined />}
|
||||||
|
type="primary"
|
||||||
|
ghost
|
||||||
|
onClick={() => setSummaryVisible(true)}
|
||||||
|
disabled={actionLoading}
|
||||||
|
>
|
||||||
|
重新总结
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{isOwner && meeting.status === 2 && (
|
||||||
|
<Button
|
||||||
|
icon={<LoadingOutlined />}
|
||||||
|
type="primary"
|
||||||
|
ghost
|
||||||
|
disabled
|
||||||
|
loading
|
||||||
|
>
|
||||||
|
正在总结
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button icon={<LeftOutlined />} onClick={() => navigate('/meetings')}>返回列表</Button>
|
<Button icon={<LeftOutlined />} onClick={() => navigate('/meetings')}>返回列表</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
||||||
|
|
@ -241,10 +241,10 @@ const PromptTemplates: React.FC = () => {
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 12 }}>
|
{/*<div style={{ marginBottom: 12 }}>*/}
|
||||||
<Text strong style={{ fontSize: 16, display: 'block' }} ellipsis={{ tooltip: item.templateName }}>{item.templateName}</Text>
|
{/* <Text strong style={{ fontSize: 16, display: 'block' }} ellipsis={{ tooltip: item.templateName }}>{item.templateName}</Text>*/}
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>使用次数: {item.usageCount || 0}</Text>
|
{/* <Text type="secondary" style={{ fontSize: 12 }}>使用次数: {item.usageCount || 0}</Text>*/}
|
||||||
</div>
|
{/*</div>*/}
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 20, height: 22, overflow: 'hidden' }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 20, height: 22, overflow: 'hidden' }}>
|
||||||
{item.tags?.map(tag => {
|
{item.tags?.map(tag => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue