watchvuln icon indicating copy to clipboard operation
watchvuln copied to clipboard

AI 驱动的漏洞情报 SCA 分析

Open Challengers-win opened this issue 11 months ago • 2 comments

刚好集成和sca库对接,其中核心需要ai或者正则提取组件和判断是否推送,下面是核心代码提取代码

package util

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"regexp"
	"strconv"
	"strings"
	"time"
)

// AIExtractResult AI提取结果结构
type AIExtractResult struct {
	Dependency  string  `json:"dependency"`
	Version     string  `json:"version"`
	Confidence  float64 `json:"confidence,omitempty"`
	RawResponse string  `json:"raw_response,omitempty"`
}

// AIConfig AI配置
type AIConfig struct {
	URL            string        `json:"url"`
	APIKey         string        `json:"api_key"`
	Model          string        `json:"model"`
	FallbackModels []string      `json:"fallback_models"`
	Temperature    float64       `json:"temperature"`
	MaxTokens      int           `json:"max_tokens"`
	Timeout        time.Duration `json:"timeout"`
	MaxRetries     int           `json:"max_retries"`
}

// DefaultAIConfig 默认AI配置
func DefaultAIConfig() *AIConfig {
	return &AIConfig{
		URL:         "https://api.openai.com/v1/chat/completions",
		Model:       "gpt-4",
		Temperature: 0.1,
		MaxTokens:   5000,
		Timeout:     30 * time.Second,
		MaxRetries:  3,
	}
}

// ExtractDependencyAndVersionWithRetry 带重试的AI提取
func ExtractDependencyAndVersionWithRetry(title, description string, config *AIConfig) (*AIExtractResult, error) {
	if config == nil {
		config = DefaultAIConfig()
	}

	// 尝试主模型和备用模型
	models := append([]string{config.Model}, config.FallbackModels...)
	var lastErr error
	for _, model := range models {
		cfgCopy := *config
		cfgCopy.Model = model
		for attempt := 0; attempt < cfgCopy.MaxRetries; attempt++ {
			fmt.Printf("[AI提取] 尝试模型: %s, 第%d次尝试\n", model, attempt+1)
			result, err := ExtractDependencyAndVersionAI(title, description, &cfgCopy)
			if err == nil {
				fmt.Printf("[AI提取] 模型: %s, 返回: %+v\n", model, result)
				if isValidResult(result) {
					fmt.Printf("[AI提取] 模型: %s, 提取成功: 依赖=%s, 版本=%s, 置信度=%.2f\n", model, result.Dependency, result.Version, result.Confidence)
					return result, nil
				}
				lastErr = fmt.Errorf("AI返回结果无效: dependency=%s, version=%s", result.Dependency, result.Version)
				fmt.Printf("[AI提取] 模型: %s, 结果无效: %v\n", model, lastErr)
			} else {
				lastErr = err
				fmt.Printf("[AI提取] 模型: %s, 错误: %v\n", model, err)
			}

			if attempt < cfgCopy.MaxRetries-1 {
				time.Sleep(time.Duration(attempt+1) * time.Second)
			}
		}
	}

	fmt.Printf("[AI提取] 所有模型均失败,最后错误: %v\n", lastErr)
	return nil, fmt.Errorf("AI提取失败,主模型及所有备用模型均失败: %v", lastErr)
}

// ExtractComponentAndVersionEnhanced 增强版提取函数
func ExtractComponentAndVersionEnhanced(
	title, description string,
	aiEnabled, forceRegex bool,
	config *AIConfig,
) (component, version string, confidence float64, err error) {

	if aiEnabled && !forceRegex {
		result, err := ExtractDependencyAndVersionWithRetry(title, description, config)
		if err == nil && isValidResult(result) {
			return result.Dependency, result.Version, result.Confidence, nil
		}
		fmt.Printf("AI提取失败,降级到正则提取: %v\n", err)
	}

	comp, ver, regexErr := ExtractDependencyAndVersionRegex(title, description)
	if regexErr == nil {
		return comp, ver, 0.6, nil
	}

	return "", "", 0, fmt.Errorf("AI和正则提取均失败: AI错误=%v, 正则错误=%v", err, regexErr)
}

