包含关键字 BI 的文章 - 空痕博客 - 编程学习分享
首页
小记
php
python
uniapp
前端
其他
机器人
QQ机器人
项目
功能库
应用
其他页面
友情链接
用户留言
联系空痕
热门文章
PHP搭建QQ机器人(QQ官方)
解决三个导致 Google Antigravity 无法登录的问题
移动云盘分享的链接信息获取接口请求体/响应体加密及解密
下载文件到指定文件夹
上传文件到夸克网盘python代码
标签搜索
uniapp
python
PHP
UTS
uniapp-x
模板
html
VUE
夸克网盘
移动云盘
APP
KongHen
机器人
QQ
ID3
pyinstaller
redis
Echarts
邮箱
js
发布
登录
注册
找到
9
篇与
BI
相关的结果
2025-12-08
粒子特效圣诞祝福HTML页面模板及AI提示词
Vue3粒子动画,Three.js圣诞树,AI生成代码,3D交互圣诞祝福,粒子特效,WebGL动画,前端技术演示,圣诞贺卡,弹幕飘雪,单文件HTML
模板
# 模板
# html
# 祝福
KongHen02
1年前
0
224
1
2025-11-26
移动云盘分享的链接信息获取接口请求体/响应体加密及解密
实现目的 有没有过这样的体验?手里攥着几十上百个网盘分享链接,要一个个点进页面、手动确认转存到自己的网盘——重复的操作像流水线作业,不仅浪费时间,而且还可能会漏掉文件。所以搞个自动转存脚本是非常有必要的。 打开分享链接->点击F12->查看网络,$[阿鲁表情]::(哭泣) 网盘的分享链接数据居然全是加密的。观察了一下加密字符好像是base64,直接一手解码,乱码!!!作为一个对逆向工程一窍不通的开发者,这已经超出了我的能力范围。 既然自己搞不定,不如试试“AI”。刚好试试最近热门的Gork.我把收集到的几条请求体、响应体密文整理好,清晰标注了获取场景,然后一股脑发给了Gork。 让我惊喜的是,Gork不仅精准识别出这些密文来自哪个网盘站点,还直接给出了对应的加密算法类型。我拿着密钥代入算法解密,结果发现解密失败$[阿鲁表情]::(中指)。 我只能继续问Gork,让它提供判断加密算法和密钥的参考来源。好在Gork很配合,直接给出了几个链接,包含GitHub及CSDN的链接。将几个站点都查看了一遍,选择了最简单的,文件名清晰且代码精简。 经过测试,解密可以使用,但是加密报错。将问题再扔给Gork,修复了即便终于给了我可以使用的代码,自己再整合一下就完成了转存分享中最难的部分$[阿鲁表情]::(得意)。 演示链接 链接: https://yun.139.com/shareweb/#/w/i/2qidG1XEkUKi0 提取码:d78q 复制内容打开中国移动云盘手机APP,操作更方便哦 解密解密分析 项目说明算法AES-128-CBC + PKCS7 填充密钥"PVGDwmcvfs1uV3d1"(UTF-8 编码)IV随机,前 16 字节拼接在密文前密文Base64 解码后第 17 字节起编码整体 Base64(标准,无 URL-safe)加解密代码 需要安装crypto-js库 const CryptoJS = require("crypto-js"); // 新版外链接口固定密钥 const FIXED_KEY = "PVGDwmcvfs1uV3d1"; /** * 移动云盘新版外链加密(OutLink) * @param {string} plaintextJson 明文JSON字符串 * @returns {string} 最终的Base64密文(和官方App一模一样) */ function encryptOutLink(plaintextJson) { // 1. 生成随机IV(16字节) const iv = CryptoJS.lib.WordArray.random(16); // 2. AES-128-CBC 加密 const encrypted = CryptoJS.AES.encrypt( plaintextJson, CryptoJS.enc.Utf8.parse(FIXED_KEY), { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 } ); // 3. 把 IV(16字节) + 密文 拼接起来 const ivAndCiphertext = iv.concat(encrypted.ciphertext); // 4. 整体转Base64 → 这就是最终发出去的字符串 return ivAndCiphertext.toString(CryptoJS.enc.Base64); } /** * 解密移动云盘新版外链(OutLink) * @param {string} encryptedBase64 响应体的 Base64 字符串(可能带空格) * @returns {string} 解密后的明文 JSON */ function decryptOutLink(encryptedBase64) { try { // 1. 清理 Base64 const cleanB64 = encryptedBase64.replace(/\s+/g, '');; // 2. Base64 解码 → WordArray const combined = CryptoJS.enc.Base64.parse(cleanB64); const totalLength = combined.sigBytes; // 总字节数 // 3. 提取 IV(前 16 字节 = 4 words)和密文(剩余) const ivBytes = combined.sigBytes / 4; // 每个 word 4 字节 const iv = CryptoJS.lib.WordArray.create(combined.words.slice(0, 4)); // 前 16 字节 const ciphertext = CryptoJS.lib.WordArray.create( combined.words.slice(4), totalLength - 16 // 剩余长度 ); // 4. AES-128-CBC 解密 const decrypted = CryptoJS.AES.decrypt( { ciphertext: ciphertext }, CryptoJS.enc.Utf8.parse(FIXED_KEY), { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 } ); // 5. 转 UTF-8 字符串(JSON) const plaintext = decrypted.toString(CryptoJS.enc.Utf8); if (!plaintext) { throw new Error("解密为空,可能是填充错误或密钥不对"); } return plaintext; } catch (error) { throw new Error(`解密失败: ${error.message}`); } } // 测试 // getOutLinkGeneral 请求体加密 const test1 = JSON.stringify({ getOutLinkGeneralReq: { linkID: "2qidFfUiXYAas", isPasswd: 1 } }); const res1 = encryptOutLink(test1); console.log("getOutLinkGeneral 请求体加密结果:", res1); console.log("getOutLinkGeneral 请求体解密结果:", decryptOutLink(res1)); // getOutLinkInfoV6 请求体加密 const test2 = JSON.stringify({ getOutLinkInfoReq: { account: "", linkID: "2qidG1XEkUKi0", passwd: "d78q", caSrt: 0, coSrt: 0, srtDr: 1, bNum: 1, pCaID: "root", eNum: 200 } }); const res2 = encryptOutLink(test2); console.log("getOutLinkInfoV6 请求体加密结果:", res2); console.log("getOutLinkInfoV6 请求体解密结果:", decryptOutLink(res2)); // getOutLinkGeneral 响应体解密 const test3 = `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`; const res3 = decryptOutLink(test3); console.log("getOutLinkGeneral 响应体解密结果:", res3); console.log("getOutLinkGeneral 响应体加密结果:", encryptOutLink(res3)); // getOutLinkInfoV6 响应体解密 const text4 = "q1FepUXZdRCJESkrGVyAkMsyT+9 NJNaPxqtbSHUvoMsRyKS2Ju2Ed8nQR/XmD9c1hlRRRpMtR++bsEbNRKL6Z9dvrqioStjUN3Cfi41ANo8YDbXvqvVfIosSGWym73S7+UxadvzDsLFwx6sgAOxMWhoyY2Lkcmt9YkI6oNAYmf5AZUgUhl/oSuXg4PJYOk1MimRv5etgqpTvwMQwQHRsZt4FPOlSLq6bL3R721PUQ3049/UBrom92I0FYGQJ6kjP1I7fRrC1yj2RPiqtVTOv3dsldvtaDj0j9dxkMhBiW3kwzN4cAMld60Q3llLEuaNcwmMANNeKjaeyEpXUUgtDgEIrL9+kSC/Qpkll3rh27uF3lUG4eLB5Ij0a6ABG+zHSau3gGM8/vrFWKOigdAlq2zMaa/Zp3hIp2rBEAKRt+YIR4hC9BnlxrP8d9Wm7vJB/EGWWiHb2kkiULpI5DFToX1w/8 jSvr3XuGtg9JopuRbK4Z6HJYRaJ9D+45 yHyYurSucqzakgDJER1WpfM3UZNcBvD2xgkbUb0vUdiVCvqz1E+/0I+OEzVb3p/w6ARI3SHENQGLTvdCkpW9HmAIWDwz8ONl6v2/8 yqAZFVpwJYmfqrYjUyWXmZZwsoNBgE5fg+SE2nkb90I7frKqRNiRfu7+V0NuL5T/dHHCaEXxuVxR3dm7h9hDmh/9 fnTWBTSIld4sKBmQY8wJNDziy0guKNOVO1TpSAcCWXUcpc913s1ycVWPY6eGfohZUrMu0C3WmuxGQEVHD3EPaU17fBCmxaGLm5SqaNY/g+P0kGNw0SZFybo/K6rk3c24M2zrSR6YiN2TGc+9 YpDDnV0PUH0vfcgThw8EPpmx0pJ4AgfqsbxYXOwo3OpMkiVZWjkfUj2yZiRypd+RybH7iJpd141kp8Y+H4r/o47GJ8D72D74f3AI25lJz0jtdnRE/UHJR9VAIRL9gTYVIxupObrJUcXbVEZ1sCEVx9NyZssoUiaTwDNazaLqHnmelCaqoetNEdWIt8" const res4 = decryptOutLink(text4); console.log("getOutLinkInfoV6 响应体解密结果:", res4); console.log("getOutLinkInfoV6 响应体加密结果:", encryptOutLink(res4)); 参考内容 139cloudsecret
前端
# 移动云盘
# CryptoJS
KongHen02
1年前
0
683
0
2025-10-24
UTS编写字符串编解码/加密插件(安卓及鸿蒙端)
全局说明 编写说明 uts在安卓端编译为kotlin,所以,使用可以使用安卓自带库+kotlin的方法来实现 uts在鸿蒙端编译为ArkTs,ArkTs和UTS很相似,包括一些方法都一样,所以可以直接从ArkTs的文档里复制代码,稍微修改即可使用。 使用的库 安卓端 import MessageDigest from 'java.security.MessageDigest'; import BigInteger from 'java.math.BigInteger'; import Base64 from 'java.util.Base64'; 鸿蒙端 import util from '@ohos.util'; import { cryptoFramework } from '@kit.CryptoArchitectureKit';插件接口定义 /** * interface.uts * uts插件接口定义文件,按规范定义接口文件可以在HBuilderX中更好的做到语法提示 */ /** * 哈希算法枚举 */ export type HashAlgorithm = | "MD5" | "SHA1" | "SHA224" | "SHA256" | "SHA384" | "SHA512" /** * 哈希加密返回结果 */ export type HashResult = { hash: string } /** * 哈希加密函数定义 */ export type HashFunction = (input: string, algorithm: HashAlgorithm) => string1. Base64编解码 安卓端 /** * BASE64编码方法 * * @param input 输入字符串 * @return BASE64加密后的字符串 */ export const Base64Encode = function (input: string) : string { try { // 将字符串转换为字节数组 // toByteArray()为kotlin的方法 const inputBytes = input.toByteArray(); // 使用Base64编码器进行编码 const encodedBytes = Base64.getEncoder().encodeToString(inputBytes); // 将编码后的字节数组转换为字符串 return encodedBytes; } catch (e) { console.error("BASE64加密错误:", e); return ""; } } /** * BASE64解码方法 * * @param input 输入字符串 * @return BASE64解密后的字符串 */ export const Base64Decode = function (input: string) : string { try { // 将Base64字符串转换为字节数组 const decodedBytes = Base64.getDecoder().decode(input); // 将Java字节数组转换为UTS字符串 return new String(decodedBytes); } catch (e) { console.error("BASE64解密错误:", e); return ""; } } 鸿蒙端 /** * BASE64编码方法 * * @param input 输入字符串 * @return BASE64加密后的字符串 */ export const Base64Encode = function (input: string) : string { let textEncoder = new util.TextEncoder("utf-8"); let uint8Array = textEncoder.encodeInto(input); let base64Helper = new util.Base64Helper(); return base64Helper.encodeToStringSync(uint8Array); } /** * BASE64解码方法 * * @param input 输入字符串 * @return BASE64解密后的字符串 */ export const Base64Decode = function (input: string) : string { let Base64Helper = new util.Base64Helper(); let arr = Base64Helper.decodeSync(input) let textDecoder = util.TextDecoder.create('utf-8'); return textDecoder.decodeToString(arr); } 消息摘要计算 HASH加密使用统一方法,包含MD5、SHA1、SHA224、SHA256、SHA384、SHA512 安卓端 /** * 统一哈希加密方法 * * @param input 输入字符串 * @param algorithm 哈希算法枚举 * @return 哈希加密后的十六进制字符串 */ export const hash : HashFunction = function (input : string, algorithm: HashAlgorithm) : string { try { // 创建MessageDigest实例 const md = MessageDigest.getInstance(algorithm); // 输入数据转化为字节数组 const dataArray = input.toByteArray() // 计算哈希值 const hashBytes = md.digest(dataArray); // 转换为十六进制字符串 const result = BigInteger(1, hashBytes).toString(16) return result; } catch (e) { // 方法出错时返回空字符串 console.error(`${algorithm}加密错误:`, e); return ""; } }鸿蒙端 /** * 统一哈希加密方法 * * @param input 输入字符串 * @param algorithm 哈希算法枚举 * @return 哈希加密后的十六进制字符串 */ export const hash : HashFunction = function (input : string, algorithm: HashAlgorithm) : string { try { // 创建哈希实例 let md = cryptoFramework.createMd(algorithm); // 使用同步方法更新数据 let textEncoder = util.TextEncoder.create('utf-8'); let dataBlob : cryptoFramework.DataBlob = { data: textEncoder.encodeInto(input); }; md.updateSync(dataBlob); // 使用同步方法计算摘要 let mdResult : cryptoFramework.DataBlob = md.digestSync(); // 转换为十六进制字符串 let result = Array.from(mdResult.data).map(byte => byte.toString(16).padStart(2, '0')).join(''); return result; } catch (e) { // 方法出错时返回空字符串 // console.error(`${algorithm}加密错误:`, e); return ""; } }规范调用方法 这里安卓端和鸿蒙端相同 // MD5加密 export const MD5 = function (input: string) : string { return hash(input, 'MD5') } // SHA1加密 export const SHA1 = function (input: string) : string { return hash(input, 'SHA1') } // SHA224加密 export const SHA224 = function (input: string) : string { return hash(input, 'SHA224') } // SHA256加密 export const SHA256 = function (input: string) : string { return hash(input, 'SHA256') } // SHA384加密 export const SHA384 = function (input: string) : string { return hash(input, 'SHA384') } // SHA512加密 export const SHA512 = function (input: string) : string { return hash(input, 'SHA512') }使用方法 import * as KhCrypto from '@/uni_modules/kh-crypto' const input = ref<string>('待加密字符串'); const output = ref<string>('') // base64编码 output.value = KhCrypto.Base64Encode(inputText.value) // base64解码 output.value = KhCrypto.Base64Decode(inputText.value) // MD5加密 output.value = KhCrypto.MD5(inputText.value) // SHA1加密 output.value = KhCrypto.SHA1(inputText.value) // SHA224加密 output.value = KhCrypto.SHA224(inputText.value) // SHA256加密 output.value = KhCrypto.SHA256(inputText.value) // SHA384加密 output.value = KhCrypto.SHA384(inputText.value) // SHA512加密 output.value = KhCrypto.SHA512(inputText.value)插件源码 kh-crypto - DCloud插件市场 参考文档 在uts中如何将字符串转换为ByteArray Base64Helper - 鸿蒙开发API参考 消息摘要计算介绍及算法规格 - 鸿蒙开发指南
uniapp
uniapp-x
功能库
uts
# uniapp
# UTS
# uniapp-x
# 鸿蒙
KongHen02
1年前
0
70
0
2025-08-21
uniapp-x实现自定义tabbar
uniapp-x自带导航栏位置固定,且UI无法修改。如果需要适配自己的应用UI及色彩就需要自定义tabbar。 实现说明 将tabbar写入主页面,需要显示的页面作为组件引入。 示例样式 演示示例图片 实现方法 使用swiper实现 说明: 所有页面一次性加载 允许左右滑动 优点:允许滑动切换,用户体验升级 演示代码 <template> <!-- 页面内容区域 --> <swiper style="flex: 1;" :current="selectedIndex" @change="swiperChange"> <swiper-item item-id="index"> <IndexPage></IndexPage> </swiper-item> <swiper-item item-id="more"> <MorePage></MorePage> </swiper-item> <swiper-item item-id="user"> <UserPage></UserPage> </swiper-item> </swiper> <!-- tabber区域 --> <view class="tab-bar-container"> <view v-for="(item, index) in tabList" class="tab-bar-item" @click="switchTab(index)"> <image class="tab-bar-icon" :src="(selectedIndex === index ? item.s_icon : item.icon)"></image> <text class="tab-bar-text" :style="'color:' + (selectedIndex === index ? '#F59E0B' : '#999999') +';'">{{ item.name }}</text> </view> </view> </template> <script setup lang="uts"> // 导入页面 import IndexPage from "./tabbar/index.uvue" import MorePage from "./tabbar/more.uvue" import UserPage from "./tabbar/user.uvue" // tabbar接口类型 type TabInfo = { name : string, icon : string, s_icon : string } // 页面列表 const tabList = reactive<TabInfo[]>([ { name: "首页", icon: "/static/tabbar/home.png", s_icon: "/static/tabbar/home_selected.png" }, { name: "活动", icon: "/static/tabbar/more.png", s_icon: "/static/tabbar/more_selected.png" }, { name: "我的", icon: "/static/tabbar/user.png", s_icon: "/static/tabbar/user_selected.png" } ]) // 选中的页面 const selectedIndex = ref<number>(0) // swiper切换 const swiperChange = (e: UniSwiperChangeEvent) => { let index = e.detail.current if (selectedIndex.value === index) return selectedIndex.value = index } // 页面切换 const switchTab = (index : number) => { if (selectedIndex.value === index) return selectedIndex.value = index } </script> <style lang="scss"> .tab-bar-container { position: fixed; bottom: 60rpx; width: 80%; left: 10%; z-index: 999; display: flex; flex-direction: row; justify-content: space-around; height: 120rpx; border-radius: 60rpx; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); background: rgba(255, 255, 255, 0.4); } .tab-bar-item { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 15rpx 40rpx; } .tab-bar-icon { width: 44rpx; height: 44rpx; margin-bottom: 8rpx; } .tab-bar-text { font-size: 24rpx; } </style>官方示例 说明: 单次只加载一个页面 加载成功后使用v-show控制显示/隐藏,不重复加载(官方使用CSS属性visibility控制,测试不行) 优点:分页加载,减小单次加载压力(如果页面DOM多的话) 演示代码 <template> <!-- 页面内容区域 --> <view style="flex: 1;"> <IndexPage v-if="tabList[0].init" v-show="selectedIndex==0"></IndexPage> <MorePage v-if="tabList[1].init" v-show="selectedIndex==1"></MorePage> <MorePage v-if="tabList[2].init" v-show="selectedIndex==2"></MorePage> </view> <!-- tabber区域 --> <view class="tab-bar-container"> <view v-for="(item, index) in tabList" class="tab-bar-item" @click="switchTab(index)"> <image class="tab-bar-icon" :src="(selectedIndex === index ? item.s_icon : item.icon)"></image> <text class="tab-bar-text" :style="'color:' + (selectedIndex === index ? '#F59E0B' : '#999999') +';'">{{ item.name }}</text> </view> </view> </template> <script setup lang="uts"> // 导入页面 import IndexPage from "./tabbar/index.uvue" import MorePage from "./tabbar/more.uvue" import UserPage from "./tabbar/user.uvue" // tabbar接口类型 type TabInfo = { init: boolean, name : string, icon : string, s_icon : string } // 页面列表 const tabList = reactive<TabInfo[]>([ { init: true, name: "首页", icon: "/static/tabbar/home.png", s_icon: "/static/tabbar/home_selected.png" }, { init: false, name: "更多", icon: "/static/tabbar/more.png", s_icon: "/static/tabbar/more_selected.png" }, { init: false, name: "我的", icon: "/static/tabbar/user.png", s_icon: "/static/tabbar/user_selected.png" } ]) // 选中的页面 const selectedIndex = ref<number>(0) // 页面切换 const switchTab = (index : number) => { if (selectedIndex.value === index) return if (!tabList[index].init) { tabList[index].init = true } selectedIndex.value = index } </script> <style lang="scss"> .tab-bar-container { position: fixed; bottom: 60rpx; width: 80%; left: 10%; z-index: 999; display: flex; flex-direction: row; justify-content: space-around; height: 120rpx; border-radius: 60rpx; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); background: rgba(255, 255, 255, 0.4); } .tab-bar-item { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 15rpx 40rpx; } .tab-bar-icon { width: 44rpx; height: 44rpx; margin-bottom: 8rpx; } .tab-bar-text { font-size: 24rpx; } </style>其他方法 使用share-element组件实现 复制官方代码,偶先切换页面组件闪动问题。官方uniapp-xapp的demo测试正常,不知道申明原因。 使用components组件+share-element组件实现的tabbar组件实现,tabbar组件会出现与页面移入方向反向滑动的动画 share-element文档 静态资源 tabbar图标(png) 下载地址:https://www.khkj6.com/usr/uploads/2025/08/2337268233.zip 提取码:
uniapp-x
# VUE
# uniapp-x
# tabbar
# swiper
KongHen02
1年前
0
168
1
2025-07-06
上传文件到夸克网盘python代码
夸克网盘完整功能请参考文章:夸克网盘SDK 以下为根据个人需求,参考OpenList让DeepSeek编写的文件上传python脚本 简要说明 包含的功能 创建文件夹 分片上传文件 分享文件夹 获取分享链接 cookie存储在quark.cookie.txt文件中即可 基本变量说明: PARENT_DIR_ID:创建文件夹的父文件夹id,如果在根目录下创建文件夹,输入0 FOLDER_NAME:要创建的文件夹名 FILE_PATH:要上传的文件 上传频繁会被拉黑 脚本代码 import base64 import hashlib import io import json import logging import mimetypes import os import time import requests from datetime import datetime, timezone from typing import Dict, List, Tuple, Optional, Callable # 配置日志 log = logging.getLogger(__name__) logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) try: from colorama import init, Fore, Style init(autoreset=True) COLOR_SUPPORT = True except ImportError: COLOR_SUPPORT = False print("未安装colorama库,将显示无颜色输出。安装命令: pip install colorama") class QuarkOrUC: def __init__(self, cookie: str, parent_dir_id: str): """ 初始化Quark网盘客户端 :param cookie: 完整的认证Cookie字符串 :param parent_dir_id: 默认父目录ID """ self.cookie = cookie self.parent_dir_id = parent_dir_id # 配置信息 self.config = { "api": "https://drive.quark.cn/1/clouddrive", "referer": "https://pan.quark.cn", "origin": "https://pan.quark.cn", "pr": "ucpro", "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36" } def colored_print(self, message: str, color: str = "white") -> None: """带颜色打印消息""" if not COLOR_SUPPORT: print(message) return colors = { "red": Fore.RED, "green": Fore.GREEN, "yellow": Fore.YELLOW, "blue": Fore.BLUE, "cyan": Fore.CYAN, "white": Fore.WHITE, } print(colors.get(color, Fore.WHITE) + message + Style.RESET_ALL) def request(self, pathname: str, method: str, data: Optional[Dict] = None, query_params: Optional[Dict] = None) -> Tuple[Optional[Dict], Optional[Exception]]: """ 发送API请求 :param pathname: API路径 :param method: HTTP方法 (GET/POST) :param data: POST请求的数据 :param query_params: 额外的查询参数 :return: (响应数据, 错误对象) """ url = self.config['api'] + pathname headers = { "Cookie": self.cookie, "Accept": "application/json, text/plain, */*", "Referer": self.config['referer'], "User-Agent": self.config['ua'], "Origin": self.config['origin'], "Priority": "u=1, i" } # 基础查询参数 params = {"pr": self.config['pr'], "fr": "pc"} # 添加额外的查询参数 if query_params: params.update(query_params) try: if method.upper() == "GET": response = requests.get(url, headers=headers, params=params) elif method.upper() == "POST": headers["Content-Type"] = "application/json" response = requests.post(url, headers=headers, params=params, json=data) else: raise ValueError(f"不支持的HTTP方法: {method}") response.raise_for_status() json_resp = response.json() # 更新cookie if '__puus' in response.cookies: self.cookie = f"{self.cookie}; __puus={response.cookies['__puus']}" # 检查错误响应 if json_resp.get('status', 200) >= 400 or json_resp.get('code', 0) != 0: error_msg = json_resp.get('message', '未知错误') log.error(f"API错误: {error_msg}") return None, Exception(error_msg) return json_resp, None except requests.exceptions.RequestException as e: log.error(f"请求失败: {str(e)}") return None, e except Exception as e: log.error(f"处理响应时出错: {str(e)}") return None, e def create_folder(self, folder_name: str) -> Tuple[Optional[Dict], Optional[Exception]]: """ 创建文件夹 :param folder_name: 文件夹名称 :return: (响应数据, 错误对象) """ data = { "pdir_fid": self.parent_dir_id, "file_name": folder_name, "dir_path": "", "dir_init_lock": False } return self.request("/file", "POST", data) def create_share(self, fid_list: List[str], title: str, expired_type: int = 1, url_type: int = 1) -> Tuple[Optional[Dict], Optional[Exception]]: """ 创建分享任务 :param fid_list: 要分享的文件/文件夹ID列表 :param title: 分享标题 :param expired_type: 过期类型 (1: 永久) :param url_type: URL类型 (1: 标准链接) :return: (响应数据, 错误对象) """ data = { "fid_list": fid_list, "title": title, "url_type": url_type, "expired_type": expired_type } return self.request("/share", "POST", data) def check_share_task(self, task_id: str) -> Tuple[Optional[Dict], Optional[Exception]]: """ 检查分享任务状态 :param task_id: 分享任务ID :return: (响应数据, 错误对象) """ query_params = { "task_id": task_id, "retry_index": 0 } return self.request("/task", "GET", query_params=query_params) def get_share_info(self, share_id: str) -> Tuple[Optional[Dict], Optional[Exception]]: """ 获取分享链接信息 :param share_id: 分享ID :return: (响应数据, 错误对象) """ data = {"share_id": share_id} return self.request("/share/password", "POST", data) def up_pre(self, file_name: str, mimetype: str, size: int, parent_id: str) -> Tuple[Optional[Dict], Optional[Exception]]: """ 预上传请求 :param file_name: 文件名 :param mimetype: MIME类型 :param size: 文件大小 :param parent_id: 父目录ID :return: (预上传响应, 错误对象) """ now = int(time.time() * 1000) data = { "ccp_hash_update": True, "dir_name": '', "file_name": file_name, "format_type": mimetype, "l_created_at": now, "l_updated_at": now, "pdir_fid": parent_id, "size": size } return self.request("/file/upload/pre", "POST", data) def up_hash(self, md5_hash: str, sha1_hash: str, task_id: str) -> Tuple[bool, Optional[Exception]]: """ 提交文件哈希验证 :param md5_hash: MD5哈希值 :param sha1_hash: SHA1哈希值 :param task_id: 上传任务ID :return: (是否已完成上传, 错误对象) """ data = { "md5": md5_hash, "sha1": sha1_hash, "task_id": task_id } resp, err = self.request("/file/update/hash", "POST", data) if err: return False, err return (resp.get('data', {}).get('finish', False), None) def up_part(self, pre: Dict, mime_type: str, part_number: int, chunk_data: bytes) -> Tuple[Optional[str], Optional[Exception]]: """ 上传文件分片 :param pre: 预上传响应数据 :param mime_type: MIME类型 :param part_number: 分片编号 :param chunk_data: 分片数据 :return: (ETag, 错误对象) """ # 使用 timezone-aware datetime now = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT') auth_meta = f"PUT\n\n{mime_type}\n{now}\nx-oss-date:{now}\nx-oss-user-agent:aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit\n/{pre['data']['bucket']}/{pre['data']['obj_key']}?partNumber={part_number}&uploadId={pre['data']['upload_id']}" auth_data = { "auth_info": pre['data']['auth_info'], "auth_meta": auth_meta, "task_id": pre['data']['task_id'] } # 获取上传授权 auth_resp, err = self.request("/file/upload/auth", "POST", auth_data) if err: return None, err # 执行分块上传 upload_url = f"https://{pre['data']['bucket']}.{pre['data']['upload_url'][7:]}/{pre['data']['obj_key']}" headers = { "Authorization": auth_resp['data']['auth_key'], "Content-Type": mime_type, "Referer": "https://pan.quark.cn/", "x-oss-date": now, "x-oss-user-agent": "aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit" } params = { "partNumber": str(part_number), "uploadId": pre['data']['upload_id'] } try: response = requests.put( upload_url, headers=headers, params=params, data=chunk_data, timeout=30 ) response.raise_for_status() return response.headers.get('ETag'), None except requests.exceptions.RequestException as e: log.error(f"分片上传失败: {str(e)}") return None, e def up_commit(self, pre: Dict, etags: List[str]) -> Optional[Exception]: """ 提交分片上传完成 :param pre: 预上传响应数据 :param etags: 所有分片的ETag列表 :return: 错误对象 """ # 使用 timezone-aware datetime now = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT') # 构建XML body xml_parts = [] for i, etag in enumerate(etags, 1): xml_parts.append(f"<Part><PartNumber>{i}</PartNumber><ETag>{etag}</ETag></Part>") xml_body = f"""<?xml version="1.0" encoding="UTF-8"?> <CompleteMultipartUpload> {"".join(xml_parts)} </CompleteMultipartUpload>""" # 计算Content-MD5 md5 = hashlib.md5() md5.update(xml_body.encode('utf-8')) content_md5 = base64.b64encode(md5.digest()).decode('utf-8') # 处理回调数据 callback_json = json.dumps(pre['data']['callback']) callback_b64 = base64.b64encode(callback_json.encode('utf-8')).decode('utf-8') # 构建授权元数据 auth_meta = f"POST\n{content_md5}\napplication/xml\n{now}\nx-oss-callback:{callback_b64}\nx-oss-date:{now}\nx-oss-user-agent:aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit\n/{pre['data']['bucket']}/{pre['data']['obj_key']}?uploadId={pre['data']['upload_id']}" auth_data = { "auth_info": pre['data']['auth_info'], "auth_meta": auth_meta, "task_id": pre['data']['task_id'] } # 获取提交授权 auth_resp, err = self.request("/file/upload/auth", "POST", auth_data) if err: return err # 执行提交请求 upload_url = f"https://{pre['data']['bucket']}.{pre['data']['upload_url'][7:]}/{pre['data']['obj_key']}" headers = { "Authorization": auth_resp['data']['auth_key'], "Content-MD5": content_md5, "Content-Type": "application/xml", "Referer": "https://pan.quark.cn/", "x-oss-callback": callback_b64, "x-oss-date": now, "x-oss-user-agent": "aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit" } params = {"uploadId": pre['data']['upload_id']} try: response = requests.post( upload_url, headers=headers, params=params, data=xml_body, timeout=30 ) response.raise_for_status() return None except requests.exceptions.RequestException as e: log.error(f"提交上传失败: {str(e)}") return e def up_finish(self, pre: Dict) -> Optional[Exception]: """ 完成上传流程 :param pre: 预上传响应数据 :return: 错误对象 """ data = { "obj_key": pre['data']['obj_key'], "task_id": pre['data']['task_id'] } _, err = self.request("/file/upload/finish", "POST", data) time.sleep(1) # 等待服务器处理 return err def upload_file(self, file_name: str, file_data: bytes, parent_dir_id: Optional[str] = None, progress_callback: Optional[Callable] = None) -> Tuple[Optional[str], Optional[Exception]]: """ 完整的文件上传流程 :param file_name: 文件名 :param file_data: 文件二进制数据 :param parent_dir_id: 父目录ID(默认为初始化时设置的目录) :param progress_callback: 进度回调函数 :return: (文件ID, 错误对象) - 成功时返回文件ID,失败时返回错误 """ # 使用默认父目录ID(如果未提供) if parent_dir_id is None: parent_dir_id = self.parent_dir_id # 自动检测MIME类型 mime_type, _ = mimetypes.guess_type(file_name) if not mime_type: mime_type = "application/octet-stream" file_size = len(file_data) file_stream = io.BytesIO(file_data) # 步骤1: 计算文件哈希 md5_hash = hashlib.md5() sha1_hash = hashlib.sha1() file_stream.seek(0) # 计算哈希 while True: chunk = file_stream.read(8192) if not chunk: break md5_hash.update(chunk) sha1_hash.update(chunk) md5_hex = md5_hash.hexdigest() sha1_hex = sha1_hash.hexdigest() # 重置文件流指针 file_stream.seek(0) # 步骤2: 预上传请求 pre_resp, err = self.up_pre(file_name, mime_type, file_size, parent_dir_id) if err: return None, f"预上传失败: {str(err)}" # 步骤3: 提交哈希验证 finished, err = self.up_hash(md5_hex, sha1_hex, pre_resp['data']['task_id']) if err: return None, f"哈希验证失败: {str(err)}" if finished: self.colored_print("服务器已有相同文件,跳过上传", "yellow") return None, "服务器已有相同文件" # 步骤4: 分块上传 part_size = pre_resp['metadata']['part_size'] etags = [] part_number = 1 while True: chunk = file_stream.read(part_size) if not chunk: break etag, err = self.up_part( pre_resp, mime_type, part_number, chunk ) if err: return None, f"分片{part_number}上传失败: {str(err)}" etags.append(etag) part_number += 1 # 更新进度 if progress_callback: progress = min(100, int(file_stream.tell() / file_size * 100)) progress_callback(progress) # 步骤5: 提交上传 err = self.up_commit(pre_resp, etags) if err: return None, f"提交上传失败: {str(err)}" # 步骤6: 完成上传 err = self.up_finish(pre_resp) if err: return None, f"完成上传失败: {str(err)}" # 返回上传文件的ID file_id = pre_resp.get('data', {}).get('fid') if not file_id: return None, "未能获取上传文件ID" return file_id, None def load_cookie_from_file(file_path: str) -> str: """从文件加载Cookie""" try: with open(file_path, "r") as f: return f.read().strip() except Exception as e: raise Exception(f"加载Cookie文件失败: {str(e)}") if __name__ == "__main__": # 配置信息 COOKIE_FILE = "cookie.quark.txt" PARENT_DIR_ID = "0" FOLDER_NAME = "新建文件夹名" FILE_PATH = "ce5.zip" # 1. 加载Cookie try: cookie = load_cookie_from_file(COOKIE_FILE) if not cookie: raise Exception("Cookie文件为空") except Exception as e: print(f"❌ 错误: {str(e)}") exit(1) # 创建客户端实例 client = QuarkOrUC(cookie=cookie, parent_dir_id=PARENT_DIR_ID) # 2. 创建文件夹 client.colored_print("\n🟡 正在创建文件夹...", "yellow") create_resp, create_err = client.create_folder(FOLDER_NAME) if create_err: client.colored_print(f"❌ 未创建文件夹失败: {str(create_err)}", "red") exit(1) # 获取新创建的文件夹ID new_folder_id = create_resp.get('data', {}).get('fid') if not new_folder_id: client.colored_print("❌ 未能获取新文件夹ID", "red") exit(1) client.colored_print(f"✅ 文件夹创建成功! ID: {new_folder_id}", "green") # 3. 上传文件到新创建的文件夹 try: if not os.path.exists(FILE_PATH): raise Exception(f"文件不存在: {FILE_PATH}") # 获取文件大小(以字节为单位) file_size_bytes = os.path.getsize(FILE_PATH) file_size_mb = file_size_bytes / (1024 * 1024) with open(FILE_PATH, "rb") as f: file_data = f.read() # 确保读取的文件大小与实际文件大小一致 if len(file_data) != file_size_bytes: raise Exception(f"文件读取不完整: 预期 {file_size_bytes} 字节, 实际读取 {len(file_data)} 字节") # 进度回调函数 def progress_callback(percent): print(f"\r🟢 上传进度: {percent}%", end="", flush=True) file_name = os.path.basename(FILE_PATH) client.colored_print(f"\n🟡 开始上传文件: {file_name} ({file_size_mb:.2f} MB)", "yellow") file_id, upload_err = client.upload_file( file_name=file_name, file_data=file_data, parent_dir_id=new_folder_id, progress_callback=progress_callback ) if upload_err: client.colored_print(f"\n❌ 文件上传失败: {str(upload_err)}", "red") exit(1) else: client.colored_print(f"\n✅ 文件上传成功! 文件ID: {file_id}", "green") # 4. 创建分享任务 client.colored_print("\n🟡 正在创建分享任务...", "yellow") share_resp, share_err = client.create_share( fid_list=[new_folder_id], title=FOLDER_NAME ) if share_err: client.colored_print(f"❌ 创建分享任务失败: {str(share_err)}", "red") exit(1) # 获取分享任务ID task_id = share_resp.get('data', {}).get('task_id') if not task_id: client.colored_print("❌ 未能获取分享任务ID", "red") exit(1) client.colored_print(f"✅ 分享任务创建成功! 任务ID: {task_id}", "green") # 5. 轮询分享任务状态 interval = 5 # 每次间隔5秒 client.colored_print("\n🟡 正在监测分享状态...", "yellow") while True: print("\r进行中...", end="", flush=True) # 检查分享任务状态 task_resp, task_err = client.check_share_task(task_id) if task_err: client.colored_print(f"\n❌ 检查分享任务状态失败: {str(task_err)}", "red") exit(1) task_data = task_resp.get('data', {}) task_status = task_data.get('status') # 状态2表示分享成功 if task_status == 2: share_id = task_data.get('share_id') if not share_id: client.colored_print("\n❌ 分享成功但未获取到share_id", "red") exit(1) # 获取分享链接信息 share_info_resp, share_info_err = client.get_share_info(share_id) if share_info_err: client.colored_print(f"\n❌ 获取分享链接失败: {str(share_info_err)}", "red") exit(1) share_info_data = share_info_resp.get('data', {}) share_url = share_info_data.get('share_url', '') passcode = share_info_data.get('passcode', '') # 注意:实际键名是pwd_id # 如果获取到分享链接,退出循环 if share_url: print("\n", end="") # 清除"进行中..."提示 break # 检查任务是否失败 elif task_status in [3, 4]: # 3: 失败, 4: 取消 client.colored_print(f"\n❌ 分享任务失败: 状态码 {task_status}", "red") exit(1) # 等待下一次检查 time.sleep(interval) # 6. 显示分享结果 client.colored_print("\n✅ 分享创建成功!", "green") client.colored_print(f"📁 分享标题: {FOLDER_NAME}", "cyan") client.colored_print(f"🔗 分享链接: {share_url}", "cyan") if passcode: client.colored_print(f"🔑 提取密码: {passcode}", "cyan") except Exception as e: client.colored_print(f"❌ 操作失败: {str(e)}", "red") exit(1)运行演示 运行演示图片 参考文献 OpenList
python
# python
# 夸克网盘
KongHen02
1年前
2
630
5
1
2
下一页
易航博客