package fqapi import ( "encoding/json" "fanqie/internal/common" "fmt" "strings" "sync" ) var ( multiContentLogger = common.NewLogger("multi_content", "INFO", "") ) // handleDecryptTask 处理解密任务(crypt_status 分支见 decodeContentBody) func handleDecryptTask(itemID string, itemData map[string]interface{}, keyInfo *KeyInfo, textMode, imageMode bool) (string, map[string]interface{}) { if code, ok := itemData["code"].(float64); !ok || code != 0 { return itemID, itemData } if _, ok := itemData["content"].(string); !ok { return itemID, itemData } // 解密(含压缩与明文分支) contentStr, err := decodeContentBody(itemData, keyInfo) if err != nil || contentStr == "" { // 解密失败保留上游原样(ignore_failed 默认跳过失败章节) return itemID, itemData } // 更新内容 bodyCopy := make(map[string]interface{}) for k, v := range itemData { bodyCopy[k] = v } bodyCopy["content"] = contentStr // 缓存 cacheData := buildCacheData(bodyCopy) saveCacheJSON("content_"+itemID, cacheData, 86400) // 解析内容 parser := NewContentParser() bodyCopy["content"] = parser.Parse(contentStr, textMode, imageMode) return itemID, bodyCopy } // MultiContent 批量获取内容 func MultiContent(itemIDs interface{}, bookID string, textMode, imageMode, ignoreFailed interface{}, depth int) (string, error) { // 参数转换(URL 参数优先,未传时回退配置) textModeBool := parseBoolParamDefault(textMode, getContentConfig().RemoveHTMLTags) imageModeBool := parseBoolParamDefault(imageMode, !getContentConfig().RemoveImages) ignoreFailedBool := parseBoolParamDefault(ignoreFailed, true) if depth > 3 { return BuildErrorResponse("获取失败,请重试!"), nil } if bookID == "" { return BuildErrorResponse("需要book_id!"), nil } if itemIDs == nil || itemIDs == "" { return BuildSuccessResponse([]interface{}{}), nil } // 转换itemIDs var itemIDStr string switch v := itemIDs.(type) { case string: itemIDStr = v case []string: itemIDStr = strings.Join(v, ",") default: itemIDStr = fmt.Sprintf("%v", v) } // 从设备密钥池获取设备 pool := GetDeviceKeyPool() item, err := pool.GetNext() if err != nil { return `{"code": -1, "msg": "从设备池获取设备失败"}`, err } header := item.DeviceHeader currentKeyInfo := &KeyInfo{ Key: item.Key, Ver: item.KeyVer, Ts: item.KeyTs, } multiContentLogger.Debug(fmt.Sprintf("使用池中设备 ID=%d (depth: %d)", item.ID, depth)) // 构建参数(form 编码,见 buildSortedQuery 注释) headerWithoutVersion := removeVersionFromHeader(header) params := []string{ "aid=1967", "app_name=novelapp", "channel=0", "device_platform=android", "os_version=0", "version_code=" + ClientVersionCode, "book_id=" + bookID, "item_ids=" + itemIDStr, "novel_text_type=1", "req_type=1", fmt.Sprintf("key_register_ts=%d", currentKeyInfo.Ts), headerWithoutVersion, } sortedParams := buildSortedQuery(params...) url := "https://reading.snssdk.com/reading/reader/batch_full/v/?" + sortedParams resp, err := ContentRequest(url, map[string]interface{}{ "method": "GET", "headers": map[string]string{ "User-Agent": "com.dragon.read", }, }) if err != nil { multiContentLogger.Warning(fmt.Sprintf("批量请求失败,标记设备无效并切换设备 (depth: %d): %v", depth, err)) pool.MarkInvalid(header) pool.RemoveInvalid() return MultiContent(itemIDs, bookID, textModeBool, imageModeBool, ignoreFailedBool, depth+1) } var data map[string]interface{} json.Unmarshal(resp.Content, &data) if code, ok := data["code"].(float64); ok && code == 0 { dataMap, ok := data["data"].(map[string]interface{}) if !ok { multiContentLogger.Warning(fmt.Sprintf("响应data字段无效,切换设备重试 (depth: %d)", depth)) pool.MarkInvalid(header) pool.RemoveInvalid() return MultiContent(itemIDs, bookID, textModeBool, imageModeBool, ignoreFailedBool, depth+1) } // 检查是否有Invalid内容或密钥版本不匹配 needRetry := false for _, itemData := range dataMap { itemMap, ok := itemData.(map[string]interface{}) if !ok { continue } itemCode, _ := itemMap["code"].(float64) if itemCode != 0 && !ignoreFailedBool { multiContentLogger.Warning(fmt.Sprintf("item code异常: %.0f,切换设备重试 (depth: %d)", itemCode, depth)) pool.MarkInvalid(header) pool.RemoveInvalid() return MultiContent(itemIDs, bookID, textModeBool, imageModeBool, ignoreFailedBool, depth+1) } if content, ok := itemMap["content"].(string); ok && content == "Invalid" { // 标记设备无效并移除 multiContentLogger.Warning(fmt.Sprintf("内容返回 Invalid,标记设备无效并切换设备 (depth: %d)", depth)) pool.MarkInvalid(header) pool.RemoveInvalid() needRetry = true break } if keyVersion, ok := itemMap["key_version"].(float64); ok { if currentKeyInfo != nil && int(keyVersion) != currentKeyInfo.Ver { // 更新密钥版本 multiContentLogger.Warning("密钥版本不匹配,重新获取密钥") newKeyInfo, err := GetKey(header) if err == nil { UpdateKeyInPool(header, newKeyInfo) } else { // 获取密钥失败,切换设备 multiContentLogger.Warning(fmt.Sprintf("获取密钥失败,切换设备重试 (depth: %d)", depth)) pool.MarkInvalid(header) pool.RemoveInvalid() } needRetry = true break } } } if needRetry { return MultiContent(itemIDs, bookID, textModeBool, imageModeBool, ignoreFailedBool, depth+1) } // 并发解密 results := make(map[string]map[string]interface{}) var mutex sync.Mutex var wg sync.WaitGroup for itemID, itemData := range dataMap { itemMap, ok := itemData.(map[string]interface{}) if !ok { continue } wg.Add(1) go func(id string, data map[string]interface{}) { defer wg.Done() _, decrypted := handleDecryptTask(id, data, currentKeyInfo, textModeBool, imageModeBool) mutex.Lock() results[id] = decrypted mutex.Unlock() }(itemID, itemMap) } wg.Wait() // 检查结果数量 itemCount := len(strings.Split(itemIDStr, ",")) if len(results) == 1 && itemCount != 1 { multiContentLogger.Warning(fmt.Sprintf("结果数量异常,切换设备重试 (depth: %d)", depth)) pool.MarkInvalid(header) pool.RemoveInvalid() return MultiContent(itemIDs, bookID, textModeBool, imageModeBool, ignoreFailedBool, depth+1) } // 构建响应 response := buildCacheData(results) return buildJSONResponse(UnwrapJSONStrings(response)), nil } // 响应code不为0,切换设备重试 multiContentLogger.Warning(fmt.Sprintf("响应code异常,切换设备重试 (depth: %d)", depth)) pool.MarkInvalid(header) pool.RemoveInvalid() return MultiContent(itemIDs, bookID, textModeBool, imageModeBool, ignoreFailedBool, depth+1) } // HandleMultiContent 处理批量获取内容 func HandleMultiContent(itemIDs interface{}, bookID string, textMode, imageMode, ignoreFailed interface{}) (string, error) { return MultiContent(itemIDs, bookID, textMode, imageMode, ignoreFailed, 0) }