// ExtractDependencyAndVersionAI AI方式提取(改进版)
func ExtractDependencyAndVersionAI(title, description string, config *AIConfig) (*AIExtractResult, error) {
	if config == nil {
		config = DefaultAIConfig()
	}

	prompt := buildRobustPrompt(title, description)

	ctx, cancel := context.WithTimeout(context.Background(), config.Timeout)
	defer cancel()

	reqBody := map[string]interface{}{
		"model":       config.Model,
		"temperature": config.Temperature,
		"max_tokens":  config.MaxTokens,
		"messages": []map[string]string{
			{"role": "system", "content": "你是专业的网络安全分析师,专门从漏洞信息中提取准确的依赖组件名和版本号。请严格按照JSON格式返回结果。"},
			{"role": "user", "content": prompt},
		},
	}

	body, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("构建请求失败: %v", err)
	}

	req, err := http.NewRequestWithContext(ctx, "POST", config.URL, bytes.NewBuffer(body))
	if err != nil {
		return nil, fmt.Errorf("创建请求失败: %v", err)
	}

	req.Header.Set("Authorization", "Bearer "+config.APIKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("请求失败: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		respBody, _ := ioutil.ReadAll(resp.Body)
		fmt.Printf("[AI提取] 模型: %s, API返回错误状态码 %d: %s\n", config.Model, resp.StatusCode, string(respBody))
		return nil, fmt.Errorf("API返回错误状态码 %d: %s", resp.StatusCode, string(respBody))
	}

	respBody, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("读取响应失败: %v", err)
	}

	var result struct {
		Choices []struct {
			Message struct {
				Content string `json:"content"`
			} `json:"message"`
		} `json:"choices"`
		Error struct {
			Message string `json:"message"`
		} `json:"error"`
	}

	if err := json.Unmarshal(respBody, &result); err != nil {
		fmt.Printf("[AI提取] 模型: %s, 解析API响应失败: %v, 原始内容: %s\n", config.Model, err, string(respBody))
		return nil, fmt.Errorf("解析API响应失败: %v", err)
	}

	if len(result.Choices) > 0 {
		fmt.Printf("[AI提取] 模型: %s, 原始返回: %s\n", config.Model, result.Choices[0].Message.Content)
	}

	if result.Error.Message != "" {
		fmt.Printf("[AI提取] 模型: %s, API返回错误: %s\n", config.Model, result.Error.Message)
		return nil, fmt.Errorf("API返回错误: %s", result.Error.Message)
	}

	if len(result.Choices) == 0 {
		fmt.Printf("[AI提取] 模型: %s, API返回空结果\n", config.Model)
		return nil, fmt.Errorf("API返回空结果")
	}

	content := strings.TrimSpace(result.Choices[0].Message.Content)
	if content == "" {
		fmt.Printf("[AI提取] 模型: %s, AI返回空内容\n", config.Model)
		return nil, fmt.Errorf("AI返回空内容")
	}

	aiResult, err := parseAIResponse(content)
	if err != nil {
		fmt.Printf("[AI提取] 模型: %s, 解析AI响应失败: %v, 原始内容: %s\n", config.Model, err, content)
		return nil, fmt.Errorf("解析AI响应失败: %v, 原始内容: %s", err, content)
	}

	aiResult.RawResponse = content
	fmt.Printf("[AI提取] 模型: %s, 解析后结果: 依赖=%s, 版本=%s, 置信度=%.2f\n", config.Model, aiResult.Dependency, aiResult.Version, aiResult.Confidence)
	return aiResult, nil
}

// buildRobustPrompt 构建健壮的prompt
func buildRobustPrompt(title, description string) string {
	return fmt.Sprintf(`请从以下漏洞信息中提取最准确的依赖组件名(dependency)和版本号(version)。

要求:
1. 返回严格的JSON格式:{"dependency":"组件名","version":"版本号","confidence":0.0-1.0}
2. dependency: 软件包/库/组件的准确名称,去除无关词汇
3. version: 具体版本号(如1.2.3),如果有多个版本取最相关的
4. confidence: 提取结果的置信度(0.0-1.0)
5. 如果无法确定某个字段,请设为空字符串""
6. 不要添加任何解释文字,只返回JSON

漏洞信息:
标题: %s
描述: %s

请返回JSON:`, title, description)
}

