Commit da795257 authored by yuguo's avatar yuguo

fix

parent 79589e01
......@@ -56,7 +56,8 @@
"Bash(sed:*)",
"Bash(docker-compose ps:*)",
"Bash(cat web/src/app/\\\\\\(main\\\\\\)/admin/agents/page.tsx)",
"Bash(awk NR==68,NR==376:*)"
"Bash(awk NR==68,NR==376:*)",
"Bash(LANG=en_US.UTF-8 sed:*)"
]
}
}
# AI 智能助手工具化升级 — 第 1 批实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 新增 11 个业务工具(问诊域 5 + 处方域 3 + 患者信息域 3),使 AI 助手能触达核心诊疗业务数据,与现有 API 端到端一致。
**Architecture:** 所有查询工具直接使用 `DB.WithContext(ctx).Raw()` 查询数据库(与 `query_medical_record` 模式一致);写操作工具通过回调函数注入 Service 层方法(与 `NotifyFn` 模式一致),确保事务、校验、工作流触发与 HTTP API 完全相同。工具内通过 `ctx.Value(ContextKeyUserRole/UserID)` 做角色权限和数据隔离。
**Tech Stack:** Go 1.22, GORM, `pkg/agent` Tool 接口, `internal/agent/init.go` 注册
---
## Task 1: 问诊列表查询工具 `query_consultation_list`
**Files:**
- Create: `server/pkg/agent/tools/consultation_list.go`
- Modify: `server/internal/agent/init.go` (InitTools + syncToolsToDB categoryMap + initToolSelector categoryMap)
- Modify: `server/internal/agent/agents.go` (patientTools + doctorTools + adminTools)
**Step 1: Create `server/pkg/agent/tools/consultation_list.go`**
```go
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// ConsultationListTool 查询问诊列表
type ConsultationListTool struct {
DB *gorm.DB
}
func (t *ConsultationListTool) Name() string { return "query_consultation_list" }
func (t *ConsultationListTool) Description() string {
return "查询问诊列表:患者查自己的问诊记录,医生查自己接诊的问诊记录,支持按状态过滤"
}
func (t *ConsultationListTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "status", Type: "string", Description: "问诊状态过滤(可选):pending/in_progress/completed/cancelled", Required: false, Enum: []string{"pending", "in_progress", "completed", "cancelled"}},
{Name: "limit", Type: "number", Description: "返回记录数量,默认10,最大50", Required: false},
}
}
func (t *ConsultationListTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
if userID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
limit := 10
if v, ok := params["limit"].(float64); ok && v > 0 {
limit = int(v)
if limit > 50 {
limit = 50
}
}
status, _ := params["status"].(string)
// 根据角色决定查询条件
var roleField string
switch userRole {
case "doctor":
// 医生需要通过 doctors 表找到 doctor.id
roleField = "doctor_id"
var doctorID string
if err := t.DB.WithContext(ctx).Raw("SELECT id FROM doctors WHERE user_id = ?", userID).Scan(&doctorID).Error; err != nil || doctorID == "" {
return map[string]interface{}{"consultations": []interface{}{}, "total": 0}, nil
}
userID = doctorID
case "admin":
roleField = "" // 管理员查所有
default:
roleField = "patient_id"
}
query := "SELECT c.id, c.serial_number, c.chief_complaint, c.status, c.type, c.created_at, c.started_at, c.ended_at, " +
"d.name as doctor_name, dep.name as department_name, u.real_name as patient_name " +
"FROM consultations c " +
"LEFT JOIN doctors d ON c.doctor_id = d.id " +
"LEFT JOIN departments dep ON d.department_id = dep.id " +
"LEFT JOIN users u ON c.patient_id = u.id " +
"WHERE c.deleted_at IS NULL"
args := []interface{}{}
if roleField != "" {
query += " AND c." + roleField + " = ?"
args = append(args, userID)
}
if status != "" {
query += " AND c.status = ?"
args = append(args, status)
}
query += " ORDER BY c.created_at DESC LIMIT ?"
args = append(args, limit)
var results []map[string]interface{}
rows, err := t.DB.WithContext(ctx).Raw(query, args...).Rows()
if err != nil {
return map[string]interface{}{"consultations": []interface{}{}, "total": 0}, nil
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
row := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
row[col] = vals[i]
}
results = append(results, row)
}
}
return map[string]interface{}{
"consultations": results,
"total": len(results),
"role": userRole,
}, nil
}
```
**Step 2: Register in `server/internal/agent/init.go`**
In `InitTools()`, after the `r.Register(&tools.NavigatePageTool{})` line, add:
```go
// 第1批业务工具:问诊管理
r.Register(&tools.ConsultationListTool{DB: db})
```
In `syncToolsToDB` categoryMap, add:
```go
"query_consultation_list": "consult",
```
In `initToolSelector` categoryMap, add the same entry.
**Step 3: Add to agent tool lists in `server/internal/agent/agents.go`**
Add `"query_consultation_list"` to `patientTools`, `doctorTools`, and `adminTools` arrays.
**Step 4: Verify compilation**
Run: `cd server && go build ./...`
Expected: BUILD SUCCESS
**Step 5: Commit**
```bash
git add server/pkg/agent/tools/consultation_list.go server/internal/agent/init.go server/internal/agent/agents.go
git commit -m "feat(agent): add query_consultation_list tool for consult list queries"
```
---
## Task 2: 问诊详情查询工具 `query_consultation_detail`
**Files:**
- Create: `server/pkg/agent/tools/consultation_detail.go`
- Modify: `server/internal/agent/init.go` (registration + categoryMap)
- Modify: `server/internal/agent/agents.go`
**Step 1: Create `server/pkg/agent/tools/consultation_detail.go`**
```go
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// ConsultationDetailTool 查询问诊详情
type ConsultationDetailTool struct {
DB *gorm.DB
}
func (t *ConsultationDetailTool) Name() string { return "query_consultation_detail" }
func (t *ConsultationDetailTool) Description() string {
return "查询问诊详情,包括患者信息、医生信息、主诉、诊断、消息记录,支持UUID或流水号(C开头)查询"
}
func (t *ConsultationDetailTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "consultation_id", Type: "string", Description: "问诊ID(UUID)或流水号(C开头,如C20260305-0001)", Required: true},
{Name: "include_messages", Type: "boolean", Description: "是否包含消息记录,默认true", Required: false},
}
}
func (t *ConsultationDetailTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
consultID, ok := params["consultation_id"].(string)
if !ok || consultID == "" {
return nil, fmt.Errorf("consultation_id 必填")
}
includeMessages := true
if v, ok := params["include_messages"].(bool); ok {
includeMessages = v
}
// 支持流水号查询
resolvedID := consultID
if len(consultID) > 0 && consultID[0] == 'C' {
var id string
if err := t.DB.WithContext(ctx).Raw("SELECT id FROM consultations WHERE serial_number = ?", consultID).Scan(&id).Error; err != nil || id == "" {
return nil, fmt.Errorf("流水号 %s 对应的问诊不存在", consultID)
}
resolvedID = id
}
// 查询问诊详情
var detail map[string]interface{}
row := t.DB.WithContext(ctx).Raw(`
SELECT c.id, c.serial_number, c.patient_id, c.doctor_id, c.type, c.status,
c.chief_complaint, c.medical_history, c.diagnosis, c.summary,
c.started_at, c.ended_at, c.created_at, c.satisfaction_score,
d.name as doctor_name, d.title as doctor_title, dep.name as department_name,
u.real_name as patient_name, u.phone as patient_phone
FROM consultations c
LEFT JOIN doctors d ON c.doctor_id = d.id
LEFT JOIN departments dep ON d.department_id = dep.id
LEFT JOIN users u ON c.patient_id = u.id
WHERE c.id = ? AND c.deleted_at IS NULL`, resolvedID).Row()
cols := []string{"id", "serial_number", "patient_id", "doctor_id", "type", "status",
"chief_complaint", "medical_history", "diagnosis", "summary",
"started_at", "ended_at", "created_at", "satisfaction_score",
"doctor_name", "doctor_title", "department_name",
"patient_name", "patient_phone"}
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := row.Scan(ptrs...); err != nil {
return nil, fmt.Errorf("问诊不存在: %s", consultID)
}
detail = make(map[string]interface{})
for i, col := range cols {
detail[col] = vals[i]
}
// 权限校验:患者只能查自己的,医生只能查自己接诊的
if userRole == "patient" && detail["patient_id"] != userID {
return nil, fmt.Errorf("无权查看该问诊记录")
}
if userRole == "doctor" {
var doctorID string
t.DB.WithContext(ctx).Raw("SELECT id FROM doctors WHERE user_id = ?", userID).Scan(&doctorID)
if detail["doctor_id"] != doctorID {
return nil, fmt.Errorf("无权查看该问诊记录")
}
}
// 可选:查询消息记录
if includeMessages {
var messages []map[string]interface{}
msgRows, err := t.DB.WithContext(ctx).Raw(`
SELECT id, sender_type, content, content_type, created_at
FROM consult_messages
WHERE consult_id = ? AND deleted_at IS NULL
ORDER BY created_at ASC LIMIT 50`, resolvedID).Rows()
if err == nil {
defer msgRows.Close()
msgCols, _ := msgRows.Columns()
for msgRows.Next() {
msg := make(map[string]interface{})
mVals := make([]interface{}, len(msgCols))
mPtrs := make([]interface{}, len(msgCols))
for i := range mVals {
mPtrs[i] = &mVals[i]
}
if err := msgRows.Scan(mPtrs...); err == nil {
for i, col := range msgCols {
msg[col] = mVals[i]
}
messages = append(messages, msg)
}
}
}
detail["messages"] = messages
detail["message_count"] = len(messages)
}
return detail, nil
}
```
**Step 2: Register in `init.go`, add to categoryMap:**
```go
r.Register(&tools.ConsultationDetailTool{DB: db})
// categoryMap:
"query_consultation_detail": "consult",
```
**Step 3: Add to `agents.go`** — add `"query_consultation_detail"` to patientTools, doctorTools, adminTools.
**Step 4: Verify compilation**
Run: `cd server && go build ./...`
**Step 5: Commit**
```bash
git add server/pkg/agent/tools/consultation_detail.go server/internal/agent/init.go server/internal/agent/agents.go
git commit -m "feat(agent): add query_consultation_detail tool with message history"
```
---
## Task 3: 创建问诊工具 `create_consultation`(写操作 — 回调模式)
**Files:**
- Create: `server/pkg/agent/tools/consultation_create.go`
- Modify: `server/internal/agent/init.go` (registration + WireCallbacks)
- Modify: `server/internal/agent/agents.go`
**Step 1: Create `server/pkg/agent/tools/consultation_create.go`**
```go
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
)
// CreateConsultFn 创建问诊回调,由 WireCallbacks 注入 consult.Service.CreateConsult
// 参数: ctx, patientID, doctorID, consultType, chiefComplaint, medicalHistory
// 返回: 问诊ID, 流水号, error
var CreateConsultFn func(ctx context.Context, patientID, doctorID, consultType, chiefComplaint, medicalHistory string) (string, string, error)
// CreateConsultationTool 创建问诊(患者发起)
type CreateConsultationTool struct{}
func (t *CreateConsultationTool) Name() string { return "create_consultation" }
func (t *CreateConsultationTool) Description() string {
return "患者创建新的问诊,需要指定医生ID、问诊类型和主诉。创建成功后返回问诊ID和流水号"
}
func (t *CreateConsultationTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "doctor_id", Type: "string", Description: "接诊医生ID(从 query_doctor_list 获取)", Required: true},
{Name: "type", Type: "string", Description: "问诊类型", Required: true, Enum: []string{"text", "video"}},
{Name: "chief_complaint", Type: "string", Description: "主诉(患者症状描述)", Required: true},
{Name: "medical_history", Type: "string", Description: "既往病史(可选)", Required: false},
}
}
func (t *CreateConsultationTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
if userRole != "patient" {
return nil, fmt.Errorf("仅患者可创建问诊")
}
if userID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
doctorID, ok := params["doctor_id"].(string)
if !ok || doctorID == "" {
return nil, fmt.Errorf("doctor_id 必填")
}
consultType, ok := params["type"].(string)
if !ok || (consultType != "text" && consultType != "video") {
return nil, fmt.Errorf("type 必须为 text 或 video")
}
chiefComplaint, ok := params["chief_complaint"].(string)
if !ok || chiefComplaint == "" {
return nil, fmt.Errorf("chief_complaint 必填")
}
medicalHistory, _ := params["medical_history"].(string)
if CreateConsultFn == nil {
return nil, fmt.Errorf("问诊服务未初始化")
}
consultID, serialNumber, err := CreateConsultFn(ctx, userID, doctorID, consultType, chiefComplaint, medicalHistory)
if err != nil {
return nil, fmt.Errorf("创建问诊失败: %v", err)
}
return map[string]interface{}{
"consultation_id": consultID,
"serial_number": serialNumber,
"status": "pending",
"message": "问诊已创建,等待医生接诊",
}, nil
}
```
**Step 2: Wire callback in `init.go` WireCallbacks()**
```go
// 注入创建问诊回调
tools.CreateConsultFn = func(ctx context.Context, patientID, doctorID, consultType, chiefComplaint, medicalHistory string) (string, string, error) {
consultSvc := consult.NewService()
resp, err := consultSvc.CreateConsult(ctx, patientID, &consult.CreateConsultRequest{
DoctorID: doctorID,
Type: consultType,
ChiefComplaint: chiefComplaint,
MedicalHistory: medicalHistory,
})
if err != nil {
return "", "", err
}
return resp.ID, resp.SerialNumber, nil
}
```
Add import `"internet-hospital/internal/service/consult"` to init.go.
**Step 3: Register tool + categoryMap:**
```go
r.Register(&tools.CreateConsultationTool{})
// categoryMap:
"create_consultation": "consult",
```
**Step 4: Add to `agents.go`** — add `"create_consultation"` to patientTools only.
**Step 5: Verify compilation + Commit**
```bash
cd server && go build ./...
git add server/pkg/agent/tools/consultation_create.go server/internal/agent/init.go server/internal/agent/agents.go
git commit -m "feat(agent): add create_consultation tool with service callback"
```
---
## Task 4: 医生接诊工具 `accept_consultation`(写操作 — 回调模式)
**Files:**
- Create: `server/pkg/agent/tools/consultation_accept.go`
- Modify: `server/internal/agent/init.go`
- Modify: `server/internal/agent/agents.go`
**Step 1: Create `server/pkg/agent/tools/consultation_accept.go`**
```go
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
)
// AcceptConsultFn 接诊回调,由 WireCallbacks 注入 doctorportal.Service.AcceptConsult
var AcceptConsultFn func(ctx context.Context, consultID, doctorUserID string) error
// AcceptConsultationTool 医生接诊
type AcceptConsultationTool struct{}
func (t *AcceptConsultationTool) Name() string { return "accept_consultation" }
func (t *AcceptConsultationTool) Description() string {
return "医生接受一个等候中的问诊,将问诊状态从pending变为in_progress"
}
func (t *AcceptConsultationTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "consultation_id", Type: "string", Description: "要接诊的问诊ID", Required: true},
}
}
func (t *AcceptConsultationTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
if userRole != "doctor" {
return nil, fmt.Errorf("仅医生可接诊")
}
consultID, ok := params["consultation_id"].(string)
if !ok || consultID == "" {
return nil, fmt.Errorf("consultation_id 必填")
}
if AcceptConsultFn == nil {
return nil, fmt.Errorf("接诊服务未初始化")
}
if err := AcceptConsultFn(ctx, consultID, userID); err != nil {
return nil, fmt.Errorf("接诊失败: %v", err)
}
return map[string]interface{}{
"consultation_id": consultID,
"status": "in_progress",
"message": "接诊成功,问诊已开始",
}, nil
}
```
**Step 2: Wire callback in `init.go` WireCallbacks()**
```go
// 注入接诊回调
tools.AcceptConsultFn = func(ctx context.Context, consultID, doctorUserID string) error {
dpSvc := doctorportal.NewService()
_, err := dpSvc.AcceptConsult(ctx, consultID, doctorUserID)
return err
}
```
Add import `"internet-hospital/internal/service/doctorportal"`.
**Step 3: Register + categoryMap + agents.go** — add to doctorTools only.
**Step 4: Verify + Commit**
```bash
cd server && go build ./...
git add server/pkg/agent/tools/consultation_accept.go server/internal/agent/init.go server/internal/agent/agents.go
git commit -m "feat(agent): add accept_consultation tool for doctor to accept consults"
```
---
## Task 5: 等候队列查询工具 `query_waiting_queue`
**Files:**
- Create: `server/pkg/agent/tools/waiting_queue.go`
- Modify: `server/internal/agent/init.go`
- Modify: `server/internal/agent/agents.go`
**Step 1: Create `server/pkg/agent/tools/waiting_queue.go`**
```go
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// WaitingQueueTool 查询医生的等候队列
type WaitingQueueTool struct {
DB *gorm.DB
}
func (t *WaitingQueueTool) Name() string { return "query_waiting_queue" }
func (t *WaitingQueueTool) Description() string {
return "查询当前医生的等候队列,显示等候中的患者数量和列表"
}
func (t *WaitingQueueTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{}
}
func (t *WaitingQueueTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
if userRole != "doctor" {
return nil, fmt.Errorf("仅医生可查看等候队列")
}
// 获取 doctor.id
var doctorID string
if err := t.DB.WithContext(ctx).Raw("SELECT id FROM doctors WHERE user_id = ?", userID).Scan(&doctorID).Error; err != nil || doctorID == "" {
return map[string]interface{}{"waiting_count": 0, "patients": []interface{}{}}, nil
}
var results []map[string]interface{}
rows, err := t.DB.WithContext(ctx).Raw(`
SELECT c.id, c.serial_number, c.chief_complaint, c.type, c.created_at,
u.real_name as patient_name,
EXTRACT(EPOCH FROM (NOW() - c.created_at))::int as wait_seconds
FROM consultations c
LEFT JOIN users u ON c.patient_id = u.id
WHERE c.doctor_id = ? AND c.status = 'pending' AND c.deleted_at IS NULL
ORDER BY c.created_at ASC`, doctorID).Rows()
if err != nil {
return map[string]interface{}{"waiting_count": 0, "patients": []interface{}{}}, nil
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
row := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
row[col] = vals[i]
}
results = append(results, row)
}
}
return map[string]interface{}{
"waiting_count": len(results),
"patients": results,
}, nil
}
```
**Step 2: Register + categoryMap + agents.go** — add to doctorTools only.
```go
r.Register(&tools.WaitingQueueTool{DB: db})
// categoryMap:
"query_waiting_queue": "consult",
```
**Step 3: Verify + Commit**
```bash
cd server && go build ./...
git add server/pkg/agent/tools/waiting_queue.go server/internal/agent/init.go server/internal/agent/agents.go
git commit -m "feat(agent): add query_waiting_queue tool for doctor waiting list"
```
---
## Task 6: 处方列表查询工具 `query_prescription_list`
**Files:**
- Create: `server/pkg/agent/tools/prescription_list.go`
- Modify: `server/internal/agent/init.go`
- Modify: `server/internal/agent/agents.go`
**Step 1: Create `server/pkg/agent/tools/prescription_list.go`**
```go
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// PrescriptionListTool 查询处方列表
type PrescriptionListTool struct {
DB *gorm.DB
}
func (t *PrescriptionListTool) Name() string { return "query_prescription_list" }
func (t *PrescriptionListTool) Description() string {
return "查询处方列表:患者查自己的处方,医生查自己开的处方,支持按状态过滤"
}
func (t *PrescriptionListTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "status", Type: "string", Description: "处方状态过滤(可选):pending/signed/dispensed/completed/cancelled", Required: false},
{Name: "limit", Type: "number", Description: "返回数量,默认10,最大50", Required: false},
}
}
func (t *PrescriptionListTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
if userID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
limit := 10
if v, ok := params["limit"].(float64); ok && v > 0 {
limit = int(v)
if limit > 50 {
limit = 50
}
}
status, _ := params["status"].(string)
query := `SELECT p.id, p.prescription_no, p.patient_name, p.diagnosis,
p.total_amount, p.status, p.created_at,
d.name as doctor_name, dep.name as department_name,
(SELECT COUNT(*) FROM prescription_items pi WHERE pi.prescription_id = p.id) as drug_count
FROM prescriptions p
LEFT JOIN doctors d ON p.doctor_id = d.id
LEFT JOIN departments dep ON d.department_id = dep.id
WHERE p.deleted_at IS NULL`
args := []interface{}{}
switch userRole {
case "patient":
query += " AND p.patient_id = ?"
args = append(args, userID)
case "doctor":
var doctorID string
t.DB.WithContext(ctx).Raw("SELECT id FROM doctors WHERE user_id = ?", userID).Scan(&doctorID)
if doctorID == "" {
return map[string]interface{}{"prescriptions": []interface{}{}, "total": 0}, nil
}
query += " AND p.doctor_id = ?"
args = append(args, doctorID)
case "admin":
// 管理员查所有
default:
return nil, fmt.Errorf("无权限查询处方")
}
if status != "" {
query += " AND p.status = ?"
args = append(args, status)
}
query += " ORDER BY p.created_at DESC LIMIT ?"
args = append(args, limit)
var results []map[string]interface{}
rows, err := t.DB.WithContext(ctx).Raw(query, args...).Rows()
if err != nil {
return map[string]interface{}{"prescriptions": []interface{}{}, "total": 0}, nil
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
row := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
row[col] = vals[i]
}
results = append(results, row)
}
}
return map[string]interface{}{
"prescriptions": results,
"total": len(results),
}, nil
}
```
**Step 2: Register + categoryMap + agents.go** — add to patientTools, doctorTools, adminTools.
```go
r.Register(&tools.PrescriptionListTool{DB: db})
// categoryMap:
"query_prescription_list": "prescription",
```
**Step 3: Verify + Commit**
```bash
cd server && go build ./...
git add server/pkg/agent/tools/prescription_list.go server/internal/agent/init.go server/internal/agent/agents.go
git commit -m "feat(agent): add query_prescription_list tool for prescription queries"
```
---
## Task 7: 处方详情查询工具 `query_prescription_detail`
**Files:**
- Create: `server/pkg/agent/tools/prescription_detail.go`
- Modify: `server/internal/agent/init.go`
- Modify: `server/internal/agent/agents.go`
**Step 1: Create `server/pkg/agent/tools/prescription_detail.go`**
```go
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// PrescriptionDetailTool 查询处方详情
type PrescriptionDetailTool struct {
DB *gorm.DB
}
func (t *PrescriptionDetailTool) Name() string { return "query_prescription_detail" }
func (t *PrescriptionDetailTool) Description() string {
return "查询处方详情,包括药品明细、用法用量、状态和费用,支持处方ID或处方编号(RX开头)查询"
}
func (t *PrescriptionDetailTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "prescription_id", Type: "string", Description: "处方ID(UUID)或处方编号(RX开头)", Required: true},
}
}
func (t *PrescriptionDetailTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
prescriptionID, ok := params["prescription_id"].(string)
if !ok || prescriptionID == "" {
return nil, fmt.Errorf("prescription_id 必填")
}
// 支持处方编号查询
resolvedID := prescriptionID
if len(prescriptionID) >= 2 && prescriptionID[:2] == "RX" {
var id string
if err := t.DB.WithContext(ctx).Raw("SELECT id FROM prescriptions WHERE prescription_no = ?", prescriptionID).Scan(&id).Error; err != nil || id == "" {
return nil, fmt.Errorf("处方编号 %s 不存在", prescriptionID)
}
resolvedID = id
}
// 查询处方主记录
var detail map[string]interface{}
row := t.DB.WithContext(ctx).Raw(`
SELECT p.id, p.prescription_no, p.consult_id, p.patient_id, p.patient_name,
p.patient_gender, p.patient_age, p.diagnosis, p.allergy_history,
p.remark, p.total_amount, p.status, p.created_at,
d.name as doctor_name, d.title as doctor_title, dep.name as department_name
FROM prescriptions p
LEFT JOIN doctors d ON p.doctor_id = d.id
LEFT JOIN departments dep ON d.department_id = dep.id
WHERE p.id = ? AND p.deleted_at IS NULL`, resolvedID).Row()
cols := []string{"id", "prescription_no", "consult_id", "patient_id", "patient_name",
"patient_gender", "patient_age", "diagnosis", "allergy_history",
"remark", "total_amount", "status", "created_at",
"doctor_name", "doctor_title", "department_name"}
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := row.Scan(ptrs...); err != nil {
return nil, fmt.Errorf("处方不存在: %s", prescriptionID)
}
detail = make(map[string]interface{})
for i, col := range cols {
detail[col] = vals[i]
}
// 权限校验
if userRole == "patient" && detail["patient_id"] != userID {
return nil, fmt.Errorf("无权查看该处方")
}
// 查询药品明细
var items []map[string]interface{}
itemRows, err := t.DB.WithContext(ctx).Raw(`
SELECT medicine_name, specification, usage, dosage, frequency,
days, quantity, unit, price, note
FROM prescription_items
WHERE prescription_id = ?
ORDER BY created_at ASC`, resolvedID).Rows()
if err == nil {
defer itemRows.Close()
itemCols, _ := itemRows.Columns()
for itemRows.Next() {
item := make(map[string]interface{})
iVals := make([]interface{}, len(itemCols))
iPtrs := make([]interface{}, len(itemCols))
for i := range iVals {
iPtrs[i] = &iVals[i]
}
if err := itemRows.Scan(iPtrs...); err == nil {
for i, col := range itemCols {
item[col] = iVals[i]
}
items = append(items, item)
}
}
}
detail["items"] = items
detail["drug_count"] = len(items)
return detail, nil
}
```
**Step 2: Register + categoryMap + agents.go** — add to patientTools, doctorTools, adminTools.
```go
r.Register(&tools.PrescriptionDetailTool{DB: db})
// categoryMap:
"query_prescription_detail": "prescription",
```
**Step 3: Verify + Commit**
```bash
cd server && go build ./...
git add server/pkg/agent/tools/prescription_detail.go server/internal/agent/init.go server/internal/agent/agents.go
git commit -m "feat(agent): add query_prescription_detail tool with drug items"
```
---
## Task 8: 药品目录搜索工具 `search_medicine_catalog`
**Files:**
- Create: `server/pkg/agent/tools/medicine_catalog.go`
- Modify: `server/internal/agent/init.go`
- Modify: `server/internal/agent/agents.go`
**Step 1: Create `server/pkg/agent/tools/medicine_catalog.go`**
```go
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// MedicineCatalogTool 搜索药品目录
type MedicineCatalogTool struct {
DB *gorm.DB
}
func (t *MedicineCatalogTool) Name() string { return "search_medicine_catalog" }
func (t *MedicineCatalogTool) Description() string {
return "搜索药品目录,查询药品名称、规格、库存、价格等信息,为开方提供数据支持"
}
func (t *MedicineCatalogTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "keyword", Type: "string", Description: "药品名称或关键词", Required: true},
{Name: "category", Type: "string", Description: "药品分类过滤(可选)", Required: false},
{Name: "limit", Type: "number", Description: "返回数量,默认10", Required: false},
}
}
func (t *MedicineCatalogTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
if userRole != "doctor" && userRole != "admin" {
return nil, fmt.Errorf("仅医生和管理员可搜索药品目录")
}
keyword, ok := params["keyword"].(string)
if !ok || keyword == "" {
return nil, fmt.Errorf("keyword 必填")
}
limit := 10
if v, ok := params["limit"].(float64); ok && v > 0 {
limit = int(v)
if limit > 30 {
limit = 30
}
}
category, _ := params["category"].(string)
query := `SELECT id, name, generic_name, specification, manufacturer,
unit, price, stock, category, status, usage_method, contraindication
FROM medicines
WHERE (name ILIKE ? OR generic_name ILIKE ?) AND status = 'active'`
searchPattern := "%" + keyword + "%"
args := []interface{}{searchPattern, searchPattern}
if category != "" {
query += " AND category = ?"
args = append(args, category)
}
query += " ORDER BY name ASC LIMIT ?"
args = append(args, limit)
var results []map[string]interface{}
rows, err := t.DB.WithContext(ctx).Raw(query, args...).Rows()
if err != nil {
return map[string]interface{}{"medicines": []interface{}{}, "total": 0}, nil
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
row := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
row[col] = vals[i]
}
results = append(results, row)
}
}
return map[string]interface{}{
"medicines": results,
"total": len(results),
}, nil
}
```
**Step 2: Register + categoryMap + agents.go** — add to doctorTools only.
```go
r.Register(&tools.MedicineCatalogTool{DB: db})
// categoryMap:
"search_medicine_catalog": "pharmacy",
```
**Step 3: Verify + Commit**
```bash
cd server && go build ./...
git add server/pkg/agent/tools/medicine_catalog.go server/internal/agent/init.go server/internal/agent/agents.go
git commit -m "feat(agent): add search_medicine_catalog tool for drug catalog search"
```
---
## Task 9: 患者健康档案查询工具 `query_patient_profile`
**Files:**
- Create: `server/pkg/agent/tools/patient_profile.go`
- Modify: `server/internal/agent/init.go`
- Modify: `server/internal/agent/agents.go`
**Step 1: Create `server/pkg/agent/tools/patient_profile.go`**
```go
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// PatientProfileTool 查询患者健康档案
type PatientProfileTool struct {
DB *gorm.DB
}
func (t *PatientProfileTool) Name() string { return "query_patient_profile" }
func (t *PatientProfileTool) Description() string {
return "查询患者健康档案(基本信息、过敏史、病史、保险信息),患者查自己的,医生按patient_id查"
}
func (t *PatientProfileTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "patient_id", Type: "string", Description: "患者ID(医生/管理员使用,患者无需传入自动查自己的)", Required: false},
}
}
func (t *PatientProfileTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
targetID := userID
if pid, ok := params["patient_id"].(string); ok && pid != "" {
if userRole == "patient" && pid != userID {
return nil, fmt.Errorf("患者只能查看自己的健康档案")
}
targetID = pid
}
if targetID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
// 查询用户基本信息
var userInfo map[string]interface{}
uRow := t.DB.WithContext(ctx).Raw(`
SELECT id, real_name, phone, gender, age, avatar, role, status, created_at
FROM users WHERE id = ?`, targetID).Row()
uCols := []string{"id", "real_name", "phone", "gender", "age", "avatar", "role", "status", "created_at"}
uVals := make([]interface{}, len(uCols))
uPtrs := make([]interface{}, len(uCols))
for i := range uVals {
uPtrs[i] = &uVals[i]
}
if err := uRow.Scan(uPtrs...); err != nil {
return nil, fmt.Errorf("用户不存在")
}
userInfo = make(map[string]interface{})
for i, col := range uCols {
userInfo[col] = uVals[i]
}
// 查询健康档案
var profile map[string]interface{}
pRow := t.DB.WithContext(ctx).Raw(`
SELECT gender, birth_date, medical_history, allergy_history,
emergency_contact, insurance_type, insurance_no
FROM patient_profiles WHERE user_id = ?`, targetID).Row()
pCols := []string{"gender", "birth_date", "medical_history", "allergy_history",
"emergency_contact", "insurance_type", "insurance_no"}
pVals := make([]interface{}, len(pCols))
pPtrs := make([]interface{}, len(pCols))
for i := range pVals {
pPtrs[i] = &pVals[i]
}
profile = make(map[string]interface{})
if err := pRow.Scan(pPtrs...); err == nil {
for i, col := range pCols {
profile[col] = pVals[i]
}
}
// 查询最近问诊数
var consultCount int64
t.DB.WithContext(ctx).Raw("SELECT COUNT(*) FROM consultations WHERE patient_id = ? AND deleted_at IS NULL", targetID).Scan(&consultCount)
return map[string]interface{}{
"user": userInfo,
"health_profile": profile,
"consult_count": consultCount,
}, nil
}
```
**Step 2: Register + categoryMap + agents.go** — add to patientTools and doctorTools.
```go
r.Register(&tools.PatientProfileTool{DB: db})
// categoryMap:
"query_patient_profile": "patient",
```
**Step 3: Verify + Commit**
```bash
cd server && go build ./...
git add server/pkg/agent/tools/patient_profile.go server/internal/agent/init.go server/internal/agent/agents.go
git commit -m "feat(agent): add query_patient_profile tool for health profile queries"
```
---
## Task 10: 健康指标查询工具 `query_health_metrics`
**Files:**
- Create: `server/pkg/agent/tools/health_metrics.go`
- Modify: `server/internal/agent/init.go`
- Modify: `server/internal/agent/agents.go`
**Step 1: Create `server/pkg/agent/tools/health_metrics.go`**
```go
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// HealthMetricsTool 查询健康指标记录
type HealthMetricsTool struct {
DB *gorm.DB
}
func (t *HealthMetricsTool) Name() string { return "query_health_metrics" }
func (t *HealthMetricsTool) Description() string {
return "查询患者健康指标记录(血压、血糖、心率、体温),支持类型过滤,按时间倒序返回"
}
func (t *HealthMetricsTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "patient_id", Type: "string", Description: "患者ID(医生使用,患者无需传入)", Required: false},
{Name: "metric_type", Type: "string", Description: "指标类型过滤(可选)", Required: false, Enum: []string{"blood_pressure", "blood_glucose", "heart_rate", "body_temperature"}},
{Name: "limit", Type: "number", Description: "返回数量,默认20", Required: false},
}
}
func (t *HealthMetricsTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
targetID := userID
if pid, ok := params["patient_id"].(string); ok && pid != "" {
if userRole == "patient" && pid != userID {
return nil, fmt.Errorf("患者只能查看自己的健康指标")
}
targetID = pid
}
if targetID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
limit := 20
if v, ok := params["limit"].(float64); ok && v > 0 {
limit = int(v)
if limit > 100 {
limit = 100
}
}
metricType, _ := params["metric_type"].(string)
query := `SELECT id, metric_type, value1, value2, unit, notes, recorded_at, created_at
FROM health_metrics WHERE user_id = ?`
args := []interface{}{targetID}
if metricType != "" {
query += " AND metric_type = ?"
args = append(args, metricType)
}
query += " ORDER BY recorded_at DESC LIMIT ?"
args = append(args, limit)
var results []map[string]interface{}
rows, err := t.DB.WithContext(ctx).Raw(query, args...).Rows()
if err != nil {
return map[string]interface{}{"metrics": []interface{}{}, "total": 0}, nil
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
row := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
row[col] = vals[i]
}
results = append(results, row)
}
}
return map[string]interface{}{
"metrics": results,
"total": len(results),
}, nil
}
```
**Step 2: Register + categoryMap + agents.go** — add to patientTools and doctorTools.
```go
r.Register(&tools.HealthMetricsTool{DB: db})
// categoryMap:
"query_health_metrics": "health",
```
**Step 3: Verify + Commit**
```bash
cd server && go build ./...
git add server/pkg/agent/tools/health_metrics.go server/internal/agent/init.go server/internal/agent/agents.go
git commit -m "feat(agent): add query_health_metrics tool for health metric queries"
```
---
## Task 11: 检验报告查询工具 `query_lab_reports`
**Files:**
- Create: `server/pkg/agent/tools/lab_reports.go`
- Modify: `server/internal/agent/init.go`
- Modify: `server/internal/agent/agents.go`
**Step 1: Create `server/pkg/agent/tools/lab_reports.go`**
```go
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// LabReportsTool 查询检验报告
type LabReportsTool struct {
DB *gorm.DB
}
func (t *LabReportsTool) Name() string { return "query_lab_reports" }
func (t *LabReportsTool) Description() string {
return "查询患者的检验报告列表,包括AI解读结果"
}
func (t *LabReportsTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "patient_id", Type: "string", Description: "患者ID(医生使用,患者无需传入)", Required: false},
{Name: "limit", Type: "number", Description: "返回数量,默认10", Required: false},
}
}
func (t *LabReportsTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
targetID := userID
if pid, ok := params["patient_id"].(string); ok && pid != "" {
if userRole == "patient" && pid != userID {
return nil, fmt.Errorf("患者只能查看自己的检验报告")
}
targetID = pid
}
if targetID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
limit := 10
if v, ok := params["limit"].(float64); ok && v > 0 {
limit = int(v)
if limit > 30 {
limit = 30
}
}
var results []map[string]interface{}
rows, err := t.DB.WithContext(ctx).Raw(`
SELECT id, title, report_date, file_url, file_type, category,
ai_interpret, created_at
FROM lab_reports
WHERE user_id = ? AND deleted_at IS NULL
ORDER BY report_date DESC, created_at DESC LIMIT ?`, targetID, limit).Rows()
if err != nil {
return map[string]interface{}{"reports": []interface{}{}, "total": 0}, nil
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
row := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
row[col] = vals[i]
}
results = append(results, row)
}
}
return map[string]interface{}{
"reports": results,
"total": len(results),
}, nil
}
```
**Step 2: Register + categoryMap + agents.go** — add to patientTools and doctorTools.
```go
r.Register(&tools.LabReportsTool{DB: db})
// categoryMap:
"query_lab_reports": "health",
```
**Step 3: Verify + Commit**
```bash
cd server && go build ./...
git add server/pkg/agent/tools/lab_reports.go server/internal/agent/init.go server/internal/agent/agents.go
git commit -m "feat(agent): add query_lab_reports tool for lab report queries"
```
---
## Task 12: 更新系统提示词(告知 AI 新工具的使用场景)
**Files:**
- Modify: `server/internal/agent/seed_prompts.go`
**Step 1: Update patient agent system prompt**
在患者智能助手的 `Content` 中,更新核心能力列表,增加:
```
6. **问诊管理**:查询问诊列表和详情,帮助患者创建新的问诊(需指定医生和主诉)
7. **处方查询**:查询处方列表和详情(药品明细、用法用量、费用)
8. **健康档案**:查询个人健康档案、健康指标(血压/血糖/心率/体温)趋势、检验报告及AI解读
```
更新使用原则,增加:
```
- 当患者想看病时,先用 recommend_department 推荐科室,然后建议使用 navigate_page 导航到找医生页面
- 当患者问"我的问诊"时,使用 query_consultation_list 查询
- 当患者问"我的处方/药"时,使用 query_prescription_list 查询
- 当患者问"我的健康数据"时,使用 query_health_metrics 查询
```
**Step 2: Update doctor agent system prompt**
增加核心能力:
```
7. **问诊管理**:查看等候队列、问诊列表和详情,快速接诊
8. **处方管理**:查看处方列表和详情,搜索药品目录
9. **患者档案**:查询患者健康档案、健康指标趋势、检验报告
```
更新使用原则:
```
- 查看等候队列用 query_waiting_queue,接诊用 accept_consultation
- 查患者信息用 query_patient_profile + query_health_metrics + query_lab_reports
- 搜索药品用 search_medicine_catalog,开方通过 navigate_page 导航到开方页面
```
**Step 3: Update admin agent system prompt**
增加核心能力:
```
8. **业务数据查询**:查看问诊记录、处方记录的列表和详情
```
**Step 4: Verify + Commit**
```bash
cd server && go build ./...
git add server/internal/agent/seed_prompts.go
git commit -m "feat(agent): update system prompts with new tool usage guidance"
```
---
## Task 13: 更新关键词索引(buildToolKeywords 补充新工具关键词)
**Files:**
- Modify: `server/internal/agent/init.go`
**Step 1:** In `buildToolKeywords`, add new Chinese keywords to the keyword list:
```go
"问诊", "就诊", "接诊", "挂号", "预约",
"等候", "队列", "排队",
"处方", "开方", "用药", "药品",
"档案", "信息", "资料",
"指标", "血压", "血糖", "心率", "体温",
"检验", "化验",
```
**Step 2: Verify + Commit**
```bash
cd server && go build ./...
git add server/internal/agent/init.go
git commit -m "feat(agent): expand keyword index for new business tools"
```
---
## Task 14: 整体编译验证 + 最终提交
**Step 1: Full build**
```bash
cd server && go build ./...
```
**Step 2: Run tests**
```bash
cd server && go test ./pkg/agent/... -v -count=1
```
**Step 3: Verify tool count**
手动检查 `init.go``InitTools()` 的注册数量:原 19 + 新增 11 = 30 个内置工具。
**Step 4: Final commit if any remaining changes**
```bash
git add -A
git commit -m "feat(agent): batch 1 complete - 11 new business tools (consult/prescription/patient)"
```
---
## Summary: Batch 1 Deliverables
| # | Tool Name | Type | File | Roles |
|---|-----------|------|------|-------|
| 1 | `query_consultation_list` | Query | `consultation_list.go` | patient, doctor, admin |
| 2 | `query_consultation_detail` | Query | `consultation_detail.go` | patient, doctor, admin |
| 3 | `create_consultation` | Write(callback) | `consultation_create.go` | patient |
| 4 | `accept_consultation` | Write(callback) | `consultation_accept.go` | doctor |
| 5 | `query_waiting_queue` | Query | `waiting_queue.go` | doctor |
| 6 | `query_prescription_list` | Query | `prescription_list.go` | patient, doctor, admin |
| 7 | `query_prescription_detail` | Query | `prescription_detail.go` | patient, doctor, admin |
| 8 | `search_medicine_catalog` | Query | `medicine_catalog.go` | doctor |
| 9 | `query_patient_profile` | Query | `patient_profile.go` | patient, doctor |
| 10 | `query_health_metrics` | Query | `health_metrics.go` | patient, doctor |
| 11 | `query_lab_reports` | Query | `lab_reports.go` | patient, doctor |
**Modified files:**
- `server/internal/agent/init.go` — Registration + categoryMap + keywords
- `server/internal/agent/agents.go` — Agent tool list updates
- `server/internal/agent/seed_prompts.go` — System prompt updates
**After Batch 1, tool coverage:**
- 问诊流程: ✅ 查列表/详情 + 创建问诊 + 接诊 + 等候队列
- 处方流程: ✅ 查列表/详情 + 药品目录搜索(开方通过导航)
- 患者信息: ✅ 健康档案 + 健康指标 + 检验报告
......@@ -11,6 +11,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"context"
internalagent "internet-hospital/internal/agent"
"internet-hospital/internal/model"
"internet-hospital/internal/service/knowledgesvc"
......@@ -25,6 +27,7 @@ import (
"internet-hospital/internal/service/chronic"
"internet-hospital/internal/service/health"
"internet-hospital/internal/service/user"
"internet-hospital/pkg/agent/tools"
"internet-hospital/pkg/ai"
"internet-hospital/pkg/config"
"internet-hospital/pkg/database"
......@@ -202,6 +205,69 @@ func main() {
// 注入跨包回调(AgentCallFn / WorkflowTriggerFn)
internalagent.WireCallbacks()
// v17: 注入问诊业务回调(避免 internal/agent ↔ internal/service 循环引用)
tools.CreateConsultFn = func(ctx context.Context, patientID, doctorID, consultType, chiefComplaint, medicalHistory string) (string, string, error) {
consultSvc := consult.NewService()
resp, err := consultSvc.CreateConsult(ctx, patientID, &consult.CreateConsultRequest{
DoctorID: doctorID,
Type: consultType,
ChiefComplaint: chiefComplaint,
MedicalHistory: medicalHistory,
})
if err != nil {
return "", "", err
}
return resp.ID, resp.SerialNumber, nil
}
tools.AcceptConsultFn = func(ctx context.Context, consultID, doctorUserID string) error {
dpSvc := doctorportal.NewService()
_, err := dpSvc.AcceptConsult(ctx, consultID, doctorUserID)
return err
}
log.Println("[Main] 问诊业务回调注入完成")
// v17 第2批: 注入慢病/续方/健康指标/排班回调
tools.CreateChronicRecordFn = func(ctx context.Context, userID, diseaseName, hospital, doctorName, currentMeds, notes string) (string, error) {
chronicSvc := chronic.NewService()
rec, err := chronicSvc.CreateChronicRecord(ctx, userID, &chronic.ChronicRecordReq{
DiseaseName: diseaseName, Hospital: hospital,
DoctorName: doctorName, CurrentMeds: currentMeds, Notes: notes,
})
if err != nil {
return "", err
}
return rec.ID, nil
}
tools.CreateRenewalFn = func(ctx context.Context, userID, chronicID, diseaseName, reason string, medicines []string) (string, error) {
chronicSvc := chronic.NewService()
rec, err := chronicSvc.CreateRenewal(ctx, userID, &chronic.RenewalReq{
ChronicID: chronicID, DiseaseName: diseaseName,
Medicines: medicines, Reason: reason,
})
if err != nil {
return "", err
}
return rec.ID, nil
}
tools.RecordHealthMetricFn = func(ctx context.Context, userID, metricType string, value1, value2 float64, unit, notes string) (string, error) {
chronicSvc := chronic.NewService()
rec, err := chronicSvc.CreateMetric(ctx, userID, &chronic.MetricReq{
MetricType: metricType, Value1: value1, Value2: value2,
Unit: unit, Notes: notes,
})
if err != nil {
return "", err
}
return rec.ID, nil
}
tools.CreateScheduleFn = func(ctx context.Context, doctorUserID, date, startTime, endTime string, maxCount int) error {
dpSvc := doctorportal.NewService()
return dpSvc.CreateSchedule(ctx, doctorUserID, []doctorportal.ScheduleSlotReq{
{Date: date, StartTime: startTime, EndTime: endTime, MaxCount: maxCount},
})
}
log.Println("[Main] 第2批业务回调注入完成")
// 设置 Gin 模式
gin.SetMode(cfg.Server.Mode)
......
......@@ -16,6 +16,21 @@ func defaultAgentDefinitions() []model.AgentDefinition {
"search_medical_knowledge", "query_drug",
"query_medical_record", "generate_follow_up_plan",
"send_notification", "navigate_page",
// v17: 问诊管理
"query_consultation_list", "query_consultation_detail", "create_consultation",
// v17: 处方查询
"query_prescription_list", "query_prescription_detail",
// v17: 患者信息
"query_patient_profile", "query_health_metrics", "query_lab_reports",
// v17: 医生/科室
"query_doctor_list", "query_doctor_detail", "query_department_list",
// v17: 慢病管理
"query_chronic_records", "create_chronic_record",
"query_renewal_requests", "create_renewal_request",
// v17: 健康指标写入 + 排班查询
"record_health_metric", "query_doctor_schedule",
// v17: 支付订单查询
"query_order_list", "query_order_detail",
})
// 医生通用智能体 — 合并 diagnosis + prescription + follow_up 能力
......@@ -23,6 +38,19 @@ func defaultAgentDefinitions() []model.AgentDefinition {
"query_medical_record", "query_symptom_knowledge", "search_medical_knowledge",
"query_drug", "check_drug_interaction", "check_contraindication", "calculate_dosage",
"generate_follow_up_plan", "send_notification", "navigate_page",
// v17: 问诊管理
"query_consultation_list", "query_consultation_detail",
"accept_consultation", "query_waiting_queue",
// v17: 处方管理
"query_prescription_list", "query_prescription_detail", "search_medicine_catalog",
// v17: 患者信息
"query_patient_profile", "query_health_metrics", "query_lab_reports",
// v17: 慢病续方审批
"query_renewal_requests",
// v17: 排班管理
"query_doctor_schedule", "create_doctor_schedule",
// v17: 收入统计
"query_income_stats", "query_income_records",
})
// 管理员通用智能体 — 合并 admin_assistant + general 管理能力
......@@ -31,6 +59,14 @@ func defaultAgentDefinitions() []model.AgentDefinition {
"trigger_workflow", "request_human_review",
"list_knowledge_collections", "send_notification",
"query_drug", "search_medical_knowledge", "navigate_page",
// v17: 业务数据查询
"query_consultation_list", "query_consultation_detail",
"query_prescription_list", "query_prescription_detail",
"generate_tool",
// v17: 管理统计 + 用户管理 + 系统日志 + 订单
"query_dashboard_stats", "query_dashboard_trend",
"query_user_list", "query_system_logs",
"query_order_list", "query_order_detail",
})
return []model.AgentDefinition{
......
......@@ -6,6 +6,7 @@ import (
"log"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"internet-hospital/internal/model"
"internet-hospital/pkg/agent"
......@@ -31,6 +32,7 @@ func NewHandler() *Handler {
// RegisterRoutes 用户侧路由(全角色可用,仅需 JWT)
func (h *Handler) RegisterRoutes(r gin.IRouter) {
g := r.Group("/agent")
g.POST("/sessions", h.CreateSession)
g.POST("/:agent_id/chat", h.Chat)
g.POST("/:agent_id/chat/stream", h.ChatStream)
g.GET("/sessions", h.ListSessions)
......@@ -139,6 +141,39 @@ func (h *Handler) ListAgents(c *gin.Context) {
response.Success(c, h.svc.ListAgents())
}
// CreateSession 创建新会话,返回 session_id
func (h *Handler) CreateSession(c *gin.Context) {
var req struct {
AgentID string `json:"agent_id" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
userID, _ := c.Get("user_id")
uid, _ := userID.(string)
role, _ := c.Get("role")
userRole, _ := role.(string)
sessionID := uuid.New().String()
session := model.AgentSession{
SessionID: sessionID,
AgentID: req.AgentID,
UserID: uid,
History: "[]",
Context: "null",
PageContext: "null",
EntityContext: "null",
Status: "active",
UserRole: userRole,
}
if err := database.GetDB().Create(&session).Error; err != nil {
response.Error(c, 500, "创建会话失败: "+err.Error())
return
}
response.Success(c, gin.H{"session_id": sessionID})
}
// ListSessions 获取用户的 Agent 会话列表
func (h *Handler) ListSessions(c *gin.Context) {
userID, _ := c.Get("user_id")
......@@ -303,14 +338,15 @@ func (h *Handler) GetDefinition(c *gin.Context) {
// CreateDefinition 创建新 Agent
func (h *Handler) CreateDefinition(c *gin.Context) {
var req struct {
AgentID string `json:"agent_id" binding:"required"`
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Category string `json:"category"`
SystemPrompt string `json:"system_prompt"`
Tools []string `json:"tools"`
Skills []string `json:"skills"`
MaxIterations int `json:"max_iterations"`
AgentID string `json:"agent_id" binding:"required"`
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Category string `json:"category"`
SystemPrompt string `json:"system_prompt"`
PromptTemplateID *uint `json:"prompt_template_id"`
Tools []string `json:"tools"`
Skills []string `json:"skills"`
MaxIterations int `json:"max_iterations"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
......@@ -322,15 +358,16 @@ func (h *Handler) CreateDefinition(c *gin.Context) {
req.MaxIterations = 10
}
def := model.AgentDefinition{
AgentID: req.AgentID,
Name: req.Name,
Description: req.Description,
Category: req.Category,
SystemPrompt: req.SystemPrompt,
Tools: string(toolsJSON),
Skills: string(skillsJSON),
MaxIterations: req.MaxIterations,
Status: "active",
AgentID: req.AgentID,
Name: req.Name,
Description: req.Description,
Category: req.Category,
SystemPrompt: req.SystemPrompt,
PromptTemplateID: req.PromptTemplateID,
Tools: string(toolsJSON),
Skills: string(skillsJSON),
MaxIterations: req.MaxIterations,
Status: "active",
}
if err := database.GetDB().Create(&def).Error; err != nil {
response.Error(c, 500, err.Error())
......@@ -344,14 +381,16 @@ func (h *Handler) CreateDefinition(c *gin.Context) {
func (h *Handler) UpdateDefinition(c *gin.Context) {
agentID := c.Param("agent_id")
var req struct {
Name string `json:"name"`
Description string `json:"description"`
Category string `json:"category"`
SystemPrompt string `json:"system_prompt"`
Tools []string `json:"tools"`
Skills []string `json:"skills"`
MaxIterations int `json:"max_iterations"`
Status string `json:"status"`
Name string `json:"name"`
Description string `json:"description"`
Category string `json:"category"`
SystemPrompt string `json:"system_prompt"`
PromptTemplateID *uint `json:"prompt_template_id"`
ClearTemplate bool `json:"clear_template"`
Tools []string `json:"tools"`
Skills []string `json:"skills"`
MaxIterations int `json:"max_iterations"`
Status string `json:"status"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
......@@ -378,6 +417,11 @@ func (h *Handler) UpdateDefinition(c *gin.Context) {
if req.SystemPrompt != "" {
updates["system_prompt"] = req.SystemPrompt
}
if req.PromptTemplateID != nil {
updates["prompt_template_id"] = *req.PromptTemplateID
} else if req.ClearTemplate {
updates["prompt_template_id"] = nil
}
if req.Tools != nil {
toolsJSON, _ := json.Marshal(req.Tools)
updates["tools"] = string(toolsJSON)
......
......@@ -62,6 +62,55 @@ func InitTools() {
// v15: 热代码 Tool 生成器
r.Register(&tools.CodeGenTool{})
// v17: 第1批业务工具 — 问诊管理
r.Register(&tools.ConsultationListTool{DB: db})
r.Register(&tools.ConsultationDetailTool{DB: db})
r.Register(&tools.CreateConsultationTool{})
r.Register(&tools.AcceptConsultationTool{})
r.Register(&tools.WaitingQueueTool{DB: db})
// v17: 第1批业务工具 — 处方管理
r.Register(&tools.PrescriptionListTool{DB: db})
r.Register(&tools.PrescriptionDetailTool{DB: db})
r.Register(&tools.MedicineCatalogTool{DB: db})
// v17: 第1批业务工具 — 患者信息
r.Register(&tools.PatientProfileTool{DB: db})
r.Register(&tools.HealthMetricsTool{DB: db})
r.Register(&tools.LabReportsTool{DB: db})
// v17: 第2批业务工具 — 医生/科室
r.Register(&tools.DoctorListTool{DB: db})
r.Register(&tools.DoctorDetailTool{DB: db})
r.Register(&tools.DepartmentListTool{DB: db})
// v17: 第2批业务工具 — 慢病管理
r.Register(&tools.ChronicRecordsTool{DB: db})
r.Register(&tools.CreateChronicRecordTool{})
r.Register(&tools.RenewalRequestsTool{DB: db})
r.Register(&tools.CreateRenewalTool{})
// v17: 第2批业务工具 — 健康指标写入
r.Register(&tools.RecordHealthMetricTool{})
// v17: 第2批业务工具 — 排班管理
r.Register(&tools.DoctorScheduleTool{DB: db})
r.Register(&tools.CreateDoctorScheduleTool{})
// v17: 第3批业务工具 — 支付订单
r.Register(&tools.OrderListTool{DB: db})
r.Register(&tools.OrderDetailTool{DB: db})
// v17: 第3批业务工具 — 医生收入
r.Register(&tools.IncomeStatsTool{DB: db})
r.Register(&tools.IncomeRecordsTool{DB: db})
// v17: 第3批业务工具 — 管理统计
r.Register(&tools.DashboardStatsTool{DB: db})
r.Register(&tools.DashboardTrendTool{DB: db})
r.Register(&tools.UserListTool{DB: db})
r.Register(&tools.SystemLogsTool{DB: db})
// v15: 从数据库加载动态 SQL 工具
loadDynamicSQLTools(r, db)
......@@ -75,8 +124,17 @@ func InitTools() {
log.Println("[InitTools] ToolMonitor & ToolSelector 初始化完成")
// v16: 初始化智能工具推荐器(使用pgvector)
agent.InitToolRecommender(db, embedder)
recommender := agent.InitToolRecommender(db, embedder)
log.Println("[InitTools] ToolRecommender 初始化完成")
// v17: 自动同步工具向量索引(异步,不阻塞启动)
if recommender != nil && embedder != nil {
go func() {
if err := recommender.IndexToolEmbeddings(context.Background()); err != nil {
log.Printf("[InitTools] 工具向量索引构建失败(embedder可能未配置API key): %v", err)
}
}()
}
}
// WireCallbacks 注入跨包回调(在 InitTools 和 GetService 初始化完成后调用)
......@@ -155,6 +213,45 @@ func syncToolsToDB(r *agent.ToolRegistry) {
"navigate_page": "navigation",
// v15: 热代码生成
"generate_tool": "code_gen",
// v17: 问诊管理
"query_consultation_list": "consult",
"query_consultation_detail": "consult",
"create_consultation": "consult",
"accept_consultation": "consult",
"query_waiting_queue": "consult",
// v17: 处方管理
"query_prescription_list": "prescription",
"query_prescription_detail": "prescription",
"search_medicine_catalog": "pharmacy",
// v17: 患者信息
"query_patient_profile": "patient",
"query_health_metrics": "health",
"query_lab_reports": "health",
// v17: 医生/科室
"query_doctor_list": "doctor",
"query_doctor_detail": "doctor",
"query_department_list": "department",
// v17: 慢病管理
"query_chronic_records": "chronic",
"create_chronic_record": "chronic",
"query_renewal_requests": "chronic",
"create_renewal_request": "chronic",
// v17: 健康指标写入
"record_health_metric": "health",
// v17: 排班管理
"query_doctor_schedule": "schedule",
"create_doctor_schedule": "schedule",
// v17: 支付订单
"query_order_list": "payment",
"query_order_detail": "payment",
// v17: 医生收入
"query_income_stats": "income",
"query_income_records": "income",
// v17: 管理统计
"query_dashboard_stats": "dashboard",
"query_dashboard_trend": "dashboard",
"query_user_list": "user_manage",
"query_system_logs": "system",
}
for name, tool := range r.All() {
......@@ -252,6 +349,21 @@ func buildToolKeywords(name, description, category string) string {
"通知", "提醒", "随访", "复诊", "安全", "禁忌",
"相互作用", "剂量", "用量", "计算", "表达式",
"患者", "医生", "管理", "查询",
// v17: 新增业务关键词
"问诊", "就诊", "接诊", "挂号", "预约", "主诉",
"等候", "队列", "排队",
"档案", "信息", "资料", "健康",
"指标", "血压", "血糖", "心率", "体温",
"化验", "解读", "目录", "库存",
// v17: 第2批业务关键词
"慢病", "慢性", "续方", "续药", "审批",
"排班", "时段", "预约",
"科室", "专长", "评分", "在线",
// v17: 第3批业务关键词
"订单", "支付", "付款", "退款", "收入", "余额",
"提现", "账单", "收益", "分成",
"仪表盘", "统计", "趋势", "运营",
"用户", "日志", "操作", "系统",
} {
if strings.Contains(description, kw) {
descKeywords[kw] = true
......@@ -299,6 +411,45 @@ func initToolSelector(r *agent.ToolRegistry) {
"navigate_page": "navigation",
// v15: 热代码生成
"generate_tool": "code_gen",
// v17: 问诊管理
"query_consultation_list": "consult",
"query_consultation_detail": "consult",
"create_consultation": "consult",
"accept_consultation": "consult",
"query_waiting_queue": "consult",
// v17: 处方管理
"query_prescription_list": "prescription",
"query_prescription_detail": "prescription",
"search_medicine_catalog": "pharmacy",
// v17: 患者信息
"query_patient_profile": "patient",
"query_health_metrics": "health",
"query_lab_reports": "health",
// v17: 医生/科室
"query_doctor_list": "doctor",
"query_doctor_detail": "doctor",
"query_department_list": "department",
// v17: 慢病管理
"query_chronic_records": "chronic",
"create_chronic_record": "chronic",
"query_renewal_requests": "chronic",
"create_renewal_request": "chronic",
// v17: 健康指标写入
"record_health_metric": "health",
// v17: 排班管理
"query_doctor_schedule": "schedule",
"create_doctor_schedule": "schedule",
// v17: 支付订单
"query_order_list": "payment",
"query_order_detail": "payment",
// v17: 医生收入
"query_income_stats": "income",
"query_income_records": "income",
// v17: 管理统计
"query_dashboard_stats": "dashboard",
"query_dashboard_trend": "dashboard",
"query_user_list": "user_manage",
"query_system_logs": "system",
}
for name, tool := range r.All() {
......
......@@ -7,7 +7,12 @@ import (
"internet-hospital/pkg/database"
)
// currentPromptVersion 当前代码中提示词模板的版本号
// 每次修改提示词内容时递增此值,ensurePromptTemplates 会自动同步到数据库
const currentPromptVersion = 2
// ensurePromptTemplates 确保所有内置提示词模板存在于数据库中(种子数据)
// 逻辑:不存在则创建;已存在但版本低于代码版本则更新内容
func ensurePromptTemplates() {
db := database.GetDB()
if db == nil {
......@@ -15,7 +20,7 @@ func ensurePromptTemplates() {
}
templates := []model.PromptTemplate{
// 患者智能助手系统提示词
// ==================== 患者智能助手系统提示词 ====================
{
TemplateKey: "patient_universal_agent_system",
Name: "患者智能助手-系统提示词",
......@@ -26,10 +31,28 @@ func ensurePromptTemplates() {
你的核心能力:
1. **预问诊**:通过友好对话收集症状信息(持续时间、严重程度、伴随症状),利用知识库分析症状,推荐合适的就诊科室
2. **找医生/挂号**:根据患者症状推荐科室和医生,帮助了解就医流程
2. **找医生/挂号**:根据患者症状推荐科室和医生,查看医生详情和排班,帮助了解就医流程
3. **健康咨询**:搜索医学知识提供健康科普,查询药品信息和用药指导
4. **随访管理**:查询处方和用药情况,提醒按时用药,评估病情变化,生成随访计划
5. **药品查询**:查询药品信息、规格、用法和注意事项
6. **问诊管理**:查询问诊列表和详情,帮助患者创建新的问诊(需指定医生和主诉)
7. **处方查询**:查询处方列表和详情(药品明细、用法用量、费用)
8. **健康档案**:查询个���健康档案、健康指标(血压/血糖/心率/体温)趋势、检验报告及AI解读
9. **慢病管理**:查询慢病档案、创建慢病记录、申请续方、查看续方状态
10. **支付订单**:查询支付订单列表和详情,了解订单状态
工具使用指南:
- 当患者想看病时,先用 recommend_department 推荐科室,再用 navigate_page 导航到找医生页面
- 当患者想找医生时,用 query_doctor_list 搜索,用 query_doctor_detail 查详情和排班
- 当患者问科室时,用 query_department_list 查科室列表
- 当患者问"我的问诊"时,使用 query_consultation_list 查询
- 当患者问"我的处方/药"时,使用 query_prescription_list 查询
- 当患者问"我的健康数据"时,使用 query_health_metrics 查询
- 当患者问"我的订单/支付"时,用 query_order_list 查询,用 query_order_detail 查详情
- 当患者问慢病/续方时,用 query_chronic_records 和 query_renewal_requests 查询
- 当患者要记录血压/���糖等指标时,用 record_health_metric 记录
- 当患者查排班时,用 query_doctor_schedule 查询
- 创建问诊前需确认患者提供了医生ID和主诉信息
使用原则:
- 用通俗易懂、温和专业的中文与患者交流
......@@ -38,16 +61,16 @@ func ensurePromptTemplates() {
- 关注患者的用药依从性和健康状况变化
- 所有医疗建议仅供参考,请以专业医生判断为准
导航能力:
��导航能力:
- 你可以使用 navigate_page 工具为用户准备页面导航
- 【重要】调用工具后,页面不会自动打开,用户需要点击工具结果中的"打开页面"按钮才能跳转
- 因此你的回复应该说"我已为您准备好XXX页面,请点击下方按钮打开",而不是"已为您打开XXX页面"
- 你只能导航到 patient_* 开头的页面,不能访问管理端或医生端页面
- 在回复中,你也可以使用 ACTIONS 标记提供导航按钮,格式:<!--ACTIONS:[{"type":"navigate","label":"页面名称","path":"/路径"}]-->`,
Status: "active",
Version: 1,
Version: currentPromptVersion,
},
// 医生智能助手系统提示词
// ==================== 医生智能助手系统提示词 ====================
{
TemplateKey: "doctor_universal_agent_system",
Name: "医生智能助手-系统提示词",
......@@ -63,10 +86,26 @@ func ensurePromptTemplates() {
4. **病历生成**:根据对话记录生成标准门诊病历(主诉、现病史、既往史、查体、辅助检查、初步诊断、处置意见)
5. **随访计划**:制定随访方案,包含复诊时间、复查项目、用药提醒、生活方式建议
6. **医嘱生成**:生成结构化医嘱(检查、治疗、护理、饮食、活动)
7. **问诊管理**:查看等候队列、问诊列表和详情,快速接诊
8. **处方管理**:查看处方列表和详情,搜索药品目录(名称、规格、库存、价格)
9. **患者档案**:查询患者健康档案、健康指标趋势、检验报告及AI解读
10. **排班管理**:查看和创建排班时段
11. **续方审批**:查看患者续方申请,通过导航页面进行审批
12. **收入管理**:查看收入统计(余额、本月收入、问诊量)和收入明细
诊断流程:首先查询患者病历了解病史 → 使用知识库检索诊断标准 → 综合分析后给出建议
处方审核流程:检查药物相互作用 → 检查禁忌症 → 验证剂量 → 综合评估
工具使用指南:
- 查看等候队列用 query_waiting_queue,接诊用 accept_consultation
- 查问诊列表用 query_consultation_list,查详情用 query_consultation_detail
- 查患者信息用 query_patient_profile + query_health_metrics + query_lab_reports
- 搜索药品用 search_medicine_catalog,开方通过 navigate_page 导航到开方页面
- 查看处方用 query_prescription_list / query_prescription_detail
- 查看/创建排班用 query_doctor_schedule / create_doctor_schedule
- 查看收入统计用 query_income_stats,查收入明细用 query_income_records
- 查续方申请用 query_renewal_requests,审批通过 navigate_page 导航到审批页面
使用原则:
- 基于循证医学原则提供建议
- 主动使用工具获取真实数据
......@@ -80,9 +119,9 @@ func ensurePromptTemplates() {
- 你只能导航到 doctor_* 开头的页面,不能访问管理端或患者端页面
- 在回复中,你也可以使用 ACTIONS 标记提供导航按钮,格式:<!--ACTIONS:[{"type":"navigate","label":"页面名称","path":"/路径"}]-->`,
Status: "active",
Version: 1,
Version: currentPromptVersion,
},
// 管理员智能助手系统提示词
// ==================== 管理员智能助手系统提示词 ====================
{
TemplateKey: "admin_universal_agent_system",
Name: "管理员智能助手-系统提示词",
......@@ -92,13 +131,24 @@ func ensurePromptTemplates() {
Content: `你是互联网医院管理后台的专属AI智能助手,帮助管理员高效管理平台。
你的核心能力:
1. **运营数据**:查询和计算运营指标,分析平台运行状况
1. **运营数据**:查询仪表盘统计(用户数、医生数、问诊量、收入)和运营趋势
2. **Agent监控**:调用其他Agent获取信息,监控Agent运行状态
3. **工作流管理**:触发和查询工作流执行状态
4. **知识库管理**:浏览知识库集合,了解知识库使用情况
5. **人工审核**:发起和管理人工审核任务
6. **通知管理**:发送系统通知
7. **药品/医学查询**:查询药品信息和医学知识辅助决策
8. **业务数据查询**:查看问诊记录、处方记录、支付订单,支持全局查看
9. **用户管理**:查询系统用户列表,按角色/状态/关键词搜索
10. **系统日志**:查看系统操作日志,按操作类型和资源过滤
工具使用指南:
- 查运营数据用 query_dashboard_stats,查趋势用 query_dashboard_trend
- 查用户列表用 query_user_list,查系统日志用 query_system_logs
- 查问诊数据用 query_consultation_list / query_consultation_detail
- 查处方数据用 query_prescription_list / query_prescription_detail
- 查订单数据用 query_order_list / query_order_detail
- 需要新的数据查询能力时,使用 generate_tool 动态生成 SQL 工具
使用原则:
- 以简洁专业的方式回答管理员的问题
......@@ -114,9 +164,9 @@ func ensurePromptTemplates() {
- 支持 open_add 操作准备新增弹窗(如新增医生、新增科室等)
- 在回复中,你也可以使用 ACTIONS 标记提供导航按钮,格式:<!--ACTIONS:[{"type":"navigate","label":"页面名称","path":"/路径"}]-->`,
Status: "active",
Version: 1,
Version: currentPromptVersion,
},
// ACTIONS按钮格式说明(通用附加提示词)
// ==================== ACTIONS按钮格式说明(通用附加提示词) ====================
{
TemplateKey: "actions_button_format",
Name: "建议操作按钮格式说明",
......@@ -144,9 +194,9 @@ func ensurePromptTemplates() {
- 导航路径必须是系统中存在的页面
`,
Status: "active",
Version: 1,
Version: currentPromptVersion,
},
// 预问诊对话提示词
// ==================== 预问诊对话提示词 ====================
{
TemplateKey: "pre_consult_chat",
Name: "预问诊对话提示词",
......@@ -163,9 +213,9 @@ func ensurePromptTemplates() {
6. 不做确定性诊断,用"建议"、"可能"等措辞
7. 如果患者情况紧急,明确建议立即就医`,
Status: "active",
Version: 1,
Version: currentPromptVersion,
},
// 预问诊分析提示词
// ==================== 预问诊分析提示词 ====================
{
TemplateKey: "pre_consult_analysis",
Name: "预问诊综合分析提示词",
......@@ -194,9 +244,9 @@ func ensurePromptTemplates() {
请确保分析专业准确,建议切实可行。`,
Status: "active",
Version: 1,
Version: currentPromptVersion,
},
// 鉴别诊断提示词(直接调用模型,不走智能体)
// ==================== 鉴别诊断提示词 ====================
{
TemplateKey: "consult_diagnosis",
Name: "鉴别诊断分析",
......@@ -232,9 +282,9 @@ func ensurePromptTemplates() {
**注意:以上分析仅供临床参考,最终诊断请结合实际检查结果。**`,
Status: "active",
Version: 1,
Version: currentPromptVersion,
},
// 用药建议提示词(直接调用模型,不走智能体)
// ==================== 用药建议提示词 ====================
{
TemplateKey: "consult_medication",
Name: "用药建议分析",
......@@ -270,9 +320,9 @@ func ensurePromptTemplates() {
**注意:以上用药建议仅供临床参考,请医生根据患者实际情况调整处方。**`,
Status: "active",
Version: 1,
Version: currentPromptVersion,
},
// 检验报告解读提示词
// ==================== 检验报告解读提示词 ====================
{
TemplateKey: "lab_report_interpret",
Name: "检验报告AI解读提示词",
......@@ -280,7 +330,7 @@ func ensurePromptTemplates() {
TemplateType: "system",
Content: `你是一位专业的医学检验报告解读专家。请对检验报告进行通俗易懂的解读,说明各项指标的含义和健康建议。请用中文回答,分条列出关键信息,避免使用过于专业的术语,让普通患者能够理解。`,
Status: "active",
Version: 1,
Version: currentPromptVersion,
},
}
......@@ -292,7 +342,19 @@ func ensurePromptTemplates() {
if err := db.Create(&tmpl).Error; err != nil {
log.Printf("[PromptTemplates] 创建提示词模板失败 %s: %v", tmpl.TemplateKey, err)
} else {
log.Printf("[PromptTemplates] 已创建提示词模板: %s", tmpl.TemplateKey)
log.Printf("[PromptTemplates] 已创建提示词模板: %s (v%d)", tmpl.TemplateKey, tmpl.Version)
}
} else if existing.Version < tmpl.Version {
// 已存在但版本低于代码版本 → 更新内容
updates := map[string]interface{}{
"content": tmpl.Content,
"version": tmpl.Version,
"name": tmpl.Name,
}
if err := db.Model(&existing).Updates(updates).Error; err != nil {
log.Printf("[PromptTemplates] 更新提示词模板失败 %s: %v", tmpl.TemplateKey, err)
} else {
log.Printf("[PromptTemplates] 已更新提示词模板: %s (v%d → v%d)", tmpl.TemplateKey, existing.Version, tmpl.Version)
}
}
}
......
......@@ -66,7 +66,7 @@ func buildAgentFromDef(def model.AgentDefinition) *agent.ReActAgent {
}
}
// 加载系统提示词:优先从 prompt_template_id 加载,否则使用 system_prompt 字段
// 加载系统提示词:优先从 prompt_template_id 加载,再按 agent_id 查找,最后使用 system_prompt 字段
systemPrompt := def.SystemPrompt
if def.PromptTemplateID != nil {
db := database.GetDB()
......@@ -78,6 +78,17 @@ func buildAgentFromDef(def model.AgentDefinition) *agent.ReActAgent {
}
}
}
// 如果仍为空,按 agent_id + template_type=system 从 prompt_templates 表加载
if systemPrompt == "" && def.AgentID != "" {
db := database.GetDB()
if db != nil {
var template model.PromptTemplate
if err := db.Where("agent_id = ? AND template_type = 'system' AND status = 'active'", def.AgentID).
First(&template).Error; err == nil {
systemPrompt = template.Content
}
}
}
// 加载技能包中的工具和提示词
var skillIDs []string
......@@ -159,6 +170,7 @@ func getOrchestrationSkills(def model.AgentDefinition) []model.AgentSkill {
}
// ensureBuiltinAgents 如果数据库中不存在内置Agent,则写入默认配置
// 如果已存在,同步工具列表(Tools字段)以确保新增工具生效
func (s *AgentService) ensureBuiltinAgents() {
db := database.GetDB()
if db == nil {
......@@ -166,15 +178,6 @@ func (s *AgentService) ensureBuiltinAgents() {
}
defaults := defaultAgentDefinitions()
for _, def := range defaults {
// 如果内存中已有(来自数据库),跳过
s.mu.RLock()
_, exists := s.agents[def.AgentID]
s.mu.RUnlock()
if exists {
continue
}
// 写入数据库
var existing model.AgentDefinition
if err := db.Where("agent_id = ?", def.AgentID).First(&existing).Error; err != nil {
// 不存在则创建
......@@ -183,7 +186,18 @@ func (s *AgentService) ensureBuiltinAgents() {
continue
}
existing = def
log.Printf("[AgentService] 已创建内置Agent: %s", def.AgentID)
} else if existing.Tools != def.Tools {
// 已存在但工具列表有变化 → 同步更新
if err := db.Model(&existing).Update("tools", def.Tools).Error; err != nil {
log.Printf("[AgentService] 同步Agent工具列表失败 %s: %v", def.AgentID, err)
} else {
log.Printf("[AgentService] 已同步Agent工具列表: %s", def.AgentID)
existing.Tools = def.Tools
}
}
// 如果内存中已有(来自 loadFromDB),用同步后的定义重建
s.mu.Lock()
s.agents[def.AgentID] = buildAgentFromDef(existing)
s.mu.Unlock()
......@@ -248,37 +262,41 @@ func (s *AgentService) Chat(ctx context.Context, agentID, userID, userRole, sess
db := database.GetDB()
// 加载或创建会话
if sessionID == "" {
sessionID = uuid.New().String()
}
// 加载会话(前端通过 POST /agent/sessions 预创建)
var session model.AgentSession
db.Where("session_id = ?", sessionID).First(&session)
if sessionID != "" {
db.Where("session_id = ?", sessionID).First(&session)
}
if session.ID == 0 {
if sessionID == "" {
sessionID = uuid.New().String()
}
session = model.AgentSession{
SessionID: sessionID, AgentID: agentID, UserID: userID,
History: "[]", Context: "null", PageContext: "null", EntityContext: "null",
Status: "active", UserRole: userRole,
}
db.Create(&session)
}
// 解析历史消息
var history []ai.ChatMessage
if session.History != "" {
if session.History != "" && session.History != "[]" {
json.Unmarshal([]byte(session.History), &history)
}
input := agent.AgentInput{
SessionID: sessionID,
UserID: userID,
UserRole: userRole,
Message: message,
Context: contextData,
History: history,
SessionID: sessionID, UserID: userID, UserRole: userRole,
Message: message, Context: contextData, History: history,
}
start := time.Now()
output, err := a.Run(ctx, input)
durationMs := int(time.Since(start).Milliseconds())
if err != nil {
return nil, err
}
// 更新历史
// 更新会话
history = append(history,
ai.ChatMessage{Role: "user", Content: message},
ai.ChatMessage{Role: "assistant", Content: output.Response},
......@@ -286,9 +304,8 @@ func (s *AgentService) Chat(ctx context.Context, agentID, userID, userRole, sess
historyJSON, _ := json.Marshal(history)
contextJSON, _ := json.Marshal(contextData)
// v16: 提取页面上下文
currentPage := ""
pageContextJSON := ""
pageContextJSON := "null"
if contextData != nil {
if page, ok := contextData["page"].(map[string]interface{}); ok {
if pathname, ok := page["pathname"].(string); ok {
......@@ -299,31 +316,17 @@ func (s *AgentService) Chat(ctx context.Context, agentID, userID, userRole, sess
}
}
if session.ID == 0 {
session = model.AgentSession{
SessionID: sessionID,
AgentID: agentID,
UserID: userID,
History: string(historyJSON),
Context: string(contextJSON),
Status: "active",
UserRole: userRole,
CurrentPage: currentPage,
PageContext: pageContextJSON,
MessageCount: 2,
TotalTokens: output.TotalTokens,
}
db.Create(&session)
} else {
db.Model(&session).Updates(map[string]interface{}{
"history": string(historyJSON),
"user_role": userRole,
"current_page": currentPage,
"page_context": pageContextJSON,
"message_count": session.MessageCount + 2,
"total_tokens": session.TotalTokens + output.TotalTokens,
"updated_at": time.Now(),
})
if err := db.Model(&session).Updates(map[string]interface{}{
"history": string(historyJSON),
"context": string(contextJSON),
"user_role": userRole,
"current_page": currentPage,
"page_context": pageContextJSON,
"message_count": session.MessageCount + 2,
"total_tokens": session.TotalTokens + output.TotalTokens,
"updated_at": time.Now(),
}).Error; err != nil {
log.Printf("[Chat] 更新会话失败: %v", err)
}
// 记录执行日志
......@@ -357,22 +360,87 @@ func (s *AgentService) ChatStream(ctx context.Context, agentID, userID, userRole
return
}
// v15: 检查是否有多Agent编排的技能
db := database.GetDB()
var agentDef model.AgentDefinition
if db != nil {
db.Where("agent_id = ?", agentID).First(&agentDef)
// 加载会话(前端通过 POST /agent/sessions 预创建)
var session model.AgentSession
if sessionID != "" {
db.Where("session_id = ?", sessionID).First(&session)
}
if session.ID == 0 {
// 兼容:前端未预创建时自动创建
if sessionID == "" {
sessionID = uuid.New().String()
}
session = model.AgentSession{
SessionID: sessionID,
AgentID: agentID,
UserID: userID,
History: "[]",
Context: "null",
PageContext: "null",
EntityContext: "null",
Status: "active",
UserRole: userRole,
}
if err := db.Create(&session).Error; err != nil {
log.Printf("[ChatStream] 自动创建会话失败: %v", err)
}
}
// 发送 session 事件
sessionJSON, _ := json.Marshal(map[string]string{"session_id": sessionID})
emit("session", string(sessionJSON))
// saveSession 统一的会话保存函数
saveSession := func(responseText string, tokens int) {
var history []ai.ChatMessage
if session.History != "" && session.History != "[]" {
json.Unmarshal([]byte(session.History), &history)
}
history = append(history,
ai.ChatMessage{Role: "user", Content: message},
ai.ChatMessage{Role: "assistant", Content: responseText},
)
historyJSON, _ := json.Marshal(history)
contextJSON, _ := json.Marshal(contextData)
currentPage := ""
pageContextJSON := "null"
if contextData != nil {
if page, ok := contextData["page"].(map[string]interface{}); ok {
if pathname, ok := page["pathname"].(string); ok {
currentPage = pathname
}
pageCtxBytes, _ := json.Marshal(page)
pageContextJSON = string(pageCtxBytes)
}
}
if err := db.Model(&session).Updates(map[string]interface{}{
"history": string(historyJSON),
"context": string(contextJSON),
"user_role": userRole,
"current_page": currentPage,
"page_context": pageContextJSON,
"message_count": session.MessageCount + 2,
"total_tokens": session.TotalTokens + tokens,
"updated_at": time.Now(),
}).Error; err != nil {
log.Printf("[ChatStream] 更新会话失败: %v", err)
} else {
log.Printf("[ChatStream] 会话保存成功: session_id=%s, messages=%d", sessionID, len(history))
}
}
// v15: 检查是否有多Agent编排的技能
var agentDef model.AgentDefinition
db.Where("agent_id = ?", agentID).First(&agentDef)
if orchSkills := getOrchestrationSkills(agentDef); len(orchSkills) > 0 {
skill := orchSkills[0] // 取第一个编排技能执行
skill := orchSkills[0]
executor := agent.GetSkillExecutor()
if executor != nil {
if sessionID == "" {
sessionID = uuid.New().String()
}
sessionJSON, _ := json.Marshal(map[string]string{"session_id": sessionID})
emit("session", string(sessionJSON))
thinkJSON, _ := json.Marshal(map[string]interface{}{"iteration": 1, "status": "orchestrating_skill", "skill": skill.Name})
emit("thinking", string(thinkJSON))
......@@ -387,7 +455,6 @@ func (s *AgentService) ChatStream(ctx context.Context, agentID, userID, userRole
log.Printf("[ChatStream] 技能编排失败 %s: %v", skill.SkillID, result.Error)
// 回退到普通 Agent 执行(不中断)
} else {
// 流式分块发送编排结果
chunkSize := 3
runes := []rune(result.FinalResponse)
for i := 0; i < len(runes); i += chunkSize {
......@@ -406,24 +473,15 @@ func (s *AgentService) ChatStream(ctx context.Context, agentID, userID, userRole
"mode": result.Mode,
})
emit("done", string(doneData))
saveSession(result.FinalResponse, 0)
return
}
}
}
// db already declared above for orchestration check; reuse it below
if sessionID == "" {
sessionID = uuid.New().String()
}
var session model.AgentSession
db.Where("session_id = ?", sessionID).First(&session)
// 发送 session 事件
sessionJSON, _ := json.Marshal(map[string]string{"session_id": sessionID})
emit("session", string(sessionJSON))
// 普通 Agent 执行
var history []ai.ChatMessage
if session.History != "" {
if session.History != "" && session.History != "[]" {
json.Unmarshal([]byte(session.History), &history)
}
......@@ -437,8 +495,6 @@ func (s *AgentService) ChatStream(ctx context.Context, agentID, userID, userRole
}
start := time.Now()
// 将 StreamEvent 转发为 SSE 事件
onEvent := func(ev agent.StreamEvent) error {
data, _ := json.Marshal(ev.Data)
emit(string(ev.Type), string(data))
......@@ -451,56 +507,12 @@ func (s *AgentService) ChatStream(ctx context.Context, agentID, userID, userRole
if err != nil {
errJSON, _ := json.Marshal(map[string]string{"error": err.Error()})
emit("error", string(errJSON))
return
}
// 持久化会话
history = append(history,
ai.ChatMessage{Role: "user", Content: message},
ai.ChatMessage{Role: "assistant", Content: output.Response},
)
historyJSON, _ := json.Marshal(history)
contextJSON, _ := json.Marshal(contextData)
// v16: 提取页面上下文(ChatStream)
currentPageStream := ""
pageContextJSONStream := ""
if contextData != nil {
if page, ok := contextData["page"].(map[string]interface{}); ok {
if pathname, ok := page["pathname"].(string); ok {
currentPageStream = pathname
}
pageCtxBytes, _ := json.Marshal(page)
pageContextJSONStream = string(pageCtxBytes)
if output == nil {
return
}
}
if session.ID == 0 {
session = model.AgentSession{
SessionID: sessionID,
AgentID: agentID,
UserID: userID,
History: string(historyJSON),
Context: string(contextJSON),
Status: "active",
UserRole: userRole,
CurrentPage: currentPageStream,
PageContext: pageContextJSONStream,
MessageCount: 2,
TotalTokens: output.TotalTokens,
}
db.Create(&session)
} else {
db.Model(&session).Updates(map[string]interface{}{
"history": string(historyJSON),
"user_role": userRole,
"current_page": currentPageStream,
"page_context": pageContextJSONStream,
"message_count": session.MessageCount + 2,
"total_tokens": session.TotalTokens + output.TotalTokens,
"updated_at": time.Now(),
})
}
saveSession(output.Response, output.TotalTokens)
// 记录执行日志
inputJSON, _ := json.Marshal(input)
......
......@@ -28,15 +28,15 @@ func (ChronicRecord) TableName() string { return "chronic_records" }
type RenewalRequest struct {
ID string `gorm:"type:uuid;primaryKey" json:"id"`
UserID string `gorm:"type:uuid;index;not null" json:"user_id"`
ChronicID string `gorm:"type:uuid;index" json:"chronic_id"`
ChronicID *string `gorm:"type:uuid;index" json:"chronic_id"`
DiseaseName string `gorm:"type:varchar(100)" json:"disease_name"`
Medicines string `gorm:"type:text" json:"medicines"` // JSON array
Reason string `gorm:"type:text" json:"reason"`
Status string `gorm:"type:varchar(20);default:'pending'" json:"status"` // pending|approved|rejected
DoctorID string `gorm:"type:uuid" json:"doctor_id"`
DoctorID *string `gorm:"type:uuid" json:"doctor_id"`
DoctorName string `gorm:"type:varchar(50)" json:"doctor_name"`
DoctorNote string `gorm:"type:text" json:"doctor_note"`
PrescriptionID string `gorm:"type:uuid" json:"prescription_id"`
PrescriptionID *string `gorm:"type:uuid" json:"prescription_id"`
AIAdvice string `gorm:"type:text" json:"ai_advice"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
......
......@@ -61,7 +61,7 @@ func (h *Handler) CreateRecord(c *gin.Context) {
}
r, err := h.service.CreateChronicRecord(c.Request.Context(), userID.(string), &req)
if err != nil {
response.Error(c, 500, "创建慢病档案失败")
response.Error(c, 500, "创建慢病档案失败: "+err.Error())
return
}
response.Success(c, r)
......@@ -110,7 +110,7 @@ func (h *Handler) CreateRenewal(c *gin.Context) {
}
r, err := h.service.CreateRenewal(c.Request.Context(), userID.(string), &req)
if err != nil {
response.Error(c, 500, "创建续方申请失败")
response.Error(c, 500, "创建续方申请失败: "+err.Error())
return
}
response.Success(c, r)
......
......@@ -93,9 +93,13 @@ func (s *Service) ListRenewals(ctx context.Context, userID string) ([]model.Rene
func (s *Service) CreateRenewal(ctx context.Context, userID string, req *RenewalReq) (*model.RenewalRequest, error) {
medsJSON, _ := json.Marshal(req.Medicines)
var chronicID *string
if req.ChronicID != "" {
chronicID = &req.ChronicID
}
r := &model.RenewalRequest{
ID: uuid.New().String(), UserID: userID,
ChronicID: req.ChronicID, DiseaseName: req.DiseaseName,
ChronicID: chronicID, DiseaseName: req.DiseaseName,
Medicines: string(medsJSON), Reason: req.Reason, Status: "pending",
}
if err := s.db.WithContext(ctx).Create(r).Error; err != nil {
......
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
)
// CreateChronicRecordFn 创建慢病记录回调,由 main.go 注入
var CreateChronicRecordFn func(ctx context.Context, userID, diseaseName, hospital, doctorName, currentMeds, notes string) (string, error)
// CreateChronicRecordTool 创建慢病档案
type CreateChronicRecordTool struct{}
func (t *CreateChronicRecordTool) Name() string { return "create_chronic_record" }
func (t *CreateChronicRecordTool) Description() string {
return "患者创建慢病档案记录,记录慢性疾病的诊断信息和当前用药情况"
}
func (t *CreateChronicRecordTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "disease_name", Type: "string", Description: "疾病名称(如高血压、糖尿病)", Required: true},
{Name: "hospital", Type: "string", Description: "确诊医院(可选)", Required: false},
{Name: "doctor_name", Type: "string", Description: "确诊医生(可选)", Required: false},
{Name: "current_meds", Type: "string", Description: "当前用药情况(可选)", Required: false},
{Name: "notes", Type: "string", Description: "备注(可选)", Required: false},
}
}
func (t *CreateChronicRecordTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
if userRole != "patient" {
return nil, fmt.Errorf("仅患者可创建慢病记录")
}
if userID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
diseaseName, ok := params["disease_name"].(string)
if !ok || diseaseName == "" {
return nil, fmt.Errorf("disease_name 必填")
}
hospital, _ := params["hospital"].(string)
doctorName, _ := params["doctor_name"].(string)
currentMeds, _ := params["current_meds"].(string)
notes, _ := params["notes"].(string)
if CreateChronicRecordFn == nil {
return nil, fmt.Errorf("慢病服务未初始化")
}
recordID, err := CreateChronicRecordFn(ctx, userID, diseaseName, hospital, doctorName, currentMeds, notes)
if err != nil {
return nil, fmt.Errorf("创建慢病记录失败: %v", err)
}
return map[string]interface{}{
"record_id": recordID,
"disease_name": diseaseName,
"message": "慢病档案已创建",
}, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// ChronicRecordsTool 查询慢病记录
type ChronicRecordsTool struct {
DB *gorm.DB
}
func (t *ChronicRecordsTool) Name() string { return "query_chronic_records" }
func (t *ChronicRecordsTool) Description() string {
return "查询患者的慢病档案记录(疾病名称、确诊日期、当前用药、控制状态)"
}
func (t *ChronicRecordsTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "patient_id", Type: "string", Description: "患者ID(医生使用,患者无需传入)", Required: false},
}
}
func (t *ChronicRecordsTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
targetID := userID
if pid, ok := params["patient_id"].(string); ok && pid != "" {
if userRole == "patient" && pid != userID {
return nil, fmt.Errorf("患者只能查看自己的慢病记录")
}
targetID = pid
}
if targetID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
var results []map[string]interface{}
rows, err := t.DB.WithContext(ctx).Raw(`
SELECT id, disease_name, diagnosis_date, hospital, doctor_name,
current_meds, control_status, notes, created_at
FROM chronic_records
WHERE user_id = ? AND deleted_at IS NULL
ORDER BY created_at DESC`, targetID).Rows()
if err != nil {
return map[string]interface{}{"records": []interface{}{}, "total": 0}, nil
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
row := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
row[col] = vals[i]
}
results = append(results, row)
}
}
return map[string]interface{}{
"records": results,
"total": len(results),
}, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
)
// AcceptConsultFn 接诊回调,由 WireCallbacks 注入
var AcceptConsultFn func(ctx context.Context, consultID, doctorUserID string) error
// AcceptConsultationTool 医生接诊
type AcceptConsultationTool struct{}
func (t *AcceptConsultationTool) Name() string { return "accept_consultation" }
func (t *AcceptConsultationTool) Description() string {
return "医生接受一个等候中的问诊,将问诊状态从pending变为in_progress"
}
func (t *AcceptConsultationTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "consultation_id", Type: "string", Description: "要接诊的问诊ID", Required: true},
}
}
func (t *AcceptConsultationTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
if userRole != "doctor" {
return nil, fmt.Errorf("仅医生可接诊")
}
consultID, ok := params["consultation_id"].(string)
if !ok || consultID == "" {
return nil, fmt.Errorf("consultation_id 必填")
}
if AcceptConsultFn == nil {
return nil, fmt.Errorf("接诊服务未初始化")
}
if err := AcceptConsultFn(ctx, consultID, userID); err != nil {
return nil, fmt.Errorf("接诊失败: %v", err)
}
return map[string]interface{}{
"consultation_id": consultID,
"status": "in_progress",
"message": "接诊成功,问诊已开始",
}, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
)
// CreateConsultFn 创建问诊回调,由 WireCallbacks 注入
// 参数: ctx, patientID, doctorID, consultType, chiefComplaint, medicalHistory
// 返回: 问诊ID, 流水号, error
var CreateConsultFn func(ctx context.Context, patientID, doctorID, consultType, chiefComplaint, medicalHistory string) (string, string, error)
// CreateConsultationTool 创建问诊(患者发起)
type CreateConsultationTool struct{}
func (t *CreateConsultationTool) Name() string { return "create_consultation" }
func (t *CreateConsultationTool) Description() string {
return "患者创建新的问诊,需要指定医生ID、问诊类型和主诉。创建成功后返回问诊ID和流水号"
}
func (t *CreateConsultationTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "doctor_id", Type: "string", Description: "接诊医生ID(从 query_doctor_list 获取)", Required: true},
{Name: "type", Type: "string", Description: "问诊类型", Required: true, Enum: []string{"text", "video"}},
{Name: "chief_complaint", Type: "string", Description: "主诉(患者症状描述)", Required: true},
{Name: "medical_history", Type: "string", Description: "既往病史(可选)", Required: false},
}
}
func (t *CreateConsultationTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
if userRole != "patient" {
return nil, fmt.Errorf("仅患者可创建问诊")
}
if userID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
doctorID, ok := params["doctor_id"].(string)
if !ok || doctorID == "" {
return nil, fmt.Errorf("doctor_id 必填")
}
consultType, ok := params["type"].(string)
if !ok || (consultType != "text" && consultType != "video") {
return nil, fmt.Errorf("type 必须为 text 或 video")
}
chiefComplaint, ok := params["chief_complaint"].(string)
if !ok || chiefComplaint == "" {
return nil, fmt.Errorf("chief_complaint 必填")
}
medicalHistory, _ := params["medical_history"].(string)
if CreateConsultFn == nil {
return nil, fmt.Errorf("问诊服务未初始化")
}
consultID, serialNumber, err := CreateConsultFn(ctx, userID, doctorID, consultType, chiefComplaint, medicalHistory)
if err != nil {
return nil, fmt.Errorf("创建问诊失败: %v", err)
}
return map[string]interface{}{
"consultation_id": consultID,
"serial_number": serialNumber,
"status": "pending",
"message": "问诊已创建,等待医生接诊",
}, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// ConsultationDetailTool 查询问诊详情
type ConsultationDetailTool struct {
DB *gorm.DB
}
func (t *ConsultationDetailTool) Name() string { return "query_consultation_detail" }
func (t *ConsultationDetailTool) Description() string {
return "查询问诊详情,包括患者信息、医生信息、主诉、诊断、消息记录,支持UUID或流水号(C开头)查询"
}
func (t *ConsultationDetailTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "consultation_id", Type: "string", Description: "问诊ID(UUID)或流水号(C开头,如C20260305-0001)", Required: true},
{Name: "include_messages", Type: "boolean", Description: "是否包含消息记录,默认true", Required: false},
}
}
func (t *ConsultationDetailTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
consultID, ok := params["consultation_id"].(string)
if !ok || consultID == "" {
return nil, fmt.Errorf("consultation_id 必填")
}
includeMessages := true
if v, ok := params["include_messages"].(bool); ok {
includeMessages = v
}
// 支持流水号查询
resolvedID := consultID
if len(consultID) > 0 && consultID[0] == 'C' {
var id string
if err := t.DB.WithContext(ctx).Raw("SELECT id FROM consultations WHERE serial_number = ?", consultID).Scan(&id).Error; err != nil || id == "" {
return nil, fmt.Errorf("流水号 %s 对应的问诊不存在", consultID)
}
resolvedID = id
}
// 查询问诊详情
var detail map[string]interface{}
detailRows, err := t.DB.WithContext(ctx).Raw(`
SELECT c.id, c.serial_number, c.patient_id, c.doctor_id, c.type, c.status,
c.chief_complaint, c.medical_history, c.diagnosis, c.summary,
c.started_at, c.ended_at, c.created_at, c.satisfaction_score,
d.name as doctor_name, d.title as doctor_title, dep.name as department_name,
u.real_name as patient_name, u.phone as patient_phone
FROM consultations c
LEFT JOIN doctors d ON c.doctor_id = d.id
LEFT JOIN departments dep ON d.department_id = dep.id
LEFT JOIN users u ON c.patient_id = u.id
WHERE c.id = ? AND c.deleted_at IS NULL`, resolvedID).Rows()
if err != nil {
return nil, fmt.Errorf("问诊不存在: %s", consultID)
}
defer detailRows.Close()
cols, _ := detailRows.Columns()
if !detailRows.Next() {
return nil, fmt.Errorf("问诊不存在: %s", consultID)
}
detail = make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := detailRows.Scan(ptrs...); err != nil {
return nil, fmt.Errorf("问诊不存在: %s", consultID)
}
for i, col := range cols {
detail[col] = vals[i]
}
// 权限校验:患者只能查自己的,医生只能查自己接诊的
if userRole == "patient" && detail["patient_id"] != userID {
return nil, fmt.Errorf("无权查看该问诊记录")
}
if userRole == "doctor" {
var doctorID string
t.DB.WithContext(ctx).Raw("SELECT id FROM doctors WHERE user_id = ?", userID).Scan(&doctorID)
if detail["doctor_id"] != doctorID {
return nil, fmt.Errorf("无权查看该问诊记录")
}
}
// 可选:查询消息记录
if includeMessages {
var messages []map[string]interface{}
msgRows, err := t.DB.WithContext(ctx).Raw(`
SELECT id, sender_type, content, content_type, created_at
FROM consult_messages
WHERE consult_id = ? AND deleted_at IS NULL
ORDER BY created_at ASC LIMIT 50`, resolvedID).Rows()
if err == nil {
defer msgRows.Close()
msgCols, _ := msgRows.Columns()
for msgRows.Next() {
msg := make(map[string]interface{})
mVals := make([]interface{}, len(msgCols))
mPtrs := make([]interface{}, len(msgCols))
for i := range mVals {
mPtrs[i] = &mVals[i]
}
if err := msgRows.Scan(mPtrs...); err == nil {
for i, col := range msgCols {
msg[col] = mVals[i]
}
messages = append(messages, msg)
}
}
}
detail["messages"] = messages
detail["message_count"] = len(messages)
}
return detail, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// ConsultationListTool 查询问诊列表
type ConsultationListTool struct {
DB *gorm.DB
}
func (t *ConsultationListTool) Name() string { return "query_consultation_list" }
func (t *ConsultationListTool) Description() string {
return "查询问诊列表:患者查自己的问诊记录,医生查自己接诊的问诊记录,支持按状态过滤"
}
func (t *ConsultationListTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "status", Type: "string", Description: "问诊状态过滤(可选):pending/in_progress/completed/cancelled", Required: false, Enum: []string{"pending", "in_progress", "completed", "cancelled"}},
{Name: "limit", Type: "number", Description: "返回记录数量,默认10,最大50", Required: false},
}
}
func (t *ConsultationListTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
if userID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
limit := 10
if v, ok := params["limit"].(float64); ok && v > 0 {
limit = int(v)
if limit > 50 {
limit = 50
}
}
status, _ := params["status"].(string)
// 根据角色决定查询条件
var roleField string
switch userRole {
case "doctor":
roleField = "doctor_id"
var doctorID string
if err := t.DB.WithContext(ctx).Raw("SELECT id FROM doctors WHERE user_id = ?", userID).Scan(&doctorID).Error; err != nil || doctorID == "" {
return map[string]interface{}{"consultations": []interface{}{}, "total": 0}, nil
}
userID = doctorID
case "admin":
roleField = ""
default:
roleField = "patient_id"
}
query := "SELECT c.id, c.serial_number, c.chief_complaint, c.status, c.type, c.created_at, c.started_at, c.ended_at, " +
"d.name as doctor_name, dep.name as department_name, u.real_name as patient_name " +
"FROM consultations c " +
"LEFT JOIN doctors d ON c.doctor_id = d.id " +
"LEFT JOIN departments dep ON d.department_id = dep.id " +
"LEFT JOIN users u ON c.patient_id = u.id " +
"WHERE c.deleted_at IS NULL"
args := []interface{}{}
if roleField != "" {
query += " AND c." + roleField + " = ?"
args = append(args, userID)
}
if status != "" {
query += " AND c.status = ?"
args = append(args, status)
}
query += " ORDER BY c.created_at DESC LIMIT ?"
args = append(args, limit)
var results []map[string]interface{}
rows, err := t.DB.WithContext(ctx).Raw(query, args...).Rows()
if err != nil {
return map[string]interface{}{"consultations": []interface{}{}, "total": 0}, nil
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
row := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
row[col] = vals[i]
}
results = append(results, row)
}
}
return map[string]interface{}{
"consultations": results,
"total": len(results),
"role": userRole,
}, nil
}
package tools
import (
"context"
"fmt"
"time"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// DashboardStatsTool 查询管理端仪表盘统计数据
type DashboardStatsTool struct {
DB *gorm.DB
}
func (t *DashboardStatsTool) Name() string { return "query_dashboard_stats" }
func (t *DashboardStatsTool) Description() string {
return "查询管理端仪表盘统计:总用户数、总医生数、总问诊数、今日问诊、待审核医生、今日/本月收入,仅管理员可用"
}
func (t *DashboardStatsTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{}
}
func (t *DashboardStatsTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
if userRole != "admin" {
return nil, fmt.Errorf("仅管理员可查看仪表盘数据")
}
today := time.Now().Format("2006-01-02")
monthStart := time.Now().Format("2006-01") + "-01"
stats := map[string]interface{}{}
var count int64
// 总用户数
t.DB.WithContext(ctx).Raw("SELECT COUNT(*) FROM users WHERE deleted_at IS NULL").Scan(&count)
stats["total_users"] = count
// 总医生数
t.DB.WithContext(ctx).Raw("SELECT COUNT(*) FROM users WHERE role = 'doctor' AND deleted_at IS NULL").Scan(&count)
stats["total_doctors"] = count
// 总问诊数
t.DB.WithContext(ctx).Raw("SELECT COUNT(*) FROM consultations WHERE deleted_at IS NULL").Scan(&count)
stats["total_consultations"] = count
// 今日问诊数
t.DB.WithContext(ctx).Raw("SELECT COUNT(*) FROM consultations WHERE DATE(created_at) = ? AND deleted_at IS NULL", today).Scan(&count)
stats["today_consultations"] = count
// 待审核医生数
t.DB.WithContext(ctx).Raw("SELECT COUNT(*) FROM doctor_reviews WHERE status = 'pending'").Scan(&count)
stats["pending_doctor_reviews"] = count
// 今日收入(已支付订单)
var todayRevenue int64
t.DB.WithContext(ctx).Raw(
"SELECT COALESCE(SUM(amount), 0) FROM payment_orders WHERE DATE(paid_at) = ? AND status = 'paid' AND deleted_at IS NULL",
today,
).Scan(&todayRevenue)
stats["revenue_today"] = todayRevenue
// 本月收入
var monthRevenue int64
t.DB.WithContext(ctx).Raw(
"SELECT COALESCE(SUM(amount), 0) FROM payment_orders WHERE paid_at >= ? AND status = 'paid' AND deleted_at IS NULL",
monthStart,
).Scan(&monthRevenue)
stats["revenue_month"] = monthRevenue
// 总处方数
t.DB.WithContext(ctx).Raw("SELECT COUNT(*) FROM prescriptions WHERE deleted_at IS NULL").Scan(&count)
stats["total_prescriptions"] = count
return stats, nil
}
package tools
import (
"context"
"fmt"
"time"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// DashboardTrendTool 查询管理端运营趋势数据
type DashboardTrendTool struct {
DB *gorm.DB
}
func (t *DashboardTrendTool) Name() string { return "query_dashboard_trend" }
func (t *DashboardTrendTool) Description() string {
return "查询最近7天运营趋势:每日问诊量、完成量、收入,仅管理员可用"
}
func (t *DashboardTrendTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "days", Type: "number", Description: "查询天数,默认7,最大30", Required: false},
}
}
func (t *DashboardTrendTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
if userRole != "admin" {
return nil, fmt.Errorf("仅管理员可查看运营趋势")
}
days := 7
if v, ok := params["days"].(float64); ok && v > 0 {
days = int(v)
if days > 30 {
days = 30
}
}
startDate := time.Now().AddDate(0, 0, -(days - 1)).Format("2006-01-02")
rows, err := t.DB.WithContext(ctx).Raw(`
SELECT
DATE(created_at)::text AS date,
COUNT(*) AS consult_count,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed_count
FROM consultations
WHERE DATE(created_at) >= ? AND deleted_at IS NULL
GROUP BY DATE(created_at)
ORDER BY date
`, startDate).Rows()
if err != nil {
return map[string]interface{}{"trend": []interface{}{}}, nil
}
defer rows.Close()
var results []map[string]interface{}
for rows.Next() {
var date string
var consultCount, completedCount int
if err := rows.Scan(&date, &consultCount, &completedCount); err == nil {
results = append(results, map[string]interface{}{
"date": date,
"consult_count": consultCount,
"completed_count": completedCount,
})
}
}
if results == nil {
results = []map[string]interface{}{}
}
return map[string]interface{}{
"trend": results,
"days": days,
}, nil
}
package tools
import (
"context"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// DepartmentListTool 查询科室列表
type DepartmentListTool struct {
DB *gorm.DB
}
func (t *DepartmentListTool) Name() string { return "query_department_list" }
func (t *DepartmentListTool) Description() string {
return "查询所有科室列表(含层级关系),为推荐科室后查询具体医生提供数据支持"
}
func (t *DepartmentListTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{}
}
func (t *DepartmentListTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
var results []map[string]interface{}
rows, err := t.DB.WithContext(ctx).Raw(`
SELECT id, name, icon, parent_id, sort_order
FROM departments
WHERE deleted_at IS NULL
ORDER BY sort_order ASC, name ASC`).Rows()
if err != nil {
return map[string]interface{}{"departments": []interface{}{}, "total": 0}, nil
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
row := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
row[col] = vals[i]
}
results = append(results, row)
}
}
return map[string]interface{}{
"departments": results,
"total": len(results),
}, nil
}
package tools
import (
"context"
"fmt"
"time"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// DoctorDetailTool 查询医生详情+排班
type DoctorDetailTool struct {
DB *gorm.DB
}
func (t *DoctorDetailTool) Name() string { return "query_doctor_detail" }
func (t *DoctorDetailTool) Description() string {
return "查询医生详情信息和近7天排班,包括简介、专长、评分、价格、可预约时段"
}
func (t *DoctorDetailTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "doctor_id", Type: "string", Description: "医生ID", Required: true},
}
}
func (t *DoctorDetailTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
doctorID, ok := params["doctor_id"].(string)
if !ok || doctorID == "" {
return nil, fmt.Errorf("doctor_id 必填")
}
// 查询医生详情
detailRows, err := t.DB.WithContext(ctx).Raw(`
SELECT d.id, d.name, d.title, d.hospital, d.specialties, d.introduction,
d.rating, d.consult_count, d.price, d.is_online, d.avatar,
dep.name as department_name
FROM doctors d
LEFT JOIN departments dep ON d.department_id = dep.id
WHERE d.id = ? AND d.deleted_at IS NULL`, doctorID).Rows()
if err != nil {
return nil, fmt.Errorf("医生不存在: %s", doctorID)
}
defer detailRows.Close()
cols, _ := detailRows.Columns()
if !detailRows.Next() {
return nil, fmt.Errorf("医生不存在: %s", doctorID)
}
detail := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := detailRows.Scan(ptrs...); err != nil {
return nil, fmt.Errorf("医生不存在: %s", doctorID)
}
for i, col := range cols {
detail[col] = vals[i]
}
// 查询近7天排班
today := time.Now().Format("2006-01-02")
endDate := time.Now().AddDate(0, 0, 7).Format("2006-01-02")
var schedules []map[string]interface{}
schedRows, err := t.DB.WithContext(ctx).Raw(`
SELECT id, date, start_time, end_time, max_count, remaining
FROM doctor_schedules
WHERE doctor_id = ? AND date >= ? AND date <= ?
ORDER BY date ASC, start_time ASC`, doctorID, today, endDate).Rows()
if err == nil {
defer schedRows.Close()
sCols, _ := schedRows.Columns()
for schedRows.Next() {
sched := make(map[string]interface{})
sVals := make([]interface{}, len(sCols))
sPtrs := make([]interface{}, len(sCols))
for i := range sVals {
sPtrs[i] = &sVals[i]
}
if err := schedRows.Scan(sPtrs...); err == nil {
for i, col := range sCols {
sched[col] = sVals[i]
}
schedules = append(schedules, sched)
}
}
}
detail["schedules"] = schedules
return detail, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// DoctorListTool 搜索医生列表
type DoctorListTool struct {
DB *gorm.DB
}
func (t *DoctorListTool) Name() string { return "query_doctor_list" }
func (t *DoctorListTool) Description() string {
return "搜索医生列表,支持按科室、关键词筛选,按评分/问诊量/价格排序,返回医生基本信息和在线状态"
}
func (t *DoctorListTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "department_id", Type: "string", Description: "科室ID过滤(可选)", Required: false},
{Name: "keyword", Type: "string", Description: "医生姓名关键词(可选)", Required: false},
{Name: "sort_by", Type: "string", Description: "排序方式,默认按评分", Required: false, Enum: []string{"rating", "consult_count", "price"}},
{Name: "limit", Type: "number", Description: "返回数量,默认10", Required: false},
}
}
func (t *DoctorListTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
if userRole != "patient" && userRole != "admin" {
return nil, fmt.Errorf("仅患者和管理员可搜索医生")
}
limit := 10
if v, ok := params["limit"].(float64); ok && v > 0 {
limit = int(v)
if limit > 30 {
limit = 30
}
}
departmentID, _ := params["department_id"].(string)
keyword, _ := params["keyword"].(string)
sortBy, _ := params["sort_by"].(string)
query := "SELECT d.id, d.name, d.title, d.hospital, d.specialties, d.rating, " +
"d.consult_count, d.price, d.is_online, d.avatar, " +
"dep.name as department_name " +
"FROM doctors d " +
"LEFT JOIN departments dep ON d.department_id = dep.id " +
"WHERE d.status = 'approved' AND d.deleted_at IS NULL"
args := []interface{}{}
if departmentID != "" {
query += " AND d.department_id = ?"
args = append(args, departmentID)
}
if keyword != "" {
query += " AND d.name ILIKE ?"
args = append(args, "%"+keyword+"%")
}
switch sortBy {
case "consult_count":
query += " ORDER BY d.consult_count DESC"
case "price":
query += " ORDER BY d.price ASC"
default:
query += " ORDER BY d.rating DESC"
}
query += " LIMIT ?"
args = append(args, limit)
var results []map[string]interface{}
rows, err := t.DB.WithContext(ctx).Raw(query, args...).Rows()
if err != nil {
return map[string]interface{}{"doctors": []interface{}{}, "total": 0}, nil
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
row := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
row[col] = vals[i]
}
results = append(results, row)
}
}
return map[string]interface{}{
"doctors": results,
"total": len(results),
}, nil
}
package tools
import (
"context"
"fmt"
"time"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// DoctorScheduleTool 查询医生排班
type DoctorScheduleTool struct {
DB *gorm.DB
}
func (t *DoctorScheduleTool) Name() string { return "query_doctor_schedule" }
func (t *DoctorScheduleTool) Description() string {
return "查询医生排班信息,患者用于预约,医生用于查看自己的排班"
}
func (t *DoctorScheduleTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "doctor_id", Type: "string", Description: "医生ID(患者必填,医生可不填查自己的)", Required: false},
{Name: "days", Type: "number", Description: "查询未来天数,默认7天", Required: false},
}
}
func (t *DoctorScheduleTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
doctorID, _ := params["doctor_id"].(string)
// 医生查自己排班时无需传 doctor_id
if doctorID == "" && userRole == "doctor" {
var id string
t.DB.WithContext(ctx).Raw("SELECT id FROM doctors WHERE user_id = ?", userID).Scan(&id)
doctorID = id
}
if doctorID == "" {
return nil, fmt.Errorf("doctor_id 必填")
}
days := 7
if v, ok := params["days"].(float64); ok && v > 0 {
days = int(v)
if days > 30 {
days = 30
}
}
today := time.Now().Format("2006-01-02")
endDate := time.Now().AddDate(0, 0, days).Format("2006-01-02")
var results []map[string]interface{}
rows, err := t.DB.WithContext(ctx).Raw(`
SELECT id, date, start_time, end_time, max_count, remaining
FROM doctor_schedules
WHERE doctor_id = ? AND date >= ? AND date <= ?
ORDER BY date ASC, start_time ASC`, doctorID, today, endDate).Rows()
if err != nil {
return map[string]interface{}{"schedules": []interface{}{}, "total": 0}, nil
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
row := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
row[col] = vals[i]
}
results = append(results, row)
}
}
return map[string]interface{}{
"schedules": results,
"total": len(results),
"doctor_id": doctorID,
}, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
)
// CreateScheduleFn 创建排班回调,由 main.go 注入
// 参数: ctx, doctorUserID, date, startTime, endTime, maxCount
var CreateScheduleFn func(ctx context.Context, doctorUserID, date, startTime, endTime string, maxCount int) error
// CreateDoctorScheduleTool 医生创建排班
type CreateDoctorScheduleTool struct{}
func (t *CreateDoctorScheduleTool) Name() string { return "create_doctor_schedule" }
func (t *CreateDoctorScheduleTool) Description() string {
return "医生创建排班时段,设置日期、时间段和最大接诊人数"
}
func (t *CreateDoctorScheduleTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "date", Type: "string", Description: "排班日期(格式:2006-01-02)", Required: true},
{Name: "start_time", Type: "string", Description: "开始时间(格式:09:00)", Required: true},
{Name: "end_time", Type: "string", Description: "结束时间(格式:12:00)", Required: true},
{Name: "max_count", Type: "number", Description: "最大接诊人数", Required: true},
}
}
func (t *CreateDoctorScheduleTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
if userRole != "doctor" {
return nil, fmt.Errorf("仅医生可创建排班")
}
if userID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
date, ok := params["date"].(string)
if !ok || date == "" {
return nil, fmt.Errorf("date 必填")
}
startTime, ok := params["start_time"].(string)
if !ok || startTime == "" {
return nil, fmt.Errorf("start_time 必填")
}
endTime, ok := params["end_time"].(string)
if !ok || endTime == "" {
return nil, fmt.Errorf("end_time 必填")
}
maxCount := 0
if v, ok := params["max_count"].(float64); ok {
maxCount = int(v)
}
if maxCount <= 0 {
return nil, fmt.Errorf("max_count 必须大于0")
}
if CreateScheduleFn == nil {
return nil, fmt.Errorf("排班服务未初始化")
}
if err := CreateScheduleFn(ctx, userID, date, startTime, endTime, maxCount); err != nil {
return nil, fmt.Errorf("创建排班失败: %v", err)
}
return map[string]interface{}{
"date": date,
"start_time": startTime,
"end_time": endTime,
"max_count": maxCount,
"message": "排班已创建",
}, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
)
// RecordHealthMetricFn 记录健康指标回调,由 main.go 注入
var RecordHealthMetricFn func(ctx context.Context, userID, metricType string, value1, value2 float64, unit, notes string) (string, error)
// RecordHealthMetricTool 记录健康指标
type RecordHealthMetricTool struct{}
func (t *RecordHealthMetricTool) Name() string { return "record_health_metric" }
func (t *RecordHealthMetricTool) Description() string {
return "患者记录健康指标(血压、血糖、心率、体温),异常值会自动触发健康告警"
}
func (t *RecordHealthMetricTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "metric_type", Type: "string", Description: "指标类型", Required: true, Enum: []string{"blood_pressure", "blood_glucose", "heart_rate", "body_temperature"}},
{Name: "value1", Type: "number", Description: "主值(血压为收缩压,血糖/心率/体温为测量值)", Required: true},
{Name: "value2", Type: "number", Description: "副值(血压为舒张压,其他类型可不填)", Required: false},
{Name: "unit", Type: "string", Description: "单位(可选,如mmHg、mmol/L、bpm、℃)", Required: false},
{Name: "notes", Type: "string", Description: "备注(可选,如饭前/饭后、运动后等)", Required: false},
}
}
func (t *RecordHealthMetricTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
if userRole != "patient" {
return nil, fmt.Errorf("仅患者可记录健康指标")
}
if userID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
metricType, ok := params["metric_type"].(string)
if !ok || metricType == "" {
return nil, fmt.Errorf("metric_type 必填")
}
value1, ok := params["value1"].(float64)
if !ok {
return nil, fmt.Errorf("value1 必填")
}
var value2 float64
if v, ok := params["value2"].(float64); ok {
value2 = v
}
unit, _ := params["unit"].(string)
notes, _ := params["notes"].(string)
// 默认单位
if unit == "" {
switch metricType {
case "blood_pressure":
unit = "mmHg"
case "blood_glucose":
unit = "mmol/L"
case "heart_rate":
unit = "bpm"
case "body_temperature":
unit = "℃"
}
}
if RecordHealthMetricFn == nil {
return nil, fmt.Errorf("健康指标服务未初始化")
}
metricID, err := RecordHealthMetricFn(ctx, userID, metricType, value1, value2, unit, notes)
if err != nil {
return nil, fmt.Errorf("记录健康指标失败: %v", err)
}
return map[string]interface{}{
"metric_id": metricID,
"metric_type": metricType,
"value1": value1,
"value2": value2,
"unit": unit,
"message": "健康指标已记录",
}, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// HealthMetricsTool 查询健康指标记录
type HealthMetricsTool struct {
DB *gorm.DB
}
func (t *HealthMetricsTool) Name() string { return "query_health_metrics" }
func (t *HealthMetricsTool) Description() string {
return "查询患者健康指标记录(血压、血糖、心率、体温),支持类型过滤,按时间倒序返回"
}
func (t *HealthMetricsTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "patient_id", Type: "string", Description: "患者ID(医生使用,患者无需传入)", Required: false},
{Name: "metric_type", Type: "string", Description: "指标类型过滤(可选)", Required: false, Enum: []string{"blood_pressure", "blood_glucose", "heart_rate", "body_temperature"}},
{Name: "limit", Type: "number", Description: "返回数量,默认20", Required: false},
}
}
func (t *HealthMetricsTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
targetID := userID
if pid, ok := params["patient_id"].(string); ok && pid != "" {
if userRole == "patient" && pid != userID {
return nil, fmt.Errorf("患者只能查看自己的健康指标")
}
targetID = pid
}
if targetID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
limit := 20
if v, ok := params["limit"].(float64); ok && v > 0 {
limit = int(v)
if limit > 100 {
limit = 100
}
}
metricType, _ := params["metric_type"].(string)
query := "SELECT id, metric_type, value1, value2, unit, notes, recorded_at, created_at " +
"FROM health_metrics WHERE user_id = ?"
args := []interface{}{targetID}
if metricType != "" {
query += " AND metric_type = ?"
args = append(args, metricType)
}
query += " ORDER BY recorded_at DESC LIMIT ?"
args = append(args, limit)
var results []map[string]interface{}
rows, err := t.DB.WithContext(ctx).Raw(query, args...).Rows()
if err != nil {
return map[string]interface{}{"metrics": []interface{}{}, "total": 0}, nil
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
row := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
row[col] = vals[i]
}
results = append(results, row)
}
}
return map[string]interface{}{
"metrics": results,
"total": len(results),
}, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// IncomeRecordsTool 查询医生收入明细
type IncomeRecordsTool struct {
DB *gorm.DB
}
func (t *IncomeRecordsTool) Name() string { return "query_income_records" }
func (t *IncomeRecordsTool) Description() string {
return "查询医生收入明细记录,包含收入类型、金额、患者姓名、状态,支持日期过滤"
}
func (t *IncomeRecordsTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "start_date", Type: "string", Description: "开始日期(可选,格式YYYY-MM-DD)", Required: false},
{Name: "end_date", Type: "string", Description: "结束日期(可选,格式YYYY-MM-DD)", Required: false},
{Name: "limit", Type: "number", Description: "返回数量,默认20,最大100", Required: false},
}
}
func (t *IncomeRecordsTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
if userID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
if userRole != "doctor" {
return nil, fmt.Errorf("仅医生可查看收入明细")
}
// 查询医生ID
var doctorID string
t.DB.WithContext(ctx).Raw("SELECT id FROM doctors WHERE user_id = ?", userID).Scan(&doctorID)
if doctorID == "" {
return nil, fmt.Errorf("医生信息不存在")
}
limit := 20
if v, ok := params["limit"].(float64); ok && v > 0 {
limit = int(v)
if limit > 100 {
limit = 100
}
}
startDate, _ := params["start_date"].(string)
endDate, _ := params["end_date"].(string)
query := "SELECT id, consult_id, income_type, amount, status, patient_name, settled_at, created_at " +
"FROM doctor_incomes WHERE doctor_id = ?"
args := []interface{}{doctorID}
if startDate != "" {
query += " AND created_at >= ?"
args = append(args, startDate)
}
if endDate != "" {
query += " AND created_at <= ?"
args = append(args, endDate+" 23:59:59")
}
query += " ORDER BY created_at DESC LIMIT ?"
args = append(args, limit)
var results []map[string]interface{}
rows, err := t.DB.WithContext(ctx).Raw(query, args...).Rows()
if err != nil {
return map[string]interface{}{"records": []interface{}{}, "total": 0}, nil
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
row := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
row[col] = vals[i]
}
results = append(results, row)
}
}
return map[string]interface{}{
"records": results,
"total": len(results),
}, nil
}
package tools
import (
"context"
"fmt"
"time"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// IncomeStatsTool 查询医生收入统计
type IncomeStatsTool struct {
DB *gorm.DB
}
func (t *IncomeStatsTool) Name() string { return "query_income_stats" }
func (t *IncomeStatsTool) Description() string {
return "查询医生收入统计:可提现余额、本月收入、本月问诊量,仅医生可用"
}
func (t *IncomeStatsTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{}
}
func (t *IncomeStatsTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
if userID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
if userRole != "doctor" {
return nil, fmt.Errorf("仅医生可查看收入统计")
}
// 查询医生ID
var doctorID string
t.DB.WithContext(ctx).Raw("SELECT id FROM doctors WHERE user_id = ?", userID).Scan(&doctorID)
if doctorID == "" {
return nil, fmt.Errorf("医生信息不存在")
}
var totalBalance int64
var monthIncome int64
var monthConsults int64
// 可提现余额(已结算未提现)
t.DB.WithContext(ctx).Raw(
"SELECT COALESCE(SUM(amount), 0) FROM doctor_incomes WHERE doctor_id = ? AND status = 'settled'",
doctorID,
).Scan(&totalBalance)
// 本月收入
startOfMonth := time.Now().Format("2006-01") + "-01"
t.DB.WithContext(ctx).Raw(
"SELECT COALESCE(SUM(amount), 0) FROM doctor_incomes WHERE doctor_id = ? AND created_at >= ?",
doctorID, startOfMonth,
).Scan(&monthIncome)
// 本月问诊量
t.DB.WithContext(ctx).Raw(
"SELECT COUNT(*) FROM doctor_incomes WHERE doctor_id = ? AND created_at >= ?",
doctorID, startOfMonth,
).Scan(&monthConsults)
return map[string]interface{}{
"total_balance": totalBalance,
"month_income": monthIncome,
"month_consults": monthConsults,
}, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// LabReportsTool 查询检验报告
type LabReportsTool struct {
DB *gorm.DB
}
func (t *LabReportsTool) Name() string { return "query_lab_reports" }
func (t *LabReportsTool) Description() string {
return "查询患者的检验报告列表,包括AI解读结果"
}
func (t *LabReportsTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "patient_id", Type: "string", Description: "患者ID(医生使用,患者无需传入)", Required: false},
{Name: "limit", Type: "number", Description: "返回数量,默认10", Required: false},
}
}
func (t *LabReportsTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
targetID := userID
if pid, ok := params["patient_id"].(string); ok && pid != "" {
if userRole == "patient" && pid != userID {
return nil, fmt.Errorf("患者只能查看自己的检验报告")
}
targetID = pid
}
if targetID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
limit := 10
if v, ok := params["limit"].(float64); ok && v > 0 {
limit = int(v)
if limit > 30 {
limit = 30
}
}
var results []map[string]interface{}
rows, err := t.DB.WithContext(ctx).Raw(`
SELECT id, title, report_date, file_url, file_type, category,
ai_interpret, created_at
FROM lab_reports
WHERE user_id = ? AND deleted_at IS NULL
ORDER BY report_date DESC, created_at DESC LIMIT ?`, targetID, limit).Rows()
if err != nil {
return map[string]interface{}{"reports": []interface{}{}, "total": 0}, nil
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
row := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
row[col] = vals[i]
}
results = append(results, row)
}
}
return map[string]interface{}{
"reports": results,
"total": len(results),
}, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// MedicineCatalogTool 搜索药品目录
type MedicineCatalogTool struct {
DB *gorm.DB
}
func (t *MedicineCatalogTool) Name() string { return "search_medicine_catalog" }
func (t *MedicineCatalogTool) Description() string {
return "搜索药品目录,查询药品名称、规格、库存、价格等信息,为开方提供数据支持"
}
func (t *MedicineCatalogTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "keyword", Type: "string", Description: "药品名称或关键词", Required: true},
{Name: "category", Type: "string", Description: "药品分类过滤(可选)", Required: false},
{Name: "limit", Type: "number", Description: "返回数量,默认10", Required: false},
}
}
func (t *MedicineCatalogTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
if userRole != "doctor" && userRole != "admin" {
return nil, fmt.Errorf("仅医生和管理员可搜索药品目录")
}
keyword, ok := params["keyword"].(string)
if !ok || keyword == "" {
return nil, fmt.Errorf("keyword 必填")
}
limit := 10
if v, ok := params["limit"].(float64); ok && v > 0 {
limit = int(v)
if limit > 30 {
limit = 30
}
}
category, _ := params["category"].(string)
query := "SELECT id, name, generic_name, specification, manufacturer, " +
"unit, price, stock, category, status, usage_method, contraindication " +
"FROM medicines " +
"WHERE (name ILIKE ? OR generic_name ILIKE ?) AND status = 'active'"
searchPattern := "%" + keyword + "%"
args := []interface{}{searchPattern, searchPattern}
if category != "" {
query += " AND category = ?"
args = append(args, category)
}
query += " ORDER BY name ASC LIMIT ?"
args = append(args, limit)
var results []map[string]interface{}
rows, err := t.DB.WithContext(ctx).Raw(query, args...).Rows()
if err != nil {
return map[string]interface{}{"medicines": []interface{}{}, "total": 0}, nil
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
row := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
row[col] = vals[i]
}
results = append(results, row)
}
}
return map[string]interface{}{
"medicines": results,
"total": len(results),
}, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// OrderDetailTool 查询支付订单详情
type OrderDetailTool struct {
DB *gorm.DB
}
func (t *OrderDetailTool) Name() string { return "query_order_detail" }
func (t *OrderDetailTool) Description() string {
return "查询支付订单详情,包含订单号、金额、状态、支付方式、关联业务等完整信息"
}
func (t *OrderDetailTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "order_id", Type: "string", Description: "订单ID", Required: true},
}
}
func (t *OrderDetailTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
if userID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
orderID, _ := params["order_id"].(string)
if orderID == "" {
return nil, fmt.Errorf("请提供订单ID")
}
query := "SELECT id, order_no, user_id, order_type, related_id, amount, status, " +
"payment_method, transaction_id, paid_at, refunded_at, expire_at, remark, created_at " +
"FROM payment_orders WHERE id = ? AND deleted_at IS NULL"
args := []interface{}{orderID}
// 患者只能查自己的订单
if userRole == "patient" {
query += " AND user_id = ?"
args = append(args, userID)
}
var result map[string]interface{}
row := t.DB.WithContext(ctx).Raw(query, args...)
rows, err := row.Rows()
if err != nil {
return nil, fmt.Errorf("查询订单失败")
}
defer rows.Close()
cols, _ := rows.Columns()
if rows.Next() {
result = make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
result[col] = vals[i]
}
}
}
if result == nil {
return nil, fmt.Errorf("订单不存在")
}
return result, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// OrderListTool 查询支付订单列表
type OrderListTool struct {
DB *gorm.DB
}
func (t *OrderListTool) Name() string { return "query_order_list" }
func (t *OrderListTool) Description() string {
return "查询支付订单列表:患者查自己的订单,管理员查所有订单,支持按状态和类型过滤"
}
func (t *OrderListTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "status", Type: "string", Description: "订单状态过滤(可选)", Required: false, Enum: []string{"pending", "paid", "refunded", "cancelled", "completed"}},
{Name: "order_type", Type: "string", Description: "订单类型过滤(可选)", Required: false, Enum: []string{"consult", "prescription", "pharmacy"}},
{Name: "limit", Type: "number", Description: "返回数量,默认10,最大50", Required: false},
}
}
func (t *OrderListTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
if userID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
limit := 10
if v, ok := params["limit"].(float64); ok && v > 0 {
limit = int(v)
if limit > 50 {
limit = 50
}
}
status, _ := params["status"].(string)
orderType, _ := params["order_type"].(string)
query := "SELECT id, order_no, order_type, related_id, amount, status, payment_method, paid_at, created_at " +
"FROM payment_orders WHERE deleted_at IS NULL"
args := []interface{}{}
switch userRole {
case "patient":
query += " AND user_id = ?"
args = append(args, userID)
case "admin":
// 管理员查所有
default:
return nil, fmt.Errorf("无权限查询订单")
}
if status != "" {
query += " AND status = ?"
args = append(args, status)
}
if orderType != "" {
query += " AND order_type = ?"
args = append(args, orderType)
}
query += " ORDER BY created_at DESC LIMIT ?"
args = append(args, limit)
var results []map[string]interface{}
rows, err := t.DB.WithContext(ctx).Raw(query, args...).Rows()
if err != nil {
return map[string]interface{}{"orders": []interface{}{}, "total": 0}, nil
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
row := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
row[col] = vals[i]
}
results = append(results, row)
}
}
return map[string]interface{}{
"orders": results,
"total": len(results),
}, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// PatientProfileTool 查询患者健康档案
type PatientProfileTool struct {
DB *gorm.DB
}
func (t *PatientProfileTool) Name() string { return "query_patient_profile" }
func (t *PatientProfileTool) Description() string {
return "查询患者健康档案(基本信息、过敏史、病史、保险信息),患者查自己的,医生按patient_id查"
}
func (t *PatientProfileTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "patient_id", Type: "string", Description: "患者ID(医生/管理员使用,患者无需传入自动查自己的)", Required: false},
}
}
func (t *PatientProfileTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
targetID := userID
if pid, ok := params["patient_id"].(string); ok && pid != "" {
if userRole == "patient" && pid != userID {
return nil, fmt.Errorf("患者只能查看自己的健康档案")
}
targetID = pid
}
if targetID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
// 查询用户基本信息
userInfo := make(map[string]interface{})
uRows, err := t.DB.WithContext(ctx).Raw(`
SELECT id, real_name, phone, gender, age, avatar, role, status, created_at
FROM users WHERE id = ?`, targetID).Rows()
if err != nil {
return nil, fmt.Errorf("用户不存在")
}
defer uRows.Close()
if uRows.Next() {
uCols, _ := uRows.Columns()
uVals := make([]interface{}, len(uCols))
uPtrs := make([]interface{}, len(uCols))
for i := range uVals {
uPtrs[i] = &uVals[i]
}
if err := uRows.Scan(uPtrs...); err == nil {
for i, col := range uCols {
userInfo[col] = uVals[i]
}
}
} else {
return nil, fmt.Errorf("用户不存在")
}
// 查询健康档案
profile := make(map[string]interface{})
pRows, err := t.DB.WithContext(ctx).Raw(`
SELECT gender, birth_date, medical_history, allergy_history,
emergency_contact, insurance_type, insurance_no
FROM patient_profiles WHERE user_id = ?`, targetID).Rows()
if err == nil {
defer pRows.Close()
if pRows.Next() {
pCols, _ := pRows.Columns()
pVals := make([]interface{}, len(pCols))
pPtrs := make([]interface{}, len(pCols))
for i := range pVals {
pPtrs[i] = &pVals[i]
}
if err := pRows.Scan(pPtrs...); err == nil {
for i, col := range pCols {
profile[col] = pVals[i]
}
}
}
}
// 查询最近问诊数
var consultCount int64
t.DB.WithContext(ctx).Raw("SELECT COUNT(*) FROM consultations WHERE patient_id = ? AND deleted_at IS NULL", targetID).Scan(&consultCount)
return map[string]interface{}{
"user": userInfo,
"health_profile": profile,
"consult_count": consultCount,
}, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// PrescriptionDetailTool 查询处方详情
type PrescriptionDetailTool struct {
DB *gorm.DB
}
func (t *PrescriptionDetailTool) Name() string { return "query_prescription_detail" }
func (t *PrescriptionDetailTool) Description() string {
return "查询处方详情,包括药品明细、用法用量、状态和费用,支持处方ID或处方编号(RX开头)查询"
}
func (t *PrescriptionDetailTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "prescription_id", Type: "string", Description: "处方ID(UUID)或处方编号(RX开头)", Required: true},
}
}
func (t *PrescriptionDetailTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
prescriptionID, ok := params["prescription_id"].(string)
if !ok || prescriptionID == "" {
return nil, fmt.Errorf("prescription_id 必填")
}
// 支持处方编号查询
resolvedID := prescriptionID
if len(prescriptionID) >= 2 && prescriptionID[:2] == "RX" {
var id string
if err := t.DB.WithContext(ctx).Raw("SELECT id FROM prescriptions WHERE prescription_no = ?", prescriptionID).Scan(&id).Error; err != nil || id == "" {
return nil, fmt.Errorf("处方编号 %s 不存在", prescriptionID)
}
resolvedID = id
}
// 查询处方主记录
detailRows, err := t.DB.WithContext(ctx).Raw(`
SELECT p.id, p.prescription_no, p.consult_id, p.patient_id, p.patient_name,
p.patient_gender, p.patient_age, p.diagnosis, p.allergy_history,
p.remark, p.total_amount, p.status, p.created_at,
d.name as doctor_name, d.title as doctor_title, dep.name as department_name
FROM prescriptions p
LEFT JOIN doctors d ON p.doctor_id = d.id
LEFT JOIN departments dep ON d.department_id = dep.id
WHERE p.id = ? AND p.deleted_at IS NULL`, resolvedID).Rows()
if err != nil {
return nil, fmt.Errorf("处方不存在: %s", prescriptionID)
}
defer detailRows.Close()
cols, _ := detailRows.Columns()
if !detailRows.Next() {
return nil, fmt.Errorf("处方不存在: %s", prescriptionID)
}
detail := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := detailRows.Scan(ptrs...); err != nil {
return nil, fmt.Errorf("处方不存在: %s", prescriptionID)
}
for i, col := range cols {
detail[col] = vals[i]
}
// 权限校验
if userRole == "patient" && detail["patient_id"] != userID {
return nil, fmt.Errorf("无权查看该处方")
}
// 查询药品明细
var items []map[string]interface{}
itemRows, err := t.DB.WithContext(ctx).Raw(`
SELECT medicine_name, specification, usage, dosage, frequency,
days, quantity, unit, price, note
FROM prescription_items
WHERE prescription_id = ?
ORDER BY created_at ASC`, resolvedID).Rows()
if err == nil {
defer itemRows.Close()
itemCols, _ := itemRows.Columns()
for itemRows.Next() {
item := make(map[string]interface{})
iVals := make([]interface{}, len(itemCols))
iPtrs := make([]interface{}, len(itemCols))
for i := range iVals {
iPtrs[i] = &iVals[i]
}
if err := itemRows.Scan(iPtrs...); err == nil {
for i, col := range itemCols {
item[col] = iVals[i]
}
items = append(items, item)
}
}
}
detail["items"] = items
detail["drug_count"] = len(items)
return detail, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// PrescriptionListTool 查询处方列表
type PrescriptionListTool struct {
DB *gorm.DB
}
func (t *PrescriptionListTool) Name() string { return "query_prescription_list" }
func (t *PrescriptionListTool) Description() string {
return "查询处方列表:患者查自己的处方,医生查自己开的处方,支持按状态过滤"
}
func (t *PrescriptionListTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "status", Type: "string", Description: "处方状态过滤(可选):pending/signed/dispensed/completed/cancelled", Required: false},
{Name: "limit", Type: "number", Description: "返回数量,默认10,最大50", Required: false},
}
}
func (t *PrescriptionListTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
if userID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
limit := 10
if v, ok := params["limit"].(float64); ok && v > 0 {
limit = int(v)
if limit > 50 {
limit = 50
}
}
status, _ := params["status"].(string)
query := "SELECT p.id, p.prescription_no, p.patient_name, p.diagnosis, " +
"p.total_amount, p.status, p.created_at, " +
"d.name as doctor_name, dep.name as department_name, " +
"(SELECT COUNT(*) FROM prescription_items pi WHERE pi.prescription_id = p.id) as drug_count " +
"FROM prescriptions p " +
"LEFT JOIN doctors d ON p.doctor_id = d.id " +
"LEFT JOIN departments dep ON d.department_id = dep.id " +
"WHERE p.deleted_at IS NULL"
args := []interface{}{}
switch userRole {
case "patient":
query += " AND p.patient_id = ?"
args = append(args, userID)
case "doctor":
var doctorID string
t.DB.WithContext(ctx).Raw("SELECT id FROM doctors WHERE user_id = ?", userID).Scan(&doctorID)
if doctorID == "" {
return map[string]interface{}{"prescriptions": []interface{}{}, "total": 0}, nil
}
query += " AND p.doctor_id = ?"
args = append(args, doctorID)
case "admin":
// 管理员查所有
default:
return nil, fmt.Errorf("无权限查询处方")
}
if status != "" {
query += " AND p.status = ?"
args = append(args, status)
}
query += " ORDER BY p.created_at DESC LIMIT ?"
args = append(args, limit)
var results []map[string]interface{}
rows, err := t.DB.WithContext(ctx).Raw(query, args...).Rows()
if err != nil {
return map[string]interface{}{"prescriptions": []interface{}{}, "total": 0}, nil
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
row := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
row[col] = vals[i]
}
results = append(results, row)
}
}
return map[string]interface{}{
"prescriptions": results,
"total": len(results),
}, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
)
// CreateRenewalFn 创建续方申请回调,由 main.go 注入
var CreateRenewalFn func(ctx context.Context, userID, chronicID, diseaseName, reason string, medicines []string) (string, error)
// CreateRenewalTool 患者创建续方申请
type CreateRenewalTool struct{}
func (t *CreateRenewalTool) Name() string { return "create_renewal_request" }
func (t *CreateRenewalTool) Description() string {
return "患者发起续方申请,需要指定疾病名称和续方药品列表,提交后等待医生审批"
}
func (t *CreateRenewalTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "disease_name", Type: "string", Description: "疾病名称", Required: true},
{Name: "medicines", Type: "array", Description: "续方药品名称列表", Required: true},
{Name: "chronic_id", Type: "string", Description: "关联的慢病记录ID(可选)", Required: false},
{Name: "reason", Type: "string", Description: "续方原因(可选)", Required: false},
}
}
func (t *CreateRenewalTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
if userRole != "patient" {
return nil, fmt.Errorf("仅患者可发起续方申请")
}
if userID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
diseaseName, ok := params["disease_name"].(string)
if !ok || diseaseName == "" {
return nil, fmt.Errorf("disease_name 必填")
}
// 解析药品列表
var medicines []string
if arr, ok := params["medicines"].([]interface{}); ok {
for _, v := range arr {
if s, ok := v.(string); ok {
medicines = append(medicines, s)
}
}
}
if len(medicines) == 0 {
return nil, fmt.Errorf("medicines 必填且至少包含一种药品")
}
chronicID, _ := params["chronic_id"].(string)
reason, _ := params["reason"].(string)
if CreateRenewalFn == nil {
return nil, fmt.Errorf("续方服务未初始化")
}
renewalID, err := CreateRenewalFn(ctx, userID, chronicID, diseaseName, reason, medicines)
if err != nil {
return nil, fmt.Errorf("创建续方申请失败: %v", err)
}
return map[string]interface{}{
"renewal_id": renewalID,
"disease_name": diseaseName,
"status": "pending",
"message": "续方申请已提交,等待医生审批",
}, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// RenewalRequestsTool 查询续方申请列表
type RenewalRequestsTool struct {
DB *gorm.DB
}
func (t *RenewalRequestsTool) Name() string { return "query_renewal_requests" }
func (t *RenewalRequestsTool) Description() string {
return "查询续方申请列表:患者查自己的续方申请(含状态和AI建议),医生查待审批的续方申请"
}
func (t *RenewalRequestsTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "status", Type: "string", Description: "状态过滤(可选):pending/approved/rejected", Required: false, Enum: []string{"pending", "approved", "rejected"}},
}
}
func (t *RenewalRequestsTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
if userID == "" {
return nil, fmt.Errorf("未获取到用户信息")
}
status, _ := params["status"].(string)
query := "SELECT r.id, r.disease_name, r.medicines, r.reason, r.status, " +
"r.doctor_note, r.ai_advice, r.created_at, " +
"u.real_name as patient_name " +
"FROM renewal_requests r " +
"LEFT JOIN users u ON r.user_id = u.id " +
"WHERE r.deleted_at IS NULL"
args := []interface{}{}
switch userRole {
case "patient":
query += " AND r.user_id = ?"
args = append(args, userID)
case "doctor":
// 医生查所有待审批的
if status == "" {
status = "pending"
}
case "admin":
// 管理员查所有
default:
return nil, fmt.Errorf("无权限查询续方申请")
}
if status != "" {
query += " AND r.status = ?"
args = append(args, status)
}
query += " ORDER BY r.created_at DESC LIMIT 30"
var results []map[string]interface{}
rows, err := t.DB.WithContext(ctx).Raw(query, args...).Rows()
if err != nil {
return map[string]interface{}{"renewals": []interface{}{}, "total": 0}, nil
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
row := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
row[col] = vals[i]
}
results = append(results, row)
}
}
return map[string]interface{}{
"renewals": results,
"total": len(results),
}, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// SystemLogsTool 查询系统操作日志(管理员专用)
type SystemLogsTool struct {
DB *gorm.DB
}
func (t *SystemLogsTool) Name() string { return "query_system_logs" }
func (t *SystemLogsTool) Description() string {
return "查询系统操作日志,支持按操作类型、资源过滤,仅管理员可用"
}
func (t *SystemLogsTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "action", Type: "string", Description: "操作类型过滤(可选,如login/create/update/delete)", Required: false},
{Name: "resource", Type: "string", Description: "资源类型过滤(可选,如user/doctor/prescription)", Required: false},
{Name: "limit", Type: "number", Description: "返回数量,默认20,最大100", Required: false},
}
}
func (t *SystemLogsTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
if userRole != "admin" {
return nil, fmt.Errorf("仅管理员可查看系统日志")
}
limit := 20
if v, ok := params["limit"].(float64); ok && v > 0 {
limit = int(v)
if limit > 100 {
limit = 100
}
}
action, _ := params["action"].(string)
resource, _ := params["resource"].(string)
query := "SELECT sl.id, sl.user_id, sl.action, sl.resource, sl.detail, sl.ip, sl.created_at, " +
"u.real_name as user_name " +
"FROM system_logs sl LEFT JOIN users u ON sl.user_id = u.id WHERE 1=1"
args := []interface{}{}
if action != "" {
query += " AND sl.action = ?"
args = append(args, action)
}
if resource != "" {
query += " AND sl.resource LIKE ?"
args = append(args, "%"+resource+"%")
}
query += " ORDER BY sl.created_at DESC LIMIT ?"
args = append(args, limit)
var results []map[string]interface{}
rows, err := t.DB.WithContext(ctx).Raw(query, args...).Rows()
if err != nil {
return map[string]interface{}{"logs": []interface{}{}, "total": 0}, nil
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
row := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
row[col] = vals[i]
}
results = append(results, row)
}
}
return map[string]interface{}{
"logs": results,
"total": len(results),
}, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// UserListTool 查询用户列表(管理员专用)
type UserListTool struct {
DB *gorm.DB
}
func (t *UserListTool) Name() string { return "query_user_list" }
func (t *UserListTool) Description() string {
return "查询系统用户列表,支持按角色、关键词搜索,仅管理员可用"
}
func (t *UserListTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{
{Name: "role", Type: "string", Description: "角色过滤(可选)", Required: false, Enum: []string{"patient", "doctor", "admin"}},
{Name: "keyword", Type: "string", Description: "搜索关键词(姓名/手机号)", Required: false},
{Name: "status", Type: "string", Description: "状态过滤(可选)", Required: false, Enum: []string{"active", "disabled"}},
{Name: "limit", Type: "number", Description: "返回数量,默认10,最大50", Required: false},
}
}
func (t *UserListTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
if userRole != "admin" {
return nil, fmt.Errorf("仅管理员可查看用户列表")
}
limit := 10
if v, ok := params["limit"].(float64); ok && v > 0 {
limit = int(v)
if limit > 50 {
limit = 50
}
}
role, _ := params["role"].(string)
keyword, _ := params["keyword"].(string)
status, _ := params["status"].(string)
query := "SELECT id, phone, real_name, role, status, is_verified, created_at " +
"FROM users WHERE deleted_at IS NULL"
args := []interface{}{}
if role != "" {
query += " AND role = ?"
args = append(args, role)
}
if keyword != "" {
query += " AND (real_name LIKE ? OR phone LIKE ?)"
args = append(args, "%"+keyword+"%", "%"+keyword+"%")
}
if status != "" {
query += " AND status = ?"
args = append(args, status)
}
query += " ORDER BY created_at DESC LIMIT ?"
args = append(args, limit)
var results []map[string]interface{}
rows, err := t.DB.WithContext(ctx).Raw(query, args...).Rows()
if err != nil {
return map[string]interface{}{"users": []interface{}{}, "total": 0}, nil
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
row := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
row[col] = vals[i]
}
results = append(results, row)
}
}
// 获取总数
var total int64
countQuery := "SELECT COUNT(*) FROM users WHERE deleted_at IS NULL"
countArgs := []interface{}{}
if role != "" {
countQuery += " AND role = ?"
countArgs = append(countArgs, role)
}
if keyword != "" {
countQuery += " AND (real_name LIKE ? OR phone LIKE ?)"
countArgs = append(countArgs, "%"+keyword+"%", "%"+keyword+"%")
}
if status != "" {
countQuery += " AND status = ?"
countArgs = append(countArgs, status)
}
t.DB.WithContext(ctx).Raw(countQuery, countArgs...).Scan(&total)
return map[string]interface{}{
"users": results,
"total": total,
}, nil
}
package tools
import (
"context"
"fmt"
"internet-hospital/pkg/agent"
"gorm.io/gorm"
)
// WaitingQueueTool 查询医生的等候队列
type WaitingQueueTool struct {
DB *gorm.DB
}
func (t *WaitingQueueTool) Name() string { return "query_waiting_queue" }
func (t *WaitingQueueTool) Description() string {
return "查询当前医生的等候队列,显示等候中的患者数量和列表"
}
func (t *WaitingQueueTool) Parameters() []agent.ToolParameter {
return []agent.ToolParameter{}
}
func (t *WaitingQueueTool) Execute(ctx context.Context, params map[string]interface{}) (interface{}, error) {
userRole, _ := ctx.Value(agent.ContextKeyUserRole).(string)
userID, _ := ctx.Value(agent.ContextKeyUserID).(string)
if userRole != "doctor" {
return nil, fmt.Errorf("仅医生可查看等候队列")
}
var doctorID string
if err := t.DB.WithContext(ctx).Raw("SELECT id FROM doctors WHERE user_id = ?", userID).Scan(&doctorID).Error; err != nil || doctorID == "" {
return map[string]interface{}{"waiting_count": 0, "patients": []interface{}{}}, nil
}
var results []map[string]interface{}
rows, err := t.DB.WithContext(ctx).Raw(`
SELECT c.id, c.serial_number, c.chief_complaint, c.type, c.created_at,
u.real_name as patient_name,
EXTRACT(EPOCH FROM (NOW() - c.created_at))::int as wait_seconds
FROM consultations c
LEFT JOIN users u ON c.patient_id = u.id
WHERE c.doctor_id = ? AND c.status = 'pending' AND c.deleted_at IS NULL
ORDER BY c.created_at ASC`, doctorID).Rows()
if err != nil {
return map[string]interface{}{"waiting_count": 0, "patients": []interface{}{}}, nil
}
defer rows.Close()
cols, _ := rows.Columns()
for rows.Next() {
row := make(map[string]interface{})
vals := make([]interface{}, len(cols))
ptrs := make([]interface{}, len(cols))
for i := range vals {
ptrs[i] = &vals[i]
}
if err := rows.Scan(ptrs...); err == nil {
for i, col := range cols {
row[col] = vals[i]
}
results = append(results, row)
}
}
return map[string]interface{}{
"waiting_count": len(results),
"patients": results,
}, nil
}
......@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
)
......@@ -148,17 +149,27 @@ func (e *Embedder) EmbedSingle(ctx context.Context, text string) ([]float32, err
}
// mockEmbed 模拟向量生成(用于测试或API未配置时)
// 使用 bag-of-words 方式:预定义关键词词表,文本中出现的关键词对应维度置1,然后归一化
// 这样语义相似的文本(共享关键词)会产生较高的余弦相似度
func (e *Embedder) mockEmbed(texts []string) [][]float32 {
// 生成简单的模拟向量(基于文本哈希)
dimension := 1024
embeddings := make([][]float32, len(texts))
for i, text := range texts {
vec := make([]float32, dimension)
// 遍历预定义关键词,命中则在对应维度置1
for j, kw := range mockVocab {
if j >= dimension {
break
}
if containsKeyword(text, kw) {
vec[j] = 1.0
}
}
// 用文本哈希填充剩余维度(提供区分度)
hash := simpleHash(text)
for j := 0; j < dimension; j++ {
// 使用哈希值生成伪随机向量
vec[j] = float32((hash>>(j%32))&1)*2 - 1
for j := len(mockVocab); j < dimension; j++ {
vec[j] = float32((hash>>(uint(j)%32))&1) * 0.1
hash = hash*1103515245 + 12345
}
// 归一化
......@@ -178,6 +189,61 @@ func (e *Embedder) mockEmbed(texts []string) [][]float32 {
return embeddings
}
// containsKeyword 检查文本是否包含关键词(不区分大小写)
func containsKeyword(text, keyword string) bool {
tl := strings.ToLower(text)
kl := strings.ToLower(keyword)
return strings.Contains(tl, kl)
}
// mockVocab 预定义关键词词表(医疗+业务领域),每个关键词对应向量的一个维度
// 共享关键词的文本会在相同维度上有值,从而产生较高的余弦相似度
var mockVocab = []string{
// 医疗基础
"药品", "药物", "用药", "处方", "开方", "症状", "病症", "诊断",
"科室", "部门", "推荐", "知识", "知识库", "检索", "搜索",
"病历", "病史", "记录", "检查", "检验", "报告", "解读",
"安全", "禁忌", "相互作用", "剂量", "用量", "计算",
// 问诊
"问诊", "就诊", "接诊", "挂号", "预约", "主诉", "等候", "队列",
"consultation", "consult", "accept", "waiting", "queue",
// 处方
"prescription", "medicine", "catalog", "药品目录", "库存",
// 患者
"患者", "档案", "信息", "资料", "健康", "profile", "patient",
// 健康指标
"指标", "血压", "血糖", "心率", "体温", "metric", "health",
// 医生
"医生", "doctor", "排班", "schedule", "时段",
// 科室
"department", "科室列表",
// 慢病
"慢病", "慢性", "续方", "续药", "审批", "chronic", "renewal",
// 支付
"订单", "支付", "付款", "退款", "order", "payment",
// 收入
"收入", "余额", "提现", "账单", "收益", "分成", "income",
// 管理
"管理", "统计", "趋势", "运营", "仪表盘", "dashboard", "admin",
// 用户
"用户", "user", "列表", "list",
// 系统
"日志", "操作", "系统", "log", "system",
// 导航
"导航", "页面", "跳转", "navigate", "page",
// 工作流
"工作流", "流程", "审核", "workflow",
// 通知
"通知", "提醒", "notification",
// 随访
"随访", "复诊", "follow",
// 工具名关键词
"query", "create", "search", "generate", "record", "check",
"drug", "medical", "symptom", "knowledge", "expression",
// 英文补充
"detail", "stats", "trend", "report", "lab",
}
func simpleHash(s string) uint64 {
var h uint64 = 5381
for i := 0; i < len(s); i++ {
......
......@@ -70,6 +70,7 @@ export interface AgentDefinition {
description: string;
category: string;
system_prompt: string;
prompt_template_id: number | null;
tools: string; // JSON array string
skills: string; // JSON array string of skill_ids
config: string;
......@@ -85,6 +86,8 @@ export type AgentDefinitionParams = {
description?: string;
category?: string;
system_prompt?: string;
prompt_template_id?: number | null;
clear_template?: boolean;
tools?: string;
skills?: string[];
max_iterations?: number;
......@@ -174,6 +177,9 @@ export const agentApi = {
listTools: () =>
get<{ 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 }[]>('/agent/tools'),
createSession: (agentId: string) =>
post<{ session_id: string }>('/agent/sessions', { agent_id: agentId }),
getSessions: (agentId?: string) =>
get<AgentSession[]>('/agent/sessions', { params: agentId ? { agent_id: agentId } : {} }),
......
......@@ -15,6 +15,8 @@ import {
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { agentApi, skillApi, httpToolApi } from '@/api/agent';
import type { ToolCall, AgentDefinition } from '@/api/agent';
import { adminApi } from '@/api/admin';
import type { PromptTemplateData } from '@/api/admin';
import AllToolsTab from './AllToolsTab';
import BuiltinToolsTab from './BuiltinToolsTab';
import HTTPToolsTab from './HTTPToolsTab';
......@@ -83,6 +85,12 @@ function AgentsTab() {
});
const agents: AgentDefinition[] = agentsData?.data || [];
const { data: templatesData } = useQuery({
queryKey: ['prompt-templates'],
queryFn: () => adminApi.getPromptTemplates(),
});
const templates: PromptTemplateData[] = templatesData?.data || [];
useEffect(() => {
agentApi.listTools().then(res => {
if (res.data?.length > 0) {
......@@ -95,12 +103,15 @@ function AgentsTab() {
mutationFn: (values: Record<string, unknown>) => {
const toolsArr = values.tools_array as string[] || [];
const skillsArr = values.skills_array as string[] || [];
const templateId = values.prompt_template_id as number | undefined;
const params = {
agent_id: values.agent_id as string,
name: values.name as string,
description: values.description as string,
category: values.category as string,
system_prompt: values.system_prompt as string,
prompt_template_id: templateId || null,
clear_template: !templateId,
tools: JSON.stringify(toolsArr),
skills: skillsArr,
max_iterations: values.max_iterations as number,
......@@ -145,6 +156,7 @@ function AgentsTab() {
form.setFieldsValue({
agent_id: agent.agent_id, name: agent.name, description: agent.description,
category: agent.category, system_prompt: agent.system_prompt,
prompt_template_id: agent.prompt_template_id || undefined,
tools_array: toolsArr, skills_array: skillsArr,
max_iterations: agent.max_iterations, status: agent.status === 'active',
});
......@@ -238,6 +250,16 @@ function AgentsTab() {
),
},
{ title: '描述', dataIndex: 'description', key: 'description', ellipsis: true },
{
title: '提示词', key: 'prompt_source', width: 120,
render: (_: unknown, r: AgentDefinition) => {
if (r.prompt_template_id) {
const tpl = templates.find(t => t.id === r.prompt_template_id);
return <Tag color="cyan">{tpl?.name || `模板#${r.prompt_template_id}`}</Tag>;
}
return <Tag>自定义</Tag>;
},
},
{
title: '类别', dataIndex: 'category', key: 'category', width: 110,
render: (v: string) => <Tag color={categoryColor[v] || 'default'}>{categoryLabel[v] || v}</Tag>,
......@@ -312,8 +334,18 @@ function AgentsTab() {
<Form.Item name="category" label="类别">
<Select options={CATEGORY_OPTIONS} placeholder="选择类别" />
</Form.Item>
<Form.Item name="system_prompt" label="系统提示词">
<Input.TextArea rows={5} placeholder="输入 Agent 的系统提示词(留空则使用数据库中关联的提示词模板)" />
<Form.Item name="prompt_template_id" label="关联提示词模板" extra="优先使用关联模板的内容作为系统提示词,未关联时使用下方的系统提示词">
<Select
allowClear
placeholder="选择提示词模板(可选)"
options={templates.filter(t => t.status === 'active').map(t => ({
value: t.id,
label: `${t.name}(${t.template_key})`,
}))}
/>
</Form.Item>
<Form.Item name="system_prompt" label="系统提示词" extra="当未关联模板时使用此提示词">
<Input.TextArea rows={5} placeholder="输入 Agent 的系统提示词" />
</Form.Item>
<Form.Item name="tools_array" label="关联工具">
<Select
......
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import {
Tabs, Typography, Space, Empty, Tag, Alert, Spin, Badge, Button,
Collapse, Timeline, Divider, message,
......@@ -20,6 +20,9 @@ const { Text } = Typography;
type AIScene = 'consult_diagnosis' | 'consult_medication' | 'consult_lab_advice'
| 'consult_medical_record' | 'consult_follow_up' | 'consult_order_template';
// 支持流式的场景
const STREAM_SCENES: AIScene[] = ['consult_diagnosis', 'consult_medication'];
interface SceneState {
content: string;
loading: boolean;
......@@ -47,32 +50,66 @@ const AIPanel: React.FC<AIPanelProps> = ({
}) => {
const [scenes, setScenes] = useState<Record<string, SceneState>>({});
const [preConsultSubTab, setPreConsultSubTab] = useState('chat');
const abortControllersRef = useRef<Record<string, AbortController>>({});
const getScene = (scene: string): SceneState =>
scenes[scene] || { content: '', loading: false, toolCalls: [] };
const updateScene = (scene: string, update: Partial<SceneState>) => {
const updateScene = useCallback((scene: string, update: Partial<SceneState>) => {
setScenes(prev => ({
...prev,
[scene]: { ...getScene(scene), ...update },
[scene]: { ...(prev[scene] || { content: '', loading: false, toolCalls: [] }), ...update },
}));
};
}, []);
const handleAIAssistStream = useCallback((scene: AIScene) => {
if (!activeConsultId) return;
// 取消之前的同场景请求
abortControllersRef.current[scene]?.abort();
updateScene(scene, { content: '', loading: true, toolCalls: [] });
let accumulated = '';
const controller = consultApi.aiAssistStream(activeConsultId, scene, {
onChunk: (content: string) => {
accumulated += content;
updateScene(scene, { content: accumulated, loading: true });
},
onDone: () => {
updateScene(scene, { loading: false });
if (scene === 'consult_diagnosis') onDiagnosisChange?.(accumulated);
if (scene === 'consult_medication') onMedicationChange?.(accumulated);
},
onError: (error: string) => {
updateScene(scene, {
content: accumulated || ('AI分析失败: ' + error),
loading: false,
});
},
});
abortControllersRef.current[scene] = controller;
}, [activeConsultId, updateScene, onDiagnosisChange, onMedicationChange]);
const handleAIAssist = async (scene: AIScene) => {
const handleAIAssist = useCallback(async (scene: AIScene) => {
if (!activeConsultId) return;
// 鉴别诊断和用药建议走流式
if (STREAM_SCENES.includes(scene)) {
handleAIAssistStream(scene);
return;
}
updateScene(scene, { loading: true });
try {
const res = await consultApi.aiAssist(activeConsultId, scene);
const text = res.data?.response || '暂无分析结果';
const toolCalls = res.data?.tool_calls || [];
updateScene(scene, { content: text, toolCalls, loading: false });
if (scene === 'consult_diagnosis') onDiagnosisChange?.(text);
if (scene === 'consult_medication') onMedicationChange?.(text);
} catch (err: unknown) {
const text = 'AI分析失败: ' + ((err as Error)?.message || '请稍后重试');
updateScene(scene, { content: text, toolCalls: [], loading: false });
}
};
}, [activeConsultId, updateScene, handleAIAssistStream]);
// Auto-trigger diagnosis + medication when patient sends new message
useEffect(() => {
......@@ -82,6 +119,13 @@ const AIPanel: React.FC<AIPanelProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [patientMessageTrigger, activeConsultId]);
// Cleanup abort controllers on unmount
useEffect(() => {
return () => {
Object.values(abortControllersRef.current).forEach(c => c.abort());
};
}, []);
const renderToolCalls = (toolCalls: ToolCall[]) => {
if (!toolCalls || toolCalls.length === 0) return null;
return (
......@@ -137,6 +181,23 @@ const AIPanel: React.FC<AIPanelProps> = ({
extra?: React.ReactNode,
) => {
const state = getScene(scene);
const isStreamScene = STREAM_SCENES.includes(scene);
// 流式场景:loading 期间如果已有内容,显示内容+光标
if (state.loading && isStreamScene && state.content) {
return (
<div>
<div style={{ maxHeight: 360, overflow: 'auto', padding: '4px 0' }}>
<MarkdownRenderer content={state.content + ''} fontSize={12} lineHeight={1.6} />
</div>
<div style={{ fontSize: 11, color: '#0891B2', marginTop: 6, display: 'flex', alignItems: 'center', gap: 4 }}>
<Spin size="small" />
<span>AI 正在分析中...</span>
</div>
</div>
);
}
if (state.loading) {
return (
<div style={{ textAlign: 'center', padding: '32px 16px' }}>
......
......@@ -2,8 +2,8 @@
import React, { useState } from 'react';
import {
Card, Tabs, Button, Tag, Space, Form, Input, Select,
DatePicker, Switch, message, Popconfirm, Typography, Row, Col,
Card, Tabs, Button, Tag, Space, Form, Input, Select, App,
DatePicker, Switch, Popconfirm, Typography, Row, Col,
TimePicker, Statistic, Empty,
} from 'antd';
import { DrawerForm, ProTable } from '@ant-design/pro-components';
......@@ -36,6 +36,7 @@ const metricTypeMap: Record<string, { label: string; unit: string; hasValue2: bo
// ========== 慢病档案 Tab ==========
const ChronicRecordsTab: React.FC = () => {
const { message } = App.useApp();
const qc = useQueryClient();
const [modalOpen, setModalOpen] = useState(false);
const [editing, setEditing] = useState<ChronicRecord | null>(null);
......@@ -48,7 +49,7 @@ const ChronicRecordsTab: React.FC = () => {
const saveMutation = useMutation({
mutationFn: (values: any) => {
const payload = { ...values, diagnosis_date: values.diagnosis_date?.toISOString() };
const payload = { ...values, diagnosis_date: values.diagnosis_date ? dayjs(values.diagnosis_date).toISOString() : null };
return editing
? chronicApi.updateRecord(editing.id, payload)
: chronicApi.createRecord(payload);
......@@ -56,7 +57,6 @@ const ChronicRecordsTab: React.FC = () => {
onSuccess: () => {
message.success(editing ? '更新成功' : '添加成功');
qc.invalidateQueries({ queryKey: ['chronic-records'] });
setModalOpen(false);
form.resetFields();
setEditing(null);
},
......@@ -124,8 +124,8 @@ const ChronicRecordsTab: React.FC = () => {
onOpenChange={(open) => { if (!open) { setModalOpen(false); setEditing(null); form.resetFields(); } }}
form={form}
width={600}
drawerProps={{ placement: 'right', destroyOnClose: true }}
onFinish={async (values) => { saveMutation.mutate(values); return true; }}
drawerProps={{ placement: 'right' }}
onFinish={async (values) => { await saveMutation.mutateAsync(values); return true; }}
submitter={{ submitButtonProps: { loading: saveMutation.isPending } }}
>
<Form.Item name="disease_name" label="疾病名称" rules={[{ required: true }]}>
......@@ -168,6 +168,7 @@ const ChronicRecordsTab: React.FC = () => {
// ========== 续方申请 Tab ==========
const RenewalsTab: React.FC = () => {
const { message } = App.useApp();
const qc = useQueryClient();
const [modalOpen, setModalOpen] = useState(false);
const [aiLoading, setAiLoading] = useState<string | null>(null);
......@@ -185,7 +186,6 @@ const RenewalsTab: React.FC = () => {
onSuccess: () => {
message.success('续方申请已提交');
qc.invalidateQueries({ queryKey: ['renewals'] });
setModalOpen(false);
form.resetFields();
},
onError: () => message.error('提交失败'),
......@@ -260,8 +260,8 @@ const RenewalsTab: React.FC = () => {
onOpenChange={(open) => { if (!open) { setModalOpen(false); form.resetFields(); } }}
form={form}
width={480}
drawerProps={{ placement: 'right', destroyOnClose: true }}
onFinish={async (values) => { createMutation.mutate(values); return true; }}
drawerProps={{ placement: 'right' }}
onFinish={async (values) => { await createMutation.mutateAsync(values); return true; }}
submitter={{ submitButtonProps: { loading: createMutation.isPending } }}
>
<Form.Item name="chronic_id" label="关联慢病档案">
......@@ -284,6 +284,7 @@ const RenewalsTab: React.FC = () => {
// ========== 用药提醒 Tab ==========
const RemindersTab: React.FC = () => {
const { message } = App.useApp();
const qc = useQueryClient();
const [modalOpen, setModalOpen] = useState(false);
const [editing, setEditing] = useState<MedicationReminder | null>(null);
......@@ -296,15 +297,15 @@ const RemindersTab: React.FC = () => {
const payload = {
...values,
remind_times: values.remind_times?.map((t: any) => t.format('HH:mm')) || [],
start_date: values.start_date?.toISOString(),
end_date: values.end_date?.toISOString(),
start_date: values.start_date ? dayjs(values.start_date).toISOString() : null,
end_date: values.end_date ? dayjs(values.end_date).toISOString() : null,
};
return editing ? chronicApi.updateReminder(editing.id, payload) : chronicApi.createReminder(payload);
},
onSuccess: () => {
message.success(editing ? '更新成功' : '添加成功');
qc.invalidateQueries({ queryKey: ['reminders'] });
setModalOpen(false); form.resetFields(); setEditing(null);
form.resetFields(); setEditing(null);
},
onError: () => message.error('操作失败'),
});
......@@ -371,8 +372,8 @@ const RemindersTab: React.FC = () => {
onOpenChange={(open) => { if (!open) { setModalOpen(false); setEditing(null); form.resetFields(); } }}
form={form}
width={600}
drawerProps={{ placement: 'right', destroyOnClose: true }}
onFinish={async (values) => { saveMutation.mutate(values); return true; }}
drawerProps={{ placement: 'right' }}
onFinish={async (values) => { await saveMutation.mutateAsync(values); return true; }}
submitter={{ submitButtonProps: { loading: saveMutation.isPending } }}
>
<Row gutter={16}>
......@@ -401,6 +402,7 @@ const RemindersTab: React.FC = () => {
// ========== 健康指标 Tab ==========
const MetricsTab: React.FC = () => {
const { message } = App.useApp();
const qc = useQueryClient();
const [modalOpen, setModalOpen] = useState(false);
const [activeType, setActiveType] = useState('blood_pressure');
......@@ -418,13 +420,13 @@ const MetricsTab: React.FC = () => {
...values,
metric_type: activeType,
unit: meta.unit,
recorded_at: values.recorded_at?.toISOString() || new Date().toISOString(),
recorded_at: values.recorded_at ? dayjs(values.recorded_at).toISOString() : dayjs().toISOString(),
});
},
onSuccess: () => {
message.success('记录成功');
qc.invalidateQueries({ queryKey: ['metrics', activeType] });
setModalOpen(false); form.resetFields();
form.resetFields();
},
onError: () => message.error('记录失败'),
});
......@@ -535,8 +537,8 @@ const MetricsTab: React.FC = () => {
onOpenChange={(open) => { if (!open) { setModalOpen(false); form.resetFields(); } }}
form={form}
width={480}
drawerProps={{ placement: 'right', destroyOnClose: true }}
onFinish={async (values) => { createMutation.mutate(values); return true; }}
drawerProps={{ placement: 'right' }}
onFinish={async (values) => { await createMutation.mutateAsync(values); return true; }}
submitter={{ submitButtonProps: { loading: createMutation.isPending } }}
>
<Row gutter={16}>
......
'use client';
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import { Input, Button, Tag, Spin, Tooltip } from 'antd';
import { Input, Button, Tag, Spin, Tooltip, App } from 'antd';
import {
SendOutlined,
RobotOutlined,
......@@ -22,7 +22,6 @@ import ToolCallCard from './ToolCallCard';
import ToolResultCard from './ToolResultCard';
import SuggestedActions, { parseActions } from './SuggestedActions';
import { validateNavigationPermission } from '../../lib/navigation-event';
import { message as antMessage } from 'antd';
import type { ChatMessage, ToolCall, WidgetRole } from './types';
import { ROLE_AGENT_ID, ROLE_AGENT_NAME, ROLE_THEME, QUICK_ITEMS } from './types';
......@@ -50,6 +49,7 @@ const QUICK_ICON: Record<WidgetRole, React.ReactNode> = {
};
const ChatPanel: React.FC<ChatPanelProps> = ({ role, patientContext }) => {
const { message: antMessage } = App.useApp();
const store = useAIAssistStore();
const messages = Array.isArray(store.messages) ? store.messages : [];
const sessionId = store.sessionId || '';
......@@ -78,31 +78,33 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ role, patientContext }) => {
return welcomeMap[role];
}, [agentName, role]);
// 创建新会话
const createNewSession = useCallback(async () => {
try {
const res = await agentApi.createSession(agentId);
setSessionId(res.data.session_id);
return res.data.session_id;
} catch (e) {
console.warn('[ChatPanel] 创建会话失败:', e);
return '';
}
}, [agentId, setSessionId]);
// 加载最近会话的函数
const loadLatestSession = useCallback(async () => {
if (isSessionLoaded) return;
try {
console.log('[ChatPanel] 加载最近会话, agentId:', agentId);
const res = await agentApi.getSessions(agentId);
const sessions = res.data;
console.log('[ChatPanel] 获取到会话列表:', sessions?.length || 0, '');
if (sessions && sessions.length > 0) {
// 打印前3条会话的时间,用于调试排序
sessions.slice(0, 3).forEach((s, idx) => {
console.log(`[ChatPanel] 会话 ${idx}: ${s.session_id}, updated_at: ${s.updated_at}`);
});
const latest = sessions[0];
console.log('[ChatPanel] 恢复会话:', latest.session_id, 'updated_at:', latest.updated_at);
setSessionId(latest.session_id);
// 尝试恢复历史消息
try {
const history = JSON.parse(latest.history || '[]') as { role: string; content: string }[];
console.log('[ChatPanel] 解析历史消息:', history.length, '');
if (history.length > 0) {
const restored: ChatMessage[] = history.map(h => ({
role: h.role as 'user' | 'assistant',
......@@ -110,23 +112,29 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ role, patientContext }) => {
timestamp: new Date(),
}));
setMessages(restored);
setSessionLoaded(true);
return;
} else {
// 会话存在但没有历史消息,显示欢迎语,复用该会话
setMessages([{ role: 'system', content: welcomeContent, timestamp: new Date() }]);
}
} catch (e) {
console.warn('[ChatPanel] 解析历史失败:', e);
setMessages([{ role: 'system', content: welcomeContent, timestamp: new Date() }]);
}
setSessionLoaded(true);
return;
}
// 没有历史会话,显示欢迎消息
// 完全没有会话,才创建新会话
await createNewSession();
setMessages([{ role: 'system', content: welcomeContent, timestamp: new Date() }]);
setSessionLoaded(true);
} catch (e) {
console.warn('[ChatPanel] 加载会话失败:', e);
await createNewSession();
setMessages([{ role: 'system', content: welcomeContent, timestamp: new Date() }]);
setSessionLoaded(true);
}
}, [agentId, isSessionLoaded, welcomeContent, setMessages, setSessionId, setSessionLoaded]);
}, [agentId, isSessionLoaded, welcomeContent, setMessages, setSessionId, setSessionLoaded, createNewSession]);
// 当组件挂载且会话未加载时,自动加载最近会话
useEffect(() => {
......@@ -242,10 +250,10 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ role, patientContext }) => {
abortRef.current = agentApi.chatStream(agentId, buildRequestBody(userMessage), createStreamCallbacks(msgId));
};
const handleNewChat = () => {
const handleNewChat = async () => {
stopStreaming();
// 清空 sessionId,但保持 isSessionLoaded = true,避免重新加载旧会话
setSessionId('');
// 预创建新会话,获取 session_id
await createNewSession();
setMessages([{ role: 'system', content: welcomeContent, timestamp: new Date() }]);
// 标记为已加载,防止自动恢复旧会话
setSessionLoaded(true);
......
......@@ -27,28 +27,86 @@ import {
UnorderedListOutlined,
AuditOutlined,
ScheduleOutlined,
SolutionOutlined,
TeamOutlined,
HeartOutlined,
ExperimentOutlined,
DollarOutlined,
DashboardOutlined,
UserOutlined,
FileTextOutlined,
CalendarOutlined,
ProfileOutlined,
BarChartOutlined,
WalletOutlined,
} from '@ant-design/icons';
// 工具元数据映射(互联网医院场景)
const TOOL_META: Record<string, { label: string; color: string; icon: React.ReactNode }> = {
// 基础查询
query_drug: { label: '药品查询', color: '#52c41a', icon: <MedicineBoxOutlined /> },
query_medical_record: { label: '病历查询', color: '#0D9488', icon: <FileSearchOutlined /> },
query_symptom_knowledge: { label: '症状知识', color: '#0891B2', icon: <BulbOutlined /> },
recommend_department: { label: '科室推荐', color: '#13c2c2', icon: <ApartmentOutlined /> },
// 处方安全
check_drug_interaction: { label: '药物相互作用', color: '#f5222d', icon: <WarningOutlined /> },
check_contraindication: { label: '禁忌症检查', color: '#fa8c16', icon: <StopOutlined /> },
calculate_dosage: { label: '剂量计算', color: '#faad14', icon: <CalculatorOutlined /> },
// 知识库
search_medical_knowledge: { label: '知识检索', color: '#2f54eb', icon: <BookOutlined /> },
write_knowledge: { label: '知识写入', color: '#52c41a', icon: <EditOutlined /> },
list_knowledge_collections:{ label: '知识库列表', color: '#2f54eb', icon: <UnorderedListOutlined /> },
// 导航 & 通知
navigate_page: { label: '页面导航', color: '#0D9488', icon: <CompassOutlined /> },
send_notification: { label: '发送通知', color: '#fa541c', icon: <BellOutlined /> },
// Agent & 工作流
call_agent: { label: '调用Agent', color: '#0891B2', icon: <RobotOutlined /> },
trigger_workflow: { label: '触发工作流', color: '#13c2c2', icon: <DeploymentUnitOutlined /> },
query_workflow_status: { label: '工作流状态', color: '#13c2c2', icon: <DeploymentUnitOutlined /> },
eval_expression: { label: '表达式计算', color: '#a0d911', icon: <CodeOutlined /> },
write_knowledge: { label: '知识写入', color: '#52c41a', icon: <EditOutlined /> },
list_knowledge_collections:{ label: '知识库列表', color: '#2f54eb', icon: <UnorderedListOutlined /> },
request_human_review: { label: '人工审核', color: '#fa8c16', icon: <AuditOutlined /> },
// 表达式 & 随访
eval_expression: { label: '表达式计算', color: '#a0d911', icon: <CodeOutlined /> },
generate_follow_up_plan: { label: '随访计划', color: '#eb2f96', icon: <ScheduleOutlined /> },
generate_tool: { label: '工具生成', color: '#722ed1', icon: <CodeOutlined /> },
// 问诊管理
query_consultation_list: { label: '问诊列表', color: '#1890ff', icon: <SolutionOutlined /> },
query_consultation_detail: { label: '问诊详情', color: '#1890ff', icon: <SolutionOutlined /> },
create_consultation: { label: '创建问诊', color: '#52c41a', icon: <SolutionOutlined /> },
accept_consultation: { label: '接诊', color: '#13c2c2', icon: <SolutionOutlined /> },
query_waiting_queue: { label: '等候队列', color: '#faad14', icon: <TeamOutlined /> },
// 处方管理
query_prescription_list: { label: '处方列表', color: '#722ed1', icon: <ProfileOutlined /> },
query_prescription_detail: { label: '处方详情', color: '#722ed1', icon: <ProfileOutlined /> },
search_medicine_catalog: { label: '药品目录', color: '#52c41a', icon: <MedicineBoxOutlined /> },
// 患者信息
query_patient_profile: { label: '患者档案', color: '#eb2f96', icon: <UserOutlined /> },
query_health_metrics: { label: '健康指标', color: '#f5222d', icon: <HeartOutlined /> },
query_lab_reports: { label: '化验报告', color: '#fa8c16', icon: <ExperimentOutlined /> },
// 医生 & 科室
query_doctor_list: { label: '医生列表', color: '#1890ff', icon: <TeamOutlined /> },
query_doctor_detail: { label: '医生详情', color: '#1890ff', icon: <UserOutlined /> },
query_department_list: { label: '科室列表', color: '#13c2c2', icon: <ApartmentOutlined /> },
// 慢病管理
query_chronic_records: { label: '慢病记录', color: '#f5222d', icon: <FileTextOutlined /> },
create_chronic_record: { label: '创建慢病记录', color: '#f5222d', icon: <FileTextOutlined /> },
query_renewal_requests: { label: '续方申请', color: '#fa8c16', icon: <ProfileOutlined /> },
create_renewal_request: { label: '创建续方', color: '#fa8c16', icon: <ProfileOutlined /> },
// 健康指标写入
record_health_metric: { label: '记录指标', color: '#eb2f96', icon: <HeartOutlined /> },
// 排班管理
query_doctor_schedule: { label: '排班查询', color: '#1890ff', icon: <CalendarOutlined /> },
create_doctor_schedule: { label: '创建排班', color: '#1890ff', icon: <CalendarOutlined /> },
// 支付订单
query_order_list: { label: '订单列表', color: '#faad14', icon: <DollarOutlined /> },
query_order_detail: { label: '订单详情', color: '#faad14', icon: <DollarOutlined /> },
// 医生收入
query_income_stats: { label: '收入统计', color: '#52c41a', icon: <WalletOutlined /> },
query_income_records: { label: '收入明细', color: '#52c41a', icon: <WalletOutlined /> },
// 管理统计
query_dashboard_stats: { label: '运营概览', color: '#722ed1', icon: <DashboardOutlined /> },
query_dashboard_trend: { label: '趋势分析', color: '#722ed1', icon: <BarChartOutlined /> },
query_user_list: { label: '用户列表', color: '#1890ff', icon: <TeamOutlined /> },
query_system_logs: { label: '系统日志', color: '#8c8c8c', icon: <FileTextOutlined /> },
};
// 参数中文映射
......@@ -65,6 +123,20 @@ const PARAM_LABELS: Record<string, string> = {
patient_name: '患者姓名', consult_id: '问诊ID',
level: '级别', channel: '通道', target_user_id: '目标用户',
description: '描述', input: '输入',
// 问诊
status: '状态', doctor_id: '医生ID', doctor_name: '医生姓名',
consultation_id: '问诊ID', chief_complaint: '主诉',
// 处方
prescription_id: '处方ID', medicine_name: '药品名称',
// 患者
metric_type: '指标类型', start_date: '开始日期', end_date: '结束日期',
// 排班
date: '日期', time_slot: '时段', max_patients: '最大接诊数',
// 订单
order_id: '订单ID', order_type: '订单类型',
// 管理
period: '统计周期', days: '天数', role: '角色',
resource: '资源类型', limit: '条数', offset: '偏移',
};
interface ToolCallCardProps {
......
......@@ -13,6 +13,17 @@ import {
RightOutlined,
ExclamationCircleOutlined,
DatabaseOutlined,
SolutionOutlined,
HeartOutlined,
DollarOutlined,
DashboardOutlined,
TeamOutlined,
CalendarOutlined,
UserOutlined,
ExperimentOutlined,
ProfileOutlined,
WalletOutlined,
BarChartOutlined,
} from '@ant-design/icons';
import { Tag } from 'antd';
......@@ -258,6 +269,156 @@ const DataTable: React.FC<{ data: Record<string, unknown>[] }> = ({ data }) => {
);
};
/** 问诊卡片 */
const ConsultationCard: React.FC<{ data: Record<string, unknown> }> = ({ data }) => {
const statusMap: Record<string, { color: string; text: string }> = {
waiting: { color: 'orange', text: '等待中' },
in_progress: { color: 'blue', text: '进行中' },
completed: { color: 'green', text: '已完成' },
cancelled: { color: 'default', text: '已取消' },
};
const s = statusMap[String(data.status || '')] || { color: 'default', text: String(data.status || '-') };
return (
<div style={{ padding: 8, background: '#eff6ff', border: '1px solid #bfdbfe', borderRadius: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
<SolutionOutlined style={{ color: '#2563eb' }} />
<span style={{ fontWeight: 500, color: '#1e40af', fontSize: 12 }}>
{String(data.patient_name || data.doctor_name || '问诊')} #{String(data.id || data.consultation_id || '')}
</span>
<Tag color={s.color} style={{ fontSize: 10, lineHeight: '16px', margin: 0 }}>{s.text}</Tag>
</div>
<div style={{ fontSize: 11, color: '#3b82f6', display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{data.chief_complaint ? <span>主���: {String(data.chief_complaint)}</span> : null}
{data.doctor_name ? <span>医生: {String(data.doctor_name)}</span> : null}
{data.department_name ? <span>科室: {String(data.department_name)}</span> : null}
{data.created_at ? <span>时间: {String(data.created_at).slice(0, 16)}</span> : null}
</div>
</div>
);
};
/** 健康指标卡片 */
const HealthMetricCard: React.FC<{ data: Record<string, unknown> }> = ({ data }) => (
<div style={{ padding: 8, background: '#fef2f2', border: '1px solid #fecaca', borderRadius: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
<HeartOutlined style={{ color: '#ef4444' }} />
<span style={{ fontWeight: 500, color: '#991b1b', fontSize: 12 }}>
{String(data.metric_type || data.type || '健康指标')}
</span>
</div>
<div style={{ fontSize: 11, color: '#dc2626', display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{data.value !== undefined ? <span>数值: {String(data.value)} {String(data.unit || '')}</span> : null}
{data.recorded_at ? <span>时间: {String(data.recorded_at).slice(0, 16)}</span> : null}
{data.status ? <span>状态: {String(data.status)}</span> : null}
</div>
</div>
);
/** 订单卡片 */
const OrderCard: React.FC<{ data: Record<string, unknown> }> = ({ data }) => {
const statusMap: Record<string, { color: string; text: string }> = {
pending: { color: 'orange', text: '待支付' },
paid: { color: 'green', text: '已支付' },
refunded: { color: 'red', text: '已退款' },
cancelled: { color: 'default', text: '已取消' },
};
const s = statusMap[String(data.status || '')] || { color: 'default', text: String(data.status || '-') };
return (
<div style={{ padding: 8, background: '#fffbeb', border: '1px solid #fde68a', borderRadius: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
<DollarOutlined style={{ color: '#d97706' }} />
<span style={{ fontWeight: 500, color: '#92400e', fontSize: 12 }}>
订单 #{String(data.id || data.order_id || '')}
</span>
<Tag color={s.color} style={{ fontSize: 10, lineHeight: '16px', margin: 0 }}>{s.text}</Tag>
</div>
<div style={{ fontSize: 11, color: '#b45309', display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{data.amount !== undefined ? <span>金额: ¥{String(data.amount)}</span> : null}
{data.order_type ? <span>类型: {String(data.order_type)}</span> : null}
{data.created_at ? <span>时间: {String(data.created_at).slice(0, 16)}</span> : null}
</div>
</div>
);
};
/** 仪表盘统计卡片 */
const DashboardCard: React.FC<{ data: Record<string, unknown> }> = ({ data }) => (
<div style={{ padding: 8, background: '#f5f3ff', border: '1px solid #ddd6fe', borderRadius: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }}>
<DashboardOutlined style={{ color: '#7c3aed' }} />
<span style={{ fontWeight: 500, color: '#5b21b6', fontSize: 12 }}>运营概览</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 6 }}>
{Object.entries(data).filter(([, v]) => typeof v === 'number' || typeof v === 'string').slice(0, 8).map(([k, v]) => (
<div key={k} style={{ background: '#ede9fe', borderRadius: 4, padding: '4px 8px', textAlign: 'center' }}>
<div style={{ fontSize: 14, fontWeight: 600, color: '#6d28d9' }}>{String(v)}</div>
<div style={{ fontSize: 10, color: '#7c3aed' }}>{k.replace(/_/g, ' ')}</div>
</div>
))}
</div>
</div>
);
/** 收入统计卡片 */
const IncomeCard: React.FC<{ data: Record<string, unknown> }> = ({ data }) => (
<div style={{ padding: 8, background: '#f0fdf4', border: '1px solid #bbf7d0', borderRadius: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }}>
<WalletOutlined style={{ color: '#16a34a' }} />
<span style={{ fontWeight: 500, color: '#166534', fontSize: 12 }}>收入统计</span>
</div>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', fontSize: 11, color: '#15803d' }}>
{data.balance !== undefined ? <span>余额: ¥{String(data.balance)}</span> : null}
{data.month_income !== undefined ? <span>本月收入: ¥{String(data.month_income)}</span> : null}
{data.month_consults !== undefined ? <span>本月问诊: {String(data.month_consults)}</span> : null}
{data.total_income !== undefined ? <span>总收入: ¥{String(data.total_income)}</span> : null}
</div>
</div>
);
/** 处方卡片 */
const PrescriptionCard: React.FC<{ data: Record<string, unknown> }> = ({ data }) => {
const statusMap: Record<string, { color: string; text: string }> = {
pending: { color: 'orange', text: '待审核' },
approved: { color: 'green', text: '已通过' },
rejected: { color: 'red', text: '已驳回' },
dispensed: { color: 'blue', text: '已发药' },
};
const s = statusMap[String(data.status || '')] || { color: 'default', text: String(data.status || '-') };
return (
<div style={{ padding: 8, background: '#faf5ff', border: '1px solid #e9d5ff', borderRadius: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
<ProfileOutlined style={{ color: '#7c3aed' }} />
<span style={{ fontWeight: 500, color: '#5b21b6', fontSize: 12 }}>
处方 #{String(data.id || data.prescription_id || '')}
</span>
<Tag color={s.color} style={{ fontSize: 10, lineHeight: '16px', margin: 0 }}>{s.text}</Tag>
</div>
<div style={{ fontSize: 11, color: '#7c3aed', display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{data.patient_name ? <span>患者: {String(data.patient_name)}</span> : null}
{data.doctor_name ? <span>医生: {String(data.doctor_name)}</span> : null}
{data.created_at ? <span>时间: {String(data.created_at).slice(0, 16)}</span> : null}
</div>
</div>
);
};
/** 化验报告卡片 */
const LabReportCard: React.FC<{ data: Record<string, unknown> }> = ({ data }) => (
<div style={{ padding: 8, background: '#fff7ed', border: '1px solid #fed7aa', borderRadius: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
<ExperimentOutlined style={{ color: '#ea580c' }} />
<span style={{ fontWeight: 500, color: '#9a3412', fontSize: 12 }}>
{String(data.report_name || data.name || '化验报告')}
</span>
{data.abnormal ? <Tag color="red" style={{ fontSize: 10, lineHeight: '16px', margin: 0 }}>异常</Tag> : null}
</div>
<div style={{ fontSize: 11, color: '#c2410c', display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{data.report_date ? <span>日期: {String(data.report_date).slice(0, 10)}</span> : null}
{data.hospital ? <span>机构: {String(data.hospital)}</span> : null}
</div>
</div>
);
// ── 主组件 ──
const ToolResultCard: React.FC<ToolResultCardProps> = ({ toolName, success, result }) => {
......@@ -275,14 +436,16 @@ const ToolResultCard: React.FC<ToolResultCardProps> = ({ toolName, success, resu
// 按工具类型定制渲染
switch (toolName) {
case 'query_drug': {
case 'query_drug':
case 'search_medicine_catalog': {
if (Array.isArray(data)) {
return <>{(data as Record<string, unknown>[]).map((d, i) => <DrugCard key={i} data={d} />)}</>;
}
if (data && !Array.isArray(data)) return <DrugCard data={data as Record<string, unknown>} />;
break;
}
case 'recommend_department': {
case 'recommend_department':
case 'query_department_list': {
if (Array.isArray(data)) {
return <>{(data as Record<string, unknown>[]).map((d, i) => <DepartmentCard key={i} data={d} />)}</>;
}
......@@ -298,6 +461,56 @@ const ToolResultCard: React.FC<ToolResultCardProps> = ({ toolName, success, resu
if (data && !Array.isArray(data)) return <NavigateCard data={data as Record<string, unknown>} />;
break;
}
case 'query_consultation_list':
case 'query_consultation_detail':
case 'create_consultation':
case 'accept_consultation':
case 'query_waiting_queue': {
if (Array.isArray(data)) {
return <>{(data as Record<string, unknown>[]).slice(0, 5).map((d, i) => <ConsultationCard key={i} data={d} />)}</>;
}
if (data && !Array.isArray(data)) return <ConsultationCard data={data as Record<string, unknown>} />;
break;
}
case 'query_prescription_list':
case 'query_prescription_detail': {
if (Array.isArray(data)) {
return <>{(data as Record<string, unknown>[]).slice(0, 5).map((d, i) => <PrescriptionCard key={i} data={d} />)}</>;
}
if (data && !Array.isArray(data)) return <PrescriptionCard data={data as Record<string, unknown>} />;
break;
}
case 'query_health_metrics':
case 'record_health_metric': {
if (Array.isArray(data)) {
return <>{(data as Record<string, unknown>[]).slice(0, 5).map((d, i) => <HealthMetricCard key={i} data={d} />)}</>;
}
if (data && !Array.isArray(data)) return <HealthMetricCard data={data as Record<string, unknown>} />;
break;
}
case 'query_lab_reports': {
if (Array.isArray(data)) {
return <>{(data as Record<string, unknown>[]).slice(0, 5).map((d, i) => <LabReportCard key={i} data={d} />)}</>;
}
if (data && !Array.isArray(data)) return <LabReportCard data={data as Record<string, unknown>} />;
break;
}
case 'query_order_list':
case 'query_order_detail': {
if (Array.isArray(data)) {
return <>{(data as Record<string, unknown>[]).slice(0, 5).map((d, i) => <OrderCard key={i} data={d} />)}</>;
}
if (data && !Array.isArray(data)) return <OrderCard data={data as Record<string, unknown>} />;
break;
}
case 'query_income_stats': {
if (data && !Array.isArray(data)) return <IncomeCard data={data as Record<string, unknown>} />;
break;
}
case 'query_dashboard_stats': {
if (data && !Array.isArray(data)) return <DashboardCard data={data as Record<string, unknown>} />;
break;
}
}
// 数组数据 → 表格
......
......@@ -83,21 +83,30 @@ export const QUICK_ITEMS: Record<WidgetRole, QuickItem[]> = {
{ label: '预问诊', type: 'embed', embed: { pageCode: 'pre_consult', pageName: '预问诊', operation: 'view_list', route: '/patient/pre-consult' } },
{ label: '找医生', type: 'embed', embed: { pageCode: 'find_doctor', pageName: '找医生', operation: 'view_list', route: '/patient/doctors' } },
{ label: '我的问诊', type: 'embed', embed: { pageCode: 'my_consultations', pageName: '我的问诊', operation: 'view_list', route: '/patient/consult' } },
{ label: '头痛', type: 'text' },
{ label: '发热', type: 'text' },
{ label: '咳嗽', type: 'text' },
{ label: '我的处方', type: 'text' },
{ label: '我的订单', type: 'text' },
{ label: '健康指标', type: 'text' },
{ label: '化验报告', type: 'text' },
{ label: '慢病续方', type: 'text' },
],
doctor: [
{ label: '等候队列', type: 'text' },
{ label: '今日问诊', type: 'text' },
{ label: '鉴别诊断建议', type: 'text' },
{ label: '用药方案推荐', type: 'text' },
{ label: '检查项目建议', type: 'text' },
{ label: '我的排班', type: 'text' },
{ label: '收入统计', type: 'text' },
{ label: '续方审批', type: 'text' },
],
admin: [
{ label: '运营概览', type: 'text' },
{ label: '趋势分析', type: 'text' },
{ label: '注册医生', type: 'embed', embed: { pageCode: 'doctor_management', pageName: '医生管理', operation: 'open_add', route: '/admin/doctors?action=add' } },
{ label: '添加科室', type: 'embed', embed: { pageCode: 'department_management', pageName: '科室管理', operation: 'open_add', route: '/admin/departments?action=add' } },
{ label: '患者列表', type: 'embed', embed: { pageCode: 'patient_management', pageName: '患者管理', operation: 'view_list', route: '/admin/patients' } },
{ label: '医生列表', type: 'embed', embed: { pageCode: 'doctor_management', pageName: '医生管理', operation: 'view_list', route: '/admin/doctors' } },
{ label: '查看运营数据', type: 'text' },
{ label: '用户列表', type: 'text' },
{ label: '订单查询', type: 'text' },
{ label: '系统日志', type: 'text' },
{ label: '管理Agent', type: 'text' },
],
};
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment