package run import ( "encoding/json" "fmt" "regexp" "sort" "strings" "git.iwork-ai.com/xdc/iqudo-top1/internal/model" "gorm.io/gorm" ) var knowledgePlaceholderPattern = regexp.MustCompile(`\{\{\s*(?:input|derived|form)\.([A-Za-z][A-Za-z0-9_]*)\s*\}\}`) type NodeView struct { NodeKey string `json:"node_key"` Type string `json:"type"` Title string `json:"title"` Content string `json:"content"` Config json.RawMessage `json:"config,omitempty"` Fields []PublicFieldView `json:"fields,omitempty"` Presentation *NodePresentation `json:"presentation,omitempty"` Outputs []KnowledgeGroup `json:"outputs"` Collection *KnowledgeCollectionView `json:"collection,omitempty"` } type PublicFieldView struct { Key string `json:"key"` Name string `json:"name"` Type string `json:"type"` Required bool `json:"required"` Options json.RawMessage `json:"options"` Validation json.RawMessage `json:"validation"` } type KnowledgeGroup struct { Key string `json:"key"` Name string `json:"name"` Type string `json:"type"` Content json.RawMessage `json:"content"` Relations map[string][]KnowledgeItemView `json:"relations"` Suggested bool `json:"suggested,omitempty"` } type KnowledgeItemView struct { Key string `json:"key"` Name string `json:"name"` Type string `json:"type"` Content json.RawMessage `json:"content"` } type knowledgeNodeConfig struct { KnowledgeSelector *knowledgeSelector `json:"knowledge_selector"` KnowledgeCollection *knowledgeCollectionConfig `json:"knowledge_collection"` } type knowledgeCollectionConfig struct { ContextFieldKeys []string `json:"context_field_keys"` ContextTitle string `json:"context_title"` ContextHint string `json:"context_hint"` SelectionTitle string `json:"selection_title"` SelectionHint string `json:"selection_hint"` Steps []knowledgeCollectionStep `json:"steps"` } type knowledgeCollectionStep struct { FieldKey string `json:"field_key"` Name string `json:"name"` Root bool `json:"root"` CandidateScope string `json:"candidate_scope"` KnowledgeTypes []string `json:"knowledge_types"` FromField string `json:"from_field"` RelationType string `json:"relation_type"` Required bool `json:"required"` Multiple bool `json:"multiple"` } type KnowledgeCollectionView struct { ContextFields []PublicFieldView `json:"context_fields"` ContextTitle string `json:"context_title"` ContextHint string `json:"context_hint"` SelectionTitle string `json:"selection_title"` SelectionHint string `json:"selection_hint"` Steps []KnowledgeCollectionStepView `json:"steps"` } type KnowledgeCollectionStepView struct { FieldKey string `json:"field_key"` Name string `json:"name"` Required bool `json:"required"` Multiple bool `json:"multiple"` FromField string `json:"from_field,omitempty"` Options []KnowledgeCollectionOption `json:"options"` } type KnowledgeCollectionOption struct { Value string `json:"value"` Label string `json:"label"` Parents []string `json:"parents,omitempty"` Suggested bool `json:"suggested,omitempty"` } type knowledgeSelector struct { DerivedField string `json:"derived_field"` AnswerField string `json:"answer_field"` CandidateScope string `json:"candidate_scope"` KnowledgeTypes []string `json:"knowledge_types"` KnowledgeKeys []string `json:"knowledge_keys"` RelationTypes []string `json:"relation_types"` RelationLabels map[string]string `json:"relation_labels"` } func (h *Handler) nodeView(db *gorm.DB, node model.SOPNode, tenantID uint64, answers map[string]interface{}) (NodeView, error) { view := NodeView{NodeKey: node.NodeKey, Type: node.Type, Title: node.Title, Content: node.Content, Config: json.RawMessage(node.Config)} if node.Type == "start" { presentation, err := loadStartPresentation(db, node, tenantID, answers) if err != nil { return view, err } view.Presentation = presentation } if node.Type == "question" || node.Type == "choice" || node.Type == "form" { fields, err := loadPublicNodeFields(db, node, tenantID) if err != nil { return view, err } view.Fields = fields } if node.Type != "knowledge" { return view, nil } var config knowledgeNodeConfig if err := json.Unmarshal(node.Config, &config); err != nil { return view, err } if config.KnowledgeSelector != nil { outputs, err := loadKnowledgeOutputs(db, node, tenantID, answers, *config.KnowledgeSelector) view.Outputs = outputs if err != nil { return view, err } if config.KnowledgeCollection != nil { collection, err := loadKnowledgeCollection(db, node, tenantID, *config.KnowledgeCollection, outputs, answers) view.Collection = &collection return view, err } return view, nil } return view, nil } func loadKnowledgeCollection(db *gorm.DB, node model.SOPNode, tenantID uint64, config knowledgeCollectionConfig, roots []KnowledgeGroup, context map[string]interface{}) (KnowledgeCollectionView, error) { fields, err := loadPublicFieldsByKeys(db, node, tenantID, config.ContextFieldKeys) if err != nil { return KnowledgeCollectionView{}, err } view := KnowledgeCollectionView{ContextFields: fields, ContextTitle: config.ContextTitle, ContextHint: config.ContextHint, SelectionTitle: config.SelectionTitle, SelectionHint: config.SelectionHint, Steps: make([]KnowledgeCollectionStepView, 0, len(config.Steps))} childrenByParent := map[string]map[string][]string{} allItems := make([]model.KnowledgeItem, 0) if raw, ok := context["__knowledge_snapshot"].(json.RawMessage); ok && len(raw) > 0 { snapshot, parseErr := parseKnowledgeSnapshot(raw) if parseErr != nil { return KnowledgeCollectionView{}, parseErr } byID := map[uint64]model.KnowledgeItem{} for _, item := range snapshot.Items { byID[item.ID] = item if item.Status == "active" { allItems = append(allItems, item) } } for _, relation := range snapshot.Relations { from, fromOK := byID[relation.FromKnowledgeID] to, toOK := byID[relation.ToKnowledgeID] if !fromOK || !toOK || from.Status != "active" || to.Status != "active" { continue } if childrenByParent[from.Name] == nil { childrenByParent[from.Name] = map[string][]string{} } childrenByParent[from.Name][relation.RelationType] = appendUnique(childrenByParent[from.Name][relation.RelationType], to.Name) } } suggestedRoots := make(map[string]bool, len(roots)) rootOptions := make([]KnowledgeCollectionOption, 0, len(roots)) for _, root := range roots { suggestedRoots[root.Name] = root.Suggested rootOptions = append(rootOptions, KnowledgeCollectionOption{Value: root.Name, Label: root.Name, Suggested: root.Suggested}) } for _, step := range config.Steps { item := KnowledgeCollectionStepView{FieldKey: step.FieldKey, Name: step.Name, Required: step.Required, Multiple: step.Multiple, FromField: step.FromField, Options: []KnowledgeCollectionOption{}} if step.Root { item.Options = append(item.Options, rootOptions...) if step.CandidateScope == "all" { seen := map[string]bool{} for _, option := range item.Options { seen[option.Value] = true } for _, knowledgeItem := range allItems { if seen[knowledgeItem.Name] || (len(step.KnowledgeTypes) > 0 && !containsString(step.KnowledgeTypes, knowledgeItem.Type)) { continue } item.Options = append(item.Options, KnowledgeCollectionOption{Value: knowledgeItem.Name, Label: knowledgeItem.Name, Suggested: suggestedRoots[knowledgeItem.Name]}) } sort.SliceStable(item.Options, func(i, j int) bool { if item.Options[i].Suggested != item.Options[j].Suggested { return item.Options[i].Suggested } return item.Options[i].Label < item.Options[j].Label }) } } else { seen := map[string]*KnowledgeCollectionOption{} var parentStep *KnowledgeCollectionStepView for index := range view.Steps { if view.Steps[index].FieldKey == step.FromField { parentStep = &view.Steps[index] break } } if parentStep != nil { for _, parent := range parentStep.Options { for _, childName := range childrenByParent[parent.Value][step.RelationType] { option := seen[childName] if option == nil { option = &KnowledgeCollectionOption{Value: childName, Label: childName} seen[childName] = option } option.Parents = appendUnique(option.Parents, parent.Value) } } } for _, option := range seen { item.Options = append(item.Options, *option) } sort.Slice(item.Options, func(i, j int) bool { return item.Options[i].Label < item.Options[j].Label }) } view.Steps = append(view.Steps, item) } return view, nil } func loadPublicFieldsByKeys(db *gorm.DB, node model.SOPNode, tenantID uint64, keys []string) ([]PublicFieldView, error) { if len(keys) == 0 { return []PublicFieldView{}, nil } var fields []model.ScenarioField if err := db.Table("scenario_fields sf").Select("sf.*").Joins("JOIN sops s ON s.scenario_id = sf.scenario_id").Where("s.id = ? AND sf.tenant_id = ? AND sf.field_key IN ?", node.SOPID, tenantID, keys).Find(&fields).Error; err != nil { return nil, err } byKey := map[string]model.ScenarioField{} for _, field := range fields { byKey[field.FieldKey] = field } result := make([]PublicFieldView, 0, len(keys)) for _, key := range keys { if field, ok := byKey[key]; ok { result = append(result, PublicFieldView{Key: field.FieldKey, Name: field.FieldName, Type: field.FieldType, Required: field.Required, Options: json.RawMessage(field.Options), Validation: json.RawMessage(field.Validation)}) } } return result, nil } func appendUnique(values []string, value string) []string { for _, existing := range values { if existing == value { return values } } return append(values, value) } func loadPublicNodeFields(db *gorm.DB, node model.SOPNode, tenantID uint64) ([]PublicFieldView, error) { var config answerNodeConfig if err := json.Unmarshal(node.Config, &config); err != nil { return nil, err } keys := config.FieldKeys if config.FieldKey != "" { keys = []string{config.FieldKey} } if len(keys) == 0 { return nil, nil } var fields []model.ScenarioField if err := db.Table("scenario_fields sf").Select("sf.*").Joins("JOIN sops s ON s.scenario_id = sf.scenario_id").Where("s.id = ? AND sf.tenant_id = ? AND sf.field_key IN ?", node.SOPID, tenantID, keys).Find(&fields).Error; err != nil { return nil, err } byKey := make(map[string]model.ScenarioField, len(fields)) for _, field := range fields { byKey[field.FieldKey] = field } views := make([]PublicFieldView, 0, len(keys)) for _, key := range keys { field, ok := byKey[key] if !ok { continue } views = append(views, PublicFieldView{Key: field.FieldKey, Name: field.FieldName, Type: field.FieldType, Required: field.Required || config.Required || containsString(config.RequiredFieldKeys, key), Options: json.RawMessage(field.Options), Validation: json.RawMessage(field.Validation)}) } return views, nil } func loadKnowledgeOutputs(db *gorm.DB, node model.SOPNode, tenantID uint64, context map[string]interface{}, selector knowledgeSelector) ([]KnowledgeGroup, error) { var sopRow struct{ ScenarioID uint64 } if err := db.Table("sop_nodes n").Select("s.scenario_id").Joins("JOIN sops s ON s.id = n.sop_id").Where("n.id = ? AND n.tenant_id = ?", node.ID, tenantID).Scan(&sopRow).Error; err != nil { return nil, err } return loadKnowledgeOutputsForScenario(db, sopRow.ScenarioID, tenantID, context, selector) } func loadKnowledgeOutputsForScenario(db *gorm.DB, scenarioID, tenantID uint64, context map[string]interface{}, selector knowledgeSelector) ([]KnowledgeGroup, error) { keys := append([]string{}, selector.KnowledgeKeys...) if selector.DerivedField != "" { value, _ := lookupContextValue(context, "derived."+selector.DerivedField) keys = append(keys, stringSlice(value)...) } if selector.AnswerField != "" { value, _ := lookupContextValue(context, "form."+selector.AnswerField) keys = append(keys, stringSlice(value)...) } if len(keys) == 0 && selector.CandidateScope != "all" { return []KnowledgeGroup{}, nil } keySet := make(map[string]bool, len(keys)) for _, key := range keys { keySet[key] = true } items := make([]model.KnowledgeItem, 0) relations := make([]model.KnowledgeRelation, 0) snapshotTargets := map[uint64]model.KnowledgeItem{} if raw, ok := context["__knowledge_snapshot"].(json.RawMessage); ok && len(raw) > 0 { snapshot, err := parseKnowledgeSnapshot(raw) if err != nil { return nil, err } for _, item := range snapshot.Items { snapshotTargets[item.ID] = item if item.Status != "active" { continue } if selector.CandidateScope != "all" && len(keys) > 0 && !knowledgeCandidateMatches(keySet, item) { continue } if len(selector.KnowledgeTypes) > 0 && !containsString(selector.KnowledgeTypes, item.Type) { continue } items = append(items, item) } for _, relation := range snapshot.Relations { if len(selector.RelationTypes) > 0 && !containsString(selector.RelationTypes, relation.RelationType) { continue } relations = append(relations, relation) } } else { query := db.Where("tenant_id = ? AND scenario_id = ? AND status = ?", tenantID, scenarioID, "active") if selector.CandidateScope != "all" && len(keys) > 0 { query = query.Where("item_key IN ? OR name IN ?", keys, keys) } if len(selector.KnowledgeTypes) > 0 { query = query.Where("type IN ?", selector.KnowledgeTypes) } if err := query.Order("sort_order, id").Find(&items).Error; err != nil { return nil, err } } ids := make([]uint64, 0, len(items)) for _, item := range items { if selector.CandidateScope == "all" || len(keySet) == 0 || knowledgeCandidateMatches(keySet, item) { ids = append(ids, item.ID) } } if len(relations) == 0 && len(ids) > 0 { rq := db.Where("tenant_id = ? AND scenario_id = ? AND from_knowledge_id IN ?", tenantID, scenarioID, ids) if len(selector.RelationTypes) > 0 { rq = rq.Where("relation_type IN ?", selector.RelationTypes) } if err := rq.Order("sort_order,id").Find(&relations).Error; err != nil { return nil, err } } targetIDs := make([]uint64, 0, len(relations)) for _, rel := range relations { targetIDs = append(targetIDs, rel.ToKnowledgeID) } targets := make([]model.KnowledgeItem, 0) if len(snapshotTargets) > 0 { for _, id := range targetIDs { if target, ok := snapshotTargets[id]; ok && target.Status == "active" { targets = append(targets, target) } } } else if len(targetIDs) > 0 { if err := db.Where("tenant_id = ? AND id IN ? AND status = ?", tenantID, targetIDs, "active").Find(&targets).Error; err != nil { return nil, err } } targetMap := map[uint64]model.KnowledgeItem{} for _, target := range targets { targetMap[target.ID] = target } relationMap := map[uint64]map[string][]KnowledgeItemView{} for _, rel := range relations { matched, err := matchCondition(json.RawMessage(rel.Condition), context) if err != nil { return nil, fmt.Errorf("知识关系 %d 条件不正确: %w", rel.ID, err) } if !matched { continue } target, ok := targetMap[rel.ToKnowledgeID] if !ok { continue } if relationMap[rel.FromKnowledgeID] == nil { relationMap[rel.FromKnowledgeID] = map[string][]KnowledgeItemView{} } relationMap[rel.FromKnowledgeID][rel.RelationType] = append(relationMap[rel.FromKnowledgeID][rel.RelationType], KnowledgeItemView{Key: target.ItemKey, Name: target.Name, Type: target.Type, Content: renderKnowledgeContent(target.Content, context)}) } outputs := make([]KnowledgeGroup, 0, len(items)) for _, item := range items { outputs = append(outputs, KnowledgeGroup{Key: item.ItemKey, Name: item.Name, Type: item.Type, Content: renderKnowledgeContent(item.Content, context), Relations: relationMap[item.ID], Suggested: knowledgeCandidateMatches(keySet, item)}) } return outputs, nil } func knowledgeCandidateMatches(candidates map[string]bool, item model.KnowledgeItem) bool { return candidates[item.ItemKey] || candidates[item.Name] } func containsString(values []string, target string) bool { for _, value := range values { if value == target { return true } } return false } func renderKnowledgeContent(raw []byte, context map[string]interface{}) json.RawMessage { var value interface{} if json.Unmarshal(raw, &value) != nil { return json.RawMessage(raw) } value = renderKnowledgeValue(value, context) rendered, _ := json.Marshal(value) return rendered } func renderKnowledgeValue(value interface{}, context map[string]interface{}) interface{} { switch typed := value.(type) { case string: return knowledgePlaceholderPattern.ReplaceAllStringFunc(typed, func(token string) string { match := knowledgePlaceholderPattern.FindStringSubmatch(token) if len(match) != 2 { return token } resolved, ok := lookupContextValue(context, tokenNamespaceKey(token, match[1])) if !ok || resolved == nil { return "未提供" } if values, ok := resolved.([]interface{}); ok { parts := make([]string, 0, len(values)) for _, item := range values { parts = append(parts, fmt.Sprint(item)) } return strings.Join(parts, "、") } return fmt.Sprint(resolved) }) case []interface{}: for index, item := range typed { typed[index] = renderKnowledgeValue(item, context) } return typed case map[string]interface{}: for key, item := range typed { typed[key] = renderKnowledgeValue(item, context) } return typed default: return value } } func tokenNamespaceKey(token, key string) string { trimmed := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(token, "{{"), "}}")) if strings.Contains(trimmed, ".") { return trimmed } return key } func stringSlice(value interface{}) []string { result := []string{} switch values := value.(type) { case []interface{}: for _, item := range values { if text, ok := item.(string); ok { result = append(result, text) } } case []string: return values case string: return []string{values} } return result }