// parseAIResponse 解析AI响应的多种格式
func parseAIResponse(content string) (*AIExtractResult, error) {
	content = strings.TrimSpace(content)

	strategies := []func(string) (*AIExtractResult, error){
		parseDirectJSON,
		parseMarkdownJSON,
		parseTextWithJSON,
		parseRegexExtraction,
		parseLLMHallucination,
	}

	var lastErr error
	for i, strategy := range strategies {
		result, err := strategy(content)
		if err == nil && isValidResult(result) {
			if result.Confidence == 0 {
				result.Confidence = 1.0 - float64(i)*0.1
			}
			return result, nil
		}
		lastErr = err
	}

	return nil, fmt.Errorf("所有解析策略均失败,最后错误: %v", lastErr)
}

// parseDirectJSON 解析直接的JSON格式
func parseDirectJSON(content string) (*AIExtractResult, error) {
	var result AIExtractResult
	err := json.Unmarshal([]byte(content), &result)
	return &result, err
}

// parseMarkdownJSON 解析Markdown包裹的JSON
func parseMarkdownJSON(content string) (*AIExtractResult, error) {
	patterns := []string{
		"```json\n(.+?)\n```",
		"```\n(.+?)\n```",
		"`(.+?)`",
	}

	for _, pattern := range patterns {
		re := regexp.MustCompile(`(?s)` + pattern)
		if matches := re.FindStringSubmatch(content); len(matches) > 1 {
			var result AIExtractResult
			if err := json.Unmarshal([]byte(strings.TrimSpace(matches[1])), &result); err == nil {
				return &result, nil
			}
		}
	}

	return nil, fmt.Errorf("未找到有效的Markdown JSON")
}

// parseTextWithJSON 解析文本+JSON混合格式
func parseTextWithJSON(content string) (*AIExtractResult, error) {
	re := regexp.MustCompile(`\{[^{}]*"dependency"[^{}]*\}`)
	matches := re.FindAllString(content, -1)

	for _, match := range matches {
		var result AIExtractResult
		if err := json.Unmarshal([]byte(match), &result); err == nil {
			return &result, nil
		}
	}

	return nil, fmt.Errorf("未找到有效的JSON对象")
}

// parseRegexExtraction 正则提取JSON字段
func parseRegexExtraction(content string) (*AIExtractResult, error) {
	result := &AIExtractResult{}

	depPatterns := []string{
		`"dependency"\s*:\s*"([^"]+)"`,
		`dependency\s*[:=]\s*"?([^",\s}]+)"?`,
		`组件[名称]*\s*[::]\s*"?([^",\s}]+)"?`,
	}

	for _, pattern := range depPatterns {
		re := regexp.MustCompile(pattern)
		if matches := re.FindStringSubmatch(content); len(matches) > 1 {
			result.Dependency = strings.TrimSpace(matches[1])
			break
		}
	}

	verPatterns := []string{
		`"version"\s*:\s*"([^"]+)"`,
		`version\s*[:=]\s*"?([^",\s}]+)"?`,
		`版本[号]*\s*[::]\s*"?([^",\s}]+)"?`,
		`([0-9]+\.[0-9]+(?:\.[0-9]+)?)`,
	}

	for _, pattern := range verPatterns {
		re := regexp.MustCompile(pattern)
		if matches := re.FindStringSubmatch(content); len(matches) > 1 {
			result.Version = strings.TrimSpace(matches[1])
			break
		}
	}

	confPatterns := []string{
		`"confidence"\s*:\s*([0-9.]+)`,
		`confidence\s*[:=]\s*([0-9.]+)`,
	}

	for _, pattern := range confPatterns {
		re := regexp.MustCompile(pattern)
		if matches := re.FindStringSubmatch(content); len(matches) > 1 {
			if conf, err := strconv.ParseFloat(matches[1], 64); err == nil {
				result.Confidence = conf
			}
			break
		}
	}

	if result.Dependency != "" || result.Version != "" {
		return result, nil
	}

	return nil, fmt.Errorf("正则提取失败")
}

