Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
I
Internet-hospital
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Labels
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Jobs
Commits
Open sidebar
yuguo
Internet-hospital
Commits
04584395
Commit
04584395
authored
Mar 04, 2026
by
yuguo
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix
parent
671cf8df
Changes
16
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
2624 additions
and
828 deletions
+2624
-828
web/src/api/admin.ts
web/src/api/admin.ts
+2
-0
web/src/app/(main)/admin/agents/AllToolsTab.tsx
web/src/app/(main)/admin/agents/AllToolsTab.tsx
+169
-0
web/src/app/(main)/admin/agents/BuiltinToolsTab.tsx
web/src/app/(main)/admin/agents/BuiltinToolsTab.tsx
+144
-0
web/src/app/(main)/admin/agents/HTTPToolsTab.tsx
web/src/app/(main)/admin/agents/HTTPToolsTab.tsx
+238
-0
web/src/app/(main)/admin/agents/SkillsTab.tsx
web/src/app/(main)/admin/agents/SkillsTab.tsx
+207
-0
web/src/app/(main)/admin/agents/page.tsx
web/src/app/(main)/admin/agents/page.tsx
+228
-260
web/src/app/(main)/admin/agents/toolsConfig.ts
web/src/app/(main)/admin/agents/toolsConfig.ts
+30
-0
web/src/app/(main)/admin/ai-logs/page.tsx
web/src/app/(main)/admin/ai-logs/page.tsx
+513
-0
web/src/app/(main)/admin/layout.tsx
web/src/app/(main)/admin/layout.tsx
+74
-166
web/src/app/(main)/admin/menus/page.tsx
web/src/app/(main)/admin/menus/page.tsx
+260
-13
web/src/app/(main)/admin/workflows/page.tsx
web/src/app/(main)/admin/workflows/page.tsx
+2
-233
web/src/config/routes.ts
web/src/config/routes.ts
+30
-14
web/src/pages/admin/Departments/index.tsx
web/src/pages/admin/Departments/index.tsx
+171
-41
web/src/pages/admin/Doctors/index.tsx
web/src/pages/admin/Doctors/index.tsx
+34
-2
web/src/pages/admin/Workflows/index.tsx
web/src/pages/admin/Workflows/index.tsx
+259
-0
web/src/pages/patient/TextConsult/index.tsx
web/src/pages/patient/TextConsult/index.tsx
+263
-99
No files found.
web/src/api/admin.ts
View file @
04584395
...
@@ -47,6 +47,7 @@ export interface DoctorItem {
...
@@ -47,6 +47,7 @@ export interface DoctorItem {
license_no
:
string
;
license_no
:
string
;
introduction
:
string
;
introduction
:
string
;
specialties
:
string
[];
specialties
:
string
[];
price
:
number
;
review_status
:
string
;
review_status
:
string
;
review_id
:
string
;
review_id
:
string
;
user_status
:
string
;
user_status
:
string
;
...
@@ -290,6 +291,7 @@ export const adminApi = {
...
@@ -290,6 +291,7 @@ export const adminApi = {
hospital
:
string
;
hospital
:
string
;
introduction
?:
string
;
introduction
?:
string
;
specialties
?:
string
[];
specialties
?:
string
[];
price
?:
number
;
})
=>
post
<
null
>
(
'
/admin/doctors/create
'
,
data
),
})
=>
post
<
null
>
(
'
/admin/doctors/create
'
,
data
),
updateDoctor
:
(
doctorId
:
string
,
data
:
UpdateDoctorData
)
=>
updateDoctor
:
(
doctorId
:
string
,
data
:
UpdateDoctorData
)
=>
...
...
web/src/app/(main)/admin/agents/AllToolsTab.tsx
0 → 100644
View file @
04584395
'
use client
'
;
import
React
,
{
useState
}
from
'
react
'
;
import
{
Card
,
Row
,
Col
,
Tag
,
Switch
,
Badge
,
Empty
,
Tooltip
,
Typography
,
message
,
}
from
'
antd
'
;
import
{
useQuery
,
useMutation
,
useQueryClient
}
from
'
@tanstack/react-query
'
;
import
{
agentApi
,
httpToolApi
}
from
'
@/api/agent
'
;
import
type
{
CATEGORY_CONFIG_TYPE
,
SOURCE_CONFIG_TYPE
}
from
'
./toolsConfig
'
;
const
{
Text
,
Paragraph
}
=
Typography
;
type
ToolSource
=
'
builtin
'
|
'
http
'
;
interface
UnifiedTool
{
id
:
string
;
name
:
string
;
description
:
string
;
category
:
string
;
source
:
ToolSource
;
is_enabled
:
boolean
;
cache_ttl
?:
number
;
timeout
?:
number
;
rawId
?:
number
;
}
interface
Props
{
search
:
string
;
categoryFilter
:
string
;
CATEGORY_CONFIG
:
CATEGORY_CONFIG_TYPE
;
SOURCE_CONFIG
:
SOURCE_CONFIG_TYPE
;
}
export
default
function
AllToolsTab
({
search
,
categoryFilter
,
CATEGORY_CONFIG
,
SOURCE_CONFIG
}:
Props
)
{
const
qc
=
useQueryClient
();
const
[
togglingName
,
setTogglingName
]
=
useState
(
''
);
const
{
data
:
builtinData
}
=
useQuery
({
queryKey
:
[
'
agent-tools
'
],
queryFn
:
()
=>
agentApi
.
listTools
(),
select
:
r
=>
(
r
.
data
??
[])
as
{
id
:
string
;
name
:
string
;
description
:
string
;
category
:
string
;
parameters
:
Record
<
string
,
unknown
>
;
is_enabled
:
boolean
;
created_at
:
string
;
}[],
});
const
{
data
:
httpData
}
=
useQuery
({
queryKey
:
[
'
http-tools
'
],
queryFn
:
()
=>
httpToolApi
.
list
(),
select
:
r
=>
r
.
data
??
[],
});
const
allTools
:
UnifiedTool
[]
=
[
...(
builtinData
??
[]).
map
(
t
=>
({
id
:
t
.
name
,
name
:
t
.
name
,
description
:
t
.
description
,
category
:
t
.
category
,
source
:
'
builtin
'
as
ToolSource
,
is_enabled
:
t
.
is_enabled
,
})),
...(
httpData
??
[]).
map
(
t
=>
({
id
:
`http:
${
t
.
id
}
`
,
name
:
t
.
name
,
description
:
t
.
description
||
t
.
display_name
,
category
:
t
.
category
||
'
http
'
,
source
:
'
http
'
as
ToolSource
,
is_enabled
:
t
.
status
===
'
active
'
,
cache_ttl
:
t
.
cache_ttl
,
timeout
:
t
.
timeout
,
rawId
:
t
.
id
,
})),
];
const
toggleMut
=
useMutation
({
mutationFn
:
async
({
tool
,
checked
}:
{
tool
:
UnifiedTool
;
checked
:
boolean
})
=>
{
if
(
tool
.
source
===
'
builtin
'
)
{
return
agentApi
.
updateToolStatus
(
tool
.
name
,
checked
?
'
active
'
:
'
disabled
'
);
}
else
{
return
httpToolApi
.
update
(
tool
.
rawId
!
,
{
status
:
checked
?
'
active
'
:
'
disabled
'
});
}
},
onSuccess
:
(
_
,
{
tool
,
checked
})
=>
{
message
.
success
(
`
${
tool
.
name
}
已
${
checked
?
'
启用
'
:
'
禁用
'
}
`
);
qc
.
invalidateQueries
({
queryKey
:
[
'
agent-tools
'
]
});
qc
.
invalidateQueries
({
queryKey
:
[
'
http-tools
'
]
});
setTogglingName
(
''
);
},
onError
:
()
=>
{
message
.
error
(
'
操作失败
'
);
setTogglingName
(
''
);
},
});
const
filtered
=
allTools
.
filter
(
t
=>
{
const
matchSearch
=
!
search
||
t
.
name
.
includes
(
search
)
||
t
.
description
.
includes
(
search
);
const
matchCategory
=
categoryFilter
===
'
all
'
||
t
.
category
===
categoryFilter
;
return
matchSearch
&&
matchCategory
;
});
const
grouped
:
Record
<
string
,
UnifiedTool
[]
>
=
{};
for
(
const
t
of
filtered
)
{
if
(
!
grouped
[
t
.
category
])
grouped
[
t
.
category
]
=
[];
grouped
[
t
.
category
].
push
(
t
);
}
if
(
Object
.
keys
(
grouped
).
length
===
0
)
{
return
<
Empty
description=
"暂无匹配工具"
style=
{
{
marginTop
:
60
}
}
/>;
}
return
(
<
div
style=
{
{
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
gap
:
20
}
}
>
{
Object
.
entries
(
grouped
).
map
(([
category
,
tools
])
=>
{
const
cfg
=
CATEGORY_CONFIG
[
category
]
||
CATEGORY_CONFIG
.
other
;
return
(
<
div
key=
{
category
}
>
<
div
style=
{
{
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
8
,
marginBottom
:
12
}
}
>
<
Tag
color=
{
cfg
.
color
}
icon=
{
cfg
.
icon
}
style=
{
{
fontSize
:
13
,
padding
:
'
2px 10px
'
}
}
>
{
cfg
.
label
}
</
Tag
>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
12
}
}
>
{
tools
.
length
}
个工具
</
Text
>
</
div
>
<
Row
gutter=
{
[
16
,
16
]
}
>
{
tools
.
map
(
tool
=>
(
<
Col
key=
{
tool
.
id
}
xs=
{
24
}
sm=
{
12
}
md=
{
8
}
lg=
{
6
}
>
<
Card
size=
"small"
style=
{
{
borderRadius
:
12
,
border
:
`1px solid ${tool.is_enabled ? '#d9f7be' : '#f0f0f0'}`
,
background
:
tool
.
is_enabled
?
'
#f6ffed
'
:
'
#fafafa
'
,
transition
:
'
all 0.2s
'
,
}
}
styles=
{
{
body
:
{
padding
:
14
}
}
}
>
<
div
style=
{
{
display
:
'
flex
'
,
justifyContent
:
'
space-between
'
,
alignItems
:
'
flex-start
'
,
marginBottom
:
8
}
}
>
<
div
style=
{
{
flex
:
1
,
minWidth
:
0
}
}
>
<
div
style=
{
{
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
6
,
flexWrap
:
'
wrap
'
}
}
>
<
Badge
status=
{
tool
.
is_enabled
?
'
success
'
:
'
default
'
}
/>
<
Text
code
style=
{
{
fontSize
:
12
}
}
>
{
tool
.
name
}
</
Text
>
</
div
>
<
div
style=
{
{
marginTop
:
4
}
}
>
<
Tag
style=
{
{
fontSize
:
11
,
lineHeight
:
'
16px
'
,
padding
:
'
0 4px
'
,
margin
:
0
,
background
:
SOURCE_CONFIG
[
tool
.
source
].
color
+
'
15
'
,
color
:
SOURCE_CONFIG
[
tool
.
source
].
color
,
border
:
`1px solid ${SOURCE_CONFIG[tool.source].color}40`
,
}
}
>
{
SOURCE_CONFIG
[
tool
.
source
].
label
}
</
Tag
>
</
div
>
</
div
>
<
Tooltip
title=
{
tool
.
is_enabled
?
'
禁用工具
'
:
'
启用工具
'
}
>
<
Switch
size=
"small"
checked=
{
tool
.
is_enabled
}
loading=
{
togglingName
===
tool
.
id
}
onChange=
{
checked
=>
{
setTogglingName
(
tool
.
id
);
toggleMut
.
mutate
({
tool
,
checked
});
}
}
/>
</
Tooltip
>
</
div
>
<
Paragraph
ellipsis=
{
{
rows
:
2
}
}
style=
{
{
fontSize
:
12
,
color
:
'
#595959
'
,
margin
:
0
}
}
>
{
tool
.
description
}
</
Paragraph
>
{
(
tool
.
cache_ttl
!==
undefined
||
tool
.
timeout
!==
undefined
)
&&
(
<
div
style=
{
{
marginTop
:
8
,
display
:
'
flex
'
,
gap
:
8
}
}
>
{
tool
.
timeout
&&
<
Text
type=
"secondary"
style=
{
{
fontSize
:
11
}
}
>
超时
{
tool
.
timeout
}
s
</
Text
>
}
{
tool
.
cache_ttl
!==
undefined
&&
tool
.
cache_ttl
>
0
&&
<
Text
type=
"secondary"
style=
{
{
fontSize
:
11
}
}
>
缓存
{
tool
.
cache_ttl
}
s
</
Text
>
}
</
div
>
)
}
</
Card
>
</
Col
>
))
}
</
Row
>
</
div
>
);
})
}
</
div
>
);
}
web/src/app/(main)/admin/agents/BuiltinToolsTab.tsx
0 → 100644
View file @
04584395
'
use client
'
;
import
React
,
{
useState
}
from
'
react
'
;
import
{
Card
,
Table
,
Tag
,
Button
,
Modal
,
Descriptions
,
Space
,
Badge
,
Typography
,
Switch
,
message
,
}
from
'
antd
'
;
import
{
InfoCircleOutlined
,
CodeOutlined
,
ReloadOutlined
}
from
'
@ant-design/icons
'
;
import
{
useQuery
,
useQueryClient
}
from
'
@tanstack/react-query
'
;
import
{
agentApi
}
from
'
@/api/agent
'
;
import
type
{
CATEGORY_CONFIG_TYPE
}
from
'
./toolsConfig
'
;
const
{
Text
}
=
Typography
;
interface
AgentTool
{
id
:
string
;
name
:
string
;
display_name
:
string
;
description
:
string
;
category
:
string
;
parameters
:
Record
<
string
,
unknown
>
;
status
:
string
;
is_enabled
:
boolean
;
cache_ttl
:
number
;
timeout
:
number
;
max_retries
:
number
;
created_at
:
string
;
}
interface
Props
{
search
:
string
;
categoryFilter
:
string
;
CATEGORY_CONFIG
:
CATEGORY_CONFIG_TYPE
;
}
export
default
function
BuiltinToolsTab
({
search
,
categoryFilter
,
CATEGORY_CONFIG
}:
Props
)
{
const
qc
=
useQueryClient
();
const
[
togglingId
,
setTogglingId
]
=
useState
(
''
);
const
[
detailModal
,
setDetailModal
]
=
useState
<
{
open
:
boolean
;
tool
:
AgentTool
|
null
}
>
({
open
:
false
,
tool
:
null
});
const
{
data
:
tools
=
[],
isLoading
}
=
useQuery
({
queryKey
:
[
'
agent-tools
'
],
queryFn
:
()
=>
agentApi
.
listTools
(),
select
:
r
=>
(
r
.
data
??
[])
as
AgentTool
[],
});
const
handleToggle
=
async
(
tool
:
AgentTool
,
checked
:
boolean
)
=>
{
setTogglingId
(
tool
.
name
);
try
{
await
agentApi
.
updateToolStatus
(
tool
.
name
,
checked
?
'
active
'
:
'
disabled
'
);
message
.
success
(
`工具
${
tool
.
name
}
已
${
checked
?
'
启用
'
:
'
禁用
'
}
`
);
qc
.
invalidateQueries
({
queryKey
:
[
'
agent-tools
'
]
});
if
(
detailModal
.
tool
?.
name
===
tool
.
name
)
{
setDetailModal
(
prev
=>
({
...
prev
,
tool
:
prev
.
tool
?
{
...
prev
.
tool
,
is_enabled
:
checked
}
:
null
}));
}
}
catch
{
message
.
error
(
'
操作失败
'
);
}
finally
{
setTogglingId
(
''
);
}
};
const
filtered
=
tools
.
filter
(
t
=>
{
const
matchSearch
=
!
search
||
t
.
name
.
toLowerCase
().
includes
(
search
.
toLowerCase
())
||
t
.
description
.
toLowerCase
().
includes
(
search
.
toLowerCase
());
const
matchCategory
=
categoryFilter
===
'
all
'
||
t
.
category
===
categoryFilter
;
return
matchSearch
&&
matchCategory
;
});
const
columns
=
[
{
title
:
'
工具名称
'
,
dataIndex
:
'
name
'
,
key
:
'
name
'
,
render
:
(
v
:
string
)
=>
<
Space
><
CodeOutlined
style=
{
{
color
:
'
#0D9488
'
}
}
/><
Text
code
style=
{
{
fontSize
:
13
}
}
>
{
v
}
</
Text
></
Space
>,
},
{
title
:
'
描述
'
,
dataIndex
:
'
description
'
,
key
:
'
description
'
,
ellipsis
:
true
,
width
:
280
},
{
title
:
'
分类
'
,
dataIndex
:
'
category
'
,
key
:
'
category
'
,
width
:
110
,
render
:
(
v
:
string
)
=>
{
const
cfg
=
CATEGORY_CONFIG
[
v
]
||
CATEGORY_CONFIG
.
other
;
return
<
Tag
color=
{
cfg
.
color
}
>
{
cfg
.
label
}
</
Tag
>;
},
},
{
title
:
'
参数数量
'
,
dataIndex
:
'
parameters
'
,
key
:
'
parameters
'
,
width
:
90
,
render
:
(
v
:
Record
<
string
,
unknown
>
)
=>
<
Text
type=
"secondary"
>
{
v
?
Object
.
keys
(
v
).
length
:
0
}
个
</
Text
>,
},
{
title
:
'
超时/缓存
'
,
key
:
'
timing
'
,
width
:
110
,
render
:
(
_
:
unknown
,
r
:
AgentTool
)
=>
(
<
Space
direction=
"vertical"
size=
{
0
}
>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
12
}
}
>
超时
{
r
.
timeout
||
30
}
s
</
Text
>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
12
}
}
>
{
r
.
cache_ttl
>
0
?
`缓存 ${r.cache_ttl}s`
:
'
不缓存
'
}
</
Text
>
</
Space
>
),
},
{
title
:
'
启用状态
'
,
dataIndex
:
'
is_enabled
'
,
key
:
'
is_enabled
'
,
width
:
110
,
render
:
(
v
:
boolean
,
record
:
AgentTool
)
=>
(
<
Switch
checked=
{
v
}
loading=
{
togglingId
===
record
.
name
}
checkedChildren=
"启用"
unCheckedChildren=
"禁用"
onChange=
{
checked
=>
handleToggle
(
record
,
checked
)
}
/>
),
},
{
title
:
'
操作
'
,
key
:
'
action
'
,
width
:
80
,
render
:
(
_
:
unknown
,
record
:
AgentTool
)
=>
(
<
Button
type=
"link"
size=
"small"
icon=
{
<
InfoCircleOutlined
/>
}
onClick=
{
()
=>
setDetailModal
({
open
:
true
,
tool
:
record
})
}
>
详情
</
Button
>
),
},
];
return
(
<>
<
Card
style=
{
{
borderRadius
:
12
,
border
:
'
1px solid #E0F2F1
'
}
}
>
<
Table
dataSource=
{
filtered
}
columns=
{
columns
}
rowKey=
"name"
loading=
{
isLoading
}
size=
"small"
pagination=
{
{
pageSize
:
10
,
showSizeChanger
:
true
,
size
:
'
small
'
,
showTotal
:
t
=>
`共 ${t} 个工具`
}
}
/>
</
Card
>
<
Modal
title=
{
<
Space
><
CodeOutlined
style=
{
{
color
:
'
#0D9488
'
}
}
/><
span
>
工具详情
</
span
></
Space
>
}
open=
{
detailModal
.
open
}
onCancel=
{
()
=>
setDetailModal
({
open
:
false
,
tool
:
null
})
}
footer=
{
detailModal
.
tool
&&
(
<
Space
>
<
Switch
checked=
{
detailModal
.
tool
.
is_enabled
}
loading=
{
togglingId
===
detailModal
.
tool
.
name
}
checkedChildren=
"启用"
unCheckedChildren=
"禁用"
onChange=
{
checked
=>
handleToggle
(
detailModal
.
tool
!
,
checked
)
}
/>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
12
}
}
>
{
detailModal
.
tool
.
is_enabled
?
'
工具已启用,Agent 可正常调用
'
:
'
工具已禁用
'
}
</
Text
>
</
Space
>
)
}
width=
{
600
}
>
{
detailModal
.
tool
&&
(
<
div
style=
{
{
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
gap
:
16
}
}
>
<
Descriptions
column=
{
1
}
bordered
size=
"small"
>
<
Descriptions
.
Item
label=
"工具名称"
><
Text
code
style=
{
{
color
:
'
#0D9488
'
}
}
>
{
detailModal
.
tool
.
name
}
</
Text
></
Descriptions
.
Item
>
<
Descriptions
.
Item
label=
"描述"
>
{
detailModal
.
tool
.
description
}
</
Descriptions
.
Item
>
<
Descriptions
.
Item
label=
"分类"
><
Tag
color=
{
(
CATEGORY_CONFIG
[
detailModal
.
tool
.
category
]
||
CATEGORY_CONFIG
.
other
).
color
}
>
{
(
CATEGORY_CONFIG
[
detailModal
.
tool
.
category
]
||
CATEGORY_CONFIG
.
other
).
label
}
</
Tag
></
Descriptions
.
Item
>
<
Descriptions
.
Item
label=
"状态"
><
Badge
status=
{
detailModal
.
tool
.
is_enabled
?
'
success
'
:
'
default
'
}
text=
{
detailModal
.
tool
.
is_enabled
?
'
已启用
'
:
'
已禁用
'
}
/></
Descriptions
.
Item
>
<
Descriptions
.
Item
label=
"执行超时"
>
{
detailModal
.
tool
.
timeout
||
30
}
秒
</
Descriptions
.
Item
>
<
Descriptions
.
Item
label=
"结果缓存"
>
{
detailModal
.
tool
.
cache_ttl
>
0
?
`${detailModal.tool.cache_ttl} 秒`
:
'
不缓存
'
}
</
Descriptions
.
Item
>
<
Descriptions
.
Item
label=
"失败重试"
>
{
detailModal
.
tool
.
max_retries
||
0
}
次
</
Descriptions
.
Item
>
</
Descriptions
>
<
Card
size=
"small"
title=
"参数定义"
style=
{
{
background
:
'
#fafafa
'
,
borderRadius
:
8
}
}
>
<
pre
style=
{
{
fontSize
:
12
,
background
:
'
#1f2937
'
,
color
:
'
#4ade80
'
,
padding
:
12
,
borderRadius
:
6
,
overflow
:
'
auto
'
,
margin
:
0
}
}
>
{
JSON
.
stringify
(
detailModal
.
tool
.
parameters
,
null
,
2
)
}
</
pre
>
</
Card
>
</
div
>
)
}
</
Modal
>
</>
);
}
web/src/app/(main)/admin/agents/HTTPToolsTab.tsx
0 → 100644
View file @
04584395
'
use client
'
;
import
React
,
{
useState
}
from
'
react
'
;
import
{
Card
,
Table
,
Tag
,
Button
,
Modal
,
Form
,
Input
,
Select
,
InputNumber
,
Space
,
Popconfirm
,
message
,
Typography
,
Badge
,
Tooltip
,
}
from
'
antd
'
;
import
{
PlusOutlined
,
EditOutlined
,
DeleteOutlined
,
ApiOutlined
,
ThunderboltOutlined
,
ReloadOutlined
,
PlayCircleOutlined
,
CodeOutlined
,
}
from
'
@ant-design/icons
'
;
import
{
useQuery
,
useMutation
,
useQueryClient
}
from
'
@tanstack/react-query
'
;
import
{
httpToolApi
,
type
HTTPToolDefinition
}
from
'
@/api/agent
'
;
const
{
Text
}
=
Typography
;
const
{
TextArea
}
=
Input
;
const
METHOD_COLORS
:
Record
<
string
,
string
>
=
{
GET
:
'
green
'
,
POST
:
'
blue
'
,
PUT
:
'
orange
'
,
DELETE
:
'
red
'
,
PATCH
:
'
purple
'
,
};
const
AUTH_LABELS
:
Record
<
string
,
string
>
=
{
none
:
'
无认证
'
,
bearer
:
'
Bearer Token
'
,
basic
:
'
Basic Auth
'
,
apikey
:
'
API Key
'
,
};
interface
Props
{
search
:
string
}
export
default
function
HTTPToolsTab
({
search
}:
Props
)
{
const
qc
=
useQueryClient
();
const
[
form
]
=
Form
.
useForm
();
const
[
testForm
]
=
Form
.
useForm
();
const
[
modalOpen
,
setModalOpen
]
=
useState
(
false
);
const
[
testModal
,
setTestModal
]
=
useState
<
{
open
:
boolean
;
tool
:
HTTPToolDefinition
|
null
}
>
({
open
:
false
,
tool
:
null
});
const
[
editingId
,
setEditingId
]
=
useState
<
number
|
null
>
(
null
);
const
[
authType
,
setAuthType
]
=
useState
(
'
none
'
);
const
{
data
,
isLoading
}
=
useQuery
({
queryKey
:
[
'
http-tools
'
],
queryFn
:
()
=>
httpToolApi
.
list
(),
select
:
r
=>
r
.
data
??
[],
});
const
tools
=
(
data
??
[]).
filter
(
t
=>
!
search
||
t
.
name
.
includes
(
search
)
||
(
t
.
description
||
''
).
includes
(
search
)
);
const
saveMut
=
useMutation
({
mutationFn
:
(
values
:
Record
<
string
,
unknown
>
)
=>
editingId
?
httpToolApi
.
update
(
editingId
,
values
)
:
httpToolApi
.
create
(
values
),
onSuccess
:
()
=>
{
message
.
success
(
editingId
?
'
更新成功
'
:
'
创建成功
'
);
qc
.
invalidateQueries
({
queryKey
:
[
'
http-tools
'
]
});
setModalOpen
(
false
);
form
.
resetFields
();
setEditingId
(
null
);
},
onError
:
()
=>
message
.
error
(
'
操作失败
'
),
});
const
deleteMut
=
useMutation
({
mutationFn
:
(
id
:
number
)
=>
httpToolApi
.
delete
(
id
),
onSuccess
:
()
=>
{
message
.
success
(
'
已删除
'
);
qc
.
invalidateQueries
({
queryKey
:
[
'
http-tools
'
]
});
},
});
const
reloadMut
=
useMutation
({
mutationFn
:
()
=>
httpToolApi
.
reload
(),
onSuccess
:
()
=>
message
.
success
(
'
HTTP 工具已热重载
'
),
});
const
testMut
=
useMutation
({
mutationFn
:
({
id
,
params
}:
{
id
:
number
;
params
:
Record
<
string
,
unknown
>
})
=>
httpToolApi
.
test
(
id
,
params
),
});
const
openCreate
=
()
=>
{
setEditingId
(
null
);
form
.
resetFields
();
setAuthType
(
'
none
'
);
setModalOpen
(
true
);
};
const
openEdit
=
(
tool
:
HTTPToolDefinition
)
=>
{
setEditingId
(
tool
.
id
);
setAuthType
(
tool
.
auth_type
||
'
none
'
);
let
authConfig
:
Record
<
string
,
string
>
=
{};
try
{
authConfig
=
JSON
.
parse
(
tool
.
auth_config
||
'
{}
'
);
}
catch
{
/* ignore */
}
form
.
setFieldsValue
({
name
:
tool
.
name
,
display_name
:
tool
.
display_name
,
description
:
tool
.
description
,
category
:
tool
.
category
,
method
:
tool
.
method
||
'
GET
'
,
url
:
tool
.
url
,
headers
:
tool
.
headers
,
body_template
:
tool
.
body_template
,
auth_type
:
tool
.
auth_type
||
'
none
'
,
timeout
:
tool
.
timeout
||
10
,
cache_ttl
:
tool
.
cache_ttl
||
0
,
status
:
tool
.
status
,
...
authConfig
,
});
setModalOpen
(
true
);
};
const
handleSave
=
()
=>
{
form
.
validateFields
().
then
(
values
=>
{
const
authConfig
:
Record
<
string
,
string
>
=
{};
if
(
values
.
auth_type
===
'
bearer
'
)
authConfig
.
token
=
values
.
token
||
''
;
else
if
(
values
.
auth_type
===
'
basic
'
)
{
authConfig
.
username
=
values
.
username
||
''
;
authConfig
.
password
=
values
.
password
||
''
;
}
else
if
(
values
.
auth_type
===
'
apikey
'
)
{
authConfig
.
key
=
values
.
key
||
''
;
authConfig
.
value
=
values
.
value
||
''
;
authConfig
.
in
=
values
.
in
||
'
header
'
;
}
saveMut
.
mutate
({
name
:
values
.
name
,
display_name
:
values
.
display_name
||
values
.
name
,
description
:
values
.
description
||
''
,
category
:
values
.
category
||
'
http
'
,
method
:
values
.
method
||
'
GET
'
,
url
:
values
.
url
,
headers
:
values
.
headers
||
'
{}
'
,
body_template
:
values
.
body_template
||
''
,
auth_type
:
values
.
auth_type
||
'
none
'
,
auth_config
:
JSON
.
stringify
(
authConfig
),
parameters
:
'
[]
'
,
timeout
:
values
.
timeout
||
10
,
cache_ttl
:
values
.
cache_ttl
||
0
,
status
:
values
.
status
||
'
active
'
,
});
});
};
const
handleTest
=
()
=>
{
if
(
!
testModal
.
tool
)
return
;
testForm
.
validateFields
().
then
(
values
=>
{
let
params
:
Record
<
string
,
unknown
>
=
{};
try
{
params
=
JSON
.
parse
(
values
.
params
||
'
{}
'
);
}
catch
{
message
.
error
(
'
参数格式错误
'
);
return
;
}
testMut
.
mutate
({
id
:
testModal
.
tool
!
.
id
,
params
});
});
};
const
columns
=
[
{
title
:
'
名称
'
,
dataIndex
:
'
name
'
,
key
:
'
name
'
,
render
:
(
v
:
string
,
r
:
HTTPToolDefinition
)
=>
(
<
div
>
<
Text
code
style=
{
{
fontSize
:
13
}
}
>
{
v
}
</
Text
>
{
r
.
display_name
&&
r
.
display_name
!==
v
&&
<
div
style=
{
{
fontSize
:
12
,
color
:
'
#8c8c8c
'
}
}
>
{
r
.
display_name
}
</
div
>
}
</
div
>
),
},
{
title
:
'
描述
'
,
dataIndex
:
'
description
'
,
key
:
'
description
'
,
ellipsis
:
true
,
width
:
200
},
{
title
:
'
方法
'
,
dataIndex
:
'
method
'
,
key
:
'
method
'
,
width
:
80
,
render
:
(
v
:
string
)
=>
<
Tag
color=
{
METHOD_COLORS
[
v
]
||
'
default
'
}
>
{
v
}
</
Tag
>
},
{
title
:
'
URL
'
,
dataIndex
:
'
url
'
,
key
:
'
url
'
,
ellipsis
:
true
,
width
:
200
,
render
:
(
v
:
string
)
=>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
12
}
}
>
{
v
}
</
Text
>
},
{
title
:
'
认证
'
,
dataIndex
:
'
auth_type
'
,
key
:
'
auth_type
'
,
width
:
100
,
render
:
(
v
:
string
)
=>
<
Tag
>
{
AUTH_LABELS
[
v
]
||
v
}
</
Tag
>
},
{
title
:
'
超时/缓存
'
,
key
:
'
timing
'
,
width
:
110
,
render
:
(
_
:
unknown
,
r
:
HTTPToolDefinition
)
=>
(
<
Space
direction=
"vertical"
size=
{
0
}
>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
11
}
}
>
超时:
{
r
.
timeout
}
s
</
Text
>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
11
}
}
>
缓存:
{
r
.
cache_ttl
>
0
?
`${r.cache_ttl}s`
:
'
不缓存
'
}
</
Text
>
</
Space
>
),
},
{
title
:
'
状态
'
,
dataIndex
:
'
status
'
,
key
:
'
status
'
,
width
:
80
,
render
:
(
v
:
string
)
=>
<
Badge
status=
{
v
===
'
active
'
?
'
success
'
:
'
default
'
}
text=
{
v
===
'
active
'
?
'
启用
'
:
'
禁用
'
}
/>,
},
{
title
:
'
操作
'
,
key
:
'
action
'
,
width
:
160
,
render
:
(
_
:
unknown
,
record
:
HTTPToolDefinition
)
=>
(
<
Space
>
<
Tooltip
title=
"测试"
><
Button
type=
"link"
size=
"small"
icon=
{
<
PlayCircleOutlined
/>
}
onClick=
{
()
=>
{
setTestModal
({
open
:
true
,
tool
:
record
});
testForm
.
resetFields
();
testMut
.
reset
();
}
}
/></
Tooltip
>
<
Button
type=
"link"
size=
"small"
icon=
{
<
EditOutlined
/>
}
onClick=
{
()
=>
openEdit
(
record
)
}
/>
<
Popconfirm
title=
"确定删除?"
onConfirm=
{
()
=>
deleteMut
.
mutate
(
record
.
id
)
}
>
<
Button
type=
"link"
size=
"small"
danger
icon=
{
<
DeleteOutlined
/>
}
/>
</
Popconfirm
>
</
Space
>
),
},
];
return
(
<>
<
div
style=
{
{
display
:
'
flex
'
,
justifyContent
:
'
flex-end
'
,
marginBottom
:
12
}
}
>
<
Space
>
<
Button
icon=
{
<
ReloadOutlined
/>
}
onClick=
{
()
=>
reloadMut
.
mutate
()
}
loading=
{
reloadMut
.
isPending
}
>
热重载
</
Button
>
<
Button
type=
"primary"
icon=
{
<
PlusOutlined
/>
}
onClick=
{
openCreate
}
>
新建 HTTP 工具
</
Button
>
</
Space
>
</
div
>
<
Card
style=
{
{
borderRadius
:
12
,
border
:
'
1px solid #E0F2F1
'
}
}
>
<
Table
dataSource=
{
tools
}
columns=
{
columns
}
rowKey=
"id"
loading=
{
isLoading
}
size=
"small"
pagination=
{
{
pageSize
:
10
,
showTotal
:
t
=>
`共 ${t} 个工具`
}
}
/>
</
Card
>
{
/* Create/Edit Modal */
}
<
Modal
title=
{
<
Space
><
ApiOutlined
style=
{
{
color
:
'
#0D9488
'
}
}
/>
{
editingId
?
'
编辑 HTTP 工具
'
:
'
新建 HTTP 工具
'
}
</
Space
>
}
open=
{
modalOpen
}
onCancel=
{
()
=>
{
setModalOpen
(
false
);
form
.
resetFields
();
}
}
onOk=
{
handleSave
}
confirmLoading=
{
saveMut
.
isPending
}
width=
{
680
}
destroyOnHidden
>
<
Form
form=
{
form
}
layout=
"vertical"
style=
{
{
marginTop
:
12
}
}
>
<
div
style=
{
{
display
:
'
grid
'
,
gridTemplateColumns
:
'
1fr 1fr
'
,
gap
:
'
0 16px
'
}
}
>
<
Form
.
Item
name=
"name"
label=
"工具名称(英文)"
rules=
{
[{
required
:
true
,
message
:
'
必填
'
}]
}
>
<
Input
placeholder=
"my_http_tool"
disabled=
{
!!
editingId
}
/>
</
Form
.
Item
>
<
Form
.
Item
name=
"display_name"
label=
"显示名称"
><
Input
placeholder=
"我的 HTTP 工具"
/></
Form
.
Item
>
</
div
>
<
Form
.
Item
name=
"description"
label=
"描述"
><
TextArea
rows=
{
2
}
placeholder=
"工具功能说明"
/></
Form
.
Item
>
<
div
style=
{
{
display
:
'
grid
'
,
gridTemplateColumns
:
'
120px 1fr
'
,
gap
:
'
0 12px
'
}
}
>
<
Form
.
Item
name=
"method"
label=
"请求方法"
initialValue=
"GET"
>
<
Select
options=
{
[
'
GET
'
,
'
POST
'
,
'
PUT
'
,
'
DELETE
'
,
'
PATCH
'
].
map
(
m
=>
({
value
:
m
,
label
:
m
}))
}
/>
</
Form
.
Item
>
<
Form
.
Item
name=
"url"
label=
"URL"
rules=
{
[{
required
:
true
,
message
:
'
必填
'
}]
}
>
<
Input
placeholder=
"https://api.example.com/endpoint?q={{keyword}}"
/>
</
Form
.
Item
>
</
div
>
<
Form
.
Item
name=
"headers"
label=
"请求头 (JSON)"
initialValue=
"{}"
><
TextArea
rows=
{
2
}
/></
Form
.
Item
>
<
Form
.
Item
name=
"body_template"
label=
"请求体模板(支持 {{variable}})"
><
TextArea
rows=
{
3
}
/></
Form
.
Item
>
<
Form
.
Item
name=
"auth_type"
label=
"认证方式"
initialValue=
"none"
>
<
Select
options=
{
Object
.
entries
(
AUTH_LABELS
).
map
(([
v
,
l
])
=>
({
value
:
v
,
label
:
l
}))
}
onChange=
{
v
=>
setAuthType
(
v
)
}
/>
</
Form
.
Item
>
{
authType
===
'
bearer
'
&&
<
Form
.
Item
name=
"token"
label=
"Bearer Token"
rules=
{
[{
required
:
true
}]
}
><
Input
.
Password
/></
Form
.
Item
>
}
{
authType
===
'
basic
'
&&
(
<
div
style=
{
{
display
:
'
grid
'
,
gridTemplateColumns
:
'
1fr 1fr
'
,
gap
:
'
0 16px
'
}
}
>
<
Form
.
Item
name=
"username"
label=
"用户名"
rules=
{
[{
required
:
true
}]
}
><
Input
/></
Form
.
Item
>
<
Form
.
Item
name=
"password"
label=
"密码"
rules=
{
[{
required
:
true
}]
}
><
Input
.
Password
/></
Form
.
Item
>
</
div
>
)
}
{
authType
===
'
apikey
'
&&
(
<
div
style=
{
{
display
:
'
grid
'
,
gridTemplateColumns
:
'
1fr 1fr 120px
'
,
gap
:
'
0 12px
'
}
}
>
<
Form
.
Item
name=
"key"
label=
"参数名"
rules=
{
[{
required
:
true
}]
}
><
Input
/></
Form
.
Item
>
<
Form
.
Item
name=
"value"
label=
"参数值"
rules=
{
[{
required
:
true
}]
}
><
Input
.
Password
/></
Form
.
Item
>
<
Form
.
Item
name=
"in"
label=
"位置"
initialValue=
"header"
>
<
Select
options=
{
[{
value
:
'
header
'
,
label
:
'
Header
'
},
{
value
:
'
query
'
,
label
:
'
Query
'
}]
}
/>
</
Form
.
Item
>
</
div
>
)
}
<
div
style=
{
{
display
:
'
grid
'
,
gridTemplateColumns
:
'
1fr 1fr 1fr
'
,
gap
:
'
0 16px
'
}
}
>
<
Form
.
Item
name=
"timeout"
label=
"超时(秒)"
initialValue=
{
10
}
><
InputNumber
min=
{
1
}
max=
{
120
}
style=
{
{
width
:
'
100%
'
}
}
/></
Form
.
Item
>
<
Form
.
Item
name=
"cache_ttl"
label=
"缓存TTL(秒)"
initialValue=
{
0
}
><
InputNumber
min=
{
0
}
max=
{
86400
}
style=
{
{
width
:
'
100%
'
}
}
/></
Form
.
Item
>
<
Form
.
Item
name=
"status"
label=
"状态"
initialValue=
"active"
>
<
Select
options=
{
[{
value
:
'
active
'
,
label
:
'
启用
'
},
{
value
:
'
disabled
'
,
label
:
'
禁用
'
}]
}
/>
</
Form
.
Item
>
</
div
>
</
Form
>
</
Modal
>
{
/* Test Modal */
}
<
Modal
title=
{
<
Space
><
ThunderboltOutlined
style=
{
{
color
:
'
#fa8c16
'
}
}
/>
测试工具:
<
Text
code
>
{
testModal
.
tool
?.
name
}
</
Text
></
Space
>
}
open=
{
testModal
.
open
}
onCancel=
{
()
=>
setTestModal
({
open
:
false
,
tool
:
null
})
}
onOk=
{
handleTest
}
confirmLoading=
{
testMut
.
isPending
}
width=
{
600
}
>
<
Form
form=
{
testForm
}
layout=
"vertical"
>
<
Form
.
Item
name=
"params"
label=
"输入参数 (JSON)"
><
TextArea
rows=
{
4
}
placeholder=
'{"keyword": "高血压"}'
/></
Form
.
Item
>
</
Form
>
{
testMut
.
data
&&
(
<
Card
size=
"small"
title=
"调用结果"
style=
{
{
background
:
'
#fafafa
'
}
}
>
<
pre
style=
{
{
fontSize
:
12
,
background
:
'
#1f2937
'
,
color
:
testMut
.
data
.
data
?.
success
?
'
#4ade80
'
:
'
#f87171
'
,
padding
:
12
,
borderRadius
:
6
,
overflow
:
'
auto
'
,
margin
:
0
}
}
>
{
JSON
.
stringify
(
testMut
.
data
.
data
,
null
,
2
)
}
</
pre
>
</
Card
>
)
}
</
Modal
>
</>
);
}
web/src/app/(main)/admin/agents/SkillsTab.tsx
0 → 100644
View file @
04584395
'
use client
'
;
import
React
,
{
useState
}
from
'
react
'
;
import
{
Card
,
Row
,
Col
,
Tag
,
Button
,
Modal
,
Form
,
Input
,
Select
,
Empty
,
Space
,
Popconfirm
,
message
,
Typography
,
Badge
,
}
from
'
antd
'
;
import
{
PlusOutlined
,
EditOutlined
,
DeleteOutlined
,
ThunderboltOutlined
,
BookOutlined
,
HeartOutlined
,
SafetyOutlined
,
CalendarOutlined
,
MedicineBoxOutlined
,
SettingOutlined
,
}
from
'
@ant-design/icons
'
;
import
{
useQuery
,
useMutation
,
useQueryClient
}
from
'
@tanstack/react-query
'
;
import
{
skillApi
,
agentApi
,
type
AgentSkill
}
from
'
@/api/agent
'
;
const
{
Text
,
Paragraph
}
=
Typography
;
const
{
TextArea
}
=
Input
;
const
SKILL_ICONS
:
Record
<
string
,
React
.
ReactNode
>
=
{
heart
:
<
HeartOutlined
/>,
book
:
<
BookOutlined
/>,
safety
:
<
SafetyOutlined
/>,
calendar
:
<
CalendarOutlined
/>,
'
medicine-box
'
:
<
MedicineBoxOutlined
/>
,
'
file-text
'
:
<
BookOutlined
/>
,
setting
:
<
SettingOutlined
/>,
};
const
CATEGORY_COLORS
:
Record
<
string
,
string
>
=
{
patient
:
'
green
'
,
doctor
:
'
blue
'
,
admin
:
'
purple
'
,
general
:
'
cyan
'
,
};
const
CATEGORY_LABELS
:
Record
<
string
,
string
>
=
{
patient
:
'
患者
'
,
doctor
:
'
医生
'
,
admin
:
'
管理
'
,
general
:
'
通用
'
,
};
interface
Props
{
search
:
string
}
export
default
function
SkillsTab
({
search
}:
Props
)
{
const
qc
=
useQueryClient
();
const
[
form
]
=
Form
.
useForm
();
const
[
modalOpen
,
setModalOpen
]
=
useState
(
false
);
const
[
editingSkill
,
setEditingSkill
]
=
useState
<
AgentSkill
|
null
>
(
null
);
const
{
data
:
skills
=
[]
}
=
useQuery
({
queryKey
:
[
'
agent-skills
'
],
queryFn
:
()
=>
skillApi
.
list
(),
select
:
r
=>
r
.
data
??
[],
});
const
{
data
:
toolList
=
[]
}
=
useQuery
({
queryKey
:
[
'
agent-tools
'
],
queryFn
:
()
=>
agentApi
.
listTools
(),
select
:
r
=>
(
r
.
data
??
[]).
map
((
t
:
{
name
:
string
;
description
:
string
})
=>
({
value
:
t
.
name
,
label
:
`
${
t
.
name
}
-
${
t
.
description
}
`
})),
});
const
saveMut
=
useMutation
({
mutationFn
:
(
values
:
Record
<
string
,
unknown
>
)
=>
editingSkill
?
skillApi
.
update
(
editingSkill
.
skill_id
,
values
as
Partial
<
AgentSkill
>
)
:
skillApi
.
create
(
values
as
Partial
<
AgentSkill
>
),
onSuccess
:
()
=>
{
message
.
success
(
editingSkill
?
'
更新成功
'
:
'
创建成功
'
);
qc
.
invalidateQueries
({
queryKey
:
[
'
agent-skills
'
]
});
setModalOpen
(
false
);
form
.
resetFields
();
setEditingSkill
(
null
);
},
onError
:
()
=>
message
.
error
(
'
操作失败
'
),
});
const
deleteMut
=
useMutation
({
mutationFn
:
(
skillId
:
string
)
=>
skillApi
.
delete
(
skillId
),
onSuccess
:
()
=>
{
message
.
success
(
'
已删除
'
);
qc
.
invalidateQueries
({
queryKey
:
[
'
agent-skills
'
]
});
},
});
const
openCreate
=
()
=>
{
setEditingSkill
(
null
);
form
.
resetFields
();
setModalOpen
(
true
);
};
const
openEdit
=
(
skill
:
AgentSkill
)
=>
{
setEditingSkill
(
skill
);
let
parsedTools
:
string
[]
=
[];
let
parsedQR
:
string
[]
=
[];
try
{
parsedTools
=
JSON
.
parse
(
skill
.
tools
||
'
[]
'
);
}
catch
{
/* */
}
try
{
parsedQR
=
JSON
.
parse
(
skill
.
quick_replies
||
'
[]
'
);
}
catch
{
/* */
}
form
.
setFieldsValue
({
skill_id
:
skill
.
skill_id
,
name
:
skill
.
name
,
description
:
skill
.
description
,
category
:
skill
.
category
,
tools
:
parsedTools
,
system_prompt_addon
:
skill
.
system_prompt_addon
,
quick_replies
:
parsedQR
.
join
(
'
\n
'
),
icon
:
skill
.
icon
,
});
setModalOpen
(
true
);
};
const
handleSave
=
()
=>
{
form
.
validateFields
().
then
(
values
=>
{
const
quickReplies
=
(
values
.
quick_replies
||
''
).
split
(
'
\n
'
).
filter
((
s
:
string
)
=>
s
.
trim
());
saveMut
.
mutate
({
skill_id
:
values
.
skill_id
,
name
:
values
.
name
,
description
:
values
.
description
||
''
,
category
:
values
.
category
||
'
general
'
,
tools
:
values
.
tools
||
[],
system_prompt_addon
:
values
.
system_prompt_addon
||
''
,
quick_replies
:
quickReplies
,
icon
:
values
.
icon
||
'
setting
'
,
});
});
};
const
filtered
=
skills
.
filter
(
s
=>
!
search
||
s
.
name
.
includes
(
search
)
||
s
.
skill_id
.
includes
(
search
)
||
(
s
.
description
||
''
).
includes
(
search
)
);
return
(
<>
<
div
style=
{
{
display
:
'
flex
'
,
justifyContent
:
'
flex-end
'
,
marginBottom
:
12
}
}
>
<
Button
type=
"primary"
icon=
{
<
PlusOutlined
/>
}
onClick=
{
openCreate
}
>
新建技能包
</
Button
>
</
div
>
{
filtered
.
length
===
0
?
(
<
Empty
description=
"暂无技能包"
style=
{
{
marginTop
:
40
}
}
/>
)
:
(
<
Row
gutter=
{
[
16
,
16
]
}
>
{
filtered
.
map
(
skill
=>
{
let
toolCount
=
0
;
try
{
toolCount
=
JSON
.
parse
(
skill
.
tools
||
'
[]
'
).
length
;
}
catch
{
/* */
}
const
icon
=
SKILL_ICONS
[
skill
.
icon
]
||
<
ThunderboltOutlined
/>;
return
(
<
Col
key=
{
skill
.
skill_id
}
xs=
{
24
}
sm=
{
12
}
md=
{
8
}
lg=
{
6
}
>
<
Card
size=
"small"
style=
{
{
borderRadius
:
12
,
border
:
'
1px solid #E0F2F1
'
,
transition
:
'
all 0.2s
'
}
}
styles=
{
{
body
:
{
padding
:
16
}
}
}
hoverable
>
<
div
style=
{
{
display
:
'
flex
'
,
alignItems
:
'
flex-start
'
,
gap
:
12
,
marginBottom
:
10
}
}
>
<
div
style=
{
{
width
:
40
,
height
:
40
,
borderRadius
:
10
,
background
:
'
linear-gradient(135deg, #667eea, #764ba2)
'
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
justifyContent
:
'
center
'
,
color
:
'
#fff
'
,
fontSize
:
18
,
flexShrink
:
0
,
}
}
>
{
icon
}
</
div
>
<
div
style=
{
{
flex
:
1
,
minWidth
:
0
}
}
>
<
div
style=
{
{
fontWeight
:
600
,
fontSize
:
14
,
color
:
'
#1d2129
'
}
}
>
{
skill
.
name
}
</
div
>
<
div
style=
{
{
display
:
'
flex
'
,
gap
:
4
,
marginTop
:
4
}
}
>
<
Tag
color=
{
CATEGORY_COLORS
[
skill
.
category
]
||
'
default
'
}
style=
{
{
fontSize
:
11
,
margin
:
0
}
}
>
{
CATEGORY_LABELS
[
skill
.
category
]
||
skill
.
category
}
</
Tag
>
<
Tag
style=
{
{
fontSize
:
11
,
margin
:
0
}
}
>
{
toolCount
}
个工具
</
Tag
>
</
div
>
</
div
>
</
div
>
<
Paragraph
ellipsis=
{
{
rows
:
2
}
}
style=
{
{
fontSize
:
12
,
color
:
'
#595959
'
,
margin
:
'
0 0 10px
'
}
}
>
{
skill
.
description
||
'
暂无描述
'
}
</
Paragraph
>
<
div
style=
{
{
display
:
'
flex
'
,
justifyContent
:
'
flex-end
'
,
gap
:
4
}
}
>
<
Button
type=
"text"
size=
"small"
icon=
{
<
EditOutlined
/>
}
onClick=
{
()
=>
openEdit
(
skill
)
}
/>
<
Popconfirm
title=
"确定删除该技能包?"
onConfirm=
{
()
=>
deleteMut
.
mutate
(
skill
.
skill_id
)
}
>
<
Button
type=
"text"
size=
"small"
danger
icon=
{
<
DeleteOutlined
/>
}
/>
</
Popconfirm
>
</
div
>
</
Card
>
</
Col
>
);
})
}
</
Row
>
)
}
{
/* Create/Edit Modal */
}
<
Modal
title=
{
editingSkill
?
'
编辑技能包
'
:
'
新建技能包
'
}
open=
{
modalOpen
}
onCancel=
{
()
=>
{
setModalOpen
(
false
);
form
.
resetFields
();
setEditingSkill
(
null
);
}
}
onOk=
{
handleSave
}
confirmLoading=
{
saveMut
.
isPending
}
width=
{
640
}
destroyOnHidden
>
<
Form
form=
{
form
}
layout=
"vertical"
style=
{
{
marginTop
:
12
}
}
>
<
div
style=
{
{
display
:
'
grid
'
,
gridTemplateColumns
:
'
1fr 1fr
'
,
gap
:
'
0 16px
'
}
}
>
<
Form
.
Item
name=
"skill_id"
label=
"技能ID(英文)"
rules=
{
[{
required
:
true
,
message
:
'
必填
'
}]
}
>
<
Input
placeholder=
"symptom_analysis"
disabled=
{
!!
editingSkill
}
/>
</
Form
.
Item
>
<
Form
.
Item
name=
"name"
label=
"技能名称"
rules=
{
[{
required
:
true
,
message
:
'
必填
'
}]
}
>
<
Input
placeholder=
"症状分析"
/>
</
Form
.
Item
>
</
div
>
<
Form
.
Item
name=
"description"
label=
"描述"
>
<
TextArea
rows=
{
2
}
placeholder=
"技能功能说明"
/>
</
Form
.
Item
>
<
div
style=
{
{
display
:
'
grid
'
,
gridTemplateColumns
:
'
1fr 1fr
'
,
gap
:
'
0 16px
'
}
}
>
<
Form
.
Item
name=
"category"
label=
"分类"
initialValue=
"general"
>
<
Select
options=
{
Object
.
entries
(
CATEGORY_LABELS
).
map
(([
v
,
l
])
=>
({
value
:
v
,
label
:
l
}))
}
/>
</
Form
.
Item
>
<
Form
.
Item
name=
"icon"
label=
"图标"
initialValue=
"setting"
>
<
Select
options=
{
Object
.
keys
(
SKILL_ICONS
).
map
(
k
=>
({
value
:
k
,
label
:
k
}))
}
/>
</
Form
.
Item
>
</
div
>
<
Form
.
Item
name=
"tools"
label=
"关联工具"
>
<
Select
mode=
"multiple"
placeholder=
"选择工具"
options=
{
toolList
}
allowClear
filterOption=
{
(
input
,
option
)
=>
(
option
?.
label
??
''
).
toString
().
toLowerCase
().
includes
(
input
.
toLowerCase
())
}
/>
</
Form
.
Item
>
<
Form
.
Item
name=
"system_prompt_addon"
label=
"系统提示词追加"
>
<
TextArea
rows=
{
3
}
placeholder=
"加载此技能时追加到Agent系统提示的内容"
/>
</
Form
.
Item
>
<
Form
.
Item
name=
"quick_replies"
label=
"快捷回复(每行一个)"
>
<
TextArea
rows=
{
3
}
placeholder=
{
"
头痛
\n
发热
\n
咳嗽
"
}
/>
</
Form
.
Item
>
</
Form
>
</
Modal
>
</>
);
}
web/src/app/(main)/admin/agents/page.tsx
View file @
04584395
'
use client
'
;
'
use client
'
;
import
{
useState
,
useEffect
}
from
'
react
'
;
import
{
useState
,
useEffect
,
useMemo
}
from
'
react
'
;
import
{
import
{
Card
,
Table
,
Tag
,
Button
,
Drawer
,
Input
,
message
,
Space
,
Collapse
,
Timeline
,
Card
,
Table
,
Tag
,
Button
,
Modal
,
Input
,
message
,
Space
,
Collapse
,
Timeline
,
Typography
,
Tabs
,
DatePicker
,
Select
,
Badge
,
Tooltip
,
Form
,
InputNumber
,
Switch
,
Typography
,
Tabs
,
Select
,
Badge
,
Tooltip
,
Form
,
InputNumber
,
Switch
,
Segmented
,
}
from
'
antd
'
;
}
from
'
antd
'
;
import
{
DrawerForm
}
from
'
@ant-design/pro-components
'
;
import
{
import
{
RobotOutlined
,
PlayCircleOutlined
,
ToolOutlined
,
CheckCircleOutlined
,
RobotOutlined
,
PlayCircleOutlined
,
ToolOutlined
,
CheckCircleOutlined
,
CloseCircleOutlined
,
HistoryOutlined
,
ThunderboltOutlined
,
EditOutlined
,
CloseCircleOutlined
,
ThunderboltOutlined
,
EditOutlined
,
ReloadOutlined
,
PlusOutlined
,
ReloadOutlined
,
PlusOutlined
,
SearchOutlined
,
AppstoreOutlined
,
CloudOutlined
,
}
from
'
@ant-design/icons
'
;
}
from
'
@ant-design/icons
'
;
import
{
useQuery
,
useMutation
,
useQueryClient
}
from
'
@tanstack/react-query
'
;
import
{
useQuery
,
useMutation
,
useQueryClient
}
from
'
@tanstack/react-query
'
;
import
{
agentApi
,
skillApi
}
from
'
@/api/agent
'
;
import
{
agentApi
,
skillApi
,
httpToolApi
}
from
'
@/api/agent
'
;
import
type
{
ToolCall
,
AgentExecutionLog
,
AgentDefinition
}
from
'
@/api/agent
'
;
import
type
{
ToolCall
,
AgentDefinition
}
from
'
@/api/agent
'
;
import
AllToolsTab
from
'
./AllToolsTab
'
;
import
BuiltinToolsTab
from
'
./BuiltinToolsTab
'
;
import
HTTPToolsTab
from
'
./HTTPToolsTab
'
;
import
SkillsTab
from
'
./SkillsTab
'
;
import
{
CATEGORY_CONFIG
,
SOURCE_CONFIG
}
from
'
./toolsConfig
'
;
const
{
Text
}
=
Typography
;
const
{
Text
}
=
Typography
;
const
{
RangePicker
}
=
DatePicker
;
const
categoryColor
:
Record
<
string
,
string
>
=
{
const
categoryColor
:
Record
<
string
,
string
>
=
{
patient
:
'
green
'
,
doctor
:
'
blue
'
,
pharmacy
:
'
orange
'
,
admin
:
'
purple
'
,
general
:
'
cyan
'
,
patient
:
'
green
'
,
doctor
:
'
blue
'
,
pharmacy
:
'
orange
'
,
admin
:
'
purple
'
,
general
:
'
cyan
'
,
...
@@ -59,57 +64,33 @@ interface AgentResponse {
...
@@ -59,57 +64,33 @@ interface AgentResponse {
total_tokens
?:
number
;
total_tokens
?:
number
;
}
}
export
default
function
AgentsPage
()
{
/* ============ 智能体 Tab ============ */
function
AgentsTab
()
{
const
queryClient
=
useQueryClient
();
const
queryClient
=
useQueryClient
();
const
[
form
]
=
Form
.
useForm
();
const
[
form
]
=
Form
.
useForm
();
// 测试对话
const
[
testModal
,
setTestModal
]
=
useState
<
{
open
:
boolean
;
agentId
:
string
;
agentName
:
string
}
>
({
open
:
false
,
agentId
:
''
,
agentName
:
''
});
const
[
testModal
,
setTestModal
]
=
useState
<
{
open
:
boolean
;
agentId
:
string
;
agentName
:
string
}
>
({
open
:
false
,
agentId
:
''
,
agentName
:
''
});
const
[
testMessages
,
setTestMessages
]
=
useState
<
{
role
:
string
;
content
:
string
;
toolCalls
?:
ToolCall
[];
meta
?:
{
iterations
?:
number
;
tokens
?:
number
}
}[]
>
([]);
const
[
testMessages
,
setTestMessages
]
=
useState
<
{
role
:
string
;
content
:
string
;
toolCalls
?:
ToolCall
[];
meta
?:
{
iterations
?:
number
;
tokens
?:
number
}
}[]
>
([]);
const
[
inputMsg
,
setInputMsg
]
=
useState
(
''
);
const
[
inputMsg
,
setInputMsg
]
=
useState
(
''
);
const
[
chatLoading
,
setChatLoading
]
=
useState
(
false
);
const
[
chatLoading
,
setChatLoading
]
=
useState
(
false
);
const
[
sessionId
,
setSessionId
]
=
useState
(
''
);
const
[
sessionId
,
setSessionId
]
=
useState
(
''
);
// 编辑 Agent
const
[
editModal
,
setEditModal
]
=
useState
<
{
open
:
boolean
;
agent
:
AgentDefinition
|
null
;
isNew
:
boolean
}
>
({
open
:
false
,
agent
:
null
,
isNew
:
false
});
const
[
editModal
,
setEditModal
]
=
useState
<
{
open
:
boolean
;
agent
:
AgentDefinition
|
null
;
isNew
:
boolean
}
>
({
open
:
false
,
agent
:
null
,
isNew
:
false
});
// 可用工具列表
const
[
availableTools
,
setAvailableTools
]
=
useState
<
{
name
:
string
;
description
:
string
;
category
:
string
}[]
>
([]);
const
[
availableTools
,
setAvailableTools
]
=
useState
<
{
name
:
string
;
description
:
string
;
category
:
string
}[]
>
([]);
// 执行日志
const
[
logs
,
setLogs
]
=
useState
<
AgentExecutionLog
[]
>
([]);
const
[
logTotal
,
setLogTotal
]
=
useState
(
0
);
const
[
logLoading
,
setLogLoading
]
=
useState
(
false
);
const
[
logFilter
,
setLogFilter
]
=
useState
<
{
agent_id
?:
string
;
page
:
number
;
page_size
:
number
}
>
({
page
:
1
,
page_size
:
10
});
const
[
expandedLog
,
setExpandedLog
]
=
useState
<
AgentExecutionLog
|
null
>
(
null
);
// 加载 Agent 列表
const
{
data
:
agentsData
,
isLoading
:
agentsLoading
}
=
useQuery
({
const
{
data
:
agentsData
,
isLoading
:
agentsLoading
}
=
useQuery
({
queryKey
:
[
'
agent-definitions
'
],
queryKey
:
[
'
agent-definitions
'
],
queryFn
:
()
=>
agentApi
.
listDefinitions
(),
queryFn
:
()
=>
agentApi
.
listDefinitions
(),
});
});
const
agents
:
AgentDefinition
[]
=
agentsData
?.
data
||
[];
const
agents
:
AgentDefinition
[]
=
agentsData
?.
data
||
[];
// 加载工具列表
useEffect
(()
=>
{
useEffect
(()
=>
{
agentApi
.
listTools
().
then
(
res
=>
{
agentApi
.
listTools
().
then
(
res
=>
{
if
(
res
.
data
?.
length
>
0
)
{
if
(
res
.
data
?.
length
>
0
)
{
setAvailableTools
(
res
.
data
.
map
(
t
=>
({
name
:
t
.
name
,
description
:
t
.
description
,
category
:
t
.
category
})));
setAvailableTools
(
res
.
data
.
map
(
t
=>
({
name
:
t
.
name
,
description
:
t
.
description
,
category
:
t
.
category
})));
}
}
}).
catch
(()
=>
{});
}).
catch
(()
=>
{});
fetchLogs
();
},
[]);
},
[]);
const
fetchLogs
=
async
(
filter
=
logFilter
)
=>
{
setLogLoading
(
true
);
try
{
const
res
=
await
agentApi
.
getExecutionLogs
(
filter
);
setLogs
(
res
.
data
?.
list
||
[]);
setLogTotal
(
res
.
data
?.
total
||
0
);
}
catch
{}
finally
{
setLogLoading
(
false
);
}
};
// 保存 Agent(创建或更新)
const
saveMutation
=
useMutation
({
const
saveMutation
=
useMutation
({
mutationFn
:
(
values
:
Record
<
string
,
unknown
>
)
=>
{
mutationFn
:
(
values
:
Record
<
string
,
unknown
>
)
=>
{
const
toolsArr
=
values
.
tools_array
as
string
[]
||
[];
const
toolsArr
=
values
.
tools_array
as
string
[]
||
[];
...
@@ -120,7 +101,7 @@ export default function AgentsPage() {
...
@@ -120,7 +101,7 @@ export default function AgentsPage() {
description
:
values
.
description
as
string
,
description
:
values
.
description
as
string
,
category
:
values
.
category
as
string
,
category
:
values
.
category
as
string
,
system_prompt
:
values
.
system_prompt
as
string
,
system_prompt
:
values
.
system_prompt
as
string
,
tools
:
toolsArr
,
tools
:
JSON
.
stringify
(
toolsArr
)
,
skills
:
skillsArr
,
skills
:
skillsArr
,
max_iterations
:
values
.
max_iterations
as
number
,
max_iterations
:
values
.
max_iterations
as
number
,
status
:
values
.
status
as
string
,
status
:
values
.
status
as
string
,
...
@@ -140,7 +121,6 @@ export default function AgentsPage() {
...
@@ -140,7 +121,6 @@ export default function AgentsPage() {
onError
:
()
=>
message
.
error
(
'
操作失败
'
),
onError
:
()
=>
message
.
error
(
'
操作失败
'
),
});
});
// 热重载
const
reloadMutation
=
useMutation
({
const
reloadMutation
=
useMutation
({
mutationFn
:
(
agentId
:
string
)
=>
agentApi
.
reloadAgent
(
agentId
),
mutationFn
:
(
agentId
:
string
)
=>
agentApi
.
reloadAgent
(
agentId
),
onSuccess
:
()
=>
{
onSuccess
:
()
=>
{
...
@@ -163,15 +143,10 @@ export default function AgentsPage() {
...
@@ -163,15 +143,10 @@ export default function AgentsPage() {
try
{
toolsArr
=
JSON
.
parse
(
agent
.
tools
||
'
[]
'
);
}
catch
{}
try
{
toolsArr
=
JSON
.
parse
(
agent
.
tools
||
'
[]
'
);
}
catch
{}
try
{
skillsArr
=
JSON
.
parse
(
agent
.
skills
||
'
[]
'
);
}
catch
{}
try
{
skillsArr
=
JSON
.
parse
(
agent
.
skills
||
'
[]
'
);
}
catch
{}
form
.
setFieldsValue
({
form
.
setFieldsValue
({
agent_id
:
agent
.
agent_id
,
agent_id
:
agent
.
agent_id
,
name
:
agent
.
name
,
description
:
agent
.
description
,
name
:
agent
.
name
,
category
:
agent
.
category
,
system_prompt
:
agent
.
system_prompt
,
description
:
agent
.
description
,
tools_array
:
toolsArr
,
skills_array
:
skillsArr
,
category
:
agent
.
category
,
max_iterations
:
agent
.
max_iterations
,
status
:
agent
.
status
===
'
active
'
,
system_prompt
:
agent
.
system_prompt
,
tools_array
:
toolsArr
,
skills_array
:
skillsArr
,
max_iterations
:
agent
.
max_iterations
,
status
:
agent
.
status
===
'
active
'
,
});
});
setEditModal
({
open
:
true
,
agent
,
isNew
:
false
});
setEditModal
({
open
:
true
,
agent
,
isNew
:
false
});
}
else
{
}
else
{
...
@@ -230,12 +205,30 @@ export default function AgentsPage() {
...
@@ -230,12 +205,30 @@ export default function AgentsPage() {
);
);
};
};
// 按分类分组工具选项
const
toolCategoryLabels
:
Record
<
string
,
string
>
=
{
knowledge
:
'
知识库
'
,
recommendation
:
'
智能推荐
'
,
medical
:
'
病历管理
'
,
pharmacy
:
'
药品管理
'
,
safety
:
'
安全检查
'
,
follow_up
:
'
随访管理
'
,
notification
:
'
消息通知
'
,
agent
:
'
Agent调用
'
,
workflow
:
'
工作流
'
,
expression
:
'
表达式
'
,
other
:
'
其他
'
,
};
const
toolsByCategory
=
availableTools
.
reduce
((
acc
,
t
)
=>
{
const
cat
=
t
.
category
||
'
other
'
;
if
(
!
acc
[
cat
])
acc
[
cat
]
=
[];
acc
[
cat
].
push
(
t
);
return
acc
;
},
{}
as
Record
<
string
,
typeof
availableTools
>
);
const
toolOptions
=
Object
.
entries
(
toolsByCategory
).
map
(([
cat
,
tools
])
=>
({
label
:
toolCategoryLabels
[
cat
]
||
cat
,
options
:
tools
.
map
(
t
=>
({
value
:
t
.
name
,
label
:
t
.
name
,
title
:
t
.
description
})),
}));
const
agentColumns
=
[
const
agentColumns
=
[
{
{
title
:
'
智能体
'
,
key
:
'
name
'
,
title
:
'
智能体
'
,
key
:
'
name
'
,
render
:
(
_
:
unknown
,
r
:
AgentDefinition
)
=>
(
render
:
(
_
:
unknown
,
r
:
AgentDefinition
)
=>
(
<
Space
>
<
Space
>
<
RobotOutlined
style=
{
{
color
:
'
#
1890ff
'
}
}
/>
<
RobotOutlined
style=
{
{
color
:
'
#
0D9488
'
}
}
/>
<
div
>
<
div
>
<
Text
strong
>
{
r
.
name
}
</
Text
>
<
Text
strong
>
{
r
.
name
}
</
Text
>
<
br
/>
<
br
/>
...
@@ -278,195 +271,84 @@ export default function AgentsPage() {
...
@@ -278,195 +271,84 @@ export default function AgentsPage() {
},
},
];
];
const
logColumns
=
[
{
title
:
'
时间
'
,
dataIndex
:
'
created_at
'
,
key
:
'
created_at
'
,
width
:
160
,
render
:
(
v
:
string
)
=>
<
Text
style=
{
{
fontSize
:
12
}
}
>
{
new
Date
(
v
).
toLocaleString
(
'
zh-CN
'
)
}
</
Text
>,
},
{
title
:
'
智能体
'
,
dataIndex
:
'
agent_id
'
,
key
:
'
agent_id
'
,
width
:
160
,
render
:
(
v
:
string
)
=>
<
Tag
color=
{
categoryColor
[
v
?.
replace
(
'
_agent
'
,
''
)]
||
'
default
'
}
>
{
v
}
</
Tag
>,
},
{
title
:
'
用户ID
'
,
dataIndex
:
'
user_id
'
,
key
:
'
user_id
'
,
width
:
130
,
ellipsis
:
true
,
render
:
(
v
:
string
)
=>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
12
}
}
>
{
v
}
</
Text
>
},
{
title
:
'
输入摘要
'
,
dataIndex
:
'
input
'
,
key
:
'
input
'
,
ellipsis
:
true
,
render
:
(
v
:
string
)
=>
{
try
{
const
obj
=
JSON
.
parse
(
v
);
return
<
Tooltip
title=
{
obj
.
message
}
><
Text
style=
{
{
fontSize
:
12
}
}
>
{
(
obj
.
message
||
''
).
slice
(
0
,
30
)
}
...
</
Text
></
Tooltip
>;
}
catch
{
return
v
;
}
},
},
{
title
:
'
迭代
'
,
dataIndex
:
'
iterations
'
,
key
:
'
iterations
'
,
width
:
70
,
render
:
(
v
:
number
)
=>
<
Badge
count=
{
v
}
style=
{
{
backgroundColor
:
'
#722ed1
'
}
}
/>
},
{
title
:
'
Tokens
'
,
dataIndex
:
'
total_tokens
'
,
key
:
'
total_tokens
'
,
width
:
80
,
render
:
(
v
:
number
)
=>
<
Text
style=
{
{
fontSize
:
12
}
}
>
{
v
}
</
Text
>
},
{
title
:
'
耗时(ms)
'
,
dataIndex
:
'
duration_ms
'
,
key
:
'
duration_ms
'
,
width
:
90
,
render
:
(
v
:
number
)
=>
<
Text
style=
{
{
fontSize
:
12
}
}
>
{
v
}
</
Text
>
},
{
title
:
'
状态
'
,
dataIndex
:
'
success
'
,
key
:
'
success
'
,
width
:
80
,
render
:
(
v
:
boolean
)
=>
v
?
<
Badge
status=
"success"
text=
"成功"
/>
:
<
Badge
status=
"error"
text=
"失败"
/>,
},
{
title
:
'
操作
'
,
key
:
'
action
'
,
width
:
80
,
render
:
(
_
:
unknown
,
record
:
AgentExecutionLog
)
=>
(
<
Button
type=
"link"
size=
"small"
icon=
{
<
HistoryOutlined
/>
}
onClick=
{
()
=>
setExpandedLog
(
record
)
}
>
详情
</
Button
>
),
},
];
// 按分类分组工具选项
const
toolCategoryLabels
:
Record
<
string
,
string
>
=
{
knowledge
:
'
知识库
'
,
recommendation
:
'
智能推荐
'
,
medical
:
'
病历管理
'
,
pharmacy
:
'
药品管理
'
,
safety
:
'
安全检查
'
,
follow_up
:
'
随访管理
'
,
notification
:
'
消息通知
'
,
agent
:
'
Agent调用
'
,
workflow
:
'
工作流
'
,
expression
:
'
表达式
'
,
other
:
'
其他
'
,
};
const
toolsByCategory
=
availableTools
.
reduce
((
acc
,
t
)
=>
{
const
cat
=
t
.
category
||
'
other
'
;
if
(
!
acc
[
cat
])
acc
[
cat
]
=
[];
acc
[
cat
].
push
(
t
);
return
acc
;
},
{}
as
Record
<
string
,
typeof
availableTools
>
);
const
toolOptions
=
Object
.
entries
(
toolsByCategory
).
map
(([
cat
,
tools
])
=>
({
label
:
toolCategoryLabels
[
cat
]
||
cat
,
options
:
tools
.
map
(
t
=>
({
value
:
t
.
name
,
label
:
t
.
name
,
title
:
t
.
description
})),
}));
return
(
return
(
<
div
style=
{
{
padding
:
'
20px 24px
'
,
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
gap
:
16
}
}
>
<>
<
div
>
<
div
style=
{
{
display
:
'
flex
'
,
justifyContent
:
'
flex-end
'
,
marginBottom
:
12
}
}
>
<
h2
style=
{
{
fontSize
:
20
,
fontWeight
:
700
,
color
:
'
#1d2129
'
,
margin
:
0
}
}
>
智能体管理
</
h2
>
<
Button
type=
"primary"
icon=
{
<
PlusOutlined
/>
}
onClick=
{
()
=>
openEdit
()
}
>
新增智能体
</
Button
>
<
div
style=
{
{
fontSize
:
13
,
color
:
'
#8c8c8c
'
,
marginTop
:
2
}
}
>
从数据库加载 Agent 配置,支持在线编辑、热重载与对话测试
</
div
>
</
div
>
</
div
>
<
Table
dataSource=
{
agents
}
columns=
{
agentColumns
}
rowKey=
"agent_id"
loading=
{
agentsLoading
}
pagination=
{
false
}
size=
"small"
scroll=
{
{
x
:
1100
}
}
/>
<
Card
style=
{
{
borderRadius
:
12
,
border
:
'
1px solid #edf2fc
'
}
}
>
{
/* 新增/编辑 Agent Modal */
}
<
Tabs
<
Modal
tabBarExtraContent=
{
title=
{
editModal
.
isNew
?
'
新增智能体
'
:
`编辑 · ${editModal.agent?.name}`
}
<
Button
type=
"primary"
icon=
{
<
PlusOutlined
/>
}
onClick=
{
()
=>
openEdit
()
}
>
新增 Agent
</
Button
>
}
items=
{
[
{
key
:
'
agents
'
,
label
:
<
Space
><
RobotOutlined
/>
智能体列表
</
Space
>,
children
:
(
<
Table
dataSource=
{
agents
}
columns=
{
agentColumns
}
rowKey=
"agent_id"
loading=
{
agentsLoading
}
pagination=
{
false
}
size=
"small"
scroll=
{
{
x
:
1100
}
}
/>
),
},
{
key
:
'
logs
'
,
label
:
<
Space
><
HistoryOutlined
/>
执行日志
</
Space
>,
children
:
(
<
div
style=
{
{
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
gap
:
12
}
}
>
<
div
style=
{
{
display
:
'
flex
'
,
gap
:
8
,
alignItems
:
'
center
'
,
flexWrap
:
'
wrap
'
}
}
>
<
Select
placeholder=
"筛选智能体"
allowClear
style=
{
{
width
:
200
}
}
options=
{
agents
.
map
(
a
=>
({
value
:
a
.
agent_id
,
label
:
a
.
name
}))
}
onChange=
{
v
=>
{
const
newFilter
=
{
...
logFilter
,
agent_id
:
v
,
page
:
1
};
setLogFilter
(
newFilter
);
fetchLogs
(
newFilter
);
}
}
/>
<
RangePicker
onChange=
{
(
_
,
strs
)
=>
{
const
newFilter
=
{
...
logFilter
,
...(
strs
[
0
]
?
{
start
:
strs
[
0
]
}
:
{}),
...(
strs
[
1
]
?
{
end
:
strs
[
1
]
}
:
{}),
page
:
1
};
setLogFilter
(
newFilter
);
fetchLogs
(
newFilter
);
}
}
/>
<
Button
icon=
{
<
ThunderboltOutlined
/>
}
onClick=
{
()
=>
fetchLogs
()
}
>
刷新
</
Button
>
</
div
>
<
Table
dataSource=
{
logs
}
columns=
{
logColumns
}
rowKey=
"id"
loading=
{
logLoading
}
size=
"small"
pagination=
{
{
current
:
logFilter
.
page
,
pageSize
:
logFilter
.
page_size
,
total
:
logTotal
,
size
:
'
small
'
,
showTotal
:
(
t
)
=>
`共 ${t} 条`
,
onChange
:
(
page
,
pageSize
)
=>
{
const
newFilter
=
{
...
logFilter
,
page
,
page_size
:
pageSize
};
setLogFilter
(
newFilter
);
fetchLogs
(
newFilter
);
},
}
}
/>
</
div
>
),
},
]
}
/>
</
Card
>
{
/* 新增/编辑 Agent DrawerForm */
}
<
DrawerForm
title=
{
editModal
.
isNew
?
'
新增 Agent
'
:
`编辑 · ${editModal.agent?.name}`
}
open=
{
editModal
.
open
}
open=
{
editModal
.
open
}
onOpenChange=
{
(
open
)
=>
{
if
(
!
open
)
{
setEditModal
({
open
:
false
,
agent
:
null
,
isNew
:
false
});
form
.
resetFields
();
}
}
}
onCancel=
{
()
=>
{
setEditModal
({
open
:
false
,
agent
:
null
,
isNew
:
false
});
form
.
resetFields
();
}
}
onFinish=
{
async
(
values
)
=>
{
saveMutation
.
mutate
({
...
values
,
status
:
values
.
status
?
'
active
'
:
'
disabled
'
});
return
true
;
}
}
onOk=
{
()
=>
form
.
submit
()
}
drawerProps=
{
{
placement
:
'
right
'
,
destroyOnClose
:
true
}
}
confirmLoading=
{
saveMutation
.
isPending
}
width=
{
600
}
width=
{
700
}
loading=
{
saveMutation
.
isPending
}
form=
{
form
}
>
>
<
Form
.
Item
name=
"agent_id"
label=
"Agent ID"
rules=
{
[{
required
:
true
,
message
:
'
请输入 Agent ID
'
}]
}
>
<
Form
<
Input
placeholder=
"如: doctor_universal_agent"
disabled=
{
!
editModal
.
isNew
}
/>
form=
{
form
}
</
Form
.
Item
>
layout=
"vertical"
<
Form
.
Item
name=
"name"
label=
"名称"
rules=
{
[{
required
:
true
}]
}
>
onFinish=
{
(
values
)
=>
saveMutation
.
mutate
({
...
values
,
status
:
values
.
status
?
'
active
'
:
'
disabled
'
})
}
<
Input
placeholder=
"如: 诊断辅助 Agent"
/>
>
</
Form
.
Item
>
<
Form
.
Item
name=
"agent_id"
label=
"Agent ID"
rules=
{
[{
required
:
true
,
message
:
'
请输入 Agent ID
'
}]
}
>
<
Form
.
Item
name=
"description"
label=
"描述"
>
<
Input
placeholder=
"如: doctor_universal_agent"
disabled=
{
!
editModal
.
isNew
}
/>
<
Input
placeholder=
"简短描述 Agent 的功能"
/>
</
Form
.
Item
>
</
Form
.
Item
>
<
Form
.
Item
name=
"name"
label=
"名称"
rules=
{
[{
required
:
true
}]
}
>
<
Form
.
Item
name=
"category"
label=
"类别"
>
<
Input
placeholder=
"如: 诊断辅助 Agent"
/>
<
Select
options=
{
CATEGORY_OPTIONS
}
placeholder=
"选择类别"
/>
</
Form
.
Item
>
</
Form
.
Item
>
<
Form
.
Item
name=
"description"
label=
"描述"
>
<
Form
.
Item
name=
"system_prompt"
label=
"系统提示词"
>
<
Input
placeholder=
"简短描述 Agent 的功能"
/>
<
Input
.
TextArea
rows=
{
5
}
placeholder=
"输入 Agent 的系统提示词(留空则使用数据库中关联的提示词模板)"
/>
</
Form
.
Item
>
</
Form
.
Item
>
<
Form
.
Item
name=
"category"
label=
"类别"
>
<
Form
.
Item
name=
"tools_array"
label=
"关联工具"
>
<
Select
options=
{
CATEGORY_OPTIONS
}
placeholder=
"选择类别"
/>
<
Select
</
Form
.
Item
>
mode=
"multiple"
<
Form
.
Item
name=
"system_prompt"
label=
"系统提示词"
>
options=
{
toolOptions
}
<
Input
.
TextArea
rows=
{
5
}
placeholder=
"输入 Agent 的系统提示词(留空则使用数据库中关联的提示词模板)"
/>
placeholder=
"选择 Agent 可使用的工具"
</
Form
.
Item
>
allowClear
<
Form
.
Item
name=
"tools_array"
label=
"关联工具"
>
/>
<
Select
</
Form
.
Item
>
mode=
"multiple"
<
Form
.
Item
name=
"skills_array"
label=
"技能包"
>
options=
{
toolOptions
}
<
SkillSelect
/>
placeholder=
"选择 Agent 可使用的工具"
</
Form
.
Item
>
allowClear
<
Form
.
Item
name=
"max_iterations"
label=
"最大迭代次数"
>
/>
<
InputNumber
min=
{
1
}
max=
{
50
}
style=
{
{
width
:
120
}
}
/>
</
Form
.
Item
>
</
Form
.
Item
>
<
Form
.
Item
name=
"skills_array"
label=
"技能包"
>
<
Form
.
Item
name=
"status"
label=
"状态"
valuePropName=
"checked"
>
<
SkillSelect
/>
<
Switch
checkedChildren=
"启用"
unCheckedChildren=
"停用"
/>
</
Form
.
Item
>
</
Form
.
Item
>
<
Form
.
Item
name=
"max_iterations"
label=
"最大迭代次数"
>
</
DrawerForm
>
<
InputNumber
min=
{
1
}
max=
{
50
}
style=
{
{
width
:
120
}
}
/>
</
Form
.
Item
>
<
Form
.
Item
name=
"status"
label=
"状态"
valuePropName=
"checked"
>
<
Switch
checkedChildren=
"启用"
unCheckedChildren=
"停用"
/>
</
Form
.
Item
>
</
Form
>
</
Modal
>
{
/* 测试对话
Drawer
*/
}
{
/* 测试对话
Modal
*/
}
<
Drawer
<
Modal
title=
{
`测试 · ${testModal.agentName}`
}
title=
{
`测试 · ${testModal.agentName}`
}
open=
{
testModal
.
open
}
open=
{
testModal
.
open
}
onClose=
{
()
=>
setTestModal
({
open
:
false
,
agentId
:
''
,
agentName
:
''
})
}
onCancel=
{
()
=>
setTestModal
({
open
:
false
,
agentId
:
''
,
agentName
:
''
})
}
placement=
"right"
footer=
{
null
}
destroyOnClose
width=
{
700
}
width=
{
600
}
>
>
<
div
style=
{
{
height
:
400
,
overflowY
:
'
auto
'
,
border
:
'
1px solid #f0f0f0
'
,
borderRadius
:
8
,
padding
:
12
,
marginBottom
:
12
}
}
>
<
div
style=
{
{
height
:
400
,
overflowY
:
'
auto
'
,
border
:
'
1px solid #f0f0f0
'
,
borderRadius
:
8
,
padding
:
12
,
marginBottom
:
12
}
}
>
{
testMessages
.
map
((
m
,
i
)
=>
(
{
testMessages
.
map
((
m
,
i
)
=>
(
<
div
key=
{
i
}
style=
{
{
marginBottom
:
12
,
textAlign
:
m
.
role
===
'
user
'
?
'
right
'
:
'
left
'
}
}
>
<
div
key=
{
i
}
style=
{
{
marginBottom
:
12
,
textAlign
:
m
.
role
===
'
user
'
?
'
right
'
:
'
left
'
}
}
>
<
div
style=
{
{
<
div
style=
{
{
display
:
'
inline-block
'
,
padding
:
'
8px 14px
'
,
borderRadius
:
8
,
maxWidth
:
'
85%
'
,
display
:
'
inline-block
'
,
padding
:
'
8px 14px
'
,
borderRadius
:
8
,
maxWidth
:
'
85%
'
,
background
:
m
.
role
===
'
user
'
?
'
#
1890ff
'
:
'
#f5f5f5
'
,
background
:
m
.
role
===
'
user
'
?
'
#
0D9488
'
:
'
#f5f5f5
'
,
color
:
m
.
role
===
'
user
'
?
'
#fff
'
:
'
#333
'
,
color
:
m
.
role
===
'
user
'
?
'
#fff
'
:
'
#333
'
,
textAlign
:
'
left
'
,
textAlign
:
'
left
'
,
}
}
>
}
}
>
...
@@ -486,45 +368,131 @@ export default function AgentsPage() {
...
@@ -486,45 +368,131 @@ export default function AgentsPage() {
<
Input
value=
{
inputMsg
}
onChange=
{
e
=>
setInputMsg
(
e
.
target
.
value
)
}
onPressEnter=
{
sendMessage
}
placeholder=
"输入消息..."
/>
<
Input
value=
{
inputMsg
}
onChange=
{
e
=>
setInputMsg
(
e
.
target
.
value
)
}
onPressEnter=
{
sendMessage
}
placeholder=
"输入消息..."
/>
<
Button
type=
"primary"
onClick=
{
sendMessage
}
loading=
{
chatLoading
}
>
发送
</
Button
>
<
Button
type=
"primary"
onClick=
{
sendMessage
}
loading=
{
chatLoading
}
>
发送
</
Button
>
</
Space
.
Compact
>
</
Space
.
Compact
>
</
Drawer
>
</
Modal
>
</>
);
}
{
/* 执行日志详情 Drawer */
}
/* ============ 工具 Tab (内嵌子Tab) ============ */
<
Drawer
function
ToolsTab
()
{
title=
{
<
Space
><
HistoryOutlined
/>
执行日志详情
</
Space
>
}
const
[
search
,
setSearch
]
=
useState
(
''
);
open=
{
!!
expandedLog
}
const
[
categoryFilter
,
setCategoryFilter
]
=
useState
(
'
all
'
);
onClose=
{
()
=>
setExpandedLog
(
null
)
}
const
[
activeSubTab
,
setActiveSubTab
]
=
useState
(
'
all
'
);
placement=
"right"
destroyOnClose
const
{
data
:
builtinData
}
=
useQuery
({
width=
{
600
}
queryKey
:
[
'
agent-tools
'
],
>
queryFn
:
()
=>
agentApi
.
listTools
(),
{
expandedLog
&&
(()
=>
{
select
:
r
=>
(
r
.
data
??
[])
as
{
name
:
string
;
is_enabled
:
boolean
;
category
:
string
}[],
let
toolCalls
:
ToolCall
[]
=
[];
});
try
{
toolCalls
=
JSON
.
parse
(
expandedLog
.
tool_calls
||
'
[]
'
);
}
catch
{}
const
{
data
:
httpData
}
=
useQuery
({
let
inputObj
:
Record
<
string
,
unknown
>
=
{};
queryKey
:
[
'
http-tools
'
],
try
{
inputObj
=
JSON
.
parse
(
expandedLog
.
input
||
'
{}
'
);
}
catch
{}
queryFn
:
()
=>
httpToolApi
.
list
(),
let
outputObj
:
Record
<
string
,
unknown
>
=
{};
select
:
r
=>
r
.
data
??
[],
try
{
outputObj
=
JSON
.
parse
(
expandedLog
.
output
||
'
{}
'
);
}
catch
{}
});
return
(
<
div
style=
{
{
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
gap
:
12
}
}
>
const
totalCount
=
(
builtinData
?.
length
||
0
)
+
(
httpData
?.
length
||
0
);
<
div
style=
{
{
display
:
'
grid
'
,
gridTemplateColumns
:
'
1fr 1fr
'
,
gap
:
8
,
fontSize
:
13
}
}
>
const
enabledCount
=
(
builtinData
?.
filter
(
t
=>
t
.
is_enabled
).
length
||
0
)
+
<
div
><
Text
type=
"secondary"
>
智能体:
</
Text
><
Tag
>
{
expandedLog
.
agent_id
}
</
Tag
></
div
>
(
httpData
?.
filter
(
t
=>
t
.
status
===
'
active
'
).
length
||
0
);
<
div
><
Text
type=
"secondary"
>
状态:
</
Text
><
Badge
status=
{
expandedLog
.
success
?
'
success
'
:
'
error
'
}
text=
{
expandedLog
.
success
?
'
成功
'
:
'
失败
'
}
/></
div
>
<
div
><
Text
type=
"secondary"
>
迭代次数:
</
Text
>
{
expandedLog
.
iterations
}
</
div
>
const
allCategories
=
useMemo
(()
=>
{
<
div
><
Text
type=
"secondary"
>
耗时:
</
Text
>
{
expandedLog
.
duration_ms
}
ms
</
div
>
const
cats
=
new
Set
<
string
>
();
<
div
><
Text
type=
"secondary"
>
Tokens:
</
Text
>
{
expandedLog
.
total_tokens
}
</
div
>
builtinData
?.
forEach
(
t
=>
cats
.
add
(
t
.
category
));
<
div
><
Text
type=
"secondary"
>
完成原因:
</
Text
>
{
expandedLog
.
finish_reason
||
'
-
'
}
</
div
>
httpData
?.
forEach
(
t
=>
cats
.
add
(
t
.
category
||
'
http
'
));
</
div
>
return
[...
cats
];
<
Card
size=
"small"
title=
"用户输入"
>
},
[
builtinData
,
httpData
]);
<
Text
style=
{
{
fontSize
:
12
}
}
>
{
(
inputObj
.
message
as
string
)
||
expandedLog
.
input
}
</
Text
>
</
Card
>
return
(
<
Card
size=
"small"
title=
"AI 回复"
>
<
div
style=
{
{
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
gap
:
12
}
}
>
<
Text
style=
{
{
fontSize
:
12
}
}
>
{
(
outputObj
.
response
as
string
)
||
expandedLog
.
output
}
</
Text
>
{
/* 统计 + 筛选 */
}
</
Card
>
<
div
style=
{
{
{
renderToolCalls
(
toolCalls
)
}
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
12
,
flexWrap
:
'
wrap
'
,
</
div
>
}
}
>
);
<
Input
})()
}
placeholder=
"搜索工具名称或描述..."
</
Drawer
>
prefix=
{
<
SearchOutlined
/>
}
value=
{
search
}
onChange=
{
e
=>
setSearch
(
e
.
target
.
value
)
}
style=
{
{
width
:
240
,
borderRadius
:
8
}
}
allowClear
/>
{
(
activeSubTab
===
'
all
'
||
activeSubTab
===
'
builtin
'
)
&&
(
<
Segmented
value=
{
categoryFilter
}
onChange=
{
v
=>
setCategoryFilter
(
v
as
string
)
}
options=
{
[
{
value
:
'
all
'
,
label
:
'
全部分类
'
},
...
allCategories
.
map
(
c
=>
({
value
:
c
,
label
:
CATEGORY_CONFIG
[
c
]?.
label
||
c
})),
]
}
/>
)
}
<
div
style=
{
{
marginLeft
:
'
auto
'
,
fontSize
:
13
,
color
:
'
#8c8c8c
'
}
}
>
共
<
Text
strong
>
{
totalCount
}
</
Text
>
个工具,已启用
<
Text
strong
style=
{
{
color
:
'
#52c41a
'
}
}
>
{
enabledCount
}
</
Text
>
个
</
div
>
</
div
>
<
Tabs
activeKey=
{
activeSubTab
}
onChange=
{
setActiveSubTab
}
size=
"small"
items=
{
[
{
key
:
'
all
'
,
label
:
<
span
><
AppstoreOutlined
/>
全部
</
span
>,
children
:
<
AllToolsTab
search=
{
search
}
categoryFilter=
{
categoryFilter
}
CATEGORY_CONFIG=
{
CATEGORY_CONFIG
}
SOURCE_CONFIG=
{
SOURCE_CONFIG
}
/>,
},
{
key
:
'
builtin
'
,
label
:
<
span
><
ToolOutlined
/>
内置工具
</
span
>,
children
:
<
BuiltinToolsTab
search=
{
search
}
categoryFilter=
{
categoryFilter
}
CATEGORY_CONFIG=
{
CATEGORY_CONFIG
}
/>,
},
{
key
:
'
http
'
,
label
:
<
span
><
CloudOutlined
/>
HTTP 工具
</
span
>,
children
:
<
HTTPToolsTab
search=
{
search
}
/>,
},
]
}
/>
</
div
>
);
}
/* ============ 主页面 ============ */
export
default
function
AgentManagementPage
()
{
const
[
activeTab
,
setActiveTab
]
=
useState
(
'
agents
'
);
return
(
<
div
style=
{
{
padding
:
'
20px 24px
'
,
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
gap
:
16
}
}
>
<
div
>
<
h2
style=
{
{
fontSize
:
20
,
fontWeight
:
700
,
color
:
'
#1d2129
'
,
margin
:
0
}
}
>
<
RobotOutlined
style=
{
{
marginRight
:
8
,
color
:
'
#0D9488
'
}
}
/>
智能体管理
</
h2
>
<
div
style=
{
{
fontSize
:
13
,
color
:
'
#8c8c8c
'
,
marginTop
:
2
}
}
>
管理智能体、工具与技能,支持在线编辑、热重载与对话测试
</
div
>
</
div
>
<
Card
style=
{
{
borderRadius
:
12
,
border
:
'
1px solid #E0F2F1
'
}
}
>
<
Tabs
activeKey=
{
activeTab
}
onChange=
{
setActiveTab
}
items=
{
[
{
key
:
'
agents
'
,
label
:
<
Space
><
RobotOutlined
/>
智能体
</
Space
>,
children
:
<
AgentsTab
/>,
},
{
key
:
'
tools
'
,
label
:
<
Space
><
ToolOutlined
/>
Tools 工具
</
Space
>,
children
:
<
ToolsTab
/>,
},
{
key
:
'
skills
'
,
label
:
<
Space
><
ThunderboltOutlined
/>
Skill 技能
</
Space
>,
children
:
<
SkillsTab
search=
""
/>,
},
]
}
/>
</
Card
>
</
div
>
</
div
>
);
);
}
}
web/src/app/(main)/admin/agents/toolsConfig.ts
0 → 100644
View file @
04584395
import
React
from
'
react
'
;
import
{
ToolOutlined
,
BookOutlined
,
ThunderboltOutlined
,
HeartOutlined
,
SafetyOutlined
,
BellOutlined
,
RobotOutlined
,
DeploymentUnitOutlined
,
CodeOutlined
,
ApiOutlined
,
}
from
'
@ant-design/icons
'
;
export
const
CATEGORY_CONFIG
:
Record
<
string
,
{
color
:
string
;
label
:
string
;
icon
:
React
.
ReactNode
}
>
=
{
knowledge
:
{
color
:
'
blue
'
,
label
:
'
知识库
'
,
icon
:
React
.
createElement
(
BookOutlined
)
},
recommendation
:
{
color
:
'
cyan
'
,
label
:
'
智能推荐
'
,
icon
:
React
.
createElement
(
ThunderboltOutlined
)
},
medical
:
{
color
:
'
purple
'
,
label
:
'
病历管理
'
,
icon
:
React
.
createElement
(
HeartOutlined
)
},
pharmacy
:
{
color
:
'
green
'
,
label
:
'
药品管理
'
,
icon
:
React
.
createElement
(
ToolOutlined
)
},
safety
:
{
color
:
'
red
'
,
label
:
'
安全检查
'
,
icon
:
React
.
createElement
(
SafetyOutlined
)
},
follow_up
:
{
color
:
'
orange
'
,
label
:
'
随访管理
'
,
icon
:
React
.
createElement
(
HeartOutlined
)
},
notification
:
{
color
:
'
gold
'
,
label
:
'
消息通知
'
,
icon
:
React
.
createElement
(
BellOutlined
)
},
agent
:
{
color
:
'
geekblue
'
,
label
:
'
Agent调用
'
,
icon
:
React
.
createElement
(
RobotOutlined
)
},
workflow
:
{
color
:
'
volcano
'
,
label
:
'
工作流
'
,
icon
:
React
.
createElement
(
DeploymentUnitOutlined
)
},
expression
:
{
color
:
'
lime
'
,
label
:
'
表达式
'
,
icon
:
React
.
createElement
(
CodeOutlined
)
},
http
:
{
color
:
'
magenta
'
,
label
:
'
HTTP服务
'
,
icon
:
React
.
createElement
(
ApiOutlined
)
},
other
:
{
color
:
'
default
'
,
label
:
'
其他
'
,
icon
:
React
.
createElement
(
ToolOutlined
)
},
};
export
type
CATEGORY_CONFIG_TYPE
=
typeof
CATEGORY_CONFIG
;
export
const
SOURCE_CONFIG
=
{
builtin
:
{
label
:
'
内置
'
,
color
:
'
#0D9488
'
},
http
:
{
label
:
'
HTTP
'
,
color
:
'
#fa8c16
'
},
}
as
const
;
export
type
SOURCE_CONFIG_TYPE
=
typeof
SOURCE_CONFIG
;
web/src/app/(main)/admin/ai-logs/page.tsx
0 → 100644
View file @
04584395
'
use client
'
;
import
React
,
{
useState
}
from
'
react
'
;
import
{
Card
,
Table
,
Tag
,
Button
,
Space
,
Modal
,
Typography
,
Row
,
Col
,
Statistic
,
Tabs
,
Input
,
Select
,
Timeline
,
Spin
,
Empty
,
DatePicker
,
Badge
,
Collapse
,
}
from
'
antd
'
;
import
{
FileTextOutlined
,
SearchOutlined
,
ToolOutlined
,
RobotOutlined
,
ThunderboltOutlined
,
CheckCircleOutlined
,
CloseCircleOutlined
,
HistoryOutlined
,
FundOutlined
,
}
from
'
@ant-design/icons
'
;
import
{
useQuery
}
from
'
@tanstack/react-query
'
;
import
{
agentApi
}
from
'
@/api/agent
'
;
import
type
{
AgentExecutionLog
,
ToolCall
}
from
'
@/api/agent
'
;
import
type
{
ColumnsType
}
from
'
antd/es/table
'
;
import
dayjs
from
'
dayjs
'
;
const
{
Text
}
=
Typography
;
const
{
RangePicker
}
=
DatePicker
;
/* ---- AI 调用日志类型 ---- */
interface
AIUsageLog
{
id
:
number
;
scene
:
string
;
user_id
:
string
;
provider
:
string
;
model
:
string
;
total_tokens
:
number
;
response_time_ms
:
number
;
success
:
boolean
;
is_mock
:
boolean
;
trace_id
:
string
;
agent_id
:
string
;
session_id
:
string
;
iteration
:
number
;
created_at
:
string
;
}
interface
TraceDetail
{
trace_id
:
string
;
execution_logs
:
unknown
[];
llm_calls
:
AIUsageLog
[];
tool_calls
:
{
tool_name
:
string
;
iteration
:
number
;
success
:
boolean
;
duration_ms
:
number
}[];
}
const
token
=
()
=>
localStorage
.
getItem
(
'
access_token
'
)
||
''
;
async
function
fetchAIStats
(
page
:
number
,
agentID
:
string
,
startDate
?:
string
,
endDate
?:
string
)
{
const
params
=
new
URLSearchParams
({
page
:
String
(
page
),
page_size
:
'
20
'
});
if
(
agentID
)
params
.
append
(
'
agent_id
'
,
agentID
);
if
(
startDate
)
params
.
append
(
'
start_date
'
,
startDate
);
if
(
endDate
)
params
.
append
(
'
end_date
'
,
endDate
);
const
res
=
await
fetch
(
`/api/v1/admin/ai-center/stats?
${
params
}
`
,
{
headers
:
{
Authorization
:
`Bearer
${
token
()}
`
},
});
const
data
=
await
res
.
json
();
return
data
.
data
??
{};
}
async
function
fetchTrace
(
traceID
:
string
)
{
const
res
=
await
fetch
(
`/api/v1/admin/ai-center/trace?trace_id=
${
traceID
}
`
,
{
headers
:
{
Authorization
:
`Bearer
${
token
()}
`
},
});
const
data
=
await
res
.
json
();
return
data
.
data
as
TraceDetail
;
}
// AI日志页面
export
default
function
AILogsPage
()
{
const
[
activeTab
,
setActiveTab
]
=
useState
(
'
execution
'
);
// 执行日志状态
const
[
execLogs
,
setExecLogs
]
=
useState
<
AgentExecutionLog
[]
>
([]);
const
[
execTotal
,
setExecTotal
]
=
useState
(
0
);
const
[
execLoading
,
setExecLoading
]
=
useState
(
false
);
const
[
execFilter
,
setExecFilter
]
=
useState
<
{
agent_id
?:
string
;
page
:
number
;
page_size
:
number
;
start
?:
string
;
end
?:
string
;
}
>
({
page
:
1
,
page_size
:
20
,
start
:
dayjs
().
format
(
'
YYYY-MM-DD
'
),
end
:
dayjs
().
format
(
'
YYYY-MM-DD
'
),
});
const
[
expandedLog
,
setExpandedLog
]
=
useState
<
AgentExecutionLog
|
null
>
(
null
);
// AI 调用日志状态
const
[
aiPage
,
setAiPage
]
=
useState
(
1
);
const
[
aiAgentFilter
,
setAiAgentFilter
]
=
useState
(
''
);
const
[
aiDateRange
,
setAiDateRange
]
=
useState
<
[
string
,
string
]
>
([
dayjs
().
format
(
'
YYYY-MM-DD
'
),
dayjs
().
format
(
'
YYYY-MM-DD
'
),
]);
const
[
traceSearch
,
setTraceSearch
]
=
useState
(
''
);
const
[
traceModalOpen
,
setTraceModalOpen
]
=
useState
(
false
);
const
[
selectedTraceID
,
setSelectedTraceID
]
=
useState
(
''
);
// 获取 Agent 列表
const
{
data
:
agentsData
}
=
useQuery
({
queryKey
:
[
'
agent-definitions
'
],
queryFn
:
()
=>
agentApi
.
listDefinitions
(),
});
const
agents
=
agentsData
?.
data
||
[];
// 获取 AI 调用统计
const
{
data
:
aiStats
,
isLoading
:
aiLoading
}
=
useQuery
({
queryKey
:
[
'
ai-logs-stats
'
,
aiPage
,
aiAgentFilter
,
aiDateRange
],
queryFn
:
()
=>
fetchAIStats
(
aiPage
,
aiAgentFilter
,
aiDateRange
[
0
],
aiDateRange
[
1
]),
refetchInterval
:
60000
,
});
// 链路追踪详情
const
{
data
:
traceDetail
,
isFetching
:
traceFetching
}
=
useQuery
({
queryKey
:
[
'
trace-detail
'
,
selectedTraceID
],
queryFn
:
()
=>
fetchTrace
(
selectedTraceID
),
enabled
:
!!
selectedTraceID
&&
traceModalOpen
,
});
// 获取执行日志
const
fetchExecLogs
=
async
(
filter
=
execFilter
)
=>
{
setExecLoading
(
true
);
try
{
const
res
=
await
agentApi
.
getExecutionLogs
(
filter
);
setExecLogs
(
res
.
data
?.
list
||
[]);
setExecTotal
(
res
.
data
?.
total
||
0
);
}
catch
{}
finally
{
setExecLoading
(
false
);
}
};
// 初始加载执行日志
React
.
useEffect
(()
=>
{
fetchExecLogs
();
},
[]);
const
openTrace
=
(
traceID
:
string
)
=>
{
setSelectedTraceID
(
traceID
);
setTraceModalOpen
(
true
);
};
const
renderToolCalls
=
(
toolCalls
?:
ToolCall
[])
=>
{
if
(
!
toolCalls
||
toolCalls
.
length
===
0
)
return
null
;
return
(
<
Collapse
size=
"small"
style=
{
{
marginTop
:
8
}
}
items=
{
[{
key
:
'
tools
'
,
label
:
<
span
style=
{
{
fontSize
:
12
}
}
><
ToolOutlined
style=
{
{
marginRight
:
4
}
}
/>
Tool调用记录 (
{
toolCalls
.
length
}
)
</
span
>,
children
:
(
<
Timeline
items=
{
toolCalls
.
map
((
tc
,
idx
)
=>
({
color
:
tc
.
success
?
'
green
'
:
'
red
'
,
dot
:
tc
.
success
?
<
CheckCircleOutlined
/>
:
<
CloseCircleOutlined
/>,
children
:
(
<
div
key=
{
idx
}
style=
{
{
fontSize
:
12
}
}
>
<
div
style=
{
{
fontWeight
:
500
}
}
>
{
tc
.
tool_name
}
</
div
>
<
div
style=
{
{
color
:
'
#8c8c8c
'
}
}
>
参数:
{
tc
.
arguments
}
</
div
>
{
tc
.
result
&&
(
<
div
style=
{
{
color
:
tc
.
success
?
'
#52c41a
'
:
'
#ff4d4f
'
}
}
>
{
tc
.
success
?
JSON
.
stringify
(
tc
.
result
.
data
).
slice
(
0
,
100
)
+
(
JSON
.
stringify
(
tc
.
result
.
data
).
length
>
100
?
'
...
'
:
''
)
:
tc
.
result
.
error
}
</
div
>
)
}
</
div
>
),
}))
}
/>
),
}]
}
/>
);
};
/* ---- 执行日志列定义 ---- */
const
execColumns
:
ColumnsType
<
AgentExecutionLog
>
=
[
{
title
:
'
时间
'
,
dataIndex
:
'
created_at
'
,
key
:
'
created_at
'
,
width
:
160
,
render
:
(
v
:
string
)
=>
<
Text
style=
{
{
fontSize
:
12
}
}
>
{
dayjs
(
v
).
format
(
'
MM-DD HH:mm:ss
'
)
}
</
Text
>,
},
{
title
:
'
智能体
'
,
dataIndex
:
'
agent_id
'
,
key
:
'
agent_id
'
,
width
:
160
,
render
:
(
v
:
string
)
=>
{
const
agent
=
agents
.
find
(
a
=>
a
.
agent_id
===
v
);
return
<
Tag
color=
"blue"
>
{
agent
?.
name
||
v
}
</
Tag
>;
},
},
{
title
:
'
用户ID
'
,
dataIndex
:
'
user_id
'
,
key
:
'
user_id
'
,
width
:
130
,
ellipsis
:
true
,
render
:
(
v
:
string
)
=>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
12
}
}
>
{
v
}
</
Text
>
},
{
title
:
'
输入摘要
'
,
dataIndex
:
'
input
'
,
key
:
'
input
'
,
ellipsis
:
true
,
render
:
(
v
:
string
)
=>
{
try
{
const
obj
=
JSON
.
parse
(
v
);
const
msg
=
(
obj
.
message
||
''
)
as
string
;
return
<
Text
style=
{
{
fontSize
:
12
}
}
>
{
msg
.
slice
(
0
,
40
)
}{
msg
.
length
>
40
?
'
...
'
:
''
}
</
Text
>;
}
catch
{
return
<
Text
style=
{
{
fontSize
:
12
}
}
>
{
v
}
</
Text
>;
}
},
},
{
title
:
'
迭代
'
,
dataIndex
:
'
iterations
'
,
key
:
'
iterations
'
,
width
:
70
,
render
:
(
v
:
number
)
=>
<
Badge
count=
{
v
}
style=
{
{
backgroundColor
:
'
#0891B2
'
}
}
/>
},
{
title
:
'
Tokens
'
,
dataIndex
:
'
total_tokens
'
,
key
:
'
total_tokens
'
,
width
:
80
,
render
:
(
v
:
number
)
=>
<
Text
style=
{
{
fontSize
:
12
}
}
>
{
v
?.
toLocaleString
()
}
</
Text
>
},
{
title
:
'
耗时(ms)
'
,
dataIndex
:
'
duration_ms
'
,
key
:
'
duration_ms
'
,
width
:
90
,
render
:
(
v
:
number
)
=>
<
Text
style=
{
{
fontSize
:
12
}
}
>
{
v
}
</
Text
>
},
{
title
:
'
状态
'
,
dataIndex
:
'
success
'
,
key
:
'
success
'
,
width
:
80
,
render
:
(
v
:
boolean
)
=>
v
?
<
Badge
status=
"success"
text=
"成功"
/>
:
<
Badge
status=
"error"
text=
"失败"
/>,
},
{
title
:
'
操作
'
,
key
:
'
action
'
,
width
:
80
,
render
:
(
_
:
unknown
,
record
:
AgentExecutionLog
)
=>
(
<
Button
type=
"link"
size=
"small"
icon=
{
<
HistoryOutlined
/>
}
onClick=
{
()
=>
setExpandedLog
(
record
)
}
>
详情
</
Button
>
),
},
];
/* ---- AI 调用日志列定义 ---- */
const
aiLogColumns
:
ColumnsType
<
AIUsageLog
>
=
[
{
title
:
'
TraceID
'
,
dataIndex
:
'
trace_id
'
,
width
:
140
,
render
:
(
v
)
=>
v
?
(
<
Button
type=
"link"
size=
"small"
style=
{
{
padding
:
0
}
}
onClick=
{
()
=>
openTrace
(
v
)
}
>
{
v
.
slice
(
0
,
12
)
}
...
</
Button
>
)
:
'
-
'
,
},
{
title
:
'
场景
'
,
dataIndex
:
'
scene
'
,
width
:
140
,
render
:
(
v
)
=>
<
Tag
>
{
v
}
</
Tag
>
},
{
title
:
'
Agent
'
,
dataIndex
:
'
agent_id
'
,
width
:
140
,
render
:
(
v
)
=>
v
?
<
Tag
color=
"blue"
>
{
v
}
</
Tag
>
:
'
-
'
},
{
title
:
'
迭代
'
,
dataIndex
:
'
iteration
'
,
width
:
60
,
render
:
(
v
)
=>
v
>
0
?
<
Tag
color=
"purple"
>
#
{
v
}
</
Tag
>
:
'
-
'
},
{
title
:
'
Tokens
'
,
dataIndex
:
'
total_tokens
'
,
width
:
80
,
render
:
(
v
)
=>
<
Text
>
{
v
?.
toLocaleString
()
}
</
Text
>
},
{
title
:
'
耗时(ms)
'
,
dataIndex
:
'
response_time_ms
'
,
width
:
90
,
render
:
(
v
)
=>
{
const
color
=
v
>
5000
?
'
#ff4d4f
'
:
v
>
2000
?
'
#fa8c16
'
:
'
#52c41a
'
;
return
<
Text
style=
{
{
color
}
}
>
{
v
}
</
Text
>;
},
},
{
title
:
'
状态
'
,
dataIndex
:
'
success
'
,
width
:
70
,
render
:
(
v
,
r
)
=>
(
<
Tag
color=
{
!
v
?
'
error
'
:
r
.
is_mock
?
'
warning
'
:
'
success
'
}
>
{
!
v
?
'
失败
'
:
r
.
is_mock
?
'
模拟
'
:
'
成功
'
}
</
Tag
>
),
},
{
title
:
'
时间
'
,
dataIndex
:
'
created_at
'
,
width
:
140
,
render
:
(
v
)
=>
dayjs
(
v
).
format
(
'
MM-DD HH:mm:ss
'
)
},
];
return
(
<
div
style=
{
{
padding
:
'
20px 24px
'
,
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
gap
:
16
}
}
>
<
div
>
<
h2
style=
{
{
fontSize
:
20
,
fontWeight
:
700
,
color
:
'
#1d2129
'
,
margin
:
0
}
}
>
<
FileTextOutlined
style=
{
{
marginRight
:
8
,
color
:
'
#0D9488
'
}
}
/>
AI 日志
</
h2
>
<
div
style=
{
{
fontSize
:
13
,
color
:
'
#8c8c8c
'
,
marginTop
:
2
}
}
>
智能体执行日志与 AI 调用追踪,默认查询当天数据
</
div
>
</
div
>
{
/* 统计卡片 */
}
<
Row
gutter=
{
[
16
,
16
]
}
>
<
Col
span=
{
4
}
>
<
Card
loading=
{
aiLoading
}
size=
"small"
style=
{
{
borderRadius
:
12
,
border
:
'
1px solid #E0F2F1
'
}
}
>
<
Statistic
title=
"总调用"
value=
{
aiStats
?.
total_calls
??
0
}
prefix=
{
<
ThunderboltOutlined
/>
}
/>
</
Card
>
</
Col
>
<
Col
span=
{
4
}
>
<
Card
loading=
{
aiLoading
}
size=
"small"
style=
{
{
borderRadius
:
12
,
border
:
'
1px solid #E0F2F1
'
}
}
>
<
Statistic
title=
"成功率"
value=
{
aiStats
?.
total_calls
?
Math
.
round
((
aiStats
.
success_calls
/
aiStats
.
total_calls
)
*
100
)
:
0
}
suffix=
"%"
valueStyle=
{
{
color
:
'
#52c41a
'
}
}
/>
</
Card
>
</
Col
>
<
Col
span=
{
4
}
>
<
Card
loading=
{
aiLoading
}
size=
"small"
style=
{
{
borderRadius
:
12
,
border
:
'
1px solid #E0F2F1
'
}
}
>
<
Statistic
title=
"总 Tokens"
value=
{
aiStats
?.
total_tokens
??
0
}
/>
</
Card
>
</
Col
>
<
Col
span=
{
4
}
>
<
Card
loading=
{
aiLoading
}
size=
"small"
style=
{
{
borderRadius
:
12
,
border
:
'
1px solid #E0F2F1
'
}
}
>
<
Statistic
title=
"Agent 执行"
value=
{
aiStats
?.
agent_execs
??
0
}
prefix=
{
<
RobotOutlined
/>
}
/>
</
Card
>
</
Col
>
<
Col
span=
{
4
}
>
<
Card
loading=
{
aiLoading
}
size=
"small"
style=
{
{
borderRadius
:
12
,
border
:
'
1px solid #E0F2F1
'
}
}
>
<
Statistic
title=
"工具调用"
value=
{
aiStats
?.
tool_calls
??
0
}
prefix=
{
<
ToolOutlined
/>
}
/>
</
Card
>
</
Col
>
<
Col
span=
{
4
}
>
<
Card
loading=
{
aiLoading
}
size=
"small"
style=
{
{
borderRadius
:
12
,
border
:
'
1px solid #E0F2F1
'
}
}
>
<
Statistic
title=
"模拟调用"
value=
{
aiStats
?.
mock_calls
??
0
}
valueStyle=
{
{
color
:
'
#fa8c16
'
}
}
/>
</
Card
>
</
Col
>
</
Row
>
<
Card
style=
{
{
borderRadius
:
12
,
border
:
'
1px solid #E0F2F1
'
}
}
>
<
Tabs
activeKey=
{
activeTab
}
onChange=
{
setActiveTab
}
items=
{
[
{
key
:
'
execution
'
,
label
:
<
Space
><
RobotOutlined
/>
智能体执行日志
</
Space
>,
children
:
(
<
div
style=
{
{
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
gap
:
12
}
}
>
<
div
style=
{
{
display
:
'
flex
'
,
gap
:
8
,
alignItems
:
'
center
'
,
flexWrap
:
'
wrap
'
}
}
>
<
Select
placeholder=
"筛选智能体"
allowClear
style=
{
{
width
:
200
}
}
options=
{
agents
.
map
(
a
=>
({
value
:
a
.
agent_id
,
label
:
a
.
name
}))
}
onChange=
{
v
=>
{
const
f
=
{
...
execFilter
,
agent_id
:
v
,
page
:
1
};
setExecFilter
(
f
);
fetchExecLogs
(
f
);
}
}
/>
<
RangePicker
defaultValue=
{
[
dayjs
(),
dayjs
()]
}
onChange=
{
(
dates
)
=>
{
const
start
=
dates
?.[
0
]?.
format
(
'
YYYY-MM-DD
'
)
||
''
;
const
end
=
dates
?.[
1
]?.
format
(
'
YYYY-MM-DD
'
)
||
''
;
const
f
=
{
...
execFilter
,
start
,
end
,
page
:
1
};
setExecFilter
(
f
);
fetchExecLogs
(
f
);
}
}
/>
<
Button
icon=
{
<
ThunderboltOutlined
/>
}
onClick=
{
()
=>
fetchExecLogs
()
}
>
刷新
</
Button
>
</
div
>
<
Table
dataSource=
{
execLogs
}
columns=
{
execColumns
}
rowKey=
"id"
loading=
{
execLoading
}
size=
"small"
pagination=
{
{
current
:
execFilter
.
page
,
pageSize
:
execFilter
.
page_size
,
total
:
execTotal
,
size
:
'
small
'
,
showTotal
:
(
t
)
=>
`共 ${t} 条`
,
onChange
:
(
page
,
pageSize
)
=>
{
const
f
=
{
...
execFilter
,
page
,
page_size
:
pageSize
};
setExecFilter
(
f
);
fetchExecLogs
(
f
);
},
}
}
/>
</
div
>
),
},
{
key
:
'
ai-calls
'
,
label
:
<
Space
><
FundOutlined
/>
AI 调用日志
</
Space
>,
children
:
(
<
div
style=
{
{
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
gap
:
12
}
}
>
<
Space
style=
{
{
flexWrap
:
'
wrap
'
}
}
>
<
Select
placeholder=
"按 Agent 过滤"
allowClear
style=
{
{
width
:
200
}
}
value=
{
aiAgentFilter
||
undefined
}
onChange=
{
(
v
)
=>
{
setAiAgentFilter
(
v
??
''
);
setAiPage
(
1
);
}
}
options=
{
agents
.
map
(
a
=>
({
value
:
a
.
agent_id
,
label
:
a
.
name
}))
}
/>
<
RangePicker
defaultValue=
{
[
dayjs
(),
dayjs
()]
}
onChange=
{
(
dates
)
=>
{
const
start
=
dates
?.[
0
]?.
format
(
'
YYYY-MM-DD
'
)
||
dayjs
().
format
(
'
YYYY-MM-DD
'
);
const
end
=
dates
?.[
1
]?.
format
(
'
YYYY-MM-DD
'
)
||
dayjs
().
format
(
'
YYYY-MM-DD
'
);
setAiDateRange
([
start
,
end
]);
setAiPage
(
1
);
}
}
/>
<
Input
.
Search
placeholder=
"按 TraceID 追踪"
value=
{
traceSearch
}
onChange=
{
(
e
)
=>
setTraceSearch
(
e
.
target
.
value
)
}
onSearch=
{
(
v
)
=>
v
&&
openTrace
(
v
)
}
style=
{
{
width
:
280
}
}
prefix=
{
<
SearchOutlined
/>
}
/>
</
Space
>
<
Table
columns=
{
aiLogColumns
}
dataSource=
{
aiStats
?.
recent_logs
??
[]
}
rowKey=
"id"
loading=
{
aiLoading
}
size=
"small"
pagination=
{
{
current
:
aiPage
,
total
:
aiStats
?.
logs_total
??
0
,
pageSize
:
20
,
onChange
:
setAiPage
,
showTotal
:
(
t
)
=>
`共 ${t} 条`
,
}
}
/>
</
div
>
),
},
{
key
:
'
agent-stats
'
,
label
:
<
Space
><
FundOutlined
/>
统计分析
</
Space
>,
children
:
(
<
Row
gutter=
{
16
}
>
<
Col
span=
{
12
}
>
<
Card
title=
"各 Agent 调用量 TOP 10"
size=
"small"
>
{
(
aiStats
?.
agent_counts
??
[]).
map
((
item
:
{
agent_id
:
string
;
count
:
number
},
idx
:
number
)
=>
(
<
div
key=
{
item
.
agent_id
}
style=
{
{
display
:
'
flex
'
,
justifyContent
:
'
space-between
'
,
padding
:
'
4px 0
'
,
borderBottom
:
'
1px solid #f0f0f0
'
}
}
>
<
Text
>
{
idx
+
1
}
.
{
item
.
agent_id
}
</
Text
>
<
Tag
color=
"blue"
>
{
item
.
count
}
</
Tag
>
</
div
>
))
}
{
(
aiStats
?.
agent_counts
??
[]).
length
===
0
&&
<
Empty
description=
"暂无数据"
/>
}
</
Card
>
</
Col
>
<
Col
span=
{
12
}
>
<
Card
title=
"各场景调用量 TOP 10"
size=
"small"
>
{
(
aiStats
?.
scene_counts
??
[]).
map
((
item
:
{
scene
:
string
;
count
:
number
},
idx
:
number
)
=>
(
<
div
key=
{
item
.
scene
}
style=
{
{
display
:
'
flex
'
,
justifyContent
:
'
space-between
'
,
padding
:
'
4px 0
'
,
borderBottom
:
'
1px solid #f0f0f0
'
}
}
>
<
Text
>
{
idx
+
1
}
.
{
item
.
scene
}
</
Text
>
<
Tag
color=
"green"
>
{
item
.
count
}
</
Tag
>
</
div
>
))
}
{
(
aiStats
?.
scene_counts
??
[]).
length
===
0
&&
<
Empty
description=
"暂无数据"
/>
}
</
Card
>
</
Col
>
</
Row
>
),
},
]
}
/>
</
Card
>
{
/* 执行日志详情 Modal */
}
<
Modal
title=
{
<
Space
><
HistoryOutlined
/>
执行日志详情
</
Space
>
}
open=
{
!!
expandedLog
}
onCancel=
{
()
=>
setExpandedLog
(
null
)
}
footer=
{
null
}
width=
{
700
}
>
{
expandedLog
&&
(()
=>
{
let
toolCalls
:
ToolCall
[]
=
[];
try
{
toolCalls
=
JSON
.
parse
(
expandedLog
.
tool_calls
||
'
[]
'
);
}
catch
{}
let
inputObj
:
Record
<
string
,
unknown
>
=
{};
try
{
inputObj
=
JSON
.
parse
(
expandedLog
.
input
||
'
{}
'
);
}
catch
{}
let
outputObj
:
Record
<
string
,
unknown
>
=
{};
try
{
outputObj
=
JSON
.
parse
(
expandedLog
.
output
||
'
{}
'
);
}
catch
{}
return
(
<
div
style=
{
{
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
gap
:
12
}
}
>
<
div
style=
{
{
display
:
'
grid
'
,
gridTemplateColumns
:
'
1fr 1fr
'
,
gap
:
8
,
fontSize
:
13
}
}
>
<
div
><
Text
type=
"secondary"
>
智能体:
</
Text
><
Tag
>
{
expandedLog
.
agent_id
}
</
Tag
></
div
>
<
div
><
Text
type=
"secondary"
>
状态:
</
Text
><
Badge
status=
{
expandedLog
.
success
?
'
success
'
:
'
error
'
}
text=
{
expandedLog
.
success
?
'
成功
'
:
'
失败
'
}
/></
div
>
<
div
><
Text
type=
"secondary"
>
迭代次数:
</
Text
>
{
expandedLog
.
iterations
}
</
div
>
<
div
><
Text
type=
"secondary"
>
耗时:
</
Text
>
{
expandedLog
.
duration_ms
}
ms
</
div
>
<
div
><
Text
type=
"secondary"
>
Tokens:
</
Text
>
{
expandedLog
.
total_tokens
}
</
div
>
<
div
><
Text
type=
"secondary"
>
完成原因:
</
Text
>
{
expandedLog
.
finish_reason
||
'
-
'
}
</
div
>
</
div
>
<
Card
size=
"small"
title=
"用户输入"
>
<
Text
style=
{
{
fontSize
:
12
}
}
>
{
(
inputObj
.
message
as
string
)
||
expandedLog
.
input
}
</
Text
>
</
Card
>
<
Card
size=
"small"
title=
"AI 回复"
>
<
Text
style=
{
{
fontSize
:
12
}
}
>
{
(
outputObj
.
response
as
string
)
||
expandedLog
.
output
}
</
Text
>
</
Card
>
{
renderToolCalls
(
toolCalls
)
}
</
div
>
);
})()
}
</
Modal
>
{
/* 链路追踪详情弹窗 */
}
<
Modal
title=
{
`链路追踪: ${selectedTraceID?.slice(0, 20)}
...
`
}
open=
{
traceModalOpen
}
onCancel=
{
()
=>
{
setTraceModalOpen
(
false
);
setSelectedTraceID
(
''
);
}
}
footer=
{
<
Button
onClick=
{
()
=>
setTraceModalOpen
(
false
)
}
>
关闭
</
Button
>
}
width=
{
700
}
>
{
traceFetching
?
(
<
div
style=
{
{
textAlign
:
'
center
'
,
padding
:
40
}
}
><
Spin
/></
div
>
)
:
traceDetail
?
(
<
div
>
<
Card
size=
"small"
title=
"LLM 调用序列"
style=
{
{
marginBottom
:
16
}
}
>
<
Timeline
items=
{
(
traceDetail
.
llm_calls
??
[]).
map
((
log
:
AIUsageLog
)
=>
({
color
:
log
.
success
?
'
green
'
:
'
red
'
,
children
:
(
<
div
>
<
Space
>
<
Tag
color=
"purple"
>
迭代 #
{
log
.
iteration
}
</
Tag
>
<
Tag
>
{
log
.
scene
}
</
Tag
>
<
Text
type=
"secondary"
>
{
log
.
total_tokens
}
tokens
</
Text
>
<
Text
type=
"secondary"
>
{
log
.
response_time_ms
}
ms
</
Text
>
</
Space
>
</
div
>
),
}))
}
/>
{
(
traceDetail
.
llm_calls
??
[]).
length
===
0
&&
<
Empty
description=
"无 LLM 调用记录"
/>
}
</
Card
>
<
Card
size=
"small"
title=
"工具调用序列"
>
<
Timeline
items=
{
(
traceDetail
.
tool_calls
??
[]).
map
((
tc
)
=>
({
color
:
tc
.
success
?
'
blue
'
:
'
red
'
,
children
:
(
<
div
>
<
Space
>
<
Tag
color=
"blue"
>
迭代 #
{
tc
.
iteration
}
</
Tag
>
<
Tag
color=
"cyan"
>
{
tc
.
tool_name
}
</
Tag
>
<
Tag
color=
{
tc
.
success
?
'
green
'
:
'
red
'
}
>
{
tc
.
success
?
'
成功
'
:
'
失败
'
}
</
Tag
>
<
Text
type=
"secondary"
>
{
tc
.
duration_ms
}
ms
</
Text
>
</
Space
>
</
div
>
),
}))
}
/>
{
(
traceDetail
.
tool_calls
??
[]).
length
===
0
&&
<
Empty
description=
"无工具调用记录"
/>
}
</
Card
>
</
div
>
)
:
(
<
Empty
description=
"未找到追踪数据"
/>
)
}
</
Modal
>
</
div
>
);
}
web/src/app/(main)/admin/layout.tsx
View file @
04584395
'
use client
'
;
'
use client
'
;
import
React
,
{
Suspense
,
useState
,
useEffect
,
useMemo
,
useCallback
}
from
'
react
'
;
import
React
,
{
useState
,
useEffect
}
from
'
react
'
;
import
{
useRouter
,
usePathname
,
useSearchParams
}
from
'
next/navigation
'
;
import
{
useRouter
,
usePathname
}
from
'
next/navigation
'
;
import
{
Layout
,
Menu
,
Avatar
,
Dropdown
,
Badge
,
Space
,
Typography
,
Tag
,
Spin
,
Popover
,
List
}
from
'
antd
'
;
import
{
Layout
,
Menu
,
Avatar
,
Dropdown
,
Badge
,
Space
,
Typography
,
Tag
}
from
'
antd
'
;
import
{
import
{
DashboardOutlined
,
UserOutlined
,
TeamOutlined
,
ApartmentOutlined
,
DashboardOutlined
,
UserOutlined
,
TeamOutlined
,
ApartmentOutlined
,
SettingOutlined
,
LogoutOutlined
,
BellOutlined
,
MedicineBoxOutlined
,
SettingOutlined
,
LogoutOutlined
,
BellOutlined
,
MedicineBoxOutlined
,
FileSearchOutlined
,
FileTextOutlined
,
RobotOutlined
,
SafetyCertificateOutlined
,
FileSearchOutlined
,
FileTextOutlined
,
RobotOutlined
,
SafetyCertificateOutlined
,
ApiOutlined
,
DeploymentUnitOutlined
,
BookOutlined
,
CheckCircleOutlined
,
ApiOutlined
,
DeploymentUnitOutlined
,
BookOutlined
,
CheckCircleOutlined
,
SafetyOutlined
,
FundOutlined
,
AppstoreOutlined
,
ShopOutlined
,
SafetyOutlined
,
FundOutlined
,
AppstoreOutlined
,
MenuFoldOutlined
,
MenuUnfoldOutlined
,
CompassOutlined
,
ToolOutlined
,
MenuFoldOutlined
,
MenuUnfoldOutlined
,
AuditOutlined
,
ScheduleOutlined
,
}
from
'
@ant-design/icons
'
;
}
from
'
@ant-design/icons
'
;
import
{
useUserStore
}
from
'
@/store/userStore
'
;
import
{
useUserStore
}
from
'
@/store/userStore
'
;
import
type
{
Menu
as
MenuType
}
from
'
@/api/rbac
'
;
import
{
myMenuApi
}
from
'
@/api/rbac
'
;
import
{
notificationApi
,
type
Notification
}
from
'
@/api/notification
'
;
const
{
Sider
,
Content
}
=
Layout
;
const
{
Sider
,
Content
}
=
Layout
;
const
{
Text
}
=
Typography
;
const
{
Text
}
=
Typography
;
// 图标名称 → React 图标组件映射
const
menuItems
=
[
const
ICON_MAP
:
Record
<
string
,
React
.
ReactNode
>
=
{
{
key
:
'
/admin/dashboard
'
,
icon
:
<
DashboardOutlined
/>,
label
:
'
运营大盘
'
},
DashboardOutlined
:
<
DashboardOutlined
/>,
{
UserOutlined
:
<
UserOutlined
/>,
key
:
'
user-mgmt
'
,
icon
:
<
TeamOutlined
/>,
label
:
'
用户管理
'
,
TeamOutlined
:
<
TeamOutlined
/>,
children
:
[
ApartmentOutlined
:
<
ApartmentOutlined
/>,
{
key
:
'
/admin/patients
'
,
icon
:
<
UserOutlined
/>,
label
:
'
患者管理
'
},
SettingOutlined
:
<
SettingOutlined
/>,
{
key
:
'
/admin/doctors
'
,
icon
:
<
MedicineBoxOutlined
/>,
label
:
'
医生管理
'
},
MedicineBoxOutlined
:
<
MedicineBoxOutlined
/>,
{
key
:
'
/admin/admins
'
,
icon
:
<
SettingOutlined
/>,
label
:
'
管理员管理
'
},
FileSearchOutlined
:
<
FileSearchOutlined
/>,
],
FileTextOutlined
:
<
FileTextOutlined
/>,
},
RobotOutlined
:
<
RobotOutlined
/>,
{
key
:
'
/admin/departments
'
,
icon
:
<
ApartmentOutlined
/>,
label
:
'
科室管理
'
},
SafetyCertificateOutlined
:
<
SafetyCertificateOutlined
/>,
{
key
:
'
/admin/consultations
'
,
icon
:
<
FileSearchOutlined
/>,
label
:
'
问诊管理
'
},
ApiOutlined
:
<
ApiOutlined
/>,
{
key
:
'
/admin/prescription
'
,
icon
:
<
FileTextOutlined
/>,
label
:
'
处方监管
'
},
DeploymentUnitOutlined
:
<
DeploymentUnitOutlined
/>,
{
key
:
'
/admin/pharmacy
'
,
icon
:
<
MedicineBoxOutlined
/>,
label
:
'
药品库
'
},
BookOutlined
:
<
BookOutlined
/>,
{
key
:
'
/admin/ai-config
'
,
icon
:
<
RobotOutlined
/>,
label
:
'
AI配置
'
},
CheckCircleOutlined
:
<
CheckCircleOutlined
/>,
{
key
:
'
/admin/compliance
'
,
icon
:
<
SafetyCertificateOutlined
/>,
label
:
'
合规报表
'
},
SafetyOutlined
:
<
SafetyOutlined
/>,
{
FundOutlined
:
<
FundOutlined
/>,
key
:
'
ai-platform
'
,
icon
:
<
ApiOutlined
/>,
label
:
'
智能体平台
'
,
AppstoreOutlined
:
<
AppstoreOutlined
/>,
children
:
[
ShopOutlined
:
<
ShopOutlined
/>,
{
key
:
'
/admin/agents
'
,
icon
:
<
RobotOutlined
/>,
label
:
'
智能体管理
'
},
CompassOutlined
:
<
CompassOutlined
/>,
{
key
:
'
/admin/ai-logs
'
,
icon
:
<
FundOutlined
/>,
label
:
'
AI日志
'
},
ToolOutlined
:
<
ToolOutlined
/>,
{
key
:
'
/admin/workflows
'
,
icon
:
<
DeploymentUnitOutlined
/>,
label
:
'
工作流
'
},
AuditOutlined
:
<
AuditOutlined
/>,
{
key
:
'
/admin/tasks
'
,
icon
:
<
CheckCircleOutlined
/>,
label
:
'
人工审核
'
},
ScheduleOutlined
:
<
ScheduleOutlined
/>,
{
key
:
'
/admin/knowledge
'
,
icon
:
<
BookOutlined
/>,
label
:
'
知识库
'
},
BellOutlined
:
<
BellOutlined
/>,
{
key
:
'
/admin/safety
'
,
icon
:
<
SafetyOutlined
/>,
label
:
'
内容安全
'
},
};
],
},
// 将数据库菜单树转换为 Ant Design Menu items
{
function
convertMenuTree
(
menus
:
MenuType
[]):
any
[]
{
key
:
'
system-mgmt
'
,
icon
:
<
SafetyCertificateOutlined
/>,
label
:
'
系统管理
'
,
return
menus
.
map
(
m
=>
{
children
:
[
const
item
:
any
=
{
{
key
:
'
/admin/roles
'
,
icon
:
<
SafetyOutlined
/>,
label
:
'
角色管理
'
},
key
:
m
.
path
||
`menu-
${
m
.
id
}
`
,
{
key
:
'
/admin/menus
'
,
icon
:
<
AppstoreOutlined
/>,
label
:
'
菜单管理
'
},
label
:
m
.
name
,
],
icon
:
ICON_MAP
[
m
.
icon
]
||
null
,
},
};
];
if
(
m
.
children
&&
m
.
children
.
length
>
0
)
{
item
.
children
=
convertMenuTree
(
m
.
children
);
}
return
item
;
});
}
// 从菜单树中收集所有叶子节点路径
function
collectLeafPaths
(
menus
:
MenuType
[]):
string
[]
{
const
paths
:
string
[]
=
[];
for
(
const
m
of
menus
)
{
if
(
m
.
children
&&
m
.
children
.
length
>
0
)
{
paths
.
push
(...
collectLeafPaths
(
m
.
children
));
}
else
if
(
m
.
path
)
{
paths
.
push
(
m
.
path
);
}
}
return
paths
;
}
// 从菜单树中查找包含某路径的父节点key
function
findOpenKeys
(
menus
:
MenuType
[],
targetPath
:
string
):
string
[]
{
const
keys
:
string
[]
=
[];
for
(
const
m
of
menus
)
{
if
(
m
.
children
&&
m
.
children
.
length
>
0
)
{
const
childPaths
=
collectLeafPaths
(
m
.
children
);
if
(
childPaths
.
some
(
p
=>
targetPath
.
startsWith
(
p
)))
{
keys
.
push
(
m
.
path
||
`menu-
${
m
.
id
}
`
);
}
keys
.
push
(...
findOpenKeys
(
m
.
children
,
targetPath
));
}
}
return
keys
;
}
export
default
function
AdminLayout
({
children
}:
{
children
:
React
.
ReactNode
})
{
export
default
function
AdminLayout
({
children
}:
{
children
:
React
.
ReactNode
})
{
return
(
<
Suspense
fallback=
{
<
div
style=
{
{
minHeight
:
'
100vh
'
,
background
:
'
#f5f6fa
'
}
}
/>
}
>
<
AdminLayoutInner
>
{
children
}
</
AdminLayoutInner
>
</
Suspense
>
);
}
function
AdminLayoutInner
({
children
}:
{
children
:
React
.
ReactNode
})
{
const
router
=
useRouter
();
const
router
=
useRouter
();
const
pathname
=
usePathname
();
const
pathname
=
usePathname
();
const
searchParams
=
useSearchParams
();
const
{
user
,
logout
}
=
useUserStore
();
const
isEmbed
=
searchParams
?.
get
(
'
embed
'
)
===
'
1
'
;
const
{
user
,
logout
,
menus
,
setMenus
}
=
useUserStore
();
const
[
collapsed
,
setCollapsed
]
=
useState
(
false
);
const
[
collapsed
,
setCollapsed
]
=
useState
(
false
);
const
[
unreadCount
,
setUnreadCount
]
=
useState
(
0
);
const
[
notifications
,
setNotifications
]
=
useState
<
Notification
[]
>
([]);
const
currentPath
=
pathname
||
''
;
const
currentPath
=
pathname
||
''
;
//
菜单为空时主动拉取
//
监听 AI 助手导航事件
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
!
user
||
menus
.
length
>
0
)
return
;
myMenuApi
.
getMenus
().
then
(
res
=>
{
if
(
res
.
data
&&
res
.
data
.
length
>
0
)
setMenus
(
res
.
data
);
}).
catch
(()
=>
{});
},
[
user
,
menus
.
length
,
setMenus
]);
useEffect
(()
=>
{
if
(
!
user
)
return
;
const
fetchUnread
=
()
=>
{
notificationApi
.
getUnreadCount
().
then
(
res
=>
setUnreadCount
(
res
.
data
?.
count
||
0
)).
catch
(()
=>
{});
};
fetchUnread
();
const
timer
=
setInterval
(
fetchUnread
,
60000
);
return
()
=>
clearInterval
(
timer
);
},
[
user
]);
const
handleBellClick
=
()
=>
{
notificationApi
.
list
({
page
:
1
,
page_size
:
5
}).
then
(
res
=>
setNotifications
(
res
.
data
?.
list
||
[])).
catch
(()
=>
{});
};
const
handleMarkAllRead
=
()
=>
{
notificationApi
.
markAllRead
().
then
(()
=>
{
setUnreadCount
(
0
);
setNotifications
(
n
=>
n
.
map
(
i
=>
({
...
i
,
is_read
:
true
})));
}).
catch
(()
=>
{});
};
// 转换为 Ant Design menu items(直接使用store中的菜单数据)
const
menuItems
=
useMemo
(()
=>
convertMenuTree
(
menus
),
[
menus
]);
// 监听 AI 助手导航事件(embed 模式下跳过,避免 iframe 内拦截)
useEffect
(()
=>
{
if
(
isEmbed
)
return
;
const
handleAIAction
=
(
e
:
Event
)
=>
{
const
handleAIAction
=
(
e
:
Event
)
=>
{
const
detail
=
(
e
as
CustomEvent
).
detail
;
const
detail
=
(
e
as
CustomEvent
).
detail
;
if
(
detail
?.
action
===
'
navigate
'
&&
typeof
detail
.
page
===
'
string
'
)
{
if
(
detail
?.
action
===
'
navigate
'
&&
typeof
detail
.
page
===
'
string
'
)
{
...
@@ -151,7 +71,7 @@ function AdminLayoutInner({ children }: { children: React.ReactNode }) {
...
@@ -151,7 +71,7 @@ function AdminLayoutInner({ children }: { children: React.ReactNode }) {
};
};
window
.
addEventListener
(
'
ai-action
'
,
handleAIAction
);
window
.
addEventListener
(
'
ai-action
'
,
handleAIAction
);
return
()
=>
window
.
removeEventListener
(
'
ai-action
'
,
handleAIAction
);
return
()
=>
window
.
removeEventListener
(
'
ai-action
'
,
handleAIAction
);
},
[
router
,
isEmbed
]);
},
[
router
]);
const
userMenuItems
=
[
const
userMenuItems
=
[
{
key
:
'
profile
'
,
icon
:
<
UserOutlined
/>,
label
:
'
个人信息
'
},
{
key
:
'
profile
'
,
icon
:
<
UserOutlined
/>,
label
:
'
个人信息
'
},
...
@@ -168,23 +88,35 @@ function AdminLayoutInner({ children }: { children: React.ReactNode }) {
...
@@ -168,23 +88,35 @@ function AdminLayoutInner({ children }: { children: React.ReactNode }) {
const
handleUserMenuClick
=
({
key
}:
{
key
:
string
})
=>
{
const
handleUserMenuClick
=
({
key
}:
{
key
:
string
})
=>
{
if
(
key
===
'
logout
'
)
{
logout
();
router
.
push
(
'
/login
'
);
}
if
(
key
===
'
logout
'
)
{
logout
();
router
.
push
(
'
/login
'
);
}
else
if
(
key
===
'
profile
'
||
key
===
'
settings
'
)
{
router
.
push
(
'
/admin/dashboard
'
);
}
};
};
const
getSelectedKeys
=
useCallback
(()
=>
{
const
getSelectedKeys
=
()
=>
{
const
allPaths
=
collectLeafPaths
(
menus
);
const
allKeys
=
[
'
/admin/dashboard
'
,
'
/admin/patients
'
,
'
/admin/doctors
'
,
'
/admin/admins
'
,
const
match
=
allPaths
.
find
(
k
=>
currentPath
.
startsWith
(
k
));
'
/admin/departments
'
,
'
/admin/consultations
'
,
'
/admin/prescription
'
,
'
/admin/pharmacy
'
,
'
/admin/ai-config
'
,
'
/admin/compliance
'
,
'
/admin/agents
'
,
'
/admin/ai-logs
'
,
'
/admin/workflows
'
,
'
/admin/tasks
'
,
'
/admin/knowledge
'
,
'
/admin/safety
'
,
'
/admin/roles
'
,
'
/admin/menus
'
];
const
match
=
allKeys
.
find
(
k
=>
currentPath
.
startsWith
(
k
));
return
match
?
[
match
]
:
[];
return
match
?
[
match
]
:
[];
}
,
[
menus
,
currentPath
])
;
};
const
getOpenKeys
=
useCallback
(
()
=>
{
const
getOpenKeys
=
()
=>
{
if
(
collapsed
)
return
[];
if
(
collapsed
)
return
[];
return
findOpenKeys
(
menus
,
currentPath
);
const
keys
:
string
[]
=
[];
},
[
menus
,
currentPath
,
collapsed
]);
if
([
'
/admin/patients
'
,
'
/admin/doctors
'
,
'
/admin/admins
'
].
some
(
k
=>
currentPath
.
startsWith
(
k
)))
{
keys
.
push
(
'
user-mgmt
'
);
if
(
isEmbed
)
{
}
return
<
div
style=
{
{
minHeight
:
'
100vh
'
,
background
:
'
#f5f6fa
'
}
}
>
{
children
}
</
div
>;
if
([
'
/admin/agents
'
,
'
/admin/ai-logs
'
,
}
'
/admin/workflows
'
,
'
/admin/tasks
'
,
'
/admin/knowledge
'
,
'
/admin/safety
'
]
.
some
(
k
=>
currentPath
.
startsWith
(
k
)))
{
keys
.
push
(
'
ai-platform
'
);
}
if
([
'
/admin/roles
'
,
'
/admin/menus
'
].
some
(
k
=>
currentPath
.
startsWith
(
k
)))
{
keys
.
push
(
'
system-mgmt
'
);
}
return
keys
;
};
return
(
return
(
<
Layout
style=
{
{
minHeight
:
'
100vh
'
}
}
>
<
Layout
style=
{
{
minHeight
:
'
100vh
'
}
}
>
...
@@ -213,7 +145,7 @@ function AdminLayoutInner({ children }: { children: React.ReactNode }) {
...
@@ -213,7 +145,7 @@ function AdminLayoutInner({ children }: { children: React.ReactNode }) {
>
>
<
div
style=
{
{
<
div
style=
{
{
width
:
32
,
height
:
32
,
borderRadius
:
8
,
flexShrink
:
0
,
width
:
32
,
height
:
32
,
borderRadius
:
8
,
flexShrink
:
0
,
background
:
'
linear-gradient(135deg, #
722ed1 0%, #531dab
100%)
'
,
background
:
'
linear-gradient(135deg, #
0D9488 0%, #0F766E
100%)
'
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
justifyContent
:
'
center
'
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
justifyContent
:
'
center
'
,
}
}
>
}
}
>
<
MedicineBoxOutlined
style=
{
{
fontSize
:
16
,
color
:
'
#fff
'
}
}
/>
<
MedicineBoxOutlined
style=
{
{
fontSize
:
16
,
color
:
'
#fff
'
}
}
/>
...
@@ -221,7 +153,7 @@ function AdminLayoutInner({ children }: { children: React.ReactNode }) {
...
@@ -221,7 +153,7 @@ function AdminLayoutInner({ children }: { children: React.ReactNode }) {
{
!
collapsed
&&
(
{
!
collapsed
&&
(
<>
<>
<
span
style=
{
{
fontSize
:
15
,
fontWeight
:
700
,
color
:
'
#1d2129
'
,
marginLeft
:
8
}
}
>
互联网医院
</
span
>
<
span
style=
{
{
fontSize
:
15
,
fontWeight
:
700
,
color
:
'
#1d2129
'
,
marginLeft
:
8
}
}
>
互联网医院
</
span
>
<
Tag
color=
"
purple
"
style=
{
{
marginLeft
:
6
,
fontSize
:
10
,
lineHeight
:
'
18px
'
,
padding
:
'
0 5px
'
}
}
>
管理
</
Tag
>
<
Tag
color=
"
cyan
"
style=
{
{
marginLeft
:
6
,
fontSize
:
10
,
lineHeight
:
'
18px
'
,
padding
:
'
0 5px
'
}
}
>
管理
</
Tag
>
</>
</>
)
}
)
}
</
div
>
</
div
>
...
@@ -254,37 +186,13 @@ function AdminLayoutInner({ children }: { children: React.ReactNode }) {
...
@@ -254,37 +186,13 @@ function AdminLayoutInner({ children }: { children: React.ReactNode }) {
{
collapsed
?
<
MenuUnfoldOutlined
/>
:
<
MenuFoldOutlined
/>
}
{
collapsed
?
<
MenuUnfoldOutlined
/>
:
<
MenuFoldOutlined
/>
}
</
div
>
</
div
>
<
div
style=
{
{
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
16
}
}
>
<
div
style=
{
{
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
16
}
}
>
<
Popover
<
Badge
count=
{
2
}
size=
"small"
>
trigger=
"click"
<
BellOutlined
style=
{
{
fontSize
:
16
,
color
:
'
#595959
'
,
cursor
:
'
pointer
'
}
}
/>
placement=
"bottomRight"
</
Badge
>
onOpenChange=
{
(
open
)
=>
{
if
(
open
)
handleBellClick
();
}
}
content=
{
<
div
style=
{
{
width
:
300
}
}
>
<
div
style=
{
{
display
:
'
flex
'
,
justifyContent
:
'
space-between
'
,
alignItems
:
'
center
'
,
marginBottom
:
8
}
}
>
<
span
style=
{
{
fontWeight
:
600
,
fontSize
:
14
}
}
>
通知
</
span
>
{
unreadCount
>
0
&&
<
a
onClick=
{
handleMarkAllRead
}
style=
{
{
fontSize
:
12
}
}
>
全部已读
</
a
>
}
</
div
>
<
List
size=
"small"
dataSource=
{
notifications
}
locale=
{
{
emptyText
:
'
暂无通知
'
}
}
renderItem=
{
(
item
)
=>
(
<
List
.
Item
style=
{
{
opacity
:
item
.
is_read
?
0.6
:
1
}
}
>
<
List
.
Item
.
Meta
title=
{
<
span
style=
{
{
fontSize
:
13
}
}
>
{
item
.
title
}
</
span
>
}
description=
{
<
span
style=
{
{
fontSize
:
12
}
}
>
{
item
.
content
}
</
span
>
}
/>
</
List
.
Item
>
)
}
/>
</
div
>
}
>
<
Badge
count=
{
unreadCount
}
size=
"small"
>
<
BellOutlined
style=
{
{
fontSize
:
16
,
color
:
'
#595959
'
,
cursor
:
'
pointer
'
}
}
/>
</
Badge
>
</
Popover
>
{
user
?
(
{
user
?
(
<
Dropdown
menu=
{
{
items
:
userMenuItems
,
onClick
:
handleUserMenuClick
}
}
>
<
Dropdown
menu=
{
{
items
:
userMenuItems
,
onClick
:
handleUserMenuClick
}
}
>
<
Space
style=
{
{
cursor
:
'
pointer
'
}
}
>
<
Space
style=
{
{
cursor
:
'
pointer
'
}
}
>
<
Avatar
size=
"small"
icon=
{
<
UserOutlined
/>
}
style=
{
{
backgroundColor
:
'
#
722ed1
'
}
}
/>
<
Avatar
size=
"small"
icon=
{
<
UserOutlined
/>
}
style=
{
{
backgroundColor
:
'
#
0D9488
'
}
}
/>
<
Text
style=
{
{
color
:
'
#1d2129
'
,
fontSize
:
13
}
}
>
{
user
.
real_name
||
'
管理员
'
}
</
Text
>
<
Text
style=
{
{
color
:
'
#1d2129
'
,
fontSize
:
13
}
}
>
{
user
.
real_name
||
'
管理员
'
}
</
Text
>
</
Space
>
</
Space
>
</
Dropdown
>
</
Dropdown
>
...
@@ -292,7 +200,7 @@ function AdminLayoutInner({ children }: { children: React.ReactNode }) {
...
@@ -292,7 +200,7 @@ function AdminLayoutInner({ children }: { children: React.ReactNode }) {
</
div
>
</
div
>
</
header
>
</
header
>
<
Content
style=
{
{
minHeight
:
'
calc(100vh - 56px)
'
,
background
:
'
#
f5f6fa
'
}
}
>
<
Content
style=
{
{
minHeight
:
'
calc(100vh - 56px)
'
,
background
:
'
#
F8FAFB
'
}
}
>
{
children
}
{
children
}
</
Content
>
</
Content
>
</
Layout
>
</
Layout
>
...
...
web/src/app/(main)/admin/menus/page.tsx
View file @
04584395
...
@@ -3,11 +3,18 @@
...
@@ -3,11 +3,18 @@
import
React
,
{
useEffect
,
useState
,
useCallback
,
useMemo
}
from
'
react
'
;
import
React
,
{
useEffect
,
useState
,
useCallback
,
useMemo
}
from
'
react
'
;
import
{
import
{
Card
,
Button
,
Modal
,
Tree
,
Tag
,
Space
,
Spin
,
App
,
Card
,
Button
,
Modal
,
Tree
,
Tag
,
Space
,
Spin
,
App
,
Dropdown
,
}
from
'
antd
'
;
}
from
'
antd
'
;
import
{
import
{
AppstoreOutlined
,
ReloadOutlined
,
AppstoreOutlined
,
ReloadOutlined
,
SaveOutlined
,
CheckSquareOutlined
,
MinusSquareOutlined
,
SaveOutlined
,
CheckSquareOutlined
,
MinusSquareOutlined
,
PlusOutlined
,
EditOutlined
,
DeleteOutlined
,
EyeOutlined
,
EyeInvisibleOutlined
,
MoreOutlined
,
}
from
'
@ant-design/icons
'
;
}
from
'
@ant-design/icons
'
;
import
{
DrawerForm
,
ProFormText
,
ProFormDigit
,
ProFormSelect
,
ProFormSwitch
,
ProFormTreeSelect
,
}
from
'
@ant-design/pro-components
'
;
import
{
menuApi
,
roleApi
}
from
'
@/api/rbac
'
;
import
{
menuApi
,
roleApi
}
from
'
@/api/rbac
'
;
import
type
{
Menu
,
Role
}
from
'
@/api/rbac
'
;
import
type
{
Menu
,
Role
}
from
'
@/api/rbac
'
;
...
@@ -17,22 +24,66 @@ type TreeDataNode = {
...
@@ -17,22 +24,66 @@ type TreeDataNode = {
children
?:
TreeDataNode
[];
children
?:
TreeDataNode
[];
};
};
// 将菜单树转为 Tree 组件的 treeData
// 菜单类型选项
function
menusToTreeData
(
menus
:
Menu
[]):
TreeDataNode
[]
{
const
menuTypeOptions
=
[
{
label
:
'
目录
'
,
value
:
'
directory
'
},
{
label
:
'
菜单
'
,
value
:
'
menu
'
},
{
label
:
'
按钮
'
,
value
:
'
button
'
},
];
// 将菜单树转为 Tree 组件的 treeData(带操作按钮)
function
menusToTreeData
(
menus
:
Menu
[],
onEdit
:
(
m
:
Menu
)
=>
void
,
onDelete
:
(
m
:
Menu
)
=>
void
,
onAddChild
:
(
m
:
Menu
)
=>
void
,
):
TreeDataNode
[]
{
return
menus
.
map
((
m
)
=>
({
return
menus
.
map
((
m
)
=>
({
key
:
m
.
id
,
key
:
m
.
id
,
title
:
(
title
:
(
<
span
>
<
div
style=
{
{
display
:
'
flex
'
,
alignItems
:
'
center
'
,
justifyContent
:
'
space-between
'
,
width
:
'
100%
'
,
paddingRight
:
8
}
}
>
{
m
.
name
}
<
span
style=
{
{
flex
:
1
}
}
>
{
m
.
path
&&
<
span
style=
{
{
color
:
'
#999
'
,
fontSize
:
12
,
marginLeft
:
8
}
}
>
{
m
.
path
}
</
span
>
}
{
m
.
name
}
{
!
m
.
visible
&&
<
Tag
color=
"default"
style=
{
{
marginLeft
:
6
,
fontSize
:
11
}
}
>
隐藏
</
Tag
>
}
{
m
.
path
&&
<
span
style=
{
{
color
:
'
#999
'
,
fontSize
:
12
,
marginLeft
:
8
}
}
>
{
m
.
path
}
</
span
>
}
</
span
>
{
m
.
type
&&
<
Tag
color=
{
m
.
type
===
'
directory
'
?
'
blue
'
:
m
.
type
===
'
button
'
?
'
orange
'
:
'
green
'
}
style=
{
{
marginLeft
:
6
,
fontSize
:
11
}
}
>
{
m
.
type
===
'
directory
'
?
'
目录
'
:
m
.
type
===
'
button
'
?
'
按钮
'
:
'
菜单
'
}
</
Tag
>
}
{
!
m
.
visible
&&
<
Tag
color=
"default"
style=
{
{
marginLeft
:
6
,
fontSize
:
11
}
}
><
EyeInvisibleOutlined
/>
隐藏
</
Tag
>
}
</
span
>
<
Space
size=
{
4
}
style=
{
{
flexShrink
:
0
}
}
onClick=
{
(
e
)
=>
e
.
stopPropagation
()
}
>
<
Dropdown
menu=
{
{
items
:
[
{
key
:
'
addChild
'
,
icon
:
<
PlusOutlined
/>,
label
:
'
添加子菜单
'
},
{
key
:
'
edit
'
,
icon
:
<
EditOutlined
/>,
label
:
'
编辑
'
},
{
type
:
'
divider
'
},
{
key
:
'
delete
'
,
icon
:
<
DeleteOutlined
/>,
label
:
'
删除
'
,
danger
:
true
},
],
onClick
:
({
key
})
=>
{
if
(
key
===
'
edit
'
)
onEdit
(
m
);
else
if
(
key
===
'
delete
'
)
onDelete
(
m
);
else
if
(
key
===
'
addChild
'
)
onAddChild
(
m
);
},
}
}
trigger=
{
[
'
click
'
]
}
>
<
Button
type=
"text"
size=
"small"
icon=
{
<
MoreOutlined
/>
}
style=
{
{
opacity
:
0.6
}
}
/>
</
Dropdown
>
</
Space
>
</
div
>
),
),
children
:
m
.
children
?.
length
?
menusToTreeData
(
m
.
children
)
:
undefined
,
children
:
m
.
children
?.
length
?
menusToTreeData
(
m
.
children
,
onEdit
,
onDelete
,
onAddChild
)
:
undefined
,
}));
}));
}
}
// 收集树中所有叶节点 key(用于全选逻辑中正确处理 checkStrictly=false 模式)
// 将菜单树转为 TreeSelect 的 treeData
function
menusToSelectData
(
menus
:
Menu
[]):
any
[]
{
return
menus
.
map
((
m
)
=>
({
value
:
m
.
id
,
title
:
m
.
name
,
children
:
m
.
children
?.
length
?
menusToSelectData
(
m
.
children
)
:
undefined
,
}));
}
// 收集树中所有 key
function
collectAllKeys
(
menus
:
Menu
[]):
number
[]
{
function
collectAllKeys
(
menus
:
Menu
[]):
number
[]
{
const
keys
:
number
[]
=
[];
const
keys
:
number
[]
=
[];
for
(
const
m
of
menus
)
{
for
(
const
m
of
menus
)
{
...
@@ -44,6 +95,18 @@ function collectAllKeys(menus: Menu[]): number[] {
...
@@ -44,6 +95,18 @@ function collectAllKeys(menus: Menu[]): number[] {
return
keys
;
return
keys
;
}
}
// 在菜单树中查找指定 id 的菜单
function
findMenuById
(
menus
:
Menu
[],
id
:
number
):
Menu
|
undefined
{
for
(
const
m
of
menus
)
{
if
(
m
.
id
===
id
)
return
m
;
if
(
m
.
children
?.
length
)
{
const
found
=
findMenuById
(
m
.
children
,
id
);
if
(
found
)
return
found
;
}
}
return
undefined
;
}
export
default
function
MenusPage
()
{
export
default
function
MenusPage
()
{
const
{
message
}
=
App
.
useApp
();
const
{
message
}
=
App
.
useApp
();
const
[
roles
,
setRoles
]
=
useState
<
Role
[]
>
([]);
const
[
roles
,
setRoles
]
=
useState
<
Role
[]
>
([]);
...
@@ -54,8 +117,57 @@ export default function MenusPage() {
...
@@ -54,8 +117,57 @@ export default function MenusPage() {
const
[
menuLoading
,
setMenuLoading
]
=
useState
(
false
);
const
[
menuLoading
,
setMenuLoading
]
=
useState
(
false
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
// Menu CRUD state
const
[
addVisible
,
setAddVisible
]
=
useState
(
false
);
const
[
editVisible
,
setEditVisible
]
=
useState
(
false
);
const
[
editingMenu
,
setEditingMenu
]
=
useState
<
Menu
|
null
>
(
null
);
const
[
defaultParentId
,
setDefaultParentId
]
=
useState
<
number
|
undefined
>
(
undefined
);
const
allMenuKeys
=
useMemo
(()
=>
collectAllKeys
(
menus
),
[
menus
]);
const
allMenuKeys
=
useMemo
(()
=>
collectAllKeys
(
menus
),
[
menus
]);
const
treeData
=
useMemo
(()
=>
menusToTreeData
(
menus
),
[
menus
]);
const
parentTreeData
=
useMemo
(()
=>
menusToSelectData
(
menus
),
[
menus
]);
const
handleEdit
=
useCallback
((
m
:
Menu
)
=>
{
setEditingMenu
(
m
);
setEditVisible
(
true
);
},
[]);
const
handleDelete
=
useCallback
((
m
:
Menu
)
=>
{
Modal
.
confirm
({
title
:
'
确认删除
'
,
content
:
`确定要删除菜单「
${
m
.
name
}
」吗?子菜单将一并删除,该操作不可恢复。`
,
okType
:
'
danger
'
,
onOk
:
async
()
=>
{
try
{
await
menuApi
.
delete
(
m
.
id
);
message
.
success
(
'
已删除
'
);
fetchMenus
();
}
catch
{
message
.
error
(
'
删除失败
'
);
}
},
});
},
[]);
const
handleAddChild
=
useCallback
((
m
:
Menu
)
=>
{
setDefaultParentId
(
m
.
id
);
setAddVisible
(
true
);
},
[]);
const
treeData
=
useMemo
(
()
=>
menusToTreeData
(
menus
,
handleEdit
,
handleDelete
,
handleAddChild
),
[
menus
,
handleEdit
,
handleDelete
,
handleAddChild
],
);
const
fetchMenus
=
useCallback
(
async
()
=>
{
setMenuLoading
(
true
);
try
{
const
res
=
await
menuApi
.
listTree
();
setMenus
(
res
.
data
||
[]);
}
catch
{
message
.
error
(
'
加载菜单失败
'
);
}
setMenuLoading
(
false
);
},
[]);
// 加载角色列表和菜单树
// 加载角色列表和菜单树
const
fetchInitial
=
useCallback
(
async
()
=>
{
const
fetchInitial
=
useCallback
(
async
()
=>
{
...
@@ -68,7 +180,6 @@ export default function MenusPage() {
...
@@ -68,7 +180,6 @@ export default function MenusPage() {
const
roleList
=
rolesRes
.
data
||
[];
const
roleList
=
rolesRes
.
data
||
[];
setRoles
(
roleList
);
setRoles
(
roleList
);
setMenus
(
menusRes
.
data
||
[]);
setMenus
(
menusRes
.
data
||
[]);
// 默认选中第一个角色
if
(
roleList
.
length
>
0
&&
!
selectedRoleId
)
{
if
(
roleList
.
length
>
0
&&
!
selectedRoleId
)
{
setSelectedRoleId
(
roleList
[
0
].
id
);
setSelectedRoleId
(
roleList
[
0
].
id
);
}
}
...
@@ -120,9 +231,69 @@ export default function MenusPage() {
...
@@ -120,9 +231,69 @@ export default function MenusPage() {
setCheckedKeys
([]);
setCheckedKeys
([]);
};
};
const
selectedRole
=
roles
.
find
((
r
)
=>
r
.
id
===
selectedRoleId
);
const
selectedRole
=
roles
.
find
((
r
)
=>
r
.
id
===
selectedRoleId
);
// 表单字段
const
menuFormFields
=
(
<>
<
ProFormText
name=
"name"
label=
"菜单名称"
rules=
{
[{
required
:
true
,
message
:
'
请输入菜单名称
'
}]
}
placeholder=
"请输入菜单名称"
/>
<
ProFormSelect
name=
"type"
label=
"菜单类型"
options=
{
menuTypeOptions
}
rules=
{
[{
required
:
true
,
message
:
'
请选择菜单类型
'
}]
}
placeholder=
"请选择菜单类型"
/>
<
ProFormTreeSelect
name=
"parent_id"
label=
"上级菜单"
placeholder=
"留空表示顶级菜单"
allowClear
fieldProps=
{
{
treeData
:
parentTreeData
,
treeDefaultExpandAll
:
true
,
}
}
/>
<
ProFormText
name=
"path"
label=
"路由路径"
placeholder=
"如 /admin/menus"
/>
<
ProFormText
name=
"icon"
label=
"图标"
placeholder=
"Ant Design 图标名称"
/>
<
ProFormText
name=
"component"
label=
"组件路径"
placeholder=
"前端组件路径(选填)"
/>
<
ProFormText
name=
"permission"
label=
"权限标识"
placeholder=
"如 admin:menu:list(选填)"
/>
<
ProFormDigit
name=
"sort"
label=
"排序号"
placeholder=
"数字越小越靠前"
min=
{
0
}
fieldProps=
{
{
style
:
{
width
:
'
100%
'
}
}
}
/>
<
ProFormSwitch
name=
"visible"
label=
"是否显示"
fieldProps=
{
{
checkedChildren
:
'
显示
'
,
unCheckedChildren
:
'
隐藏
'
}
}
/>
</>
);
return
(
return
(
<
div
style=
{
{
padding
:
'
20px 24px
'
,
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
gap
:
16
}
}
>
<
div
style=
{
{
padding
:
'
20px 24px
'
,
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
gap
:
16
}
}
>
{
/* Header */
}
{
/* Header */
}
...
@@ -132,11 +303,18 @@ export default function MenusPage() {
...
@@ -132,11 +303,18 @@ export default function MenusPage() {
<
AppstoreOutlined
style=
{
{
marginRight
:
8
,
color
:
'
#1890ff
'
}
}
/>
菜单管理
<
AppstoreOutlined
style=
{
{
marginRight
:
8
,
color
:
'
#1890ff
'
}
}
/>
菜单管理
</
h2
>
</
h2
>
<
div
style=
{
{
fontSize
:
13
,
color
:
'
#8c8c8c
'
,
marginTop
:
2
}
}
>
<
div
style=
{
{
fontSize
:
13
,
color
:
'
#8c8c8c
'
,
marginTop
:
2
}
}
>
选择角色,勾选
分配菜单权限
管理系统菜单结构,为角色
分配菜单权限
</
div
>
</
div
>
</
div
>
</
div
>
<
Space
>
<
Space
>
<
Button
icon=
{
<
ReloadOutlined
/>
}
onClick=
{
fetchInitial
}
>
刷新
</
Button
>
<
Button
icon=
{
<
ReloadOutlined
/>
}
onClick=
{
fetchInitial
}
>
刷新
</
Button
>
<
Button
type=
"primary"
icon=
{
<
PlusOutlined
/>
}
onClick=
{
()
=>
{
setDefaultParentId
(
undefined
);
setAddVisible
(
true
);
}
}
>
添加菜单
</
Button
>
</
Space
>
</
Space
>
</
div
>
</
div
>
...
@@ -225,6 +403,75 @@ export default function MenusPage() {
...
@@ -225,6 +403,75 @@ export default function MenusPage() {
</
Card
>
</
Card
>
</
div
>
</
div
>
</
Spin
>
</
Spin
>
{
/* Add Menu Drawer */
}
<
DrawerForm
title=
"添加菜单"
open=
{
addVisible
}
onOpenChange=
{
(
open
)
=>
{
setAddVisible
(
open
);
if
(
!
open
)
setDefaultParentId
(
undefined
);
}
}
width=
{
480
}
drawerProps=
{
{
placement
:
'
right
'
,
destroyOnClose
:
true
}
}
initialValues=
{
{
sort
:
0
,
visible
:
true
,
type
:
'
menu
'
,
parent_id
:
defaultParentId
}
}
onFinish=
{
async
(
values
)
=>
{
try
{
await
menuApi
.
create
({
...
values
,
parent_id
:
values
.
parent_id
||
0
,
});
message
.
success
(
'
菜单创建成功
'
);
fetchMenus
();
return
true
;
}
catch
{
message
.
error
(
'
操作失败
'
);
return
false
;
}
}
}
>
{
menuFormFields
}
</
DrawerForm
>
{
/* Edit Menu Drawer */
}
<
DrawerForm
title=
"编辑菜单"
open=
{
editVisible
}
onOpenChange=
{
(
open
)
=>
{
setEditVisible
(
open
);
if
(
!
open
)
setEditingMenu
(
null
);
}
}
width=
{
480
}
drawerProps=
{
{
placement
:
'
right
'
,
destroyOnClose
:
true
}
}
initialValues=
{
editingMenu
?
{
name
:
editingMenu
.
name
,
type
:
editingMenu
.
type
||
'
menu
'
,
parent_id
:
editingMenu
.
parent_id
||
undefined
,
path
:
editingMenu
.
path
,
icon
:
editingMenu
.
icon
,
component
:
editingMenu
.
component
,
permission
:
editingMenu
.
permission
,
sort
:
editingMenu
.
sort
,
visible
:
editingMenu
.
visible
,
}
:
undefined
}
onFinish=
{
async
(
values
)
=>
{
if
(
!
editingMenu
)
return
false
;
try
{
await
menuApi
.
update
(
editingMenu
.
id
,
{
...
values
,
parent_id
:
values
.
parent_id
||
0
,
});
message
.
success
(
'
菜单更新成功
'
);
fetchMenus
();
return
true
;
}
catch
{
message
.
error
(
'
操作失败
'
);
return
false
;
}
}
}
>
{
menuFormFields
}
</
DrawerForm
>
</
div
>
</
div
>
);
);
}
}
web/src/app/(main)/admin/workflows/page.tsx
View file @
04584395
'
use client
'
;
import
PageComponent
from
'
@/pages/admin/Workflows
'
;
export
default
function
Page
()
{
return
<
PageComponent
/>;
}
import
{
useEffect
,
useState
,
useCallback
}
from
'
react
'
;
import
{
Card
,
Table
,
Tag
,
Button
,
Drawer
,
Form
,
Input
,
Select
,
message
,
Space
,
Badge
,
Popconfirm
}
from
'
antd
'
;
import
{
DrawerForm
,
ProFormText
,
ProFormTextArea
,
ProFormSelect
}
from
'
@ant-design/pro-components
'
;
import
{
DeploymentUnitOutlined
,
PlayCircleOutlined
,
PlusOutlined
,
EditOutlined
,
DeleteOutlined
}
from
'
@ant-design/icons
'
;
import
{
workflowApi
}
from
'
@/api/agent
'
;
import
VisualWorkflowEditor
from
'
@/components/workflow/VisualWorkflowEditor
'
;
interface
Workflow
{
id
:
number
;
workflow_id
:
string
;
name
:
string
;
description
:
string
;
category
:
string
;
status
:
string
;
version
:
number
;
definition
?:
string
;
}
const
statusColor
:
Record
<
string
,
'
success
'
|
'
warning
'
|
'
default
'
>
=
{
active
:
'
success
'
,
draft
:
'
warning
'
,
archived
:
'
default
'
,
};
const
statusLabel
:
Record
<
string
,
string
>
=
{
active
:
'
已启用
'
,
draft
:
'
草稿
'
,
archived
:
'
已归档
'
,
};
const
categoryLabel
:
Record
<
string
,
string
>
=
{
pre_consult
:
'
预问诊
'
,
diagnosis
:
'
诊断
'
,
prescription
:
'
处方审核
'
,
follow_up
:
'
随访
'
,
};
export
default
function
WorkflowsPage
()
{
const
[
workflows
,
setWorkflows
]
=
useState
<
Workflow
[]
>
([]);
const
[
createDrawer
,
setCreateDrawer
]
=
useState
(
false
);
const
[
editorDrawer
,
setEditorDrawer
]
=
useState
(
false
);
const
[
editingWorkflow
,
setEditingWorkflow
]
=
useState
<
Workflow
|
null
>
(
null
);
const
[
form
]
=
Form
.
useForm
();
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
tableLoading
,
setTableLoading
]
=
useState
(
false
);
const
fetchWorkflows
=
async
()
=>
{
setTableLoading
(
true
);
try
{
const
res
=
await
workflowApi
.
list
();
setWorkflows
((
res
.
data
as
Workflow
[])
||
[]);
}
catch
{}
finally
{
setTableLoading
(
false
);
}
};
useEffect
(()
=>
{
fetchWorkflows
();
},
[]);
const
handleCreate
=
async
(
values
:
Record
<
string
,
string
>
)
=>
{
setLoading
(
true
);
try
{
const
definition
=
{
id
:
values
.
workflow_id
,
name
:
values
.
name
,
nodes
:
{
start
:
{
id
:
'
start
'
,
type
:
'
start
'
,
name
:
'
开始
'
,
config
:
{},
next_nodes
:
[
'
end
'
]
},
end
:
{
id
:
'
end
'
,
type
:
'
end
'
,
name
:
'
结束
'
,
config
:
{},
next_nodes
:
[]
},
},
edges
:
[{
id
:
'
e1
'
,
source_node
:
'
start
'
,
target_node
:
'
end
'
}],
};
await
workflowApi
.
create
({
workflow_id
:
values
.
workflow_id
,
name
:
values
.
name
,
description
:
values
.
description
,
category
:
values
.
category
,
definition
:
JSON
.
stringify
(
definition
),
});
message
.
success
(
'
创建成功
'
);
setCreateDrawer
(
false
);
form
.
resetFields
();
fetchWorkflows
();
}
catch
{
message
.
error
(
'
创建失败
'
);
}
finally
{
setLoading
(
false
);
}
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const
handleSaveWorkflow
=
useCallback
(
async
(
nodes
:
any
[],
edges
:
any
[])
=>
{
if
(
!
editingWorkflow
)
return
;
try
{
await
workflowApi
.
update
(
editingWorkflow
.
id
,
{
definition
:
JSON
.
stringify
({
nodes
,
edges
})
});
message
.
success
(
'
工作流已保存
'
);
fetchWorkflows
();
}
catch
{
message
.
error
(
'
保存失败
'
);
}
},
[
editingWorkflow
]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const
handleExecuteFromEditor
=
useCallback
(
async
(
nodes
:
any
[],
edges
:
any
[])
=>
{
if
(
!
editingWorkflow
)
return
;
try
{
const
result
=
await
workflowApi
.
execute
(
editingWorkflow
.
workflow_id
,
{
workflow_data
:
{
nodes
,
edges
}
});
message
.
success
(
`执行已启动:
${
result
.
data
?.
execution_id
}
`);
} catch { message.error('执行失败'); }
}, [editingWorkflow]);
const handleExecute = async (workflowId: string) => {
try {
const result = await workflowApi.execute(workflowId);
message.success(`
执行已启动
:
$
{
result
.
data
?.
execution_id
}
`);
} catch { message.error('执行失败'); }
};
const getEditorInitialData = (): { nodes?: unknown[]; edges?: unknown[] } | undefined => {
if (!editingWorkflow?.definition) return undefined;
try { return JSON.parse(editingWorkflow.definition); } catch { return undefined; }
};
const columns = [
{
title: '工作流', key: 'info',
render: (_: unknown, r: Workflow) => (
<div>
<div style={{ fontWeight: 500 }}>{r.name}</div>
<div style={{ fontSize: 12, color: '#8c8c8c' }}>{r.workflow_id}</div>
</div>
),
},
{ title: '描述', dataIndex: 'description', key: 'description', ellipsis: true },
{
title: '类别', dataIndex: 'category', key: 'category', width: 100,
render: (v: string) => <Tag color="blue">{categoryLabel[v] || v}</Tag>,
},
{ title: '版本', dataIndex: 'version', key: 'version', width: 70, render: (v: number) => `
v$
{
v
}
` },
{
title: '状态', dataIndex: 'status', key: 'status', width: 90,
render: (v: string) => <Badge status={statusColor[v] || 'default'} text={statusLabel[v] || v} />,
},
{
title: '操作', key: 'action', width: 260,
render: (_: unknown, r: Workflow) => (
<Space size={0}>
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => { setEditingWorkflow(r); setEditorDrawer(true); }}>编辑</Button>
<Button type="link" size="small" icon={<PlayCircleOutlined />} disabled={r.status !== 'active'} onClick={() => handleExecute(r.workflow_id)}>执行</Button>
{r.status === 'draft' && (
<Button type="link" size="small" onClick={async () => {
await workflowApi.publish(r.id);
message.success('已激活');
fetchWorkflows();
}}>激活</Button>
)}
<Popconfirm title="确认删除?" onConfirm={async () => {
await workflowApi.delete(r.workflow_id);
message.success('删除成功');
fetchWorkflows();
}}>
<Button type="link" danger size="small" icon={<DeleteOutlined />}>删除</Button>
</Popconfirm>
</Space>
),
},
];
return (
<div style={{ padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#1d2129', margin: 0 }}>工作流管理</h2>
<div style={{ fontSize: 13, color: '#8c8c8c', marginTop: 2 }}>设计和管理 AI 工作流,实现复杂业务流程自动化</div>
</div>
{/* 操作栏 */}
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<DeploymentUnitOutlined style={{ color: '#8c8c8c' }} />
<span style={{ fontSize: 13, color: '#8c8c8c' }}>共 {workflows.length} 个工作流</span>
<Button type="primary" icon={<PlusOutlined />} style={{ marginLeft: 'auto' }} onClick={() => setCreateDrawer(true)}>
新建工作流
</Button>
</div>
</Card>
{/* 工作流列表 */}
<Card style={{ borderRadius: 12, border: '1px solid #edf2fc' }}>
<Table dataSource={workflows} columns={columns} rowKey="id" loading={tableLoading} size="small"
pagination={{ pageSize: 10, showSizeChanger: true, size: 'small', showTotal: (t) => `
共
$
{
t
}
条
` }}
/>
</Card>
{/* 新建工作流 DrawerForm */}
<DrawerForm
title="新建工作流"
open={createDrawer}
onOpenChange={(open) => { setCreateDrawer(open); if (!open) form.resetFields(); }}
onFinish={async (values) => { await handleCreate(values); return true; }}
drawerProps={{ placement: 'right', destroyOnClose: true }}
width={480}
loading={loading}
form={form}
>
<ProFormText name="workflow_id" label="工作流 ID" placeholder="如: smart_pre_consult"
rules={[{ required: true, message: '请输入工作流ID' }]} />
<ProFormText name="name" label="名称" placeholder="请输入工作流名称"
rules={[{ required: true, message: '请输入名称' }]} />
<ProFormTextArea name="description" label="描述" placeholder="请输入描述(选填)"
fieldProps={{ rows: 2 }} />
<ProFormSelect name="category" label="类别" placeholder="选择类别"
options={[
{ value: 'pre_consult', label: '预问诊' },
{ value: 'diagnosis', label: '诊断' },
{ value: 'prescription', label: '处方审核' },
{ value: 'follow_up', label: '随访' },
]} />
</DrawerForm>
{/* 可视化编辑器 Drawer */}
<Drawer
title={`
编辑工作流
·
$
{
editingWorkflow
?.
name
}
`}
open={editorDrawer}
onClose={() => { setEditorDrawer(false); setEditingWorkflow(null); }}
placement="right"
destroyOnClose
width={960}
>
<div style={{ height: 650 }}>
<VisualWorkflowEditor
workflowName={editingWorkflow?.name || '编辑工作流'}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
initialNodes={getEditorInitialData()?.nodes as any}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
initialEdges={getEditorInitialData()?.edges as any}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onSave={handleSaveWorkflow as any}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onExecute={handleExecuteFromEditor as any}
/>
</div>
</Drawer>
</div>
);
}
web/src/config/routes.ts
View file @
04584395
...
@@ -95,6 +95,30 @@ export const ROUTE_REGISTRY: RouteDefinition[] = [
...
@@ -95,6 +95,30 @@ export const ROUTE_REGISTRY: RouteDefinition[] = [
permissions
:
[
'
admin:pharmacy:list
'
],
permissions
:
[
'
admin:pharmacy:list
'
],
operations
:
{
list
:
'
/admin/pharmacy
'
},
operations
:
{
list
:
'
/admin/pharmacy
'
},
},
},
{
code
:
'
admin_users
'
,
path
:
'
/admin/users
'
,
name
:
'
用户管理
'
,
role
:
'
admin
'
,
permissions
:
[
'
admin:users:list
'
],
operations
:
{
list
:
'
/admin/users
'
},
},
{
code
:
'
admin_doctor_review
'
,
path
:
'
/admin/doctor-review
'
,
name
:
'
医生审核
'
,
role
:
'
admin
'
,
permissions
:
[
'
admin:doctor-review:list
'
],
operations
:
{
list
:
'
/admin/doctor-review
'
},
},
{
code
:
'
admin_statistics
'
,
path
:
'
/admin/statistics
'
,
name
:
'
数据统计
'
,
role
:
'
admin
'
,
permissions
:
[
'
admin:statistics:view
'
],
operations
:
{
list
:
'
/admin/statistics
'
},
},
// AI 管理
// AI 管理
{
{
code
:
'
admin_ai_config
'
,
code
:
'
admin_ai_config
'
,
...
@@ -104,29 +128,21 @@ export const ROUTE_REGISTRY: RouteDefinition[] = [
...
@@ -104,29 +128,21 @@ export const ROUTE_REGISTRY: RouteDefinition[] = [
permissions
:
[
'
admin:ai-config:view
'
],
permissions
:
[
'
admin:ai-config:view
'
],
operations
:
{
list
:
'
/admin/ai-config
'
},
operations
:
{
list
:
'
/admin/ai-config
'
},
},
},
{
code
:
'
admin_ai_center
'
,
path
:
'
/admin/ai-center
'
,
name
:
'
AI中心
'
,
role
:
'
admin
'
,
permissions
:
[
'
admin:ai-center:view
'
],
operations
:
{
list
:
'
/admin/ai-center
'
},
},
{
{
code
:
'
admin_agents
'
,
code
:
'
admin_agents
'
,
path
:
'
/admin/agents
'
,
path
:
'
/admin/agents
'
,
name
:
'
Agent
管理
'
,
name
:
'
智能体
管理
'
,
role
:
'
admin
'
,
role
:
'
admin
'
,
permissions
:
[
'
admin:agents:list
'
],
permissions
:
[
'
admin:agents:list
'
],
operations
:
{
list
:
'
/admin/agents
'
},
operations
:
{
list
:
'
/admin/agents
'
},
},
},
{
{
code
:
'
admin_
tool
s
'
,
code
:
'
admin_
ai_log
s
'
,
path
:
'
/admin/
tool
s
'
,
path
:
'
/admin/
ai-log
s
'
,
name
:
'
工具管理
'
,
name
:
'
AI日志
'
,
role
:
'
admin
'
,
role
:
'
admin
'
,
permissions
:
[
'
admin:
tools:list
'
],
permissions
:
[
'
admin:
ai-logs:view
'
],
operations
:
{
list
:
'
/admin/
tool
s
'
},
operations
:
{
list
:
'
/admin/
ai-log
s
'
},
},
},
{
{
code
:
'
admin_workflows
'
,
code
:
'
admin_workflows
'
,
...
...
web/src/pages/admin/Departments/index.tsx
View file @
04584395
...
@@ -2,28 +2,42 @@
...
@@ -2,28 +2,42 @@
import
React
,
{
useState
,
useEffect
,
useRef
}
from
'
react
'
;
import
React
,
{
useState
,
useEffect
,
useRef
}
from
'
react
'
;
import
{
useSearchParams
}
from
'
next/navigation
'
;
import
{
useSearchParams
}
from
'
next/navigation
'
;
import
{
Typography
,
Space
,
Button
,
Modal
,
App
}
from
'
antd
'
;
import
{
Typography
,
Space
,
Button
,
Modal
,
Tag
,
App
}
from
'
antd
'
;
import
{
PlusOutlined
,
EditOutlined
,
DeleteOutlined
}
from
'
@ant-design/icons
'
;
import
{
import
{
ProTable
,
DrawerForm
,
ProFormText
,
ProFormDigit
}
from
'
@ant-design/pro-components
'
;
PlusOutlined
,
EditOutlined
,
DeleteOutlined
,
MedicineBoxOutlined
,
}
from
'
@ant-design/icons
'
;
import
{
ProTable
,
DrawerForm
,
ProFormText
,
ProFormDigit
,
}
from
'
@ant-design/pro-components
'
;
import
type
{
ActionType
,
ProColumns
}
from
'
@ant-design/pro-components
'
;
import
type
{
ActionType
,
ProColumns
}
from
'
@ant-design/pro-components
'
;
import
{
adminApi
}
from
'
../../../api/admin
'
;
import
{
adminApi
}
from
'
../../../api/admin
'
;
import
type
{
Department
}
from
'
../../../api/doctor
'
;
import
type
{
Department
}
from
'
../../../api/doctor
'
;
const
{
Text
}
=
Typography
;
const
AdminDepartmentsPage
:
React
.
FC
=
()
=>
{
const
AdminDepartmentsPage
:
React
.
FC
=
()
=>
{
const
{
message
}
=
App
.
useApp
();
const
{
message
}
=
App
.
useApp
();
const
searchParams
=
useSearchParams
();
const
searchParams
=
useSearchParams
();
const
actionRef
=
useRef
<
ActionType
>
();
const
actionRef
=
useRef
<
ActionType
>
(
null
);
const
[
modalVisible
,
setModalVisible
]
=
useState
(
false
);
const
[
addModalVisible
,
setAddModalVisible
]
=
useState
(
false
);
const
[
editModalVisible
,
setEditModalVisible
]
=
useState
(
false
);
const
[
editingDept
,
setEditingDept
]
=
useState
<
Department
|
null
>
(
null
);
const
[
editingDept
,
setEditingDept
]
=
useState
<
Department
|
null
>
(
null
);
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
searchParams
.
get
(
'
action
'
)
===
'
add
'
)
setModalVisible
(
true
);
if
(
searchParams
.
get
(
'
action
'
)
===
'
add
'
)
set
Add
ModalVisible
(
true
);
},
[
searchParams
]);
},
[
searchParams
]);
const
handleEdit
=
(
record
:
Department
)
=>
{
setEditingDept
(
record
);
setEditModalVisible
(
true
);
};
const
handleDelete
=
(
record
:
Department
)
=>
{
const
handleDelete
=
(
record
:
Department
)
=>
{
Modal
.
confirm
({
Modal
.
confirm
({
title
:
'
确认删除
'
,
title
:
'
确认删除
'
,
content
:
`确定要删除科室「
${
record
.
name
}
」吗?该操作不可恢复。`
,
content
:
`确定要删除科室「
${
record
.
name
}
」吗?该操作不可恢复。`
,
okType
:
'
danger
'
,
onOk
:
async
()
=>
{
onOk
:
async
()
=>
{
try
{
try
{
await
adminApi
.
deleteDepartment
(
record
.
id
);
await
adminApi
.
deleteDepartment
(
record
.
id
);
...
@@ -38,74 +52,192 @@ const AdminDepartmentsPage: React.FC = () => {
...
@@ -38,74 +52,192 @@ const AdminDepartmentsPage: React.FC = () => {
const
columns
:
ProColumns
<
Department
>
[]
=
[
const
columns
:
ProColumns
<
Department
>
[]
=
[
{
{
title
:
'
图标
'
,
title
:
'
关键词
'
,
dataIndex
:
'
icon
'
,
dataIndex
:
'
keyword
'
,
width
:
60
,
hideInTable
:
true
,
render
:
(
_
,
record
)
=>
<
span
style=
{
{
fontSize
:
24
}
}
>
{
record
.
icon
||
'
🏥
'
}
</
span
>
,
fieldProps
:
{
placeholder
:
'
搜索科室名称
'
}
,
},
},
{
{
title
:
'
科室
名称
'
,
title
:
'
科室
'
,
dataIndex
:
'
name
'
,
dataIndex
:
'
name
'
,
render
:
(
_
,
record
)
=>
<
Typography
.
Text
strong
>
{
record
.
name
}
</
Typography
.
Text
>,
search
:
false
,
render
:
(
_
,
record
)
=>
(
<
Space
>
<
div
style=
{
{
width
:
40
,
height
:
40
,
borderRadius
:
8
,
background
:
'
linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%)
'
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
justifyContent
:
'
center
'
,
fontSize
:
20
,
}
}
>
{
record
.
icon
||
<
MedicineBoxOutlined
style=
{
{
color
:
'
#1890ff
'
}
}
/>
}
</
div
>
<
div
>
<
Text
strong
>
{
record
.
name
}
</
Text
>
{
record
.
parent_id
&&
(
<>
<
br
/>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
12
}
}
>
子科室
</
Text
>
</>
)
}
</
div
>
</
Space
>
),
},
},
{
{
title
:
'
排序
'
,
title
:
'
排序
'
,
dataIndex
:
'
sort_order
'
,
dataIndex
:
'
sort_order
'
,
search
:
false
,
width
:
80
,
render
:
(
v
)
=>
<
Tag
>
{
v
as
number
}
</
Tag
>,
},
{
title
:
'
子科室
'
,
dataIndex
:
'
children
'
,
width
:
100
,
search
:
false
,
render
:
(
_
,
record
)
=>
{
const
count
=
record
.
children
?.
length
||
0
;
return
count
>
0
?
<
Tag
color=
"blue"
>
{
count
}
个
</
Tag
>
:
<
Text
type=
"secondary"
>
-
</
Text
>;
},
},
},
{
{
title
:
'
操作
'
,
title
:
'
操作
'
,
valueType
:
'
option
'
,
valueType
:
'
option
'
,
width
:
160
,
render
:
(
_
,
record
)
=>
(
render
:
(
_
,
record
)
=>
(
<
Space
>
<
Space
size=
{
0
}
>
<
Button
type=
"link"
size=
"small"
icon=
{
<
EditOutlined
/>
}
onClick=
{
()
=>
{
setEditingDept
(
record
);
setModalVisible
(
true
);
}
}
>
编辑
</
Button
>
<
Button
type=
"link"
size=
"small"
icon=
{
<
EditOutlined
/>
}
onClick=
{
()
=>
handleEdit
(
record
)
}
>
<
Button
type=
"link"
size=
"small"
danger
icon=
{
<
DeleteOutlined
/>
}
onClick=
{
()
=>
handleDelete
(
record
)
}
>
删除
</
Button
>
编辑
</
Button
>
<
Button
type=
"link"
size=
"small"
danger
icon=
{
<
DeleteOutlined
/>
}
onClick=
{
()
=>
handleDelete
(
record
)
}
>
删除
</
Button
>
</
Space
>
</
Space
>
),
),
},
},
];
];
return
(
const
formContent
=
(
<>
<>
<
ProFormText
name=
"name"
label=
"科室名称"
rules=
{
[{
required
:
true
,
message
:
'
请输入科室名称
'
}]
}
placeholder=
"请输入科室名称"
/>
<
ProFormText
name=
"icon"
label=
"图标"
placeholder=
"请输入Emoji图标,如 🏥 🫀 🧠"
extra=
"支持 Emoji 表情,留空将显示默认图标"
/>
<
ProFormDigit
name=
"sort_order"
label=
"排序号"
placeholder=
"数字越小越靠前"
min=
{
1
}
fieldProps=
{
{
style
:
{
width
:
'
100%
'
}
}
}
extra=
"排序号越小,科室排列越靠前"
/>
</>
);
return
(
<
div
style=
{
{
padding
:
'
20px 24px
'
,
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
gap
:
16
}
}
>
<
div
>
<
h2
style=
{
{
fontSize
:
20
,
fontWeight
:
700
,
color
:
'
#1d2129
'
,
margin
:
0
}
}
>
科室管理
</
h2
>
<
div
style=
{
{
fontSize
:
13
,
color
:
'
#8c8c8c
'
,
marginTop
:
2
}
}
>
管理医院科室分类与排序
</
div
>
</
div
>
<
ProTable
<
Department
>
<
ProTable
<
Department
>
headerTitle="科室管理"
headerTitle="科室列表"
tooltip="管理医院科室分类与排序"
rowKey="id"
rowKey="id"
actionRef=
{
actionRef
}
actionRef=
{
actionRef
}
cardBordered
cardBordered
search=
{
false
}
search=
{
{
labelWidth
:
'
auto
'
,
optionRender
:
(
searchConfig
,
formProps
,
dom
)
=>
[
...
dom
,
<
Button
key=
"add"
type=
"primary"
icon=
{
<
PlusOutlined
/>
}
onClick=
{
()
=>
setAddModalVisible
(
true
)
}
>
添加科室
</
Button
>,
],
}
}
options=
{
{
density
:
true
,
reload
:
true
,
setting
:
true
}
}
options=
{
{
density
:
true
,
reload
:
true
,
setting
:
true
}
}
request=
{
async
()
=>
{
request=
{
async
(
params
)
=>
{
const
res
=
await
adminApi
.
getDepartmentList
();
const
res
=
await
adminApi
.
getDepartmentList
();
return
{
data
:
res
.
data
||
[],
success
:
true
};
const
list
=
res
.
data
||
[];
// Flatten tree for display
const
rows
:
Department
[]
=
[];
const
flatten
=
(
items
:
Department
[])
=>
{
for
(
const
item
of
items
)
{
rows
.
push
(
item
);
if
(
item
.
children
?.
length
)
flatten
(
item
.
children
);
}
};
flatten
(
Array
.
isArray
(
list
)
?
list
:
[]);
// Client-side keyword filter
const
keyword
=
params
.
keyword
?.
trim
()?.
toLowerCase
();
const
filtered
=
keyword
?
rows
.
filter
((
d
)
=>
d
.
name
.
toLowerCase
().
includes
(
keyword
))
:
rows
;
return
{
data
:
filtered
,
success
:
true
,
total
:
filtered
.
length
};
}
}
}
}
pagination=
{
false
}
pagination=
{
{
defaultPageSize
:
20
,
showSizeChanger
:
true
,
showTotal
:
(
t
)
=>
`共 ${t} 个科室`
}
}
toolBarRender=
{
()
=>
[
toolBarRender=
{
()
=>
[]
}
<
Button
key=
"add"
type=
"primary"
icon=
{
<
PlusOutlined
/>
}
onClick=
{
()
=>
{
setEditingDept
(
null
);
setModalVisible
(
true
);
}
}
>
添加科室
</
Button
>,
]
}
columns=
{
columns
}
columns=
{
columns
}
/
>
/
>
{
/* Add Department */
}
<
DrawerForm
title=
"添加科室"
open=
{
addModalVisible
}
onOpenChange=
{
setAddModalVisible
}
initialValues=
{
{
sort_order
:
1
}
}
width=
{
480
}
drawerProps=
{
{
placement
:
'
right
'
,
destroyOnClose
:
true
}
}
onFinish=
{
async
(
values
)
=>
{
try
{
await
adminApi
.
createDepartment
(
values
);
message
.
success
(
'
科室创建成功
'
);
actionRef
.
current
?.
reload
();
return
true
;
}
catch
{
message
.
error
(
'
操作失败
'
);
return
false
;
}
}
}
>
{
formContent
}
</
DrawerForm
>
{
/* Edit Department */
}
<
DrawerForm
<
DrawerForm
title=
{
editingDept
?
'
编辑科室
'
:
'
添加科室
'
}
title=
"编辑科室"
open=
{
m
odalVisible
}
open=
{
editM
odalVisible
}
onOpenChange=
{
(
open
)
=>
{
onOpenChange=
{
(
open
)
=>
{
setModalVisible
(
open
);
set
Edit
ModalVisible
(
open
);
if
(
!
open
)
setEditingDept
(
null
);
if
(
!
open
)
setEditingDept
(
null
);
}
}
}
}
initialValues=
{
editingDept
||
{
sort_order
:
1
}
}
initialValues=
{
editingDept
||
{
sort_order
:
1
}
}
width=
{
480
}
width=
{
480
}
drawerProps=
{
{
placement
:
'
right
'
,
destroyOnClose
:
true
}
}
drawerProps=
{
{
placement
:
'
right
'
,
destroyOnClose
:
true
}
}
onFinish=
{
async
(
values
)
=>
{
onFinish=
{
async
(
values
)
=>
{
if
(
!
editingDept
)
return
false
;
try
{
try
{
if
(
editingDept
)
{
await
adminApi
.
updateDepartment
(
editingDept
.
id
,
values
);
await
adminApi
.
updateDepartment
(
editingDept
.
id
,
values
);
message
.
success
(
'
科室更新成功
'
);
message
.
success
(
'
科室更新成功
'
);
}
else
{
await
adminApi
.
createDepartment
(
values
);
message
.
success
(
'
科室创建成功
'
);
}
actionRef
.
current
?.
reload
();
actionRef
.
current
?.
reload
();
return
true
;
return
true
;
}
catch
{
}
catch
{
...
@@ -114,11 +246,9 @@ const AdminDepartmentsPage: React.FC = () => {
...
@@ -114,11 +246,9 @@ const AdminDepartmentsPage: React.FC = () => {
}
}
}
}
}
}
>
>
<
ProFormText
name=
"name"
label=
"科室名称"
rules=
{
[{
required
:
true
,
message
:
'
请输入科室名称
'
}]
}
placeholder=
"请输入科室名称"
/>
{
formContent
}
<
ProFormText
name=
"icon"
label=
"图标"
placeholder=
"请输入Emoji图标"
/>
<
ProFormDigit
name=
"sort_order"
label=
"排序号"
min=
{
1
}
fieldProps=
{
{
style
:
{
width
:
'
100%
'
}
}
}
/>
</
DrawerForm
>
</
DrawerForm
>
</>
</
div
>
);
);
};
};
...
...
web/src/pages/admin/Doctors/index.tsx
View file @
04584395
...
@@ -12,6 +12,7 @@ import {
...
@@ -12,6 +12,7 @@ import {
}
from
'
@ant-design/icons
'
;
}
from
'
@ant-design/icons
'
;
import
{
import
{
ProTable
,
DrawerForm
,
ProFormText
,
ProFormSelect
,
ProFormTextArea
,
ProTable
,
DrawerForm
,
ProFormText
,
ProFormSelect
,
ProFormTextArea
,
ProFormDigit
,
}
from
'
@ant-design/pro-components
'
;
}
from
'
@ant-design/pro-components
'
;
import
type
{
ActionType
,
ProColumns
}
from
'
@ant-design/pro-components
'
;
import
type
{
ActionType
,
ProColumns
}
from
'
@ant-design/pro-components
'
;
import
{
adminApi
}
from
'
../../../api/admin
'
;
import
{
adminApi
}
from
'
../../../api/admin
'
;
...
@@ -221,6 +222,16 @@ const AdminDoctorsPage: React.FC = () => {
...
@@ -221,6 +222,16 @@ const AdminDoctorsPage: React.FC = () => {
width
:
150
,
width
:
150
,
ellipsis
:
true
,
ellipsis
:
true
,
},
},
{
title
:
'
问诊价格
'
,
dataIndex
:
'
price
'
,
search
:
false
,
width
:
90
,
render
:
(
_
,
record
)
=>
{
const
p
=
record
.
price
;
return
p
?
<
Text
style=
{
{
color
:
'
#1890ff
'
,
fontWeight
:
600
}
}
>
¥
{
(
p
/
100
).
toFixed
(
0
)
}
</
Text
>
:
<
Text
type=
"secondary"
>
未设置
</
Text
>;
},
},
{
{
title
:
'
认证状态
'
,
title
:
'
认证状态
'
,
dataIndex
:
'
review_status
'
,
dataIndex
:
'
review_status
'
,
...
@@ -397,11 +408,20 @@ const AdminDoctorsPage: React.FC = () => {
...
@@ -397,11 +408,20 @@ const AdminDoctorsPage: React.FC = () => {
placeholder=
"请输入执业证号(选填)"
placeholder=
"请输入执业证号(选填)"
colProps=
{
{
span
:
12
}
}
colProps=
{
{
span
:
12
}
}
/>
/>
<
ProFormDigit
name=
"price"
label=
"问诊价格(分)"
placeholder=
"例如 5000 = ¥50"
min=
{
0
}
fieldProps=
{
{
precision
:
0
}
}
rules=
{
[{
required
:
true
,
message
:
'
请输入问诊价格
'
}]
}
colProps=
{
{
span
:
12
}
}
/>
<
ProFormText
.
Password
<
ProFormText
.
Password
name=
"password"
name=
"password"
label=
"初始密码"
label=
"初始密码"
placeholder=
"默认密码:123456"
placeholder=
"默认密码:123456"
colProps=
{
{
span
:
24
}
}
colProps=
{
{
span
:
12
}
}
/>
/>
<
ProFormTextArea
<
ProFormTextArea
name=
"introduction"
name=
"introduction"
...
@@ -432,6 +452,7 @@ const AdminDoctorsPage: React.FC = () => {
...
@@ -432,6 +452,7 @@ const AdminDoctorsPage: React.FC = () => {
title
:
editingDoctor
.
title
,
title
:
editingDoctor
.
title
,
department_id
:
editingDoctor
.
department_id
,
department_id
:
editingDoctor
.
department_id
,
hospital
:
editingDoctor
.
hospital
,
hospital
:
editingDoctor
.
hospital
,
price
:
editingDoctor
.
price
||
0
,
}
}
:
undefined
:
undefined
}
}
...
@@ -480,7 +501,15 @@ const AdminDoctorsPage: React.FC = () => {
...
@@ -480,7 +501,15 @@ const AdminDoctorsPage: React.FC = () => {
name=
"hospital"
name=
"hospital"
label=
"医院"
label=
"医院"
placeholder=
"请输入医院名称"
placeholder=
"请输入医院名称"
colProps=
{
{
span
:
24
}
}
colProps=
{
{
span
:
12
}
}
/>
<
ProFormDigit
name=
"price"
label=
"问诊价格(分)"
placeholder=
"例如 5000 = ¥50"
min=
{
0
}
fieldProps=
{
{
precision
:
0
}
}
colProps=
{
{
span
:
12
}
}
/>
/>
</
DrawerForm
>
</
DrawerForm
>
...
@@ -501,6 +530,9 @@ const AdminDoctorsPage: React.FC = () => {
...
@@ -501,6 +530,9 @@ const AdminDoctorsPage: React.FC = () => {
<
Descriptions
.
Item
label=
"科室"
>
{
currentDoctor
.
department_name
||
'
-
'
}
</
Descriptions
.
Item
>
<
Descriptions
.
Item
label=
"科室"
>
{
currentDoctor
.
department_name
||
'
-
'
}
</
Descriptions
.
Item
>
<
Descriptions
.
Item
label=
"医院"
span=
{
2
}
>
{
currentDoctor
.
hospital
||
'
-
'
}
</
Descriptions
.
Item
>
<
Descriptions
.
Item
label=
"医院"
span=
{
2
}
>
{
currentDoctor
.
hospital
||
'
-
'
}
</
Descriptions
.
Item
>
<
Descriptions
.
Item
label=
"执业证号"
>
{
currentDoctor
.
license_no
||
'
-
'
}
</
Descriptions
.
Item
>
<
Descriptions
.
Item
label=
"执业证号"
>
{
currentDoctor
.
license_no
||
'
-
'
}
</
Descriptions
.
Item
>
<
Descriptions
.
Item
label=
"问诊价格"
>
{
currentDoctor
.
price
?
`¥${(currentDoctor.price / 100).toFixed(0)}`
:
'
未设置
'
}
</
Descriptions
.
Item
>
<
Descriptions
.
Item
label=
"认证状态"
>
{
getStatusTag
(
currentDoctor
.
review_status
)
}
</
Descriptions
.
Item
>
<
Descriptions
.
Item
label=
"认证状态"
>
{
getStatusTag
(
currentDoctor
.
review_status
)
}
</
Descriptions
.
Item
>
<
Descriptions
.
Item
label=
"账号状态"
>
<
Descriptions
.
Item
label=
"账号状态"
>
<
Tag
color=
{
currentDoctor
.
user_status
===
'
active
'
?
'
green
'
:
'
red
'
}
>
<
Tag
color=
{
currentDoctor
.
user_status
===
'
active
'
?
'
green
'
:
'
red
'
}
>
...
...
web/src/pages/admin/Workflows/index.tsx
0 → 100644
View file @
04584395
'
use client
'
;
import
React
,
{
useState
,
useRef
}
from
'
react
'
;
import
dynamic
from
'
next/dynamic
'
;
import
{
Button
,
Space
,
Tag
,
Badge
,
Drawer
,
Popconfirm
,
App
}
from
'
antd
'
;
import
{
PlayCircleOutlined
,
PlusOutlined
,
EditOutlined
,
DeleteOutlined
,
}
from
'
@ant-design/icons
'
;
import
{
ProTable
,
DrawerForm
,
ProFormText
,
ProFormTextArea
,
ProFormSelect
,
}
from
'
@ant-design/pro-components
'
;
import
type
{
ActionType
,
ProColumns
}
from
'
@ant-design/pro-components
'
;
import
{
workflowApi
}
from
'
@/api/agent
'
;
const
VisualWorkflowEditor
=
dynamic
(
()
=>
import
(
'
@/components/workflow/VisualWorkflowEditor
'
),
{
ssr
:
false
},
);
interface
Workflow
{
id
:
number
;
workflow_id
:
string
;
name
:
string
;
description
:
string
;
category
:
string
;
status
:
string
;
version
:
number
;
definition
?:
string
;
}
const
statusColor
:
Record
<
string
,
'
success
'
|
'
warning
'
|
'
default
'
>
=
{
active
:
'
success
'
,
draft
:
'
warning
'
,
archived
:
'
default
'
,
};
const
statusLabel
:
Record
<
string
,
string
>
=
{
active
:
'
已启用
'
,
draft
:
'
草稿
'
,
archived
:
'
已归档
'
,
};
const
categoryLabel
:
Record
<
string
,
string
>
=
{
pre_consult
:
'
预问诊
'
,
consult_created
:
'
问诊创建
'
,
consult_ended
:
'
问诊结束
'
,
follow_up
:
'
随访
'
,
prescription_created
:
'
处方创建
'
,
prescription_approved
:
'
处方审核通过
'
,
payment_completed
:
'
支付完成
'
,
renewal_requested
:
'
续方申请
'
,
health_alert
:
'
健康预警
'
,
doctor_review
:
'
医生审核
'
,
};
// 后端 definition 格式转 ReactFlow 格式
function
convertDefinitionToReactFlow
(
definition
:
string
|
undefined
)
{
if
(
!
definition
)
return
undefined
;
try
{
const
def
=
JSON
.
parse
(
definition
);
let
nodeArray
:
unknown
[]
=
[];
if
(
def
.
nodes
&&
!
Array
.
isArray
(
def
.
nodes
))
{
const
entries
=
Object
.
values
(
def
.
nodes
)
as
Array
<
{
id
:
string
;
type
:
string
;
name
:
string
;
config
?:
Record
<
string
,
unknown
>
}
>
;
nodeArray
=
entries
.
map
((
n
,
i
)
=>
({
id
:
n
.
id
,
type
:
'
custom
'
,
position
:
{
x
:
250
,
y
:
50
+
i
*
120
},
data
:
{
label
:
n
.
name
,
nodeType
:
n
.
type
,
config
:
n
.
config
},
}));
}
else
if
(
Array
.
isArray
(
def
.
nodes
))
{
nodeArray
=
def
.
nodes
;
}
let
edgeArray
:
unknown
[]
=
[];
if
(
Array
.
isArray
(
def
.
edges
))
{
edgeArray
=
def
.
edges
.
map
((
e
:
{
id
:
string
;
source_node
?:
string
;
source
?:
string
;
target_node
?:
string
;
target
?:
string
})
=>
({
id
:
e
.
id
,
source
:
e
.
source_node
||
e
.
source
,
target
:
e
.
target_node
||
e
.
target
,
animated
:
true
,
style
:
{
stroke
:
'
#1890ff
'
},
}));
}
return
{
nodes
:
nodeArray
,
edges
:
edgeArray
};
}
catch
{
return
undefined
;
}
}
const
AdminWorkflowsPage
:
React
.
FC
=
()
=>
{
const
{
message
}
=
App
.
useApp
();
const
actionRef
=
useRef
<
ActionType
>
(
null
);
const
[
addModalVisible
,
setAddModalVisible
]
=
useState
(
false
);
const
[
editorDrawer
,
setEditorDrawer
]
=
useState
(
false
);
const
[
editingWorkflow
,
setEditingWorkflow
]
=
useState
<
Workflow
|
null
>
(
null
);
const
handleExecute
=
async
(
workflowId
:
string
)
=>
{
try
{
const
result
=
await
workflowApi
.
execute
(
workflowId
);
message
.
success
(
`执行已启动:
${
result
.
data
?.
execution_id
}
`);
} catch { message.error('执行失败'); }
};
const columns: ProColumns<Workflow>[] = [
{
title: '工作流', dataIndex: 'name',
render: (_, r) => (
<div>
<div style={{ fontWeight: 500 }}>{r.name}</div>
<div style={{ fontSize: 12, color: '#8c8c8c' }}>{r.workflow_id}</div>
</div>
),
},
{ title: '描述', dataIndex: 'description', search: false, ellipsis: true },
{
title: '类别', dataIndex: 'category', width: 110,
valueEnum: Object.fromEntries(Object.entries(categoryLabel).map(([k, v]) => [k, { text: v }])),
render: (_, r) => <Tag color="blue">{categoryLabel[r.category] || r.category}</Tag>,
},
{ title: '版本', dataIndex: 'version', search: false, width: 70, render: (v) => `
v$
{
v
as
number
}
` },
{
title: '状态', dataIndex: 'status', width: 90,
valueEnum: { active: { text: '已启用', status: 'Success' }, draft: { text: '草稿', status: 'Warning' }, archived: { text: '已归档', status: 'Default' } },
render: (_, r) => <Badge status={statusColor[r.status] || 'default'} text={statusLabel[r.status] || r.status} />,
},
{
title: '操作', valueType: 'option', width: 260,
render: (_, r) => (
<Space size={0}>
<a onClick={() => { setEditingWorkflow(r); setEditorDrawer(true); }}><EditOutlined /> 编辑</a>
{r.status === 'active' && <a onClick={() => handleExecute(r.workflow_id)}><PlayCircleOutlined /> 执行</a>}
{r.status === 'draft' && (
<a onClick={async () => {
await workflowApi.publish(r.id);
message.success('已激活');
actionRef.current?.reload();
}}>激活</a>
)}
<Popconfirm title="确认删除?" onConfirm={async () => {
await workflowApi.delete(r.workflow_id);
message.success('删除成功');
actionRef.current?.reload();
}}>
<a style={{ color: '#ff4d4f' }}><DeleteOutlined /> 删除</a>
</Popconfirm>
</Space>
),
},
];
const editorData = editorDrawer && editingWorkflow ? convertDefinitionToReactFlow(editingWorkflow.definition) : undefined;
return (
<div style={{ padding: '20px 24px' }}>
<ProTable<Workflow>
headerTitle="工作流管理"
tooltip="设计和管理 AI 工作流,实现复杂业务流程自动化"
actionRef={actionRef}
rowKey="id"
columns={columns}
cardBordered
request={async () => {
const res = await workflowApi.list();
return {
data: (res.data as Workflow[]) || [],
total: (res.data as Workflow[])?.length || 0,
success: true,
};
}}
pagination={{ defaultPageSize: 10, showSizeChanger: true, showTotal: (t) => `
共
$
{
t
}
条
` }}
search={{ labelWidth: 'auto' }}
toolBarRender={() => [
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => setAddModalVisible(true)}>
新建工作流
</Button>,
]}
/>
{/* 新建工作流 DrawerForm */}
<DrawerForm
title="新建工作流"
open={addModalVisible}
onOpenChange={setAddModalVisible}
width={480}
drawerProps={{ placement: 'right', destroyOnClose: true }}
submitter={{
searchConfig: { submitText: '创建', resetText: '取消' },
resetButtonProps: { onClick: () => setAddModalVisible(false) },
}}
onFinish={async (values) => {
try {
const definition = {
id: values.workflow_id, name: values.name,
nodes: {
start: { id: 'start', type: 'start', name: '开始', config: {}, next_nodes: ['end'] },
end: { id: 'end', type: 'end', name: '结束', config: {}, next_nodes: [] },
},
edges: [{ id: 'e1', source_node: 'start', target_node: 'end' }],
};
await workflowApi.create({
workflow_id: values.workflow_id,
name: values.name,
description: values.description,
category: values.category,
definition: JSON.stringify(definition),
});
message.success('创建成功');
actionRef.current?.reload();
return true;
} catch {
message.error('创建失败');
return false;
}
}}
>
<ProFormText name="workflow_id" label="工作流 ID" placeholder="如: smart_pre_consult"
rules={[{ required: true, message: '请输入工作流ID' }]} />
<ProFormText name="name" label="名称" placeholder="请输入工作流名称"
rules={[{ required: true, message: '请输入名称' }]} />
<ProFormTextArea name="description" label="描述" placeholder="请输入描述(选填)"
fieldProps={{ rows: 2 }} />
<ProFormSelect name="category" label="类别" placeholder="选择类别"
options={Object.entries(categoryLabel).map(([value, label]) => ({ value, label }))} />
</DrawerForm>
{/* 可视化编辑器 Drawer */}
<Drawer
title={`
编辑工作流
·
$
{
editingWorkflow
?.
name
||
''
}
`}
open={editorDrawer}
onClose={() => { setEditorDrawer(false); setEditingWorkflow(null); }}
placement="right"
destroyOnClose
width={960}
>
{editorData && (
<div style={{ height: 650 }}>
<VisualWorkflowEditor
workflowName={editingWorkflow?.name || '编辑工作流'}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
initialNodes={editorData.nodes as any}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
initialEdges={editorData.edges as any}
onSave={async (nodes, edges) => {
if (!editingWorkflow) return;
try {
await workflowApi.update(editingWorkflow.id, { definition: JSON.stringify({ nodes, edges }) });
message.success('工作流已保存');
actionRef.current?.reload();
} catch { message.error('保存失败'); }
}}
onExecute={async (nodes, edges) => {
if (!editingWorkflow) return;
try {
const result = await workflowApi.execute(editingWorkflow.workflow_id, { workflow_data: { nodes, edges } });
message.success(`
执行已启动
:
$
{
result
.
data
?.
execution_id
}
`);
} catch { message.error('执行失败'); }
}}
/>
</div>
)}
</Drawer>
</div>
);
};
export default AdminWorkflowsPage;
web/src/pages/patient/TextConsult/index.tsx
View file @
04584395
'
use client
'
;
'
use client
'
;
import
React
,
{
useState
,
useEffect
,
useRef
,
useCallback
}
from
'
react
'
;
import
React
,
{
useState
,
useEffect
,
useRef
}
from
'
react
'
;
import
{
useRouter
,
useParams
}
from
'
next/navigation
'
;
import
{
useRouter
,
useParams
}
from
'
next/navigation
'
;
import
{
Card
,
Input
,
Button
,
List
,
Avatar
,
Typography
,
Space
,
Row
,
Col
,
Tag
,
Divider
,
message
}
from
'
antd
'
;
import
{
Input
,
Button
,
Avatar
,
Typography
,
Space
,
Tag
,
Divider
,
App
,
List
,
Empty
,
Tooltip
}
from
'
antd
'
;
import
{
import
{
SendOutlined
,
RobotOutlined
,
UserOutlined
,
PhoneOutlined
,
SendOutlined
,
RobotOutlined
,
UserOutlined
,
PhoneOutlined
,
MedicineBoxOutlined
,
ClockCircleOutlined
,
ArrowLeftOutlined
,
MedicineBoxOutlined
,
ClockCircleOutlined
,
ArrowLeftOutlined
,
CheckOutlined
,
CheckOutlined
,
MessageOutlined
,
PictureOutlined
,
SmileOutlined
,
FileTextOutlined
,
}
from
'
@ant-design/icons
'
;
}
from
'
@ant-design/icons
'
;
import
{
useQuery
,
useMutation
,
useQueryClient
}
from
'
@tanstack/react-query
'
;
import
{
useQuery
,
useMutation
,
useQueryClient
}
from
'
@tanstack/react-query
'
;
import
{
consultApi
,
type
ConsultMessage
}
from
'
../../../api/consult
'
;
import
{
consultApi
,
type
ConsultMessage
}
from
'
../../../api/consult
'
;
import
{
useUserStore
}
from
'
../../../store/userStore
'
;
import
{
useUserStore
}
from
'
../../../store/userStore
'
;
import
dayjs
from
'
dayjs
'
;
import
dayjs
from
'
dayjs
'
;
const
{
Text
,
Title
}
=
Typography
;
const
{
Text
}
=
Typography
;
const
{
TextArea
}
=
Input
;
const
{
TextArea
}
=
Input
;
const
statusMap
:
Record
<
string
,
{
text
:
string
;
color
:
string
}
>
=
{
const
statusMap
:
Record
<
string
,
{
text
:
string
;
color
:
string
;
bg
:
string
}
>
=
{
pending
:
{
text
:
'
等待接诊
'
,
color
:
'
orange
'
},
pending
:
{
text
:
'
等待接诊
'
,
color
:
'
#fa8c16
'
,
bg
:
'
#fff7e6
'
},
waiting
:
{
text
:
'
等待接诊
'
,
color
:
'
orange
'
},
waiting
:
{
text
:
'
等待接诊
'
,
color
:
'
#fa8c16
'
,
bg
:
'
#fff7e6
'
},
in_progress
:
{
text
:
'
问诊中
'
,
color
:
'
green
'
},
in_progress
:
{
text
:
'
问诊中
'
,
color
:
'
#52c41a
'
,
bg
:
'
#f6ffed
'
},
completed
:
{
text
:
'
已完成
'
,
color
:
'
default
'
},
completed
:
{
text
:
'
已完成
'
,
color
:
'
#8c8c8c
'
,
bg
:
'
#fafafa
'
},
cancelled
:
{
text
:
'
已取消
'
,
color
:
'
red
'
},
cancelled
:
{
text
:
'
已取消
'
,
color
:
'
#ff4d4f
'
,
bg
:
'
#fff2f0
'
},
};
};
const
PatientTextConsultPage
:
React
.
FC
=
()
=>
{
const
PatientTextConsultPage
:
React
.
FC
=
()
=>
{
...
@@ -29,6 +30,7 @@ const PatientTextConsultPage: React.FC = () => {
...
@@ -29,6 +30,7 @@ const PatientTextConsultPage: React.FC = () => {
const
id
=
params
?.
id
;
const
id
=
params
?.
id
;
const
router
=
useRouter
();
const
router
=
useRouter
();
const
{
user
}
=
useUserStore
();
const
{
user
}
=
useUserStore
();
const
{
message
}
=
App
.
useApp
();
const
queryClient
=
useQueryClient
();
const
queryClient
=
useQueryClient
();
const
[
inputValue
,
setInputValue
]
=
useState
(
''
);
const
[
inputValue
,
setInputValue
]
=
useState
(
''
);
const
messagesEndRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
messagesEndRef
=
useRef
<
HTMLDivElement
>
(
null
);
...
@@ -91,8 +93,16 @@ const PatientTextConsultPage: React.FC = () => {
...
@@ -91,8 +93,16 @@ const PatientTextConsultPage: React.FC = () => {
if
(
isSystem
)
{
if
(
isSystem
)
{
return
(
return
(
<
div
key=
{
msg
.
id
}
style=
{
{
textAlign
:
'
center
'
,
margin
:
'
12px 0
'
}
}
>
<
div
key=
{
msg
.
id
}
style=
{
{
display
:
'
flex
'
,
justifyContent
:
'
center
'
,
margin
:
'
16px 0
'
}
}
>
<
Tag
color=
"blue"
style=
{
{
fontSize
:
12
}
}
>
{
msg
.
content
}
</
Tag
>
<
div
style=
{
{
background
:
'
rgba(0,0,0,0.04)
'
,
borderRadius
:
12
,
padding
:
'
3px 14px
'
,
fontSize
:
11
,
color
:
'
#8c8c8c
'
,
}
}
>
{
msg
.
content
}
</
div
>
</
div
>
</
div
>
);
);
}
}
...
@@ -108,42 +118,58 @@ const PatientTextConsultPage: React.FC = () => {
...
@@ -108,42 +118,58 @@ const PatientTextConsultPage: React.FC = () => {
>
>
{
!
isMe
&&
(
{
!
isMe
&&
(
<
Avatar
<
Avatar
icon=
{
isAI
?
<
RobotOutlined
/>
:
<
UserOutlined
/>
}
size=
{
36
}
icon=
{
isAI
?
<
RobotOutlined
/>
:
<
MedicineBoxOutlined
/>
}
src=
{
!
isAI
?
consult
?.
doctor_avatar
:
undefined
}
src=
{
!
isAI
?
consult
?.
doctor_avatar
:
undefined
}
style=
{
{
backgroundColor
:
isAI
?
'
#722ed1
'
:
'
#1890ff
'
,
marginRight
:
8
,
flexShrink
:
0
}
}
style=
{
{
backgroundColor
:
isAI
?
'
#722ed1
'
:
'
#52c41a
'
,
marginRight
:
10
,
flexShrink
:
0
,
}
}
/>
/>
)
}
)
}
<
div
style=
{
{
maxWidth
:
'
7
0%
'
}
}
>
<
div
style=
{
{
maxWidth
:
'
7
2%
'
,
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
alignItems
:
isMe
?
'
flex-end
'
:
'
flex-start
'
}
}
>
{
!
isMe
&&
(
<
div
style=
{
{
fontSize
:
11
,
color
:
'
#aaa
'
,
marginBottom
:
5
}
}
>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
12
,
marginBottom
:
4
,
display
:
'
block
'
}
}
>
{
isMe
?
'
我
'
:
isAI
?
'
AI助手
'
:
(
consult
?.
doctor_name
||
'
医生
'
)
}
{
isAI
?
'
AI助手
'
:
consult
?.
doctor_name
||
'
医生
'
}
{
'
·
'
}
</
Text
>
{
dayjs
(
msg
.
created_at
).
format
(
'
HH:mm
'
)
}
)
}
</
div
>
<
div
<
div
style=
{
{
style=
{
{
background
:
isMe
?
'
#1890ff
'
:
isAI
?
'
#f9f0ff
'
:
'
#f5f5f5
'
,
color
:
isMe
?
'
#fff
'
:
'
#000
'
,
padding
:
'
10px 14px
'
,
padding
:
'
10px 14px
'
,
borderRadius
:
isMe
?
'
12px 12px 4px 12px
'
:
'
12px 12px 12px 4px
'
,
borderRadius
:
isMe
?
'
12px 4px 12px 12px
'
:
'
4px 12px 12px 12px
'
,
background
:
isMe
?
'
#1890ff
'
:
isAI
?
'
#f9f0ff
'
:
'
#fff
'
,
color
:
isMe
?
'
#fff
'
:
'
#1d2129
'
,
boxShadow
:
'
0 1px 3px rgba(0,0,0,0.08)
'
,
whiteSpace
:
'
pre-wrap
'
,
wordBreak
:
'
break-word
'
,
fontSize
:
14
,
lineHeight
:
1.6
,
lineHeight
:
1.6
,
}
}
}
}
>
>
{
msg
.
content
}
{
msg
.
content_type
===
'
image
'
?
(
</
div
>
<
img
src=
{
msg
.
media_url
||
msg
.
content
}
alt=
"图片"
style=
{
{
maxWidth
:
200
,
borderRadius
:
8
}
}
/>
<
div
style=
{
{
textAlign
:
isMe
?
'
right
'
:
'
left
'
,
marginTop
:
4
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
justifyContent
:
isMe
?
'
flex-end
'
:
'
flex-start
'
,
gap
:
6
}
}
>
)
:
msg
.
content_type
===
'
prescription
'
?
(
<
Text
type=
"secondary"
style=
{
{
fontSize
:
11
}
}
>
<
div
style=
{
{
padding
:
'
4px 0
'
}
}
>
{
dayjs
(
msg
.
created_at
).
format
(
'
HH:mm
'
)
}
<
div
style=
{
{
fontSize
:
12
,
fontWeight
:
600
,
color
:
isMe
?
'
#fff
'
:
'
#d48806
'
,
marginBottom
:
4
}
}
>
</
Text
>
<
FileTextOutlined
style=
{
{
marginRight
:
4
}
}
/>
处方
{
isMe
&&
msg
.
read_at
&&
(
</
div
>
<
span
style=
{
{
fontSize
:
10
,
color
:
'
#52c41a
'
}
}
>
<
div
style=
{
{
fontSize
:
13
}
}
>
{
msg
.
content
}
</
div
>
<
CheckOutlined
/><
CheckOutlined
style=
{
{
marginLeft
:
-
4
}
}
/>
已读
</
div
>
</
span
>
)
:
(
msg
.
content
)
}
)
}
</
div
>
</
div
>
{
isMe
&&
msg
.
read_at
&&
(
<
div
style=
{
{
fontSize
:
10
,
color
:
'
#52c41a
'
,
marginTop
:
2
}
}
>
<
CheckOutlined
/><
CheckOutlined
style=
{
{
marginLeft
:
-
4
}
}
/>
已读
</
div
>
)
}
</
div
>
</
div
>
{
isMe
&&
(
{
isMe
&&
(
<
Avatar
<
Avatar
style=
{
{
marginLeft
:
8
,
flexShrink
:
0
,
backgroundColor
:
'
#52c41a
'
}
}
size=
{
36
}
style=
{
{
marginLeft
:
10
,
flexShrink
:
0
,
backgroundColor
:
'
#1890ff
'
}
}
src=
{
user
?.
avatar
}
src=
{
user
?.
avatar
}
icon=
{
<
UserOutlined
/>
}
icon=
{
<
UserOutlined
/>
}
/>
/>
...
@@ -153,80 +179,218 @@ const PatientTextConsultPage: React.FC = () => {
...
@@ -153,80 +179,218 @@ const PatientTextConsultPage: React.FC = () => {
};
};
return
(
return
(
<
div
style=
{
{
height
:
'
calc(100vh - 120px)
'
}
}
>
<
div
style=
{
{
height
:
'
calc(100vh - 72px)
'
,
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
padding
:
'
4px 24px 16px
'
}
}
>
<
Button
type=
"link"
size=
"small"
icon=
{
<
ArrowLeftOutlined
/>
}
{
/* 顶部标题栏 */
}
onClick=
{
()
=>
router
.
push
(
'
/patient/consult
'
)
}
className=
"p-0! mb-1"
>
返回列表
</
Button
>
<
div
style=
{
{
flexShrink
:
0
,
marginBottom
:
12
}
}
>
<
div
style=
{
{
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
12
}
}
>
<
Row
gutter=
{
8
}
style=
{
{
height
:
'
calc(100% - 32px)
'
}
}
>
<
Button
<
Col
span=
{
6
}
>
type=
"text"
<
Card
size=
"small"
style=
{
{
height
:
'
100%
'
}
}
styles=
{
{
body
:
{
padding
:
12
}
}
}
>
icon=
{
<
ArrowLeftOutlined
/>
}
<
div
className=
"text-center mb-2"
>
onClick=
{
()
=>
router
.
push
(
'
/patient/consult
'
)
}
<
Avatar
size=
{
48
}
src=
{
consult
?.
doctor_avatar
}
icon=
{
<
UserOutlined
/>
}
style=
{
{
backgroundColor
:
'
#1890ff
'
}
}
/>
style=
{
{
color
:
'
#8c8c8c
'
,
padding
:
'
4px 8px
'
}
}
<
div
className=
"text-sm font-bold mt-1"
>
{
consult
?.
doctor_name
||
'
医生
'
}
</
div
>
/>
<
Tag
color=
{
statusInfo
.
color
}
>
{
statusInfo
.
text
}
</
Tag
>
<
div
style=
{
{
flex
:
1
}
}
>
<
div
style=
{
{
fontSize
:
20
,
fontWeight
:
700
,
color
:
'
#1d2129
'
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
8
}
}
>
<
MessageOutlined
style=
{
{
color
:
'
#1890ff
'
}
}
/>
图文问诊
</
div
>
<
div
style=
{
{
fontSize
:
12
,
color
:
'
#8c8c8c
'
,
marginTop
:
2
}
}
>
{
consult
?.
doctor_name
?
`${consult.doctor_name} 医生`
:
'
在线问诊对话
'
}
</
div
>
</
div
>
<
div
style=
{
{
padding
:
'
4px 14px
'
,
borderRadius
:
8
,
background
:
statusInfo
.
bg
,
border
:
`1px solid ${statusInfo.color}30`
,
fontSize
:
13
,
fontWeight
:
500
,
color
:
statusInfo
.
color
,
}
}
>
{
statusInfo
.
text
}
</
div
>
</
div
>
</
div
>
{
/* 主体区域 */
}
<
div
style=
{
{
flex
:
1
,
display
:
'
flex
'
,
gap
:
12
,
overflow
:
'
hidden
'
,
minHeight
:
0
}
}
>
{
/* 左侧:医生信息面板 */
}
<
div
style=
{
{
width
:
240
,
flexShrink
:
0
,
borderRadius
:
12
,
border
:
'
1px solid #edf2fc
'
,
background
:
'
#fff
'
,
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
overflow
:
'
hidden
'
,
}
}
>
<
div
style=
{
{
padding
:
20
,
textAlign
:
'
center
'
}
}
>
<
Avatar
size=
{
56
}
src=
{
consult
?.
doctor_avatar
}
icon=
{
<
MedicineBoxOutlined
/>
}
style=
{
{
backgroundColor
:
'
#52c41a
'
}
}
/>
<
div
style=
{
{
fontSize
:
16
,
fontWeight
:
600
,
marginTop
:
10
,
color
:
'
#1d2129
'
}
}
>
{
consult
?.
doctor_name
||
'
医生
'
}
</
div
>
{
consult
?.
doctor_title
&&
(
<
Tag
color=
"blue"
style=
{
{
marginTop
:
6
,
fontSize
:
11
}
}
>
{
consult
.
doctor_title
}
</
Tag
>
)
}
</
div
>
<
Divider
style=
{
{
margin
:
0
}
}
/>
<
div
style=
{
{
padding
:
'
16px 20px
'
,
flex
:
1
}
}
>
<
div
style=
{
{
marginBottom
:
16
}
}
>
<
div
style=
{
{
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
6
,
fontSize
:
12
,
color
:
'
#8c8c8c
'
,
marginBottom
:
6
}
}
>
<
MedicineBoxOutlined
style=
{
{
color
:
'
#1890ff
'
}
}
/>
问诊类型
</
div
>
<
Tag
color=
"green"
style=
{
{
marginLeft
:
18
}
}
>
图文问诊
</
Tag
>
</
div
>
</
div
>
<
Divider
className=
"my-1!"
/>
<
div
className=
"text-xs space-y-2"
>
<
div
style=
{
{
marginBottom
:
16
}
}
>
<
div
>
<
div
style=
{
{
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
6
,
fontSize
:
12
,
color
:
'
#8c8c8c
'
,
marginBottom
:
6
}
}
>
<
MedicineBoxOutlined
className=
"text-blue-500 mr-1"
/><
Text
type=
"secondary"
>
类型
</
Text
>
<
ClockCircleOutlined
style=
{
{
color
:
'
#1890ff
'
}
}
/
>
<
div
className=
"ml-4 mt-0.5"
><
Tag
color=
"green"
>
图文
</
Tag
></
div
>
发起时间
</
div
>
</
div
>
<
div
>
<
div
style=
{
{
marginLeft
:
18
,
fontSize
:
13
,
color
:
'
#1d2129
'
}
}
>
<
ClockCircleOutlined
className=
"text-blue-500 mr-1"
/><
Text
type=
"secondary"
>
发起
</
Text
>
{
consult
?.
created_at
?
dayjs
(
consult
.
created_at
).
format
(
'
YYYY-MM-DD HH:mm
'
)
:
'
-
'
}
<
div
className=
"ml-4 mt-0.5"
>
{
consult
?.
created_at
?
dayjs
(
consult
.
created_at
).
format
(
'
MM-DD HH:mm
'
)
:
'
-
'
}
</
div
>
</
div
>
</
div
>
{
consult
?.
started_at
&&
(
<
div
>
<
PhoneOutlined
className=
"text-green-500 mr-1"
/><
Text
type=
"secondary"
>
接诊
</
Text
>
<
div
className=
"ml-4 mt-0.5"
>
{
dayjs
(
consult
.
started_at
).
format
(
'
MM-DD HH:mm
'
)
}
</
div
>
</
div
>
)
}
</
div
>
</
div
>
<
Divider
className=
"my-1!"
/>
<
div
className=
"text-[11px] text-gray-400 mb-1"
>
主诉
</
div
>
{
consult
?.
started_at
&&
(
<
div
className=
"text-xs bg-gray-50 rounded p-2"
>
{
consult
?.
chief_complaint
||
'
未填写
'
}
</
div
>
<
div
style=
{
{
marginBottom
:
16
}
}
>
</
Card
>
<
div
style=
{
{
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
6
,
fontSize
:
12
,
color
:
'
#8c8c8c
'
,
marginBottom
:
6
}
}
>
</
Col
>
<
PhoneOutlined
style=
{
{
color
:
'
#52c41a
'
}
}
/>
接诊时间
<
Col
span=
{
18
}
>
<
Card
size=
"small"
title=
{
<
span
className=
"text-xs"
>
<
Avatar
size=
{
20
}
src=
{
consult
?.
doctor_avatar
}
icon=
{
<
UserOutlined
/>
}
className=
"mr-1"
/>
{
consult
?.
doctor_name
||
'
医生
'
}
<
Tag
color=
{
statusInfo
.
color
}
className=
"ml-1"
>
{
statusInfo
.
text
}
</
Tag
>
</
span
>
}
style=
{
{
height
:
'
100%
'
,
display
:
'
flex
'
,
flexDirection
:
'
column
'
}
}
styles=
{
{
body
:
{
flex
:
1
,
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
padding
:
0
,
overflow
:
'
hidden
'
}
}
}
>
<
div
style=
{
{
flex
:
1
,
overflow
:
'
auto
'
,
padding
:
12
}
}
>
{
(
!
messagesData
?.
data
||
messagesData
.
data
.
length
===
0
)
?
(
<
div
className=
"text-center py-10 text-xs text-gray-400"
>
{
consult
?.
status
===
'
pending
'
||
consult
?.
status
===
'
waiting
'
?
'
等待医生接诊中..
'
:
'
暂无消息
'
}
</
div
>
</
div
>
)
:
(
<
div
style=
{
{
marginLeft
:
18
,
fontSize
:
13
,
color
:
'
#1d2129
'
}
}
>
<
List
dataSource=
{
messagesData
.
data
}
renderItem=
{
renderMessage
}
split=
{
false
}
/>
{
dayjs
(
consult
.
started_at
).
format
(
'
YYYY-MM-DD HH:mm
'
)
}
)
}
<
div
ref=
{
messagesEndRef
}
/>
</
div
>
<
div
className=
"border-t border-gray-100 p-2 bg-gray-50"
>
{
canSendMessage
?
(
<
Space
.
Compact
style=
{
{
width
:
'
100%
'
}
}
>
<
TextArea
value=
{
inputValue
}
onChange=
{
(
e
)
=>
setInputValue
(
e
.
target
.
value
)
}
placeholder=
"输入消息..."
autoSize=
{
{
minRows
:
1
,
maxRows
:
3
}
}
size=
"small"
onPressEnter=
{
(
e
)
=>
{
if
(
!
e
.
shiftKey
)
{
e
.
preventDefault
();
handleSend
();
}
}
}
style=
{
{
flex
:
1
}
}
/>
<
Button
type=
"primary"
size=
"small"
icon=
{
<
SendOutlined
/>
}
onClick=
{
handleSend
}
loading=
{
sendMutation
.
isPending
}
>
发送
</
Button
>
</
Space
.
Compact
>
)
:
(
<
div
className=
"text-center text-xs text-gray-400 py-1"
>
{
consult
?.
status
===
'
completed
'
?
'
问诊已结束
'
:
'
等待医生接诊后可发送消息
'
}
</
div
>
</
div
>
)
}
</
div
>
)
}
<
Divider
style=
{
{
margin
:
'
12px 0
'
}
}
/>
<
div
>
<
div
style=
{
{
fontSize
:
12
,
color
:
'
#8c8c8c
'
,
marginBottom
:
6
}
}
>
主诉
</
div
>
<
div
style=
{
{
fontSize
:
13
,
color
:
'
#1d2129
'
,
background
:
'
#f5f7fb
'
,
borderRadius
:
8
,
padding
:
'
10px 12px
'
,
lineHeight
:
1.6
,
}
}
>
{
consult
?.
chief_complaint
||
'
未填写
'
}
</
div
>
</
div
>
</
div
>
</
div
>
{
/* 右侧:对话区域 */
}
<
div
style=
{
{
flex
:
1
,
minWidth
:
0
,
borderRadius
:
12
,
border
:
'
1px solid #edf2fc
'
,
background
:
'
#fff
'
,
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
overflow
:
'
hidden
'
,
}
}
>
{
/* 对话头部 */
}
<
div
style=
{
{
padding
:
'
10px 16px
'
,
borderBottom
:
'
1px solid #f0f0f0
'
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
10
,
flexShrink
:
0
,
}
}
>
<
Avatar
size=
{
32
}
src=
{
consult
?.
doctor_avatar
}
icon=
{
<
MedicineBoxOutlined
/>
}
style=
{
{
backgroundColor
:
'
#52c41a
'
}
}
/>
<
div
style=
{
{
flex
:
1
}
}
>
<
Text
strong
style=
{
{
fontSize
:
14
}
}
>
{
consult
?.
doctor_name
||
'
医生
'
}
</
Text
>
<
Tag
color=
{
statusInfo
.
color
===
'
#52c41a
'
?
'
success
'
:
statusInfo
.
color
===
'
#fa8c16
'
?
'
warning
'
:
'
default
'
}
style=
{
{
fontSize
:
11
,
marginLeft
:
8
}
}
>
{
statusInfo
.
text
}
</
Tag
>
</
div
>
</
div
>
</
Card
>
</
div
>
</
Col
>
</
Row
>
{
/* 消息区域 */
}
<
div
style=
{
{
flex
:
1
,
overflow
:
'
auto
'
,
padding
:
16
,
background
:
'
#f5f7fb
'
}
}
>
{
(
!
messagesData
?.
data
||
messagesData
.
data
.
length
===
0
)
?
(
<
Empty
description=
{
<
span
style=
{
{
color
:
'
#8c8c8c
'
,
fontSize
:
13
}
}
>
{
consult
?.
status
===
'
pending
'
||
consult
?.
status
===
'
waiting
'
?
'
等待医生接诊中...
'
:
'
暂无消息
'
}
</
span
>
}
style=
{
{
marginTop
:
80
}
}
/>
)
:
(
messagesData
.
data
.
map
(
renderMessage
)
)
}
<
div
ref=
{
messagesEndRef
}
/>
</
div
>
{
/* 输入区域 */
}
<
div
style=
{
{
borderTop
:
'
1px solid #f0f0f0
'
,
background
:
'
#fff
'
,
flexShrink
:
0
}
}
>
{
canSendMessage
?
(
<>
{
/* 工具栏 */
}
<
div
style=
{
{
padding
:
'
6px 12px 0
'
,
display
:
'
flex
'
,
gap
:
2
,
borderBottom
:
'
1px solid #f5f5f5
'
}
}
>
<
Tooltip
title=
"发送图片"
>
<
Button
type=
"text"
size=
"small"
icon=
{
<
PictureOutlined
/>
}
style=
{
{
color
:
'
#1890ff
'
,
fontSize
:
13
}
}
/>
</
Tooltip
>
<
Tooltip
title=
"表情"
>
<
Button
type=
"text"
size=
"small"
icon=
{
<
SmileOutlined
/>
}
style=
{
{
color
:
'
#8c8c8c
'
,
fontSize
:
13
}
}
/>
</
Tooltip
>
</
div
>
{
/* 输入框 */
}
<
div
style=
{
{
padding
:
'
8px 12px 10px
'
,
display
:
'
flex
'
,
gap
:
8
,
alignItems
:
'
flex-end
'
}
}
>
<
TextArea
value=
{
inputValue
}
onChange=
{
(
e
)
=>
setInputValue
(
e
.
target
.
value
)
}
placeholder=
"输入消息,Enter 发送,Shift+Enter 换行..."
autoSize=
{
{
minRows
:
2
,
maxRows
:
5
}
}
onPressEnter=
{
(
e
)
=>
{
if
(
!
e
.
shiftKey
)
{
e
.
preventDefault
();
handleSend
();
}
}
}
style=
{
{
flex
:
1
,
borderRadius
:
8
,
resize
:
'
none
'
}
}
/>
<
Button
type=
"primary"
icon=
{
<
SendOutlined
/>
}
onClick=
{
handleSend
}
loading=
{
sendMutation
.
isPending
}
style=
{
{
borderRadius
:
8
,
height
:
'
auto
'
,
paddingTop
:
8
,
paddingBottom
:
8
}
}
>
发送
</
Button
>
</
div
>
</>
)
:
(
<
div
style=
{
{
padding
:
'
14px 16px
'
,
textAlign
:
'
center
'
,
color
:
'
#8c8c8c
'
,
fontSize
:
13
}
}
>
{
consult
?.
status
===
'
completed
'
?
'
本次问诊已结束
'
:
'
等待医生接诊后可发送消息
'
}
</
div
>
)
}
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
);
);
};
};
export
default
PatientTextConsultPage
;
export
default
PatientTextConsultPage
;
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