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
f5410548
Commit
f5410548
authored
Mar 02, 2026
by
yuguo
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix
parent
626473e4
Changes
9
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
285 additions
and
98 deletions
+285
-98
server/internal/service/chronic/service.go
server/internal/service/chronic/service.go
+24
-11
server/internal/service/doctorportal/handler.go
server/internal/service/doctorportal/handler.go
+34
-0
server/internal/service/doctorportal/prescription_service.go
server/internal/service/doctorportal/prescription_service.go
+34
-0
server/internal/service/preconsult/chat_handler.go
server/internal/service/preconsult/chat_handler.go
+9
-0
server/pkg/workflow/engine.go
server/pkg/workflow/engine.go
+15
-0
web/src/api/consult.ts
web/src/api/consult.ts
+8
-2
web/src/api/doctorPortal.ts
web/src/api/doctorPortal.ts
+11
-0
web/src/components/GlobalAIFloat/ChatPanel.tsx
web/src/components/GlobalAIFloat/ChatPanel.tsx
+47
-36
web/src/pages/doctor/Consult/AIPanel.tsx
web/src/pages/doctor/Consult/AIPanel.tsx
+103
-49
No files found.
server/internal/service/chronic/service.go
View file @
f5410548
...
@@ -9,8 +9,8 @@ import (
...
@@ -9,8 +9,8 @@ import (
"github.com/google/uuid"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm"
internalagent
"internet-hospital/internal/agent"
"internet-hospital/internal/model"
"internet-hospital/internal/model"
"internet-hospital/pkg/ai"
"internet-hospital/pkg/database"
"internet-hospital/pkg/database"
)
)
...
@@ -103,17 +103,30 @@ func (s *Service) GetAIRenewalAdvice(ctx context.Context, userID, renewalID stri
...
@@ -103,17 +103,30 @@ func (s *Service) GetAIRenewalAdvice(ctx context.Context, userID, renewalID stri
if
err
:=
s
.
db
.
WithContext
(
ctx
)
.
Where
(
"id = ? AND user_id = ?"
,
renewalID
,
userID
)
.
First
(
&
r
)
.
Error
;
err
!=
nil
{
if
err
:=
s
.
db
.
WithContext
(
ctx
)
.
Where
(
"id = ? AND user_id = ?"
,
renewalID
,
userID
)
.
First
(
&
r
)
.
Error
;
err
!=
nil
{
return
""
,
err
return
""
,
err
}
}
prompt
:=
fmt
.
Sprintf
(
"患者患有%s,申请续方药品:%s,续方原因:%s。请从医学角度给出续方建议,包括用药注意事项、可能的药物相互作用和生活方式建议。"
,
r
.
DiseaseName
,
r
.
Medicines
,
r
.
Reason
)
result
:=
ai
.
Call
(
ctx
,
ai
.
CallParams
{
// 解析药品列表以获取疗程长度(取续方申请里的 Medicines JSON)
Scene
:
"renewal_advice"
,
var
medicines
[]
string
UserID
:
userID
,
json
.
Unmarshal
([]
byte
(
r
.
Medicines
),
&
medicines
)
Messages
:
[]
ai
.
ChatMessage
{{
Role
:
"user"
,
Content
:
prompt
}},
durationMonths
:=
1
RequestSummary
:
r
.
DiseaseName
,
})
agentCtx
:=
map
[
string
]
interface
{}{
if
result
.
Error
!=
nil
{
"patient_id"
:
userID
,
return
""
,
result
.
Error
"diagnosis"
:
r
.
DiseaseName
,
"current_drugs"
:
medicines
,
"duration_months"
:
durationMonths
,
}
msg
:=
fmt
.
Sprintf
(
"患者%s申请续药%d个月,当前用药:%s,原因:%s。请评估续药合理性并给出专业建议。"
,
r
.
DiseaseName
,
durationMonths
,
r
.
Medicines
,
r
.
Reason
)
output
,
err
:=
internalagent
.
GetService
()
.
Chat
(
ctx
,
"follow_up_agent"
,
userID
,
""
,
msg
,
agentCtx
)
if
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"AI续药建议获取失败: %w"
,
err
)
}
if
output
==
nil
{
return
""
,
fmt
.
Errorf
(
"follow_up_agent 未初始化"
)
}
}
advice
:=
result
.
Content
advice
:=
output
.
Response
s
.
db
.
WithContext
(
ctx
)
.
Model
(
&
r
)
.
Update
(
"ai_advice"
,
advice
)
s
.
db
.
WithContext
(
ctx
)
.
Model
(
&
r
)
.
Update
(
"ai_advice"
,
advice
)
return
advice
,
nil
return
advice
,
nil
}
}
...
...
server/internal/service/doctorportal/handler.go
View file @
f5410548
...
@@ -7,6 +7,7 @@ import (
...
@@ -7,6 +7,7 @@ import (
"internet-hospital/internal/model"
"internet-hospital/internal/model"
"internet-hospital/pkg/response"
"internet-hospital/pkg/response"
"internet-hospital/pkg/workflow"
)
)
// Handler 医生端API处理器
// Handler 医生端API处理器
...
@@ -59,6 +60,7 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) {
...
@@ -59,6 +60,7 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) {
// 处方管理
// 处方管理
dp
.
POST
(
"/prescription/create"
,
h
.
CreatePrescription
)
dp
.
POST
(
"/prescription/create"
,
h
.
CreatePrescription
)
dp
.
POST
(
"/prescription/check"
,
h
.
CheckPrescriptionSafety
)
dp
.
GET
(
"/prescriptions"
,
h
.
GetDoctorPrescriptions
)
dp
.
GET
(
"/prescriptions"
,
h
.
GetDoctorPrescriptions
)
dp
.
GET
(
"/prescription/:id"
,
h
.
GetPrescriptionDetail
)
dp
.
GET
(
"/prescription/:id"
,
h
.
GetPrescriptionDetail
)
}
}
...
@@ -189,10 +191,19 @@ func (h *Handler) EndConsult(c *gin.Context) {
...
@@ -189,10 +191,19 @@ func (h *Handler) EndConsult(c *gin.Context) {
Summary
string
`json:"summary"`
Summary
string
`json:"summary"`
}
}
_
=
c
.
ShouldBindJSON
(
&
req
)
_
=
c
.
ShouldBindJSON
(
&
req
)
userID
,
_
:=
c
.
Get
(
"user_id"
)
if
err
:=
h
.
service
.
EndConsult
(
c
.
Request
.
Context
(),
id
,
req
.
Summary
);
err
!=
nil
{
if
err
:=
h
.
service
.
EndConsult
(
c
.
Request
.
Context
(),
id
,
req
.
Summary
);
err
!=
nil
{
response
.
Error
(
c
,
500
,
"结束问诊失败"
)
response
.
Error
(
c
,
500
,
"结束问诊失败"
)
return
return
}
}
// 异步触发 follow_up 工作流(失败不影响主流程)
go
workflow
.
GetEngine
()
.
TriggerByCategory
(
c
.
Request
.
Context
(),
"follow_up"
,
map
[
string
]
interface
{}{
"consult_id"
:
id
,
"doctor_id"
:
fmt
.
Sprintf
(
"%v"
,
userID
),
})
response
.
Success
(
c
,
nil
)
response
.
Success
(
c
,
nil
)
}
}
...
@@ -389,3 +400,26 @@ func (h *Handler) GetPrescriptionDetail(c *gin.Context) {
...
@@ -389,3 +400,26 @@ func (h *Handler) GetPrescriptionDetail(c *gin.Context) {
}
}
response
.
Success
(
c
,
result
)
response
.
Success
(
c
,
result
)
}
}
// CheckPrescriptionSafety AI处方安全审核
func
(
h
*
Handler
)
CheckPrescriptionSafety
(
c
*
gin
.
Context
)
{
userID
,
exists
:=
c
.
Get
(
"user_id"
)
if
!
exists
{
response
.
Error
(
c
,
401
,
"未登录"
)
return
}
var
req
struct
{
PatientID
string
`json:"patient_id" binding:"required"`
Drugs
[]
string
`json:"drugs" binding:"required,min=1"`
}
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"请求参数错误: "
+
err
.
Error
())
return
}
result
,
err
:=
h
.
service
.
CheckPrescriptionSafety
(
c
.
Request
.
Context
(),
userID
.
(
string
),
req
.
PatientID
,
req
.
Drugs
)
if
err
!=
nil
{
response
.
Error
(
c
,
500
,
err
.
Error
())
return
}
response
.
Success
(
c
,
result
)
}
server/internal/service/doctorportal/prescription_service.go
View file @
f5410548
...
@@ -3,10 +3,12 @@ package doctorportal
...
@@ -3,10 +3,12 @@ package doctorportal
import
(
import
(
"context"
"context"
"fmt"
"fmt"
"strings"
"time"
"time"
"github.com/google/uuid"
"github.com/google/uuid"
internalagent
"internet-hospital/internal/agent"
"internet-hospital/internal/model"
"internet-hospital/internal/model"
)
)
...
@@ -187,3 +189,35 @@ func (s *Service) GetPrescriptionByID(ctx context.Context, id string) (*model.Pr
...
@@ -187,3 +189,35 @@ func (s *Service) GetPrescriptionByID(ctx context.Context, id string) (*model.Pr
}
}
return
&
prescription
,
nil
return
&
prescription
,
nil
}
}
// CheckPrescriptionSafety 通过 prescription_agent 审核处方安全性
func
(
s
*
Service
)
CheckPrescriptionSafety
(
ctx
context
.
Context
,
userID
,
patientID
string
,
drugs
[]
string
)
(
map
[
string
]
interface
{},
error
)
{
drugList
:=
strings
.
Join
(
drugs
,
"、"
)
agentCtx
:=
map
[
string
]
interface
{}{
"patient_id"
:
patientID
,
"drugs"
:
drugs
,
}
message
:=
fmt
.
Sprintf
(
"请审核以下处方的安全性:%s,检查药物相互作用和禁忌症"
,
drugList
)
agentSvc
:=
internalagent
.
GetService
()
output
,
err
:=
agentSvc
.
Chat
(
ctx
,
"prescription_agent"
,
userID
,
""
,
message
,
agentCtx
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"处方安全审核失败: %w"
,
err
)
}
if
output
==
nil
{
return
nil
,
fmt
.
Errorf
(
"prescription_agent 未初始化"
)
}
// 判断是否有警告(简单关键词检测)
resp
:=
output
.
Response
hasWarning
:=
strings
.
Contains
(
resp
,
"相互作用"
)
||
strings
.
Contains
(
resp
,
"注意"
)
||
strings
.
Contains
(
resp
,
"警告"
)
||
strings
.
Contains
(
resp
,
"慎用"
)
hasContraindication
:=
strings
.
Contains
(
resp
,
"禁忌"
)
||
strings
.
Contains
(
resp
,
"禁止"
)
||
strings
.
Contains
(
resp
,
"不宜"
)
return
map
[
string
]
interface
{}{
"report"
:
resp
,
"tool_calls"
:
output
.
ToolCalls
,
"iterations"
:
output
.
Iterations
,
"has_warning"
:
hasWarning
,
"has_contraindication"
:
hasContraindication
,
},
nil
}
server/internal/service/preconsult/chat_handler.go
View file @
f5410548
...
@@ -11,6 +11,7 @@ import (
...
@@ -11,6 +11,7 @@ import (
"internet-hospital/internal/model"
"internet-hospital/internal/model"
"internet-hospital/pkg/ai"
"internet-hospital/pkg/ai"
"internet-hospital/pkg/middleware"
"internet-hospital/pkg/middleware"
"internet-hospital/pkg/workflow"
)
)
// ChatMessage 对话消息(统一格式)
// ChatMessage 对话消息(统一格式)
...
@@ -265,6 +266,14 @@ func (h *Handler) FinishChat(c *gin.Context) {
...
@@ -265,6 +266,14 @@ func (h *Handler) FinishChat(c *gin.Context) {
h
.
service
.
db
.
Save
(
&
preConsult
)
h
.
service
.
db
.
Save
(
&
preConsult
)
// 异步触发 pre_consult 工作流(失败不影响主流程)
go
workflow
.
GetEngine
()
.
TriggerByCategory
(
c
.
Request
.
Context
(),
"pre_consult"
,
map
[
string
]
interface
{}{
"pre_consult_id"
:
preConsult
.
ID
,
"patient_id"
:
preConsult
.
PatientID
,
"department"
:
preConsult
.
AIDepartment
,
"severity"
:
preConsult
.
AISeverity
,
})
// 查找推荐医生
// 查找推荐医生
doctors
:=
h
.
service
.
findRecommendedDoctors
(
preConsult
.
AIDepartment
)
doctors
:=
h
.
service
.
findRecommendedDoctors
(
preConsult
.
AIDepartment
)
doctorsJSON
,
_
:=
json
.
Marshal
(
doctors
)
doctorsJSON
,
_
:=
json
.
Marshal
(
doctors
)
...
...
server/pkg/workflow/engine.go
View file @
f5410548
...
@@ -472,6 +472,21 @@ func (e *Engine) executeTemplate(_ context.Context, node *Node, execCtx *Executi
...
@@ -472,6 +472,21 @@ func (e *Engine) executeTemplate(_ context.Context, node *Node, execCtx *Executi
},
nil
},
nil
}
}
// TriggerByCategory 查找指定 category 的第一个 active 工作流并异步执行
// 失败不影响主流程(纯异步触发)
func
(
e
*
Engine
)
TriggerByCategory
(
ctx
context
.
Context
,
category
string
,
input
map
[
string
]
interface
{})
{
db
:=
database
.
GetDB
()
var
wfDef
model
.
WorkflowDefinition
if
err
:=
db
.
Where
(
"category = ? AND status = 'active'"
,
category
)
.
First
(
&
wfDef
)
.
Error
;
err
!=
nil
{
// 没有对应分类的活跃工作流,静默跳过
return
}
go
func
()
{
defer
func
()
{
recover
()
}()
e
.
Execute
(
ctx
,
wfDef
.
WorkflowID
,
input
,
"system"
)
}()
}
func
replaceAll
(
s
,
old
,
new
string
)
string
{
func
replaceAll
(
s
,
old
,
new
string
)
string
{
if
old
==
""
{
if
old
==
""
{
return
s
return
s
...
...
web/src/api/consult.ts
View file @
f5410548
...
@@ -121,9 +121,15 @@ export const consultApi = {
...
@@ -121,9 +121,15 @@ export const consultApi = {
// 结束问诊
// 结束问诊
endConsult
:
(
id
:
string
)
=>
post
<
null
>
(
`/consult/
${
id
}
/end`
),
endConsult
:
(
id
:
string
)
=>
post
<
null
>
(
`/consult/
${
id
}
/end`
),
// AI辅助分析(鉴别诊断/用药建议)
// AI辅助分析(鉴别诊断/用药建议)
—— 通过 Agent,返回 response + tool_calls
aiAssist
:
(
id
:
string
,
scene
:
string
)
=>
aiAssist
:
(
id
:
string
,
scene
:
string
)
=>
post
<
{
scene
:
string
;
content
:
string
}
>
(
`/consult/
${
id
}
/ai-assist`
,
{
scene
}),
post
<
{
scene
:
string
;
response
:
string
;
tool_calls
?:
import
(
'
./agent
'
).
ToolCall
[];
iterations
?:
number
;
total_tokens
?:
number
;
}
>
(
`/consult/
${
id
}
/ai-assist`
,
{
scene
}),
// 取消问诊
// 取消问诊
cancelConsult
:
(
id
:
string
,
reason
?:
string
)
=>
cancelConsult
:
(
id
:
string
,
reason
?:
string
)
=>
...
...
web/src/api/doctorPortal.ts
View file @
f5410548
import
{
get
,
post
,
put
}
from
'
./request
'
;
import
{
get
,
post
,
put
}
from
'
./request
'
;
import
type
{
Consultation
,
ConsultMessage
}
from
'
./consult
'
;
import
type
{
Consultation
,
ConsultMessage
}
from
'
./consult
'
;
import
type
{
ToolCall
}
from
'
./agent
'
;
// ==================== 医生端 API ====================
// ==================== 医生端 API ====================
...
@@ -181,4 +182,14 @@ export const doctorPortalApi = {
...
@@ -181,4 +182,14 @@ export const doctorPortalApi = {
getPatientDetail
:
(
patientId
:
string
)
=>
getPatientDetail
:
(
patientId
:
string
)
=>
get
<
PatientDetail
>
(
`/doctor-portal/patient/
${
patientId
}
/detail`
),
get
<
PatientDetail
>
(
`/doctor-portal/patient/
${
patientId
}
/detail`
),
// === 处方安全审核 ===
checkPrescriptionSafety
:
(
params
:
{
patient_id
:
string
;
drugs
:
string
[]
})
=>
post
<
{
report
:
string
;
tool_calls
?:
ToolCall
[];
iterations
?:
number
;
has_warning
:
boolean
;
has_contraindication
:
boolean
;
}
>
(
'
/doctor-portal/prescription/check
'
,
params
),
};
};
web/src/components/GlobalAIFloat/ChatPanel.tsx
View file @
f5410548
...
@@ -15,14 +15,12 @@ import {
...
@@ -15,14 +15,12 @@ import {
ThunderboltOutlined
,
ThunderboltOutlined
,
SwapOutlined
,
SwapOutlined
,
}
from
'
@ant-design/icons
'
;
}
from
'
@ant-design/icons
'
;
import
{
useUserStore
}
from
'
../../store/userStore
'
;
import
{
agentApi
}
from
'
../../api/agent
'
;
import
type
{
ChatMessage
,
ToolCall
,
AgentOption
}
from
'
./types
'
;
import
type
{
ChatMessage
,
ToolCall
,
AgentOption
}
from
'
./types
'
;
import
{
PATIENT_AGENTS
,
DOCTOR_AGENTS
}
from
'
./types
'
;
import
{
PATIENT_AGENTS
,
DOCTOR_AGENTS
}
from
'
./types
'
;
const
{
TextArea
}
=
Input
;
const
{
TextArea
}
=
Input
;
const
API
=
process
.
env
.
NEXT_PUBLIC_API_URL
||
''
;
interface
ChatPanelProps
{
interface
ChatPanelProps
{
role
:
'
patient
'
|
'
doctor
'
;
role
:
'
patient
'
|
'
doctor
'
;
patientContext
?:
{
patientContext
?:
{
...
@@ -33,11 +31,10 @@ interface ChatPanelProps {
...
@@ -33,11 +31,10 @@ interface ChatPanelProps {
}
}
const
ChatPanel
:
React
.
FC
<
ChatPanelProps
>
=
({
role
,
patientContext
})
=>
{
const
ChatPanel
:
React
.
FC
<
ChatPanelProps
>
=
({
role
,
patientContext
})
=>
{
const
{
accessToken
:
token
}
=
useUserStore
();
const
[
messages
,
setMessages
]
=
useState
<
ChatMessage
[]
>
([]);
const
[
messages
,
setMessages
]
=
useState
<
ChatMessage
[]
>
([]);
const
[
inputValue
,
setInputValue
]
=
useState
(
''
);
const
[
inputValue
,
setInputValue
]
=
useState
(
''
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
sessionId
]
=
useState
(()
=>
crypto
.
randomUUID
()
);
const
[
sessionId
,
setSessionId
]
=
useState
(
''
);
const
[
selectedAgent
,
setSelectedAgent
]
=
useState
<
string
>
(
const
[
selectedAgent
,
setSelectedAgent
]
=
useState
<
string
>
(
role
===
'
patient
'
?
'
pre_consult_agent
'
:
'
diagnosis_agent
'
role
===
'
patient
'
?
'
pre_consult_agent
'
:
'
diagnosis_agent
'
);
);
...
@@ -45,8 +42,31 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ role, patientContext }) => {
...
@@ -45,8 +42,31 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ role, patientContext }) => {
const
agents
:
AgentOption
[]
=
role
===
'
patient
'
?
PATIENT_AGENTS
:
DOCTOR_AGENTS
;
const
agents
:
AgentOption
[]
=
role
===
'
patient
'
?
PATIENT_AGENTS
:
DOCTOR_AGENTS
;
// mount时恢复最近会话
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
messages
.
length
===
0
)
{
const
restoreSession
=
async
()
=>
{
try
{
const
res
=
await
agentApi
.
getSessions
(
selectedAgent
);
const
sessions
=
res
.
data
;
if
(
sessions
&&
sessions
.
length
>
0
)
{
const
latest
=
sessions
[
0
];
setSessionId
(
latest
.
session_id
);
// 恢复历史消息
try
{
const
history
=
JSON
.
parse
(
latest
.
history
||
'
[]
'
)
as
{
role
:
string
;
content
:
string
}[];
if
(
history
.
length
>
0
)
{
const
restored
:
ChatMessage
[]
=
history
.
map
(
h
=>
({
role
:
h
.
role
as
'
user
'
|
'
assistant
'
,
content
:
h
.
content
,
timestamp
:
new
Date
(),
}));
setMessages
(
restored
);
return
;
}
}
catch
{
/* 历史解析失败,使用默认欢迎消息 */
}
}
}
catch
{
/* 会话恢复失败,使用默认欢迎消息 */
}
// 没有历史会话,展示欢迎消息
const
agent
=
agents
.
find
(
a
=>
a
.
id
===
selectedAgent
);
const
agent
=
agents
.
find
(
a
=>
a
.
id
===
selectedAgent
);
setMessages
([{
setMessages
([{
role
:
'
system
'
,
role
:
'
system
'
,
...
@@ -55,7 +75,8 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ role, patientContext }) => {
...
@@ -55,7 +75,8 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ role, patientContext }) => {
: `
您好,我是
$
{
agent
?.
name
||
'
AI诊断助手
'
}
。请描述患者症状或输入您的问题。
`,
: `
您好,我是
$
{
agent
?.
name
||
'
AI诊断助手
'
}
。请描述患者症状或输入您的问题。
`,
timestamp: new Date(),
timestamp: new Date(),
}]);
}]);
}
};
restoreSession();
}, []);
}, []);
useEffect(() => {
useEffect(() => {
...
@@ -76,18 +97,14 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ role, patientContext }) => {
...
@@ -76,18 +97,14 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ role, patientContext }) => {
};
};
if (patientContext) requestBody.context = patientContext;
if (patientContext) requestBody.context = patientContext;
const res = await fetch(`
$
{
API
}
/api/
v1
/
agent
/
$
{
selectedAgent
}
/chat`,
{
const res = await agentApi.chat(selectedAgent, requestBody as Parameters<typeof agentApi.chat>[1]);
method
:
'
POST
'
,
const agentData = res.data;
headers
:
{
'
Content-Type
'
:
'
application/json
'
,
// 如果服务端分配了新 session_id 就更新
Authorization
:
`Bearer
${
token
}
`
,
if (agentData?.session_id && !sessionId) {
},
setSessionId(agentData.session_id);
body
:
JSON
.
stringify
(
requestBody
),
}
}
);
if (res.ok) {
const data = await res.json();
const agentData = data.data;
setMessages(prev => [...prev, {
setMessages(prev => [...prev, {
role: 'assistant',
role: 'assistant',
content: agentData?.response || '暂时无法回答,请稍后重试。',
content: agentData?.response || '暂时无法回答,请稍后重试。',
...
@@ -99,17 +116,10 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ role, patientContext }) => {
...
@@ -99,17 +116,10 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ role, patientContext }) => {
},
},
timestamp: new Date(),
timestamp: new Date(),
}]);
}]);
} else {
setMessages(prev => [...prev, {
role: 'assistant',
content: '请求失败,请稍后重试。',
timestamp: new Date(),
}]);
}
} catch {
} catch {
setMessages(prev => [...prev, {
setMessages(prev => [...prev, {
role: 'assistant',
role: 'assistant',
content: '
网络错误,请检查网络连接
。',
content: '
请求失败,请稍后重试
。',
timestamp: new Date(),
timestamp: new Date(),
}]);
}]);
} finally {
} finally {
...
@@ -119,6 +129,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ role, patientContext }) => {
...
@@ -119,6 +129,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ role, patientContext }) => {
const handleAgentChange = (agentId: string) => {
const handleAgentChange = (agentId: string) => {
setSelectedAgent(agentId);
setSelectedAgent(agentId);
setSessionId(''); // 切换 Agent 时重置会话,由服务端创建新会话
const agent = agents.find(a => a.id === agentId);
const agent = agents.find(a => a.id === agentId);
setMessages([{
setMessages([{
role: 'system',
role: 'system',
...
...
web/src/pages/doctor/Consult/AIPanel.tsx
View file @
f5410548
import
React
,
{
useState
}
from
'
react
'
;
import
React
,
{
useState
}
from
'
react
'
;
import
{
import
{
Card
,
Tabs
,
Typography
,
Space
,
Empty
,
Tag
,
Divider
,
Alert
,
Spin
,
Badge
,
Button
,
Card
,
Tabs
,
Typography
,
Space
,
Empty
,
Tag
,
Divider
,
Alert
,
Spin
,
Badge
,
Button
,
Collapse
,
Timeline
,
}
from
'
antd
'
;
}
from
'
antd
'
;
import
{
import
{
RobotOutlined
,
FileTextOutlined
,
UserOutlined
,
RobotOutlined
,
FileTextOutlined
,
UserOutlined
,
MessageOutlined
,
MedicineBoxOutlined
,
ThunderboltOutlined
,
MessageOutlined
,
MedicineBoxOutlined
,
ThunderboltOutlined
,
ToolOutlined
,
CheckCircleOutlined
,
CloseCircleOutlined
,
}
from
'
@ant-design/icons
'
;
}
from
'
@ant-design/icons
'
;
import
type
{
PreConsultResponse
,
ChatMessage
}
from
'
../../../api/preConsult
'
;
import
type
{
PreConsultResponse
,
ChatMessage
}
from
'
../../../api/preConsult
'
;
import
type
{
ToolCall
}
from
'
../../../api/agent
'
;
import
{
consultApi
}
from
'
../../../api/consult
'
;
import
{
consultApi
}
from
'
../../../api/consult
'
;
import
MarkdownRenderer
from
'
../../../components/MarkdownRenderer
'
;
import
MarkdownRenderer
from
'
../../../components/MarkdownRenderer
'
;
...
@@ -27,29 +30,77 @@ const AIPanel: React.FC<AIPanelProps> = ({
...
@@ -27,29 +30,77 @@ const AIPanel: React.FC<AIPanelProps> = ({
})
=>
{
})
=>
{
const
[
diagnosisContent
,
setDiagnosisContent
]
=
useState
(
''
);
const
[
diagnosisContent
,
setDiagnosisContent
]
=
useState
(
''
);
const
[
diagnosisLoading
,
setDiagnosisLoading
]
=
useState
(
false
);
const
[
diagnosisLoading
,
setDiagnosisLoading
]
=
useState
(
false
);
const
[
diagnosisToolCalls
,
setDiagnosisToolCalls
]
=
useState
<
ToolCall
[]
>
([]);
const
[
medicationContent
,
setMedicationContent
]
=
useState
(
''
);
const
[
medicationContent
,
setMedicationContent
]
=
useState
(
''
);
const
[
medicationLoading
,
setMedicationLoading
]
=
useState
(
false
);
const
[
medicationLoading
,
setMedicationLoading
]
=
useState
(
false
);
const
[
medicationToolCalls
,
setMedicationToolCalls
]
=
useState
<
ToolCall
[]
>
([]);
const
[
preConsultSubTab
,
setPreConsultSubTab
]
=
useState
(
'
chat
'
);
const
[
preConsultSubTab
,
setPreConsultSubTab
]
=
useState
(
'
chat
'
);
const
handleAIAssist
=
async
(
scene
:
'
consult_diagnosis
'
|
'
consult_medication
'
)
=>
{
const
handleAIAssist
=
async
(
scene
:
'
consult_diagnosis
'
|
'
consult_medication
'
)
=>
{
if
(
!
activeConsultId
)
return
;
if
(
!
activeConsultId
)
return
;
const
setLoading
=
scene
===
'
consult_diagnosis
'
?
setDiagnosisLoading
:
setMedicationLoading
;
const
setLoading
=
scene
===
'
consult_diagnosis
'
?
setDiagnosisLoading
:
setMedicationLoading
;
const
setContent
=
scene
===
'
consult_diagnosis
'
?
setDiagnosisContent
:
setMedicationContent
;
const
setContent
=
scene
===
'
consult_diagnosis
'
?
setDiagnosisContent
:
setMedicationContent
;
const
setToolCalls
=
scene
===
'
consult_diagnosis
'
?
setDiagnosisToolCalls
:
setMedicationToolCalls
;
setLoading
(
true
);
setLoading
(
true
);
try
{
try
{
const
res
=
await
consultApi
.
aiAssist
(
activeConsultId
,
scene
);
const
res
=
await
consultApi
.
aiAssist
(
activeConsultId
,
scene
);
setContent
(
res
.
data
?.
content
||
'
暂无分析结果
'
);
setContent
(
res
.
data
?.
response
||
'
暂无分析结果
'
);
setToolCalls
(
res
.
data
?.
tool_calls
||
[]);
}
catch
(
err
:
any
)
{
}
catch
(
err
:
any
)
{
setContent
(
'
AI分析失败:
'
+
(
err
?.
message
||
'
请稍后重试
'
));
setContent
(
'
AI分析失败:
'
+
(
err
?.
message
||
'
请稍后重试
'
));
setToolCalls
([]);
}
finally
{
}
finally
{
setLoading
(
false
);
setLoading
(
false
);
}
}
};
};
const
renderToolCalls
=
(
toolCalls
:
ToolCall
[])
=>
{
if
(
!
toolCalls
||
toolCalls
.
length
===
0
)
return
null
;
return
(
<
Collapse
size=
"small"
style=
{
{
marginTop
:
8
,
background
:
'
#f9fafb
'
,
borderRadius
:
8
}
}
items=
{
[{
key
:
'
tools
'
,
label
:
(
<
span
style=
{
{
fontSize
:
12
,
color
:
'
#6b7280
'
}
}
>
<
ToolOutlined
style=
{
{
marginRight
:
4
}
}
/>
调用了
{
toolCalls
.
length
}
个工具
</
span
>
),
children
:
(
<
Timeline
style=
{
{
marginTop
:
8
,
marginBottom
:
0
}
}
items=
{
toolCalls
.
map
((
tc
,
idx
)
=>
({
color
:
tc
.
success
?
'
green
'
:
'
red
'
,
dot
:
tc
.
success
?
<
CheckCircleOutlined
style=
{
{
fontSize
:
12
}
}
/>
:
<
CloseCircleOutlined
style=
{
{
fontSize
:
12
}
}
/>,
children
:
(
<
div
key=
{
idx
}
style=
{
{
fontSize
:
12
}
}
>
<
div
style=
{
{
fontWeight
:
500
,
color
:
'
#374151
'
}
}
>
{
tc
.
tool_name
}
</
div
>
<
div
style=
{
{
color
:
'
#9ca3af
'
,
overflow
:
'
hidden
'
,
textOverflow
:
'
ellipsis
'
,
whiteSpace
:
'
nowrap
'
,
maxWidth
:
260
}
}
>
参数:
{
tc
.
arguments
}
</
div
>
{
tc
.
result
&&
(
<
div
style=
{
{
color
:
tc
.
success
?
'
#52c41a
'
:
'
#ff4d4f
'
,
fontSize
:
11
}
}
>
{
tc
.
success
?
'
✓ 成功
'
:
`✗ ${tc.result.error}`
}
</
div
>
)
}
</
div
>
),
}))
}
/>
),
}]
}
/>
);
};
const
hasPreConsultData
=
preConsultReport
&&
(
const
hasPreConsultData
=
preConsultReport
&&
(
preConsultReport
.
ai_analysis
||
(
preConsultReport
.
chat_messages
&&
preConsultReport
.
chat_messages
.
length
>
0
)
preConsultReport
.
ai_analysis
||
(
preConsultReport
.
chat_messages
&&
preConsultReport
.
chat_messages
.
length
>
0
)
);
);
// 渲染完整对话记录
const
renderFullChatHistory
=
(
chatMsgs
:
ChatMessage
[])
=>
{
const
renderFullChatHistory
=
(
chatMsgs
:
ChatMessage
[])
=>
{
if
(
!
chatMsgs
||
chatMsgs
.
length
===
0
)
{
if
(
!
chatMsgs
||
chatMsgs
.
length
===
0
)
{
return
<
Alert
message=
"暂无对话记录"
type=
"info"
showIcon
style=
{
{
fontSize
:
12
}
}
/>;
return
<
Alert
message=
"暂无对话记录"
type=
"info"
showIcon
style=
{
{
fontSize
:
12
}
}
/>;
...
@@ -74,7 +125,6 @@ const AIPanel: React.FC<AIPanelProps> = ({
...
@@ -74,7 +125,6 @@ const AIPanel: React.FC<AIPanelProps> = ({
const
renderPreConsultContent
=
()
=>
{
const
renderPreConsultContent
=
()
=>
{
if
(
!
preConsultReport
)
return
null
;
if
(
!
preConsultReport
)
return
null
;
const
patientInfo
=
(
const
patientInfo
=
(
<
div
style=
{
{
padding
:
8
,
background
:
'
#f9f0ff
'
,
borderRadius
:
6
,
marginBottom
:
8
}
}
>
<
div
style=
{
{
padding
:
8
,
background
:
'
#f9f0ff
'
,
borderRadius
:
6
,
marginBottom
:
8
}
}
>
<
Text
strong
style=
{
{
fontSize
:
13
,
color
:
'
#722ed1
'
}
}
>
<
Text
strong
style=
{
{
fontSize
:
13
,
color
:
'
#722ed1
'
}
}
>
...
@@ -88,7 +138,6 @@ const AIPanel: React.FC<AIPanelProps> = ({
...
@@ -88,7 +138,6 @@ const AIPanel: React.FC<AIPanelProps> = ({
)
}
)
}
</
div
>
</
div
>
);
);
const
tags
=
(
preConsultReport
.
ai_severity
||
preConsultReport
.
ai_department
)
&&
(
const
tags
=
(
preConsultReport
.
ai_severity
||
preConsultReport
.
ai_department
)
&&
(
<
div
style=
{
{
display
:
'
flex
'
,
gap
:
4
,
marginBottom
:
8
,
flexWrap
:
'
wrap
'
}
}
>
<
div
style=
{
{
display
:
'
flex
'
,
gap
:
4
,
marginBottom
:
8
,
flexWrap
:
'
wrap
'
}
}
>
{
preConsultReport
.
ai_severity
&&
(
{
preConsultReport
.
ai_severity
&&
(
...
@@ -99,7 +148,6 @@ const AIPanel: React.FC<AIPanelProps> = ({
...
@@ -99,7 +148,6 @@ const AIPanel: React.FC<AIPanelProps> = ({
{
preConsultReport
.
ai_department
&&
<
Tag
color=
"blue"
>
{
preConsultReport
.
ai_department
}
</
Tag
>
}
{
preConsultReport
.
ai_department
&&
<
Tag
color=
"blue"
>
{
preConsultReport
.
ai_department
}
</
Tag
>
}
</
div
>
</
div
>
);
);
return
(
return
(
<
div
>
<
div
>
{
patientInfo
}
{
patientInfo
}
...
@@ -136,6 +184,54 @@ const AIPanel: React.FC<AIPanelProps> = ({
...
@@ -136,6 +184,54 @@ const AIPanel: React.FC<AIPanelProps> = ({
);
);
};
};
const
renderAIAssistContent
=
(
loading
:
boolean
,
content
:
string
,
toolCalls
:
ToolCall
[],
scene
:
'
consult_diagnosis
'
|
'
consult_medication
'
,
)
=>
{
if
(
loading
)
return
<
div
style=
{
{
textAlign
:
'
center
'
,
padding
:
20
}
}
><
Spin
tip=
"AI分析.."
/></
div
>;
if
(
content
)
{
return
(
<
div
>
<
div
style=
{
{
maxHeight
:
400
,
overflow
:
'
auto
'
}
}
>
<
MarkdownRenderer
content=
{
content
}
fontSize=
{
12
}
lineHeight=
{
1.6
}
/>
</
div
>
{
renderToolCalls
(
toolCalls
)
}
{
toolCalls
.
length
>
0
&&
(
<
div
style=
{
{
fontSize
:
11
,
color
:
'
#9ca3af
'
,
marginTop
:
4
}
}
>
<
ThunderboltOutlined
style=
{
{
marginRight
:
2
}
}
/>
通过
{
toolCalls
.
length
}
次工具调用生成
</
div
>
)
}
<
Divider
style=
{
{
margin
:
'
8px 0
'
}
}
/>
<
Button
size=
"small"
icon=
{
scene
===
'
consult_diagnosis
'
?
<
ThunderboltOutlined
/>
:
<
MedicineBoxOutlined
/>
}
onClick=
{
()
=>
handleAIAssist
(
scene
)
}
>
重新分析
</
Button
>
</
div
>
);
}
return
(
<
div
style=
{
{
textAlign
:
'
center
'
,
padding
:
16
}
}
>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
12
,
display
:
'
block
'
,
marginBottom
:
12
}
}
>
{
scene
===
'
consult_diagnosis
'
?
'
基于问诊对话内容,AI将辅助生成鉴别诊断
'
:
'
基于问诊对话内容,AI将辅助生成用药建议
'
}
</
Text
>
<
Button
type=
"primary"
size=
"small"
icon=
{
scene
===
'
consult_diagnosis
'
?
<
ThunderboltOutlined
/>
:
<
MedicineBoxOutlined
/>
}
onClick=
{
()
=>
handleAIAssist
(
scene
)
}
>
{
scene
===
'
consult_diagnosis
'
?
'
生成鉴别诊断
'
:
'
生成用药建议
'
}
</
Button
>
</
div
>
);
};
return
(
return
(
<
Card
<
Card
title=
{
title=
{
...
@@ -172,54 +268,12 @@ const AIPanel: React.FC<AIPanelProps> = ({
...
@@ -172,54 +268,12 @@ const AIPanel: React.FC<AIPanelProps> = ({
{
{
key
:
'
diagnosis
'
,
key
:
'
diagnosis
'
,
label
:
'
鉴别诊断
'
,
label
:
'
鉴别诊断
'
,
children
:
diagnosisLoading
?
(
children
:
renderAIAssistContent
(
diagnosisLoading
,
diagnosisContent
,
diagnosisToolCalls
,
'
consult_diagnosis
'
),
<
div
style=
{
{
textAlign
:
'
center
'
,
padding
:
20
}
}
><
Spin
tip=
"AI分析.."
/></
div
>
)
:
diagnosisContent
?
(
<
div
>
<
div
style=
{
{
maxHeight
:
400
,
overflow
:
'
auto
'
}
}
>
<
MarkdownRenderer
content=
{
diagnosisContent
}
fontSize=
{
12
}
lineHeight=
{
1.6
}
/>
</
div
>
<
Divider
style=
{
{
margin
:
'
8px 0
'
}
}
/>
<
Button
size=
"small"
icon=
{
<
ThunderboltOutlined
/>
}
onClick=
{
()
=>
handleAIAssist
(
'
consult_diagnosis
'
)
}
>
重新分析
</
Button
>
</
div
>
)
:
(
<
div
style=
{
{
textAlign
:
'
center
'
,
padding
:
16
}
}
>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
12
,
display
:
'
block
'
,
marginBottom
:
12
}
}
>
基于问诊对话内容,AI将辅助生成鉴别诊断
</
Text
>
<
Button
type=
"primary"
size=
"small"
icon=
{
<
ThunderboltOutlined
/>
}
onClick=
{
()
=>
handleAIAssist
(
'
consult_diagnosis
'
)
}
>
生成鉴别诊断
</
Button
>
</
div
>
),
},
},
{
{
key
:
'
drugs
'
,
key
:
'
drugs
'
,
label
:
'
用药建议
'
,
label
:
'
用药建议
'
,
children
:
medicationLoading
?
(
children
:
renderAIAssistContent
(
medicationLoading
,
medicationContent
,
medicationToolCalls
,
'
consult_medication
'
),
<
div
style=
{
{
textAlign
:
'
center
'
,
padding
:
20
}
}
><
Spin
tip=
"AI分析.."
/></
div
>
)
:
medicationContent
?
(
<
div
>
<
div
style=
{
{
maxHeight
:
400
,
overflow
:
'
auto
'
}
}
>
<
MarkdownRenderer
content=
{
medicationContent
}
fontSize=
{
12
}
lineHeight=
{
1.6
}
/>
</
div
>
<
Divider
style=
{
{
margin
:
'
8px 0
'
}
}
/>
<
Button
size=
"small"
icon=
{
<
MedicineBoxOutlined
/>
}
onClick=
{
()
=>
handleAIAssist
(
'
consult_medication
'
)
}
>
重新生成
</
Button
>
</
div
>
)
:
(
<
div
style=
{
{
textAlign
:
'
center
'
,
padding
:
16
}
}
>
<
Text
type=
"secondary"
style=
{
{
fontSize
:
12
,
display
:
'
block
'
,
marginBottom
:
12
}
}
>
基于问诊对话内容,AI将辅助生成用药建议
</
Text
>
<
Button
type=
"primary"
size=
"small"
icon=
{
<
MedicineBoxOutlined
/>
}
onClick=
{
()
=>
handleAIAssist
(
'
consult_medication
'
)
}
>
生成用药建议
</
Button
>
</
div
>
),
},
},
]
}
]
}
/>
/>
...
...
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