// parseLLMHallucination 处理LLM幻觉内容
func parseLLMHallucination(content string) (*AIExtractResult, error) {
	content = strings.ReplaceAll(content, "根据提供的漏洞信息", "")
	content = strings.ReplaceAll(content, "分析如下", "")
	content = strings.ReplaceAll(content, "提取结果为", "")

	lines := strings.Split(content, "\n")
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if strings.HasPrefix(line, "{") && strings.HasSuffix(line, "}") {
			var result AIExtractResult
			if err := json.Unmarshal([]byte(line), &result); err == nil {
				return &result, nil
			}
		}
	}

	return nil, fmt.Errorf("无法处理幻觉内容")
}

// isValidResult 验证结果有效性
func isValidResult(result *AIExtractResult) bool {
	if result == nil {
		return false
	}

	dep := strings.TrimSpace(result.Dependency)
	ver := strings.TrimSpace(result.Version)

	if dep == "" && ver == "" {
		return false
	}

	if dep != "" && (len(dep) < 2 || len(dep) > 100) {
		return false
	}

	if ver != "" {
		versionPattern := regexp.MustCompile(`^[0-9]+(\.[0-9]+)*([a-zA-Z0-9\-\+]*)?$`)
		if !versionPattern.MatchString(ver) {
			return false
		}
	}

	return true
}

// ExtractDependencyAndVersionRegex 正则方式提取(增强版)
func ExtractDependencyAndVersionRegex(title, description string) (string, string, error) {
	text := title + " " + description

	patterns := []struct {
		name    string
		pattern string
		depIdx  int
		verIdx  int
	}{
		{"标准格式", `([A-Za-z0-9\-_\.]+)[\s,,、]+([0-9]+\.[0-9]+(?:\.[0-9]+)?)`, 1, 2},
		{"漏洞标题格式", `^([A-Za-z0-9\-_\.]+)[\s]*漏洞.*?([0-9]+\.[0-9]+(?:\.[0-9]+)?)`, 1, 2},
		{"版本号在前", `([0-9]+\.[0-9]+(?:\.[0-9]+)?)[\s]*([A-Za-z0-9\-_\.]+)`, 2, 1},
		{"中文描述", `([A-Za-z0-9\-_\.]+)[\s]*组件.*?([0-9]+\.[0-9]+(?:\.[0-9]+)?)`, 1, 2},
		{"CVE格式", `([A-Za-z0-9\-_\.]+)[\s]*([0-9]+\.[0-9]+(?:\.[0-9]+)?)[\s]*存在`, 1, 2},
		{"英文描述", `([A-Za-z0-9\-_\.]+)[\s]+version[\s]*([0-9]+\.[0-9]+(?:\.[0-9]+)?)`, 1, 2},
		{"括号版本", `([A-Za-z0-9\-_\.]+)[\s]*\(([0-9]+\.[0-9]+(?:\.[0-9]+)?)\)`, 1, 2},
		{"冒号分隔", `([A-Za-z0-9\-_\.]+)[\s]*:[\s]*([0-9]+\.[0-9]+(?:\.[0-9]+)?)`, 1, 2},
	}

	// 尝试匹配所有模式
	for _, p := range patterns {
		re := regexp.MustCompile(p.pattern)
		if matches := re.FindStringSubmatch(text); len(matches) > max(p.depIdx, p.verIdx) {
			dependency := strings.TrimSpace(matches[p.depIdx])
			version := strings.TrimSpace(matches[p.verIdx])
			if dependency != "" && version != "" {
				return dependency, version, nil
			}
		}
	}

	// 单独提取组件名的模式
	componentPatterns := []string{
		`^([A-Za-z0-9\-_\.]+)[\s]*漏洞`,
		`([A-Za-z0-9\-_\.]+)[\s]*存在安全漏洞`,
		`([A-Za-z0-9\-_\.]+)[\s]*组件`,
		`^([A-Za-z][A-Za-z0-9\-_\.]{1,})`,
	}

	component := ""
	for _, pattern := range componentPatterns {
		re := regexp.MustCompile(pattern)
		if matches := re.FindStringSubmatch(title); len(matches) > 1 {
			component = strings.TrimSpace(matches[1])
			break
		}
	}

	// 单独提取版本号的模式
	versionPatterns := []string{
		`([0-9]+\.[0-9]+\.[0-9]+)`,
		`([0-9]+\.[0-9]+)`,
		`v([0-9]+\.[0-9]+(?:\.[0-9]+)?)`,
		`版本[\s]*([0-9]+\.[0-9]+(?:\.[0-9]+)?)`,
		`version[\s]*([0-9]+\.[0-9]+(?:\.[0-9]+)?)`,
	}

	version := ""
	for _, pattern := range versionPatterns {
		re := regexp.MustCompile(pattern)
		if matches := re.FindStringSubmatch(text); len(matches) > 1 {
			version = strings.TrimSpace(matches[1])
			break
		}
	}

	// 清理和验证结果
	component = cleanComponentName(component)
	version = cleanVersionString(version)

	if component != "" || version != "" {
		return component, version, nil
	}

	return "", "", fmt.Errorf("未匹配到组件和版本")
}

