diff --git a/common/xunlei_login.go b/common/xunlei_login.go new file mode 100644 index 0000000..49e5e4d --- /dev/null +++ b/common/xunlei_login.go @@ -0,0 +1,232 @@ +package pan + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "strings" + "time" +) + +// 新增常量定义 +const ( + XLUSER_CLIENT_ID = "XW5SkOhLDjnOZP7J" // 登录 + PAN_CLIENT_ID = "Xqp0kJBXWhwaTpB6" // 获取文件列表 + CLIENT_SECRET = "Og9Vr1L8Ee6bh0olFxFDRg" + CLIENT_VERSION = "1.92.9" // 更新为与xunlei_3项目相同的版本 + PACKAG_ENAME = "pan.xunlei.com" +) + +var SALTS = []string{ + "QG3/GhopO+5+T", + "1Sv94+ANND3lDmmw", + "q2eTxRva8b3B5d", + "m2", + "VIc5CZRBMU71ENfbOh0+RgWIuzLy", + "66M8Wpw6nkBEekOtL6e", + "N0rucK7S8W/vrRkfPto5urIJJS8dVY0S", + "oLAR7pdUVUAp9xcuHWzrU057aUhdCJrt", + "6lxcykBSsfI//GR9", + "r50cz+1I4gbU/fk8", + "tdwzrTc4SNFC4marNGTgf05flC85A", + "qvNVUDFjfsOMqvdi2gB8gCvtaJAIqxXs", +} + +// captchaSign 生成验证码签名 - 完全复制自xunlei_3项目 +func (x *XunleiPanService) captchaSign(clientId string, deviceID string, timestamp string) string { + sign := clientId + CLIENT_VERSION + PACKAG_ENAME + deviceID + timestamp + log.Printf("urldb 签名基础字符串: %s", sign) + for _, salt := range SALTS { // salt = + hash := md5.Sum([]byte(sign + salt)) + sign = hex.EncodeToString(hash[:]) + } + log.Printf("urldb 最终签名: 1.%s", sign) + return fmt.Sprintf("1.%s", sign) +} + +// getTimestamp 获取当前时间戳 +func (x *XunleiPanService) getTimestamp() int64 { + return time.Now().UnixMilli() +} + +// LoginWithCredentials 使用账号密码登录 +func (x *XunleiPanService) LoginWithCredentials(username, password string) (XunleiTokenData, error) { + loginURL := "https://xluser-ssl.xunlei.com/v1/auth/signin" + + // 初始化验证码 - 完全模仿xunlei_3的CaptchaInit方法 + captchaURL := "https://xluser-ssl.xunlei.com/v1/shield/captcha/init" + + // 构造meta参数(完全模仿xunlei_3,只包含phone_number) + meta := map[string]interface{}{ + "phone_number": "+86" + username, + } + + // 构造验证码请求(完全模仿xunlei_3) + captchaBody := map[string]interface{}{ + "client_id": XLUSER_CLIENT_ID, + "action": "POST:/v1/auth/signin", + "device_id": x.deviceId, + "meta": meta, + } + + log.Printf("发送验证码初始化请求: %+v", captchaBody) + resp, err := x.sendCaptchaRequest(captchaURL, captchaBody) + if err != nil { + return XunleiTokenData{}, fmt.Errorf("获取验证码失败: %v", err) + } + + if resp["captcha_token"] == nil { + return XunleiTokenData{}, fmt.Errorf("获取验证码失败: 响应中没有captcha_token") + } + + captchaToken, ok := resp["captcha_token"].(string) + if !ok { + return XunleiTokenData{}, fmt.Errorf("获取验证码失败: captcha_token格式错误") + } + log.Printf("成功获取captcha_token: %s", captchaToken) + + // 构造登录请求数据 + loginData := map[string]interface{}{ + "client_id": XLUSER_CLIENT_ID, + "client_secret": CLIENT_SECRET, + "password": password, + "username": "+86 " + username, + "captcha_token": captchaToken, + } + + // 发送登录请求 + userInfo, err := x.sendCaptchaRequest(loginURL, loginData) + if err != nil { + return XunleiTokenData{}, fmt.Errorf("登录请求失败: %v", err) + } + + // 提取token信息 + accessToken, ok := userInfo["access_token"].(string) + if !ok { + return XunleiTokenData{}, fmt.Errorf("登录响应中没有access_token") + } + + refreshToken, ok := userInfo["refresh_token"].(string) + if !ok { + return XunleiTokenData{}, fmt.Errorf("登录响应中没有refresh_token") + } + + sub, ok := userInfo["sub"].(string) + if !ok { + sub = "" + } + + // 计算过期时间 + expiresIn := int64(3600) // 默认1小时 + if exp, ok := userInfo["expires_in"].(float64); ok { + expiresIn = int64(exp) + } + expiresAt := time.Now().Unix() + expiresIn - 60 // 减去60秒缓冲 + + log.Printf("登录成功,获取到token") + return XunleiTokenData{ + AccessToken: accessToken, + RefreshToken: refreshToken, + ExpiresIn: expiresIn, + ExpiresAt: expiresAt, + Sub: sub, + TokenType: "Bearer", + UserId: sub, + }, nil +} + +// sendCaptchaRequest 发送验证码请求 - 完全复制xunlei_3的sendRequest实现 +func (x *XunleiPanService) sendCaptchaRequest(url string, data map[string]interface{}) (map[string]interface{}, error) { + jsonData, err := json.Marshal(data) + if err != nil { + return nil, err + } + + log.Printf("发送验证码请求URL: %s", url) + log.Printf("发送验证码请求数据: %s", string(jsonData)) + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, err + } + + // 完全复制xunlei_3的请求头设置 + reqHeaders := x.getHeadersForRequest(nil) + // 添加特定的headers + reqHeaders["Content-Type"] = "application/x-www-form-urlencoded" + reqHeaders["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + + for k, v := range reqHeaders { + req.Header.Set(k, v) + } + + // 根据URL确定使用哪个client_id + if strings.Contains(url, "shield/captcha/init") { + // 对于验证码初始化,如果数据中指定了client_id,则使用该client_id + if clientID, ok := data["client_id"].(string); ok { + req.Header.Set("X-Client-Id", clientID) + } else { + // 默认使用PAN_CLIENT_ID用于API相关的验证码 + req.Header.Set("X-Client-Id", PAN_CLIENT_ID) + } + } else if strings.Contains(url, "auth/") { + // 对于认证相关的请求,使用登录相关的client_id + req.Header.Set("X-Client-Id", XLUSER_CLIENT_ID) + } else { + // 对于一般的API请求,使用PAN_CLIENT_ID + req.Header.Set("X-Client-Id", PAN_CLIENT_ID) + } + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + log.Printf("验证码响应状态码: %d", resp.StatusCode) + log.Printf("验证码响应内容: %s", string(body)) + + var result map[string]interface{} + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("JSON 解析失败: %v, raw: %s", err, string(body)) + } + + log.Printf("解析后的响应: %+v", result) + return result, nil +} + +// getHeadersForRequest 获取请求头 +func (x *XunleiPanService) getHeadersForRequest(accessToken *string) map[string]string { + headers := map[string]string{ + "Content-Type": "application/json; charset=utf-8", + } + + // 这里我们简化处理,因为验证码请求不需要这些 + // if x.CaptchaToken != nil { + // headers["User-Agent"] = x.buildCustomUserAgent() + // headers["X-Captcha-Token"] = *x.CaptchaToken + // } else { + headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36" + // } + + // if accessToken != nil { + // headers["Authorization"] = fmt.Sprintf("Bearer %s", *accessToken) + // } + + // if x.DeviceID != "" { + // headers["X-Device-Id"] = x.DeviceID + // } + + return headers +} \ No newline at end of file diff --git a/common/xunlei_pan.bak b/common/xunlei_pan.bak new file mode 100644 index 0000000..629130c --- /dev/null +++ b/common/xunlei_pan.bak @@ -0,0 +1,897 @@ +package pan + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "github.com/ctwj/urldb/db/entity" + "github.com/ctwj/urldb/db/repo" +) + +// CaptchaData 存储在数据库中的验证码令牌数据 +type CaptchaData struct { + CaptchaToken string `json:"captcha_token"` + ExpiresAt int64 `json:"expires_at"` +} + +// XunleiExtraData 所有额外数据的容器 +type XunleiTokenData struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int64 `json:"expires_in"` + ExpiresAt int64 `json:"expires_at"` + Sub string `json:"sub"` + TokenType string `json:"token_type"` + UserId string `json:"user_id"` +} + +type XunleiExtraData struct { + Captcha *CaptchaData + Token *XunleiTokenData +} + +type XunleiPanService struct { + *BasePanService + configMutex sync.RWMutex + clientId string + deviceId string + entity entity.Cks + cksRepo repo.CksRepository + extra XunleiExtraData // 需要保存到数据库的token信息 +} + +// 配置化 API Host +func (x *XunleiPanService) apiHost(apiType string) string { + if apiType == "user" { + return "https://xluser-ssl.xunlei.com" + } + return "https://api-pan.xunlei.com" +} + +func (x *XunleiPanService) setCommonHeader(req *http.Request) { + for k, v := range x.headers { + req.Header.Set(k, v) + } +} + +// NewXunleiPanService 创建迅雷网盘服务 +func NewXunleiPanService(config *PanConfig) *XunleiPanService { + xunleiInstance := &XunleiPanService{ + BasePanService: NewBasePanService(config), + clientId: "Xqp0kJBXWhwaTpB6", + deviceId: "925b7631473a13716b791d7f28289cad", + extra: XunleiExtraData{}, // Initialize extra with zero values + } + xunleiInstance.SetHeaders(map[string]string{ + "Accept": "*/;", + "Accept-Encoding": "deflate", + "Accept-Language": "zh-CN,zh;q=0.9", + "Cache-Control": "no-cache", + "Content-Type": "application/json", + "Origin": "https://pan.xunlei.com", + "Pragma": "no-cache", + "Priority": "u=1,i", + "Referer": "https://pan.xunlei.com/", + "sec-ch-ua": `"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"`, + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": `"Windows"`, + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36", + "Authorization": "", + "x-captcha-token": "", + "x-client-id": xunleiInstance.clientId, + "x-device-id": xunleiInstance.deviceId, + }) + + xunleiInstance.UpdateConfig(config) + return xunleiInstance +} + +// SetCKSRepository 设置 CksRepository 和 entity +func (x *XunleiPanService) SetCKSRepository(cksRepo repo.CksRepository, entity entity.Cks) { + x.cksRepo = cksRepo + x.entity = entity + var extra XunleiExtraData + if err := json.Unmarshal([]byte(x.entity.Extra), &extra); err != nil { + log.Printf("解析 extra 数据失败: %v,使用空数据", err) + } + x.extra = extra +} + +// GetXunleiInstance 获取迅雷网盘服务单例实例 +func GetXunleiInstance() *XunleiPanService { + return NewXunleiPanService(nil) +} + +func (x *XunleiPanService) GetAccessTokenByRefreshToken(refreshToken string) (XunleiTokenData, error) { + // 构造请求体 + body := map[string]interface{}{ + "client_id": x.clientId, + "grant_type": "refresh_token", + "refresh_token": refreshToken, + } + + // 过滤 headers(移除 Authorization 和 x-captcha-token) + filteredHeaders := make(map[string]string) + for k, v := range x.headers { + if k != "Authorization" && k != "x-captcha-token" { + filteredHeaders[k] = v + } + } + + // 调用 API 获取新的 token + resp, err := x.requestXunleiApi("https://xluser-ssl.xunlei.com/v1/auth/token", "POST", body, nil, filteredHeaders) + if err != nil { + return XunleiTokenData{}, fmt.Errorf("获取 access_token 请求失败: %v", err) + } + + // 正确做法:用 exists 判断 + if _, exists := resp["access_token"]; exists { + // 会输出,即使值为 nil + } else { + return XunleiTokenData{}, fmt.Errorf("获取 access_token 请求失败: %v 不存在", "access_token") + } + + // 计算过期时间(当前时间 + expires_in - 60 秒缓冲) + currentTime := time.Now().Unix() + expiresAt := currentTime + int64(resp["expires_in"].(float64)) - 60 + resp["expires_at"] = expiresAt + jsonBytes, _ := json.Marshal(resp) + + var result XunleiTokenData + json.Unmarshal(jsonBytes, &result) + return result, nil +} + +// getAccessToken 获取 Access Token(内部包含缓存判断、刷新、保存)- 匹配 PHP 版本 +func (x *XunleiPanService) getAccessToken() (string, error) { + // 检查 Access Token 是否有效 + currentTime := time.Now().Unix() + if x.extra.Token != nil && x.extra.Token.AccessToken != "" && x.extra.Token.ExpiresAt > currentTime { + return x.extra.Token.AccessToken, nil + } + newData, err := x.GetAccessTokenByRefreshToken(x.extra.Token.RefreshToken) + if err != nil { + return "", fmt.Errorf("获取 access_token 失败: %v", err) + } + + x.extra.Token.AccessToken = newData.AccessToken + x.extra.Token.ExpiresAt = newData.ExpiresAt + + // 保存到数据库 + extraBytes, err := json.Marshal(x.extra) + if err != nil { + return "", fmt.Errorf("序列化 extra 数据失败: %v", err) + } + x.entity.Extra = string(extraBytes) + if err := x.cksRepo.UpdateWithAllFields(&x.entity); err != nil { + return "", fmt.Errorf("保存 access_token 到数据库失败: %v", err) + } + return newData.AccessToken, nil +} + +// getCaptchaToken 获取 captcha_token - 匹配 PHP 版本 +func (x *XunleiPanService) getCaptchaToken() (string, error) { + // 检查 Captcha Token 是否有效 + currentTime := time.Now().Unix() + if x.extra.Captcha != nil && x.extra.Captcha.CaptchaToken != "" && x.extra.Captcha.ExpiresAt > currentTime { + return x.extra.Captcha.CaptchaToken, nil + } + + // 构造请求体 + body := map[string]interface{}{ + "client_id": x.clientId, + "action": "get:/drive/v1/share", + "device_id": x.deviceId, + "meta": map[string]interface{}{ + "username": "", + "phone_number": "", + "email": "", + "package_name": "pan.xunlei.com", + "client_version": "1.45.0", + "captcha_sign": "1.fe2108ad808a74c9ac0243309242726c", + "timestamp": "1645241033384", + "user_id": "0", + }, + } + + captchaHeaders := map[string]string{ + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + } + + // 调用 API 获取 captcha_token + resp, err := x.requestXunleiApi("https://xluser-ssl.xunlei.com/v1/shield/captcha/init", "POST", body, nil, captchaHeaders) + if err != nil { + return "", fmt.Errorf("获取 captcha_token 请求失败: %v", err) + } + + if resp["captcha_token"] != nil && resp["captcha_token"] != "" { + // + } else { + return "", fmt.Errorf("获取 captcha_token 失败: %v", resp) + } + + // 计算过期时间(当前时间 + expires_in - 10 秒缓冲) + expiresAt := currentTime + int64(resp["expires_in"].(float64)) - 10 + + // 更新 extra 数据 + if x.extra.Captcha == nil { + x.extra.Captcha = &CaptchaData{} + } + x.extra.Captcha.CaptchaToken = resp["captcha_token"].(string) + x.extra.Captcha.ExpiresAt = expiresAt + + // 保存到数据库 + extraBytes, err := json.Marshal(x.extra) + if err != nil { + return "", fmt.Errorf("序列化 extra 数据失败: %v", err) + } + x.entity.Extra = string(extraBytes) + if err := x.cksRepo.UpdateWithAllFields(&x.entity); err != nil { + return "", fmt.Errorf("保存 captcha_token 到数据库失败: %v", err) + } + + return resp["captcha_token"].(string), nil +} + +// requestXunleiApi 迅雷 API 通用请求方法 - 使用 BasePanService 方法 +func (x *XunleiPanService) requestXunleiApi(url string, method string, data map[string]interface{}, queryParams map[string]string, headers map[string]string) (map[string]interface{}, error) { + var respData []byte + var err error + + // 先更新当前请求的 headers + originalHeaders := make(map[string]string) + for k, v := range x.headers { + originalHeaders[k] = v + } + + // 临时设置请求的 headers + for k, v := range headers { + x.SetHeader(k, v) + } + defer func() { + // 恢复原始 headers + for k, v := range originalHeaders { + x.SetHeader(k, v) + } + }() + + // 根据方法调用相应的 BasePanService 方法 + if method == "GET" { + respData, err = x.HTTPGet(url, queryParams) + } else if method == "POST" { + respData, err = x.HTTPPost(url, data, queryParams) + } else { + return nil, fmt.Errorf("不支持的HTTP方法: %s", method) + } + + if err != nil { + return nil, err + } + + var result map[string]interface{} + if err := json.Unmarshal(respData, &result); err != nil { + return nil, fmt.Errorf("JSON 解析失败: %v, raw: %s", err, string(respData)) + } + + return result, nil +} + +func (x *XunleiPanService) UpdateConfig(config *PanConfig) { + if config == nil { + return + } + x.configMutex.Lock() + defer x.configMutex.Unlock() + x.config = config + if config.Cookie != "" { + x.SetHeader("Cookie", config.Cookie) + } +} + +// GetServiceType 获取服务类型 +func (x *XunleiPanService) GetServiceType() ServiceType { + return Xunlei +} + +func extractCode(url string) string { + // 查找 pwd= 的位置 + if pwdIndex := strings.Index(url, "pwd="); pwdIndex != -1 { + code := url[pwdIndex+4:] + // 移除 # 及后面的内容(如果存在) + if hashIndex := strings.Index(code, "#"); hashIndex != -1 { + code = code[:hashIndex] + } + return code + } + return "" +} + +// Transfer 转存分享链接 - 实现 PanService 接口,匹配 XunleiPan.php 的逻辑 +func (x *XunleiPanService) Transfer(shareID string) (*TransferResult, error) { + // 读取配置(线程安全) + x.configMutex.RLock() + config := x.config + x.configMutex.RUnlock() + + log.Printf("开始处理迅雷分享: %s", shareID) + + // 1️⃣ 获取 AccessToken 和 CaptchaToken + accessToken, err := x.getAccessToken() + if err != nil { + return ErrorResult(fmt.Sprintf("获取accessToken失败: %v", err)), nil + } + + captchaToken, err := x.getCaptchaToken() + if err != nil { + return ErrorResult(fmt.Sprintf("获取captchaToken失败: %v", err)), nil + } + + // 转存模式:实现完整的转存流程 + thisCode := extractCode(config.URL) + + // 获取分享详情 + shareDetail, err := x.getShare(shareID, thisCode, accessToken, captchaToken) + if err != nil { + return ErrorResult(fmt.Sprintf("获取分享详情失败: %v", err)), nil + } + if shareDetail["share_status"].(string) != "OK" { + return ErrorResult(fmt.Sprintf("获取分享详情失败: %v", "分享状态异常")), nil + } + if shareDetail["file_num"].(string) == "0" { + return ErrorResult(fmt.Sprintf("获取分享详情失败: %v", "文件列表为空")), nil + } + + parent_id := "" // 默认存储路径 + + // 检查是否为检验模式 + if config.IsType == 1 { + // 检验模式:直接获取分享信息 + urls := map[string]interface{}{ + "title": shareDetail["title"], + "share_url": config.URL, + "stoken": "", + } + return SuccessResult("检验成功", urls), nil + } + + // files := shareDetail["files"].([]interface{}) + // fileIDs := make([]string, 0) + // for _, file := range files { + // fileMap := file.(map[string]interface{}) + // if fid, ok := fileMap["id"].(string); ok { + // fileIDs = append(fileIDs, fid) + // } + // } + + // 处理广告过滤(这里简化处理) + // TODO: 添加广告文件过滤逻辑 + + // 转存资源 + restoreResult, err := x.getRestore(shareID, shareDetail, accessToken, captchaToken, parent_id) + if err != nil { + return ErrorResult(fmt.Sprintf("转存失败: %v", err)), nil + } + + // 获取转存任务信息 + taskID := restoreResult["restore_task_id"].(string) + + // 等待转存完成 + taskResp, err := x.waitForTask(taskID, accessToken, captchaToken) + if err != nil { + return ErrorResult(fmt.Sprintf("等待转存完成失败: %v", err)), nil + } + + // 获取任务结果以获取文件ID + existingFileIds := make([]string, 0) + if params, ok2 := taskResp["params"].(map[string]interface{}); ok2 { + if traceIds, ok3 := params["trace_file_ids"].(string); ok3 { + traceData := make(map[string]interface{}) + json.Unmarshal([]byte(traceIds), &traceData) + for _, fid := range traceData { + existingFileIds = append(existingFileIds, fid.(string)) + } + } + } + + // 创建分享链接 + expirationDays := "-1" + if config.ExpiredType == 2 { + expirationDays = "2" + } + + // 根据share_id获取到分享链接 + shareResult, err := x.getSharePassword(existingFileIds, accessToken, captchaToken, expirationDays) + if err != nil { + return ErrorResult(fmt.Sprintf("创建分享链接失败: %v", err)), nil + } + + var fid string + if len(existingFileIds) > 1 { + fid = strings.Join(existingFileIds, ",") + } else { + fid = existingFileIds[0] + } + + result := map[string]interface{}{ + "title": "", + "shareUrl": shareResult["share_url"].(string) + "?pwd=" + shareResult["pass_code"].(string), + "code": shareResult["pass_code"].(string), + "fid": fid, + } + + return SuccessResult("转存成功", result), nil +} + +// waitForTask 等待任务完成 - 使用 HTTPGet 方法 +func (x *XunleiPanService) waitForTask(taskID string, accessToken, captchaToken string) (map[string]interface{}, error) { + maxRetries := 50 + retryDelay := 2 * time.Second + + for retryIndex := 0; retryIndex < maxRetries; retryIndex++ { + result, err := x.getTaskStatus(taskID, retryIndex, accessToken, captchaToken) + if err != nil { + return nil, err + } + + if int64(result["progress"].(float64)) == 100 { // 任务完成 + return result, nil + } + + time.Sleep(retryDelay) + } + + return nil, fmt.Errorf("任务超时") +} + +// getTaskStatus 获取任务状态 - 使用 HTTPGet 方法 +func (x *XunleiPanService) getTaskStatus(taskID string, retryIndex int, accessToken, captchaToken string) (map[string]interface{}, error) { + apiURL := x.apiHost("") + "/drive/v1/tasks/" + taskID + queryParams := map[string]string{} + + // 设置 request 所需的 headers + headers := map[string]string{ + "Authorization": "Bearer " + accessToken, + "x-captcha-token": captchaToken, + } + + resp, err := x.requestXunleiApi(apiURL, "GET", nil, queryParams, headers) + if err != nil { + return nil, err + } + return resp, nil +} + +// GetUserInfoByEntity 根据 entity.Cks 获取用户信息(待实现) +func (x *XunleiPanService) GetUserInfoByEntity(cks entity.Cks) (*UserInfo, error) { + return nil, nil +} + +// getShare 获取分享详情 - 匹配 PHP 版本 +func (x *XunleiPanService) getShare(shareID, passCode, accessToken, captchaToken string) (map[string]interface{}, error) { + // 设置 headers + headers := make(map[string]string) + for k, v := range x.headers { + headers[k] = v + } + headers["Authorization"] = "Bearer " + accessToken + headers["x-captcha-token"] = captchaToken + + queryParams := map[string]string{ + "share_id": shareID, + "pass_code": passCode, + "limit": "100", + "pass_code_token": "", + "page_token": "", + "thumbnail_size": "SIZE_SMALL", + } + + return x.requestXunleiApi("https://api-pan.xunlei.com/drive/v1/share", "GET", nil, queryParams, headers) +} + +// getRestore 转存到网盘 - 匹配 PHP 版本 +func (x *XunleiPanService) getRestore(shareID string, infoData map[string]interface{}, accessToken, captchaToken, parentID string) (map[string]interface{}, error) { + ids := make([]string, 0) + if files, ok := infoData["files"].([]interface{}); ok { + for _, file := range files { + if fileMap, ok2 := file.(map[string]interface{}); ok2 { + if id, ok3 := fileMap["id"].(string); ok3 { + ids = append(ids, id) + } + } + } + } + + passCodeToken := "" + if token, ok := infoData["pass_code_token"]; ok { + if tokenStr, ok2 := token.(string); ok2 { + passCodeToken = tokenStr + } + } + + data := map[string]interface{}{ + "parent_id": parentID, + "share_id": shareID, + "pass_code_token": passCodeToken, + "ancestor_ids": []string{}, + "specify_parent_id": true, + "file_ids": ids, + } + + headers := make(map[string]string) + for k, v := range x.headers { + headers[k] = v + } + headers["Authorization"] = "Bearer " + accessToken + headers["x-captcha-token"] = captchaToken + + return x.requestXunleiApi("https://api-pan.xunlei.com/drive/v1/share/restore", "POST", data, nil, headers) +} + +// getTasks 获取转存任务状态 - 匹配 PHP 版本 +func (x *XunleiPanService) getTasks(taskID, accessToken, captchaToken string) (map[string]interface{}, error) { + headers := make(map[string]string) + for k, v := range x.headers { + headers[k] = v + } + headers["Authorization"] = "Bearer " + accessToken + headers["x-captcha-token"] = captchaToken + + return x.requestXunleiApi("https://api-pan.xunlei.com/drive/v1/tasks/"+taskID, "GET", nil, nil, headers) +} + +// getSharePassword 创建分享链接 - 匹配 PHP 版本 +func (x *XunleiPanService) getSharePassword(fileIDs []string, accessToken, captchaToken, expirationDays string) (map[string]interface{}, error) { + data := map[string]interface{}{ + "file_ids": fileIDs, + "share_to": "copy", + "params": map[string]interface{}{ + "subscribe_push": "false", + "WithPassCodeInLink": "true", + }, + "title": "云盘资源分享", + "restore_limit": "-1", + "expiration_days": expirationDays, + } + + headers := make(map[string]string) + for k, v := range x.headers { + headers[k] = v + } + headers["Authorization"] = "Bearer " + accessToken + headers["x-captcha-token"] = captchaToken + + return x.requestXunleiApi("https://api-pan.xunlei.com/drive/v1/share", "POST", data, nil, headers) +} + +// getShareInfo 获取分享信息(用于检验模式) +func (x *XunleiPanService) getShareInfo(shareID string) (*XLShareInfo, error) { + // 使用现有的 GetShareFolder 方法获取分享信息 + shareDetail, err := x.GetShareFolder(shareID, "", "") + if err != nil { + return nil, err + } + + // 构造分享信息 + shareInfo := &XLShareInfo{ + ShareID: shareID, + Title: fmt.Sprintf("迅雷分享_%s", shareID), + Files: make([]XLFileInfo, 0), + } + + // 处理文件信息 + for _, file := range shareDetail.Data.Files { + shareInfo.Files = append(shareInfo.Files, XLFileInfo{ + FileID: file.FileID, + Name: file.Name, + }) + } + + return shareInfo, nil +} + +// GetFiles 获取文件列表 - 匹配 PHP 版本接口调用 +func (x *XunleiPanService) GetFiles(pdirFid string) (*TransferResult, error) { + log.Printf("开始获取迅雷网盘文件列表,目录ID: %s", pdirFid) + + // 获取 tokens + accessToken, err := x.getAccessToken() + if err != nil { + return ErrorResult(fmt.Sprintf("获取accessToken失败: %v", err)), nil + } + + captchaToken, err := x.getCaptchaToken() + if err != nil { + return ErrorResult(fmt.Sprintf("获取captchaToken失败: %v", err)), nil + } + + // 设置 headers + headers := make(map[string]string) + for k, v := range x.headers { + headers[k] = v + } + headers["Authorization"] = "Bearer " + accessToken + headers["x-captcha-token"] = captchaToken + + filters := map[string]interface{}{ + "phase": map[string]interface{}{ + "eq": "PHASE_TYPE_COMPLETE", + }, + "trashed": map[string]interface{}{ + "eq": false, + }, + } + + filtersStr, _ := json.Marshal(filters) + queryParams := map[string]string{ + "parent_id": pdirFid, + "filters": string(filtersStr), + "with_audit": "true", + "thumbnail_size": "SIZE_SMALL", + "limit": "50", + } + + result, err := x.requestXunleiApi("https://api-pan.xunlei.com/drive/v1/files", "GET", nil, queryParams, headers) + if err != nil { + return ErrorResult(fmt.Sprintf("获取文件列表失败: %v", err)), nil + } + + if code, ok := result["code"].(float64); ok && code != 0 { + return ErrorResult("获取文件列表失败"), nil + } + + if data, ok := result["data"].(map[string]interface{}); ok { + if files, ok2 := data["files"]; ok2 { + return SuccessResult("获取成功", files), nil + } + } + + return SuccessResult("获取成功", []interface{}{}), nil +} + +// DeleteFiles 删除文件 - 实现 PanService 接口 +func (x *XunleiPanService) DeleteFiles(fileList []string) (*TransferResult, error) { + log.Printf("开始删除迅雷网盘文件,文件数量: %d", len(fileList)) + + // 使用现有的 ShareBatchDelete 方法删除分享 + result, err := x.ShareBatchDelete(fileList) + if err != nil { + return ErrorResult(fmt.Sprintf("删除文件失败: %v", err)), nil + } + + if result.Code != 0 { + return ErrorResult(fmt.Sprintf("删除文件失败: %s", result.Msg)), nil + } + + return SuccessResult("删除成功", nil), nil +} + +// GetUserInfo 获取用户信息 - 实现 PanService 接口,cookie 参数为 refresh_token,先获取 access_token 再访问 API +func (x *XunleiPanService) GetUserInfo(cookie *string) (*UserInfo, error) { + userInfo := &UserInfo{} + accessToken, err := x.getAccessToken() + if err != nil { + return nil, err + } + + captchaToken, err := x.getCaptchaToken() + if err != nil { + return nil, err + } + + headers := make(map[string]string) + for k, v := range x.headers { + headers[k] = v + } + headers["Authorization"] = "Bearer " + accessToken + headers["x-captcha-token"] = captchaToken + + resp, err := x.requestXunleiApi("https://api-pan.xunlei.com/drive/v1/about", "GET", nil, nil, headers) + if err != nil { + return nil, fmt.Errorf("获取用户信息失败: %v", err) + } + limit := resp["quota"].(map[string]interface{})["limit"].(string) + limitInt, _ := strconv.ParseInt(limit, 10, 64) + used := resp["quota"].(map[string]interface{})["usage"].(string) + usedInt, _ := strconv.ParseInt(used, 10, 64) + userInfo.TotalSpace = limitInt + userInfo.UsedSpace = usedInt + + // 获取用户信息 + respData, err := x.requestXunleiApi(x.apiHost("user")+"/v1/user/me", "GET", nil, nil, headers) + if err != nil { + return nil, fmt.Errorf("获取用户信息失败: %v", err) + } + + vipInfo := respData["vip_info"].([]interface{}) + isVip := vipInfo[0].(map[string]interface{})["is_vip"].(string) != "0" + + userInfo.Username = respData["name"].(string) + userInfo.ServiceType = x.GetServiceType().String() + userInfo.VIPStatus = isVip + return userInfo, nil +} + +// GetShareList 严格对齐 GET + query(使用 BasePanService) +func (x *XunleiPanService) GetShareList(pageToken string) (*XLShareListResp, error) { + api := x.apiHost("") + "/drive/v1/share/list" + queryParams := map[string]string{ + "limit": "100", + "thumbnail_size": "SIZE_SMALL", + } + if pageToken != "" { + queryParams["page_token"] = pageToken + } + + respData, err := x.HTTPGet(api, queryParams) + if err != nil { + return nil, fmt.Errorf("获取分享列表失败: %v", err) + } + + var data XLShareListResp + if err := json.Unmarshal(respData, &data); err != nil { + return nil, fmt.Errorf("解析分享列表失败: %v", err) + } + return &data, nil +} + +// FileBatchShare 创建分享(使用 BasePanService) +func (x *XunleiPanService) FileBatchShare(ids []string, needPassword bool, expirationDays int) (*XLBatchShareResp, error) { + apiURL := x.apiHost("") + "/drive/v1/share/batch" + body := map[string]interface{}{ + "file_ids": ids, + "need_password": needPassword, + "expiration_days": expirationDays, + } + + respData, err := x.HTTPPost(apiURL, body, nil) + if err != nil { + return nil, fmt.Errorf("创建分享失败: %v", err) + } + + var data XLBatchShareResp + if err := json.Unmarshal(respData, &data); err != nil { + return nil, fmt.Errorf("解析分享响应失败: %v", err) + } + return &data, nil +} + +// ShareBatchDelete 取消分享(使用 BasePanService) +func (x *XunleiPanService) ShareBatchDelete(ids []string) (*XLCommonResp, error) { + apiURL := x.apiHost("") + "/drive/v1/share/batch/delete" + body := map[string]interface{}{ + "share_ids": ids, + } + + respData, err := x.HTTPPost(apiURL, body, nil) + if err != nil { + return nil, fmt.Errorf("删除分享失败: %v", err) + } + + var data XLCommonResp + if err := json.Unmarshal(respData, &data); err != nil { + return nil, fmt.Errorf("解析删除响应失败: %v", err) + } + return &data, nil +} + +// GetShareFolder 获取分享内容(使用 BasePanService) +func (x *XunleiPanService) GetShareFolder(shareID, passCodeToken, parentID string) (*XLShareFolderResp, error) { + apiURL := x.apiHost("") + "/drive/v1/share/detail" + body := map[string]interface{}{ + "share_id": shareID, + "pass_code_token": passCodeToken, + "parent_id": parentID, + "limit": 100, + "thumbnail_size": "SIZE_LARGE", + "order": "6", + } + + respData, err := x.HTTPPost(apiURL, body, nil) + if err != nil { + return nil, fmt.Errorf("获取分享文件夹失败: %v", err) + } + + var data XLShareFolderResp + if err := json.Unmarshal(respData, &data); err != nil { + return nil, fmt.Errorf("解析分享文件夹失败: %v", err) + } + return &data, nil +} + +// Restore 转存(使用 BasePanService) +func (x *XunleiPanService) Restore(shareID, passCodeToken string, fileIDs []string) (*XLRestoreResp, error) { + apiURL := x.apiHost("") + "/drive/v1/share/restore" + body := map[string]interface{}{ + "share_id": shareID, + "pass_code_token": passCodeToken, + "file_ids": fileIDs, + "folder_type": "NORMAL", + "specify_parent_id": true, + "parent_id": "", + } + + respData, err := x.HTTPPost(apiURL, body, nil) + if err != nil { + return nil, fmt.Errorf("转存失败: %v", err) + } + + var data XLRestoreResp + if err := json.Unmarshal(respData, &data); err != nil { + return nil, fmt.Errorf("解析转存响应失败: %v", err) + } + return &data, nil +} + +// 结构体完全对齐 xunleix +type XLShareListResp struct { + Data struct { + List []struct { + ShareID string `json:"share_id"` + Title string `json:"title"` + } `json:"list"` + } `json:"data"` + Code int `json:"code"` + Msg string `json:"msg"` +} + +type XLBatchShareResp struct { + Data struct { + ShareURL string `json:"share_url"` + } `json:"data"` + Code int `json:"code"` + Msg string `json:"msg"` +} + +type XLCommonResp struct { + Code int `json:"code"` + Msg string `json:"msg"` +} + +type XLShareFolderResp struct { + Data struct { + Files []struct { + FileID string `json:"file_id"` + Name string `json:"name"` + } `json:"files"` + } `json:"data"` + Code int `json:"code"` + Msg string `json:"msg"` +} + +type XLRestoreResp struct { + Data struct { + TaskID string `json:"task_id"` + } `json:"data"` + Code int `json:"code"` + Msg string `json:"msg"` +} + +// 新增辅助结构体 +type XLShareInfo struct { + ShareID string `json:"share_id"` + Title string `json:"title"` + Files []XLFileInfo `json:"files"` +} + +type XLFileInfo struct { + FileID string `json:"file_id"` + Name string `json:"name"` +} + +type XLTaskResult struct { + Status int `json:"status"` + TaskID string `json:"task_id"` + Data struct { + ShareID string `json:"share_id"` + } `json:"data"` +} diff --git a/common/xunlei_pan.go b/common/xunlei_pan.go index d889dbf..a45c67d 100644 --- a/common/xunlei_pan.go +++ b/common/xunlei_pan.go @@ -1,12 +1,8 @@ package pan import ( - "bytes" - "crypto/md5" - "encoding/hex" "encoding/json" "fmt" - "io/ioutil" "log" "net/http" "strconv" @@ -18,30 +14,6 @@ import ( "github.com/ctwj/urldb/db/repo" ) -// 新增常量定义 -const ( - XLUSER_CLIENT_ID = "XW5SkOhLDjnOZP7J" // 登录 - PAN_CLIENT_ID = "Xqp0kJBXWhwaTpB6" // 获取文件列表 - CLIENT_SECRET = "Og9Vr1L8Ee6bh0olFxFDRg" - CLIENT_VERSION = "1.92.9" // 更新为与xunlei_3项目相同的版本 - PACKAG_ENAME = "pan.xunlei.com" -) - -var SALTS = []string{ - "QG3/GhopO+5+T", - "1Sv94+ANND3lDmmw", - "q2eTxRva8b3B5d", - "m2", - "VIc5CZRBMU71ENfbOh0+RgWIuzLy", - "66M8Wpw6nkBEekOtL6e", - "N0rucK7S8W/vrRkfPto5urIJJS8dVY0S", - "oLAR7pdUVUAp9xcuHWzrU057aUhdCJrt", - "6lxcykBSsfI//GR9", - "r50cz+1I4gbU/fk8", - "tdwzrTc4SNFC4marNGTgf05flC85A", - "qvNVUDFjfsOMqvdi2gB8gCvtaJAIqxXs", -} - // CaptchaData 存储在数据库中的验证码令牌数据 type CaptchaData struct { CaptchaToken string `json:"captcha_token"` @@ -94,8 +66,8 @@ func NewXunleiPanService(config *PanConfig) *XunleiPanService { xunleiInstance := &XunleiPanService{ BasePanService: NewBasePanService(config), clientId: "Xqp0kJBXWhwaTpB6", - deviceId: "a0a4beea8f0db68a5b7ae95ce7dab335", // 使用与xunlei_3相同的设备ID - extra: XunleiExtraData{}, // Initialize extra with zero values + deviceId: "925b7631473a13716b791d7f28289cad", + extra: XunleiExtraData{}, // Initialize extra with zero values } xunleiInstance.SetHeaders(map[string]string{ "Accept": "*/;", @@ -190,6 +162,21 @@ func (x *XunleiPanService) GetAccessTokenByRefreshToken(refreshToken string) (Xu return result, nil } +// reloginWithCredentials 使用账号密码重新登录 +func (x *XunleiPanService) reloginWithCredentials() (XunleiTokenData, error) { + if x.extra.Credentials == nil { + return XunleiTokenData{}, fmt.Errorf("无账号密码信息") + } + + tokenData, err := x.LoginWithCredentials(x.extra.Credentials.Username, x.extra.Credentials.Password) + if err != nil { + return XunleiTokenData{}, fmt.Errorf("账号密码登录失败: %v", err) + } + + log.Printf("账号 %s 重新登录成功", x.extra.Credentials.Username) + return tokenData, nil +} + // getAccessToken 获取 Access Token(内部包含缓存判断、刷新、重新登录、保存) func (x *XunleiPanService) getAccessToken() (string, error) { // 检查 Access Token 是否有效 @@ -249,124 +236,6 @@ func (x *XunleiPanService) getAccessToken() (string, error) { return newData.AccessToken, nil } -// reloginWithCredentials 使用账号密码重新登录 -func (x *XunleiPanService) reloginWithCredentials() (XunleiTokenData, error) { - if x.extra.Credentials == nil { - return XunleiTokenData{}, fmt.Errorf("无账号密码信息") - } - - tokenData, err := x.LoginWithCredentials(x.extra.Credentials.Username, x.extra.Credentials.Password) - if err != nil { - return XunleiTokenData{}, fmt.Errorf("账号密码登录失败: %v", err) - } - - log.Printf("账号 %s 重新登录成功", x.extra.Credentials.Username) - return tokenData, nil -} - -// getTimestamp 获取当前时间戳 -func (x *XunleiPanService) getTimestamp() int64 { - return time.Now().UnixMilli() -} - -// captchaSign 生成验证码签名 - 完全复制自xunlei_3项目 -func (x *XunleiPanService) captchaSign(clientId string, deviceID string, timestamp string) string { - sign := clientId + CLIENT_VERSION + PACKAG_ENAME + deviceID + timestamp - log.Printf("urldb 签名基础字符串: %s", sign) - for _, salt := range SALTS { // salt = - hash := md5.Sum([]byte(sign + salt)) - sign = hex.EncodeToString(hash[:]) - } - log.Printf("urldb 最终签名: 1.%s", sign) - return fmt.Sprintf("1.%s", sign) -} - -// LoginWithCredentials 使用账号密码登录 -func (x *XunleiPanService) LoginWithCredentials(username, password string) (XunleiTokenData, error) { - loginURL := "https://xluser-ssl.xunlei.com/v1/auth/signin" - - // 初始化验证码 - 完全模仿xunlei_3的CaptchaInit方法 - captchaURL := "https://xluser-ssl.xunlei.com/v1/shield/captcha/init" - - // 构造meta参数(完全模仿xunlei_3,只包含phone_number) - meta := map[string]interface{}{ - "phone_number": "+86" + username, - } - - // 构造验证码请求(完全模仿xunlei_3) - captchaBody := map[string]interface{}{ - "client_id": XLUSER_CLIENT_ID, - "action": "POST:/v1/auth/signin", - "device_id": x.deviceId, - "meta": meta, - } - - log.Printf("发送验证码初始化请求: %+v", captchaBody) - resp, err := x.sendCaptchaRequest(captchaURL, captchaBody) - if err != nil { - return XunleiTokenData{}, fmt.Errorf("获取验证码失败: %v", err) - } - - if resp["captcha_token"] == nil { - return XunleiTokenData{}, fmt.Errorf("获取验证码失败: 响应中没有captcha_token") - } - - captchaToken, ok := resp["captcha_token"].(string) - if !ok { - return XunleiTokenData{}, fmt.Errorf("获取验证码失败: captcha_token格式错误") - } - log.Printf("成功获取captcha_token: %s", captchaToken) - - // 构造登录请求数据 - loginData := map[string]interface{}{ - "client_id": XLUSER_CLIENT_ID, - "client_secret": CLIENT_SECRET, - "password": password, - "username": "+86 " + username, - "captcha_token": captchaToken, - } - - // 发送登录请求 - userInfo, err := x.sendCaptchaRequest(loginURL, loginData) - if err != nil { - return XunleiTokenData{}, fmt.Errorf("登录请求失败: %v", err) - } - - // 提取token信息 - accessToken, ok := userInfo["access_token"].(string) - if !ok { - return XunleiTokenData{}, fmt.Errorf("登录响应中没有access_token") - } - - refreshToken, ok := userInfo["refresh_token"].(string) - if !ok { - return XunleiTokenData{}, fmt.Errorf("登录响应中没有refresh_token") - } - - sub, ok := userInfo["sub"].(string) - if !ok { - sub = "" - } - - // 计算过期时间 - expiresIn := int64(3600) // 默认1小时 - if exp, ok := userInfo["expires_in"].(float64); ok { - expiresIn = int64(exp) - } - expiresAt := time.Now().Unix() + expiresIn - 60 // 减去60秒缓冲 - - log.Printf("登录成功,获取到token") - return XunleiTokenData{ - AccessToken: accessToken, - RefreshToken: refreshToken, - ExpiresIn: expiresIn, - ExpiresAt: expiresAt, - Sub: sub, - TokenType: "Bearer", - UserId: sub, - }, nil -} - // getCaptchaToken 获取 captcha_token - 匹配 PHP 版本 func (x *XunleiPanService) getCaptchaToken() (string, error) { // 检查 Captcha Token 是否有效 @@ -375,10 +244,6 @@ func (x *XunleiPanService) getCaptchaToken() (string, error) { return x.extra.Captcha.CaptchaToken, nil } - // 生成动态时间戳和签名 - timestamp := strconv.FormatInt(x.getTimestamp(), 10) - newCaptchaSign := x.captchaSign(x.clientId, x.deviceId, timestamp) - // 构造请求体 body := map[string]interface{}{ "client_id": x.clientId, @@ -389,9 +254,9 @@ func (x *XunleiPanService) getCaptchaToken() (string, error) { "phone_number": "", "email": "", "package_name": "pan.xunlei.com", - "client_version": CLIENT_VERSION, - "captcha_sign": newCaptchaSign, - "timestamp": timestamp, + "client_version": "1.45.0", + "captcha_sign": "1.fe2108ad808a74c9ac0243309242726c", + "timestamp": "1645241033384", "user_id": "0", }, } @@ -441,6 +306,12 @@ func (x *XunleiPanService) requestXunleiApi(url string, method string, data map[ var respData []byte var err error + // 检查是否是验证码初始化请求 + if strings.Contains(url, "shield/captcha/init") { + // 对于验证码初始化,直接发送HTTP请求,不使用BasePanService,使用sendCaptchaRequest + return x.sendCaptchaRequest(url, data) + } + // 先更新当前请求的 headers originalHeaders := make(map[string]string) for k, v := range x.headers { @@ -458,12 +329,6 @@ func (x *XunleiPanService) requestXunleiApi(url string, method string, data map[ } }() - // 检查是否是验证码初始化请求 - if strings.Contains(url, "shield/captcha/init") { - // 对于验证码初始化,直接发送HTTP请求,不使用BasePanService - return x.sendCaptchaRequest(url, data) - } - // 根据方法调用相应的 BasePanService 方法 if method == "GET" { respData, err = x.HTTPGet(url, queryParams) @@ -485,85 +350,6 @@ func (x *XunleiPanService) requestXunleiApi(url string, method string, data map[ return result, nil } -// sendCaptchaRequest 发送验证码请求 - 完全复制xunlei_3的sendRequest实现 -func (x *XunleiPanService) sendCaptchaRequest(url string, data map[string]interface{}) (map[string]interface{}, error) { - jsonData, err := json.Marshal(data) - if err != nil { - return nil, err - } - - log.Printf("发送验证码请求URL: %s", url) - log.Printf("发送验证码请求数据: %s", string(jsonData)) - - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) - if err != nil { - return nil, err - } - - // 完全复制xunlei_3的请求头设置 - reqHeaders := x.getHeadersForRequest(nil) - // 添加特定的headers - reqHeaders["Content-Type"] = "application/x-www-form-urlencoded" - reqHeaders["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" - - for k, v := range reqHeaders { - req.Header.Set(k, v) - } - - // 检查是否需要添加X-Client-Id - if !strings.Contains(url, "shield/captcha/init") { - req.Header.Set("X-Client-Id", PAN_CLIENT_ID) - } - - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - log.Printf("验证码响应状态码: %d", resp.StatusCode) - log.Printf("验证码响应内容: %s", string(body)) - - var result map[string]interface{} - if err := json.Unmarshal(body, &result); err != nil { - return nil, fmt.Errorf("JSON 解析失败: %v, raw: %s", err, string(body)) - } - - log.Printf("解析后的响应: %+v", result) - return result, nil -} - -// getHeadersForRequest 获取请求头 -func (x *XunleiPanService) getHeadersForRequest(accessToken *string) map[string]string { - headers := map[string]string{ - "Content-Type": "application/json; charset=utf-8", - } - - // 这里我们简化处理,因为验证码请求不需要这些 - // if x.CaptchaToken != nil { - // headers["User-Agent"] = x.buildCustomUserAgent() - // headers["X-Captcha-Token"] = *x.CaptchaToken - // } else { - headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36" - // } - - // if accessToken != nil { - // headers["Authorization"] = fmt.Sprintf("Bearer %s", *accessToken) - // } - - // if x.DeviceID != "" { - // headers["X-Device-Id"] = x.DeviceID - // } - - return headers -} - func (x *XunleiPanService) UpdateConfig(config *PanConfig) { if config == nil { return @@ -1172,4 +958,4 @@ type XLTaskResult struct { Data struct { ShareID string `json:"share_id"` } `json:"data"` -} +} \ No newline at end of file diff --git a/handlers/system_config_handler.go b/handlers/system_config_handler.go index 76419c4..75f52cf 100644 --- a/handlers/system_config_handler.go +++ b/handlers/system_config_handler.go @@ -180,9 +180,9 @@ func UpdateSystemConfig(c *gin.Context) { } if req.AutoTransferMinSpace != nil { - if *req.AutoTransferMinSpace < 100 || *req.AutoTransferMinSpace > 1024 { + if *req.AutoTransferMinSpace < 5 || *req.AutoTransferMinSpace > 1024 { utils.Warn("配置验证失败 - AutoTransferMinSpace超出范围: %d", *req.AutoTransferMinSpace) - ErrorResponse(c, "最小存储空间必须在100-1024GB之间", http.StatusBadRequest) + ErrorResponse(c, "最小存储空间必须在5-1024GB之间", http.StatusBadRequest) return } }