会话总结工作流接入、前后端处理

This commit is contained in:
2026-01-01 15:12:29 +08:00
parent 4e373e6d2c
commit eb15706ccc
22 changed files with 1738 additions and 43 deletions

View File

@@ -11,7 +11,9 @@ import type {
ChatMemberVO,
ChatRoomMessageVO,
CustomerServiceVO,
VideoMeetingVO
VideoMeetingVO,
ChatRoomSummaryRequest,
ChatRoomSummaryResponse
} from '@/types/workcase'
/**
@@ -286,5 +288,29 @@ export const workcaseChatAPI = {
params: { commentLevel }
})
return response.data
},
// ====================== 聊天室总结管理 ======================
/**
* 获取聊天室最新总结
* @param roomId 聊天室ID
*/
async getLatestSummary(roomId: string): Promise<ResultDomain<ChatRoomSummaryResponse>> {
const response = await api.get<ChatRoomSummaryResponse>(`${this.baseUrl}/room/${roomId}/summary`)
return response.data
},
/**
* 生成聊天室对话总结
* @param request 总结请求参数
*/
async summaryChatRoom(request: ChatRoomSummaryRequest): Promise<ResultDomain<ChatRoomSummaryResponse>> {
const { roomId, ...body } = request
const response = await api.post<ChatRoomSummaryResponse>(
`${this.baseUrl}/room/${roomId}/summary`,
Object.keys(body).length > 0 ? body : {}
)
return response.data
}
}

View File

@@ -313,4 +313,26 @@ export interface CreateMeetingParam {
export interface MarkReadParam {
roomId: string
messageIds?: string[]
}
/**
* 聊天室总结请求参数
*/
export interface ChatRoomSummaryRequest {
roomId: string
includeSystemMessages?: boolean
includeMeetingMessages?: boolean
}
/**
* 聊天室总结响应结果
*/
export interface ChatRoomSummaryResponse {
question?: string
needs?: string[]
answer?: string
workcloud?: string[]
roomId?: string
summaryTime?: string
messageCount?: number
}

View File

@@ -174,26 +174,166 @@
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 20px;
padding: 24px;
min-height: 420px;
}
.summary-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400px;
gap: 16px;
color: #6b7280;
font-size: 14px;
.loading-spinner {
width: 48px;
height: 48px;
border: 4px solid #e5e7eb;
border-top-color: #4b87ff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.summary-content {
display: flex;
flex-direction: column;
gap: 16px;
gap: 24px;
}
.summary-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 16px;
border-bottom: 2px solid #e5e7eb;
h3 {
font-size: 18px;
font-weight: 700;
color: #111827;
margin: 0;
}
.regenerate-btn {
padding: 6px 16px;
background: #fff;
color: #4b87ff;
border: 1px solid #4b87ff;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #4b87ff;
color: #fff;
}
}
}
.summary-section {
.summary-title {
font-size: 14px;
font-weight: 700;
color: #111827;
margin-bottom: 8px;
font-weight: 600;
color: #374151;
margin-bottom: 12px;
padding-left: 12px;
border-left: 3px solid #4b87ff;
}
.summary-text {
font-size: 14px;
color: #374151;
line-height: 1.6;
color: #1f2937;
line-height: 1.8;
padding: 16px;
background: #f9fafb;
border-radius: 8px;
white-space: pre-wrap;
}
.summary-list {
margin: 0;
padding-left: 24px;
li {
font-size: 14px;
color: #1f2937;
line-height: 2;
margin-bottom: 8px;
position: relative;
&:last-child {
margin-bottom: 0;
}
&::marker {
color: #4b87ff;
}
}
}
}
.summary-meta {
display: flex;
gap: 16px;
padding: 12px 16px;
background: #fef3c7;
border-radius: 8px;
font-size: 12px;
color: #92400e;
margin-top: 8px;
span {
&:not(:last-child)::after {
content: '';
margin-left: 16px;
color: #d97706;
}
}
}
.summary-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400px;
gap: 16px;
.empty-icon {
font-size: 64px;
opacity: 0.5;
}
.empty-text {
font-size: 14px;
color: #9ca3af;
}
.generate-btn {
padding: 10px 24px;
background: #4b87ff;
color: #fff;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #3b77ef;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(75, 135, 255, 0.3);
}
}
}

View File

