package fqapi import ( "encoding/base64" "encoding/json" "fanqie/internal/common" "fmt" "sort" "strings" ) var ( contentLogger = common.NewLogger("content", "INFO", "") ) // ContentFetcher 内容获取器 type ContentFetcher struct{} // HandleGetContent 处理获取内容 func HandleGetContent(itemID string, textMode, imageMode interface{}) (string, error) { if itemID == "" { return `{"code": -1, "msg": "需要参数 item_id"}`, nil } // 参数转换(URL 参数优先,未传时回退配置) textModeBool := parseBoolParamDefault(textMode, getContentConfig().RemoveHTMLTags) imageModeBool := parseBoolParamDefault(imageMode, !getContentConfig().RemoveImages) // contentLogger.Info(fmt.Sprintf("获取内容 item_id=%s text_mode=%v image_mode=%v", itemID, textModeBool, imageModeBool)) // 检查缓存 cache := GetCache("content_" + itemID) if cache != "" { return processCachedContent(cache, textModeBool, imageModeBool) } // 缓存未命中,从API获取 return fetchContentFromAPI(itemID, textModeBool, imageModeBool) } // processCachedContent 处理缓存内容 func processCachedContent(cache string, textMode, imageMode bool) (string, error) { var d map[string]interface{} json.Unmarshal([]byte(cache), &d) if data, ok := d["data"].(map[string]interface{}); ok { if content, ok := data["content"].(string); ok { parser := NewContentParser() data["content"] = parser.Parse(content, textMode, imageMode) } } return buildJSONResponse(UnwrapJSONStrings(d)), nil } // fetchContentFromAPI 从API获取内容(带设备切换重试) func fetchContentFromAPI(itemID string, textMode, imageMode bool) (string, error) { return fetchContentWithRetry(itemID, textMode, imageMode, 0) } // fetchContentWithRetry 从API获取内容,支持设备切换重试 func fetchContentWithRetry(itemID string, textMode, imageMode bool, retryCount int) (string, error) { // 防止无限递归 const maxRetries = 5 if retryCount >= maxRetries { contentLogger.Warning(fmt.Sprintf("已重试 %d 次,尝试备用接口", maxRetries)) return tryBackupAPI(itemID, textMode, imageMode) } // 从设备密钥池获取设备 pool := GetDeviceKeyPool() item, err := pool.GetNext() if err != nil { contentLogger.Error("从设备池获取设备失败: " + err.Error()) return `{"code": -1, "msg": "设备池暂无可用设备"}`, err } header := item.DeviceHeader currentKeyInfo := &KeyInfo{ Key: item.Key, Ver: item.KeyVer, } contentLogger.Debug(fmt.Sprintf("使用池中设备 ID=%d (重试次数: %d)", item.ID, retryCount)) // 移除version_name参数 headerWithoutVersion := removeVersionFromHeader(header) params := []string{ "aid=1967", "app_name=novelapp", "channel=0", "device_platform=android", "version_code=66.9", "item_id=" + itemID, "os_version=0", headerWithoutVersion, } allParams := strings.Join(params, "&") paramList := strings.Split(allParams, "&") sort.Strings(paramList) sortedParams := strings.Join(paramList, "&") url := "https://reading.snssdk.com/reading/reader/full/v/?" + sortedParams resp, err := ContentRequest(url, map[string]interface{}{ "method": "GET", "headers": map[string]string{ "User-Agent": "com.dragon.read", }, }) if err != nil { contentLogger.Warning(fmt.Sprintf("主接口请求出错,标记设备无效并切换设备: %v", err)) pool.MarkInvalid(header) pool.RemoveInvalid() return fetchContentWithRetry(itemID, textMode, imageMode, retryCount+1) } var body map[string]interface{} json.Unmarshal(resp.Content, &body) if code, ok := body["code"].(float64); ok && code == 0 { data, ok := body["data"].(map[string]interface{}) if !ok { contentLogger.Warning("响应data字段无效,切换设备重试") pool.MarkInvalid(header) pool.RemoveInvalid() return fetchContentWithRetry(itemID, textMode, imageMode, retryCount+1) } contentEncoded, ok := data["content"].(string) if !ok || contentEncoded == "Invalid" { // 内容无效,标记当前设备为无效并切换设备 contentLogger.Warning(fmt.Sprintf("内容返回 Invalid,标记设备无效并切换设备 (重试: %d)", retryCount)) pool.MarkInvalid(header) pool.RemoveInvalid() return fetchContentWithRetry(itemID, textMode, imageMode, retryCount+1) } // 检查密钥版本 keyVersion, _ := data["key_version"].(float64) if currentKeyInfo != nil && int(keyVersion) != currentKeyInfo.Ver { contentLogger.Warning("密钥版本不匹配,重新获取密钥") // 更新池中的密钥 newKeyInfo, err := GetKey(header) if err == nil { UpdateKeyInPool(header, newKeyInfo.Key, newKeyInfo.Ver) currentKeyInfo = newKeyInfo } else { // 获取密钥失败,切换设备 contentLogger.Warning("获取密钥失败,切换设备重试") pool.MarkInvalid(header) pool.RemoveInvalid() return fetchContentWithRetry(itemID, textMode, imageMode, retryCount+1) } } // 解密内容 contentBytes, _ := base64.StdEncoding.DecodeString(contentEncoded) if len(contentBytes) < 16 { contentLogger.Warning("内容长度不足,切换设备重试") pool.MarkInvalid(header) pool.RemoveInvalid() return fetchContentWithRetry(itemID, textMode, imageMode, retryCount+1) } iv := contentBytes[0:16] encryptedData := contentBytes[16:] keyBytes, _ := base64.StdEncoding.DecodeString(currentKeyInfo.Key) decrypted, err := De(encryptedData, keyBytes, iv) if err != nil { contentLogger.Warning(fmt.Sprintf("解密失败,切换设备重试: %v", err)) pool.MarkInvalid(header) pool.RemoveInvalid() return fetchContentWithRetry(itemID, textMode, imageMode, retryCount+1) } // 处理内容 contentStr := string(decrypted) contentStr = strings.Replace(contentStr, ``, "", -1) contentStr = strings.TrimSpace(contentStr) data["content"] = contentStr // 更新缓存 cacheData := buildCacheData(data) saveCacheJSON("content_"+itemID, cacheData, 86400*2) // 解析内容 parser := NewContentParser() data["content"] = parser.Parse(contentStr, textMode, imageMode) return buildJSONResponse(UnwrapJSONStrings(body)), nil } // 响应code不为0,切换设备重试 contentLogger.Warning(fmt.Sprintf("响应code异常: %.0f,切换设备重试", body["code"])) pool.MarkInvalid(header) pool.RemoveInvalid() return fetchContentWithRetry(itemID, textMode, imageMode, retryCount+1) } // tryBackupAPI 尝试备用API(所有设备都失败后的最后手段) func tryBackupAPI(itemID string, textMode, imageMode bool) (string, error) { contentLogger.Warning("尝试备用接口1(所有设备均失败)") // 从设备池获取任意一个有效设备用于备用接口 pool := GetDeviceKeyPool() item, err := pool.GetNext() if err != nil { contentLogger.Error("备用接口:无法获取设备") return `{"code": -1, "msg": "获取失败"}`, err } header := item.DeviceHeader // 备用接口1:使用DH加密 backupAPI := fmt.Sprintf("https://novel.snssdk.com/api/novel/book/reader/content/v1?support_image=1&item_id=%s&aid=1319&app_name=novelapp&channel=0&device_platform=android&os_version=2&version_code=58932&%s", itemID, header) cm := NewCM() resp, err := GorgonRequest(backupAPI, map[string]interface{}{ "method": "GET", "headers": map[string]string{ "User-Agent": "com.dragon.read", "y": cm.ClientHandshake(), }, }) if err != nil { contentLogger.Warning("备用接口1获取正文出错!") return `{"code": -1, "msg": "获取失败"}`, err } var body map[string]interface{} json.Unmarshal(resp.Content, &body) if code, ok := body["code"].(float64); ok && code == 0 { delete(body, "log_id") data, ok := body["data"].(map[string]interface{}) if !ok { return `{"code": -1, "msg": "获取失败"}`, err } contentEncrypted, ok := data["content"].(string) if !ok || contentEncrypted == "Bad Content" { return `{"code": -1, "msg": "获取失败"}`, err } // 解密内容 cHeader := resp.GetHeader("c") yHeader := resp.GetHeader("y") decryptedContent, err := cm.Decrypt(cHeader, yHeader, contentEncrypted) if err != nil { contentLogger.Error("解密失败: " + err.Error()) return `{"code": -1, "msg": "解密失败"}`, err } data["content"] = decryptedContent // 更新缓存 bodyJSONStr := buildJSONResponse(body) UpdateCache("content_"+itemID, bodyJSONStr, 86400) // 解析内容 parser := NewContentParser() data["content"] = parser.Parse(decryptedContent, textMode, imageMode) return buildJSONResponse(UnwrapJSONStrings(body)), nil } contentLogger.Warning("备用接口1获取正文出错!") return `{"code": -1, "msg": "获取失败"}`, fmt.Errorf("所有接口都失败") }