diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java index 78fc131..e2b9d81 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java @@ -214,26 +214,18 @@ public class AiTaskServiceImpl extends ServiceImpl impleme ? String.valueOf(taskRecord.getResponseData().getOrDefault("task_id", "")) : ""; - if (taskId == null || taskId.isBlank()) { - updateProgress(meeting.getId(), 5, "正在提交识别请求...", 0); - Map req = buildAsrRequest(meeting, taskRecord, asrModel); - taskRecord.setRequestData(req); - this.updateById(taskRecord); - - String respBody = postJson(submitUrl, req, asrModel.getApiKey()); - JsonNode submitNode = objectMapper.readTree(respBody); - if (submitNode.path("code").asInt() != 0) { - updateAiTaskFail(taskRecord, "提交失败: " + 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); - } else { + if (taskId != null && !taskId.isBlank()) { updateProgress(meeting.getId(), 5, "Resuming ASR polling...", 0); + if (!canResumeAsrTask(asrModel, meeting.getId(), taskId)) { + clearAsrTaskId(taskRecord); + taskId = ""; + } + } + + if (taskId == null || taskId.isBlank()) { + taskId = submitAsrTask(meeting, taskRecord, asrModel, submitUrl); } this.updateById(taskRecord); - String queryUrl = appendPath(asrModel.getBaseUrl(), "api/v1/asr/transcriptions/" + taskId); // 轮询逻辑(带防卡死防护) @@ -245,6 +237,11 @@ public class AiTaskServiceImpl extends ServiceImpl impleme Thread.sleep(2000); String queryResp = get(queryUrl, asrModel.getApiKey()); JsonNode statusNode = objectMapper.readTree(queryResp); + int code = statusNode.path("code").asInt(500); + if (code!=0){ + updateAiTaskFail(taskRecord, "ASR 引擎返回失败:" + queryResp); + throw new RuntimeException("ASR引擎处理失败: " + statusNode.get("message").asText()); + } JsonNode data = statusNode.path("data"); String status = data.path("status").asText(); @@ -325,6 +322,58 @@ public class AiTaskServiceImpl extends ServiceImpl impleme return req; } + private boolean canResumeAsrTask(AiModelVO asrModel, Long meetingId, String taskId) { + String queryUrl = appendPath(asrModel.getBaseUrl(), "api/v1/asr/transcriptions/" + taskId); + try { + String queryResp = get(queryUrl, asrModel.getApiKey()); + JsonNode statusNode = objectMapper.readTree(queryResp); + int code = statusNode.path("code").asInt(500); + if (code != 0) { + log.warn("ASR task {} progress fetch failed for meeting {}, will resubmit task. response={}", + taskId, meetingId, queryResp); + return false; + } + + String status = statusNode.path("data").path("status").asText(); + if ("failed".equalsIgnoreCase(status)) { + log.warn("ASR task {} already failed for meeting {}, will resubmit task.", taskId, meetingId); + return false; + } + return true; + } catch (Exception ex) { + log.warn("ASR task {} progress fetch threw exception for meeting {}, will resubmit task.", + taskId, meetingId, ex); + return false; + } + } + + private void clearAsrTaskId(AiTask taskRecord) { + if (taskRecord.getResponseData() == null || taskRecord.getResponseData().isEmpty()) { + return; + } + Map responseData = new HashMap<>(taskRecord.getResponseData()); + responseData.remove("task_id"); + taskRecord.setResponseData(responseData.isEmpty() ? null : responseData); + this.updateById(taskRecord); + } + + private String submitAsrTask(Meeting meeting, AiTask taskRecord, AiModelVO asrModel, String submitUrl) throws Exception { + updateProgress(meeting.getId(), 5, "重新提交任务...", 0); + Map req = buildAsrRequest(meeting, taskRecord, asrModel); + taskRecord.setRequestData(req); + this.updateById(taskRecord); + + String respBody = postJson(submitUrl, req, asrModel.getApiKey()); + JsonNode submitNode = objectMapper.readTree(respBody); + if (submitNode.path("code").asInt() != 0) { + updateAiTaskFail(taskRecord, "ASR识别失败: " + respBody); + throw new RuntimeException("ASR识别失败: " + submitNode.path("msg").asText()); + } + String taskId = submitNode.path("data").path("task_id").asText(); + taskRecord.setResponseData(Map.of("task_id", taskId)); + this.updateById(taskRecord); + return taskId; + } @Transactional(rollbackFor = Exception.class) protected String saveTranscripts(Meeting meeting, JsonNode resultNode) { // 关键:入库前清理旧记录,防止恢复任务导致数据重复 diff --git a/frontend/src/pages/dashboard/index.tsx b/frontend/src/pages/dashboard/index.tsx index 3fb6e9a..f15ab96 100644 --- a/frontend/src/pages/dashboard/index.tsx +++ b/frontend/src/pages/dashboard/index.tsx @@ -1,5 +1,6 @@ -import React, { useState, useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import PageContainer from "@/components/shared/PageContainer"; +import AppPagination from '@/components/shared/AppPagination'; import { Row, Col, Card, Statistic, List, Tag, Typography, Button, Space, Empty, Steps, Progress, Divider } from 'antd'; import { HistoryOutlined, @@ -16,8 +17,8 @@ import { } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import dayjs from 'dayjs'; -import { getDashboardStats, getRecentTasks, DashboardStats } from '@/api/business/dashboard'; -import { MeetingVO, getMeetingProgress, MeetingProgress } from '@/api/business/meeting'; +import { getDashboardStats, DashboardStats } from '@/api/business/dashboard'; +import { MeetingVO, getMeetingPage, getMeetingProgress, MeetingProgress } from '@/api/business/meeting'; const { Title, Text } = Typography; @@ -33,12 +34,12 @@ const MeetingProgressDisplay: React.FC<{ meeting: MeetingVO }> = ({ meeting }) = if (res.data?.data) { setProgress(res.data.data); } - } catch (err) { + } catch { // ignore } }; - fetchProgress(); + void fetchProgress(); const timer = setInterval(fetchProgress, 3000); return () => clearInterval(timer); }, [meeting.id, meeting.status]); @@ -50,7 +51,7 @@ const MeetingProgressDisplay: React.FC<{ meeting: MeetingVO }> = ({ meeting }) = return (
-
+
{progress?.message || '准备分析中...'} @@ -73,33 +74,62 @@ export const Dashboard: React.FC = () => { const [stats, setStats] = useState(null); const [recentTasks, setRecentTasks] = useState([]); const [loading, setLoading] = useState(true); + const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }); const processingCount = Number(stats?.processingTasks || 0); - const dashboardLoading = loading && processingCount > 0; + const dashboardLoading = loading; useEffect(() => { - fetchDashboardData(); - const timer = setInterval(fetchDashboardData, 5000); - return () => clearInterval(timer); - }, []); + void fetchDashboardData(); + }, [pagination.current, pagination.pageSize]); - const fetchDashboardData = async () => { + useEffect(() => { + const timer = setInterval(() => { + void fetchDashboardData(true); + }, 5000); + return () => clearInterval(timer); + }, [pagination.current, pagination.pageSize]); + + const fetchDashboardData = async (silent = false) => { + if (!silent) { + setLoading(true); + } try { - const [statsRes, tasksRes] = await Promise.all([getDashboardStats(), getRecentTasks()]); + const [statsRes, tasksRes] = await Promise.all([ + getDashboardStats(), + getMeetingPage({ + current: pagination.current, + size: pagination.pageSize, + }), + ]); setStats(statsRes.data.data); - setRecentTasks(tasksRes.data.data || []); + setRecentTasks(tasksRes.data?.data?.records || []); + setPagination((prev) => ({ + ...prev, + total: tasksRes.data?.data?.total || 0, + })); } catch (err) { console.error('Dashboard data load failed', err); } finally { - setLoading(false); + if (!silent) { + setLoading(false); + } } }; + const handlePageChange = (page: number, pageSize: number) => { + setPagination((prev) => ({ + ...prev, + current: page, + pageSize, + })); + }; + const renderTaskProgress = (item: MeetingVO) => { const currentStep = item.status === 4 ? 0 : (item.status === 3 ? 2 : item.status); return ( -
+
{ { title: '语音转录', icon: item.status === 1 ? : , - description: item.status > 1 ? '识别完成' : (item.status === 1 ? 'AI转录中' : '排队中') + description: item.status > 1 ? '识别完成' : (item.status === 1 ? 'AI 转录中' : '排队中') }, { title: '智能总结', @@ -139,71 +169,77 @@ export const Dashboard: React.FC = () => { return ( - - {statCards.map((s, idx) => ( - - - {s.label}} - value={s.value || 0} - valueStyle={{ color: s.color, fontWeight: 700 }} - prefix={React.cloneElement(s.icon as React.ReactElement, { style: { marginRight: 8 } })} - /> - - - ))} - + + {statCards.map((s, idx) => ( + + + {s.label}} + value={s.value || 0} + valueStyle={{ color: s.color, fontWeight: 700 }} + prefix={React.cloneElement(s.icon as React.ReactElement, { style: { marginRight: 8 } })} + /> + + + ))} + - - 最近任务动态 - -
- } - variant="borderless" - style={{ borderRadius: 16, boxShadow: 'var(--app-shadow)', background: 'var(--app-bg-card)', border: '1px solid var(--app-border-color)', backdropFilter: 'blur(16px)' }} - > + + 最近任务动态 + +
+ } + variant="borderless" + style={{ flex: 1, minHeight: 0, borderRadius: 16, boxShadow: 'var(--app-shadow)', background: 'var(--app-bg-card)', border: '1px solid var(--app-border-color)', backdropFilter: 'blur(16px)', overflow: 'hidden' }} + styles={{ body: { display: 'flex', flexDirection: 'column', gap: 16, flex: 1, minHeight: 0, overflow: 'hidden',height:'90%' } }} + > +
(
- - - - navigate(`/meetings/${item.id}`)}> + <Row gutter={[24, 16]} align="middle"> + <Col xs={24} xl={8}> + <Space direction="vertical" size={4} style={{ width: '100%' }}> + <Title level={5} style={{ margin: 0, cursor: 'pointer', wordBreak: 'break-word' }} onClick={() => navigate(`/meetings/${item.id}`)}> {item.title} - }> + }> {dayjs(item.meetingTime).format('MM-DD HH:mm')} {item.participants || item.creatorName || '未指定'} -
+
{item.tags?.split(',').filter(Boolean).map((t) => ( - {t} + {t} ))}
- + {renderTaskProgress(item)} - - + +
+ +
@@ -213,11 +249,44 @@ export const Dashboard: React.FC = () => { )} locale={{ emptyText: }} /> - +
+ + );