Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
P
Pangea-Agent
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
2
Merge Requests
2
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Gavin-Group
Pangea-Agent
Commits
7211c835
Commit
7211c835
authored
Dec 22, 2025
by
王舵
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'main' into feature/take-road
parents
c107c059
e87c7566
Changes
12
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
1300 additions
and
344 deletions
+1300
-344
WorkPanel和Event模块优化方案.md
WorkPanel和Event模块优化方案.md
+290
-0
TimelineContainer.vue
frontend/src/components/TimelineContainer.vue
+124
-0
TimelinePanel.vue
frontend/src/components/TimelinePanel.vue
+148
-344
EventDeduplicationService.ts
frontend/src/services/EventDeduplicationService.ts
+71
-0
EventProcessingService.ts
frontend/src/services/EventProcessingService.ts
+100
-0
ObjectPoolService.ts
frontend/src/services/ObjectPoolService.ts
+53
-0
OptimizedEventProcessingService.ts
frontend/src/services/OptimizedEventProcessingService.ts
+167
-0
README.md
frontend/src/services/README.md
+85
-0
UnifiedEventProcessor.ts
frontend/src/services/UnifiedEventProcessor.ts
+108
-0
timeline.ts
frontend/src/types/timeline.ts
+60
-0
timelineUtils.ts
frontend/src/utils/timelineUtils.ts
+39
-0
typeGuards.ts
frontend/src/utils/typeGuards.ts
+55
-0
No files found.
WorkPanel和Event模块优化方案.md
0 → 100644
View file @
7211c835
# 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
frontend/src/components/TimelineContainer.vue
0 → 100644
View file @
7211c835
<
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
frontend/src/components/TimelinePanel.vue
View file @
7211c835
...
@@ -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
);
}
}
...
...
frontend/src/services/EventDeduplicationService.ts
0 → 100644
View file @
7211c835
// 事件去重服务
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
frontend/src/services/EventProcessingService.ts
0 → 100644
View file @
7211c835
// 统一的事件处理服务
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
frontend/src/services/ObjectPoolService.ts
0 → 100644
View file @
7211c835
// 对象池服务
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
frontend/src/services/OptimizedEventProcessingService.ts
0 → 100644
View file @
7211c835
// 优化的事件处理服务
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
frontend/src/services/README.md
0 → 100644
View file @
7211c835
# 前端服务层优化说明
## 概述
本文档介绍了前端服务层的一系列优化措施,旨在提高代码质量、可维护性和性能。
## 优化内容
### 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
frontend/src/services/UnifiedEventProcessor.ts
0 → 100644
View file @
7211c835
// 统一事件处理器
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
frontend/src/types/timeline.ts
0 → 100644
View file @
7211c835
// 统一的时间轴事件类型定义
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
frontend/src/utils/timelineUtils.ts
0 → 100644
View file @
7211c835
// 时间轴工具函数库
/**
* 检查是否为工具事件类型
* @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
frontend/src/utils/typeGuards.ts
0 → 100644
View file @
7211c835
// 类型守卫函数
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
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment