feat: 支持停止对话

This commit is contained in:
digua
2025-12-05 00:45:14 +08:00
parent ac99203075
commit 04dc2a79c1
13 changed files with 259 additions and 53 deletions
+17 -35
View File
@@ -175,15 +175,10 @@ onMounted(() => {
</script>
<template>
<div class="p-6">
<div class="max-w-5xl p-6">
<!-- 页面标题 -->
<div class="mb-6">
<div class="flex items-center gap-3">
<div
class="flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-br from-blue-500 to-indigo-600"
>
<UIcon name="i-heroicons-user-group" class="h-6 w-6 text-white" />
</div>
<div>
<h2 class="text-xl font-bold text-gray-900 dark:text-white">群成员管理</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
@@ -197,10 +192,9 @@ onMounted(() => {
<div class="mb-4">
<UInput
v-model="searchQuery"
placeholder="搜索群昵称、账号名称、ID 或别名..."
placeholder="搜索群昵称、账号名称、ID 或别名"
icon="i-heroicons-magnifying-glass"
size="lg"
:ui="{ icon: { trailing: { pointer: '' } } }"
class="w-100"
>
<template #trailing v-if="searchQuery">
<UButton icon="i-heroicons-x-mark" variant="link" color="neutral" size="xs" @click="searchQuery = ''" />
@@ -229,7 +223,6 @@ onMounted(() => {
<table class="w-full">
<thead class="sticky top-0 bg-gray-50 dark:bg-gray-800">
<tr class="text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">
<th class="px-4 py-4">ID</th>
<th class="px-4 py-4">账号名称</th>
<th class="px-4 py-4">群昵称</th>
<th class="px-4 py-4">
@@ -244,7 +237,7 @@ onMounted(() => {
/>
</button>
</th>
<th class="px-4 py-4">自定义别名</th>
<th class="px-4 py-4 w-64">自定义别名</th>
<th class="px-4 py-4 text-right">操作</th>
</tr>
</thead>
@@ -254,28 +247,26 @@ onMounted(() => {
:key="member.id"
class="hover:bg-gray-50 dark:hover:bg-gray-800/50"
>
<!-- 平台ID -->
<!-- 账号名称 (ID) -->
<td class="px-4 py-4">
<div class="flex items-center gap-2">
<div
class="flex h-8 w-8 items-center justify-center rounded-full bg-gradient-to-br from-pink-400 to-pink-600 text-xs font-medium text-white"
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-pink-400 to-pink-600 text-xs font-medium text-white"
>
{{ getFirstChar(member) }}
</div>
<span class="text-sm text-gray-500 dark:text-gray-400">{{ member.platformId }}</span>
<div>
<span class="text-sm font-medium text-gray-900 dark:text-white">
{{ member.accountName || '-' }}
</span>
<span class="ml-1 text-sm text-gray-500 dark:text-gray-400">({{ member.platformId }})</span>
</div>
</div>
</td>
<!-- 账号名称 -->
<td class="px-4 py-4">
<span class="text-sm text-gray-900 dark:text-white">
{{ member.accountName || '-' }}
</span>
</td>
<!-- 群昵称 -->
<td class="px-4 py-4">
<span v-if="member.groupNickname" class="text-sm font-medium text-blue-600 dark:text-blue-400">
<span v-if="member.groupNickname" class="text-sm font-medium text-gray-900 dark:text-white">
{{ member.groupNickname }}
</span>
<span v-else class="text-sm text-gray-400 dark:text-gray-500">-</span>
@@ -290,13 +281,12 @@ onMounted(() => {
<!-- 别名 - 直接编辑 -->
<td class="px-4 py-4">
<div class="relative max-w-xs">
<div class="max-w-xs">
<UInputTags
:model-value="member.aliases"
@update:model-value="(val) => updateAliases(member, val)"
placeholder="输入后回车..."
size="sm"
:ui="{ base: 'min-h-[36px]' }"
placeholder="输入后回车添加"
class="w-80"
/>
<!-- 保存中指示器 -->
<div v-if="savingAliasesId === member.id" class="absolute right-2 top-1/2 -translate-y-1/2">
@@ -307,15 +297,7 @@ onMounted(() => {
<!-- 操作 -->
<td class="px-4 py-4 text-right">
<UTooltip text="删除成员">
<UButton
icon="i-heroicons-trash"
variant="ghost"
color="error"
size="sm"
@click="showDeleteConfirm(member)"
/>
</UTooltip>
<UButton label="删除" variant="link" color="error" size="xs" @click="showDeleteConfirm(member)" />
</td>
</tr>
</tbody>
+14 -2
View File
@@ -210,6 +210,7 @@ watch(
<template v-for="msg in messages" :key="msg.id">
<!-- 聊天消息 -->
<ChatMessage
v-if="msg.role === 'user' || msg.content"
:role="msg.role"
:content="msg.content"
:timestamp="msg.timestamp"
@@ -338,7 +339,7 @@ watch(
</template>
<!-- AI 思考中指示器 -->
<div v-if="isAIThinking && !messages[messages.length - 1]?.isStreaming" class="flex items-start gap-3">
<div v-if="isAIThinking && !messages[messages.length - 1]?.content" class="flex items-start gap-3">
<div
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-linear-to-br from-violet-500 to-purple-600"
>
@@ -376,6 +377,17 @@ watch(
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-violet-500 [animation-delay:150ms]" />
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-violet-500 [animation-delay:300ms]" />
</span>
<span
v-else-if="currentToolStatus.status === 'done'"
class="flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400"
>
<span>处理结果中</span>
<span class="flex gap-1">
<span class="h-1 w-1 animate-bounce rounded-full bg-gray-400 [animation-delay:0ms]" />
<span class="h-1 w-1 animate-bounce rounded-full bg-gray-400 [animation-delay:150ms]" />
<span class="h-1 w-1 animate-bounce rounded-full bg-gray-400 [animation-delay:300ms]" />
</span>
</span>
</div>
<!-- 已使用的工具列表 -->
<div v-if="toolsUsedInCurrentRound.length > 1" class="flex flex-wrap gap-1">
@@ -409,7 +421,7 @@ watch(
<div class="mx-auto max-w-3xl">
<ChatInput
:disabled="isAIThinking"
:status="isAIThinking ? (isLoadingSource ? 'submitted' : 'streaming') : 'ready'"
:status="isAIThinking ? 'streaming' : 'ready'"
@send="handleSend"
@stop="handleStop"
/>
+9 -1
View File
@@ -49,7 +49,15 @@ function handleStop() {
variant="subtle"
@submit="handleSubmit"
>
<UChatPromptSubmit :status="chatStatus" class="rounded-full" color="primary" @stop="handleStop" />
<UButton
v-if="chatStatus === 'streaming'"
icon="i-heroicons-stop-20-solid"
color="primary"
variant="solid"
class="rounded-full"
@click="handleStop"
/>
<UChatPromptSubmit v-else :status="chatStatus" class="rounded-full" color="primary" @stop="handleStop" />
</UChatPrompt>
</div>
</div>
+23 -2
View File
@@ -73,6 +73,8 @@ export function useAIChat(sessionId: string, timeFilter?: { startTs: number; end
let isAborted = false
// 当前请求 ID,用于区分不同请求的响应
let currentRequestId = ''
// 当前 Agent 请求 ID,用于中止请求
let currentAgentRequestId = ''
// 生成消息 ID
function generateId(prefix: string): string {
@@ -151,7 +153,8 @@ export function useAIChat(sessionId: string, timeFilter?: { startTs: number; end
console.log('[AI] 调用 Agent API...', context)
const result = await window.agentApi.runStream(content, context, (chunk) => {
// 获取 requestId 和 promise
const { requestId: agentReqId, promise: agentPromise } = window.agentApi.runStream(content, context, (chunk) => {
// 如果已中止或请求 ID 不匹配,忽略后续 chunks
if (isAborted || thisRequestId !== currentRequestId) {
console.log('[AI] 已中止或请求已过期,忽略 chunk', {
@@ -255,6 +258,12 @@ export function useAIChat(sessionId: string, timeFilter?: { startTs: number; end
}
})
// 存储 Agent 请求 ID(用于中止)
currentAgentRequestId = agentReqId
console.log('[AI] Agent 请求已启动,agentReqId:', agentReqId)
// 等待 Agent 完成
const result = await agentPromise
console.log('[AI] Agent 返回结果:', result)
// 如果请求已过期,不更新
@@ -397,7 +406,7 @@ export function useAIChat(sessionId: string, timeFilter?: { startTs: number; end
/**
* 停止生成
*/
function stopGeneration(): void {
async function stopGeneration(): Promise<void> {
if (!isAIThinking.value) return
console.log('[AI] 用户停止生成')
@@ -406,6 +415,18 @@ export function useAIChat(sessionId: string, timeFilter?: { startTs: number; end
isLoadingSource.value = false
currentToolStatus.value = null
// 调用主进程中止 Agent 请求
if (currentAgentRequestId) {
console.log('[AI] 中止 Agent 请求:', currentAgentRequestId)
try {
await window.agentApi.abort(currentAgentRequestId)
console.log('[AI] Agent 请求已中止')
} catch (error) {
console.error('[AI] 中止 Agent 请求失败:', error)
}
currentAgentRequestId = ''
}
// 标记最后一条 AI 消息为已完成
const lastMessage = messages.value[messages.value.length - 1]
if (lastMessage && lastMessage.role === 'assistant' && lastMessage.isStreaming) {