Commit 7211c835 authored by 王舵's avatar 王舵

Merge branch 'main' into feature/take-road

parents c107c059 e87c7566
# WorkPanel和Event模块代码设计分析与优化方案
## 1. 概述
本文档旨在分析WorkPanel和Event模块在前后端代码设计中存在的功能冗余和重复实现问题,并提出相应的简化和优化方案。通过对Java后端代码和Vue前端组件的全面审查,识别出多个可以改进的地方,以提升代码质量、可维护性和性能。
## 2. 后端代码分析
### 2.1 事件管理逻辑重复
#### 2.1.1 工具调用状态跟踪重复
- **问题**: `WorkPanelDataCollector``DefaultEventManager`中都实现了工具调用状态跟踪逻辑,造成代码冗余。
- **具体表现**:
- `WorkPanelDataCollector`实现了自己的工具调用状态跟踪机制(`getLastPendingToolCall`方法)
- `DefaultEventManager`也有类似的工具调用状态跟踪机制(`pendingToolCalls`字段和相关方法)
#### 2.1.2 事件去重逻辑重复
- **问题**: 多个类中实现了相似的事件去重逻辑。
- **具体表现**:
- `WorkPanelDataCollector`实现了事件去重机制(`isDuplicateEvent`方法和`recentEventsCache`
- `EventDeduplicationService`也提供了事件去重功能
#### 2.1.3 事件类型处理重复
- **问题**: 在多个地方都有类似的事件类型判断和处理逻辑,造成代码重复且难以维护。
- **具体表现**:
-`WorkPanelDataCollector``SseEventSender`中都有类似的事件类型判断和处理逻辑
### 2.2 工具类功能重复
#### 2.2.1 JSON转换功能重复
- **问题**: 多个地方都有JSON处理逻辑,存在功能重复。
- **具体表现**:
- `WorkPanelUtils`提供了`convertToJsonString`方法
- `EventDataManager`在处理事件数据时也有类似的JSON处理逻辑
#### 2.2.2 事件类型映射重复
- **问题**: 多个地方都有根据状态确定事件类型的逻辑。
- **具体表现**:
- 多个地方都有根据状态确定事件类型的逻辑(如`WorkPanelUtils.getEventTypeFromStatus`
### 2.3 事件数据构建重复
#### 2.3.1 事件数据构建逻辑分散
- **问题**: 相同功能分散在多处,违反单一职责原则。
- **具体表现**:
- `EventDataManager`负责构建事件数据
- `SseEventSender`中也有事件数据处理逻辑
- `WorkPanelDataCollector`中也涉及事件数据的处理
### 2.4 事件工厂类与事件DTO类关系不清晰
#### 2.4.1 TimelineEventFactory职责不明确
- **问题**: 工厂类与具体的事件DTO类之间缺少清晰的设计模式应用。
- **具体表现**:
- `TimelineEventFactory`作为工厂类,负责根据事件类型创建相应的事件DTO对象
- 但在实际使用中,事件创建逻辑分散在多个地方
#### 2.4.2 事件DTO类继承体系问题
- **问题**: 各子类之间的差异较大,继承体系不够清晰。
- **具体表现**:
- 但各子类之间的差异较大,继承体系不够清晰
- 缺少统一的事件构建接口
## 3. 前端代码分析
### 3.1 事件类型定义重复
#### 3.1.1 类型接口重复定义
- **问题**: 在`TimelineContainer.vue``TimelinePanel.vue`中都定义了相同的事件类型接口。
- **具体表现**:
- `BaseTimelineEvent`
- `ThoughtEvent`
- `ToolCallEvent`
- `ToolResultEvent`
- `ToolErrorEvent`
- `EmbedEvent`
#### 3.1.2 事件类型标签映射重复
- **问题**: 两个组件中都定义了相同的`eventTypeLabels`映射。
- **具体表现**:
- 相同的映射关系在两个组件中重复定义
### 3.2 工具事件判断逻辑重复
#### 3.2.1 工具类型判断函数重复
- **问题**: 在`TimelineContainer.vue``TimelinePanel.vue`中都有相同的工具类型判断函数。
- **具体表现**:
- `isToolEventType`函数在两个组件中都存在
#### 3.2.2 工具输入输出验证函数重复
- **问题**: 在`TimelineContainer.vue``TimelinePanel.vue`中都有相同的工具输入输出验证函数。
- **具体表现**:
- `hasValidToolInput``hasValidToolOutput`函数在两个组件中都存在
### 3.3 类型守卫函数重复
#### 3.3.1 类型守卫函数重复定义
- **问题**: 在`TimelineContainer.vue``TimelinePanel.vue`中都定义了相同的类型守卫函数。
- **具体表现**:
- `isThoughtEvent`
- `isToolCallEvent`
- `isToolResultEvent`
- `isToolErrorEvent`
- `isEmbedEvent`
### 3.4 内容展开逻辑重复
#### 3.4.1 内容展开/折叠逻辑重复
- **问题**: 在`TimelineContainer.vue``TimelinePanel.vue`中都有内容展开/折叠的相关逻辑,两处实现基本相同。
- **具体表现**:
- 相同的展开/折叠逻辑在两个组件中重复实现
### 3.5 事件处理逻辑分散
#### 3.5.1 事件创建逻辑分散
- **问题**: 相同功能分散在多处,难以维护。
- **具体表现**:
- `TimelineContainer.vue`中有事件标准化逻辑
- `timelineEventHandler.ts`中也有事件处理逻辑
- `ChatArea.vue`中还有事件构造逻辑
#### 3.5.2 事件去重逻辑重复
- **问题**: 多个地方都有事件去重判断逻辑,缺少统一的事件去重服务。
- **具体表现**:
- 多个地方都有事件去重判断逻辑
- 缺少统一的事件去重服务
## 4. 后端优化方案
### 4.1 统一事件管理
#### 4.1.1 整合工具调用状态跟踪
- **解决方案**:
- 移除`WorkPanelDataCollector`中的工具调用状态跟踪逻辑
- 完全依赖`DefaultEventManager`进行工具调用状态管理
- `WorkPanelDataCollector`专注于数据收集和分发
#### 4.1.2 统一事件去重处理
- **解决方案**:
- 创建独立的`EventDeduplicationService`服务处理事件去重
- 移除`WorkPanelDataCollector`中的事件去重逻辑
- 所有事件去重需求统一通过`EventDeduplicationService`处理
#### 4.1.3 集中事件类型处理
- **解决方案**:
- 创建`EventTypeConverter`工具类统一处理事件类型转换
- 移除各处分散的状态到事件类型转换逻辑
### 4.2 优化事件数据构建
#### 4.2.1 重构事件数据构建流程
- **解决方案**:
- `EventDataManager`作为唯一的事件数据构建入口
- `SseEventSender`只负责事件发送,不处理数据构建
- `WorkPanelDataCollector`只负责数据收集,不处理数据构建
#### 4.2.2 优化对象池使用
- **解决方案**:
- 统一通过`MapPoolService`管理对象池
- 避免在多个地方重复创建和销毁对象
### 4.3 简化工具类功能
#### 4.3.1 合并JSON处理功能
- **解决方案**:
- 统一使用`WorkPanelUtils`处理JSON转换
- 移除`EventDataManager`中的重复实现
#### 4.3.2 优化工具类设计
- **解决方案**:
-`WorkPanelUtils`拆分为更小的专用工具类
-`JsonUtils``EventTypeUtils`等,提高代码内聚性
### 4.4 重构事件工厂与DTO类设计
#### 4.4.1 明确TimelineEventFactory职责
- **解决方案**:
-`TimelineEventFactory`定位为统一的事件创建入口
- 所有事件对象的创建都通过工厂类完成
- 移除其他地方的事件创建逻辑
#### 4.4.2 优化事件DTO类继承体系
- **解决方案**:
- 明确`WorkPanelEvent`作为基类的职责
- 为不同类型事件定义清晰的接口规范
- 考虑使用组合而非继承来处理事件属性
#### 4.4.3 引入Builder模式
- **解决方案**:
- 为复杂事件DTO类引入Builder模式
- 提高事件对象创建的灵活性和可读性
## 5. 前端优化方案
### 5.1 统一类型定义
#### 5.1.1 创建共享类型定义文件
- **解决方案**:
- 创建`types/timeline.ts`文件统一定义所有时间轴相关类型
-`TimelineContainer.vue``TimelinePanel.vue`中导入使用
#### 5.1.2 统一事件类型标签映射
- **解决方案**:
- 创建`constants/eventTypes.ts`文件定义事件类型标签映射
- 在需要的地方导入使用
### 5.2 提取公共功能
#### 5.2.1 创建工具函数库
- **解决方案**:
- 创建`utils/timelineUtils.ts`文件统一存放工具事件判断函数
- 包括`isToolEventType``hasValidToolInput``hasValidToolOutput`
#### 5.2.2 提取类型守卫函数
- **解决方案**:
- 将所有类型守卫函数移到`utils/typeGuards.ts`文件中
- 在组件中导入使用
### 5.3 优化组件结构
#### 5.3.1 简化TimelineContainer组件
- **解决方案**:
- 移除重复的逻辑实现
- 专注数据管理和与后端通信
#### 5.3.2 优化TimelinePanel组件
- **解决方案**:
- 移除重复的类型定义和工具函数
- 专注UI渲染和用户交互
#### 5.3.3 优化内容展开逻辑
- **解决方案**:
- 创建独立的`useContentExpansion`组合式函数
- 在需要的组件中复用
### 5.4 统一事件处理流程
#### 5.4.1 集中事件处理逻辑
- **解决方案**:
- 创建`services/EventProcessingService.ts`统一处理事件接收、解析和分发
- 移除`TimelineContainer.vue``ChatArea.vue`中的事件处理逻辑
- 所有组件通过服务获取标准化的事件对象
#### 5.4.2 统一事件去重机制
- **解决方案**:
- 创建`services/EventDeduplicationService.ts`处理事件去重
- 所有事件在进入系统前先经过去重检查
- 提高去重效率和准确性
## 6. 实施建议
### 6.1 实施步骤
#### 第一阶段:类型和常量统一
- 创建共享的类型定义文件
- 创建事件类型标签映射常量文件
- 更新组件引用
#### 第二阶段:工具函数提取
- 提取公共工具函数到独立文件
- 提取类型守卫函数
- 更新组件引用
#### 第三阶段:后端重构
- 整合事件管理逻辑
- 统一事件数据构建流程
- 优化工具类设计
- 重构事件工厂与DTO类设计
#### 第四阶段:前端重构
- 简化TimelineContainer组件
- 优化TimelinePanel组件
- 统一事件处理流程
- 测试验证功能完整性
### 6.2 风险控制
1. **逐步重构**: 采用渐进式重构方式,避免一次性大规模改动
2. **充分测试**: 每完成一个阶段都要进行充分测试,确保功能不受影响
3. **版本控制**: 使用Git进行版本控制,便于回滚和追踪变更
4. **文档更新**: 及时更新相关文档,确保团队成员了解变更
## 7. 预期收益
1. **代码质量提升**: 消除重复代码,提高代码内聚性
2. **可维护性增强**: 统一的实现方式便于后续维护和扩展
3. **性能优化**: 减少重复计算和对象创建,提高运行效率
4. **开发效率提高**: 清晰的职责划分和统一的接口降低开发复杂度
5. **架构清晰化**: 明确各组件和模块的职责边界,提高系统可理解性
\ No newline at end of file
<template>
<TimelinePanel
:events="events"
:getEventTypeLabel="getEventTypeLabel"
:formatTime="formatTime"
:getExpandedState="getExpandedState"
:toggleExpand="toggleExpand"
:isToolEventType="isToolEventType"
:hasValidToolInput="hasValidToolInput"
:hasValidToolOutput="hasValidToolOutput"
:onClearTimeline="handleClearTimeline"
/>
</template>
<script setup lang="ts">
import { ref, onUnmounted, onMounted } from 'vue'
import TimelinePanel from './TimelinePanel.vue'
import { TimelineEventStateManager } from '../services/timelineEventStateManager'
import { CacheService } from '../services/CacheService'
import type { TimelineEvent } from '../types/timeline'
import { eventTypeLabels } from '../types/timeline'
import { isToolEventType, hasValidToolInput, hasValidToolOutput } from '../utils/timelineUtils'
import { UnifiedEventProcessor } from '../services/UnifiedEventProcessor'
// 状态管理器
const stateManager = new TimelineEventStateManager();
// 缓存服务
const cacheService = new CacheService();
// 统一事件处理器
const eventProcessor = new UnifiedEventProcessor();
// 事件数据
const events = ref<TimelineEvent[]>([]);
// 注册事件处理器
eventProcessor.registerHandler((event: TimelineEvent) => {
// 添加事件到列表
events.value.push(event);
console.log('[TimelineContainer] 成功添加事件:', event.type, event.title);
});
// 添加时间轴事件
const addEvent = (event: any) => {
// 使用统一事件处理器处理并分发事件
eventProcessor.processAndDispatch(event);
};
// 获取事件类型标签
const getEventTypeLabel = (type: string): string => {
return cacheService.getCachedEventTypeLabel(type, eventTypeLabels,
(labels, t) => labels[t] || t);
};
// 格式化时间
const formatTime = (timestamp: number): string => {
return cacheService.getCachedFormattedTime(timestamp, (t) => {
const date = new Date(t);
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${hours}:${minutes}:${seconds}`;
});
};
// 获取事件的展开状态
const getExpandedState = (index: number): boolean => {
return stateManager.getExpandedState(index);
};
// 切换事件详细信息的展开状态
const toggleExpand = (index: number) => {
stateManager.toggleExpand(index);
};
// 清除时间轴
const handleClearTimeline = () => {
events.value = [];
eventProcessor.clearProcessedEvents();
stateManager.clearAllStates();
cacheService.clearAllCaches();
};
// 显示性能统计信息
const showPerformanceStats = () => {
const stats = eventProcessor.getPerformanceStats();
console.log('[TimelineContainer] 性能统计:', stats);
alert(`总处理事件数: ${stats.totalProcessed}\n重用事件数: ${stats.totalReused}\n重用率: ${stats.reuseRate}%`);
};
// 暴露方法供父组件调用
defineExpose({
addEvent,
clearTimeline: handleClearTimeline,
showPerformanceStats
});
// 组件卸载时清理资源
onUnmounted(() => {
stateManager.clearAllStates();
cacheService.clearAllCaches();
});
// 组件挂载时启动定期性能监控
onMounted(() => {
// 每30秒输出一次性能统计
const perfInterval = setInterval(() => {
const stats = eventProcessor.getPerformanceStats();
console.log('[TimelineContainer] 定期性能统计:', stats);
}, 30000);
// 组件卸载时清除定时器
onUnmounted(() => {
clearInterval(perfInterval);
});
});
</script>
<style scoped>
.timeline-container-wrapper {
height: 100%;
}
</style>
\ No newline at end of file
...@@ -2,45 +2,63 @@ ...@@ -2,45 +2,63 @@
<div class="timeline-panel"> <div class="timeline-panel">
<div class="timeline-header"> <div class="timeline-header">
<h3>执行过程</h3> <h3>执行过程</h3>
<el-button text @click="clearTimeline" :disabled="events.length === 0">清除</el-button> <el-button text @click="props.onClearTimeline" :disabled="!props.events || props.events.length === 0">清除</el-button>
</div> </div>
<div class="timeline-container" ref="timelineContainer"> <div class="timeline-container" ref="timelineContainer">
<div v-if="events.length === 0" class="empty-timeline"> <div v-if="!props.events || props.events.length === 0" class="empty-timeline">
<div class="empty-icon">📋</div> <div class="empty-icon">📋</div>
<div class="empty-text">等待执行过程...</div> <div class="empty-text">等待执行过程...</div>
</div> </div>
<div v-else class="timeline-list"> <div v-else class="timeline-list">
<div v-for="(event, index) in events" :key="index" class="timeline-item" :class="event.type"> <div v-for="(event, index) in reversedEvents" :key="event.timestamp + '-' + index" class="timeline-item" :class="event.type">
<div class="timeline-dot"></div> <div class="timeline-dot"></div>
<div class="timeline-content"> <div class="timeline-content">
<div class="event-header"> <div class="event-header">
<span class="event-type-badge" :class="event.type">{{ getEventTypeLabel(event.type) }}</span> <span class="event-type-badge" :class="event.type">{{ props.getEventTypeLabel(event.type) }}</span>
<span class="event-time">{{ formatTime(event.timestamp) }}</span> <span class="event-title">{{ truncateTitle(event.title) }}</span>
<span class="event-time">{{ props.formatTime(event.timestamp) }}</span>
</div> </div>
<div class="event-body"> <div class="event-body">
<div class="event-title">{{ event.title }}</div>
<div v-if="event.content" class="event-content"> <div v-if="event.content" class="event-content">
<pre><code>{{ event.content }}</code></pre> <div
class="content-text-wrapper"
@click="shouldShowToggle(event.timestamp) && toggleContentExpand(event.timestamp)"
>
<div
class="content-text"
:class="{ 'collapsed': !getContentExpandedState(event.timestamp), 'expanded': getContentExpandedState(event.timestamp) }"
:ref="(el) => setContentRef(el as HTMLElement, event.timestamp)"
>
{{ event.content }}
</div>
</div>
<div
v-if="shouldShowToggle(event.timestamp)"
class="content-toggle"
@click="toggleContentExpand(event.timestamp)"
>
{{ getContentExpandedState(event.timestamp) ? '收起' : '展开' }}
</div>
</div> </div>
<!-- 工具调用输入输出详情 --> <!-- 工具调用输入输出详情 -->
<div <div
v-if="(event.type === 'tool_call' || event.type === 'tool_result' || event.type === 'tool_error') && isToolDataVisible(event)" v-if="props.isToolEventType(event.type)"
class="tool-details" class="tool-details"
> >
<!-- 展开/折叠按钮 --> <!-- 展开/折叠按钮 -->
<div class="detail-toggle" @click="toggleExpand(index)"> <div class="detail-toggle" @click="props.toggleExpand(props.events.length - 1 - index)">
<span class="toggle-text">{{ getExpandedState(index) ? '收起详情' : '查看详情' }}</span> <span class="toggle-text">{{ props.getExpandedState(props.events.length - 1 - index) ? '收起详情' : '查看详情' }}</span>
<span class="toggle-icon">{{ getExpandedState(index) ? '▲' : '▼' }}</span> <span class="toggle-icon">{{ props.getExpandedState(props.events.length - 1 - index) ? '▲' : '▼' }}</span>
</div> </div>
<!-- 详细信息内容 --> <!-- 详细信息内容 -->
<div v-show="getExpandedState(index)" class="detail-content"> <div v-show="getExpandedState(props.events.length - 1 - index)" class="detail-content">
<!-- 输入参数段 --> <!-- 输入参数段 -->
<ToolDataSection <ToolDataSection
v-if="'toolInput' in event" v-if="props.hasValidToolInput(event)"
title="输入参数" title="输入参数"
:data="event.toolInput" :data="event.toolInput"
type="input" type="input"
...@@ -48,14 +66,13 @@ ...@@ -48,14 +66,13 @@
<!-- 输出结果段 --> <!-- 输出结果段 -->
<ToolDataSection <ToolDataSection
v-if="'toolOutput' in event" v-if="props.hasValidToolOutput(event)"
title="输出结果" title="输出结果"
:data="event.toolOutput" :data="event.toolOutput"
type="output" type="output"
/> />
</div> </div>
</div> </div>
<div v-if="event.metadata" class="event-metadata"> <div v-if="event.metadata" class="event-metadata">
<div v-for="(value, key) in event.metadata" :key="key" class="metadata-item"> <div v-for="(value, key) in event.metadata" :key="key" class="metadata-item">
<span class="metadata-key">{{ key }}:</span> <span class="metadata-key">{{ key }}:</span>
...@@ -71,306 +88,46 @@ ...@@ -71,306 +88,46 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, nextTick, onMounted, onUnmounted } from 'vue' import { computed, onMounted, watch } from 'vue'
import ToolDataSection from './ToolDataSection.vue' import ToolDataSection from './ToolDataSection.vue'
import { formatToolData } from '../utils/functionUtils' import type { TimelineEvent } from '../types/timeline'
import { useContentExpansion } from '../composables/useContentExpansion'
interface TimelineEvent { import { truncateTitle } from '../utils/timelineUtils'
type: 'thought' | 'action' | 'observation' | 'result' | 'error' | 'tool_call' | 'tool_result' | 'tool_error' | 'embed'
title: string // 定义组件属性
content?: string const props = defineProps<{
metadata?: Record<string, any> events: TimelineEvent[]
timestamp: number getEventTypeLabel: (type: string) => string
toolName?: string formatTime: (timestamp: number) => string
toolAction?: string getExpandedState: (index: number) => boolean
toolInput?: Record<string, any> toggleExpand: (index: number) => void
toolOutput?: any isToolEventType: (type: string) => boolean
toolStatus?: string hasValidToolInput: (event: TimelineEvent) => boolean
executionTime?: number hasValidToolOutput: (event: TimelineEvent) => boolean
embedUrl?: string onClearTimeline: () => void
embedType?: string }>()
embedTitle?: string
embedHtmlContent?: string // 计算反转后的事件列表(最新事件在顶部)
} const reversedEvents = computed(() => [...props.events].reverse())
// 为每个事件维护展开状态 // 使用内容展开管理hook
const expandedStates = ref<Record<number, boolean>>({}) const {
getContentExpandedState,
// 获取事件的展开状态,确保始终返回布尔值 setContentRef,
const getExpandedState = (index: number): boolean => { toggleContentExpand,
return !!expandedStates.value[index] shouldShowToggle,
} updateLineCounts
} = useContentExpansion(props)
const events = ref<TimelineEvent[]>([])
const timelineContainer = ref<HTMLElement>() // 在组件挂载时更新行数计数
// 切换事件详细信息的展开状态
const toggleExpand = (index: number) => {
expandedStates.value = {
...expandedStates.value,
[index]: !expandedStates.value[index]
}
}
// 事件类型标签映射
const EVENT_TYPE_LABELS: Record<string, string> = {
'thought': '💭 思考',
'action': '🎬 行动',
'observation': '👀 观察',
'result': '✅ 结果',
'error': '❌ 错误',
'tool_call': '🔧 工具调用',
'tool_result': '📤 工具结果',
'tool_error': '⚠️ 工具错误',
'embed': '🌐 网页预览'
}
// 获取事件类型标签
const getEventTypeLabel = (type: string): string => {
return EVENT_TYPE_LABELS[type] || type
}
// 格式化时间
const formatTime = (timestamp: number): string => {
const date = new Date(timestamp)
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${hours}:${minutes}:${seconds}`
}
// 检查是否为Empty数据
const isEmpty = (data: any): boolean => {
if (data === null || data === undefined) return true;
if (typeof data === 'string') return data.trim().length === 0;
if (Array.isArray(data)) return data.length === 0;
if (typeof data === 'object') return Object.keys(data).length === 0;
return false;
};
// 检查是否有工具数据
const hasToolData = (event: TimelineEvent): boolean => {
// 检查字段是否存在且值不为Empty
const hasInput = 'toolInput' in event && !isEmpty(event.toolInput);
const hasOutput = 'toolOutput' in event && !isEmpty(event.toolOutput);
return hasInput || hasOutput;
};
// 强化地检查是否有工具数据(应用于界面渲染)
const isToolDataVisible = (event: TimelineEvent): boolean => {
if (event.type !== 'tool_call' && event.type !== 'tool_result' && event.type !== 'tool_error') {
return false;
}
return hasToolData(event);
};
// 添加时间轴事件
const addEvent = (event: Omit<TimelineEvent, 'timestamp'> & { timestamp?: number }) => {
// 对于thinking事件,检查是否与上一个事件内容相同,避免重复添加
if (event.type === 'thought' && events.value.length > 0) {
const lastEvent = events.value[events.value.length - 1]
if (lastEvent.type === 'thought' && lastEvent.content === event.content) {
// 如果内容相同,不添加重复事件
return
}
}
const newIndex = events.value.length;
const finalEvent = {
...event,
timestamp: event.timestamp || Date.now(),
...(('toolInput' in event) ? { toolInput: event.toolInput } : {}),
...(('toolOutput' in event) ? { toolOutput: event.toolOutput } : {})
} as TimelineEvent;
events.value.push(finalEvent);
// 初始化新事件的展开状态
expandedStates.value[newIndex] = false;
scrollToBottom()
}
// 滚动到底部
const scrollToBottom = async () => {
await nextTick()
if (timelineContainer.value) {
timelineContainer.value.scrollTop = timelineContainer.value.scrollHeight
}
}
// 清除时间轴
const clearTimeline = () => {
events.value = []
expandedStates.value = {}
}
// 暴露方法供父组件调用
defineExpose({
addEvent,
clearTimeline
})
onMounted(() => { onMounted(() => {
// SSE连接重试相关变量 updateLineCounts()
let eventSource: EventSource | null = null;
let retryCount = 0;
const maxRetries = 5;
const retryDelay = 3000; // 3秒
// 建立SSE连接的函数
const connectSSE = () => {
// 从localStorage获取token
const token = localStorage.getItem('token')
// 构造带认证参数的URL
let eventSourceUrl = '/api/v1/agent/timeline-events'
if (token) {
eventSourceUrl += `?token=${encodeURIComponent(token)}`
}
// 监听工作面板事件
eventSource = new EventSource(eventSourceUrl)
eventSource.addEventListener('message', (event) => {
try {
const data = JSON.parse(event.data)
// 构建事件标题
let title = data.title || '事件'
if (data.type === 'tool_call' && data.toolName) {
title = `调用工具: ${data.toolName}`
} else if (data.type === 'tool_result' && data.toolName) {
title = `${data.toolName} 执行成功`
} else if (data.type === 'tool_error' && data.toolName) {
title = `${data.toolName} 执行失败`
}
// 构建元数据
const metadata: Record<string, any> = data.metadata || {}
if (data.type === 'tool_call' || data.type === 'tool_result' || data.type === 'tool_error') {
if (data.toolName) metadata['工具'] = data.toolName
if (data.toolAction) metadata['操作'] = data.toolAction
if (data.toolInput) metadata['输入'] = JSON.stringify(data.toolInput).substring(0, 100)
if (data.toolOutput) metadata['输出'] = String(data.toolOutput).substring(0, 100)
if (data.toolStatus) metadata['状态'] = data.toolStatus
if (data.executionTime) metadata['耗时'] = `${data.executionTime}ms`
} else if (data.type === 'embed') {
if (data.embedUrl) metadata['URL'] = data.embedUrl
if (data.embedType) metadata['类型'] = data.embedType
if (data.embedTitle) title = data.embedTitle
}
const timelineEventData = {
type: data.type || 'observation',
title: title,
content: data.content,
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
toolName: data.toolName,
toolAction: data.toolAction,
...(('toolInput' in data) ? { toolInput: data.toolInput } : {}),
...(('toolOutput' in data) ? { toolOutput: data.toolOutput } : {}),
toolStatus: data.toolStatus,
executionTime: data.executionTime,
embedUrl: data.embedUrl,
embedType: data.embedType,
embedTitle: data.embedTitle,
embedHtmlContent: data.embedHtmlContent,
timestamp: data.timestamp
}
addEvent(timelineEventData)
// 触发embed事件给父组件
if (data.type === 'embed' && data.embedUrl) {
window.dispatchEvent(new CustomEvent('embed-event', {
detail: {
url: data.embedUrl,
type: data.embedType,
title: data.embedTitle,
htmlContent: data.embedHtmlContent
}
}))
}
// 重置重试计数
retryCount = 0;
} catch (err) {
console.error('解析时间轴事件失败:', err)
}
})
eventSource.addEventListener('error', (error) => {
console.error('[SSE] 连接错误:', error);
// 关闭当前连接
if (eventSource) {
eventSource.close();
eventSource = null;
}
// 如果重试次数未达到最大值,尝试重新连接
if (retryCount < maxRetries) {
retryCount++;
console.log(`[SSE] 尝试重新连接 (${retryCount}/${maxRetries})...`);
setTimeout(connectSSE, retryDelay);
} else {
console.error('[SSE] 达到最大重试次数,停止重连');
// 可以在这里触发一个全局事件通知用户连接失败
window.dispatchEvent(new CustomEvent('sse-connection-failed'));
}
});
// 监听连接成功事件
eventSource.addEventListener('open', () => {
console.log('[SSE] 连接已建立');
retryCount = 0; // 重置重试计数
});
};
// 初始连接
connectSSE();
// 在组件卸载时清理连接
onUnmounted(() => {
if (eventSource) {
eventSource.close();
eventSource = null;
}
});
// 监听来自ChatArea的思考事件
const handleTimelineEvent = (e: CustomEvent) => {
const eventData = e.detail
addEvent({
type: eventData.type || 'thought',
title: eventData.title || '思考过程',
content: eventData.content,
toolName: eventData.toolName,
toolAction: eventData.toolAction,
toolInput: eventData.toolInput,
toolOutput: eventData.toolOutput,
toolStatus: eventData.toolStatus,
executionTime: eventData.executionTime,
embedUrl: eventData.embedUrl,
embedType: eventData.embedType,
embedTitle: eventData.embedTitle,
embedHtmlContent: eventData.embedHtmlContent,
metadata: eventData.metadata,
timestamp: eventData.timestamp
})
}
window.addEventListener('timeline-event', handleTimelineEvent as EventListener)
// 在组件卸载时移除事件监听器
onUnmounted(() => {
window.removeEventListener('timeline-event', handleTimelineEvent as EventListener)
})
}) })
// 监听事件变化,重新计算行数
watch(() => props.events, () => {
updateLineCounts()
}, { deep: true })
</script> </script>
<style scoped> <style scoped>
...@@ -402,7 +159,7 @@ onMounted(() => { ...@@ -402,7 +159,7 @@ onMounted(() => {
.timeline-container { .timeline-container {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: var(--spacing-4); padding: var(--spacing-3);
min-height: 0; /* 允许容器收缩 */ min-height: 0; /* 允许容器收缩 */
} }
...@@ -444,9 +201,9 @@ onMounted(() => { ...@@ -444,9 +201,9 @@ onMounted(() => {
.timeline-item { .timeline-item {
position: relative; position: relative;
margin-bottom: var(--spacing-4); margin-bottom: var(--spacing-2);
display: flex; display: flex;
gap: var(--spacing-3); gap: var(--spacing-2);
} }
.timeline-dot { .timeline-dot {
...@@ -499,7 +256,8 @@ onMounted(() => { ...@@ -499,7 +256,8 @@ onMounted(() => {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-2); gap: var(--spacing-2);
margin-bottom: var(--spacing-2); margin-bottom: var(--spacing-1);
line-height: 1.2;
} }
.event-type-badge { .event-type-badge {
...@@ -568,36 +326,77 @@ onMounted(() => { ...@@ -568,36 +326,77 @@ onMounted(() => {
} }
.event-title { .event-title {
font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-medium);
color: var(--text-primary); color: var(--text-primary);
margin-bottom: var(--spacing-2); font-size: var(--font-size-xs);
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.event-content { .event-content {
margin: var(--spacing-2) 0; margin: var(--spacing-1) 0;
background-color: var(--bg-tertiary); background-color: var(--bg-tertiary);
border-radius: var(--border-radius-md); border-radius: var(--border-radius-sm);
overflow-x: auto; overflow-x: hidden;
border: 1px solid var(--border-color-light);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
} }
.event-content pre { .content-text-wrapper {
cursor: pointer;
}
.content-text {
margin: 0; margin: 0;
padding: var(--spacing-3); padding: var(--spacing-2);
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
color: var(--text-primary); color: var(--text-primary);
font-family: var(--font-family-mono); font-family: var(--font-family-mono);
background-color: transparent; background-color: transparent;
line-height: var(--line-height-snug); line-height: var(--line-height-snug);
white-space: pre-wrap;
word-wrap: break-word;
max-height: calc(2 * var(--line-height-snug) * 1em);
overflow: hidden;
transition: max-height 0.3s ease;
position: relative;
} }
.event-content code { .content-text.collapsed {
all: unset; display: -webkit-box;
font-family: var(--font-family-mono); -webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
position: relative;
}
.content-text.collapsed::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1.2em;
background: linear-gradient(to bottom, transparent, var(--bg-tertiary));
}
.content-text.expanded {
max-height: none;
transition: max-height 0.3s ease;
}
.content-toggle {
padding: var(--spacing-1) var(--spacing-2);
text-align: center;
color: var(--primary-color);
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
color: var(--text-primary); cursor: pointer;
line-height: var(--line-height-snug); user-select: none;
white-space: pre-wrap; background-color: rgba(102, 126, 234, 0.05);
word-wrap: break-word; border-top: 1px solid var(--border-color-light);
transition: background-color 0.2s ease;
} }
/* JSON显示区域 */ /* JSON显示区域 */
...@@ -628,9 +427,9 @@ onMounted(() => { ...@@ -628,9 +427,9 @@ onMounted(() => {
/* 工具调用详情样式 */ /* 工具调用详情样式 */
.tool-details { .tool-details {
margin: var(--spacing-3) 0; margin: var(--spacing-2) 0;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: var(--border-radius-md); border-radius: var(--border-radius-sm);
overflow: hidden; overflow: hidden;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
} }
...@@ -640,7 +439,7 @@ onMounted(() => { ...@@ -640,7 +439,7 @@ onMounted(() => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: var(--spacing-2) var(--spacing-3); padding: var(--spacing-1) var(--spacing-2);
background-color: var(--bg-secondary); background-color: var(--bg-secondary);
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
...@@ -651,8 +450,12 @@ onMounted(() => { ...@@ -651,8 +450,12 @@ onMounted(() => {
background-color: var(--bg-tertiary); background-color: var(--bg-tertiary);
} }
.content-toggle:hover {
background-color: rgba(102, 126, 234, 0.1);
}
.toggle-text { .toggle-text {
font-size: var(--font-size-sm); font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
color: var(--text-primary); color: var(--text-primary);
} }
...@@ -664,6 +467,7 @@ onMounted(() => { ...@@ -664,6 +467,7 @@ onMounted(() => {
.detail-content { .detail-content {
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
padding: var(--spacing-1);
} }
.tool-input, .tool-output { .tool-input, .tool-output {
...@@ -672,13 +476,13 @@ onMounted(() => { ...@@ -672,13 +476,13 @@ onMounted(() => {
.tool-input .detail-title { .tool-input .detail-title {
background-color: rgba(24, 144, 255, 0.1); background-color: rgba(24, 144, 255, 0.1);
padding: var(--spacing-2) var(--spacing-3); padding: var(--spacing-1) var(--spacing-2);
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
} }
.tool-output .detail-title { .tool-output .detail-title {
background-color: rgba(82, 196, 26, 0.1); background-color: rgba(82, 196, 26, 0.1);
padding: var(--spacing-2) var(--spacing-3); padding: var(--spacing-1) var(--spacing-2);
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
} }
...@@ -690,9 +494,9 @@ onMounted(() => { ...@@ -690,9 +494,9 @@ onMounted(() => {
.tool-input pre, .tool-output pre { .tool-input pre, .tool-output pre {
margin: 0; margin: 0;
padding: var(--spacing-2); padding: var(--spacing-1);
background-color: var(--bg-secondary); background-color: var(--bg-secondary);
border-radius: var(--border-radius-base); border-radius: var(--border-radius-sm);
overflow-x: auto; overflow-x: auto;
} }
...@@ -700,22 +504,22 @@ onMounted(() => { ...@@ -700,22 +504,22 @@ onMounted(() => {
font-family: var(--font-family-mono); font-family: var(--font-family-mono);
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
color: var(--text-primary); color: var(--text-primary);
line-height: var(--line-height-normal); line-height: var(--line-height-tight);
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
} }
.event-metadata { .event-metadata {
margin-top: var(--spacing-2); margin-top: var(--spacing-1);
padding: var(--spacing-2); padding: var(--spacing-1);
background-color: var(--bg-tertiary); background-color: var(--bg-tertiary);
border-radius: var(--border-radius-md); border-radius: var(--border-radius-sm);
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
} }
.metadata-item { .metadata-item {
display: flex; display: flex;
gap: var(--spacing-2); gap: var(--spacing-1);
margin-bottom: var(--spacing-1); margin-bottom: var(--spacing-1);
} }
......
// 事件去重服务
export class EventDeduplicationService {
// 用于跟踪已添加的事件的Set,防止重复
private eventHashSet: Set<string> = new Set();
/**
* 检查是否为重复事件
* @param event 当前事件
* @returns 是否为重复事件
*/
isDuplicateEvent(event: any): boolean {
// 对于某些关键事件类型,我们允许重复显示(如错误事件)
const criticalEventTypes = ['error', 'result'];
if (criticalEventTypes.includes(event.type)) {
return false;
}
// 生成事件的唯一标识符
const eventHash = this.generateEventHash(event);
if (this.eventHashSet.has(eventHash)) {
return true;
}
// 将事件哈希添加到Set中
this.eventHashSet.add(eventHash);
// 限制Set大小以避免内存泄漏
if (this.eventHashSet.size > 1000) {
// 删除最早的100个条目
const iterator = this.eventHashSet.values();
for (let i = 0; i < 100; i++) {
const value = iterator.next();
if (!value.done) {
this.eventHashSet.delete(value.value);
}
}
}
return false;
}
/**
* 生成事件的唯一标识符
* @param event 事件对象
* @returns 事件哈希值
*/
private generateEventHash(event: any): string {
// 确保有时间戳
const timestamp = event.timestamp || Date.now();
// 对于工具事件,使用类型+工具名+工具操作+时间戳作为标识
if (event.type && event.type.startsWith('tool_') && event.toolName && event.toolAction) {
return `${event.type}-${event.toolName}-${event.toolAction}-${timestamp}`;
}
// 对于嵌入事件,使用URL+时间戳作为标识
if (event.type === 'embed' && event.embedUrl) {
return `embed-${event.embedUrl}-${timestamp}`;
}
// 对于其他事件,使用类型+标题+时间戳作为标识
return `${event.type}-${event.title || ''}-${timestamp}`;
}
/**
* 清除事件哈希集合
*/
clearEventHashSet(): void {
this.eventHashSet.clear();
}
}
\ No newline at end of file
// 统一的事件处理服务
import type { TimelineEvent } from '../types/timeline';
export class EventProcessingService {
/**
* 标准化事件对象
* @param event 原始事件数据
* @returns 标准化的事件对象
*/
normalizeEvent(event: any): TimelineEvent {
// 确保时间戳存在
const timestamp = event.timestamp || Date.now();
// 根据事件类型创建相应类型的事件对象
switch (event.type) {
case 'thought':
return {
type: 'thought',
title: event.title || '思考事件',
content: event.content || '',
thinkingType: event.thinkingType,
metadata: event.metadata,
timestamp
};
case 'tool_call':
return {
type: 'tool_call',
title: event.title || '工具调用',
toolName: event.toolName || '',
toolAction: event.toolAction || '',
toolInput: event.toolInput,
toolStatus: event.toolStatus,
metadata: event.metadata,
timestamp
};
case 'tool_result':
return {
type: 'tool_result',
title: event.title || '工具结果',
toolName: event.toolName || '',
toolAction: event.toolAction || '',
toolOutput: event.toolOutput,
toolStatus: event.toolStatus,
executionTime: event.executionTime,
metadata: event.metadata,
timestamp
};
case 'tool_error':
return {
type: 'tool_error',
title: event.title || '工具错误',
toolName: event.toolName || '',
errorMessage: event.errorMessage || '',
errorCode: event.errorCode,
metadata: event.metadata,
timestamp
};
case 'embed':
return {
type: 'embed',
title: event.title || '嵌入内容',
embedUrl: event.embedUrl || '',
embedType: event.embedType,
embedTitle: event.embedTitle,
embedHtmlContent: event.embedHtmlContent,
metadata: event.metadata,
timestamp
};
default:
return {
type: event.type || 'thought',
title: event.title || '未命名事件',
metadata: event.metadata,
timestamp
};
}
}
/**
* 处理事件类型转换
* @param event 事件对象
* @returns 处理后的事件对象
*/
processEventType(event: any): any {
// 处理thinking类型的事件,如果是final_answer则转换为result类型
const processedEvent = { ...event };
if (processedEvent.type === 'thought' && processedEvent.title === '最终答案') {
processedEvent.type = 'result';
}
return processedEvent;
}
}
\ No newline at end of file
// 对象池服务
export class ObjectPoolService<T> {
private pool: T[] = [];
private factory: () => T;
private resetter?: (obj: T) => void;
private maxSize: number;
constructor(factory: () => T, resetter?: (obj: T) => void, maxSize: number = 100) {
this.factory = factory;
this.resetter = resetter;
this.maxSize = maxSize;
}
/**
* 从对象池获取对象
* @returns 对象实例
*/
acquire(): T {
if (this.pool.length > 0) {
return this.pool.pop()!;
}
return this.factory();
}
/**
* 将对象归还到对象池
* @param obj 对象实例
*/
release(obj: T): void {
if (this.resetter) {
this.resetter(obj);
}
if (this.pool.length < this.maxSize) {
this.pool.push(obj);
}
}
/**
* 清空对象池
*/
clear(): void {
this.pool = [];
}
/**
* 获取对象池当前大小
* @returns 对象池大小
*/
size(): number {
return this.pool.length;
}
}
\ No newline at end of file
// 优化的事件处理服务
import { ObjectPoolService } from './ObjectPoolService';
import type { TimelineEvent } from '../types/timeline';
export class OptimizedEventProcessingService {
private eventObjectPool: ObjectPoolService<Record<string, any>>;
private MAX_POOL_SIZE: number = 100;
private totalEventsProcessed: number = 0;
private totalEventsReused: number = 0;
constructor() {
// 创建对象池用于事件对象
this.eventObjectPool = new ObjectPoolService<Record<string, any>>(
() => ({}), // 工厂函数创建空对象
(obj) => { // 重置函数清空对象属性
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
delete obj[key];
}
}
},
this.MAX_POOL_SIZE
);
}
/**
* 标准化事件对象(使用对象池优化)
* @param event 原始事件数据
* @returns 标准化的事件对象
*/
normalizeEvent(event: any): TimelineEvent {
// 从对象池获取对象
const normalizedEvent = this.eventObjectPool.acquire();
try {
// 确保时间戳存在
const timestamp = event.timestamp || Date.now();
// 根据事件类型创建相应类型的事件对象
switch (event.type) {
case 'thought':
Object.assign(normalizedEvent, {
type: 'thought',
title: event.title || '思考事件',
content: event.content || '',
thinkingType: event.thinkingType,
metadata: event.metadata,
timestamp
});
break;
case 'tool_call':
Object.assign(normalizedEvent, {
type: 'tool_call',
title: event.title || '工具调用',
toolName: event.toolName || '',
toolAction: event.toolAction || '',
toolInput: event.toolInput,
toolStatus: event.toolStatus,
metadata: event.metadata,
timestamp
});
break;
case 'tool_result':
Object.assign(normalizedEvent, {
type: 'tool_result',
title: event.title || '工具结果',
toolName: event.toolName || '',
toolAction: event.toolAction || '',
toolOutput: event.toolOutput,
toolStatus: event.toolStatus,
executionTime: event.executionTime,
metadata: event.metadata,
timestamp
});
break;
case 'tool_error':
Object.assign(normalizedEvent, {
type: 'tool_error',
title: event.title || '工具错误',
toolName: event.toolName || '',
errorMessage: event.errorMessage || '',
errorCode: event.errorCode,
metadata: event.metadata,
timestamp
});
break;
case 'embed':
Object.assign(normalizedEvent, {
type: 'embed',
title: event.title || '嵌入内容',
embedUrl: event.embedUrl || '',
embedType: event.embedType,
embedTitle: event.embedTitle,
embedHtmlContent: event.embedHtmlContent,
metadata: event.metadata,
timestamp
});
break;
default:
Object.assign(normalizedEvent, {
type: event.type || 'thought',
title: event.title || '未命名事件',
metadata: event.metadata,
timestamp
});
break;
}
this.totalEventsProcessed++;
return normalizedEvent as TimelineEvent;
} catch (error) {
// 如果出现错误,将对象归还到池中
this.eventObjectPool.release(normalizedEvent);
throw error;
}
}
/**
* 处理完事件后,将对象归还到池中
* @param obj 事件对象
*/
releaseEventObject(obj: Record<string, any>): void {
if (obj && typeof obj === 'object') {
// 清空对象属性
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
delete obj[key];
}
}
// 如果对象池未满,将对象放回池中
if (this.eventObjectPool.size() < this.MAX_POOL_SIZE) {
this.eventObjectPool.release(obj);
this.totalEventsReused++;
}
}
}
/**
* 获取性能统计信息
* @returns 性能统计信息
*/
getPerformanceStats(): { totalProcessed: number; totalReused: number; reuseRate: number } {
const reuseRate = this.totalEventsProcessed > 0
? (this.totalEventsReused / this.totalEventsProcessed) * 100
: 0;
return {
totalProcessed: this.totalEventsProcessed,
totalReused: this.totalEventsReused,
reuseRate: parseFloat(reuseRate.toFixed(2))
};
}
/**
* 清空统计信息
*/
clearStats(): void {
this.totalEventsProcessed = 0;
this.totalEventsReused = 0;
}
}
\ No newline at end of file
# 前端服务层优化说明
## 概述
本文档介绍了前端服务层的一系列优化措施,旨在提高代码质量、可维护性和性能。
## 优化内容
### 1. 统一类型定义
- **文件**: `types/timeline.ts`
- **功能**: 统一定义了所有时间轴相关的事件类型和标签映射
- **优势**: 避免类型重复定义,提高类型安全性
### 2. 工具函数库
- **文件**: `utils/timelineUtils.ts`
- **功能**: 包含时间轴相关的工具函数,如事件类型判断、输入输出验证等
- **优势**: 集中管理工具函数,避免重复实现
### 3. 类型守卫函数
- **文件**: `utils/typeGuards.ts`
- **功能**: 提供类型守卫函数,用于精确的类型检查
- **优势**: 提高类型安全性,减少运行时错误
### 4. 事件去重服务
- **文件**: `services/EventDeduplicationService.ts`
- **功能**: 统一处理事件去重逻辑
- **优势**: 避免重复事件处理,节省资源
### 5. 对象池服务
- **文件**: `services/ObjectPoolService.ts`
- **功能**: 提供通用的对象池实现
- **优势**: 减少对象创建和垃圾回收压力,提高性能
### 6. 优化的事件处理服务
- **文件**: `services/OptimizedEventProcessingService.ts`
- **功能**: 使用对象池优化事件对象的创建和管理
- **优势**: 提高性能,减少内存分配
### 7. 统一事件处理器
- **文件**: `services/UnifiedEventProcessor.ts`
- **功能**: 整合所有事件处理逻辑
- **优势**: 统一事件处理流程,便于维护
## 使用方法
### 1. 类型和工具函数使用
```typescript
import type { TimelineEvent } from '../types/timeline';
import { isToolEventType, hasValidToolInput } from '../utils/timelineUtils';
import { isThoughtEvent } from '../utils/typeGuards';
```
### 2. 事件处理服务使用
```typescript
import { UnifiedEventProcessor } from '../services/UnifiedEventProcessor';
const eventProcessor = new UnifiedEventProcessor();
eventProcessor.registerHandler((event: TimelineEvent) => {
// 处理事件
});
```
### 3. 性能监控
在TimelineContainer组件中提供了性能监控功能:
```typescript
// 显示性能统计信息
showPerformanceStats();
// 定期输出性能统计(每30秒)
// 在控制台查看: [TimelineContainer] 定期性能统计
```
## 性能优化效果
通过对象池和事件去重等优化措施,预期可以获得以下性能提升:
1. 减少对象创建次数,降低垃圾回收压力
2. 避免重复事件处理,节省CPU资源
3. 提高事件处理速度,改善用户体验
## 维护建议
1. 定期查看性能监控数据,评估优化效果
2. 新增事件类型时,需在`types/timeline.ts`中定义相应类型
3. 新增工具函数时,应添加到`utils/timelineUtils.ts`
4. 保持服务层的单一职责原则,避免功能交叉
\ No newline at end of file
// 统一事件处理器
import type { TimelineEvent } from '../types/timeline'
import { EventProcessingService } from './EventProcessingService'
import { EventDeduplicationService } from './EventDeduplicationService'
import { OptimizedEventProcessingService } from './OptimizedEventProcessingService'
export class UnifiedEventProcessor {
private eventProcessingService: EventProcessingService
private eventDeduplicationService: EventDeduplicationService
private optimizedEventProcessingService: OptimizedEventProcessingService
private eventHandlers: Array<(event: TimelineEvent) => void> = []
private processedEvents: TimelineEvent[] = []
constructor() {
this.eventProcessingService = new EventProcessingService()
this.eventDeduplicationService = new EventDeduplicationService()
this.optimizedEventProcessingService = new OptimizedEventProcessingService()
}
/**
* 处理接收到的原始事件数据
* @param rawData 原始事件数据
* @returns 处理后的标准化事件对象
*/
processRawEvent(rawData: any): TimelineEvent | null {
try {
// 验证数据
if (!rawData || typeof rawData !== 'object') {
console.warn('[UnifiedEventProcessor] 无效的事件数据:', rawData)
return null
}
// 检查是否为重复事件
if (this.eventDeduplicationService.isDuplicateEvent(rawData)) {
console.log('[UnifiedEventProcessor] 跳过重复事件:', rawData.type, rawData.title)
return null
}
// 处理事件类型转换
const processedEvent = this.eventProcessingService.processEventType(rawData)
// 标准化事件对象(使用优化的服务)
const normalizedEvent = this.optimizedEventProcessingService.normalizeEvent(processedEvent)
// 添加到已处理事件列表
this.processedEvents.push(normalizedEvent)
// 限制已处理事件列表大小以避免内存泄漏
if (this.processedEvents.length > 1000) {
this.processedEvents.shift()
}
return normalizedEvent
} catch (error) {
console.error('[UnifiedEventProcessor] 处理事件数据时发生错误:', error, '原始数据:', rawData)
return null
}
}
/**
* 注册事件处理器
* @param handler 事件处理器函数
*/
registerHandler(handler: (event: TimelineEvent) => void): void {
this.eventHandlers.push(handler)
}
/**
* 分发事件给所有注册的处理器
* @param event 事件对象
*/
dispatchEvent(event: TimelineEvent): void {
this.eventHandlers.forEach(handler => {
try {
handler(event)
} catch (error) {
console.error('[UnifiedEventProcessor] 事件处理器执行错误:', error)
}
})
}
/**
* 处理并分发事件
* @param rawData 原始事件数据
*/
processAndDispatch(rawData: any): void {
const event = this.processRawEvent(rawData)
if (event) {
this.dispatchEvent(event)
}
}
/**
* 清除已处理事件列表
*/
clearProcessedEvents(): void {
this.processedEvents = []
this.eventDeduplicationService.clearEventHashSet()
}
/**
* 获取性能统计信息
* @returns 性能统计信息
*/
getPerformanceStats(): { totalProcessed: number; totalReused: number; reuseRate: number } {
return this.optimizedEventProcessingService.getPerformanceStats();
}
}
\ No newline at end of file
// 统一的时间轴事件类型定义
export interface BaseTimelineEvent {
type: string;
title: string;
timestamp: number;
metadata?: Record<string, any>;
}
export interface ThoughtEvent extends BaseTimelineEvent {
content: string;
thinkingType?: string;
}
export interface ToolCallEvent extends BaseTimelineEvent {
toolName: string;
toolAction?: string;
toolInput?: any;
toolStatus: string;
}
export interface ToolResultEvent extends BaseTimelineEvent {
toolName: string;
toolAction?: string;
toolOutput?: any;
toolStatus: string;
executionTime?: number;
}
export interface ToolErrorEvent extends BaseTimelineEvent {
toolName: string;
errorMessage: string;
errorCode?: string;
}
export interface EmbedEvent extends BaseTimelineEvent {
embedUrl: string;
embedType?: string;
embedTitle: string;
embedHtmlContent?: string;
}
export type TimelineEvent =
| ThoughtEvent
| ToolCallEvent
| ToolResultEvent
| ToolErrorEvent
| EmbedEvent
| BaseTimelineEvent;
// 事件类型标签映射
export const eventTypeLabels: Record<string, string> = {
thought: '🧠 思考',
tool_call: '🔧 工具调用',
tool_result: '✅ 工具结果',
tool_error: '❌ 工具错误',
embed: '🌐 网页预览',
log: '📝 日志',
result: '🎯 最终答案',
observation: '🔍 观察'
};
\ No newline at end of file
// 时间轴工具函数库
/**
* 检查是否为工具事件类型
* @param type 事件类型
* @returns 是否为工具事件类型
*/
export function isToolEventType(type: string): boolean {
return ['tool_call', 'tool_result', 'tool_error'].includes(type);
}
/**
* 检查工具输入是否有效
* @param event 事件对象
* @returns 工具输入是否有效
*/
export function hasValidToolInput(event: any): boolean {
return event.type === 'tool_call' && event.toolInput !== null && event.toolInput !== undefined;
}
/**
* 检查工具输出是否有效
* @param event 事件对象
* @returns 工具输出是否有效
*/
export function hasValidToolOutput(event: any): boolean {
return event.type === 'tool_result' && event.toolOutput !== null && event.toolOutput !== undefined;
}
/**
* 截断标题
* @param title 标题
* @param maxLength 最大长度
* @returns 截断后的标题
*/
export function truncateTitle(title: string, maxLength: number = 30): string {
if (!title) return '';
return title.length > maxLength ? title.substring(0, maxLength) + '...' : title;
}
\ No newline at end of file
// 类型守卫函数
import type {
TimelineEvent,
ThoughtEvent,
ToolCallEvent,
ToolResultEvent,
ToolErrorEvent,
EmbedEvent
} from '../types/timeline';
/**
* 检查是否为思考事件
* @param event 事件对象
* @returns 是否为思考事件
*/
export function isThoughtEvent(event: TimelineEvent): event is ThoughtEvent {
return event.type === 'thought';
}
/**
* 检查是否为工具调用事件
* @param event 事件对象
* @returns 是否为工具调用事件
*/
export function isToolCallEvent(event: TimelineEvent): event is ToolCallEvent {
return event.type === 'tool_call';
}
/**
* 检查是否为工具结果事件
* @param event 事件对象
* @returns 是否为工具结果事件
*/
export function isToolResultEvent(event: TimelineEvent): event is ToolResultEvent {
return event.type === 'tool_result';
}
/**
* 检查是否为工具错误事件
* @param event 事件对象
* @returns 是否为工具错误事件
*/
export function isToolErrorEvent(event: TimelineEvent): event is ToolErrorEvent {
return event.type === 'tool_error';
}
/**
* 检查是否为嵌入事件
* @param event 事件对象
* @returns 是否为嵌入事件
*/
export function isEmbedEvent(event: TimelineEvent): event is EmbedEvent {
return event.type === 'embed';
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment