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
8a629996
Commit
8a629996
authored
Dec 23, 2025
by
高如斌
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
1
parent
8bcd41e5
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
4700 additions
and
552 deletions
+4700
-552
.gitignore
.gitignore
+3
-0
package-lock.json
frontend/package-lock.json
+4046
-138
package.json
frontend/package.json
+3
-0
ChatArea.vue
frontend/src/components/ChatArea.vue
+474
-363
FormRender.vue
frontend/src/components/FormRender.vue
+102
-0
WorkArea.vue
frontend/src/components/WorkArea.vue
+67
-51
main.ts
frontend/src/main.ts
+5
-0
No files found.
.gitignore
View file @
8a629996
...
@@ -202,6 +202,9 @@ frontend/.env.development.local
...
@@ -202,6 +202,9 @@ frontend/.env.development.local
frontend/.env.test.local
frontend/.env.test.local
frontend/.env.production.local
frontend/.env.production.local
# Kiro IDE files
.kiro/
# OS generated files
# OS generated files
Thumbs.db
Thumbs.db
.DS_Store
.DS_Store
...
...
frontend/package-lock.json
View file @
8a629996
This source diff could not be displayed because it is too large. You can
view the blob
instead.
frontend/package.json
View file @
8a629996
...
@@ -19,6 +19,7 @@
...
@@ -19,6 +19,7 @@
"highlight.js"
:
"^11.9.0"
,
"highlight.js"
:
"^11.9.0"
,
"marked"
:
"^17.0.1"
,
"marked"
:
"^17.0.1"
,
"pako"
:
"^2.1.0"
,
"pako"
:
"^2.1.0"
,
"pangea-ui"
:
"^0.14.2-beta.9"
,
"pinia"
:
"^2.1.7"
,
"pinia"
:
"^2.1.7"
,
"snabbdom"
:
"^3.6.3"
,
"snabbdom"
:
"^3.6.3"
,
"vue"
:
"^3.4.0"
,
"vue"
:
"^3.4.0"
,
...
@@ -26,11 +27,13 @@
...
@@ -26,11 +27,13 @@
"vue-router"
:
"^4.3.0"
"vue-router"
:
"^4.3.0"
},
},
"devDependencies"
:
{
"devDependencies"
:
{
"@arco-design/web-vue"
:
"^2.55.3"
,
"@types/dompurify"
:
"^3.0.5"
,
"@types/dompurify"
:
"^3.0.5"
,
"@types/node"
:
"^20.10.0"
,
"@types/node"
:
"^20.10.0"
,
"@vitejs/plugin-vue"
:
"^5.0.0"
,
"@vitejs/plugin-vue"
:
"^5.0.0"
,
"eslint"
:
"^8.55.0"
,
"eslint"
:
"^8.55.0"
,
"eslint-plugin-vue"
:
"^9.19.0"
,
"eslint-plugin-vue"
:
"^9.19.0"
,
"less"
:
"^4.2.0"
,
"terser"
:
"^5.44.1"
,
"terser"
:
"^5.44.1"
,
"typescript"
:
"^5.9.3"
,
"typescript"
:
"^5.9.3"
,
"vite"
:
"^5.0.0"
,
"vite"
:
"^5.0.0"
,
...
...
frontend/src/components/ChatArea.vue
View file @
8a629996
...
@@ -2,13 +2,27 @@
...
@@ -2,13 +2,27 @@
<div
class=
"chat-area"
>
<div
class=
"chat-area"
>
<!-- 顶部Agent选择和操作栏 -->
<!-- 顶部Agent选择和操作栏 -->
<div
class=
"chat-header"
>
<div
class=
"chat-header"
>
<el-select
v-model=
"selectedAgent"
@
change=
"handleAgentChange"
placeholder=
"选择智能体"
class=
"agent-select"
>
<el-select
<el-option
v-for=
"agent in agents"
:key=
"agent.id"
:label=
"agent.name"
:value=
"agent.id"
>
v-model=
"selectedAgent"
@
change=
"handleAgentChange"
placeholder=
"选择智能体"
class=
"agent-select"
>
<el-option
v-for=
"agent in agents"
:key=
"agent.id"
:label=
"agent.name"
:value=
"agent.id"
>
<span>
{{
agent
.
name
}}
(ID:
{{
agent
.
id
}}
)
</span>
<span>
{{
agent
.
name
}}
(ID:
{{
agent
.
id
}}
)
</span>
</el-option>
</el-option>
</el-select>
</el-select>
<el-tooltip
content=
"清空对话"
>
<el-tooltip
content=
"清空对话"
>
<el-button
@
click=
"clearMessages"
:disabled=
"messages.length === 0"
circle
>
<el-button
@
click=
"clearMessages"
:disabled=
"messages.length === 0"
circle
>
<span>
🗑️
</span>
<span>
🗑️
</span>
</el-button>
</el-button>
</el-tooltip>
</el-tooltip>
...
@@ -70,271 +84,300 @@
...
@@ -70,271 +84,300 @@
</
template
>
</
template
>
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
ref
,
nextTick
,
onMounted
,
defineExpose
}
from
'vue'
import
{
ref
,
nextTick
,
onMounted
,
defineExpose
}
from
"vue"
;
import
{
ElMessage
,
ElMessageBox
}
from
'element-plus'
import
{
ElMessage
,
ElMessageBox
}
from
"element-plus"
;
import
MessageItem
from
'./MessageItem.vue'
import
MessageItem
from
"./MessageItem.vue"
;
import
request
from
'@/utils/request'
import
request
from
"@/utils/request"
;
import
{
useRoute
}
from
'vue-router'
import
{
useRoute
}
from
"vue-router"
;
interface
Message
{
interface
Message
{
content
:
string
content
:
string
;
isUser
:
boolean
isUser
:
boolean
;
agentId
?:
string
agentId
?:
string
;
timestamp
:
number
timestamp
:
number
;
isStreaming
:
boolean
isStreaming
:
boolean
;
hasError
?:
boolean
hasError
?:
boolean
;
originalMessage
?:
string
originalMessage
?:
string
;
}
}
interface
Agent
{
interface
Agent
{
id
:
string
id
:
string
;
name
:
string
name
:
string
;
[
key
:
string
]:
any
[
key
:
string
]:
any
;
}
}
const
selectedAgent
=
ref
<
string
>
(
''
)
const
selectedAgent
=
ref
<
string
>
(
""
);
const
agents
=
ref
<
Agent
[]
>
([])
const
agents
=
ref
<
Agent
[]
>
([])
;
const
messages
=
ref
<
Message
[]
>
([])
const
messages
=
ref
<
Message
[]
>
([])
;
const
inputMessage
=
ref
(
''
)
const
inputMessage
=
ref
(
""
);
const
isLoading
=
ref
(
false
)
const
isLoading
=
ref
(
false
)
;
const
messagesContainer
=
ref
<
HTMLElement
>
()
const
messagesContainer
=
ref
<
HTMLElement
>
()
;
// 获取当前路由
// 获取当前路由
const
route
=
useRoute
()
const
route
=
useRoute
()
;
// 全局维护SSE流超时计时器引用,确保能够正确清除
// 全局维护SSE流超时计时器引用,确保能够正确清除
let
streamTimeoutTimer
:
ReturnType
<
typeof
setTimeout
>
|
null
=
null
let
streamTimeoutTimer
:
ReturnType
<
typeof
setTimeout
>
|
null
=
null
;
// 获取Agent列表
// 获取Agent列表
const
loadAgents
=
async
()
=>
{
const
loadAgents
=
async
()
=>
{
try
{
try
{
const
res
=
await
request
.
get
(
'/agent'
)
const
res
=
await
request
.
get
(
"/agent"
);
// 检查响应结构并正确提取数据
// 检查响应结构并正确提取数据
if
(
res
&&
res
.
data
&&
res
.
data
.
code
===
200
)
{
if
(
res
&&
res
.
data
&&
res
.
data
.
code
===
200
)
{
agents
.
value
=
Array
.
isArray
(
res
.
data
.
data
)
?
res
.
data
.
data
:
[]
agents
.
value
=
Array
.
isArray
(
res
.
data
.
data
)
?
res
.
data
.
data
:
[]
;
}
else
{
}
else
{
agents
.
value
=
[]
agents
.
value
=
[]
;
}
}
console
.
log
(
'[Agent列表加载] 获取到的Agent列表:'
,
agents
.
value
)
console
.
log
(
"[Agent列表加载] 获取到的Agent列表:"
,
agents
.
value
);
if
(
agents
.
value
.
length
>
0
&&
!
selectedAgent
.
value
)
{
if
(
agents
.
value
.
length
>
0
&&
!
selectedAgent
.
value
)
{
selectedAgent
.
value
=
agents
.
value
[
0
].
id
selectedAgent
.
value
=
agents
.
value
[
0
].
id
;
}
}
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
'获取Agent列表失败:'
,
error
)
console
.
error
(
"获取Agent列表失败:"
,
error
);
agents
.
value
=
[]
agents
.
value
=
[]
;
ElMessage
.
error
(
'获取Agent列表失败'
)
ElMessage
.
error
(
"获取Agent列表失败"
);
}
}
}
}
;
// 获取Agent名称
// 获取Agent名称
const
getAgentName
=
(
agentId
?:
string
):
string
=>
{
const
getAgentName
=
(
agentId
?:
string
):
string
=>
{
if
(
!
agentId
)
return
'Assistant'
if
(
!
agentId
)
return
"Assistant"
;
const
agent
=
agents
.
value
.
find
(
a
=>
a
.
id
===
agentId
)
const
agent
=
agents
.
value
.
find
(
(
a
)
=>
a
.
id
===
agentId
);
return
agent
?.
name
||
'Assistant'
return
agent
?.
name
||
"Assistant"
;
}
}
;
// 处理Agent切换
// 处理Agent切换
const
handleAgentChange
=
()
=>
{
const
handleAgentChange
=
()
=>
{
clearMessages
()
clearMessages
()
;
}
}
;
// 清空消息
// 清空消息
const
clearMessages
=
()
=>
{
const
clearMessages
=
()
=>
{
// 清除可能存在的超时计时器
// 清除可能存在的超时计时器
clearStreamTimeout
()
clearStreamTimeout
();
ElMessageBox
.
confirm
(
'确认清空所有对话吗?此操作无法撤销。'
,
'提示'
,
{
ElMessageBox
.
confirm
(
"确认清空所有对话吗?此操作无法撤销。"
,
"提示"
,
{
confirmButtonText
:
'清空'
,
confirmButtonText
:
"清空"
,
cancelButtonText
:
'取消'
,
cancelButtonText
:
"取消"
,
type
:
'warning'
type
:
"warning"
,
}).
then
(()
=>
{
messages
.
value
=
[]
ElMessage
.
success
(
'对话已清空'
)
}).
catch
(()
=>
{
// 用户取消
})
})
}
.
then
(()
=>
{
messages
.
value
=
[];
ElMessage
.
success
(
"对话已清空"
);
})
.
catch
(()
=>
{
// 用户取消
});
};
// 合并重复的历史消息加载逻辑
// 合并重复的历史消息加载逻辑
const
loadHistoryMessagesInternal
=
async
(
agentId
:
string
)
=>
{
const
loadHistoryMessagesInternal
=
async
(
agentId
:
string
)
=>
{
try
{
try
{
// 添加更严格的类型和存在性检查
// 添加更严格的类型和存在性检查
if
(
!
agentId
||
typeof
agentId
!==
'string'
||
agentId
.
trim
()
===
''
)
{
if
(
!
agentId
||
typeof
agentId
!==
"string"
||
agentId
.
trim
()
===
""
)
{
console
.
log
(
'[历史消息加载] 没有指定有效的Agent ID,跳过加载历史记录'
)
console
.
log
(
"[历史消息加载] 没有指定有效的Agent ID,跳过加载历史记录"
);
messages
.
value
=
[]
messages
.
value
=
[]
;
return
return
;
}
}
// 验证agentId是否存在于可用agents列表中
// 验证agentId是否存在于可用agents列表中
const
isValidAgent
=
agents
.
value
.
some
(
agent
=>
agent
.
id
===
agentId
.
trim
())
const
isValidAgent
=
agents
.
value
.
some
(
(
agent
)
=>
agent
.
id
===
agentId
.
trim
()
);
// 如果agents列表为空,我们仍然尝试加载历史消息,因为可能是初始化时agents还未加载完成
// 如果agents列表为空,我们仍然尝试加载历史消息,因为可能是初始化时agents还未加载完成
if
(
agents
.
value
.
length
>
0
&&
!
isValidAgent
)
{
if
(
agents
.
value
.
length
>
0
&&
!
isValidAgent
)
{
console
.
log
(
`[历史消息加载] 指定的Agent ID '
${
agentId
}
' 不存在于可用agents列表中`
)
console
.
log
(
`[历史消息加载] 指定的Agent ID '
${
agentId
}
' 不存在于可用agents列表中`
);
// 不立即返回,而是继续尝试加载历史消息
// 不立即返回,而是继续尝试加载历史消息
}
}
// 设置选中的Agent
// 设置选中的Agent
selectedAgent
.
value
=
agentId
.
trim
()
selectedAgent
.
value
=
agentId
.
trim
()
;
// 调用后端API获取历史对话记录
// 调用后端API获取历史对话记录
console
.
log
(
`[历史消息加载] 正在请求API: /memory/dialogue/agent/
${
agentId
.
trim
()}
`
)
console
.
log
(
const
res
=
await
request
.
get
(
`/memory/dialogue/agent/
${
encodeURIComponent
(
agentId
.
trim
())}
`
)
`[历史消息加载] 正在请求API: /memory/dialogue/agent/
${
agentId
.
trim
()}
`
);
console
.
log
(
'[历史消息加载] API响应:'
,
res
)
const
res
=
await
request
.
get
(
`/memory/dialogue/agent/
${
encodeURIComponent
(
agentId
.
trim
())}
`
);
console
.
log
(
"[历史消息加载] API响应:"
,
res
);
// 验证响应结构:包括code、data和messages数组
// 验证响应结构:包括code、data和messages数组
if
(
!
res
||
!
res
.
data
)
{
if
(
!
res
||
!
res
.
data
)
{
console
.
error
(
'[历史消息加载] 响应对象为空或无效'
)
console
.
error
(
"[历史消息加载] 响应对象为空或无效"
);
messages
.
value
=
[]
messages
.
value
=
[]
;
return
return
;
}
}
const
{
code
,
data
}
=
res
.
data
const
{
code
,
data
}
=
res
.
data
;
console
.
log
(
`[历史消息加载] 响应code:
${
code
}
, data:`
,
data
)
console
.
log
(
`[历史消息加载] 响应code:
${
code
}
, data:`
,
data
)
;
if
(
code
===
200
&&
data
&&
typeof
data
===
'object'
)
{
if
(
code
===
200
&&
data
&&
typeof
data
===
"object"
)
{
const
messagesArray
=
data
.
messages
const
messagesArray
=
data
.
messages
;
// 检查messages是否是数组
// 检查messages是否是数组
if
(
!
Array
.
isArray
(
messagesArray
))
{
if
(
!
Array
.
isArray
(
messagesArray
))
{
console
.
warn
(
`[历史消息加载] messages字段不是数组,类型为:
${
typeof
messagesArray
}
,值为:`
,
messagesArray
)
console
.
warn
(
messages
.
value
=
[]
`[历史消息加载] messages字段不是数组,类型为:
${
typeof
messagesArray
}
,值为:`
,
return
messagesArray
);
messages
.
value
=
[];
return
;
}
}
// 检查是否有消息
// 检查是否有消息
if
(
messagesArray
.
length
===
0
)
{
if
(
messagesArray
.
length
===
0
)
{
console
.
log
(
'[历史消息加载] 没有找到历史消息,API返回空数组'
)
console
.
log
(
"[历史消息加载] 没有找到历史消息,API返回空数组"
);
messages
.
value
=
[]
messages
.
value
=
[]
;
return
return
;
}
}
// 转换消息格式
// 转换消息格式
const
historyMessages
=
messagesArray
.
map
((
msg
:
any
)
=>
{
const
historyMessages
=
messagesArray
// 验证消息对象的必要字段
.
map
((
msg
:
any
)
=>
{
if
(
!
msg
||
typeof
msg
!==
'object'
)
{
// 验证消息对象的必要字段
console
.
warn
(
'[历史消息加载] 发现无效的消息对象:'
,
msg
)
if
(
!
msg
||
typeof
msg
!==
"object"
)
{
return
null
console
.
warn
(
"[历史消息加载] 发现无效的消息对象:"
,
msg
);
}
return
null
;
}
const
sender
=
msg
.
sender
const
content
=
msg
.
content
const
sender
=
msg
.
sender
;
const
time
=
msg
.
time
const
content
=
msg
.
content
;
const
time
=
msg
.
time
;
if
(
!
content
)
{
console
.
warn
(
'[历史消息加载] 消息内容为空:'
,
msg
)
if
(
!
content
)
{
return
null
console
.
warn
(
"[历史消息加载] 消息内容为空:"
,
msg
);
}
return
null
;
}
return
{
content
:
content
,
return
{
isUser
:
sender
===
'user'
,
content
:
content
,
agentId
:
agentId
.
trim
(),
isUser
:
sender
===
"user"
,
timestamp
:
time
?
(
typeof
time
===
'string'
?
parseInt
(
time
)
:
new
Date
(
time
).
getTime
())
:
Date
.
now
(),
agentId
:
agentId
.
trim
(),
isStreaming
:
false
timestamp
:
time
}
?
typeof
time
===
"string"
}).
filter
((
msg
:
any
)
=>
msg
!==
null
)
// 过滤掉无效消息
?
parseInt
(
time
)
:
new
Date
(
time
).
getTime
()
:
Date
.
now
(),
isStreaming
:
false
,
};
})
.
filter
((
msg
:
any
)
=>
msg
!==
null
);
// 过滤掉无效消息
// 修复:确保消息按时间顺序排列(最新的消息在最后)
// 修复:确保消息按时间顺序排列(最新的消息在最后)
historyMessages
.
sort
((
a
:
any
,
b
:
any
)
=>
a
.
timestamp
-
b
.
timestamp
)
historyMessages
.
sort
((
a
:
any
,
b
:
any
)
=>
a
.
timestamp
-
b
.
timestamp
)
;
messages
.
value
=
historyMessages
messages
.
value
=
historyMessages
;
console
.
log
(
`[历史消息加载] 成功加载
${
historyMessages
.
length
}
条历史消息`
)
console
.
log
(
`[历史消息加载] 成功加载
${
historyMessages
.
length
}
条历史消息`
)
;
// 滚动到底部
// 滚动到底部
await
nextTick
()
await
nextTick
()
;
if
(
messagesContainer
.
value
)
{
if
(
messagesContainer
.
value
)
{
messagesContainer
.
value
.
scrollTop
=
messagesContainer
.
value
.
scrollHeight
messagesContainer
.
value
.
scrollTop
=
messagesContainer
.
value
.
scrollHeight
;
}
}
}
else
{
}
else
{
console
.
error
(
`[历史消息加载] 响应格式错误或错误状态。code:
${
code
}
, data:`
,
data
)
console
.
error
(
messages
.
value
=
[]
`[历史消息加载] 响应格式错误或错误状态。code:
${
code
}
, data:`
,
data
);
messages
.
value
=
[];
}
}
}
catch
(
error
:
any
)
{
}
catch
(
error
:
any
)
{
console
.
error
(
'[历史消息加载] 加载历史对话记录失败'
,
error
)
console
.
error
(
"[历史消息加载] 加载历史对话记录失败"
,
error
);
messages
.
value
=
[]
// 出错时清空消息列表
messages
.
value
=
[]
;
// 出错时清空消息列表
// 记录详细的错误信息便于调试
// 记录详细的错误信息便于调试
if
(
error
.
response
)
{
if
(
error
.
response
)
{
console
.
error
(
'[历史消息加载] HTTP响应错误 - 状态码:'
,
error
.
response
.
status
)
console
.
error
(
console
.
error
(
'[历史消息加载] HTTP响应错误 - 数据:'
,
error
.
response
.
data
)
"[历史消息加载] HTTP响应错误 - 状态码:"
,
console
.
error
(
'[历史消息加载] HTTP响应错误 - 请求URL:'
,
error
.
config
?.
url
)
error
.
response
.
status
);
console
.
error
(
"[历史消息加载] HTTP响应错误 - 数据:"
,
error
.
response
.
data
);
console
.
error
(
"[历史消息加载] HTTP响应错误 - 请求URL:"
,
error
.
config
?.
url
);
}
else
if
(
error
.
request
)
{
}
else
if
(
error
.
request
)
{
console
.
error
(
'[历史消息加载] 网络请求错误 - 没有收到响应'
)
console
.
error
(
"[历史消息加载] 网络请求错误 - 没有收到响应"
);
console
.
error
(
'[历史消息加载] 请求配置:'
,
error
.
request
)
console
.
error
(
"[历史消息加载] 请求配置:"
,
error
.
request
);
}
else
{
}
else
{
console
.
error
(
'[历史消息加载] 错误信息:'
,
error
.
message
)
console
.
error
(
"[历史消息加载] 错误信息:"
,
error
.
message
);
console
.
error
(
'[历史消息加载] 错误堆栈:'
,
error
.
stack
)
console
.
error
(
"[历史消息加载] 错误堆栈:"
,
error
.
stack
);
}
}
}
}
}
}
;
// 加载历史对话记录
// 加载历史对话记录
const
loadHistoryMessages
=
async
()
=>
{
const
loadHistoryMessages
=
async
()
=>
{
// 首先尝试从路由参数获取agentId
// 首先尝试从路由参数获取agentId
let
agentId
=
route
.
query
.
agentId
as
string
let
agentId
=
route
.
query
.
agentId
as
string
;
// 如果路由参数中没有agentId,则使用下拉框中选中的值
// 如果路由参数中没有agentId,则使用下拉框中选中的值
if
(
!
agentId
||
typeof
agentId
!==
'string'
||
agentId
.
trim
()
===
''
)
{
if
(
!
agentId
||
typeof
agentId
!==
"string"
||
agentId
.
trim
()
===
""
)
{
agentId
=
selectedAgent
.
value
agentId
=
selectedAgent
.
value
;
}
}
// 如果仍然没有有效的agentId,则不加载历史消息
// 如果仍然没有有效的agentId,则不加载历史消息
if
(
!
agentId
||
typeof
agentId
!==
'string'
||
agentId
.
trim
()
===
''
)
{
if
(
!
agentId
||
typeof
agentId
!==
"string"
||
agentId
.
trim
()
===
""
)
{
console
.
log
(
'[历史消息加载] 没有指定有效的Agent ID,跳过加载历史记录'
)
console
.
log
(
"[历史消息加载] 没有指定有效的Agent ID,跳过加载历史记录"
);
return
return
;
}
}
await
loadHistoryMessagesInternal
(
agentId
);
await
loadHistoryMessagesInternal
(
agentId
);
}
}
;
// 通过指定agentId加载历史对话记录(供父组件调用)
// 通过指定agentId加载历史对话记录(供父组件调用)
const
loadHistoryByAgentId
=
async
(
agentId
:
string
)
=>
{
const
loadHistoryByAgentId
=
async
(
agentId
:
string
)
=>
{
await
loadHistoryMessagesInternal
(
agentId
);
await
loadHistoryMessagesInternal
(
agentId
);
}
}
;
// 处理重试
// 处理重试
const
handleRetry
=
async
(
index
:
number
)
=>
{
const
handleRetry
=
async
(
index
:
number
)
=>
{
// 清除可能存在的超时计时器
// 清除可能存在的超时计时器
clearStreamTimeout
()
clearStreamTimeout
()
;
const
msg
=
messages
.
value
[
index
]
const
msg
=
messages
.
value
[
index
]
;
if
(
!
msg
.
hasError
||
msg
.
isUser
)
return
if
(
!
msg
.
hasError
||
msg
.
isUser
)
return
;
// 移除错误消息
// 移除错误消息
messages
.
value
.
splice
(
index
,
1
)
messages
.
value
.
splice
(
index
,
1
)
;
// 重新发送原始消息
// 重新发送原始消息
inputMessage
.
value
=
msg
.
originalMessage
||
''
inputMessage
.
value
=
msg
.
originalMessage
||
""
;
await
sendMessage
()
await
sendMessage
()
;
}
}
;
// 统一的超时计时器清理函数
// 统一的超时计时器清理函数
const
clearStreamTimeout
=
()
=>
{
const
clearStreamTimeout
=
()
=>
{
if
(
streamTimeoutTimer
)
{
if
(
streamTimeoutTimer
)
{
clearTimeout
(
streamTimeoutTimer
)
clearTimeout
(
streamTimeoutTimer
)
;
streamTimeoutTimer
=
null
streamTimeoutTimer
=
null
;
}
}
}
}
;
// 防抖函数
// 防抖函数
const
debounce
=
(
func
:
Function
,
wait
:
number
)
=>
{
const
debounce
=
(
func
:
Function
,
wait
:
number
)
=>
{
let
timeout
:
ReturnType
<
typeof
setTimeout
>
let
timeout
:
ReturnType
<
typeof
setTimeout
>
;
return
function
executedFunction
(...
args
:
any
[])
{
return
function
executedFunction
(...
args
:
any
[])
{
const
later
=
()
=>
{
const
later
=
()
=>
{
clearTimeout
(
timeout
)
clearTimeout
(
timeout
)
;
func
(...
args
)
func
(...
args
)
;
}
}
;
clearTimeout
(
timeout
)
clearTimeout
(
timeout
)
;
timeout
=
setTimeout
(
later
,
wait
)
timeout
=
setTimeout
(
later
,
wait
)
;
}
}
;
}
}
;
// 自动滚动到底部(使用防抖优化性能)
// 自动滚动到底部(使用防抖优化性能)
const
scrollToBottom
=
debounce
(
async
()
=>
{
const
scrollToBottom
=
debounce
(
async
()
=>
{
await
nextTick
()
await
nextTick
()
;
if
(
messagesContainer
.
value
)
{
if
(
messagesContainer
.
value
)
{
messagesContainer
.
value
.
scrollTop
=
messagesContainer
.
value
.
scrollHeight
messagesContainer
.
value
.
scrollTop
=
messagesContainer
.
value
.
scrollHeight
;
}
}
},
100
)
},
100
)
;
// 【修复工具函数】处理转义序列,将\n、\t等转义形式还原为实际字符
// 【修复工具函数】处理转义序列,将\n、\t等转义形式还原为实际字符
const
unescapeString
=
(
str
:
string
):
string
=>
{
const
unescapeString
=
(
str
:
string
):
string
=>
{
...
@@ -342,32 +385,40 @@ const unescapeString = (str: string): string => {
...
@@ -342,32 +385,40 @@ const unescapeString = (str: string): string => {
try
{
try
{
// 处理转义序列:将转义形式还原为实际字符
// 处理转义序列:将转义形式还原为实际字符
let
unescaped
=
str
let
unescaped
=
str
.
replace
(
/
\\
n/g
,
'
\
n'
)
// \n 转换为换行符
.
replace
(
/
\\
n/g
,
"
\n
"
)
// \n 转换为换行符
.
replace
(
/
\\
r/g
,
'
\
r'
)
// \r 转换为回车符
.
replace
(
/
\\
r/g
,
"
\
r"
)
// \r 转换为回车符
.
replace
(
/
\\
t/g
,
'
\
t'
)
// \t 转换为制表符
.
replace
(
/
\\
t/g
,
"
\
t"
)
// \t 转换为制表符
.
replace
(
/
\\
b/g
,
'
\
b'
)
// \b 转换为退格符
.
replace
(
/
\\
b/g
,
"
\
b"
)
// \b 转换为退格符
.
replace
(
/
\\
f/g
,
'
\
f'
)
// \f 转换为换页符
.
replace
(
/
\\
f/g
,
"
\
f"
)
// \f 转换为换页符
.
replace
(
/
\\\\
/g
,
'
\
\'
)
//
\\
转换为单个反斜杠
.
replace
(
/
\\\\
/g
,
"
\
\"
);
//
\\
转换为单个反斜杠
return unescaped;
return unescaped;
} catch (e) {
} catch (e) {
console.warn(
'
转义序列处理失败
:
'
, e);
console.warn(
"
转义序列处理失败
:
"
, e);
return str;
return str;
}
}
};
};
// 处理SSE数据行的通用函数
// 处理SSE数据行的通用函数
const processSSELine = async (line: string, accumulatedContentRef: { value: string }, hasFinalAnswerRef: { value: boolean }, currentEventRef: { value: string }, aiMessageIndex: number, resetStreamTimeout: () => void) => {
const processSSELine = async (
if (!line.trim()) return false
line: string,
accumulatedContentRef: { value: string },
if (line.startsWith('
event
:
')) {
hasFinalAnswerRef: { value: boolean },
currentEventRef.value = line.slice(6).trim()
currentEventRef: { value: string },
return false
aiMessageIndex: number,
} else if (line.startsWith('
data
:
')) {
resetStreamTimeout: () => void
) => {
if (!line.trim()) return false;
console.log("
line
", line);
if (line.startsWith("
event
:
")) {
currentEventRef.value = line.slice(6).trim();
return false;
} else if (line.startsWith("
data
:
")) {
try {
try {
const dataStr = line.startsWith(
'
data
:
') ? line.slice(6) : line.slice(5)
const dataStr = line.startsWith(
"
data
:
") ? line.slice(6) : line.slice(5);
// 修复:检查dataStr是否为空或只包含空白字符
// 修复:检查dataStr是否为空或只包含空白字符
if (!dataStr.trim()) {
if (!dataStr.trim()) {
return false
return false
;
}
}
// 尝试解析为JSON,如果失败则作为纯文本处理
// 尝试解析为JSON,如果失败则作为纯文本处理
let data;
let data;
...
@@ -376,162 +427,188 @@ const processSSELine = async (line: string, accumulatedContentRef: { value: stri
...
@@ -376,162 +427,188 @@ const processSSELine = async (line: string, accumulatedContentRef: { value: stri
} catch (e) {
} catch (e) {
// 不是JSON格式,将其作为token事件处理(用于兼容旧协议)
// 不是JSON格式,将其作为token事件处理(用于兼容旧协议)
data = {
data = {
type:
'
token
'
,
type:
"
token
"
,
token: dataStr
token: dataStr
,
};
};
}
}
// 修复:检查是否是JSON格式的API密钥错误消息
// 修复:检查是否是JSON格式的API密钥错误消息
if (typeof data ===
'
object
'
&& data !== null) {
if (typeof data ===
"
object
"
&& data !== null) {
// 检查是否包含API密钥错误信息
// 检查是否包含API密钥错误信息
const errorMessage = data.message || data.error || data.token || data.content || '';
const errorMessage =
if (errorMessage.includes('
请配置
API
密钥
')) {
data.message || data.error || data.token || data.content || "";
if (errorMessage.includes("
请配置
API
密钥
")) {
// 处理API密钥错误
// 处理API密钥错误
clearStreamTimeout();
clearStreamTimeout();
messages.value[aiMessageIndex].isStreaming = false;
messages.value[aiMessageIndex].isStreaming = false;
messages.value[aiMessageIndex].content =
'
[
错误
]
请配置
API
密钥
'
;
messages.value[aiMessageIndex].content =
"
[
错误
]
请配置
API
密钥
"
;
messages.value[aiMessageIndex].hasError = true;
messages.value[aiMessageIndex].hasError = true;
isLoading.value = false;
isLoading.value = false;
console.error(
'
[
SSE
解析错误
]
接收到
API
密钥错误消息
:
'
, dataStr);
console.error(
"
[
SSE
解析错误
]
接收到
API
密钥错误消息
:
"
, dataStr);
return true; // 返回true表示流已完成
return true; // 返回true表示流已完成
}
}
}
}
// 特殊处理流结束标记
// 特殊处理流结束标记
if (dataStr.trim() ===
'
[
DONE
]
'
) {
if (dataStr.trim() ===
"
[
DONE
]
"
) {
clearStreamTimeout();
clearStreamTimeout();
messages.value[aiMessageIndex].isStreaming = false;
messages.value[aiMessageIndex].isStreaming = false;
isLoading.value = false;
isLoading.value = false;
console.log(
'
[
SSE
流结束
]
收到
[
DONE
]
标记
'
);
console.log(
"
[
SSE
流结束
]
收到
[
DONE
]
标记
"
);
return true; // 返回true表示流已完成
return true; // 返回true表示流已完成
}
}
// 检查是否是纯文本错误消息
// 检查是否是纯文本错误消息
// 修复:正确处理API密钥错误,检查是否为JSON格式的错误消息
// 修复:正确处理API密钥错误,检查是否为JSON格式的错误消息
if ((dataStr.startsWith('
[
错误
]
') || dataStr.includes('
请配置
API
密钥
')) && !dataStr.trim().startsWith('
{
')) {
if (
(dataStr.startsWith("
[
错误
]
") || dataStr.includes("
请配置
API
密钥
")) &&
!dataStr.trim().startsWith("
{
")
) {
// 处理纯文本错误消息,包括API密钥错误
// 处理纯文本错误消息,包括API密钥错误
clearStreamTimeout();
clearStreamTimeout();
messages.value[aiMessageIndex].isStreaming = false;
messages.value[aiMessageIndex].isStreaming = false;
messages.value[aiMessageIndex].content = dataStr.startsWith('
[
错误
]
') ? dataStr : '
[
错误
]
' + dataStr;
messages.value[aiMessageIndex].content = dataStr.startsWith("
[
错误
]
")
? dataStr
: "
[
错误
]
" + dataStr;
messages.value[aiMessageIndex].hasError = true;
messages.value[aiMessageIndex].hasError = true;
isLoading.value = false;
isLoading.value = false;
console.warn(
'
[
SSE
解析错误
]
接收到错误消息
:
'
, dataStr);
console.warn(
"
[
SSE
解析错误
]
接收到错误消息
:
"
, dataStr);
return true; // 返回true表示流已完成
return true; // 返回true表示流已完成
}
}
// 新增:处理JSON格式但包含API密钥错误的情况
// 新增:处理JSON格式但包含API密钥错误的情况
if (typeof data ===
'
object
'
&& data !== null) {
if (typeof data ===
"
object
"
&& data !== null) {
const jsonData = data;
const jsonData = data;
// 检查各种可能包含错误信息的字段
// 检查各种可能包含错误信息的字段
const errorMsg = jsonData.message || jsonData.error || jsonData.token || jsonData.content || '';
const errorMsg =
if (errorMsg.includes('
请配置
API
密钥
')) {
jsonData.message ||
jsonData.error ||
jsonData.token ||
jsonData.content ||
"";
if (errorMsg.includes("
请配置
API
密钥
")) {
// 处理API密钥错误
// 处理API密钥错误
clearStreamTimeout();
clearStreamTimeout();
messages.value[aiMessageIndex].isStreaming = false;
messages.value[aiMessageIndex].isStreaming = false;
messages.value[aiMessageIndex].content =
'
[
错误
]
请配置
API
密钥
'
;
messages.value[aiMessageIndex].content =
"
[
错误
]
请配置
API
密钥
"
;
messages.value[aiMessageIndex].hasError = true;
messages.value[aiMessageIndex].hasError = true;
isLoading.value = false;
isLoading.value = false;
console.error(
'
[
SSE
解析错误
]
接收到
API
密钥错误消息
:
'
, dataStr);
console.error(
"
[
SSE
解析错误
]
接收到
API
密钥错误消息
:
"
, dataStr);
return true; // 返回true表示流已完成
return true; // 返回true表示流已完成
}
}
}
}
const eventType = currentEventRef.value || data.type;
const eventType = currentEventRef.value || data.type;
// 根据事件类型处理数据
// 根据事件类型处理数据
switch (eventType) {
switch (eventType) {
case
'
token
'
:
case
"
token
"
:
// 重置超时计时器,接收到token说明连接还活跃
// 重置超时计时器,接收到token说明连接还活跃
resetStreamTimeout();
resetStreamTimeout();
// 修复:对接收到的token进行反转义处理,䮵保换行符和格式化符号正常显示
// 修复:对接收到的token进行反转义处理,䮵保换行符和格式化符号正常显示
const processedToken = unescapeString(data.token ||
''
);
const processedToken = unescapeString(data.token ||
""
);
accumulatedContentRef.value += processedToken;
accumulatedContentRef.value += processedToken;
messages.value[aiMessageIndex].content = accumulatedContentRef.value;
messages.value[aiMessageIndex].content = accumulatedContentRef.value;
await scrollToBottom();
await scrollToBottom();
break;
break;
case
'
complete
'
:
case
"
complete
"
:
// 收到完成事件,清除超时计时器
// 收到完成事件,清除超时计时器
clearStreamTimeout();
clearStreamTimeout();
console.log(
'
[
SSE
完成事件
]
'
, data);
console.log(
"
[
SSE
完成事件
]
"
, data);
// 如果有完整文本内容,更新消息内容
// 如果有完整文本内容,更新消息内容
if (data.fullText) {
if (data.fullText) {
messages.value[aiMessageIndex].content = data.fullText;
messages.value[aiMessageIndex].content = data.fullText;
}
}
messages.value[aiMessageIndex].isStreaming = false;
messages.value[aiMessageIndex].isStreaming = false;
isLoading.value = false;
isLoading.value = false;
return true; // 返回true表示流已完成
return true; // 返回true表示流已完成
case
'
error
'
:
case
"
error
"
:
// 收到错误事件,清除超时计时器
// 收到错误事件,清除超时计时器
clearStreamTimeout();
clearStreamTimeout();
messages.value[aiMessageIndex].isStreaming = false;
messages.value[aiMessageIndex].isStreaming = false;
// 检查是否是API密钥错误的特殊提示
// 检查是否是API密钥错误的特殊提示
const errorMsg = data.message || data.error || data.token || data.content || '';
const errorMsg =
if (errorMsg.includes('
请配置
API
密钥
')) {
data.message || data.error || data.token || data.content || "";
messages.value[aiMessageIndex].content = '
[
错误
]
请配置
API
密钥
';
if (errorMsg.includes("
请配置
API
密钥
")) {
messages.value[aiMessageIndex].content = "
[
错误
]
请配置
API
密钥
";
} else {
} else {
messages.value[aiMessageIndex].content = `[错误] ${errorMsg || '
未知错误
'}`;
messages.value[aiMessageIndex].content = `[错误] ${
errorMsg || "
未知错误
"
}`;
}
}
messages.value[aiMessageIndex].hasError = true;
messages.value[aiMessageIndex].hasError = true;
isLoading.value = false;
isLoading.value = false;
// 记录错误日志便于调试
// 记录错误日志便于调试
console.error(
'
[
SSE
错误事件
]
'
, data);
console.error(
"
[
SSE
错误事件
]
"
, data);
return true; // 返回true表示流已完成
return true; // 返回true表示流已完成
case
'
thinking
'
:
case
"
thinking
"
:
// 处理思考事件,将其发送到时间轴面板
// 处理思考事件,将其发送到时间轴面板
const event = {
const event = {
type: '
thought
',
type: "
thought
",
title: data.thinkingType === '
final_answer
' ? '
最终答案
' : '
思考过程
',
title:
data.thinkingType === "
final_answer
" ? "
最终答案
" : "
思考过程
",
content: data.content,
content: data.content,
timestamp: data.timestamp
timestamp: data.timestamp
,
};
};
// 通过事件总线将事件发送到时间轴
// 通过事件总线将事件发送到时间轴
window.dispatchEvent(new CustomEvent('
timeline
-
event
', { detail: event }));
window.dispatchEvent(
new CustomEvent("
timeline
-
event
", { detail: event })
);
// 如果是最终答案,也应该显示在主要对话框中
// 如果是最终答案,也应该显示在主要对话框中
// 修复:确保最终答案只添加一次,避免重复显示
// 修复:确保最终答案只添加一次,避免重复显示
if (data.thinkingType === '
final_answer
' && !hasFinalAnswerRef.value) {
if (
data.thinkingType === "
final_answer
" &&
!hasFinalAnswerRef.value
) {
hasFinalAnswerRef.value = true;
hasFinalAnswerRef.value = true;
// 修复:这里不应该再添加到accumulatedContentRef.value,因为这会在流结束后再次设置导致重复
// 修复:这里不应该再添加到accumulatedContentRef.value,因为这会在流结束后再次设置导致重复
// accumulatedContentRef.value += data.content || '';
// accumulatedContentRef.value += data.content || '';
// 直接设置消息内容为最终答案
// 直接设置消息内容为最终答案
messages.value[aiMessageIndex].content = data.content ||
''
;
messages.value[aiMessageIndex].content = data.content ||
""
;
messages.value[aiMessageIndex].isStreaming = false;
messages.value[aiMessageIndex].isStreaming = false;
isLoading.value = false;
isLoading.value = false;
await scrollToBottom();
await scrollToBottom();
}
}
// 对于非最终答案的思考过程,不添加到主对话框中
// 对于非最终答案的思考过程,不添加到主对话框中
break;
break;
case
'
tool_call
'
:
case
"
tool_call
"
:
case
'
embed
'
:
case
"
embed
"
:
// 处理工具调用和嵌入事件,将其发送到时间轴面板
// 处理工具调用和嵌入事件,将其发送到时间轴面板
// 构建事件标题
// 构建事件标题
let title = data.title ||
'
事件
'
;
let title = data.title ||
"
事件
"
;
if (eventType ===
'
tool_call
'
&& data.toolName) {
if (eventType ===
"
tool_call
"
&& data.toolName) {
title = `调用工具: ${data.toolName}`;
title = `调用工具: ${data.toolName}`;
} else if (eventType ===
'
embed
'
&& data.embedTitle) {
} else if (eventType ===
"
embed
"
&& data.embedTitle) {
title = data.embedTitle;
title = data.embedTitle;
}
}
// 构建元数据
// 构建元数据
const metadata: Record<string, any> = data.metadata || {};
const metadata: Record<string, any> = data.metadata || {};
if (eventType === '
tool_call
') {
if (eventType === "
tool_call
") {
if (data.toolName) metadata['
工具
'] = data.toolName;
if (data.toolName) metadata["
工具
"] = data.toolName;
if (data.toolAction) metadata['
操作
'] = data.toolAction;
if (data.toolAction) metadata["
操作
"] = data.toolAction;
if (data.toolInput) metadata['
输入
'] = JSON.stringify(data.toolInput).substring(0, 100);
if (data.toolInput)
if (data.toolOutput) metadata['
输出
'] = String(data.toolOutput).substring(0, 100);
metadata["
输入
"] = JSON.stringify(data.toolInput).substring(
if (data.toolStatus) metadata['
状态
'] = data.toolStatus;
0,
if (data.executionTime) metadata['
耗时
'] = `${data.executionTime}ms`;
100
} else if (eventType === '
embed
') {
);
if (data.embedUrl) metadata['
URL
'] = data.embedUrl;
if (data.toolOutput)
if (data.embedType) metadata['
类型
'] = data.embedType;
metadata["
输出
"] = String(data.toolOutput).substring(0, 100);
if (data.toolStatus) metadata["
状态
"] = data.toolStatus;
if (data.executionTime)
metadata["
耗时
"] = `${data.executionTime}ms`;
} else if (eventType === "
embed
") {
if (data.embedUrl) metadata["
URL
"] = data.embedUrl;
if (data.embedType) metadata["
类型
"] = data.embedType;
}
}
const timelineEvent = {
const timelineEvent = {
type: eventType,
type: eventType,
title: title,
title: title,
...
@@ -547,52 +624,61 @@ const processSSELine = async (line: string, accumulatedContentRef: { value: stri
...
@@ -547,52 +624,61 @@ const processSSELine = async (line: string, accumulatedContentRef: { value: stri
embedType: data.embedType,
embedType: data.embedType,
embedTitle: data.embedTitle,
embedTitle: data.embedTitle,
embedHtmlContent: data.embedHtmlContent,
embedHtmlContent: data.embedHtmlContent,
timestamp: data.timestamp || Date.now()
timestamp: data.timestamp || Date.now()
,
};
};
// 通过事件总线将事件发送到时间轴
// 通过事件总线将事件发送到时间轴
console.log('
[
ChatArea
]
发送
timeline
-
event
事件
:
', timelineEvent);
console.log("
[
ChatArea
]
发送
timeline
-
event
事件
:
", timelineEvent);
window.dispatchEvent(new CustomEvent('
timeline
-
event
', { detail: timelineEvent }));
window.dispatchEvent(
new CustomEvent("
timeline
-
event
", { detail: timelineEvent })
);
// 对于embed事件,还需要触发embed-event事件
// 对于embed事件,还需要触发embed-event事件
if (eventType === '
embed
' && data.embedUrl) {
if (eventType === "
embed
" && data.embedUrl) {
window.dispatchEvent(new CustomEvent('
embed
-
event
', {
window.dispatchEvent(
detail: {
new CustomEvent("
embed
-
event
", {
url: data.embedUrl,
detail: {
type: data.embedType,
url: data.embedUrl,
title: data.embedTitle,
type: data.embedType,
htmlContent: data.embedHtmlContent
title: data.embedTitle,
}
htmlContent: data.embedHtmlContent,
}));
},
})
);
}
}
break;
break;
}
}
// 重置当前事件类型
// 重置当前事件类型
currentEventRef.value =
''
;
currentEventRef.value =
""
;
} catch (err) {
} catch (err) {
console.error('
[
SSE
解析错误
]
解析
SSE
数据失败,重置超时计时器
:
', err, '
原始行
:
', line)
console.error(
"
[
SSE
解析错误
]
解析
SSE
数据失败,重置超时计时器
:
",
err,
"
原始行
:
",
line
);
// 收到任何消息,都要重置超时计时器
// 收到任何消息,都要重置超时计时器
resetStreamTimeout()
resetStreamTimeout()
;
// 根据错误类型,决定是否继续处理或中断流
// 根据错误类型,决定是否继续处理或中断流
if (line.includes('"
type
":"
error
"') || line.includes('"
error
"')) {
if (line.includes('"
type
":"
error
"') || line.includes('"
error
"')) {
// 错误消息,继续处理
// 错误消息,继续处理
return false
return false
;
}
}
}
}
return false
return false
;
}
}
return false
return false
;
}
}
;
// 发送消息
// 发送消息
const sendMessage = async () => {
const sendMessage = async () => {
if (!selectedAgent.value || !inputMessage.value.trim()) {
if (!selectedAgent.value || !inputMessage.value.trim()) {
return
return
;
}
}
const userMessage = inputMessage.value.trim()
const userMessage = inputMessage.value.trim()
;
inputMessage.value =
''
inputMessage.value =
"";
// 添加用户消息
// 添加用户消息
messages.value.push({
messages.value.push({
...
@@ -600,121 +686,142 @@ const sendMessage = async () => {
...
@@ -600,121 +686,142 @@ const sendMessage = async () => {
isUser: true,
isUser: true,
agentId: selectedAgent.value,
agentId: selectedAgent.value,
timestamp: Date.now(),
timestamp: Date.now(),
isStreaming: false
isStreaming: false
,
})
})
;
await scrollToBottom()
await scrollToBottom()
;
// 添加AI消息容器(流式接收)
// 添加AI消息容器(流式接收)
const aiMessageIndex = messages.value.length
const aiMessageIndex = messages.value.length
;
messages.value.push({
messages.value.push({
content:
''
,
content:
""
,
isUser: false,
isUser: false,
agentId: selectedAgent.value,
agentId: selectedAgent.value,
timestamp: Date.now(),
timestamp: Date.now(),
isStreaming: true,
isStreaming: true,
hasError: false,
hasError: false,
originalMessage: userMessage
originalMessage: userMessage
,
})
})
;
isLoading.value = true
isLoading.value = true
;
let hasReceivedFirstToken = false // 标记是否接收到第一个token
let hasReceivedFirstToken = false
;
// 标记是否接收到第一个token
try {
try {
// 使用自定义的流式处理
// 使用自定义的流式处理
const token = localStorage.getItem(
'
token
')
const token = localStorage.getItem(
"
token
");
const abortController = new AbortController()
const abortController = new AbortController()
;
// 调用后端的流式API
// 调用后端的流式API
const response = await fetch(
const response = await fetch(
`/api/v1/agent/chat-stream?agentId=${selectedAgent.value}`,
`/api/v1/agent/chat-stream?agentId=${selectedAgent.value}`,
{
{
method:
'
POST
'
,
method:
"
POST
"
,
headers: {
headers: {
'
Content
-
Type
': '
application
/
json
'
,
"
Content
-
Type
": "
application
/
json
"
,
'
Authorization
': `Bearer ${token || ''}`
Authorization: `Bearer ${token || ""}`,
},
},
body: JSON.stringify({ message: userMessage }),
body: JSON.stringify({ message: userMessage }),
signal: abortController.signal
signal: abortController.signal
,
}
}
)
)
;
if (!response.ok) {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
throw new Error(`HTTP error! status: ${response.status}`)
;
}
}
const reader = response.body?.getReader()
const reader = response.body?.getReader()
;
if (!reader) {
if (!reader) {
throw new Error(
'
无法读取响应体
')
throw new Error(
"
无法读取响应体
");
}
}
const accumulatedContentRef = { value:
'' }
const accumulatedContentRef = { value:
"" };
const hasFinalAnswerRef = { value: false }
const hasFinalAnswerRef = { value: false }
;
const currentEventRef = { value:
'' }
const currentEventRef = { value:
"" };
const decoder = new TextDecoder()
const decoder = new TextDecoder()
;
let buffer =
''
let buffer =
"";
let isStreamComplete = false // 标记流是否已完成
let isStreamComplete = false
;
// 标记流是否已完成
const STREAM_TIMEOUT = 60000 // 60秒无流式消息则为超时
const STREAM_TIMEOUT = 60000
;
// 60秒无流式消息则为超时
// 设置超时检查
// 设置超时检查
const resetStreamTimeout = () => {
const resetStreamTimeout = () => {
clearStreamTimeout()
clearStreamTimeout()
;
streamTimeoutTimer = setTimeout(() => {
streamTimeoutTimer = setTimeout(() => {
if (!isStreamComplete) {
if (!isStreamComplete) {
isStreamComplete = true
isStreamComplete = true
;
reader.cancel()
reader.cancel()
;
messages.value[aiMessageIndex].isStreaming = false
messages.value[aiMessageIndex].isStreaming = false
;
isLoading.value = false
isLoading.value = false
;
// 提示用户是否要重试
// 提示用户是否要重试
ElMessage.warning(
'
流式输出超时,您可以点击重试按钮重新发送消息
')
ElMessage.warning(
"
流式输出超时,您可以点击重试按钮重新发送消息
");
// 显示重试按钮
// 显示重试按钮
messages.value[aiMessageIndex].content = '
[
错误
]
流式输出超时,请重试
'
messages.value[aiMessageIndex].content =
messages.value[aiMessageIndex].hasError = true
"
[
错误
]
流式输出超时,请重试
";
messages.value[aiMessageIndex].hasError = true;
}
}
}, STREAM_TIMEOUT);
}, STREAM_TIMEOUT);
}
}
;
resetStreamTimeout()
resetStreamTimeout()
;
while (true) {
while (true) {
if (isStreamComplete) break // 如果已收到complete事件,停止读取
if (isStreamComplete) break
;
// 如果已收到complete事件,停止读取
const { done, value } = await reader.read()
const { done, value } = await reader.read()
;
if (done) break
if (done) break
;
buffer += decoder.decode(value, { stream: true })
buffer += decoder.decode(value, { stream: true })
;
const lines = buffer.split(
'
\
n
')
const lines = buffer.split(
"
\
n
");
buffer = lines.pop() ||
''
buffer = lines.pop() ||
"";
for (const line of lines) {
for (const line of lines) {
const isComplete = await processSSELine(line, accumulatedContentRef, hasFinalAnswerRef, currentEventRef, aiMessageIndex, resetStreamTimeout)
const isComplete = await processSSELine(
line,
accumulatedContentRef,
hasFinalAnswerRef,
currentEventRef,
aiMessageIndex,
resetStreamTimeout
);
if (isComplete) {
if (isComplete) {
isStreamComplete = true
isStreamComplete = true
;
}
}
// 在接收到第一个token时,立即隐藏Skeleton加载动画
// 在接收到第一个token时,立即隐藏Skeleton加载动画
if (!hasReceivedFirstToken && (line.includes('
"type"
:
"token"
') || currentEventRef.value === '
token
')) {
if (
isLoading.value = false
!hasReceivedFirstToken &&
hasReceivedFirstToken = true
(line.includes('"
type
":"
token
"') || currentEventRef.value === "
token
")
) {
isLoading.value = false;
hasReceivedFirstToken = true;
}
}
}
}
}
}
// 处理残余的数据
// 处理残余的数据
if (buffer && !isStreamComplete) {
if (buffer && !isStreamComplete) {
const lines = buffer.split(
'
\
n
')
const lines = buffer.split(
"
\
n
");
for (const line of lines) {
for (const line of lines) {
const isComplete = await processSSELine(line, accumulatedContentRef, hasFinalAnswerRef, currentEventRef, aiMessageIndex, resetStreamTimeout)
const isComplete = await processSSELine(
line,
accumulatedContentRef,
hasFinalAnswerRef,
currentEventRef,
aiMessageIndex,
resetStreamTimeout
);
if (isComplete) {
if (isComplete) {
isStreamComplete = true
isStreamComplete = true
;
}
}
// 在接收到第一个token时,立即隐藏Skeleton加载动画
// 在接收到第一个token时,立即隐藏Skeleton加载动画
if (!hasReceivedFirstToken && (line.includes('
"type"
:
"token"
') || currentEventRef.value === '
token
')) {
if (
isLoading.value = false
!hasReceivedFirstToken &&
hasReceivedFirstToken = true
(line.includes('"
type
":"
token
"') || currentEventRef.value === "
token
")
) {
isLoading.value = false;
hasReceivedFirstToken = true;
}
}
}
}
}
}
...
@@ -722,58 +829,62 @@ const sendMessage = async () => {
...
@@ -722,58 +829,62 @@ const sendMessage = async () => {
// 修复:仅有在没有最终答案且没有错误的情况下才更新消息内容
// 修复:仅有在没有最终答案且没有错误的情况下才更新消息内容
// 如果已经有最终答案或错误信息,就不需要再更新消息内容了,避免重复显示
// 如果已经有最终答案或错误信息,就不需要再更新消息内容了,避免重复显示
if (!hasFinalAnswerRef.value && !messages.value[aiMessageIndex].hasError) {
if (!hasFinalAnswerRef.value && !messages.value[aiMessageIndex].hasError) {
messages.value[aiMessageIndex].content = accumulatedContentRef.value
messages.value[aiMessageIndex].content = accumulatedContentRef.value
;
}
}
// 确保最终状态正确
// 确保最终状态正确
messages.value[aiMessageIndex].isStreaming = false
messages.value[aiMessageIndex].isStreaming = false
;
// 设置isLoading为false,结束加载状态
// 设置isLoading为false,结束加载状态
isLoading.value = false
isLoading.value = false
;
// 清除超时计时器
// 清除超时计时器
clearStreamTimeout()
clearStreamTimeout()
;
// 确保滚动到底部
// 确保滚动到底部
await scrollToBottom()
await scrollToBottom()
;
} catch (error: any) {
} catch (error: any) {
// 清除超时计时器
// 清除超时计时器
clearStreamTimeout()
clearStreamTimeout()
;
// 判断是否是网络错误
// 判断是否是网络错误
let errorMessage =
'
[
错误
]
'
let errorMessage =
"
[
错误
]
";
// 检查是否是API密钥错误的特殊提示
// 检查是否是API密钥错误的特殊提示
if (error.message && (error.message.includes('
请配置
API
密钥
') || error.message.includes('
[
错误
]
请配置
API
密钥
'))) {
if (
errorMessage = '
[
错误
]
请配置
API
密钥
'
error.message &&
(error.message.includes("
请配置
API
密钥
") ||
error.message.includes("
[
错误
]
请配置
API
密钥
"))
) {
errorMessage = "
[
错误
]
请配置
API
密钥
";
} else if (error instanceof TypeError) {
} else if (error instanceof TypeError) {
errorMessage +=
'
网络获取失败,请检查你的网络连接
'
errorMessage +=
"
网络获取失败,请检查你的网络连接
";
} else if (error.name ===
'
AbortError
'
) {
} else if (error.name ===
"
AbortError
"
) {
errorMessage +=
'
请求已取消
'
errorMessage +=
"
请求已取消
";
} else if (error.message && error.message.includes(
'
处理超时
'
)) {
} else if (error.message && error.message.includes(
"
处理超时
"
)) {
errorMessage =
'
[
错误
]
服务器处理超时,请稍后重试
'
errorMessage =
"
[
错误
]
服务器处理超时,请稍后重试
";
} else if (error.message) {
} else if (error.message) {
errorMessage += error.message
errorMessage += error.message
;
} else {
} else {
errorMessage +=
'
一个未知错误发生
'
errorMessage +=
"
一个未知错误发生
";
}
}
messages.value[aiMessageIndex].content = errorMessage
messages.value[aiMessageIndex].content = errorMessage
;
messages.value[aiMessageIndex].isStreaming = false
messages.value[aiMessageIndex].isStreaming = false
;
messages.value[aiMessageIndex].hasError = true
messages.value[aiMessageIndex].hasError = true
;
isLoading.value = false
isLoading.value = false
;
}
}
}
}
;
onMounted(async () => {
onMounted(async () => {
await loadAgents()
await loadAgents()
;
// 等待下一个tick确保agents加载完成后再加载历史消息
// 等待下一个tick确保agents加载完成后再加载历史消息
await nextTick()
await nextTick()
;
loadHistoryMessages()
loadHistoryMessages()
;
})
})
;
// 暴露方法给父组件使用
// 暴露方法给父组件使用
defineExpose({
defineExpose({
loadHistoryByAgentId
loadHistoryByAgentId
,
})
})
;
</
script
>
</
script
>
<
style
scoped
>
<
style
scoped
>
...
...
frontend/src/components/FormRender.vue
0 → 100644
View file @
8a629996
<
template
>
<hi-page-template
ref=
"templateRef"
:json=
"json"
:open-intl=
"false"
></hi-page-template>
<div
class=
"button-wrap"
>
<a-button
type=
"primary"
@
click=
"submit"
>
提交
</a-button>
</div>
</
template
>
<
script
lang=
"ts"
setup
>
import
{
ref
}
from
"vue"
;
import
HiPageTemplate
from
"pangea-ui/hi-page-template"
;
const
templateRef
=
ref
();
const
submit
=
()
=>
{
templateRef
.
value
?.
ctx
.
validate
(
1
,
(
res
,
data
)
=>
{
console
.
log
(
res
,
data
);
});
};
const
json
=
{
pages
:
[
{
key
:
0
,
type
:
"default"
,
name
:
"默认页"
,
code
:
""
,
display
:
""
,
props
:
{
margin
:
"16px"
,
padding
:
"12px"
,
backgroundColor
:
"white"
,
display
:
{},
},
bindProps
:
{},
coms
:
[
{
key
:
1
,
type
:
"node"
,
name
:
"表单容器"
,
code
:
"HiFormContainer"
,
display
:
""
,
props
:
{
status
:
"default"
,
backgroundColor
:
"transparent"
,
layout
:
"horizontal"
,
size
:
"medium"
,
labelAlign
:
"right"
,
display
:
{},
borderRadius
:
{},
boxShadow
:
{},
loop
:
{
data
:
[],
},
},
bindProps
:
{},
coms
:
[
{
key
:
1766473421208
,
name
:
"输入框"
,
code
:
"HiInput"
,
props
:
{
title
:
"输入框"
,
status
:
"default"
,
placeholder
:
"请输入"
,
name
:
"INPUT_6CP8HIBK"
,
},
bindProps
:
{},
coms
:
[],
},
{
key
:
1766476676439
,
name
:
"日期"
,
code
:
"HiDatePicker"
,
props
:
{
title
:
"日期"
,
type
:
"date"
,
format
:
"YYYY-MM-DD"
,
status
:
"default"
,
name
:
"DATE_PA9TUPQQ"
,
},
bindProps
:
{},
},
],
},
],
},
],
params
:
[],
apis
:
[],
funcs
:
[],
pageTemplate
:
{},
};
</
script
>
<
style
scoped
>
.button-wrap
{
display
:
flex
;
justify-content
:
center
;
}
</
style
>
frontend/src/components/WorkArea.vue
View file @
8a629996
<
template
>
<
template
>
<div
class=
"work-area"
>
<div
class=
"work-area"
>
<el-tabs
v-model=
"activeTab"
class=
"work-tabs"
>
<el-tabs
v-model=
"activeTab"
class=
"work-tabs"
>
<el-tab-pane
label=
"表单"
name=
"form"
>
<form-render
ref=
"formRender"
/>
</el-tab-pane>
<el-tab-pane
label=
"📋 时间轴"
name=
"timeline"
>
<el-tab-pane
label=
"📋 时间轴"
name=
"timeline"
>
<timeline-container
ref=
"timelineContainerRef"
/>
<timeline-container
ref=
"timelineContainerRef"
/>
</el-tab-pane>
</el-tab-pane>
...
@@ -12,108 +15,121 @@
...
@@ -12,108 +15,121 @@
</
template
>
</
template
>
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
ref
,
onMounted
,
onUnmounted
}
from
'vue'
import
{
ref
,
onMounted
,
onUnmounted
}
from
"vue"
;
import
TimelineContainer
from
'./TimelineContainer.vue'
import
FormRender
from
"./FormRender.vue"
;
import
WebpageBrowser
from
'./WebpageBrowser.vue'
import
TimelineContainer
from
"./TimelineContainer.vue"
;
import
{
TimelineService
}
from
'../services/TimelineService'
import
WebpageBrowser
from
"./WebpageBrowser.vue"
;
import
{
TimelineService
}
from
"../services/TimelineService"
;
const
activeTab
=
ref
(
'timeline'
)
const
activeTab
=
ref
(
"form"
);
const
timelineContainerRef
=
ref
<
InstanceType
<
typeof
TimelineContainer
>
|
null
>
(
null
)
const
formRender
=
ref
();
const
webBrowser
=
ref
()
const
timelineContainerRef
=
ref
<
InstanceType
<
typeof
TimelineContainer
>
|
null
>
(
let
timelineService
:
TimelineService
|
null
=
null
null
);
const
webBrowser
=
ref
();
let
timelineService
:
TimelineService
|
null
=
null
;
// 添加事件到时间轴
// 添加事件到时间轴
const
addEvent
=
(
event
:
any
):
void
=>
{
const
addEvent
=
(
event
:
any
):
void
=>
{
timelineContainerRef
.
value
?.
addEvent
(
event
)
timelineContainerRef
.
value
?.
addEvent
(
event
)
;
}
}
;
// 初始化Timeline服务
// 初始化Timeline服务
const
initTimelineService
=
()
=>
{
const
initTimelineService
=
()
=>
{
if
(
timelineContainerRef
.
value
)
{
if
(
timelineContainerRef
.
value
)
{
timelineService
=
new
TimelineService
((
event
:
any
)
=>
{
timelineService
=
new
TimelineService
((
event
:
any
)
=>
{
addEvent
(
event
)
addEvent
(
event
)
;
})
})
;
timelineService
.
connectSSE
()
timelineService
.
connectSSE
()
;
}
}
}
}
;
// 清除时间轴
// 清除时间轴
const
clearTimeline
=
():
void
=>
{
const
clearTimeline
=
():
void
=>
{
timelineContainerRef
.
value
?.
clearTimeline
()
timelineContainerRef
.
value
?.
clearTimeline
()
;
}
}
;
// 定义embed事件的详细信息类型
// 定义embed事件的详细信息类型
interface
EmbedEventDetail
{
interface
EmbedEventDetail
{
url
:
string
url
:
string
;
type
:
string
type
:
string
;
title
:
string
title
:
string
;
htmlContent
?:
string
htmlContent
?:
string
;
}
}
// 监听embed事件
// 监听embed事件
const
handleEmbedEvent
=
(
e
:
Event
)
=>
{
const
handleEmbedEvent
=
(
e
:
Event
)
=>
{
const
customEvent
=
e
as
CustomEvent
<
EmbedEventDetail
>
const
customEvent
=
e
as
CustomEvent
<
EmbedEventDetail
>
;
const
{
url
,
type
,
title
,
htmlContent
}
=
customEvent
.
detail
const
{
url
,
type
,
title
,
htmlContent
}
=
customEvent
.
detail
;
// 验证URL有效性
// 验证URL有效性
if
(
!
url
||
typeof
url
!==
'string'
||
url
.
trim
()
===
''
)
{
if
(
!
url
||
typeof
url
!==
"string"
||
url
.
trim
()
===
""
)
{
console
.
error
(
'[WorkArea] embed事件URL验证失败:'
,
{
console
.
error
(
"[WorkArea] embed事件URL验证失败:"
,
{
url
:
url
,
url
:
url
,
type
:
typeof
url
,
type
:
typeof
url
,
isEmpty
:
url
?.
trim
()
===
''
,
isEmpty
:
url
?.
trim
()
===
""
,
detail
:
customEvent
.
detail
detail
:
customEvent
.
detail
,
})
})
;
return
return
;
}
}
// 自动切换到浏览器标签页
// 自动切换到浏览器标签页
activeTab
.
value
=
'browser'
activeTab
.
value
=
"browser"
;
// 调用WebpageBrowser的导航方法,传递完整信息
// 调用WebpageBrowser的导航方法,传递完整信息
if
(
webBrowser
.
value
&&
typeof
webBrowser
.
value
.
navigateToUrl
===
'function'
)
{
if
(
webBrowser
.
value
&&
typeof
webBrowser
.
value
.
navigateToUrl
===
"function"
)
{
webBrowser
.
value
.
navigateToUrl
(
url
,
{
webBrowser
.
value
.
navigateToUrl
(
url
,
{
htmlContent
:
htmlContent
,
htmlContent
:
htmlContent
,
embedType
:
type
,
embedType
:
type
,
embedTitle
:
title
embedTitle
:
title
,
})
})
;
}
else
{
}
else
{
console
.
error
(
'[WorkArea] webBrowser引用无效或navigateToUrl方法不存在'
,
{
console
.
error
(
"[WorkArea] webBrowser引用无效或navigateToUrl方法不存在"
,
{
hasWebBrowser
:
!!
webBrowser
.
value
,
hasWebBrowser
:
!!
webBrowser
.
value
,
hasFn
:
webBrowser
.
value
?
typeof
webBrowser
.
value
.
navigateToUrl
:
'undefined'
hasFn
:
webBrowser
.
value
})
?
typeof
webBrowser
.
value
.
navigateToUrl
:
"undefined"
,
});
}
}
}
}
;
onMounted
(()
=>
{
onMounted
(()
=>
{
// 监听embed事件
// 监听embed事件
window
.
addEventListener
(
'embed-event'
,
handleEmbedEvent
as
EventListener
)
window
.
addEventListener
(
"embed-event"
,
handleEmbedEvent
as
EventListener
);
// 初始化Timeline服务
// 初始化Timeline服务
initTimelineService
()
initTimelineService
()
;
})
})
;
onUnmounted
(()
=>
{
onUnmounted
(()
=>
{
// 移除事件监听
// 移除事件监听
window
.
removeEventListener
(
'embed-event'
,
handleEmbedEvent
as
EventListener
)
window
.
removeEventListener
(
"embed-event"
,
handleEmbedEvent
as
EventListener
);
// 清理Timeline服务
// 清理Timeline服务
if
(
timelineService
)
{
if
(
timelineService
)
{
timelineService
.
cleanup
()
timelineService
.
cleanup
()
;
}
}
})
// 暴露方法供父组件调用
})
;
// 暴露方法供父组件调用
defineExpose
({
defineExpose
({
formRender
,
timelineContainerRef
,
timelineContainerRef
,
webBrowser
,
webBrowser
,
activeTab
,
activeTab
,
// 提供切换tab的方法
// 提供切换tab的方法
switchToForm
:
()
=>
{
activeTab
.
value
=
"form"
;
},
switchToTimeline
:
()
=>
{
switchToTimeline
:
()
=>
{
activeTab
.
value
=
'timeline'
activeTab
.
value
=
"timeline"
;
},
},
switchToBrowser
:
()
=>
{
switchToBrowser
:
()
=>
{
activeTab
.
value
=
'browser'
activeTab
.
value
=
"browser"
;
},
},
// 提供时间轴操作方法
// 提供时间轴操作方法
addEvent
,
addEvent
,
clearTimeline
clearTimeline
,
})
})
;
</
script
>
</
script
>
<
style
scoped
>
<
style
scoped
>
...
...
frontend/src/main.ts
View file @
8a629996
...
@@ -4,6 +4,9 @@ import router from './router'
...
@@ -4,6 +4,9 @@ import router from './router'
import
{
createPinia
}
from
'pinia'
import
{
createPinia
}
from
'pinia'
import
ElementPlus
from
'element-plus'
import
ElementPlus
from
'element-plus'
import
'element-plus/dist/index.css'
import
'element-plus/dist/index.css'
import
ArcoVue
from
"@arco-design/web-vue"
;
import
ArcoVueIcon
from
"@arco-design/web-vue/es/icon"
;
import
'@arco-design/web-vue/dist/arco.less'
;
import
'highlight.js/styles/atom-one-dark.css'
import
'highlight.js/styles/atom-one-dark.css'
import
'./styles/variables.css'
import
'./styles/variables.css'
import
'./styles/global.css'
import
'./styles/global.css'
...
@@ -17,6 +20,8 @@ const app = createApp(App)
...
@@ -17,6 +20,8 @@ const app = createApp(App)
app
.
use
(
createPinia
())
app
.
use
(
createPinia
())
app
.
use
(
router
)
app
.
use
(
router
)
app
.
use
(
ElementPlus
)
app
.
use
(
ElementPlus
)
app
.
use
(
ArcoVue
)
app
.
use
(
ArcoVueIcon
)
// 全局注册所有Element Plus图标
// 全局注册所有Element Plus图标
for
(
const
[
key
,
component
]
of
Object
.
entries
(
ElementPlusIconsVue
))
{
for
(
const
[
key
,
component
]
of
Object
.
entries
(
ElementPlusIconsVue
))
{
...
...
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