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"` }