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{}{}}, } }