package fqapi import ( "bytes" "compress/gzip" "encoding/json" "fanqie/internal/common" "fmt" "regexp" "strconv" "strings" ) var ( reviewLogger = common.NewLogger("review", "INFO", "") ) // compressJSON 压缩 JSON 数据为 Gzip 格式 func compressJSON(data interface{}) ([]byte, error) { jsonData, err := json.Marshal(data) if err != nil { return nil, fmt.Errorf("JSON序列化失败: %w", err) } var compressedData bytes.Buffer gzw := gzip.NewWriter(&compressedData) if _, err := gzw.Write(jsonData); err != nil { gzw.Close() return nil, fmt.Errorf("Gzip压缩失败: %w", err) } if err := gzw.Close(); err != nil { return nil, fmt.Errorf("Gzip关闭失败: %w", err) } return compressedData.Bytes(), nil } // postReviewAPI 发送段评相关的 POST 请求 func postReviewAPI(url string, requestBody interface{}) (map[string]interface{}, error) { // 压缩 JSON compressedData, err := compressJSON(requestBody) if err != nil { return nil, err } // 使用 GorgonRequest 添加签名 resp, err := GorgonRequest(url, map[string]interface{}{ "method": "POST", "body": compressedData, "headers": map[string]string{ "User-Agent": "com.dragon.read", "Content-Type": "application/json", "Content-Encoding": "gzip", }, }) if err != nil { return nil, fmt.Errorf("请求失败: %w", err) } // 解析响应 var result map[string]interface{} if err := json.Unmarshal(resp.Content, &result); err != nil { return nil, fmt.Errorf("解析响应失败: %w", err) } // 提取 data 字段(并解包 JSON 字符串字段,如 post_data.content) if data, ok := result["data"].(map[string]interface{}); ok { if unwrapped, ok := UnwrapJSONStrings(data).(map[string]interface{}); ok { return unwrapped, nil } return data, nil } return result, nil } // CommentData 评论数据结构 type CommentData struct { Data map[string]struct { Count int `json:"count"` Hot string `json:"hot"` IsAuthorComment bool `json:"is_author_comment"` UserCount int `json:"user_count"` } `json:"data"` ItemVersion string `json:"item_version"` } // GetChapterReview 获取章节的段落评论统计(想法评论) func GetChapterReview(itemID string) (map[string]interface{}, error) { // 使用 BuildURL 添加设备参数 url := BuildURL(fmt.Sprintf("https://reading.snssdk.com/novel/commentapi/idea/list/%s/v1", itemID)) requestBody := map[string]interface{}{ "comment_source": 3, "item_id": itemID, "item_version": "cdd11695939bcc41d1ff8c3f10c67802_1_e5c2474f6821deb", } return postReviewAPI(url, requestBody) } // GetChapterMixData 获取章节混合数据(评论) func GetChapterMixData(bookID, itemID string) (map[string]interface{}, error) { // 使用 BuildURL 添加设备参数 url := BuildURL("https://api5-normal-sinfonlinec.fqnovel.com/reading/ugc/item/mix_data/get/v") requestBody := map[string]interface{}{ "count": 5000, "item_id": itemID, "source_type": 51, "book_id": bookID, } return postReviewAPI(url, requestBody) } // GetParaReview 获取段落评论或评论回复 // sort: 1=热门, 3=最新 // offset: 分页偏移量 // count: 每页数量 func GetParaReview(itemID, bookID, commentID string, para, sort, offset, count int) (map[string]interface{}, error) { // 默认排序值 if sort != 1 && sort != 3 { sort = 1 // 默认热门 } // 默认数量 if count <= 0 { count = 20 } var url string if commentID != "" { // 获取评论的回复列表 - 使用 BuildURL 添加设备参数 url = BuildURL("https://api5-normal-sinfonlineb.fqnovel.com/reading/ugc/reply/topic/v/", fmt.Sprintf("offset=%d", offset), fmt.Sprintf("group_id=%s", itemID), "service_id=1", fmt.Sprintf("count=%d", count), "source_type=63", fmt.Sprintf("book_id=%s", bookID), fmt.Sprintf("comment_id=%s", commentID), ) } else { // 获取段落的评论列表 - 使用 BuildURL 添加设备参数 url = BuildURL("https://api5-normal-sinfonlineb.fqnovel.com/reading/ugc/idea/comment_list/v/", "item_version=c63f0e6195259adec926ae3209291854_1_1099f2b8324de5f", fmt.Sprintf("offset=%d", offset), "author_user_id=104957777655", fmt.Sprintf("item_id=%s", itemID), fmt.Sprintf("count=%d", count), fmt.Sprintf("para_index=%d", para), fmt.Sprintf("book_id=%s", bookID), fmt.Sprintf("sort=%d", sort), ) } // 使用 GorgonRequest 添加签名 resp, err := GorgonRequest(url, map[string]interface{}{ "method": "GET", "headers": map[string]string{ "User-Agent": "com.dragon.read", }, }) if err != nil { return nil, fmt.Errorf("请求失败: %w", err) } // 解析响应 var result map[string]interface{} if err := json.Unmarshal(resp.Content, &result); err != nil { return nil, fmt.Errorf("解析响应失败: %w", err) } // 提取 data 字段(并解包 JSON 字符串字段,如 post_data.content) if data, ok := result["data"].(map[string]interface{}); ok { if unwrapped, ok := UnwrapJSONStrings(data).(map[string]interface{}); ok { return unwrapped, nil } return data, nil } return result, nil } // GenerateParagraphsWithComments 生成带评论标记的内容 func GenerateParagraphsWithComments(itemID, bookID, content, baseURL string) (string, error) { // 获取段评数据 data, err := GetChapterReview(itemID) if err != nil { reviewLogger.Warning("获取段评数据失败: " + err.Error()) // 如果获取失败,返回原始内容 return content, nil } // 解析评论数据 commentData := make(map[string]struct { Count int `json:"count"` Hot string `json:"hot"` IsAuthorComment bool `json:"is_author_comment"` UserCount int `json:"user_count"` }) if dataMap, ok := data["data"].(map[string]interface{}); ok { for key, val := range dataMap { if valMap, ok := val.(map[string]interface{}); ok { count := 0 if c, ok := valMap["count"].(float64); ok { count = int(c) } hot := "" if h, ok := valMap["hot"].(string); ok { hot = h } isAuthor := false if ia, ok := valMap["is_author_comment"].(bool); ok { isAuthor = ia } userCount := 0 if uc, ok := valMap["user_count"].(float64); ok { userCount = int(uc) } commentData[key] = struct { Count int `json:"count"` Hot string `json:"hot"` IsAuthorComment bool `json:"is_author_comment"` UserCount int `json:"user_count"` }{ Count: count, Hot: hot, IsAuthorComment: isAuthor, UserCount: userCount, } } } } // 检测内容格式:是否包含HTML标签(检测实际内容,而非配置) isHTMLContent := strings.Contains(content, "

