Files
iqudo-top1/scripts/seed-pet-doctor/main.go
2026-08-09 00:29:26 +08:00

439 lines
29 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"reflect"
"time"
"git.iwork-ai.com/xdc/iqudo-top1/internal/config"
"git.iwork-ai.com/xdc/iqudo-top1/internal/database"
"git.iwork-ai.com/xdc/iqudo-top1/internal/logger"
"git.iwork-ai.com/xdc/iqudo-top1/internal/model"
sopservice "git.iwork-ai.com/xdc/iqudo-top1/internal/sop"
"go.uber.org/zap"
"gorm.io/datatypes"
"gorm.io/gorm"
)
type fieldDefinition struct {
Key string
Name string
Type string
Required bool
Options []string
Validation map[string]interface{}
}
type nodeDefinition struct {
Key string
Type string
Title string
Content string
Config map[string]interface{}
}
type edgeDefinition struct {
Source string
Target string
Condition map[string]interface{}
Priority int
}
func main() {
configDir := flag.String("config-dir", "configs", "configuration directory")
environment := flag.String("env", "", "runtime environment")
flag.Parse()
cfg, err := config.Load(config.LoadOptions{Environment: *environment, ConfigDir: *configDir})
if err != nil {
fmt.Fprintf(os.Stderr, "load configuration: %v\n", err)
os.Exit(1)
}
log, err := logger.New(cfg.App.Env, cfg.App.Name+"-seed")
if err != nil {
fmt.Fprintf(os.Stderr, "create logger: %v\n", err)
os.Exit(1)
}
defer log.Sync()
db, err := database.Open(cfg.Database, log)
if err != nil {
log.Fatal("connect database", zap.Error(err))
}
if err := seed(db); err != nil {
log.Fatal("seed pet doctor scenario", zap.Error(err))
}
log.Info("pet doctor scenario is ready")
}
func seed(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
tenant, user, err := seedOwner(tx)
if err != nil {
return err
}
scenario, err := seedScenario(tx, tenant.ID, user.ID)
if err != nil {
return err
}
if err := seedFields(tx, tenant.ID, scenario.ID); err != nil {
return err
}
knowledgeIDs, err := seedKnowledgeCards(tx, tenant.ID, user.ID, scenario.ID)
if err != nil {
return err
}
if err := seedSOP(tx, tenant.ID, user.ID, scenario.ID, knowledgeIDs); err != nil {
return err
}
return nil
})
}
func seedOwner(tx *gorm.DB) (model.Tenant, model.User, error) {
var tenant model.Tenant
if err := tx.Where("slug = ?", "default").First(&tenant).Error; err != nil {
return tenant, model.User{}, fmt.Errorf("find default tenant: %w", err)
}
var user model.User
if err := tx.Table("users u").Select("u.*").Joins("JOIN tenant_members tm ON tm.user_id = u.id").Joins("JOIN roles r ON r.id = tm.role_id").Where("tm.tenant_id = ? AND r.code = ? AND u.status = ?", tenant.ID, "admin", "active").Order("u.id").First(&user).Error; err != nil {
return tenant, user, fmt.Errorf("find administrator: %w", err)
}
return tenant, user, nil
}
func seedScenario(tx *gorm.DB, tenantID, userID uint64) (model.Scenario, error) {
var scenario model.Scenario
err := tx.Where("tenant_id = ? AND name = ?", tenantID, "宠物医生问诊问药").FirstOrCreate(&scenario, model.Scenario{
TenantID: tenantID, Name: "宠物医生问诊问药", CreatedBy: userID,
}).Error
if err != nil {
return scenario, fmt.Errorf("find or create scenario: %w", err)
}
updates := map[string]interface{}{
"industry": "宠物医疗", "role_name": "宠物医生、助理、客服",
"goal": "标准化收集宠物基本信息、主诉和病史,优先识别急症,在医生评估前提供安全且一致的沟通指引。",
"trigger_text": "宠物主人通过门店、电话或在线渠道咨询症状、检查、用药或是否需要就医时进入本场景。",
"visibility": "tenant", "status": "active",
}
if err := tx.Model(&scenario).Updates(updates).Error; err != nil {
return scenario, fmt.Errorf("update scenario: %w", err)
}
return scenario, nil
}
func seedFields(tx *gorm.DB, tenantID, scenarioID uint64) error {
definitions := petFieldDefinitions()
for index, definition := range definitions {
options, _ := json.Marshal(definition.Options)
if definition.Options == nil {
options = []byte(`[]`)
}
validation, _ := json.Marshal(definition.Validation)
if definition.Validation == nil {
validation = []byte(`{}`)
}
var field model.ScenarioField
err := tx.Where("scenario_id = ? AND field_key = ?", scenarioID, definition.Key).Assign(model.ScenarioField{
TenantID: tenantID, ScenarioID: scenarioID, FieldName: definition.Name, FieldType: definition.Type,
Required: definition.Required, Options: datatypes.JSON(options), Validation: datatypes.JSON(validation), SortOrder: index,
}).FirstOrCreate(&field, model.ScenarioField{FieldKey: definition.Key}).Error
if err != nil {
return fmt.Errorf("seed field %s: %w", definition.Key, err)
}
}
return nil
}
func petFieldDefinitions() []fieldDefinition {
return []fieldDefinition{
{Key: "consultation_channel", Name: "咨询渠道", Type: "select", Required: true, Options: []string{"到店", "电话", "在线图文", "视频"}},
{Key: "visit_goal", Name: "本次咨询目的", Type: "select", Required: true, Options: []string{"症状咨询", "用药咨询", "复诊随访", "检查报告解读", "其他"}},
{Key: "pet_name", Name: "宠物名称", Type: "text", Required: true, Validation: map[string]interface{}{"max_length": 50}},
{Key: "pet_type", Name: "宠物种类", Type: "select", Required: true, Options: []string{"犬", "猫", "其他"}},
{Key: "pet_breed", Name: "品种", Type: "text", Validation: map[string]interface{}{"max_length": 50}},
{Key: "pet_age", Name: "年龄(岁)", Type: "number", Validation: map[string]interface{}{"min": 0, "max": 50}},
{Key: "pet_weight", Name: "体重kg", Type: "number", Validation: map[string]interface{}{"min": 0.01, "max": 200}},
{Key: "pet_sex", Name: "性别", Type: "select", Options: []string{"公", "母", "未知"}},
{Key: "is_neutered", Name: "是否绝育", Type: "boolean"},
{Key: "pregnancy_status", Name: "妊娠或哺乳情况", Type: "select", Options: []string{"不适用", "否", "可能妊娠", "妊娠中", "哺乳中", "不清楚"}},
{Key: "symptom", Name: "主要症状", Type: "textarea", Required: true, Validation: map[string]interface{}{"max_length": 1000}},
{Key: "symptom_duration", Name: "症状开始时间及持续时长", Type: "text", Required: true, Validation: map[string]interface{}{"max_length": 100}},
{Key: "symptom_trend", Name: "症状变化趋势", Type: "select", Required: true, Options: []string{"好转", "稳定", "反复", "加重", "不清楚"}},
{Key: "prior_visit", Name: "本次问题是否已就诊", Type: "boolean"},
{Key: "breathing_difficulty", Name: "是否呼吸困难", Type: "boolean", Required: true},
{Key: "active_bleeding", Name: "是否持续出血", Type: "boolean", Required: true},
{Key: "convulsion", Name: "是否抽搐或意识异常", Type: "boolean", Required: true},
{Key: "severe_trauma", Name: "是否遭遇严重外伤或高处坠落", Type: "boolean", Required: true},
{Key: "unable_to_urinate", Name: "是否无法排尿", Type: "boolean", Required: true},
{Key: "toxin_exposure", Name: "是否可能误食毒物或异物", Type: "boolean", Required: true},
{Key: "repeated_vomiting", Name: "是否持续呕吐或干呕", Type: "boolean", Required: true},
{Key: "severe_pain", Name: "是否剧烈疼痛或腹部明显膨大", Type: "boolean", Required: true},
{Key: "heatstroke_sign", Name: "是否疑似中暑或体温异常升高", Type: "boolean", Required: true},
{Key: "unable_to_stand", Name: "是否虚弱到无法站立", Type: "boolean", Required: true},
{Key: "has_emergency_sign", Name: "是否存在其他危及生命的表现", Type: "boolean", Required: true},
{Key: "appetite", Name: "食欲情况", Type: "select", Options: []string{"正常", "下降", "完全不吃"}},
{Key: "water_intake", Name: "饮水情况", Type: "select", Options: []string{"正常", "增多", "减少", "完全不喝", "不清楚"}},
{Key: "spirit_status", Name: "精神状态", Type: "select", Options: []string{"正常", "较差", "嗜睡或无法站立"}},
{Key: "vomiting", Name: "是否呕吐", Type: "boolean"},
{Key: "diarrhea", Name: "是否腹泻", Type: "boolean"},
{Key: "urination_status", Name: "排尿情况", Type: "select", Options: []string{"正常", "增多", "减少", "频繁少量", "带血", "不清楚"}},
{Key: "stool_status", Name: "排便情况", Type: "select", Options: []string{"正常", "稀软", "水样", "带血或黑便", "便秘", "不清楚"}},
{Key: "measured_temperature", Name: "实测体温(℃)", Type: "number", Validation: map[string]interface{}{"min": 30, "max": 45}},
{Key: "chronic_disease_history", Name: "既往疾病及手术史", Type: "textarea", Validation: map[string]interface{}{"max_length": 1000}},
{Key: "vaccination_status", Name: "免疫情况", Type: "select", Options: []string{"按期完成", "未完成", "逾期", "不清楚", "不适用"}},
{Key: "medication_history", Name: "近期用药、驱虫药和保健品", Type: "textarea", Validation: map[string]interface{}{"max_length": 1000}},
{Key: "allergy_history", Name: "已知药物过敏或不良反应史", Type: "textarea", Validation: map[string]interface{}{"max_length": 500}},
{Key: "wants_medication", Name: "是否咨询具体用药", Type: "boolean", Required: true},
{Key: "medication_request_type", Name: "用药咨询类型", Type: "select", Options: []string{"新症状想用药", "询问现有处方用法", "续方或复购", "漏服或多服", "服药后异常", "其他"}},
{Key: "requested_medication", Name: "咨询的药品名称及规格", Type: "text", Validation: map[string]interface{}{"max_length": 200}},
{Key: "prescribed_by_vet", Name: "是否由宠物医生开具", Type: "boolean"},
{Key: "has_taken_medication", Name: "是否已经使用该药", Type: "boolean"},
{Key: "dose_and_time", Name: "已用剂量、次数及最后用药时间", Type: "textarea", Validation: map[string]interface{}{"max_length": 500}},
{Key: "human_medication_exposure", Name: "是否使用或误食人用药", Type: "boolean", Required: true},
{Key: "suspected_overdose", Name: "是否可能过量或重复用药", Type: "boolean", Required: true},
{Key: "adverse_reaction", Name: "用药后是否出现异常反应", Type: "boolean", Required: true},
{Key: "adverse_reaction_detail", Name: "异常反应描述", Type: "textarea", Validation: map[string]interface{}{"max_length": 1000}},
}
}
func seedKnowledgeCards(tx *gorm.DB, tenantID, userID, scenarioID uint64) (map[string]uint64, error) {
definitions := map[string]map[string]interface{}{
"线上问诊边界": {
"standard_copy": "线上沟通用于收集信息和判断紧急程度,不能替代体格检查、检验和影像检查。医生需要结合完整病史及检查结果后才能给出诊断和治疗方案。",
"forbidden_copy": "不要仅凭文字、照片或单个症状作出确定诊断,也不要承诺某种处理一定有效。",
"risk_note": "信息不完整、症状持续加重或无法准确观察时,应建议尽快到院评估。",
},
"急症红旗征象": {
"standard_copy": "出现呼吸困难、持续出血、抽搐或意识异常、严重外伤、无法排尿、持续呕吐或干呕、剧烈疼痛、腹部明显膨大、疑似中暑、无法站立、误食毒物或异物等情况时,应立即建议就近急诊或转诊,并提前联系接诊机构。",
"forbidden_copy": "不要承诺在家观察一定安全;不要在线给出能够替代急诊检查的判断。",
"risk_note": "急症分支优先级最高,不得因继续询问常规病史而延误就医。",
},
"问诊用药安全原则": {
"standard_copy": "用药需要结合物种、年龄、体重、既往病史、正在使用的药物和必要检查,由宠物医生评估后确定。请勿自行增加剂量、混用药物或使用人用药。",
"forbidden_copy": "未完成医生评估前,不给出具体处方药名称、剂量和疗程承诺。",
"risk_note": "对乙酰氨基酚、布洛芬等常见人用药可能对宠物造成严重伤害;如已误服,应按急症处理。",
},
"观察与复诊提示": {
"standard_copy": "请按医生要求记录精神、食欲、饮水、排尿、排便、呕吐次数及症状变化。若症状加重、出现新的急症表现,或在医生建议的观察时间内未改善,应尽快复诊。",
"forbidden_copy": "不要用固定天数替代医生根据病情给出的复诊时间,也不要因一次短暂好转自行停药。",
"risk_note": "离开本次沟通前,应让宠物主人复述下一步安排和需要立即就医的触发条件。",
},
}
result := make(map[string]uint64, len(definitions))
for title, content := range definitions {
var card model.KnowledgeCard
err := tx.Where("tenant_id = ? AND scenario_id = ? AND title = ?", tenantID, scenarioID, title).FirstOrCreate(&card, model.KnowledgeCard{
TenantID: tenantID, ScenarioID: scenarioID, Title: title, Status: "published", CreatedBy: userID,
}).Error
if err != nil {
return nil, fmt.Errorf("seed knowledge card %s: %w", title, err)
}
if err := tx.Model(&card).Update("status", "published").Error; err != nil {
return nil, err
}
contentJSON, _ := json.Marshal(content)
if err := publishSeedKnowledgeVersion(tx, tenantID, card.ID, contentJSON); err != nil {
return nil, fmt.Errorf("seed knowledge card version %s: %w", title, err)
}
result[title] = card.ID
}
return result, nil
}
func publishSeedKnowledgeVersion(tx *gorm.DB, tenantID, cardID uint64, contentJSON []byte) error {
var latest model.KnowledgeCardVersion
err := tx.Where("knowledge_card_id = ? AND tenant_id = ?", cardID, tenantID).Order("version DESC").First(&latest).Error
if err == gorm.ErrRecordNotFound {
return tx.Create(&model.KnowledgeCardVersion{
TenantID: tenantID, KnowledgeCardID: cardID, Version: 1,
Content: datatypes.JSON(contentJSON), Status: "published",
}).Error
}
if err != nil {
return err
}
if sameJSON(latest.Content, contentJSON) {
if err := tx.Model(&model.KnowledgeCardVersion{}).Where("knowledge_card_id = ? AND tenant_id = ? AND id <> ? AND status = ?", cardID, tenantID, latest.ID, "published").Update("status", "superseded").Error; err != nil {
return err
}
return tx.Model(&latest).Update("status", "published").Error
}
if err := tx.Model(&model.KnowledgeCardVersion{}).Where("knowledge_card_id = ? AND tenant_id = ? AND status = ?", cardID, tenantID, "published").Update("status", "superseded").Error; err != nil {
return err
}
return tx.Create(&model.KnowledgeCardVersion{
TenantID: tenantID, KnowledgeCardID: cardID, Version: latest.Version + 1,
Content: datatypes.JSON(contentJSON), Status: "published",
}).Error
}
func sameJSON(left, right []byte) bool {
var leftValue interface{}
var rightValue interface{}
if json.Unmarshal(left, &leftValue) != nil || json.Unmarshal(right, &rightValue) != nil {
return false
}
return reflect.DeepEqual(leftValue, rightValue)
}
func seedSOP(tx *gorm.DB, tenantID, userID, scenarioID uint64, knowledgeIDs map[string]uint64) error {
var sop model.SOP
err := tx.Where("tenant_id = ? AND scenario_id = ? AND name = ?", tenantID, scenarioID, "宠物问诊问药标准 SOP").FirstOrCreate(&sop, model.SOP{
TenantID: tenantID, ScenarioID: scenarioID, Name: "宠物问诊问药标准 SOP", CreatedBy: userID,
}).Error
if err != nil {
return fmt.Errorf("find or create SOP: %w", err)
}
if err := tx.Model(&sop).Updates(map[string]interface{}{
"description": "标准化采集宠物主诉和病史,通过急症及用药风险双重分流,形成医生评估、转诊和复诊闭环。",
"status": "published",
}).Error; err != nil {
return err
}
var seededPublished int64
if err := tx.Table("sop_nodes n").Joins("JOIN sop_versions v ON v.id = n.sop_version_id").Where(
"v.sop_id = ? AND n.node_key = ? AND JSON_UNQUOTE(JSON_EXTRACT(n.config, '$.seed_key')) = ?",
sop.ID, "start", "pet-doctor-v5",
).Count(&seededPublished).Error; err != nil {
return err
}
if seededPublished > 0 {
return nil
}
var version model.SOPVersion
err = tx.Where("sop_id = ? AND tenant_id = ? AND status = ?", sop.ID, tenantID, "draft").Order("version DESC").First(&version).Error
if err == gorm.ErrRecordNotFound {
var maxVersion int
if err := tx.Model(&model.SOPVersion{}).Where("sop_id = ?", sop.ID).Select("COALESCE(MAX(version), 0)").Scan(&maxVersion).Error; err != nil {
return err
}
version = model.SOPVersion{TenantID: tenantID, SOPID: sop.ID, Version: maxVersion + 1, Status: "draft", StartNodeKey: "start", CreatedBy: userID}
if err := tx.Create(&version).Error; err != nil {
return err
}
} else if err != nil {
return err
}
nodes := petNodes(knowledgeIDs)
edges := petEdges()
if err := tx.Where("sop_version_id = ?", version.ID).Delete(&model.SOPEdge{}).Error; err != nil {
return err
}
if err := tx.Where("sop_version_id = ?", version.ID).Delete(&model.SOPNode{}).Error; err != nil {
return err
}
for index, definition := range nodes {
configJSON, _ := json.Marshal(definition.Config)
node := model.SOPNode{
TenantID: tenantID, SOPVersionID: version.ID, NodeKey: definition.Key, Type: definition.Type,
Title: definition.Title, Content: definition.Content, Config: datatypes.JSON(configJSON), PositionX: 0, PositionY: index * 120,
}
if err := tx.Create(&node).Error; err != nil {
return fmt.Errorf("create node %s: %w", definition.Key, err)
}
}
for _, definition := range edges {
conditionJSON, _ := json.Marshal(definition.Condition)
edge := model.SOPEdge{
TenantID: tenantID, SOPVersionID: version.ID, SourceNodeKey: definition.Source, TargetNodeKey: definition.Target,
Condition: datatypes.JSON(conditionJSON), Priority: definition.Priority,
}
if err := tx.Create(&edge).Error; err != nil {
return fmt.Errorf("create edge %s -> %s: %w", definition.Source, definition.Target, err)
}
}
if err := sopservice.BindKnowledgeVersions(tx, tenantID, version.ID); err != nil {
return fmt.Errorf("bind knowledge versions: %w", err)
}
now := time.Now()
if err := tx.Model(&model.SOPVersion{}).Where("sop_id = ? AND id <> ? AND status = ?", sop.ID, version.ID, "published").Update("status", "superseded").Error; err != nil {
return err
}
if err := tx.Model(&version).Updates(map[string]interface{}{
"status": "published", "start_node_key": "start", "published_at": &now, "reviewed_by": userID,
}).Error; err != nil {
return err
}
payload, _ := json.Marshal(map[string]interface{}{"scenario": "宠物医生问诊问药", "version": version.Version})
return tx.Create(&model.AuditLog{
TenantID: tenantID, UserID: userID, Action: "seed", Resource: "sop", ResourceID: sop.ID, Payload: datatypes.JSON(payload),
}).Error
}
func petNodes(knowledgeIDs map[string]uint64) []nodeDefinition {
return []nodeDefinition{
{Key: "start", Type: "start", Title: "开始", Config: map[string]interface{}{"seed_key": "pet-doctor-v5"}},
{Key: "opening", Type: "message", Title: "说明问诊流程与边界", Content: "您好,我会先记录本次咨询目的、宠物基本信息和症状,优先排查是否需要立即就医,再将完整信息提交给宠物医生评估。线上沟通不能替代医生检查。", Config: map[string]interface{}{}},
{Key: "consultation_context", Type: "form", Title: "确认咨询背景", Content: "请确认本次咨询渠道和主要目的,便于医生判断沟通重点。", Config: map[string]interface{}{"field_keys": []string{"consultation_channel", "visit_goal"}}},
{Key: "basic_info", Type: "form", Title: "采集宠物基本信息", Content: "请确认宠物名称、种类、品种、年龄、体重、性别、绝育以及妊娠或哺乳情况;不确定的信息可以留空。", Config: map[string]interface{}{"field_keys": []string{"pet_name", "pet_type", "pet_breed", "pet_age", "pet_weight", "pet_sex", "is_neutered", "pregnancy_status"}}},
{Key: "chief_complaint", Type: "form", Title: "记录主诉与变化", Content: "请让宠物主人按时间顺序描述主要症状、开始时间、变化趋势,以及是否已经就诊。", Config: map[string]interface{}{"field_keys": []string{"symptom", "symptom_duration", "symptom_trend", "prior_visit"}}},
{Key: "emergency_screen", Type: "form", Title: "筛查急症红旗", Content: "请逐项询问,不要仅凭主人主动描述判断。任意一项为“是”都应停止常规问诊并优先转诊。", Config: map[string]interface{}{"field_keys": []string{"breathing_difficulty", "active_bleeding", "convulsion", "severe_trauma", "unable_to_urinate", "toxin_exposure", "repeated_vomiting", "severe_pain", "heatstroke_sign", "unable_to_stand", "has_emergency_sign"}}},
{Key: "emergency_check", Type: "condition", Title: "判断是否需要立即转诊", Content: "系统根据急症筛查结果自动分流。", Config: map[string]interface{}{"risk_level": "high"}},
{Key: "emergency_guidance", Type: "knowledge", Title: "告知急症处置原则", Content: "请按知识卡向宠物主人说明急症风险和立即就医要求。", Config: map[string]interface{}{"knowledge_card_id": knowledgeIDs["急症红旗征象"]}},
{Key: "emergency_escalate", Type: "escalate", Title: "立即急诊或转诊", Content: "请停止常规线上问诊,建议立即前往最近的宠物急诊并提前联系接诊机构。保持宠物安静和通风,除接诊医生明确指导外,不要自行喂药、催吐或强行喂食,并携带可疑药物、毒物包装及既往资料。", Config: map[string]interface{}{"action": "urgent_referral"}},
{Key: "current_status", Type: "form", Title: "补充当前生命状态", Content: "继续了解食欲、饮水、精神、呕吐、腹泻、排尿、排便和实测体温。未测体温时请留空,不要凭触摸估计。", Config: map[string]interface{}{"field_keys": []string{"appetite", "water_intake", "spirit_status", "vomiting", "diarrhea", "urination_status", "stool_status", "measured_temperature"}}},
{Key: "medical_history", Type: "form", Title: "补充既往史", Content: "请记录既往疾病和手术、免疫情况、近期所有药物及保健品,以及已知药物过敏或不良反应。", Config: map[string]interface{}{"field_keys": []string{"chronic_disease_history", "vaccination_status", "medication_history", "allergy_history"}}},
{Key: "medication_intent", Type: "question", Title: "确认用药诉求", Content: "请确认宠物主人是否正在咨询具体药物、剂量、疗程、续方、漏服或用药后异常。", Config: map[string]interface{}{"field_key": "wants_medication", "required": true}},
{Key: "medication_details", Type: "form", Title: "采集用药详情", Content: "请核对药名和规格、咨询类型、是否由宠物医生开具、是否已经使用,以及已用剂量和时间。无法确认时请查看处方或药品包装,不要猜测。", Config: map[string]interface{}{"field_keys": []string{"medication_request_type", "requested_medication", "prescribed_by_vet", "has_taken_medication", "dose_and_time"}}},
{Key: "medication_risk_screen", Type: "form", Title: "筛查用药风险", Content: "请确认是否涉及人用药、可能过量或重复用药,以及用药后异常;如有异常,请记录出现时间和具体表现。", Config: map[string]interface{}{"field_keys": []string{"human_medication_exposure", "suspected_overdose", "adverse_reaction", "adverse_reaction_detail"}}},
{Key: "medication_risk_check", Type: "condition", Title: "判断是否存在用药急症风险", Content: "系统根据用药风险信息自动分流。", Config: map[string]interface{}{"risk_level": "high"}},
{Key: "medication_escalate", Type: "escalate", Title: "立即转医生或急诊评估", Content: "存在误用人用药、可能过量或用药后异常。请立即转宠物医生评估;如出现呼吸困难、意识异常、抽搐、虚脱或症状快速加重,应立即前往急诊。请保留药品包装、处方和实际用药时间记录。", Config: map[string]interface{}{"action": "medication_risk_referral"}},
{Key: "medication_safety", Type: "knowledge", Title: "说明用药安全原则", Content: "请按知识卡说明用药边界,在医生完成评估前不要承诺具体药名、剂量或疗程。", Config: map[string]interface{}{"knowledge_card_id": knowledgeIDs["问诊用药安全原则"]}},
{Key: "consultation_boundary", Type: "knowledge", Title: "说明线上问诊边界", Content: "请说明线上沟通的作用和限制,并确认宠物主人理解下一步需要医生评估。", Config: map[string]interface{}{"knowledge_card_id": knowledgeIDs["线上问诊边界"]}},
{Key: "assessment", Type: "message", Title: "提交医生评估并明确下一步", Content: "信息已记录。请由宠物医生结合病史、体格检查和必要检验判断后续方案;向宠物主人明确是立即到院、预约就诊、按原处方处理还是居家观察,并记录医生给出的时间要求。", Config: map[string]interface{}{}},
{Key: "safety_net", Type: "knowledge", Title: "确认观察和复诊提示", Content: "请按知识卡说明观察指标、复诊时间和立即就医触发条件,并请宠物主人复述确认。", Config: map[string]interface{}{"knowledge_card_id": knowledgeIDs["观察与复诊提示"]}},
{Key: "finish", Type: "finish", Title: "完成问诊记录", Content: "本次初步问诊信息已记录,请按宠物医生给出的时间和方式安排检查、复诊或治疗;情况变化时及时重新联系或立即就医。", Config: map[string]interface{}{}},
}
}
func petEdges() []edgeDefinition {
emergencyRules := []interface{}{
map[string]interface{}{"field": "breathing_difficulty", "operator": "equals", "value": true},
map[string]interface{}{"field": "active_bleeding", "operator": "equals", "value": true},
map[string]interface{}{"field": "convulsion", "operator": "equals", "value": true},
map[string]interface{}{"field": "severe_trauma", "operator": "equals", "value": true},
map[string]interface{}{"field": "unable_to_urinate", "operator": "equals", "value": true},
map[string]interface{}{"field": "toxin_exposure", "operator": "equals", "value": true},
map[string]interface{}{"field": "repeated_vomiting", "operator": "equals", "value": true},
map[string]interface{}{"field": "severe_pain", "operator": "equals", "value": true},
map[string]interface{}{"field": "heatstroke_sign", "operator": "equals", "value": true},
map[string]interface{}{"field": "unable_to_stand", "operator": "equals", "value": true},
map[string]interface{}{"field": "has_emergency_sign", "operator": "equals", "value": true},
}
medicationRiskRules := []interface{}{
map[string]interface{}{"field": "human_medication_exposure", "operator": "equals", "value": true},
map[string]interface{}{"field": "suspected_overdose", "operator": "equals", "value": true},
map[string]interface{}{"field": "adverse_reaction", "operator": "equals", "value": true},
}
return []edgeDefinition{
{Source: "start", Target: "opening", Condition: map[string]interface{}{}},
{Source: "opening", Target: "consultation_context", Condition: map[string]interface{}{}},
{Source: "consultation_context", Target: "basic_info", Condition: map[string]interface{}{}},
{Source: "basic_info", Target: "chief_complaint", Condition: map[string]interface{}{}},
{Source: "chief_complaint", Target: "emergency_screen", Condition: map[string]interface{}{}},
{Source: "emergency_screen", Target: "emergency_check", Condition: map[string]interface{}{}},
{Source: "emergency_check", Target: "emergency_guidance", Condition: map[string]interface{}{"any": emergencyRules}, Priority: 0},
{Source: "emergency_check", Target: "current_status", Condition: map[string]interface{}{}, Priority: 100},
{Source: "emergency_guidance", Target: "emergency_escalate", Condition: map[string]interface{}{}},
{Source: "current_status", Target: "medical_history", Condition: map[string]interface{}{}},
{Source: "medical_history", Target: "medication_intent", Condition: map[string]interface{}{}},
{Source: "medication_intent", Target: "medication_details", Condition: map[string]interface{}{"field": "wants_medication", "operator": "equals", "value": true}, Priority: 0},
{Source: "medication_intent", Target: "consultation_boundary", Condition: map[string]interface{}{}, Priority: 100},
{Source: "medication_details", Target: "medication_risk_screen", Condition: map[string]interface{}{}},
{Source: "medication_risk_screen", Target: "medication_risk_check", Condition: map[string]interface{}{}},
{Source: "medication_risk_check", Target: "medication_escalate", Condition: map[string]interface{}{"any": medicationRiskRules}, Priority: 0},
{Source: "medication_risk_check", Target: "medication_safety", Condition: map[string]interface{}{}, Priority: 100},
{Source: "medication_safety", Target: "consultation_boundary", Condition: map[string]interface{}{}},
{Source: "consultation_boundary", Target: "assessment", Condition: map[string]interface{}{}},
{Source: "assessment", Target: "safety_net", Condition: map[string]interface{}{}},
{Source: "safety_net", Target: "finish", Condition: map[string]interface{}{}},
}
}