// max 返回两个数中的较大值
func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

// cleanComponentName 清理组件名
func cleanComponentName(component string) string {
	if component == "" {
		return ""
	}

	// 移除常见的无关词汇
	removeWords := []string{"漏洞", "组件", "软件", "库", "包", "vulnerability", "component", "software", "library", "package"}
	for _, word := range removeWords {
		component = strings.ReplaceAll(component, word, "")
	}

	// 移除特殊字符
	component = regexp.MustCompile(`[^\w\-\.]`).ReplaceAllString(component, "")

	return strings.TrimSpace(component)
}

// cleanVersionString 清理版本号字符串
func cleanVersionString(version string) string {
	if version == "" {
		return ""
	}
	// 移除版本号前缀
	version = strings.TrimPrefix(version, "v")
	version = strings.TrimPrefix(version, "V")
	// 确保版本号格式正确
	re := regexp.MustCompile(`^([0-9]+(?:\.[0-9]+)*(?:[a-zA-Z0-9\-\+]*)?)`) // 这里括号要闭合
	if matches := re.FindStringSubmatch(version); len(matches) > 1 {
		return matches[1]
	}
	return strings.TrimSpace(version)
}

// ExtractDependencyAndVersionFallback 兜底提取函数
func ExtractDependencyAndVersionFallback(title, description string) (string, string, error) {
	text := strings.ToLower(title + " " + description)

	// 通用软件包名模式
	commonPackages := []string{
		"apache", "nginx", "mysql", "postgresql", "redis", "mongodb", "elasticsearch",
		"spring", "struts", "hibernate", "jackson", "fastjson", "log4j", "slf4j",
		"jquery", "react", "vue", "angular", "bootstrap", "lodash", "moment",
		"openssl", "curl", "wget", "git", "svn", "docker", "kubernetes",
		"python", "pip", "django", "flask", "requests", "numpy", "pandas",
		"node", "npm", "express", "socket", "webpack", "babel", "typescript",
		"java", "maven", "gradle", "junit", "mockito", "guava", "commons",
		"php", "composer", "symfony", "laravel", "wordpress", "drupal",
		"ruby", "gem", "rails", "sinatra", "bundler", "rake",
		"go", "gin", "echo", "beego", "gorm", "cobra",
		"dotnet", "nuget", "entityframework", "newtonsoft", "autofac",
	}

	for _, pkg := range commonPackages {
		if strings.Contains(text, pkg) {
			// 尝试提取版本号
			versionRegex := regexp.MustCompile(pkg + `[\s\-_]*([0-9]+\.[0-9]+(?:\.[0-9]+)?)`)
			if matches := versionRegex.FindStringSubmatch(text); len(matches) > 1 {
				return pkg, matches[1], nil
			}
			return pkg, "", nil
		}
	}

	return "", "", fmt.Errorf("兜底提取失败")
}

