761 lines
24 KiB
Go
761 lines
24 KiB
Go
package doc
|
||
|
||
import (
|
||
"app/cfg"
|
||
M "app/models"
|
||
"bytes"
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/ledongthuc/pdf"
|
||
"github.com/openai/openai-go"
|
||
"github.com/openai/openai-go/option"
|
||
"github.com/xeipuuv/gojsonschema"
|
||
|
||
"github.com/unidoc/unioffice/document"
|
||
)
|
||
|
||
// DemandTree 表示一个完整的需求树
|
||
type DemandTree struct {
|
||
Demands []DemandNode `json:"demands" jsonschema:"required,description=需求树列表"`
|
||
HasMore bool `json:"has_more" jsonschema:"required,description=是否还有更多需求需要继续在下一次输出中继续输出"`
|
||
AnalysisPercent int `json:"analysis_percent" jsonschema:"required,description=估计的分析进度百分比,范围是0-100,不一定递增,可以调整,比如发现增长过快的时候可以减少,只有全部解析完毕才可以到100"`
|
||
}
|
||
type DemandNode struct {
|
||
ReqID string `json:"id" jsonschema:"required,description=需求ID,格式为REQ-数字"`
|
||
Title string `json:"title" jsonschema:"required,description=需求标题"`
|
||
Description string `json:"description" jsonschema:"required,description=需求详细描述"`
|
||
Priority string `json:"priority" jsonschema:"enum=高,enum=中,enum=低,description=需求优先级"`
|
||
Type string `json:"type" jsonschema:"enum=功能需求,enum=非功能需求,enum=业务需求,description=需求类型"`
|
||
Status string `json:"status" jsonschema:"enum=待实现,enum=开发中,enum=已完成,enum=已验收,description=需求状态"`
|
||
ParentReqID string `json:"parent_req_id,omitempty" jsonschema:"description=父需求ID,如果是顶级需求则为空"`
|
||
Children []DemandNode `json:"children,omitempty" jsonschema:"description=子需求列表"`
|
||
}
|
||
|
||
// 同样修改 SimpleDemandNode
|
||
type SimpleDemandNode struct {
|
||
ReqID string `json:"id" jsonschema:"required,description=需求ID,格式为REQ-数字"`
|
||
Title string `json:"title" jsonschema:"required,description=需求标题"`
|
||
Description string `json:"description" jsonschema:"required,description=需求详细描述"`
|
||
Priority string `json:"priority" jsonschema:"enum=高,enum=中,enum=低,description=需求优先级"`
|
||
Type string `json:"type" jsonschema:"enum=功能需求,enum=非功能需求,enum=业务需求,description=需求类型"`
|
||
Status string `json:"status" jsonschema:"enum=待实现,enum=开发中,enum=已完成,enum=已验收,description=需求状态"`
|
||
ParentReqID string `json:"parent_req_id,omitempty" jsonschema:"description=父需求ID,如果是顶级需求则为空"`
|
||
Children []map[string]string `json:"children,omitempty" jsonschema:"description=子需求列表"`
|
||
}
|
||
|
||
// 简化的需求树结构,用于生成 Schema
|
||
type SimpleDemandTree struct {
|
||
Demands []SimpleDemandNode `json:"demands" jsonschema:"required,description=需求树列表"`
|
||
HasMore bool `json:"has_more" jsonschema:"required,description=是否还有更多需求需要继续在下一次输出中继续输出"`
|
||
AnalysisPercent int `json:"analysis_percent" jsonschema:"required,description=估计的分析进度百分比,范围是0-100,不一定递增,可以调整,比如发现增长过快的时候可以减少,只有全部解析完毕才可以到100"`
|
||
}
|
||
|
||
// DemandChatHistory 表示与大模型的对话历史
|
||
type DemandChatHistory struct {
|
||
Messages []openai.ChatCompletionMessageParamUnion
|
||
DocContent string
|
||
Schema string
|
||
}
|
||
|
||
// DemandAnalyzer 处理需求文档分析
|
||
type DemandAnalyzer struct {
|
||
client *openai.Client
|
||
logger *log.Logger
|
||
}
|
||
|
||
// NewDemandAnalyzer 创建一个新的需求分析器实例
|
||
func NewDemandAnalyzer() (*DemandAnalyzer, error) {
|
||
client := openai.NewClient(
|
||
option.WithAPIKey("sk-0213c70194624703a1d0d80e0f762b0e"),
|
||
option.WithBaseURL("https://dashscope.aliyuncs.com/compatible-mode/v1/"),
|
||
)
|
||
fmt.Println("需求分析器初始化成功")
|
||
return &DemandAnalyzer{
|
||
client: client,
|
||
logger: setupLogger(),
|
||
}, nil
|
||
}
|
||
|
||
// generateDemandJSONSchema 生成需求树的JSON Schema
|
||
// func generateDemandJSONSchema() ([]byte, error) {
|
||
// reflector := jsonschema.Reflector{
|
||
// RequiredFromJSONSchemaTags: true,
|
||
// AllowAdditionalProperties: true,
|
||
// DoNotReference: true,
|
||
// }
|
||
// schema := reflector.Reflect(&SimpleDemandTree{})
|
||
// // schema := reflector.Reflect(&DemandTree{})
|
||
|
||
// return json.MarshalIndent(schema, "", " ")
|
||
// }
|
||
func generateDemandJSONSchema() ([]byte, error) {
|
||
// 使用预定义的 JSON Schema 字符串,手动处理递归引用
|
||
schemaStr := `{
|
||
"type": "object",
|
||
"properties": {
|
||
"demands": {
|
||
"type": "array",
|
||
"description": "需求树列表",
|
||
"items": {
|
||
"$ref": "#/definitions/demandNode"
|
||
}
|
||
},
|
||
"has_more": {
|
||
"type": "boolean",
|
||
"description": "是否还有更多需求需要继续在下一次输出中继续输出"
|
||
},
|
||
"analysis_percent": {
|
||
"type": "integer",
|
||
"description": "估计的分析进度百分比,范围是0-100,不一定递增,可以调整,比如发现增长过快的时候可以减少,只有全部解析完毕才可以到100"
|
||
}
|
||
},
|
||
"required": ["demands", "has_more", "analysis_percent"],
|
||
"definitions": {
|
||
"demandNode": {
|
||
"type": "object",
|
||
"properties": {
|
||
"id": {
|
||
"type": "string",
|
||
"description": "需求ID,格式为REQ-数字"
|
||
},
|
||
"title": {
|
||
"type": "string",
|
||
"description": "需求标题"
|
||
},
|
||
"description": {
|
||
"type": "string",
|
||
"description": "需求详细描述"
|
||
},
|
||
"priority": {
|
||
"type": "string",
|
||
"enum": ["高", "中", "低"],
|
||
"description": "需求优先级"
|
||
},
|
||
"type": {
|
||
"type": "string",
|
||
"enum": ["功能需求","性能需求","安全需求", "合规性需求", "可靠性需求"],
|
||
"description": "需求类型"
|
||
},
|
||
"status": {
|
||
"type": "string",
|
||
"enum": ["待实现", "开发中", "已完成", "已测试"],
|
||
"description": "需求状态"
|
||
},
|
||
"parent_req_id": {
|
||
"type": "string",
|
||
"description": "父需求ID,如果是顶级需求则为空"
|
||
},
|
||
"children": {
|
||
"type": "array",
|
||
"description": "子需求列表,每个子需求也是一个demandNode,而且子需求也可能有自己的子需求列表,以此类推",
|
||
"items": {
|
||
"$ref": "#/definitions/demandNode"
|
||
}
|
||
}
|
||
},
|
||
"required": ["id", "title", "description"]
|
||
}
|
||
}
|
||
}`
|
||
|
||
// 解析 JSON 以确保格式正确
|
||
var schema interface{}
|
||
if err := json.Unmarshal([]byte(schemaStr), &schema); err != nil {
|
||
return nil, fmt.Errorf("解析预定义 Schema 失败: %v", err)
|
||
}
|
||
|
||
// 重新格式化为美观的 JSON
|
||
return json.MarshalIndent(schema, "", " ")
|
||
}
|
||
|
||
// validateDemandJSON 验证JSON响应是否符合Schema
|
||
func validateDemandJSON(data []byte) error {
|
||
schema, err := generateDemandJSONSchema()
|
||
if err != nil {
|
||
return fmt.Errorf("生成Schema失败: %v", err)
|
||
}
|
||
|
||
schemaLoader := gojsonschema.NewBytesLoader(schema)
|
||
documentLoader := gojsonschema.NewBytesLoader(data)
|
||
|
||
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
|
||
if err != nil {
|
||
return fmt.Errorf("验证错误: %v", err)
|
||
}
|
||
|
||
if !result.Valid() {
|
||
var errors []string
|
||
for _, desc := range result.Errors() {
|
||
errors = append(errors, desc.String())
|
||
}
|
||
return fmt.Errorf("无效的JSON: %v", errors)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// cleanDemandJSONResponse 清理并验证LLM响应
|
||
func cleanDemandJSONResponse(response string) (string, error) {
|
||
// 查找第一个 { 和最后一个 }
|
||
start := 0
|
||
end := len(response)
|
||
|
||
for i := 0; i < len(response); i++ {
|
||
if response[i] == '{' {
|
||
start = i
|
||
break
|
||
}
|
||
}
|
||
|
||
for i := len(response) - 1; i >= 0; i-- {
|
||
if response[i] == '}' {
|
||
end = i + 1
|
||
break
|
||
}
|
||
}
|
||
|
||
if start >= end {
|
||
return "", fmt.Errorf("无效的JSON结构")
|
||
}
|
||
|
||
jsonStr := response[start:end]
|
||
|
||
// 验证JSON
|
||
if err := validateDemandJSON([]byte(jsonStr)); err != nil {
|
||
return "", fmt.Errorf("JSON验证失败: %v", err)
|
||
}
|
||
|
||
return jsonStr, nil
|
||
}
|
||
|
||
// repairDemandJSON 修复格式错误的JSON
|
||
// 修改 repairDemandJSON 方法
|
||
func (a *DemandAnalyzer) repairDemandJSON(ctx context.Context, response string, originalErr error) (string, error) {
|
||
prompt := fmt.Sprintf(`你是一个JSON修复专家。我有一个JSON字符串,但它存在一些问题无法解析。
|
||
|
||
原始JSON:
|
||
%s
|
||
|
||
错误信息:
|
||
%v
|
||
|
||
请修复这个JSON,使其符合以下要求:
|
||
1. 所有字段名必须使用双引号
|
||
2. 字符串值必须使用双引号
|
||
3. 不要添加或删除字段,只修复格式问题
|
||
4. 如果parent_req_id是null,替换为空字符串""
|
||
5. 确保demands是一个数组,即使为空也应为[]
|
||
6. 确保has_more是一个布尔值
|
||
7. 结果必须是有效的JSON
|
||
|
||
请只返回修复后的JSON,不要包含任何其他解释或评论。`, response, originalErr)
|
||
|
||
stream := a.client.Chat.Completions.NewStreaming(ctx, openai.ChatCompletionNewParams{
|
||
Messages: openai.F([]openai.ChatCompletionMessageParamUnion{
|
||
openai.UserMessage(prompt),
|
||
}),
|
||
Model: openai.F("qwen-plus"),
|
||
})
|
||
|
||
var responseBuilder strings.Builder
|
||
for stream.Next() {
|
||
chunk := stream.Current()
|
||
for _, choice := range chunk.Choices {
|
||
responseBuilder.WriteString(choice.Delta.Content)
|
||
}
|
||
}
|
||
|
||
if err := stream.Err(); err != nil {
|
||
return "", fmt.Errorf("JSON修复API调用失败: %v", err)
|
||
}
|
||
|
||
fixed := responseBuilder.String()
|
||
|
||
// 关键修复:移除可能的Markdown代码块标记
|
||
fixed = strings.TrimSpace(fixed)
|
||
|
||
// 检查并移除开头的Markdown标记
|
||
if strings.HasPrefix(fixed, "```") {
|
||
// 找到第一行结束位置
|
||
firstLineEnd := strings.Index(fixed, "\n")
|
||
if firstLineEnd != -1 {
|
||
// 跳过第一行(包含```json或```)
|
||
fixed = fixed[firstLineEnd+1:]
|
||
} else {
|
||
// 如果没有换行,可能整个字符串都是标记,返回错误
|
||
return "", fmt.Errorf("修复后的JSON格式异常")
|
||
}
|
||
}
|
||
|
||
// 检查并移除结尾的Markdown标记
|
||
if strings.HasSuffix(fixed, "```") {
|
||
fixed = fixed[:len(fixed)-3]
|
||
}
|
||
|
||
// 再次去除前后空白
|
||
fixed = strings.TrimSpace(fixed)
|
||
|
||
// 验证修复后的JSON是否有效
|
||
var testObj map[string]interface{}
|
||
if err := json.Unmarshal([]byte(fixed), &testObj); err != nil {
|
||
return "", fmt.Errorf("修复后的JSON仍然无效: %v", err)
|
||
}
|
||
|
||
return fixed, nil
|
||
}
|
||
|
||
// func (da *DemandAnalyzer) repairDemandJSON(ctx context.Context, malformedJSON string, originalError error) (string, error) {
|
||
// prompt := fmt.Sprintf(`修复这个格式错误的JSON,错误信息: %v
|
||
|
||
// 需要修复的JSON:
|
||
// %s
|
||
|
||
// 只返回修复后的JSON,不要有任何解释。`, originalError, malformedJSON)
|
||
|
||
// var responseBuilder strings.Builder
|
||
|
||
// stream := da.client.Chat.Completions.NewStreaming(ctx, openai.ChatCompletionNewParams{
|
||
// Messages: openai.F([]openai.ChatCompletionMessageParamUnion{
|
||
// openai.UserMessage(prompt),
|
||
// }),
|
||
// 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 "", fmt.Errorf("发送消息失败: %v", err)
|
||
// }
|
||
|
||
// fixed := responseBuilder.String()
|
||
// if fixed == "" {
|
||
// return "", fmt.Errorf("没有修复响应")
|
||
// }
|
||
|
||
// return fixed, nil
|
||
// }
|
||
|
||
// extractDemands 从文档内容中提取需求信息
|
||
func (da *DemandAnalyzer) extractDemands(ctx context.Context, docContent string, doc *M.Doc) error {
|
||
schema, err := generateDemandJSONSchema()
|
||
if err != nil {
|
||
return fmt.Errorf("生成Schema失败: %v", err)
|
||
}
|
||
da.logger.Printf("需求JSON Schema:\n%s", schema)
|
||
|
||
// 初始化对话历史
|
||
history := &DemandChatHistory{
|
||
Messages: make([]openai.ChatCompletionMessageParamUnion, 0),
|
||
}
|
||
|
||
// 初始化对话,发送文档内容和schema
|
||
initialPrompt := fmt.Sprintf(`你是一个需求文档分析助手。你的任务是从文档中提取需求信息并构建需求树。
|
||
请从以下文档中提取所有需求信息,包括需求ID、标题、描述、优先级、类型和状态。
|
||
请确保完整提取每个需求的所有信息,并正确构建需求的层级关系。
|
||
|
||
如果文档内容较多,你可以分批次输出,每次输出一部分需求信息:
|
||
1. 如果还有更多需求未处理,设置 "has_more": true
|
||
2. 在后续输出中,继续提取剩余的需求,不要重复之前已输出的需求
|
||
3. 当所有需求都输出完成时,设置 "has_more": false
|
||
4. 每次输出时,设置 "analysis_percent" 表示估计的分析进度百分比
|
||
|
||
需求应该按照层级结构组织,主需求包含子需求,子需求可能还有更深层次的子需求,以此类推,需求树的深度没有限制。
|
||
对于每个子需求,请设置其 "parent_req_id" 为父需求的ID,这样可以明确表示需求之间的层级关系。
|
||
你可以在后续输出中继续为前面已输出的需求添加子需求,只需正确设置 "parent_req_id" 即可。
|
||
|
||
如果文档中包含多个独立的需求树或模块,请将它们作为独立的顶级需求节点输出。
|
||
每个需求都应该有一个唯一的req_id,格式为REQ-数字,例如REQ-001。
|
||
如果文档中没有明确的需求ID,请自动生成一个。
|
||
|
||
文档内容:
|
||
%s
|
||
|
||
JSON Schema:
|
||
%s
|
||
|
||
请严格按照schema格式输出,确保是有效的JSON格式。输出的JSON必须符合以上schema的规范。`, docContent, string(schema))
|
||
|
||
totalDemands := 0 // 用于跟踪总共处理的需求数量
|
||
maxRetries := 10
|
||
|
||
// 添加初始消息到历史记录
|
||
history.Messages = append(history.Messages, openai.UserMessage(initialPrompt))
|
||
|
||
maxInitRetries := 3
|
||
var responseBuilder strings.Builder
|
||
var streamErr error
|
||
|
||
// 发送初始消息并重试
|
||
for i := 0; i < maxInitRetries; i++ {
|
||
responseBuilder.Reset()
|
||
stream := da.client.Chat.Completions.NewStreaming(ctx, openai.ChatCompletionNewParams{
|
||
Messages: openai.F(history.Messages),
|
||
Model: openai.F("qwen-plus"),
|
||
})
|
||
|
||
for stream.Next() {
|
||
chunk := stream.Current()
|
||
for _, choice := range chunk.Choices {
|
||
responseBuilder.WriteString(choice.Delta.Content)
|
||
}
|
||
}
|
||
|
||
if streamErr = stream.Err(); streamErr == nil {
|
||
break
|
||
}
|
||
time.Sleep(time.Second)
|
||
}
|
||
|
||
if streamErr != nil {
|
||
da.logger.Printf("发送初始消息失败,已重试: %v", streamErr)
|
||
return fmt.Errorf("发送初始消息失败: %v", streamErr)
|
||
}
|
||
|
||
response := responseBuilder.String()
|
||
// 添加模型的响应到历史记录
|
||
history.Messages = append(history.Messages, openai.AssistantMessage(response))
|
||
|
||
for {
|
||
// 休眠30秒以避免速率限制
|
||
time.Sleep(time.Second * 30)
|
||
|
||
da.logger.Printf("LLM响应:\n%s", response)
|
||
|
||
// 清理并验证响应
|
||
cleanedJSON, err := cleanDemandJSONResponse(response)
|
||
if err != nil {
|
||
// 尝试修复JSON
|
||
da.logger.Printf("尝试修复JSON: %v", err)
|
||
fixed, repairErr := da.repairDemandJSON(ctx, response, err)
|
||
if repairErr != nil {
|
||
return fmt.Errorf("JSON修复失败: %v (原始错误: %v)", repairErr, err)
|
||
}
|
||
da.logger.Printf("JSON已修复:\n%s", fixed)
|
||
cleanedJSON, err = cleanDemandJSONResponse(fixed)
|
||
if err != nil {
|
||
return fmt.Errorf("JSON验证失败: %v", err)
|
||
}
|
||
}
|
||
// 解析响应
|
||
var result DemandTree
|
||
if err := json.Unmarshal([]byte(cleanedJSON), &result); err != nil {
|
||
return fmt.Errorf("解析LLM响应失败: %v", err)
|
||
}
|
||
|
||
// 处理这一批次的需求
|
||
for _, demand := range result.Demands {
|
||
totalDemands += da.processDemandNode(demand, doc.ID, "")
|
||
}
|
||
|
||
// 更新文档处理进度
|
||
progress := result.AnalysisPercent
|
||
if err := cfg.DB().Model(doc).Update("analysis_percent", progress).Error; err != nil {
|
||
da.logger.Printf("更新进度失败: %v", err)
|
||
}
|
||
|
||
// 如果没有更多需求要处理,退出循环
|
||
if !result.HasMore {
|
||
break
|
||
}
|
||
|
||
// 使用chat history继续对话
|
||
followUpPrompt := `请继续提取剩余的需求信息,保持相同的输出格式。
|
||
请记住:
|
||
1. 不要重复之前已输出的需求,如果你想给之前的需求添加子需求,你可以设置 "parent_req_id" 为父需求的ID
|
||
2. 如果还有更多内容,添加 "has_more": true
|
||
3. 如果已经输出完所有内容,添加 "has_more": false
|
||
4. 请严格按照schema格式输出,确保是有效、完整的JSON格式。输出的JSON必须符合以上schema的规范,比如string类型的值要注意转义字符的使用。`
|
||
|
||
history.Messages = append(history.Messages, openai.UserMessage(followUpPrompt))
|
||
|
||
// 重试逻辑
|
||
for retry := 0; retry < maxRetries; retry++ {
|
||
responseBuilder.Reset()
|
||
stream := da.client.Chat.Completions.NewStreaming(ctx, openai.ChatCompletionNewParams{
|
||
Messages: openai.F(history.Messages),
|
||
Model: openai.F("qwen-plus"),
|
||
})
|
||
|
||
for stream.Next() {
|
||
chunk := stream.Current()
|
||
for _, choice := range chunk.Choices {
|
||
responseBuilder.WriteString(choice.Delta.Content)
|
||
}
|
||
}
|
||
|
||
if streamErr = stream.Err(); streamErr == nil {
|
||
response = responseBuilder.String()
|
||
history.Messages = append(history.Messages, openai.AssistantMessage(response))
|
||
break
|
||
}
|
||
|
||
da.logger.Printf("尝试 %d 失败: %v, 重试中...", retry+1, streamErr)
|
||
time.Sleep(time.Second * 30)
|
||
}
|
||
|
||
if streamErr != nil {
|
||
return fmt.Errorf("发送后续消息失败,已重试 %d 次: %v", maxRetries, streamErr)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// processDemandNode 递归处理需求节点及其子节点
|
||
func (da *DemandAnalyzer) processDemandNode(node DemandNode, docID string, parentID string) int {
|
||
count := 1 // 当前节点计数为1
|
||
var existingDemand M.Demand
|
||
if err := cfg.DB().Where("req_id = ? AND doc_id = ?", node.ReqID, docID).First(&existingDemand).Error; err == nil {
|
||
// 如果需求已存在,直接使用现有需求处理子节点
|
||
for _, child := range node.Children {
|
||
count += da.processDemandNode(child, docID, existingDemand.ID)
|
||
}
|
||
return count
|
||
}
|
||
|
||
// 计算当前节点的层级
|
||
level := 0
|
||
if parentID != "" {
|
||
// 如果有父节点,查询父节点的层级并加1
|
||
var parentDemand M.Demand
|
||
if err := cfg.DB().Where("id = ?", parentID).First(&parentDemand).Error; err == nil {
|
||
level = parentDemand.Level + 1
|
||
}
|
||
} else if node.ParentReqID != "" {
|
||
// 如果没有直接的父节点ID但有父需求ID,尝试通过ReqID查找父节点
|
||
var parentDemand M.Demand
|
||
if err := cfg.DB().Where("req_id = ? AND doc_id = ?", node.ParentReqID, docID).First(&parentDemand).Error; err == nil {
|
||
parentID = parentDemand.ID // 设置父节点ID
|
||
level = parentDemand.Level + 1
|
||
} else {
|
||
da.logger.Printf("通过ReqID查找父节点失败: %v, ReqID: %s", err, node.ParentReqID)
|
||
}
|
||
}
|
||
|
||
// 创建需求记录
|
||
demand := &M.Demand{
|
||
DocID: docID,
|
||
Description: node.Description,
|
||
ReqID: node.ReqID,
|
||
Priority: node.Priority,
|
||
Type: node.Type,
|
||
Status: node.Status,
|
||
ParentReqID: node.ParentReqID, // 添加父需求ID
|
||
Tree: M.Tree{
|
||
Name: node.Title,
|
||
ParentID: parentID,
|
||
Level: level,
|
||
},
|
||
}
|
||
|
||
if err := cfg.DB().Create(demand).Error; err != nil {
|
||
da.logger.Printf("创建需求记录失败: %v", err)
|
||
return count
|
||
}
|
||
|
||
// 递归处理子需求
|
||
for _, child := range node.Children {
|
||
count += da.processDemandNode(child, docID, demand.ID)
|
||
}
|
||
|
||
return count
|
||
}
|
||
|
||
// readDocContent 从文件中读取文档内容
|
||
func (da *DemandAnalyzer) readDocContent(filePath string) (string, error) {
|
||
ext := strings.ToLower(filepath.Ext(filePath))
|
||
|
||
switch ext {
|
||
case ".pdf":
|
||
return da.readPDF(filePath)
|
||
case ".txt":
|
||
return da.readTXT(filePath)
|
||
case ".docx":
|
||
return da.readDOCX(filePath)
|
||
default:
|
||
return "", fmt.Errorf("不支持的文件类型: %s", ext)
|
||
}
|
||
}
|
||
|
||
// readPDF 读取PDF文件内容
|
||
func (da *DemandAnalyzer) readPDF(filePath string) (string, error) {
|
||
f, r, err := pdf.Open(filePath)
|
||
if err != nil {
|
||
return "", fmt.Errorf("打开PDF文件失败: %v", err)
|
||
}
|
||
defer f.Close()
|
||
|
||
var buf bytes.Buffer
|
||
b, err := r.GetPlainText()
|
||
if err != nil {
|
||
return "", fmt.Errorf("读取PDF文本失败: %v", err)
|
||
}
|
||
|
||
buf.ReadFrom(b)
|
||
return buf.String(), nil
|
||
}
|
||
|
||
// readTXT 读取TXT文件内容
|
||
func (da *DemandAnalyzer) readTXT(filePath string) (string, error) {
|
||
content, err := os.ReadFile(filePath)
|
||
if err != nil {
|
||
return "", fmt.Errorf("读取TXT文件失败: %v", err)
|
||
}
|
||
return string(content), nil
|
||
}
|
||
|
||
// readDOCX 读取DOCX文件内容
|
||
func (da *DemandAnalyzer) readDOCX(filePath string) (string, error) {
|
||
// 使用 unidoc/unioffice 库读取 DOCX 文件
|
||
doc, err := document.Open(filePath)
|
||
if err != nil {
|
||
return "", fmt.Errorf("打开DOCX文件失败: %v", err)
|
||
}
|
||
|
||
var content strings.Builder
|
||
|
||
// 遍历所有段落并提取文本
|
||
for _, para := range doc.Paragraphs() {
|
||
for _, run := range para.Runs() {
|
||
content.WriteString(run.Text())
|
||
}
|
||
content.WriteString("\n") // 段落结束添加换行
|
||
}
|
||
|
||
return content.String(), nil
|
||
}
|
||
|
||
// DemandProcessor 处理需求文档的处理器
|
||
type DemandProcessor struct {
|
||
analyzer *DemandAnalyzer
|
||
tasks map[string]*M.Doc
|
||
processingDoc map[string]context.CancelFunc
|
||
mutex sync.Mutex
|
||
logger *log.Logger
|
||
}
|
||
|
||
var demandProcessor *DemandProcessor
|
||
var demandProcessorOnce sync.Once
|
||
|
||
// GetDemandProcessor 获取需求处理器单例
|
||
func GetDemandProcessor() *DemandProcessor {
|
||
demandProcessorOnce.Do(func() {
|
||
analyzer, err := NewDemandAnalyzer()
|
||
if err != nil {
|
||
log.Fatalf("初始化需求分析器失败: %v", err)
|
||
}
|
||
|
||
demandProcessor = &DemandProcessor{
|
||
analyzer: analyzer,
|
||
tasks: make(map[string]*M.Doc),
|
||
processingDoc: make(map[string]context.CancelFunc),
|
||
mutex: sync.Mutex{},
|
||
logger: setupLogger(),
|
||
}
|
||
})
|
||
return demandProcessor
|
||
}
|
||
|
||
// AddTask 添加需求文档处理任务
|
||
func (dp *DemandProcessor) 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 *DemandProcessor) CancelDocProcessing(docID string) {
|
||
dp.mutex.Lock()
|
||
defer dp.mutex.Unlock()
|
||
|
||
if cancel, exists := dp.processingDoc[docID]; exists {
|
||
cancel()
|
||
delete(dp.processingDoc, docID)
|
||
}
|
||
}
|
||
|
||
// processDoc 处理需求文档
|
||
func (dp *DemandProcessor) 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)
|
||
|
||
// 更新文档状态为失败
|
||
if dbErr := cfg.DB().Model(doc).Updates(map[string]interface{}{
|
||
"analysis_completed": true,
|
||
"analysis_error": errMsg,
|
||
}).Error; dbErr != nil {
|
||
dp.logger.Printf("更新文档状态失败: %v", dbErr)
|
||
}
|
||
return
|
||
}
|
||
|
||
// 提取需求
|
||
if err := dp.analyzer.extractDemands(ctx, content, doc); err != nil {
|
||
errMsg := fmt.Sprintf("提取需求失败: %v", err)
|
||
dp.logger.Printf(errMsg)
|
||
|
||
// 更新文档状态为失败
|
||
if dbErr := cfg.DB().Model(doc).Updates(map[string]interface{}{
|
||
"analysis_completed": true,
|
||
"analysis_error": errMsg,
|
||
}).Error; dbErr != nil {
|
||
dp.logger.Printf("更新文档状态失败: %v", dbErr)
|
||
}
|
||
return
|
||
}
|
||
|
||
// 更新文档状态为完成
|
||
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", doc.Name)
|
||
}
|