feat: simplify SOP to immediate-effect configuration
This commit is contained in:
196
internal/knowledge/graph.go
Normal file
196
internal/knowledge/graph.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package knowledge
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"git.iwork-ai.com/xdc/iqudo-top1/internal/access"
|
||||
"git.iwork-ai.com/xdc/iqudo-top1/internal/audit"
|
||||
"git.iwork-ai.com/xdc/iqudo-top1/internal/auth"
|
||||
"git.iwork-ai.com/xdc/iqudo-top1/internal/model"
|
||||
"git.iwork-ai.com/xdc/iqudo-top1/internal/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var graphKeyPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]{0,63}$`)
|
||||
|
||||
type GraphInput struct {
|
||||
Items []GraphItemInput `json:"items"`
|
||||
Relations []GraphRelationInput `json:"relations"`
|
||||
Symptoms []SymptomInput `json:"symptoms"`
|
||||
}
|
||||
|
||||
type GraphItemInput struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Content map[string]interface{} `json:"content"`
|
||||
Status string `json:"status"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
type GraphRelationInput struct {
|
||||
From string `json:"from"`
|
||||
RelationType string `json:"relation_type"`
|
||||
To string `json:"to"`
|
||||
Condition map[string]interface{} `json:"condition"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
type SymptomInput struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
CopyTemplateIDs []string `json:"copy_template_ids"`
|
||||
Diseases []GraphItemInput `json:"diseases"`
|
||||
}
|
||||
|
||||
func (h *Handler) GetGraph(c *gin.Context) {
|
||||
p, _ := auth.PrincipalFromContext(c)
|
||||
scenarioID, ok := graphScenarioID(c)
|
||||
if !ok || !access.CanViewScenario(h.db, p, scenarioID) {
|
||||
if ok {
|
||||
response.Error(c, http.StatusNotFound, "NOT_FOUND", "场景不存在")
|
||||
}
|
||||
return
|
||||
}
|
||||
items := make([]model.KnowledgeItem, 0)
|
||||
relations := make([]model.KnowledgeRelation, 0)
|
||||
if err := h.db.Where("tenant_id = ? AND scenario_id = ? AND status <> ?", p.TenantID, scenarioID, "archived").Order("sort_order, id").Find(&items).Error; err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "QUERY_FAILED", "查询知识失败")
|
||||
return
|
||||
}
|
||||
if err := h.db.Where("tenant_id = ? AND scenario_id = ?", p.TenantID, scenarioID).Order("sort_order, id").Find(&relations).Error; err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "QUERY_FAILED", "查询知识关系失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"items": items, "relations": relations})
|
||||
}
|
||||
|
||||
func (h *Handler) ReplaceGraph(c *gin.Context) {
|
||||
p, _ := auth.PrincipalFromContext(c)
|
||||
scenarioID, ok := graphScenarioID(c)
|
||||
if !ok || !access.CanEditScenario(h.db, p, scenarioID) {
|
||||
if ok {
|
||||
response.Error(c, http.StatusNotFound, "NOT_FOUND", "场景不存在或不可编辑")
|
||||
}
|
||||
return
|
||||
}
|
||||
var input GraphInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "知识 JSON 格式不正确")
|
||||
return
|
||||
}
|
||||
items, relations, err := normalizeGraphInput(input)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", err.Error())
|
||||
return
|
||||
}
|
||||
err = h.db.Transaction(func(tx *gorm.DB) error {
|
||||
var oldItems []model.KnowledgeItem
|
||||
var oldRelations []model.KnowledgeRelation
|
||||
if err := tx.Where("tenant_id = ? AND scenario_id = ?", p.TenantID, scenarioID).Find(&oldItems).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("tenant_id = ? AND scenario_id = ?", p.TenantID, scenarioID).Find(&oldRelations).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("tenant_id = ? AND scenario_id = ?", p.TenantID, scenarioID).Delete(&model.KnowledgeRelation{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("tenant_id = ? AND scenario_id = ?", p.TenantID, scenarioID).Delete(&model.KnowledgeItem{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
ids := make(map[string]uint64, len(items))
|
||||
for _, item := range items {
|
||||
raw, _ := json.Marshal(item.Content)
|
||||
row := model.KnowledgeItem{TenantID: p.TenantID, ScenarioID: scenarioID, ItemKey: item.Key, Name: item.Name, Type: item.Type, Content: datatypes.JSON(raw), Status: item.Status, SortOrder: item.SortOrder}
|
||||
if err := tx.Create(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
ids[item.Key] = row.ID
|
||||
if err := audit.RecordTx(tx, p, "create", "knowledge_item", row.ID, gin.H{"scenario_id": scenarioID, "key": row.ItemKey}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, relation := range relations {
|
||||
raw, _ := json.Marshal(relation.Condition)
|
||||
row := model.KnowledgeRelation{TenantID: p.TenantID, ScenarioID: scenarioID, FromKnowledgeID: ids[relation.From], RelationType: relation.RelationType, ToKnowledgeID: ids[relation.To], Condition: datatypes.JSON(raw), SortOrder: relation.SortOrder}
|
||||
if err := tx.Create(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := audit.RecordTx(tx, p, "create", "knowledge_relation", row.ID, gin.H{"scenario_id": scenarioID, "from": relation.From, "relation_type": relation.RelationType, "to": relation.To}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, relation := range oldRelations {
|
||||
if err := audit.RecordTx(tx, p, "archive", "knowledge_relation", relation.ID, gin.H{"scenario_id": scenarioID, "from_knowledge_id": relation.FromKnowledgeID, "relation_type": relation.RelationType, "to_knowledge_id": relation.ToKnowledgeID, "sort_order": relation.SortOrder}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, item := range oldItems {
|
||||
if err := audit.RecordTx(tx, p, "archive", "knowledge_item", item.ID, gin.H{"scenario_id": scenarioID, "key": item.ItemKey, "name": item.Name, "type": item.Type, "status": item.Status, "sort_order": item.SortOrder}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "SAVE_FAILED", "保存知识关系失败")
|
||||
return
|
||||
}
|
||||
h.GetGraph(c)
|
||||
}
|
||||
|
||||
func normalizeGraphInput(input GraphInput) ([]GraphItemInput, []GraphRelationInput, error) {
|
||||
items := append([]GraphItemInput{}, input.Items...)
|
||||
relations := append([]GraphRelationInput{}, input.Relations...)
|
||||
for _, symptom := range input.Symptoms {
|
||||
items = append(items, GraphItemInput{Key: symptom.Key, Name: symptom.Name, Type: "symptom", Status: "active"})
|
||||
for _, disease := range symptom.Diseases {
|
||||
disease.Type = "disease"
|
||||
items = append(items, disease)
|
||||
relations = append(relations, GraphRelationInput{From: symptom.Key, RelationType: "possible_disease", To: disease.Key})
|
||||
}
|
||||
for _, copyKey := range symptom.CopyTemplateIDs {
|
||||
relations = append(relations, GraphRelationInput{From: symptom.Key, RelationType: "recommended_copy", To: copyKey})
|
||||
}
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
unique := make([]GraphItemInput, 0, len(items))
|
||||
for _, item := range items {
|
||||
if !graphKeyPattern.MatchString(item.Key) || item.Name == "" || item.Type == "" {
|
||||
return nil, nil, fmt.Errorf("知识 key、name 和 type 必须填写且格式正确")
|
||||
}
|
||||
if seen[item.Key] {
|
||||
continue
|
||||
}
|
||||
seen[item.Key] = true
|
||||
if item.Status == "" {
|
||||
item.Status = "active"
|
||||
}
|
||||
if item.Content == nil {
|
||||
item.Content = map[string]interface{}{}
|
||||
}
|
||||
unique = append(unique, item)
|
||||
}
|
||||
for _, relation := range relations {
|
||||
if !seen[relation.From] || !seen[relation.To] || relation.RelationType == "" {
|
||||
return nil, nil, fmt.Errorf("知识关系引用了不存在的 key")
|
||||
}
|
||||
}
|
||||
return unique, relations, nil
|
||||
}
|
||||
|
||||
func graphScenarioID(c *gin.Context) (uint64, bool) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
response.Error(c, http.StatusBadRequest, "INVALID_ID", "场景 ID 不正确")
|
||||
return 0, false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
Reference in New Issue
Block a user