// ValidateExtractResult 验证提取结果
func ValidateExtractResult(dependency, version string) (bool, string) {
	if dependency == "" && version == "" {
		return false, "依赖名和版本号均为空"
	}

	if dependency != "" {
		if len(dependency) < 2 {
			return false, "依赖名过短"
		}
		if len(dependency) > 100 {
			return false, "依赖名过长"
		}
		// 检查是否包含有效字符
		if !regexp.MustCompile(`^[A-Za-z0-9\-_\.]+$`).MatchString(dependency) {
			return false, "依赖名包含无效字符"
		}
	}

	if version != "" {
		// 版本号格式验证
		versionPattern := regexp.MustCompile(`^[0-9]+(\.[0-9]+)*([a-zA-Z0-9\-\+]*)?$`)
		if !versionPattern.MatchString(version) {
			return false, "版本号格式无效"
		}
	}

	return true, ""
}

// ExtractDependencyAndVersionSmart 智能提取函数(主入口)
func ExtractDependencyAndVersionSmart(
	title, description string,
	aiEnabled bool,
	config *AIConfig,
) (dependency, version string, confidence float64, method string, err error) {

	// 输入验证
	if strings.TrimSpace(title) == "" && strings.TrimSpace(description) == "" {
		return "", "", 0, "", fmt.Errorf("标题和描述均为空")
	}

	// 方法1: AI提取(如果启用)
	if aiEnabled && config != nil && config.APIKey != "" {
		if result, err := ExtractDependencyAndVersionWithRetry(title, description, config); err == nil {
			if valid, _ := ValidateExtractResult(result.Dependency, result.Version); valid {
				return result.Dependency, result.Version, result.Confidence, "AI", nil
			}
		}
	}

	// 方法2: 正则提取
	if dep, ver, err := ExtractDependencyAndVersionRegex(title, description); err == nil {
		if valid, _ := ValidateExtractResult(dep, ver); valid {
			return dep, ver, 0.7, "正则", nil
		}
	}

	// 方法3: 兜底提取
	if dep, ver, err := ExtractDependencyAndVersionFallback(title, description); err == nil {
		if valid, _ := ValidateExtractResult(dep, ver); valid {
			return dep, ver, 0.5, "兜底", nil
		}
	}

	return "", "", 0, "", fmt.Errorf("所有提取方法均失败")
}

// LogExtractResult 记录提取结果(用于调试和统计)
func LogExtractResult(title, description, dependency, version, method string, confidence float64, success bool) {
	// 这里可以添加日志记录逻辑
	fmt.Printf("[提取结果] 方法=%s, 成功=%v, 置信度=%.2f, 依赖=%s, 版本=%s\n",
		method, success, confidence, dependency, version)
}

// BatchExtract 批量提取(用于批处理)
func BatchExtract(items []struct{ Title, Description string }, config *AIConfig) []AIExtractResult {
	results := make([]AIExtractResult, len(items))

	for i, item := range items {
		dep, ver, conf, method, err := ExtractDependencyAndVersionSmart(
			item.Title, item.Description, true, config)

		results[i] = AIExtractResult{
			Dependency: dep,
			Version:    ver,
			Confidence: conf,
		}

		if err != nil {
			results[i].RawResponse = fmt.Sprintf("错误: %v, 方法: %s", err, method)
		} else {
			results[i].RawResponse = fmt.Sprintf("方法: %s", method)
		}
	}

	return results
}

// 兼容老接口
func ExtractComponentAndVersion(
	title, description string,
	aiEnabled, forceRegex bool,
	aiUrl, aiKey, aiModel string,
) (component, version string, err error) {
	cfg := &AIConfig{
		URL:         aiUrl,
		APIKey:      aiKey,
		Model:       aiModel,
		Temperature: 0.1,
		MaxTokens:   500,
		Timeout:     30 * time.Second,
		MaxRetries:  3,
	}
	// 如果forceRegex为true,则只用正则
	if forceRegex {
		return ExtractDependencyAndVersionRegex(title, description)
	}
	dep, ver, _, _, err := ExtractDependencyAndVersionSmart(title, description, aiEnabled, cfg)
	return dep, ver, err
}