") || strings.Contains(content, " 标签无法承载,保持纯文本原样返回(不注入段评); // 需要段评标记时配置 remove_html_tags: false 或 text_mode=html if !isHTMLContent { return content, nil } // HTML 内容:在每个

前插入段评标记。 // 兼容

(HTML 模式结构)与无属性的

;段号取 idx,无 idx 时按出现顺序。 // header/article/div/img 等非段落标签不受影响。 pRe := regexp.MustCompile(`(?s)]*>.*?

`) idxRe := regexp.MustCompile(`idx="(\d+)"`) order := 0 return pRe.ReplaceAllStringFunc(content, func(m string) string { defer func() { order++ }() para := order if idx := idxRe.FindStringSubmatch(m); idx != nil { if n, err := strconv.Atoi(idx[1]); err == nil { para = n } } if commentInfo, exists := commentData[fmt.Sprintf("%d", para)]; exists && commentInfo.Count > 0 { commentTag := fmt.Sprintf(``, baseURL, itemID, bookID, para, commentInfo.Count) return m[:len(m)-len("

")] + commentTag + "

" } return m }), nil } // extractDeviceID 从 device header 中提取 device_id func extractDeviceID(header string) string { if header == "" { return "0" } // 从 header 中提取 device_id parts := strings.Split(header, "&") for _, part := range parts { if strings.HasPrefix(part, "device_id=") { return strings.TrimPrefix(part, "device_id=") } } return "0" } // ParseJSON 解析 JSON 字符串 func ParseJSON(data []byte, v interface{}) error { return json.Unmarshal(data, v) } // ToJSON 将对象序列化为 JSON 字符串 func ToJSON(v interface{}) (string, error) { buffer := &strings.Builder{} encoder := json.NewEncoder(buffer) encoder.SetEscapeHTML(false) // 禁用HTML转义,避免 " 被转义为 \" err := encoder.Encode(v) if err != nil { return "", err } // Encode 会在末尾添加换行符,需要去掉 result := buffer.String() return strings.TrimSuffix(result, "\n"), nil }