testFlow/api/doc/doc_processor_demand_agentic.go
Wyle.Gong-巩文昕 67b0ad2723 init
2025-04-22 16:42:48 +08:00

998 lines
32 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 ""
}
// 以下是直接复用成熟版本的函数