@@ -81,19 +81,55 @@
<!-- 对话纪要 -->
<div v-else class="summary-container">
<div class="summary-content">
<div class="summary-section">
<div class="summary-title">问题概述</div>
<!-- 加载状态 -->
<div v-if="loadingSummary" class="summary-loading">
<div class="loading-spinner"></div>
<span>正在生成对话总结...</span>
</div>
<!-- 总结内容 -->
<div v-else-if="summaryData" class="summary-content">
<div class="summary-header">
<h3>对话总结</h3>
<button class="regenerate-btn" @click="generateSummary">重新生成</button>
</div>
<div v-if="summaryData.question" class="summary-section">
<div class="summary-title">核心问题</div>
<div class="summary-text">
{{ summary.overview || '暂无概述' }}
{{ summaryData.question }}
</div>
</div>
<div class="summary-section">
<div class="summary-title">客户诉求</div>
<div v-if="summaryData.needs && summaryData.needs.length > 0" class="summary-section">
<div class="summary-title">核心诉求</div>
<ul class="summary-list">
<li v-for="(need, index) in summaryData.needs" :key="index">{{ need }}</li>
</ul>
</div>
<div v-if="summaryData.answer" class="summary-section">
<div class="summary-title">解决方案</div>
<div class="summary-text">
{{ summary.demand || '暂无诉求' }}
{{ summaryData.answer }}
</div>
</div>
<div class="summary-meta">
<span v-if="summaryData.messageCount">
基于 {{ summaryData.messageCount }} 条消息生成
</span>
<span v-if="summaryData.summaryTime">
生成时间{{ summaryData.summaryTime }}
</span>
</div>
</div>
<!-- 空状态 -->
<div v-else class="summary-empty">
<div class="empty-icon">📝</div>
<div class="empty-text">暂无对话总结</div>
<button class="generate-btn" @click="generateSummary">生成总结</button>
</div>
</div>
</main>
@@ -101,13 +137,9 @@
</template>
<script setup lang="ts">
import { ref, onMounted, watch, computed, nextTick } from 'vue'
import type { ChatRoomVO, ChatRoomMessageVO } from '@/types/workcase/chatRoom'
import type { ChatRoomVO, ChatRoomMessageVO, ChatRoomSummaryResponse } from '@/types/workcase/chatRoom'
import { workcaseChatAPI } from '@/api/workcase/workcaseChat'
interface Summary {
overview?: string
demand?: string
}
import { ElMessage } from 'element-plus'
interface Props {
chatRoom?: ChatRoomVO
@@ -123,10 +155,8 @@ const activeTab = ref<'record' | 'summary'>('record')
const messages = ref<ChatRoomMessageVO[]>([])
const loading = ref(false)
const loadingMore = ref(false)
const summary = ref<Summary>({
overview: '',
demand: ''
})
const summaryData = ref<ChatRoomSummaryResponse | null>(null)
const loadingSummary = ref(false)
// 分页相关
const currentPage = ref(1)
@@ -241,34 +271,82 @@ const handleScroll = () => {
}
}
// 生成对话纪要可以后续接入AI生成
const generateSummary = () => {
if (messages.value.length === 0) return
// 简单提取第一条和最后一条消息作为概述
const firstMsg = messages.value[0]
const lastMsg = messages.value[messages.value.length - 1]
summary.value = {
overview: `客户反馈:${firstMsg.content?.substring(0, 50) || ''}`,
demand: lastMsg.content?.substring(0, 100) || ''
// 查询最新的对话总结
const loadLatestSummary = async () => {
if (!targetRoomId.value) {
return
}
loadingSummary.value = true
summaryData.value = null
try {
const res = await workcaseChatAPI.getLatestSummary(targetRoomId.value)
if (res.success && res.data) {
summaryData.value = res.data
} else {
// 查询失败,表示还没有总结
summaryData.value = null
}
} catch (error) {
console.error('查询总结失败:', error)
summaryData.value = null
} finally {
loadingSummary.value = false
}
}
// 生成对话纪要调用后端AI接口
const generateSummary = async () => {
if (!targetRoomId.value) {
ElMessage.warning('无效的聊天室ID')
return
}
loadingSummary.value = true
summaryData.value = null
try {
const res = await workcaseChatAPI.summaryChatRoom({
roomId: targetRoomId.value,
includeSystemMessages: false,
includeMeetingMessages: false
})
if (res.success && res.data) {
summaryData.value = res.data
ElMessage.success('总结生成成功')
} else {
ElMessage.error(res.message || '生成总结失败')
}
} catch (error) {
console.error('生成总结失败:', error)
ElMessage.error('生成总结失败')
} finally {
loadingSummary.value = false
}
}
onMounted(async () => {
await loadMessages()
generateSummary()
})
// 监听 roomId 变化,重新加载数据
watch(targetRoomId, async (newVal) => {
if (newVal) {
messages.value = []
summary.value = { overview: '', demand: '' }
summaryData.value = null
currentPage.value = 1
total.value = 0
await loadMessages()
generateSummary()
}
})
// 监听切换到对话纪要标签,自动查询总结
watch(activeTab, async (newVal) => {
if (newVal === 'summary' && !summaryData.value && !loadingSummary.value) {
await loadLatestSummary()
}
})
</script>