// AI辅助判断WSS依赖库是否与组件真正相关
func AIJudgeWSSMatch(component, version string, wssLibs []string, config *AIConfig) (string, error) {
	prompt := fmt.Sprintf(`你是专业的软件成分分析专家。现在有如下信息:
组件名: %s
版本: %s
WSS依赖库列表如下(每行为一个依赖,格式为"依赖名 - 版本 (项目: 项目名)"):
%s

请判断哪些依赖库真正属于该组件,哪些是无关的,并简要说明理由,如果全部无关请直接回复"无真正相关依赖"。
请只输出:
1. 相关依赖(如有,列出)
逐个输出相关的:依赖名 - 版本 (项目: 项目名)
2. 你的关键分析结论(简明扼要)

不要输出JSON,不要重复组件名和版本号,只输出分析文本,总输出请控制在10000字符以内。`,
		component, version, strings.Join(wssLibs, "\n"))

	text, err := CallAITextOnly(prompt, config)
	if err != nil {
		return "", err
	}
	return text, nil
}

// CallAITextOnly 让AI返回纯文本分析
func CallAITextOnly(prompt string, config *AIConfig) (string, error) {
	if config == nil {
		config = DefaultAIConfig()
	}
	ctx, cancel := context.WithTimeout(context.Background(), config.Timeout)
	defer cancel()

	reqBody := map[string]interface{}{
		"model":       config.Model,
		"temperature": config.Temperature,
		"max_tokens":  config.MaxTokens,
		"messages": []map[string]string{
			{"role": "system", "content": "你是专业的软件成分分析专家,只输出分析结论文本,不要输出JSON,不要重复组件名和版本号。"},
			{"role": "user", "content": prompt},
		},
	}
	body, err := json.Marshal(reqBody)
	if err != nil {
		return "", fmt.Errorf("构建请求失败: %v", err)
	}
	req, err := http.NewRequestWithContext(ctx, "POST", config.URL, bytes.NewBuffer(body))
	if err != nil {
		return "", fmt.Errorf("创建请求失败: %v", err)
	}
	req.Header.Set("Authorization", "Bearer "+config.APIKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("请求失败: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		respBody, _ := ioutil.ReadAll(resp.Body)
		return "", fmt.Errorf("API返回错误状态码 %d: %s", resp.StatusCode, string(respBody))
	}
	respBody, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", fmt.Errorf("读取响应失败: %v", err)
	}
	var result struct {
		Choices []struct {
			Message struct {
				Content string `json:"content"`
			} `json:"message"`
		} `json:"choices"`
		Error struct {
			Message string `json:"message"`
		} `json:"error"`
	}
	if err := json.Unmarshal(respBody, &result); err != nil {
		return "", fmt.Errorf("解析API响应失败: %v", err)
	}
	if result.Error.Message != "" {
		return "", fmt.Errorf("API返回错误: %s", result.Error.Message)
	}
	if len(result.Choices) == 0 {
		return "", fmt.Errorf("API返回空结果")
	}
	content := strings.TrimSpace(result.Choices[0].Message.Content)
	if len(content) > 10000 {
		content = content[:10000]
	}
	return content, nil
}

Challengers-win avatar May 23 '25 09:05 Challengers-win

我稍微调整了一下代码格式,方便查看。

这个 sca 分析写的挺细致的,考虑了很多种情况,能看出来大佬是打磨了很久的成果。不过想保持这个项目简单纯粹一些,暂时不打算融合到代码库里,先放在这给其他人参考一下吧

zema1 avatar May 30 '25 05:05 zema1

我稍微调整了一下代码格式,方便查看。

这个 sca 分析写的挺细致的,考虑了很多种情况,能看出来大佬是打磨了很久的成果。不过想保持这个项目简单纯粹一些,暂时不打算融合到代码库里,先放在这给其他人参考一下吧

那就做成sdk吧 铁铁

Mia0a-hi avatar Jun 04 '25 08:06 Mia0a-hi