package doc import ( "context" "encoding/json" "fmt" "log" "regexp" "strings" "sync" "time" "app/cfg" M "app/models" "github.com/openai/openai-go" ) // TODO 另一种思路是先粗糙生成一些需求再建立联系 type AgenticDemandProcessor struct { analyzer *DemandAnalyzer tasks map[string]*M.Doc processingDoc map[string]context.CancelFunc mutex sync.Mutex logger *log.Logger } var agenticDemandProcessor *AgenticDemandProcessor var agenticDemandProcessorOnce sync.Once func GetAgenticDemandProcessor() *AgenticDemandProcessor { agenticDemandProcessorOnce.Do(func() { analyzer, err := NewDemandAnalyzer() if err != nil { log.Fatalf("初始化需求分析器失败: %v", err) } agenticDemandProcessor = &AgenticDemandProcessor{ analyzer: analyzer, tasks: make(map[string]*M.Doc), processingDoc: make(map[string]context.CancelFunc), mutex: sync.Mutex{}, logger: setupLogger(), } }) return agenticDemandProcessor } // AddTask 添加需求文档处理任务 func (dp *AgenticDemandProcessor) AddTask(doc *M.Doc, filePath string) { dp.mutex.Lock() defer dp.mutex.Unlock() dp.tasks[doc.ID] = doc go dp.processDoc(doc, filePath) } // CancelDocProcessing 取消文档处理 func (dp *AgenticDemandProcessor) CancelDocProcessing(docID string) { dp.mutex.Lock() defer dp.mutex.Unlock() if cancel, exists := dp.processingDoc[docID]; exists { cancel() delete(dp.processingDoc, docID) } } func (dp *AgenticDemandProcessor) splitTextIntoChunks(content string) ([]string, error) { // 直接在Go中实现文本分块,不再调用Python脚本 // 设置每个块的最大和最小字符数 maxChunkSize := 4000 minChunkSize := 500 // 设置最小块大小为500字符 // 定义标点符号正则表达式,包括中英文标点 punctuationPattern := "[。!?.!?;;]" re := regexp.MustCompile(punctuationPattern) // 优化的分块方法:从maxChunkSize位置向前查找最近的标点 var chunks []string remaining := content for len(remaining) > 0 { if len(remaining) <= maxChunkSize { // 如果剩余内容不超过最大块大小,直接添加 chunks = append(chunks, remaining) break } // 确定切分位置:从maxChunkSize位置向前查找最近的标点 cutPos := maxChunkSize if cutPos > len(remaining) { cutPos = len(remaining) } // 在maxChunkSize范围内查找最后一个标点 searchEnd := cutPos searchStart := cutPos - 100 // 向前查找100个字符范围内的标点 if searchStart < 0 { searchStart = 0 } // 在指定范围内查找最后一个标点 searchText := remaining[searchStart:searchEnd] allMatches := re.FindAllStringIndex(searchText, -1) if len(allMatches) > 0 { // 找到了标点,使用最后一个标点作为切分点 lastMatch := allMatches[len(allMatches)-1] cutPos = searchStart + lastMatch[1] // 使用标点后的位置作为切分点 // 检查切分后的块是否太小 if cutPos < minChunkSize { // 如果太小,直接使用maxChunkSize作为切分点 cutPos = maxChunkSize } } else if searchStart > 0 { // 如果在100字符范围内没找到,扩大搜索范围到整个maxChunkSize searchText = remaining[:searchEnd] allMatches = re.FindAllStringIndex(searchText, -1) if len(allMatches) > 0 { lastMatch := allMatches[len(allMatches)-1] cutPos = lastMatch[1] // 使用标点后的位置作为切分点 // 检查切分后的块是否太小 if cutPos < minChunkSize { // 如果太小,直接使用maxChunkSize作为切分点 cutPos = maxChunkSize } } else { // 如果仍然没找到标点,就使用maxChunkSize作为切分点 cutPos = maxChunkSize } } else { // 如果无法向前查找(已经在文本开头),直接使用maxChunkSize cutPos = maxChunkSize } // 添加当前块并继续处理剩余内容 chunks = append(chunks, remaining[:cutPos]) remaining = remaining[cutPos:] } return chunks, nil } // 修改 processDoc 方法 func (dp *AgenticDemandProcessor) processDoc(doc *M.Doc, filePath string) { dp.logger.Printf("开始处理需求文档: %s", doc.Name) // 创建可取消的上下文 ctx, cancel := context.WithCancel(context.Background()) dp.mutex.Lock() dp.processingDoc[doc.ID] = cancel dp.mutex.Unlock() defer func() { dp.mutex.Lock() delete(dp.processingDoc, doc.ID) dp.mutex.Unlock() }() // 更新文档状态为处理中 if err := cfg.DB().Model(doc).Updates(map[string]interface{}{ "analysis_completed": false, "analysis_percent": 0, "analysis_error": "", }).Error; err != nil { dp.logger.Printf("更新文档状态失败: %v", err) return } // 读取文档内容 content, err := dp.analyzer.readDocContent(filePath) if err != nil { errMsg := fmt.Sprintf("读取文档内容失败: %v", err) dp.logger.Printf(errMsg) dp.updateDocErrorStatus(doc, errMsg) return } // 将文档内容分块 chunks, err := dp.splitTextIntoChunks(content) if err != nil { errMsg := fmt.Sprintf("文档分块失败: %v", err) dp.logger.Printf(errMsg) dp.updateDocErrorStatus(doc, errMsg) return } // 用于跟踪所有已保存的需求 var allSavedDemands []*M.Demand totalChunks := len(chunks) // 逐块处理文本 for i, chunk := range chunks { // 检查是否被取消 select { case <-ctx.Done(): dp.logger.Printf("文档处理被取消: %s", doc.Name) return default: } // 更新处理进度 progress := float64(i) / float64(totalChunks) * 100 if err := cfg.DB().Model(doc).Update("analysis_percent", progress).Error; err != nil { dp.logger.Printf("更新进度失败: %v", err) } // 提取当前块的需求,同时考虑已有的需求树 newDemands, err := dp.extractDemandsFromChunk(ctx, chunk, allSavedDemands) if err != nil { errMsg := fmt.Sprintf("处理文本块 %d/%d 失败: %v", i+1, totalChunks, err) dp.logger.Printf(errMsg) dp.updateDocErrorStatus(doc, errMsg) return } dp.logger.Printf("文本块 %d/%d 提取了 %d 个需求", i+1, totalChunks, len(newDemands)) if len(newDemands) > 0 { // 立即保存这个块的需求 if err := dp.saveDemands(doc, newDemands); err != nil { errMsg := fmt.Sprintf("保存文本块 %d/%d 的需求失败: %v", i+1, totalChunks, err) dp.logger.Printf(errMsg) dp.updateDocErrorStatus(doc, errMsg) return } dp.logger.Printf("文本块 %d/%d 的需求已保存到数据库", i+1, totalChunks) // 更新已保存的需求列表 // 首先从数据库获取完整的需求列表,确保包含所有已保存的需求及其关系 var updatedSavedDemands []*M.Demand if err := cfg.DB().Where("doc_id = ?", doc.ID).Find(&updatedSavedDemands).Error; err != nil { dp.logger.Printf("获取已保存需求列表失败: %v", err) // 即使获取失败,也继续使用当前已知的需求列表 allSavedDemands = append(allSavedDemands, newDemands...) } else { allSavedDemands = updatedSavedDemands } } else { dp.logger.Printf("文本块 %d/%d 没有提取到新需求", i+1, totalChunks) } } // 更新文档状态为完成 if err := cfg.DB().Model(doc).Updates(map[string]interface{}{ "analysis_completed": true, "analysis_percent": 100, }).Error; err != nil { dp.logger.Printf("更新文档状态失败: %v", err) return } dp.logger.Printf("需求文档处理完成: %s,总共提取并保存了 %d 个需求", doc.Name, len(allSavedDemands)) } // 添加一个辅助方法来处理错误状态更新 func (dp *AgenticDemandProcessor) updateDocErrorStatus(doc *M.Doc, errMsg string) { if dbErr := cfg.DB().Model(doc).Updates(map[string]interface{}{ "analysis_completed": true, "analysis_error": errMsg, }).Error; dbErr != nil { dp.logger.Printf("更新文档错误状态失败: %v", dbErr) } } // extractDemandsFromChunk 从文本块中提取需求 // extractDemandsFromChunk 从文本块中提取需求 func (dp *AgenticDemandProcessor) extractDemandsFromChunk(ctx context.Context, chunk string, previousBlockDemands []*M.Demand) ([]*M.Demand, error) { // 用于跟踪当前文本块所有提取的需求 var currentBlockDemands []DemandNode // 初始提取时,没有当前块的需求 var currentDemandModels []*M.Demand // 最大尝试次数,避免无限循环 maxAttempts := 10 attemptCount := 0 hasMore := true // 初始化对话历史 history := []openai.ChatCompletionMessageParamUnion{} // && attemptCount < maxAttempts // 循环直到所有需求提取完毕或达到最大尝试次数 for hasMore { // 是否是后续提取 isFollowUp := attemptCount > 0 // 构建提示词 prompt := dp.buildPrompt(chunk, previousBlockDemands, currentDemandModels, isFollowUp) // 添加到对话历史 history = append(history, openai.UserMessage(prompt)) // 调用LLM API dp.logger.Printf("发送文本块到LLM进行需求提取 (尝试 %d/%d)", attemptCount+1, maxAttempts) // 创建流式响应 var responseBuilder strings.Builder var err error maxRetries := 3 dp.logger.Printf("发送的提示词: %s", prompt) for retry := 0; retry < maxRetries; retry++ { responseBuilder.Reset() stream := dp.analyzer.client.Chat.Completions.NewStreaming(ctx, openai.ChatCompletionNewParams{ Messages: openai.F(history), Model: openai.F("qwen-plus"), }) // 读取流式响应 for stream.Next() { chunk := stream.Current() for _, choice := range chunk.Choices { responseBuilder.WriteString(choice.Delta.Content) } } if err = stream.Err(); err == nil { break } dp.logger.Printf("尝试 %d 失败: %v, 重试中...", retry+1, err) time.Sleep(time.Second * 5) } if err != nil { return nil, fmt.Errorf("调用LLM API失败,已重试%d次: %v", maxRetries, err) } response := responseBuilder.String() dp.logger.Printf("LLM响应:\n%s", response) history = append(history, openai.AssistantMessage(response)) // 清理并验证响应 cleanedJSON, err := cleanDemandJSONResponse(response) if err != nil { // 尝试修复JSON dp.logger.Printf("尝试修复JSON: %v", err) fixed, repairErr := dp.analyzer.repairDemandJSON(ctx, response, err) if repairErr != nil { return nil, fmt.Errorf("JSON修复失败: %v (原始错误: %v)", repairErr, err) } dp.logger.Printf("JSON已修复:\n%s", fixed) cleanedJSON = fixed } // 解析响应 var result DemandTree if err := json.Unmarshal([]byte(cleanedJSON), &result); err != nil { return nil, fmt.Errorf("解析LLM响应失败: %v", err) } // 将新提取的需求添加到当前文本块的需求中 currentBlockDemands = append(currentBlockDemands, result.Demands...) // 将当前所有需求转换为模型格式,用于下一次提示 currentDemandModels, err = dp.parseDemandsToModel(currentBlockDemands) if err != nil { return nil, fmt.Errorf("转换需求格式失败: %v", err) } // 更新循环条件 hasMore = result.HasMore attemptCount++ dp.logger.Printf("需求提取进度: 已提取 %d 个需求,hasMore=%v", len(currentBlockDemands), hasMore) // 如果已经是最后一次尝试且仍有更多需求,记录警告 if attemptCount == maxAttempts && hasMore { dp.logger.Printf("警告:达到最大尝试次数 (%d),但模型表示还有更多需求未提取", maxAttempts) } } // 解析为我们的模型格式 demands, err := dp.parseDemandsToModel(currentBlockDemands) if err != nil { return nil, fmt.Errorf("转换需求格式失败: %v", err) } return demands, nil } // handleHasMoreFollowUp 处理有更多需求的情况 func (dp *AgenticDemandProcessor) handleHasMoreFollowUp(ctx context.Context, chunk string, existingDemands []*M.Demand, history []openai.ChatCompletionMessageParamUnion, currentDemands []DemandNode) ([]*M.Demand, error) { var allDemands []DemandNode allDemands = append(allDemands, currentDemands...) // 最多连续请求5次,避免无限循环 maxFollowUpAttempts := 5 for attempt := 0; attempt < maxFollowUpAttempts; attempt++ { // 将已提取的需求转换为M.Demand格式 extractedSoFar, err := dp.parseDemandsToModel(allDemands) if err != nil { return nil, fmt.Errorf("转换已提取需求失败: %v", err) } // 构建后续提示,明确区分两类需求 followUpPrompt := dp.buildPrompt(chunk, existingDemands, extractedSoFar, true) // 添加后续提示到历史记录 history = append(history, openai.UserMessage(followUpPrompt)) // 创建流式响应 var responseBuilder strings.Builder dp.logger.Printf("发送后续请求到LLM (尝试 %d/%d)", attempt+1, maxFollowUpAttempts) dp.logger.Printf("发送的提示词: %s", followUpPrompt) stream := dp.analyzer.client.Chat.Completions.NewStreaming(ctx, openai.ChatCompletionNewParams{ Messages: openai.F(history), Model: openai.F("qwen-plus"), }) // 读取流式响应 for stream.Next() { chunk := stream.Current() for _, choice := range chunk.Choices { responseBuilder.WriteString(choice.Delta.Content) } } if err = stream.Err(); err != nil { return nil, fmt.Errorf("后续请求失败: %v", err) } response := responseBuilder.String() dp.logger.Printf("收到的响应: %s", response) history = append(history, openai.AssistantMessage(response)) // 清理并验证响应 cleanedJSON, err := cleanDemandJSONResponse(response) if err != nil { fixed, repairErr := dp.analyzer.repairDemandJSON(ctx, response, err) if repairErr != nil { return nil, fmt.Errorf("JSON修复失败: %v", repairErr) } cleanedJSON = fixed } // 解析响应 var result DemandTree if err := json.Unmarshal([]byte(cleanedJSON), &result); err != nil { return nil, fmt.Errorf("解析后续响应失败: %v", err) } // 合并需求 allDemands = append(allDemands, result.Demands...) // 如果没有更多需求,退出循环 if !result.HasMore { break } // 如果这是最后一次尝试并且仍然has_more=true,记录警告 if attempt == maxFollowUpAttempts-1 && result.HasMore { dp.logger.Printf("警告:达到最大后续请求次数(%d),可能还有未提取的需求", maxFollowUpAttempts) } } // 解析为我们的模型格式 demands, err := dp.parseDemandsToModel(allDemands) if err != nil { return nil, fmt.Errorf("转换需求格式失败: %v", err) } return demands, nil } // 新增方法:将DemandNode转换为M.Demand func (dp *AgenticDemandProcessor) parseDemandsToModel(nodes []DemandNode) ([]*M.Demand, error) { var demands []*M.Demand // 递归处理需求及其子需求 var processDemand func(node DemandNode, parentReqID string) *M.Demand processDemand = func(node DemandNode, parentReqID string) *M.Demand { // 设置默认值 priority := node.Priority if priority == "" { priority = "中" } demandType := node.Type if demandType == "" { demandType = "功能需求" } status := node.Status if status == "" { status = "待实现" } demand := &M.Demand{ Tree: M.Tree{ Name: node.Title, }, Description: node.Description, ReqID: node.ReqID, Priority: priority, Type: demandType, Status: status, ParentReqID: parentReqID, } demands = append(demands, demand) // 处理子需求 for _, child := range node.Children { childDemand := processDemand(child, node.ReqID) // 子需求的父需求ID直接设置为当前需求的ID childDemand.ParentReqID = node.ReqID } return demand } // 处理所有顶级需求 for _, node := range nodes { processDemand(node, node.ParentReqID) } return demands, nil } // buildPrompt 构建提示词,用于初始提取或后续提取 func (dp *AgenticDemandProcessor) buildPrompt(chunk string, previousDemands []*M.Demand, currentChunkDemands []*M.Demand, isFollowUp bool) string { // 获取JSON Schema - 复用成熟版本的函数 schema, err := generateDemandJSONSchema() if err != nil { dp.logger.Printf("生成Schema失败: %v", err) return "" } var sb strings.Builder // 设置标题和基本介绍 if isFollowUp { sb.WriteString("# 继续提取需求\n\n") sb.WriteString("请继续从当前文本块中提取剩余的需求信息,保持相同的输出格式。\n\n") } else { sb.WriteString("# 需求文档分析任务\n\n") sb.WriteString("你是一个需求文档分析助手。你的任务是从需求规格说明书中提取需求信息并构建需求树,需求包含功能需求和非功能需求等。\n\n") } // 显示先前文本块提取的需求 if len(previousDemands) > 0 { sb.WriteString("## 先前文本块中提取的需求\n\n") sb.WriteString("以下是从先前文本块中提取的需求。你可以更新这些需求或建立与它们的关系:\n\n") for i, demand := range previousDemands { sb.WriteString(fmt.Sprintf("%d. **%s** (ID: %s)\n", i+1, demand.Name, demand.ReqID)) sb.WriteString(fmt.Sprintf(" - 描述: %s\n", demand.Description)) sb.WriteString(fmt.Sprintf(" - 优先级: %s\n", demand.Priority)) sb.WriteString(fmt.Sprintf(" - 类型: %s\n", demand.Type)) sb.WriteString(fmt.Sprintf(" - 状态: %s\n", demand.Status)) if demand.ParentReqID != "" { sb.WriteString(fmt.Sprintf(" - 父需求ID: %s\n", demand.ParentReqID)) } sb.WriteString("\n") } } // 显示当前文本块已提取的需求(在has_more=true的情况下) if len(currentChunkDemands) > 0 { sb.WriteString("## 当前文本块已提取的需求\n\n") sb.WriteString("以下是从当前文本块中已经提取的需求。请不要重复这些需求,继续提取未捕获的需求:\n\n") for i, demand := range currentChunkDemands { sb.WriteString(fmt.Sprintf("%d. **%s** (ID: %s)\n", i+1, demand.Name, demand.ReqID)) sb.WriteString(fmt.Sprintf(" - 描述: %s\n", demand.Description)) sb.WriteString(fmt.Sprintf(" - 优先级: %s\n", demand.Priority)) sb.WriteString(fmt.Sprintf(" - 类型: %s\n", demand.Type)) sb.WriteString(fmt.Sprintf(" - 状态: %s\n", demand.Status)) if demand.ParentReqID != "" { sb.WriteString(fmt.Sprintf(" - 父需求ID: %s\n", demand.ParentReqID)) } sb.WriteString("\n") } } // 当前任务说明 sb.WriteString("## 当前任务\n\n") if len(previousDemands) == 0 && !isFollowUp { sb.WriteString("这是文档的第一个文本块,请提取所有需求信息,包括需求ID、标题、描述、优先级、类型和状态。\n") sb.WriteString("请确保完整提取每个需求的所有信息,并正确构建需求的层级关系。\n\n") } else { sb.WriteString("你需要对当前文本块执行以下操作:\n") sb.WriteString("1. 提取此文本块中的**新需求**\n") if len(previousDemands) > 0 { sb.WriteString("2. **更新已有需求**:如果发现对先前文本块需求的补充信息,请使用相同的reqID更新它们\n") sb.WriteString("3. 建立需求之间的关系:如果发现已有需求的子需求,请正确设置父子关系\n") } sb.WriteString("4. 所有需求必须有唯一的ID、标题、描述、优先级、类型和状态\n") sb.WriteString("5. 如果还有更多需求要提取但未能在当前输出中包含,设置 has_more 为 true\n\n") if len(previousDemands) > 0 { sb.WriteString("注意:当你更新已有需求时,请保持相同的req_id,这样我们就知道它是对已有需求的更新而不是新需求。\n\n") } } if !isFollowUp { sb.WriteString(`注意你要总结、提取需求规格说明书中的需求而不是自己创造需求,所以如果是无关内容(比如作者、单位等),你直接输出空数组,并且has_more设置成false就可以 注意has_more为true表示的是当前给你提供的文档中还有需求没有提取出来,而不是完整文档中还有内容没有提取出来,当前待分析文档中没有更多需求的时候,你要把has_more设置成false `) sb.WriteString("优先级、状态、ID等、描述等信息不是必须的,只要你能总结出名称就可以算一个需求") // sb.WriteString("如果文本内容是目录,你也可以提取出来需求,因为之后给你提供详细内容的时候,你可以使用相同的req_id给它补充详细内容\n\n") } // 待分析文本 sb.WriteString("## 待分析文本\n\n") sb.WriteString(chunk) // 输出要求 sb.WriteString("\n\n## 输出要求\n\n") sb.WriteString("请以JSON格式输出需求列表,严格遵循以下JSON Schema:\n\n") sb.WriteString(string(schema)) sb.WriteString("\n\n请注意:\n") sb.WriteString("1. 对于新需求,请分配新的req_id\n") if len(previousDemands) > 0 { sb.WriteString("2. 对于已有需求的更新,请保持相同的req_id\n") } sb.WriteString("3. 确保输出的JSON符合上述schema格式\n") if len(previousDemands) > 0 { sb.WriteString("4. 如果此文本块包含已有需求的补充信息,请使用相同的req_id并提供完整的更新后的需求\n") } sb.WriteString("5. 如果当前文本块还有更多需求未处理完,设置 \"has_more\": true;否则设置 \"has_more\": false\n") return sb.String() } // saveDemands 保存需求到数据库 func (dp *AgenticDemandProcessor) saveDemands(doc *M.Doc, demands []*M.Demand) error { dp.logger.Printf("开始保存需求到数据库,文档ID: %s,需求数量: %d", doc.ID, len(demands)) if len(demands) == 0 { dp.logger.Printf("警告:没有需求需要保存") return nil } // 开始事务 tx := cfg.DB().Begin() dp.logger.Printf("数据库事务已开始") defer func() { if r := recover(); r != nil { dp.logger.Printf("保存过程中发生panic: %v", r) tx.Rollback() } }() // 先获取当前文档中的所有已存在需求,用于去重和更新 var existingDemands []*M.Demand if err := tx.Where("doc_id = ?", doc.ID).Find(&existingDemands).Error; err != nil { tx.Rollback() return fmt.Errorf("获取现有需求失败: %v", err) } // 创建req_id到数据库需求的映射,用于快速查找 existingReqIDMap := make(map[string]*M.Demand) for _, d := range existingDemands { existingReqIDMap[d.ReqID] = d } // 为了处理父子关系,先进行两轮处理: // 1. 第一轮:更新/插入所有需求,记录新旧ID对应关系 // 2. 第二轮:更新父子关系 // 记录req_id到数据库ID的映射 reqIDToDBID := make(map[string]string) for _, ed := range existingDemands { reqIDToDBID[ed.ReqID] = ed.ID } // 第一轮:更新或插入需求 for i, demand := range demands { demand.DocID = doc.ID // 设置默认值 if demand.Status == "" { demand.Status = "待实现" } if demand.Type == "" { demand.Type = "功能需求" } if demand.Priority == "" { demand.Priority = "中" } // 检查是否已存在相同req_id的需求 if existingDemand, exists := existingReqIDMap[demand.ReqID]; exists { dp.logger.Printf("需求 #%d (reqID=%s) 已存在,进行更新", i+1, demand.ReqID) // 保留原始ID和创建时间,更新其他字段 demand.ID = existingDemand.ID demand.CreatedAt = existingDemand.CreatedAt // 临时清除父子关系字段,稍后单独处理 tempParentReqID := demand.ParentReqID demand.ParentReqID = "" demand.Tree.ParentID = "" // 更新需求 if err := tx.Model(existingDemand).Updates(demand).Error; err != nil { dp.logger.Printf("更新需求失败: %v", err) tx.Rollback() return fmt.Errorf("更新需求失败: %v", err) } // 恢复父需求ID以便第二轮处理 demand.ParentReqID = tempParentReqID dp.logger.Printf("需求 #%d 更新成功", i+1) } else { dp.logger.Printf("需求 #%d (reqID=%s) 是新需求,创建记录", i+1, demand.ReqID) // 临时清除父子关系字段,稍后单独处理 tempParentReqID := demand.ParentReqID demand.ParentReqID = "" demand.Tree.ParentID = "" // 创建新需求 if err := tx.Create(demand).Error; err != nil { dp.logger.Printf("创建需求失败: %v", err) tx.Rollback() return fmt.Errorf("保存需求失败: %v", err) } // 恢复父需求ID以便第二轮处理 demand.ParentReqID = tempParentReqID dp.logger.Printf("需求 #%d 创建成功,ID: %s", i+1, demand.ID) // 更新映射 reqIDToDBID[demand.ReqID] = demand.ID } } // 第二轮:更新父子关系 for i, demand := range demands { if demand.ParentReqID != "" { // 查找父需求的数据库ID parentDBID, exists := reqIDToDBID[demand.ParentReqID] if !exists { dp.logger.Printf("警告:需求 #%d 的父需求 (reqID=%s) 未找到", i+1, demand.ParentReqID) continue } // 更新父子关系 updateFields := map[string]interface{}{ "parent_req_id": demand.ParentReqID, "parent_id": parentDBID, "level": demand.Tree.Level, } if err := tx.Model(&M.Demand{}).Where("id = ?", reqIDToDBID[demand.ReqID]).Updates(updateFields).Error; err != nil { dp.logger.Printf("更新需求 #%d 的父子关系失败: %v", i+1, err) tx.Rollback() return fmt.Errorf("更新父子关系失败: %v", err) } dp.logger.Printf("需求 #%d 的父子关系更新成功", i+1) } } // 提交事务 dp.logger.Printf("所有需求保存完成,提交事务") if err := tx.Commit().Error; err != nil { dp.logger.Printf("提交事务失败: %v", err) return fmt.Errorf("提交事务失败: %v", err) } dp.logger.Printf("事务提交成功,总共处理了 %d 个需求", len(demands)) return nil } // func (dp *AgenticDemandProcessor) saveDemands(doc *M.Doc, demands []*M.Demand) error { // // 开始事务 // tx := cfg.DB().Begin() // defer func() { // if r := recover(); r != nil { // tx.Rollback() // } // }() // // 为每个需求设置文档ID并保存 // for _, demand := range demands { // demand.DocID = doc.ID // // 设置默认值 // if demand.Status == "" { // demand.Status = "待实现" // } // if demand.Type == "" { // demand.Type = "功能需求" // } // if demand.Priority == "" { // demand.Priority = "中" // } // // 需要添加:计算当前节点的层级 // level := 0 // var parentID string // // 如果有父需求ID,查找父需求并设置正确的ParentID和Level // if demand.ParentReqID != "" { // var parentDemand M.Demand // if err := tx.Where("req_id = ? AND doc_id = ?", demand.ParentReqID, doc.ID).First(&parentDemand).Error; err == nil { // parentID = parentDemand.ID // level = parentDemand.Level + 1 // } // } // // 设置正确的Tree结构 // demand.Tree.ParentID = parentID // demand.Tree.Level = level // if err := tx.Create(demand).Error; err != nil { // tx.Rollback() // return fmt.Errorf("保存需求失败: %v", err) // } // } // // 提交事务 // if err := tx.Commit().Error; err != nil { // return fmt.Errorf("提交事务失败: %v", err) // } // return nil // } // mergeDemands 合并新旧需求树,处理重复和补充关系 // mergeDemands 合并新旧需求树,处理重复和补充关系 func (dp *AgenticDemandProcessor) mergeDemands(oldDemands, newDemands []*M.Demand) []*M.Demand { result := make([]*M.Demand, len(oldDemands)) copy(result, oldDemands) // 创建reqID到需求索引的映射,用于快速查找 reqIDToIndex := make(map[string]int) for i, demand := range result { reqIDToIndex[demand.ReqID] = i } // 遍历新需求 for _, newDemand := range newDemands { merged := false // 首先检查是否有相同的reqID(优先使用reqID进行匹配) if idx, exists := reqIDToIndex[newDemand.ReqID]; exists { // 找到相同reqID的需求,进行合并 dp.logger.Printf("找到相同reqID的需求: %s,进行更新", newDemand.ReqID) // 合并描述(如果新描述更详细) if len(newDemand.Description) > len(result[idx].Description) { result[idx].Description = newDemand.Description } // 更新标题(如果提供了更详细的标题) if len(newDemand.Name) > len(result[idx].Name) { result[idx].Name = newDemand.Name } // 更新优先级(如果有新的优先级) if newDemand.Priority != "" && newDemand.Priority != "中" { result[idx].Priority = newDemand.Priority } // 更新类型(如果有新的类型) if newDemand.Type != "" && newDemand.Type != "功能需求" { result[idx].Type = newDemand.Type } // 更新状态(如果有新的状态) if newDemand.Status != "" && newDemand.Status != "待实现" { result[idx].Status = newDemand.Status } // 更新父需求ID(如果有新的父需求ID) if newDemand.ParentReqID != "" { result[idx].ParentReqID = newDemand.ParentReqID } merged = true } else { // 如果没有相同的reqID,检查标题相似度 for i, oldDemand := range result { if dp.isSimilarTitle(oldDemand.Name, newDemand.Name) { dp.logger.Printf("找到标题相似的需求: %s 和 %s,进行合并", oldDemand.Name, newDemand.Name) // 合并描述(如果新描述更详细) if len(newDemand.Description) > len(oldDemand.Description) { result[i].Description = newDemand.Description } // 更新优先级(如果有新的优先级) if newDemand.Priority != "" && newDemand.Priority != "中" { result[i].Priority = newDemand.Priority } // 更新类型(如果有新的类型) if newDemand.Type != "" && newDemand.Type != "功能需求" { result[i].Type = newDemand.Type } // 更新状态(如果有新的状态) if newDemand.Status != "" && newDemand.Status != "待实现" { result[i].Status = newDemand.Status } // 更新父需求ID(如果有新的父需求ID) if newDemand.ParentReqID != "" { result[i].ParentReqID = newDemand.ParentReqID } merged = true break } } } // 如果是新需求,添加到结果中 if !merged { dp.logger.Printf("添加新需求: %s (ID: %s)", newDemand.Name, newDemand.ReqID) result = append(result, newDemand) // 更新索引映射 reqIDToIndex[newDemand.ReqID] = len(result) - 1 } } // 建立需求间的关系 dp.establishRelationships(result) return result } // isSimilarTitle 判断两个标题是否相似 func (dp *AgenticDemandProcessor) isSimilarTitle(title1, title2 string) bool { // 简单实现:如果标题包含关系或相似度高于阈值,则认为相似 // 可以使用更复杂的算法,如编辑距离、词向量相似度等 title1 = strings.ToLower(strings.TrimSpace(title1)) title2 = strings.ToLower(strings.TrimSpace(title2)) // 检查包含关系 if strings.Contains(title1, title2) || strings.Contains(title2, title1) { return true } // TODO: 实现更复杂的相似度算法 return false } // establishRelationships 建立需求间的关系 func (dp *AgenticDemandProcessor) establishRelationships(demands []*M.Demand) { // 创建标题到需求的映射,用于快速查找 titleToReqID := make(map[string]string) for _, demand := range demands { titleToReqID[demand.Name] = demand.ReqID } // 遍历临时结构中的需求,设置父需求ID for i, demand := range demands { // 尝试从parseDemandsFromResponse中的临时结构获取父需求标题 // 这里假设我们有一个方式获取父需求标题,例如通过额外字段或解析描述 // 在实际实现中,可能需要调整这部分逻辑 // 示例:从描述中提取父需求信息 parentTitle := extractParentTitleFromDescription(demand.Description) if parentTitle != "" { if parentReqID, exists := titleToReqID[parentTitle]; exists { demands[i].ParentReqID = parentReqID } } } } // extractParentTitleFromDescription 从描述中提取父需求标题 func extractParentTitleFromDescription(description string) string { // 这里实现一个简单的逻辑,从描述中提取父需求信息 // 例如,如果描述中包含"父需求:"或"Parent:"等标记 // 简单示例实现 parentPrefixes := []string{"父需求:", "Parent:", "父级需求:", "上级需求:"} lines := strings.Split(description, "\n") for _, line := range lines { line = strings.TrimSpace(line) for _, prefix := range parentPrefixes { if strings.HasPrefix(line, prefix) { return strings.TrimSpace(line[len(prefix):]) } } } return "" } // 以下是直接复用成熟版本的函数