197 lines
7.5 KiB
Go
197 lines
7.5 KiB
Go
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
|
|
}
|