Compare commits

..

391 Commits

Author SHA1 Message Date
q
742dda8677 更新前端版本号 v0.2.1b3,前端接口调整: randomAuth直接使用后端加密的encryptedAuth 2026-02-23 10:39:13 +08:00
qaiu
76e0db0cfb Merge pull request #169 from rensumo/main
fix(web-service): randomAuth 仅返回 encryptedAuth,避免泄露认证信息
2026-02-23 09:41:28 +08:00
rensumo
6458a6e2c5 refactor qk parser and add package metadata 2026-02-23 08:01:08 +08:00
rensumo
cbf2294a8e fix: randomAuth only returns encryptedAuth 2026-02-22 20:49:24 +08:00
rensumo
9d558bf4e2 fix: avoid NPE in randomAuth code check 2026-02-22 20:23:46 +08:00
q
fdf067c25e 更新 夸克解析、小飞机解析,前端版本号 2026-02-22 19:15:15 +08:00
qaiu
5f9da47513 Merge pull request #167 from rensumo/main
feat: 新增捐赠账号池并完善认证参数解码/失败熔断机制
2026-02-22 18:00:32 +08:00
rensumo
b150641e3b fix: stabilize auth/decrypt flow and refresh donate account counts 2026-02-22 16:06:22 +08:00
rensumo
6355c35452 fix: 修复捐赠账号失败计数与路由外部访问问题 2026-02-22 12:36:20 +08:00
rensumo
81ffbbd6b1 feat: harden donated-account failure token and document key usage 2026-02-22 12:24:47 +08:00
rensumo
07c650a474 feat: Add validation for donateAccount endpoint 2026-02-22 11:12:35 +08:00
rensumo
04443bcb5e feat: 添加捐赠账号功能,支持数据库存储和随机选择账号解析 2026-02-19 12:59:47 +08:00
qaiu
d06974d556 Merge pull request #163 from qaiu/dependabot/npm_and_yarn/web-front/axios-1.13.5
Bump axios from 1.12.0 to 1.13.5 in /web-front
2026-02-13 01:17:00 +08:00
qaiu
80fa51fd0a 更新 PanDomainTemplate.java 删除118网盘解析 2026-02-12 20:35:23 +08:00
qaiu
a170134456 删除 P118Tool.java 2026-02-12 20:33:49 +08:00
qaiu
ba76e0dc6c 更新 README.md 2026-02-12 17:12:52 +08:00
dependabot[bot]
4a79542a46 Bump axios from 1.12.0 to 1.13.5 in /web-front
Bumps [axios](https://github.com/axios/axios) from 1.12.0 to 1.13.5.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.12.0...v1.13.5)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.13.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-11 17:55:01 +00:00
qaiu
999e260a60 更新 README.md 2026-02-10 15:38:00 +08:00
q
ed40b254e4 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	.gitignore
2026-02-06 02:51:10 +08:00
qaiu
8e9b9e6347 删除 auth-test.properties 2026-02-05 22:11:32 +08:00
q
4fa2d88204 chore: 删除包含敏感信息的auth-test.properties并添加到.gitignore 2026-02-05 22:08:00 +08:00
q
cb6d4811d2 fix: 完善 .gitignore 防止 Maven 变量目录被提交
- 添加更多构建目录忽略规则
- 防止 Maven 变量未替换时创建的字面量目录被提交
- 包括 build/, out/, classes/ 等常见构建目录
2026-02-05 20:38:12 +08:00
q
c19601b209 fix: 完善 .gitignore 防止 Maven 变量目录被提交
- 添加更多构建目录忽略规则
- 防止 Maven 变量未替换时创建的字面量目录被提交
- 包括 build/, out/, classes/ 等常见构建目录
2026-02-05 20:38:12 +08:00
q
6e6215ad7e feat(v0.2.1): 添加认证参数支持和客户端下载命令生成
主要更新:
- 新增 auth 参数加密传递支持 (QK/UC Cookie认证)
- 实现下载命令自动生成 (curl/aria2c/迅雷)
- aria2c 命令支持 8 线程 8 片段下载
- 修复 cookie 字段映射问题
- 优化前端 clientLinks 页面
- 添加认证参数文档和测试用例
- 更新 .gitignore 忽略编译目录
2026-02-05 20:35:47 +08:00
q
97ae1a5e92 fix: 修复安全漏洞 - 升级依赖版本
- Vert.x: 4.5.22 → 4.5.14 (包含所有安全修复,Netty版本由其管理)
- Logback: 1.5.19 → 1.5.18 (最新稳定版)
- SLF4J: 2.0.5 → 2.0.16
- Jackson: 2.14.2 → 2.18.2
- 移除不必要的 Netty BOM 依赖
2026-02-04 17:21:13 +08:00
q
a4a521a6f8 fix: 修复多个安全漏洞
修复的安全问题:
1. Vert.x Web static handler 缓存操纵漏洞 - 升级到 4.5.11
2. Netty CRLF注入漏洞 (CVE-2024-47535) - 强制使用 4.1.115.Final
3. Logback 任意代码执行漏洞 (CVE-2024-12798) - 升级到 1.5.15
4. Vert.x-Web XSS漏洞 - 升级到 4.5.11
5. Logback 类实例化漏洞 (CVE-2023-6378) - 升级到 1.5.15

变更:
- 降级 vertx.version: 4.5.22 → 4.5.11 (稳定安全版本)
- 添加 netty.version: 4.1.115.Final (通过 netty-bom 强制版本)
- 降级 logback.version: 1.5.19 → 1.5.15 (稳定安全版本)
- 升级 slf4j.version: 2.0.5 → 2.0.16
- 升级 jackson.version: 2.14.2 → 2.18.2
- 在 dependencyManagement 中添加 Netty BOM 和 Logback 版本管理
2026-02-04 17:14:53 +08:00
q
2056a91071 优化超星解析,清理冗余代码 2026-02-04 17:10:52 +08:00
qaiu
f6209a8959 更新 README.md
添加夸克,uc解析
2026-02-03 16:18:55 +08:00
q
72ed0ea8f8 Fixed 蓝奏云目录解析cookie验证问题 2026-02-03 13:27:09 +08:00
qaiu
d698f82299 Update Playground password protection link in README 2026-02-03 10:41:42 +08:00
q
ba4666c32a Fixed 蓝奏优享解析,v019b21 2026-02-02 16:02:36 +08:00
q
d6d37e8204 fixed: 修复蓝奏优享 #159, #158 2026-02-02 15:59:44 +08:00
q
cb9dbfcc69 蓝奏云规则更新 2026-02-01 10:33:25 +08:00
qaiu
42b366ed0f Clean up launch configurations in launch.json,run AppMain
Removed unused Java launch configurations from launch.json.
2026-01-27 07:53:20 +08:00
qaiu
4021c507b6 更新 README.md, 添加接口文档https://nfdparser.apifox.cn 2026-01-27 00:05:56 +08:00
qaiu
4d8e82080d 189.qaiu.top 大文件解析体验版,支持天翼云盘,移动云盘等
#153 #156 #119 #92
2026-01-26 13:47:37 +08:00
qaiu
fca608b44e 更新 README.md
天翼云盘演示站限时体验
2026-01-26 13:09:49 +08:00
q
34cb89a6ea fix(LeTool): 修复子目录 fileId URL 编码问题
- 在构建 parserUrl 时对 fileId 进行 URL 编码
- 避免 %2B 等特殊字符被前端 axios 自动解码导致请求失败
- 添加异常处理和降级方案
2026-01-23 17:59:19 +08:00
q
69d5f269bd fix(LeTool): 修复联想乐云目录解析失败问题
- 添加统一的 HEADERS 定义,包含完整的浏览器请求头
- 修复 API_URL_PREFIX 路径(share -> mshare)
- 添加 getCleanShareId() 方法处理 URL 中的查询参数
- 所有请求统一使用 putHeaders(HEADERS)
- 增加调试日志输出
2026-01-23 13:24:11 +08:00
q
459c974cb8 联想乐云文件夹解析 2026-01-23 12:45:51 +08:00
qaiu
2f22cb01eb LeTool乐云目录解析 2026-01-23 03:26:35 +08:00
qaiu
e17fb99de4 启用在线脚本解析器 2026-01-06 02:21:19 +08:00
q
0f926a57ef 更新Playground和JsHttpClient相关功能,整理文档结构 2026-01-06 00:00:37 +08:00
q
4380bfe0d6 Merge branch 'main' of github.com:qaiu/netdisk-fast-download 2026-01-04 09:30:54 +08:00
q
8127cd0758 fixed. 演练场静态打包问题 2026-01-04 09:29:20 +08:00
qaiu
71a220f42b 升级前端版本 2026-01-04 01:14:26 +08:00
qaiu
d3b02676ec Update README.md 2026-01-03 21:20:54 +08:00
q
d8f0dc4f8e 更新代码和文档 2026-01-03 21:11:04 +08:00
q
48aa5b6148 Add functional test report
- Document all completed tests and fixes
- Verify BUG1, BUG2, BUG3 fixes
- Confirm TypeScript removal
- Confirm text updates (JS演练场 → 脚本演练场)
- Service startup verification
2026-01-02 19:40:26 +08:00
q
a989841a89 Remove TypeScript-related code and documentation
- Remove TypeScript API endpoints from PlaygroundApi
- Remove TypeScript methods from DbService interface and implementation
- Delete PlaygroundTypeScriptCode model class
- Delete TypeScript documentation files
- Clean up unused imports
2026-01-02 19:27:21 +08:00
q
86783e8e46 Merge branch 'copilot/add-playground-enhancements' 2026-01-02 19:25:05 +08:00
q
66b9bcc53a Fix playground bugs and remove TypeScript compiler
- Fix BUG1: JavaScript timeout with proper thread interruption using ScheduledExecutorService
- Fix BUG2: Add URL regex validation before execution in playground test API
- Fix BUG3: Register published parsers to CustomParserRegistry on save/update/delete
- Remove TypeScript compiler functionality (tsCompiler.js, dependencies, UI)
- Add password authentication for playground access
- Add mobile responsive layout support
- Load playground parsers on application startup
2026-01-02 19:24:47 +08:00
qaiu
ff08615d1e 更新 README.md 2025-12-23 07:55:59 +08:00
qaiu
4a6c3a1f90 更新 README.md 2025-12-08 23:46:58 +08:00
copilot-swe-agent[bot]
c79702eba8 Address code review feedback: protect types.js endpoint and improve code readability
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-12-07 05:27:07 +00:00
copilot-swe-agent[bot]
41fc935c09 Fix JsonResult API calls and add documentation
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-12-07 05:24:38 +00:00
copilot-swe-agent[bot]
5fbbe5b240 Add playground loading animation, password auth, and mobile layout support
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-12-07 05:20:06 +00:00
copilot-swe-agent[bot]
9c121c03f2 Initial plan 2025-12-07 05:11:50 +00:00
copilot-swe-agent[bot]
b74c3f31c4 Add implementation summary in Chinese
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-12-07 04:57:03 +00:00
copilot-swe-agent[bot]
f23b97e22c Address code review feedback - improve code quality
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-12-07 04:52:22 +00:00
copilot-swe-agent[bot]
0560989e77 Complete TypeScript compiler integration with examples and documentation
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-12-07 04:48:38 +00:00
copilot-swe-agent[bot]
f2c9c34324 Add TypeScript compiler integration - core implementation
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-12-07 04:43:36 +00:00
copilot-swe-agent[bot]
a97268c702 Initial plan 2025-12-07 04:34:47 +00:00
copilot-swe-agent[bot]
2654b550fb Add comprehensive implementation summary
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-12-06 22:56:10 +00:00
copilot-swe-agent[bot]
12a5a17a30 Address code review feedback: fix Promise.race, improve statusText, use English error messages
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-12-06 22:52:37 +00:00
copilot-swe-agent[bot]
e346812c0a Complete backend implementation with comprehensive documentation
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-12-06 22:51:08 +00:00
copilot-swe-agent[bot]
6b2e391af9 Add fetch polyfill tests and documentation
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-12-06 22:49:32 +00:00
copilot-swe-agent[bot]
199456cb11 Implement fetch polyfill and Promise for ES5 backend
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-12-06 22:44:52 +00:00
copilot-swe-agent[bot]
636994387f Initial plan 2025-12-06 22:38:17 +00:00
qaiu
90c79f7bac Merge pull request #140 from rensumo/main
增加快速部署方式
2025-12-02 17:21:05 +08:00
rensumo
79601b36a5 增加快速部署 2025-12-02 14:43:29 +08:00
rensumo
96cef89f08 增加快速部署 2025-12-02 14:42:07 +08:00
rensumo
e057825b25 增加快速部署
Add quick deployment instructions and Docker deployment section.
2025-12-02 14:40:53 +08:00
qaiu
ebe848dfe8 Merge pull request #139 from Edakerx/main
Update README.md
2025-11-30 12:30:26 +08:00
Edakerx
e259a0989e Update README.md
添加赞助商
2025-11-30 12:20:07 +08:00
q
f750aa68e8 js演练场漏洞修复 2025-11-30 02:07:56 +08:00
q
49b8501e86 js演练场,ye2 2025-11-29 03:44:47 +08:00
q
fc2e2a4697 Merge remote-tracking branch 'origin/main' 2025-11-29 03:42:25 +08:00
q
b4b1d7f923 js演练场 2025-11-29 03:41:51 +08:00
q
df646b8c43 js演练场 2025-11-29 02:56:25 +08:00
qaiu
8e790f6b22 更新 README.md 2025-11-28 20:33:07 +08:00
q
2e76af980e front ver 0.1.9.b12 2025-11-28 19:50:29 +08:00
q
80ccbe5b62 fixed. 123跨区下载错误 2025-11-28 19:48:19 +08:00
q
aa0cd68f7f 客户端链接(实验性),js解析器插件,汽水音乐,一刻相册,咪咕音乐 2025-11-25 16:34:24 +08:00
qaiu
51833148b1 更新 README.md 2025-11-17 23:06:54 +08:00
q
0fa77ebf21 Merge remote-tracking branch 'origin/main' 2025-11-15 21:50:45 +08:00
q
584c075930 - [汽水音乐-qishui_music](https://music.douyin.com/qishui/)
- [咪咕音乐-migu](https://music.migu.cn/)
- [一刻相册-baidu_photo](https://photo.baidu.com/)
2025-11-15 21:49:40 +08:00
qaiu
9e7a3718a4 Update README with new links and information 2025-11-14 06:33:16 +08:00
q
0e2ca2f1ca ce盘优化 2025-11-13 19:32:44 +08:00
q
52e889333b Merge remote-tracking branch 'origin/main' 2025-11-13 18:20:32 +08:00
qaiu
4745440079 Merge pull request #136 from qaiu/copilot/add-ce4tool-parser
Add Cloudreve 4.x API support with Ce4Tool parser
2025-11-13 18:18:30 +08:00
q
b5628eac17 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	parser/src/test/java/cn/qaiu/parser/clientlink/impl/CurlLinkGeneratorTest.java
2025-11-13 17:58:59 +08:00
copilot-swe-agent[bot]
d23b11577e Simplify and optimize Ce4Tool and CeTool version detection logic
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-11-10 09:59:48 +00:00
copilot-swe-agent[bot]
f1dd9fc0ee Add Ce4Tool for Cloudreve 4.x API support and update CeTool with version detection
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-11-10 09:57:41 +00:00
copilot-swe-agent[bot]
0877fadcfb Initial planning for Cloudreve 4.x API support
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-11-10 09:53:58 +00:00
copilot-swe-agent[bot]
733059dc8e Initial plan 2025-11-10 09:50:49 +00:00
qaiu
321380c2b9 更新 README.md 2025-11-08 00:23:22 +08:00
qaiu
deb121a51b 更新 README.md 2025-11-08 00:20:30 +08:00
qaiu
b6aef7c239 更新 README.md 2025-11-08 00:19:16 +08:00
qaiu
b13a7a5ee1 更新 README.md
测试下载链接更新
2025-11-04 15:44:17 +08:00
qaiu
fff6a00690 更新 README.md
接口参考优化
2025-10-30 22:42:28 +08:00
qaiu
b4da3cee20 Merge pull request #135 from qaiu/copilot/remove-test-filelist
[WIP] Remove test filelist from repository
2025-10-30 20:24:45 +08:00
copilot-swe-agent[bot]
0a650996a1 Remove accidentally committed test-filelist.java file
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-10-30 12:21:01 +00:00
copilot-swe-agent[bot]
37b91cd388 Initial plan 2025-10-30 12:18:50 +00:00
q
42b721eabf feat: 新增客户端协议生成系统,支持8种主流下载工具
🚀 核心功能
- 新增完整的客户端下载链接生成器系统
- 支持ARIA2、Motrix、比特彗星、迅雷、wget、cURL、IDM、FDM、PowerShell等8种客户端
- 自动处理防盗链参数(User-Agent、Referer、Cookie等)
- 提供可扩展的生成器架构,支持自定义客户端

🔧 技术实现
- ClientLinkGeneratorFactory: 工厂模式管理生成器
- DownloadLinkMeta: 元数据存储下载信息
- ClientLinkUtils: 便捷工具类
- 线程安全的ConcurrentHashMap设计

🌐 前端集成
- 新增ClientLinks.vue界面,支持客户端链接展示
- Element Plus图标系统,混合图标显示
- 客户端检测逻辑优化,避免自动打开外部应用
- 移动端和PC端环境判断

📚 文档完善
- 完整的CLIENT_LINK_GENERATOR_GUIDE.md使用指南
- API文档和测试用例
- 输出示例和最佳实践

从单纯的网盘解析工具升级为完整的下载解决方案生态
2025-10-24 09:29:05 +08:00
q
231d5c3fb9 修复WPS域名匹配正则表达式
- 修复PWPS正则表达式,支持子域名匹配
- 从 https://www\.kdocs\.cn/l/(?<KEY>.+) 修改为 https://(?:[a-zA-Z\d-]+\.)?kdocs\.cn/l/(?<KEY>.+)
- 现在可以正确匹配带子域名的WPS链接,如 www.kdocs.cn
- 测试通过:WPS云文档解析功能正常工作
2025-10-23 09:20:26 +08:00
q
064efdf3f3 feat: 完善JavaScript解析器功能
- 优化JsScriptLoader,支持JAR包内和文件系统的自动资源文件发现
- 移除预定义文件列表,完全依赖自动检测
- 添加getNoRedirect方法支持重定向处理
- 添加sendMultipartForm方法支持文件上传
- 添加代理配置支持
- 修复JSON解析的压缩处理问题
- 添加默认请求头支持(Accept-Encoding、User-Agent、Accept-Language)
- 更新文档,修正导出方式说明
- 优化README.md结构,删除不符合模块定位的内容
- 升级parser版本到10.2.1
2025-10-22 17:34:19 +08:00
qaiu
7b364a0f90 更新 README.md 2025-10-22 12:27:01 +08:00
q
c8a4ca7f16 feat: 添加getNoRedirect方法支持302重定向处理
- 在JsHttpClient中添加getNoRedirect方法,支持不自动跟随重定向的HTTP请求
- 修改baidu-photo.js解析器,使用getNoRedirect获取真实的下载链接
- 更新测试用例断言,验证重定向处理功能正常工作
- 修复百度一刻相册解析器302重定向问题,现在能正确获取真实下载链接
2025-10-21 17:47:59 +08:00
qaiu
97627b824c 更新 README.md 2025-10-21 12:49:10 +08:00
q
6dbdc9bd90 优化工作流 2025-10-20 13:45:26 +08:00
q
4166ea10af 忽略package-lock.json 2025-10-20 13:42:44 +08:00
q
fa12ab2c51 Merge branch 'main' of github.com:qaiu/netdisk-fast-download 2025-10-20 13:38:07 +08:00
q
4fc4ed8640 feat: 添加 WPS 云文档/WPS 云盘解析支持 (closes #133)
- 新增 PwpsTool 解析器,支持 WPS 云文档直链获取
- 调用 WPS API: https://www.kdocs.cn/api/office/file/{shareKey}/download
- 前端添加 kdocs.cn 链接识别规则
- 前端预览功能优化:WPS 云文档直接使用原分享链接预览
- 后端预览接口特殊处理:判断 shareKey 以 pwps: 开头自动重定向
- 支持提取文件名和有效期信息
- 更新 README 文档,添加 WPS 云文档支持说明

Parser 模块设计:
- 遵循开放封闭原则,易于扩展新网盘
- 只需实现 IPanTool 接口和注册枚举即可
- 支持自定义域名解析和责任链模式

技术特性:
- 免登录获取下载直链
- 支持在线预览(利用 WPS 原生功能)
- 文件大小限制:10M(免费版)/2G(会员版)
- 初始空间:5G(免费版)
2025-10-20 13:33:53 +08:00
qaiu
48172f2769 Merge pull request #134 from qaiu/copilot/update-parser-documentation
更新parser文档:开发者应继承PanBase并添加WebClient请求流程说明
2025-10-18 07:11:52 +08:00
copilot-swe-agent[bot]
c7e6d68fbd Update parser documentation with PanBase inheritance and WebClient flow
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-10-17 23:03:39 +00:00
copilot-swe-agent[bot]
e6672a51c5 Initial plan 2025-10-17 22:59:33 +00:00
q
abde7841ac web展示内部版本号 2025-10-17 17:19:27 +08:00
q
8e661ed1c5 版本号,123文件信息解析支持 2025-10-17 16:41:02 +08:00
q
217cb3a776 parser v10.1.17发布到maven central 允许开发者依赖
1. 添加自定义解析器扩展和相关示例
2. 优化pom结构
2025-10-17 15:51:52 +08:00
q
b8c1bca900 parser v10.1.17发布到maven central 允许开发者依赖
1. 添加自定义解析器扩展和相关示例
2. 优化pom结构
2025-10-17 15:51:41 +08:00
q
5e09b8e92a parser v10.1.17发布到maven central 允许开发者依赖
1. 添加自定义解析器扩展和相关示例
2. 优化pom结构
2025-10-17 15:50:45 +08:00
q
c16bde6bb8 parser发布到maven central方便开发者依赖, pom文件结构调整 2025-10-16 18:08:03 +08:00
qaiu
eb06eb9f3d 更新 README.md 2025-10-11 00:23:41 +08:00
qaiu
0c49088098 更新 README.md
远期规划
2025-10-11 00:19:07 +08:00
qaiu
b970241a64 Merge pull request #132 from rensumo/main
更新linux部署的下载链接
2025-10-10 09:23:36 +08:00
rensumo
6c5aafc11e 更新linux部署的下载链接 2025-10-09 20:32:22 +08:00
qaiu
ca0846f4a7 Merge pull request #131 from rensumo/main
修复命令行部署自启动命令的错误
2025-10-08 21:42:47 +08:00
rensumo
14f7fcc5ad 修复命令行部署自启动命令的错误 2025-10-08 15:31:04 +08:00
q
23a18aba5c web version 2025-09-28 13:44:07 +08:00
q
2d5a79bb16 Fixed: lz规则更新 #129 #128 2025-09-28 13:38:58 +08:00
q
51e1bbefbb 升级netty依赖 2025-09-15 10:10:30 +08:00
q
6647fc5371 fixed. ye解析,去除正则匹配, 分享key去除后缀, #123, #125 2025-09-15 09:44:32 +08:00
q
b67544f0cd fixed. ye解析,去除正则匹配, #124,#125 2025-09-15 09:25:39 +08:00
qaiu
ef5826a73b Merge pull request #124 from qaiu/dependabot/npm_and_yarn/web-front/axios-1.12.0
Bump axios from 1.11.0 to 1.12.0 in /web-front
2025-09-12 17:41:37 +08:00
dependabot[bot]
a48adbd0df Bump axios from 1.11.0 to 1.12.0 in /web-front
Bumps [axios](https://github.com/axios/axios) from 1.11.0 to 1.12.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.11.0...v1.12.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.12.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-12 09:37:49 +00:00
q
5c60493a24 修复123解析 #123 2025-09-12 17:34:07 +08:00
q
55e6227de0 优化docker脚本5 2025-09-10 18:23:58 +08:00
q
24a7395004 优化docker脚本4 2025-09-10 18:16:37 +08:00
q
b2a7187fc5 优化docker脚本3 2025-09-10 18:10:51 +08:00
q
ace7cdc88e 优化docker脚本2 2025-09-10 18:05:14 +08:00
q
2e909b5868 优化docker脚本 2025-09-10 17:53:49 +08:00
q
de78bcbc98 docker多平台支持,add 树莓派 2025-09-10 17:45:27 +08:00
q
c560f0e902 docker多平台支持 2025-09-10 17:27:44 +08:00
q
88860c9302 docker多平台支持 2025-09-10 17:21:33 +08:00
q
ef65d0e095 Merge remote-tracking branch 'origin/main' 2025-09-10 17:10:52 +08:00
q
6438505f4a icloud国际版支持,QQ邮箱文件信息获取 2025-09-10 17:10:39 +08:00
qaiu
1be5030dd1 添加docker多平台支持 2025-09-10 17:06:40 +08:00
q
421b2f4a42 Merge remote-tracking branch 'origin/main' 2025-08-19 18:57:00 +08:00
q
a66bf84381 直链API添加文件信息
修复蓝奏目录文件大小处理报错问题 #120
2025-08-19 18:56:42 +08:00
qaiu
0c4d366d6d 更新 README.md 2025-08-14 19:36:09 +08:00
qaiu
a1d0a921fa 更新 README.md 2025-08-14 19:28:44 +08:00
q
2092230a61 更新文档 2025-08-14 15:27:03 +08:00
q
6e5ae6eff3 Merge remote-tracking branch 'origin/main' 2025-08-12 13:32:09 +08:00
q
4f8259d772 1. iz match fixed
2. redirect res content add "text/html; charset=utf-8"
2025-08-12 13:29:59 +08:00
qaiu
8b987d9824 Update README.md 2025-08-11 13:32:33 +08:00
q
e8ba451d18 build status 2025-08-11 13:26:06 +08:00
q
77758db463 Merge remote-tracking branch 'origin/main' 2025-08-11 13:23:59 +08:00
q
6c58598a8e 用户API 2025-08-11 13:23:30 +08:00
qaiu
3ac35230a3 Update README.md 2025-08-11 13:18:00 +08:00
q
ca91302d28 Merge remote-tracking branch 'origin/main' 2025-08-11 13:14:59 +08:00
q
e07272a5dc 添加支持 QQ闪传,微雨云,优化前端逻辑 2025-08-11 13:14:43 +08:00
qaiu
461305e1df Update README.md 2025-08-08 12:33:46 +08:00
q
8e8ab10a0f Merge remote-tracking branch 'origin/main' 2025-08-05 15:50:50 +08:00
q
e754326925 fixed p118 link 2025-08-05 15:50:32 +08:00
qaiu
4c92994c6f 更新 README.md 2025-08-05 13:14:09 +08:00
qaiu
66c57f47ac Merge pull request #113 from qaiu/dependabot/maven/org.apache.commons-commons-lang3-3.18.0
Bump org.apache.commons:commons-lang3 from 3.12.0 to 3.18.0
2025-07-30 09:17:42 +08:00
dependabot[bot]
ec689eadd8 Bump org.apache.commons:commons-lang3 from 3.12.0 to 3.18.0
Bumps org.apache.commons:commons-lang3 from 3.12.0 to 3.18.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-lang3
  dependency-version: 3.18.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 09:05:14 +00:00
qaiu
c1e15709a7 Merge pull request #110 from qaiu/dependabot/maven/core-database/org.apache.commons-commons-lang3-3.18.0
Bump org.apache.commons:commons-lang3 from 3.12.0 to 3.18.0 in /core-database
2025-07-21 17:02:12 +08:00
q
2848937ce7 Merge remote-tracking branch 'origin/main' 2025-07-18 13:00:33 +08:00
q
42ff0c21b2 1. 默认缓存时间修改
2. 文件夹解析异常处理
3. 首页优化
2025-07-18 13:00:12 +08:00
qaiu
3ed7e547e6 更新 README.md
联系方式更新
2025-07-16 13:30:47 +08:00
q
fad8e688df 首页样式优化 2025-07-15 18:09:11 +08:00
q
b2f2dcac4c Merge remote-tracking branch 'origin/main' 2025-07-14 16:16:13 +08:00
q
fcba78e977 启动参数优化 2025-07-14 16:16:00 +08:00
qaiu
77c9d777a1 更新 README.md 2025-07-12 13:47:03 +08:00
dependabot[bot]
4460659210 Bump org.apache.commons:commons-lang3 in /core-database
Bumps org.apache.commons:commons-lang3 from 3.12.0 to 3.18.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-lang3
  dependency-version: 3.18.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-12 01:16:21 +00:00
q
8631524107 移动端布局优化 2025-07-11 11:51:08 +08:00
q
0579588814 优化构建流v0.1.9b6b 2025-07-10 19:17:22 +08:00
q
df2bfb6ac7 优化构建流 2025-07-10 19:12:15 +08:00
q
517b6f8910 del 2025-07-10 19:07:02 +08:00
q
94a46d2833 目录解析支持优化 v0.1.9b6 2025-07-10 18:59:59 +08:00
q
1631a0faa1 目录解析支持优化 v0.1.9b5 2025-07-10 18:58:12 +08:00
qaiu
06d5943cb6 Merge remote-tracking branch 'origin/main' 2025-07-09 07:58:10 +08:00
qaiu
3095e13676 ye目录解析 2025-07-09 07:57:47 +08:00
qaiu
482cbce7e8 Update maven.yml 2025-07-09 07:09:58 +08:00
qaiu
ef2fc3ab98 lz目录解析预览 2025-07-09 07:06:12 +08:00
q
5b57b05eae 目录解析支持优化 v0.1.9b2 2025-07-08 18:58:38 +08:00
q
093579c6f5 Merge remote-tracking branch 'origin/main' 2025-07-08 18:57:17 +08:00
q
c2d4990d7f 目录解析支持优化 v0.1.9b2 2025-07-08 18:55:19 +08:00
qaiu
40e8380738 更新 README.md 2025-07-08 04:03:42 +08:00
qaiu
b716e1e861 更新 README.md 2025-07-08 04:02:50 +08:00
qaiu
8432d4952c 更新 README.md 2025-07-08 03:51:46 +08:00
qaiu
dd8f085f63 更新 README.md 2025-07-08 03:51:00 +08:00
qaiu
161ff8d8a3 更新 README.md 2025-07-08 03:48:19 +08:00
qaiu
1390cd0104 更新 README.md 2025-07-08 03:47:09 +08:00
qaiu
7a02b1e97f 更新 README.md 2025-07-08 03:46:19 +08:00
qaiu
036f107c90 更新 README.md
docker镜像更新
2025-07-08 03:20:18 +08:00
qaiu
5652383450 Update README.md 2025-07-08 02:23:01 +08:00
qaiu
9a047a5da0 更新 README.md 2025-07-04 19:38:18 +08:00
qaiu
8975743a37 更新 README.md 2025-07-04 19:34:53 +08:00
q
0e30eafe49 目录解析支持 2025-07-04 19:20:06 +08:00
q
7facb62f21 Merge remote-tracking branch 'origin/main' 2025-07-04 19:17:35 +08:00
q
30d43cb961 目录解析支持 2025-07-04 19:16:36 +08:00
q
c505b17e35 目录解析支持 2025-07-04 19:11:39 +08:00
qaiu
080c4c753d Create update-release-badge.yml 2025-07-04 09:34:22 +08:00
q
ade0d34d91 start 2025-07-02 18:40:32 +08:00
qaiu
56d082eb0b Update maven.yml
提交任意tag 触发工作流
2025-07-02 18:36:45 +08:00
qaiu
795c4529ba Update AppMain.java 2025-07-02 18:29:46 +08:00
qaiu
0f5cfe22ea Update maven.yml
构建docker添加tag版本
2025-07-02 18:26:48 +08:00
qaiu
925ad2c3a5 Update AppMain.java 2025-07-02 18:20:30 +08:00
qaiu
f3e96907fe Update maven.yml
docker镜像构建添加版本号
2025-07-02 18:19:01 +08:00
qaiu
75a1e58a7d Update AppMain.java 2025-07-02 18:13:19 +08:00
yyzy-official
379e889f71 Update maven.yml
修改工作流版本号问题
2025-07-02 18:09:48 +08:00
yyzy-official
40c06f397b Update README.md
收款码隐藏
2025-07-02 17:41:23 +08:00
yyzy-official
9e9302436e Update README.md
1. 微信联系方式
2. 预览地址
2025-07-02 17:39:14 +08:00
qaiu
6d816d4193 Update README.md 2025-07-02 15:50:29 +08:00
qaiu
438eda9c08 Update README.md 2025-07-02 15:46:55 +08:00
q
ace39e4633 jdk24适配 2025-06-17 15:48:52 +08:00
qaiu
7712391f29 更新 README.md 2025-06-06 07:17:49 +08:00
qaiu
65f08dcb02 Update README.md 2025-06-04 16:28:50 +08:00
qaiu
1d332aa6f4 Update README.md 2025-06-04 16:26:56 +08:00
QAIU
ba81641517 1. 修改docker构建失败 2025-06-04 15:37:50 +08:00
qaiu
fb30bdb879 Update README.md 2025-06-04 15:27:21 +08:00
qaiu
fc451d3b41 Update README.md 文件夹解析说明 2025-06-04 15:25:26 +08:00
QAIU
ffee1f3462 1. 修复 小飞机解析错误#106
2. 添加 123云盘文件夹分享下解析为压缩包下载直链
3. 添加 123云盘全部域名支持: "www.123pan.com","www.123pan.cn","www.123865.com","www.123684.com","www.123912.com","www.123pan.cn"
2025-06-04 15:13:05 +08:00
QAIU
f30027dd13 1. 蓝奏云域名适配 2025-05-28 17:01:50 +08:00
QAIU
8b6aad17f4 0 2025-05-08 18:18:52 +08:00
QAIU
b77930adfb remove yarn.lock 2025-05-08 18:18:15 +08:00
qaiu
aff8f88076 Update README.md 2025-04-23 14:41:52 +08:00
qaiu
4e6582e24c Update nfd-service-template.xml 2025-04-23 14:31:56 +08:00
qaiu
fa9acaccfd Update README.md 2025-04-07 13:29:43 +08:00
QAIU
0414f85f12 115域名变动,360pan暂不可用,ctfile域名变动 2025-04-03 10:57:28 +08:00
QAIU
527dd0eeb4 城通分享格式适配, 优化日志打印 2025-03-28 17:59:28 +08:00
QAIU
74ed7475c9 json异常时, 快速失败 2025-03-24 13:35:40 +08:00
qaiu
54dc3dba96 蓝奏云随机404 问题修复 2025-03-22 12:53:33 +08:00
qaiu
9980159090 更新 README.md 2025-03-17 19:24:38 +08:00
QAIU
0b193ebb00 Merge remote-tracking branch 'origin/main' 2025-03-14 14:03:47 +08:00
QAIU
f5fc9843b2 微信版QQ邮箱中转站解析优化(已改名为QQ邮箱云盘) 2025-03-14 14:03:29 +08:00
qaiu
df1f67dd26 Update README.md 2025-02-25 10:29:25 +08:00
qaiu
b069a5f576 Update README.md 2025-02-25 10:26:47 +08:00
qaiu
7686763a03 Update README.md 2025-02-25 10:25:35 +08:00
qaiu
635a6eac37 Update README.md 2025-02-25 10:02:23 +08:00
QAIU
877edc535f md 2025-02-24 17:39:23 +08:00
QAIU
01d59e3c1e add 超星盘,360盘 2025-02-22 16:55:00 +08:00
QAIU
fece2799e3 Merge remote-tracking branch 'origin/main' 2025-02-21 18:28:08 +08:00
QAIU
de9756ee86 优化细节 2025-02-21 18:27:52 +08:00
qaiu
51f047a51b 更新 README.md 2025-02-20 18:11:39 +08:00
qaiu
04b66e82b7 Update README.md 2025-02-20 18:06:58 +08:00
qaiu
df89253647 Update README.md 2025-02-20 17:47:00 +08:00
qaiu
45dbca794e 更新 README.md 2025-02-13 12:14:59 +08:00
qaiu
857bf28f99 Update README.md 2025-02-12 16:25:16 +08:00
QAIU
e07ce15228 新增文件列表解析接口/redirectUrl/:type/:param 2025-02-10 14:19:18 +08:00
QAIU
0637bcfd8e 新增文件列表解析接口/v2/getFileList?url= 2025-02-07 19:28:09 +08:00
QAIU
23db0563ac 新增文件列表解析接口/v2/getFileList?url= 2025-02-07 19:27:48 +08:00
QAIU
ccba71aa4e MySQL支持, 其他优化 2025-02-06 16:54:54 +08:00
QAIU
fee4bf2ad6 MySQL支持, 其他优化 2025-02-06 16:52:22 +08:00
QAIU
5052fea9ef MySQL支持, 其他优化 2025-02-06 16:52:06 +08:00
qaiu
e85215fca1 Update README.md 2025-02-06 15:51:46 +08:00
qaiu
e42fe45329 Update README.md 2025-02-06 15:50:26 +08:00
qaiu
4240815bd1 Create FUNDING.yml 2025-02-05 11:11:01 +08:00
qaiu
6f0c5305e2 更新 README.md 2025-01-26 16:26:50 +08:00
qaiu
757005cad8 更新 README.md 2025-01-26 16:09:45 +08:00
qaiu
81651ad97c 更新 README.md 2025-01-26 15:31:43 +08:00
qaiu
f3763b6058 优化内核, QQ邮箱微信账户分享,添加123请求header 2025-01-24 19:21:58 +08:00
qaiu
82478dc485 add: 网易云音乐云盘分享URL支持 2025-01-23 17:23:58 +08:00
qaiu
703fd05d43 Update README.md 2025-01-10 14:33:30 +08:00
qaiu
ff868b6e2a Update README.md 2025-01-10 14:21:00 +08:00
qaiu
051a74b37b Update README.md 2025-01-10 14:19:59 +08:00
qaiu
a0a1085623 Update README.md 2025-01-10 14:14:20 +08:00
qaiu
2612d3919c Update README.md 2025-01-10 14:12:55 +08:00
qaiu
6f123a236f Update README.md 2025-01-10 14:12:04 +08:00
QAIU
71e57e6a08 Merge remote-tracking branch 'origin/main' 2025-01-07 15:04:07 +08:00
QAIU
7cb18d8186 remove yarn.lock 2025-01-07 15:03:53 +08:00
qaiu
cdbf670ece Update maven.yml 2025-01-07 15:00:18 +08:00
QAIU
e0dafee617 remove yarn.lock 2025-01-07 14:48:16 +08:00
QAIU
c37bce1563 优化解析器链接识别 2025-01-07 13:19:40 +08:00
QAIU
0b3c77d644 115pan 2025-01-07 11:13:52 +08:00
QAIU
2cf85caf86 115pan分享识别优化 2025-01-06 18:01:23 +08:00
QAIU
594010ba88 代理服务配置优化 2025-01-04 17:38:33 +08:00
QAIU
d91460d2e2 修复蓝奏优享解析失败, gz压缩的json解析 2025-01-04 15:21:15 +08:00
QAIU
89713e6ac9 修复蓝奏优享解析失败 2025-01-04 14:21:32 +08:00
QAIU
17c9b2538c Merge remote-tracking branch 'origin/main' 2025-01-04 14:18:46 +08:00
QAIU
d337b003cb 常规测试 2024-12-30 18:11:53 +08:00
qaiu
8f1485656b Update README.md
add docker加速地址
2024-12-23 13:05:25 +08:00
qaiu
f0c4ec3031 Update maven.yml 2024-12-23 13:02:51 +08:00
qaiu
458be84aca Update maven.yml 2024-12-23 13:00:28 +08:00
qaiu
c7716aad34 Update README.md 2024-12-18 13:05:16 +08:00
QAIU
4a3e734408 1. onedrive
常规测试
2024-12-18 11:46:06 +08:00
qaiu
54cc212753 Merge pull request #78 from xrgzs/add-ghcr
增加 ghcr.io 容器构建
2024-12-17 16:24:03 +08:00
xrgzs
f4ae1eaa51 PR时不更新依赖图 2024-12-17 16:15:59 +08:00
xrgzs
d2537282c9 增加 ghcr.io 容器构建 2024-12-17 15:47:05 +08:00
QAIU
87527688c3 1. 代理配置 2024-12-17 15:21:59 +08:00
qaiu
2be0b6505a 更新 P115Tool.java
UA问题说明
2024-12-17 09:37:48 +08:00
QAIU
672f100c7c 1. 动态UA 2024-12-16 19:01:55 +08:00
QAIU
5af402c0c5 1. 动态UA 2024-12-16 18:52:02 +08:00
QAIU
693a4f0f63 1. P115网盘解析BUG
2. 完善onedrive支持
2024-12-16 15:54:06 +08:00
QAIU
f8d2426ff6 API 2024-12-16 13:19:15 +08:00
QAIU
973a9bedcd 添加115网盘支持(测试中)#75 2024-12-16 13:15:53 +08:00
QAIU
a583733400 修复小飞机解析失败 2024-12-16 12:31:34 +08:00
QAIU
78eb51b3ca 处理编译失败问题 2024-11-29 11:50:44 +08:00
QAIU
a2606be9d8 修复蓝奏优享#71 2024-11-26 13:02:02 +08:00
qaiu
a4975c72ce Merge pull request #70 from qaiu/dependabot/npm_and_yarn/web-front/eslint/plugin-kit-0.2.3
Bump @eslint/plugin-kit from 0.2.2 to 0.2.3 in /web-front
2024-11-18 12:12:25 +08:00
dependabot[bot]
58f96822a4 Bump @eslint/plugin-kit from 0.2.2 to 0.2.3 in /web-front
Bumps [@eslint/plugin-kit](https://github.com/eslint/rewrite) from 0.2.2 to 0.2.3.
- [Release notes](https://github.com/eslint/rewrite/releases)
- [Changelog](https://github.com/eslint/rewrite/blob/main/release-please-config.json)
- [Commits](https://github.com/eslint/rewrite/compare/plugin-kit-v0.2.2...plugin-kit-v0.2.3)

---
updated-dependencies:
- dependency-name: "@eslint/plugin-kit"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-15 21:50:27 +00:00
qaiu
96b0d94986 Update README.md 2024-11-13 13:59:54 +08:00
QAIU
70b38db8c5 IP互助计划, 添加正向代理服务(TODO) 2024-11-12 19:05:43 +08:00
QAIU
b6a9c2d3a0 . 2024-11-07 18:37:08 +08:00
QAIU
a01df6c7db web file package config 2024-11-07 12:34:24 +08:00
QAIU
4455bee570 pod update 2024-11-05 18:42:32 +08:00
qaiu
cd0adef2ed Merge pull request #67 from qaiu/dependabot/npm_and_yarn/web-front/http-proxy-middleware-2.0.7
Bump http-proxy-middleware from 2.0.6 to 2.0.7 in /web-front
2024-11-04 19:13:24 +08:00
dependabot[bot]
4aa24a65fb Bump http-proxy-middleware from 2.0.6 to 2.0.7 in /web-front
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.6 to 2.0.7.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.7/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.6...v2.0.7)

---
updated-dependencies:
- dependency-name: http-proxy-middleware
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-04 10:58:07 +00:00
QAIU
760dca8772 升级vue3 2024-11-04 18:56:49 +08:00
QAIU
8269673619 1. add iCloud解析 2024-11-04 14:18:56 +08:00
qaiu
82ec586554 手残 又不小心提交了 ads代码 2024-11-02 18:59:33 +08:00
qaiu
ca98cc8708 里程碑版本前奏: 1. 添加google云盘解析(需要联网), 2. web页面人工智障自动解析URL, 3. 优化一堆细节问题, 4: git换行配置(待验证), 5. 酷我解析可用 2024-11-02 18:58:15 +08:00
QAIU
f07800985d 1. 优化123pan日志, 2. 微博短链测试, 3. 分享类添加其他参数Map(Cookie支持准备) 2024-11-01 18:18:29 +08:00
qaiu
b042df93b7 更新 urltool.py 2024-10-29 18:59:23 +08:00
QAIU
ecf4441946 .. 2024-10-29 18:36:51 +08:00
QAIU
39b2612840 .. 2024-10-28 18:59:18 +08:00
QAIU
218f486e6b .. 2024-10-28 18:58:37 +08:00
QAIU
cfcc25f175 add onedrive 2024-10-28 18:57:26 +08:00
qaiu
155e88223c 0.0 前端优化,302标识短链添加/d前缀 2024-10-28 14:36:39 +08:00
QAIU
05039ece51 add 118, 微雨云 2024-10-26 16:04:49 +08:00
QAIU
1c673f2b46 idea run Main. 2024-10-25 18:18:42 +08:00
QAIU
2232a70228 Merge remote-tracking branch 'origin/main' 2024-10-25 18:18:34 +08:00
QAIU
e661b1d817 idea run Main. 2024-10-25 18:18:17 +08:00
qaiu
5a6a65f580 Update README.md 2024-10-25 14:54:19 +08:00
qaiu
5cdd3bcd30 Update README.md 2024-10-25 14:52:36 +08:00
qaiu
1233a885b8 Update README.md 2024-10-25 14:49:09 +08:00
QAIU
adf56cd768 add 酷狗音乐, 酷我音乐, 网易云音乐, QQ音乐 2024-10-25 14:38:57 +08:00
QAIU
cd4b208be9 一处SQL语法错误 2024-10-24 10:48:36 +08:00
QAIU
502de1a5d0 0..0 2024-10-23 18:08:10 +08:00
qaiu
4158f869a3 0.0 2024-10-23 18:04:34 +08:00
QAIU
ff569d339c 1. add music parser 2024-10-21 19:14:29 +08:00
QAIU
10eec323dd 1. add 网易云音乐解析 2024-10-20 18:16:51 +08:00
qaiu
0a3db51c7d 更新 app-dev.yml 蓝奏设置合理缓存时间 2024-10-11 08:15:10 +08:00
qaiu
229aee0b30 Update README.md 2024-10-09 15:38:29 +08:00
QAIU
44714aa981 1. add 城通网盘解析(慢速) https://www.ctfile.com
2. 优化解析接口的实现
2024-10-09 15:33:33 +08:00
qaiu
2b6138a889 前端打包说明 2024-10-08 03:01:29 +08:00
qaiu
5e424f7bf4 前端打包说明 2024-10-08 02:13:50 +08:00
qaiu
294e47deed 1. 启用内嵌静态页面, 2. 蓝奏域名规则优化, 3. 反向代理优化, 4. 修复一堆细节问题 2024-10-08 02:06:37 +08:00
qaiu
dc42547b73 更新 README.md 2024-10-07 19:32:22 +08:00
qaiu
7ef7f0706b 更新 PanDomainTemplate.java
蓝奏匹配正则优化
2024-10-06 15:19:51 +08:00
qaiu
a59b98a7c9 更新 PanDomainTemplate.java 2024-10-06 15:04:13 +08:00
qaiu
088fee9a4d 1. add:123云盘的新域名
2. update: 统计API支持ce盘
3. 缓存时长
2024-10-01 17:38:57 +08:00
QAIU
d8666acfe8 国庆快乐 ^ ^ #59 2024-09-30 17:42:38 +08:00
QAIU
209e9c2866 国庆快乐 ^ ^ #59 2024-09-30 17:38:36 +08:00
qaiu
6c3195dea4 更新 README.md 2024-09-29 22:11:04 +08:00
qaiu
7d774a7433 更新 README.md 2024-09-29 22:10:23 +08:00
qaiu
f1ec4433cf 更新 README.md 2024-09-29 22:09:58 +08:00
QAIU
1f825db261 update 支持Cloudreve任意https的80端口的域名, 修复因json异常解析导致的解析时间超时的问题 2024-09-25 20:01:10 +08:00
QAIU
1019f24f1d update web-front README.md 2024-09-24 18:05:30 +08:00
QAIU
f5c5b99579 1. 修改文叔叔链接匹配规则 2024-09-24 17:55:18 +08:00
QAIU
e002d19f1b 1. 前端页面优化, 增强统计功能, 支持生成二维码
2. 添加统计接口
2024-09-24 17:08:29 +08:00
qaiu
0d5c9651f0 Update README.md 2024-09-24 10:07:25 +08:00
qaiu
53fc13b95c Merge pull request #56 from qaiu/dependabot/npm_and_yarn/web-front/micromatch-4.0.8
Bump micromatch from 4.0.5 to 4.0.8 in /web-front
2024-09-24 00:09:00 +08:00
QAIU
694c3b0ddc 修复移动云空间无法解析的前端问题, 更新前端依赖 2024-09-23 18:33:09 +08:00
dependabot[bot]
9b3d4577cc Bump micromatch from 4.0.5 to 4.0.8 in /web-front
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

---
updated-dependencies:
- dependency-name: micromatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-23 08:46:39 +00:00
QAIU
77783915dd 修复移动云空间无法解析的前端问题, 更新前端依赖 2024-09-23 16:45:30 +08:00
qaiu
b67ac21a79 Update README.md add 宝塔安装教程 2024-09-22 17:27:54 +08:00
qaiu
603afed2f2 . 2024-09-19 05:34:34 +00:00
qaiu
c2a7c34496 . 2024-09-19 05:31:24 +00:00
qaiu
edd40f48ba 依赖更新 2024-09-19 05:26:31 +00:00
qaiu
cca3d6b8b9 web-front add yarn.lock 2024-09-19 05:22:55 +00:00
qaiu
f004512903 Update devcontainer.json 2024-09-19 09:37:01 +08:00
qaiu
6407bb6730 Merge pull request #47 from qaiu/dependabot/npm_and_yarn/web-front/webpack-5.94.0
Bump webpack from 5.88.2 to 5.94.0 in /web-front
2024-09-19 09:01:53 +08:00
qaiu
b914eeadec Merge pull request #51 from qaiu/dependabot/npm_and_yarn/web-front/multi-d66d039ac5
Bump serve-static and express in /web-front
2024-09-19 09:01:04 +08:00
qaiu
dcadc6783e Merge pull request #52 from qaiu/dependabot/npm_and_yarn/web-front/express-4.21.0
Bump express from 4.19.2 to 4.21.0 in /web-front
2024-09-19 09:00:27 +08:00
dependabot[bot]
bc9f43634f Bump express from 4.19.2 to 4.21.0 in /web-front
Bumps [express](https://github.com/expressjs/express) from 4.19.2 to 4.21.0.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-19 00:59:00 +00:00
dependabot[bot]
4778f0164c Bump serve-static and express in /web-front
Bumps [serve-static](https://github.com/expressjs/serve-static) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `serve-static` from 1.15.0 to 1.16.2
- [Release notes](https://github.com/expressjs/serve-static/releases)
- [Changelog](https://github.com/expressjs/serve-static/blob/v1.16.2/HISTORY.md)
- [Commits](https://github.com/expressjs/serve-static/compare/v1.15.0...v1.16.2)

Updates `express` from 4.19.2 to 4.21.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0)

---
updated-dependencies:
- dependency-name: serve-static
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-19 00:58:59 +00:00
qaiu
9904754a07 Merge pull request #48 from qaiu/dependabot/npm_and_yarn/web-front/axios-1.7.4
Bump axios from 1.6.0 to 1.7.4 in /web-front
2024-09-19 08:57:43 +08:00
qaiu
1b79077c9e 更新 PanDomainTemplate.java 2024-09-19 06:20:12 +08:00
qaiu
c13afb05b3 示例下载链接修改 2024-09-19 05:43:54 +08:00
qaiu
03e320efb8 Create devcontainer.json 2024-09-18 17:40:52 +08:00
qaiu
7846332476 Update README.md 2024-09-18 16:58:27 +08:00
qaiu
2d5d3b86e0 Update README.md 2024-09-18 16:58:02 +08:00
qaiu
7c9ba890af 1. .. 2024-09-18 15:45:49 +08:00
qaiu
0d609daffa 1. 123后缀处理 2. 修复缓存时间戳格式问题 2024-09-18 15:41:56 +08:00
qaiu
c12e56d402 Update README.md 2024-09-18 13:34:53 +08:00
qaiu
c7b38c07d5 Update README.md 2024-09-18 13:34:26 +08:00
qaiu
dc51066cea 1. 缓存优化 2024-09-18 13:28:20 +08:00
qaiu
59d2fb3010 1. 缓存优化 2024-09-18 13:24:33 +08:00
qaiu
a0fe702c10 1. remove error update 2024-09-15 06:54:55 +08:00
qaiu
f886f7e366 1. 添加缓存
2. 优化解析架构
3. 优化核心模块
2024-09-15 06:53:11 +08:00
dependabot[bot]
1d475d88ed Bump axios from 1.6.0 to 1.7.4 in /web-front
Bumps [axios](https://github.com/axios/axios) from 1.6.0 to 1.7.4.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.6.0...v1.7.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-30 18:09:12 +00:00
dependabot[bot]
e64c901912 Bump webpack from 5.88.2 to 5.94.0 in /web-front
Bumps [webpack](https://github.com/webpack/webpack) from 5.88.2 to 5.94.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.88.2...v5.94.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-30 18:07:43 +00:00
qaiu
5fce02e623 Update README.md 2024-07-13 12:50:02 +08:00
qaiu
13997bc543 Update README.md 2024-06-19 10:04:04 +08:00
qaiu
3e05b0d6f9 Update README.md 2024-06-19 10:03:53 +08:00
qaiu
966417f867 Merge pull request #45 from qaiu/dependabot/npm_and_yarn/web-front/multi-d7cccafd4e
Bump braces and filemanager-webpack-plugin in /web-front
2024-06-14 12:14:05 +08:00
dependabot[bot]
601a0d1b91 Bump braces and filemanager-webpack-plugin in /web-front
Bumps [braces](https://github.com/micromatch/braces) to 3.0.3 and updates ancestor dependency [filemanager-webpack-plugin](https://github.com/gregnb/filemanager-webpack-plugin). These dependencies need to be updated together.


Updates `braces` from 3.0.2 to 3.0.3
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

Updates `filemanager-webpack-plugin` from 2.0.5 to 8.0.0
- [Release notes](https://github.com/gregnb/filemanager-webpack-plugin/releases)
- [Changelog](https://github.com/gregnb/filemanager-webpack-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gregnb/filemanager-webpack-plugin/compare/v2.0.5...v8.0.0)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
- dependency-name: filemanager-webpack-plugin
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 06:54:54 +00:00
24 changed files with 2203 additions and 722 deletions

8
.gitignore vendored
View File

@@ -80,6 +80,12 @@ yarn-error.log*
*.iml
*.ipr
*.iws
# Build directories
**/target/
**/${project.build.directory}/
**/build/
**/classes/
**/out/
**/${project.build.directory}/
**/${project.basedir}/target/
**/${basedir}/target/

View File

@@ -67,7 +67,6 @@ main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/
- [亿方云-fc](https://www.fangcloud.com/)
- [123云盘-ye](https://www.123pan.com/)
- ~[115网盘(失效)-p115](https://115.com/)~
- ~[118网盘(已停服)-p118](https://www.118pan.com/)~
- [文叔叔-ws](https://www.wenshushu.cn/)
- [联想乐云-le](https://lecloud.lenovo.com/)
- [QQ邮箱云盘-qqw](https://mail.qq.com/)
@@ -143,7 +142,7 @@ GET /json/getFileList?url={分享链接}&pwd={密码}
- `your_host` 替换为您的域名或 IP
### 认证参数v0.2.1+
[可以使用在线认证参数加密](https://qaiu.top/nfd-auth.html)
部分网盘如夸克、UC需要登录后的 Cookie 才能解析和下载。可通过 `auth` 参数传递认证信息:
**参数格式**`auth` 参数值为 AES 加密后的 JSON 字符串,经过 Base64 编码和 URL 编码
@@ -179,6 +178,18 @@ GET /parser?url={分享链接}&pwd={密码}&auth={加密后的认证参数}
```
> 💡 提示Web 界面已内置认证配置功能,可自动处理加密过程,无需手动构造参数。
> [可以使用在线认证参数加密](https://qaiu.top/nfd-auth.html)
#### 密钥作用说明
- `server.authEncryptKey`
- 作用:用于 `auth` 参数的 AES 加解密
- 要求16位AES-128
- `server.donatedAccountFailureTokenSignKey`
- 作用:用于“捐赠账号失败计数 token”的 HMAC 签名/验签
- 目的:防止客户端伪造失败计数请求
- 建议:使用高强度随机字符串,且不要与 `authEncryptKey` 相同
### 特殊说明

View File

@@ -318,6 +318,7 @@ public class RouterHandlerFactory implements BaseHttpApi {
// 只处理POST/PUT/PATCH等有body的请求方法避免GET请求读取body导致"Request has already been read"错误
String httpMethod = ctx.request().method().name();
if (("POST".equals(httpMethod) || "PUT".equals(httpMethod) || "PATCH".equals(httpMethod))
&& ctx.parsedHeaders() != null && ctx.parsedHeaders().contentType() != null
&& HttpHeaderValues.APPLICATION_JSON.toString().equals(ctx.parsedHeaders().contentType().value())
&& ctx.body() != null && ctx.body().asJsonObject() != null) {
JsonObject body = ctx.body().asJsonObject();
@@ -340,8 +341,12 @@ public class RouterHandlerFactory implements BaseHttpApi {
});
}
} else if (("POST".equals(httpMethod) || "PUT".equals(httpMethod) || "PATCH".equals(httpMethod))
&& ctx.body() != null) {
&& ctx.body() != null && ctx.body().length() > 0) {
try {
queryParams.addAll(ParamUtil.paramsToMap(ctx.body().asString()));
} catch (Exception e) {
LOGGER.debug("Failed to parse body as params: {}", e.getMessage());
}
}
// 解析其他参数
@@ -360,6 +365,12 @@ public class RouterHandlerFactory implements BaseHttpApi {
parameterValueList.put(k, ctx.request());
} else if (HttpServerResponse.class.getName().equals(v.getRight().getName())) {
parameterValueList.put(k, ctx.response());
} else if (JsonObject.class.getName().equals(v.getRight().getName())) {
if (ctx.body() != null && ctx.body().asJsonObject() != null) {
parameterValueList.put(k, ctx.body().asJsonObject());
} else {
parameterValueList.put(k, new JsonObject());
}
} else if (parameterValueList.get(k) == null
&& CommonUtil.matchRegList(entityPackagesReg.getList(), v.getRight().getName())) {
// 绑定实体类
@@ -374,6 +385,17 @@ public class RouterHandlerFactory implements BaseHttpApi {
});
// 调用handle 获取响应对象
Object[] parameterValueArray = parameterValueList.values().toArray(new Object[0]);
// 打印调试信息,确认参数注入的情况
if (LOGGER.isDebugEnabled() && method.getName().equals("donateAccount")) {
LOGGER.debug("donateAccount parameter list:");
int i = 0;
for (Map.Entry<String, Object> entry : parameterValueList.entrySet()) {
LOGGER.debug("Param [{}]: {} = {}", i++, entry.getKey(),
entry.getValue() != null ? entry.getValue().toString() : "null");
}
}
try {
// 反射调用
Object data = ReflectionUtil.invokeWithArguments(method, instance, parameterValueArray);

View File

@@ -48,6 +48,9 @@ public class RouterVerticle extends AbstractVerticle {
} else {
options = new HttpServerOptions();
}
// 绑定到 0.0.0.0 以允许外部访问
options.setHost("0.0.0.0");
options.setPort(port);
server = vertx.createHttpServer(options);

5
package.json Normal file
View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"mvn": "^3.5.0"
}
}

View File

@@ -36,6 +36,22 @@ URL解码 → Base64解码 → AES解密 → JSON对象
- **密钥长度**: 16位128位
- **默认密钥**: `nfd_auth_key2026`(可在 `app-dev.yml` 中通过 `server.authEncryptKey` 配置)
### 密钥作用说明(重要)
当前系统中涉及两类不同用途的密钥:
1. `server.authEncryptKey`
- 用途:加解密 `auth` 参数(前端/调用方传入的认证信息)
- 影响范围:`/parser``/json/parser``/v2/linkInfo` 等接口中的 `auth` 参数
- 注意:这是 **AES 对称加密密钥**,要求 16 位
2. `server.donatedAccountFailureTokenSignKey`
- 用途:签名和验签“捐赠账号失败计数 token”用于防伪造、失败计数
- 影响范围:捐赠账号失败计数与自动失效逻辑
- 注意:这是 **HMAC 签名密钥**,与 `authEncryptKey` 已解耦,建议使用高强度随机字符串
> 建议:生产环境务必同时自定义这两个密钥,且不要设置为相同值。
## JSON 模型定义
### AuthParam 对象
@@ -301,14 +317,25 @@ if (auths != null) {
## 配置说明
`app-dev.yml` 中配置加密密钥:
`app-dev.yml` 中配置密钥:
```yaml
server:
# auth参数加密密钥16位AES密钥
authEncryptKey: 'your_custom_key16'
# 捐赠账号失败计数token签名密钥HMAC
# 建议使用较长随机字符串,并与 authEncryptKey 不同
donatedAccountFailureTokenSignKey: 'your_random_hmac_sign_key'
```
### 密钥管理建议
- 不要在公开仓库提交生产密钥
- 建议通过环境变量或私有配置注入
- 调整 `authEncryptKey` 会影响 `auth` 参数兼容性
- 调整 `donatedAccountFailureTokenSignKey` 会使已签发的失败计数 token 失效(短期可接受)
## 更新日志
- **2026-02-05**: 初始版本,支持 accesstoken、cookie、password、custom 认证类型

View File

@@ -218,11 +218,6 @@ public enum PanDomainTemplate {
"(?<KEY>[0-9a-zA-Z_-]+)(\\?p=(?<PWD>\\w+))?"),
"https://474b.com/file/{shareKey}",
CtTool.class),
// https://xxx.118pan.com/bxxx
P118("118网盘",
compile("https://(?:[a-zA-Z\\d-]+\\.)?118pan\\.com/b(?<KEY>.+)"),
"https://qaiu.118pan.com/b{shareKey}",
P118Tool.class),
// https://www.vyuyun.com/s/QMa6ie?password=I4KG7H
// https://www.vyuyun.com/s/QMa6ie/file?password=I4KG7H
PVYY("微雨云存储",

View File

@@ -0,0 +1,55 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.IPanTool;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
/**
* 蓝奏云优享解析器选择器
* 根据配置的鉴权方式选择不同的解析器:
* - 如果配置了 username 和 password则使用 IzToolWithAuth (支持大文件)
* - 否则使用 IzTool (免登录,仅支持小文件)
*/
public class IzSelectorTool implements IPanTool {
private final IPanTool selectedTool;
public IzSelectorTool(ShareLinkInfo shareLinkInfo) {
if (shareLinkInfo.getOtherParam().containsKey("auths")) {
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
// 检查是否配置了账号密码
if (auths.contains("username") && auths.contains("password")) {
String username = auths.get("username");
String password = auths.get("password");
if (StringUtils.isNotEmpty(username) && StringUtils.isNotEmpty(password)) {
// 使用 IzToolWithAuth (账密登录,支持大文件)
this.selectedTool = new IzToolWithAuth(shareLinkInfo);
return;
}
}
}
// 无认证信息或认证信息无效,使用免登录版本(仅支持小文件)
this.selectedTool = new IzTool(shareLinkInfo);
}
@Override
public Future<String> parse() {
return selectedTool.parse();
}
@Override
public Future<List<FileInfo>> parseFileList() {
return selectedTool.parseFileList();
}
@Override
public Future<String> parseById() {
return selectedTool.parseById();
}
}

View File

@@ -5,35 +5,50 @@ import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.AESUtils;
import cn.qaiu.util.AcwScV2Generator;
import cn.qaiu.util.CommonUtils;
import cn.qaiu.util.FileSizeConverter;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.Promise;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpRequest;
import io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClientSession;
import io.vertx.uritemplate.UriTemplate;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* 蓝奏云优享
* v019b22
*
*/
public class IzTool extends PanBase {
WebClientSession webClientSession = WebClientSession.create(clientNoRedirects);
private static final String API_URL0 = "https://api.ilanzou.com/";
private static final String API_URL_PREFIX = "https://api.ilanzou.com/unproved/";
private static final String FIRST_REQUEST_URL = API_URL_PREFIX + "recommend/list?devType=6&devModel=Chrome" +
"&uuid={uuid}&extra=2&timestamp={ts}&shareId={shareId}&type=0&offset=1&limit=60";
private static final String LOGIN_URL = API_URL_PREFIX +
"login?uuid={uuid}&devType=6&devCode={uuid}&devModel=chrome&devVersion=127&appVersion=&timestamp={ts}&appToken=&extra=2";
// https://api.ilanzou.com/proved/user/info/map?devType=3&devModel=Chrome&uuid=TInRHH3QzRaMo-Ajl2PkJ&extra=2&timestamp=EC2C6E7F45EB21338A17A7621E0BB437
private static final String TOKEN_VERIFY_URL = API_URL0 +
"proved/user/info/map?devType=6&devModel=Chrome&uuid={uuid}&extra=2&timestamp={ts}";
private static final String SECOND_REQUEST_URL = API_URL_PREFIX + "file/redirect?downloadId={fidEncode}&enable=1" +
"&devType=6&uuid={uuid}&timestamp={ts}&auth={auth}&shareId={dataKey}";
// downloadId=x&enable=1&devType=6&uuid=x&timestamp=x&auth=x&shareId=lGFndCM
private static final String SECOND_REQUEST_URL_VIP = API_URL_PREFIX + "file/redirect?uuid={uuid}&devType=6&devCode={uuid}" +
"&devModel=chrome&devVersion=127&appVersion=&timestamp={ts}&appToken={appToken}&enable=1&downloadId={fidEncode}&auth={auth}";
private static final String VIP_REQUEST_URL = API_URL_PREFIX + "/buy/vip/list?devType=6&devModel=Chrome&uuid" +
"={uuid}&extra=2&timestamp={ts}";
@@ -42,16 +57,15 @@ public class IzTool extends PanBase {
"={uuid}&extra=2&timestamp={ts}&shareId={shareId}&folderId" +
"={folderId}&offset=1&limit=60";
long nowTs = System.currentTimeMillis();
String tsEncode = AESUtils.encrypt2HexIz(Long.toString(nowTs));
String uuid = UUID.randomUUID().toString();
WebClientSession webClientSession = WebClientSession.create(clientNoRedirects);
private static final MultiMap header;
static {
header = MultiMap.caseInsensitiveMultiMap();
header.set("Accept", "application/json, text/plain, */*");
header.set("Accept-Encoding", "gzip, deflate, br, zstd");
header.set("Accept-Encoding", "gzip, deflate");
header.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
header.set("Cache-Control", "no-cache");
header.set("Connection", "keep-alive");
@@ -69,38 +83,59 @@ public class IzTool extends PanBase {
header.set("sec-ch-ua-mobile", "?0");
header.set("sec-ch-ua-platform", "\"Windows\"");
}
public IzTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
private void setCookie(String html) {
int beginIndex = html.indexOf("arg1='") + 6;
String arg1 = html.substring(beginIndex, html.indexOf("';", beginIndex));
String acw_sc__v2 = AcwScV2Generator.acwScV2Simple(arg1);
// 创建一个 Cookie 并放入 CookieStore
DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2);
nettyCookie.setDomain(".ilanzou.com"); // 设置域名
nettyCookie.setPath("/"); // 设置路径
nettyCookie.setSecure(false);
nettyCookie.setHttpOnly(false);
webClientSession.cookieStore().put(nettyCookie);
}
String uuid = UUID.randomUUID().toString().toLowerCase(); // 也可以使用 UUID.randomUUID().toString()
public static String token = null;
public static boolean authFlag = true;
public Future<String> parse() {
String shareId = shareLinkInfo.getShareKey();
// 24.5.12 ilanzou改规则无需计算shareId
// String shareId = String.valueOf(AESUtils.idEncryptIz(dataKey));
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
long nowTs = System.currentTimeMillis();
String tsEncode = AESUtils.encrypt2HexIz(Long.toString(nowTs));
// 第一次请求 获取文件信息
// POST https://api.ilanzou.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
String url = StringUtils.isBlank(shareLinkInfo.getSharePassword()) ? FIRST_REQUEST_URL
: (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword());
// 检查并输出认证状态
if (shareLinkInfo.getOtherParam().containsKey("auths")) {
boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
log.info("文件解析检测到认证信息: isTempAuth={}, authFlag={}, token={}",
isTempAuth, authFlag, token != null ? "已登录(" + token.substring(0, Math.min(10, token.length())) + "...)" : "未登录");
// 如果需要认证但还没有token先执行登录
if ((isTempAuth || authFlag) && token == null) {
log.info("文件解析需要登录,开始执行登录流程...");
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
return login(tsEncode, auths)
.compose(v -> {
log.info("文件解析预登录成功,继续解析流程");
return parseWithAuth(shareId, tsEncode);
})
.onFailure(err -> {
log.warn("文件解析预登录失败: {},尝试使用免登录模式", err.getMessage());
// 登录失败,继续使用免登录模式
});
} else if (token != null) {
log.info("文件解析使用已有token: {}...", token.substring(0, Math.min(10, token.length())));
}
} else {
log.debug("文件解析无认证信息,使用免登录模式");
}
return parseWithAuth(shareId, tsEncode);
}
private Future<String> parseWithAuth(String shareId, String tsEncode) {
// 24.5.12 飞机盘 规则修改 需要固定UUID先请求会员接口, 再请求后续接口
webClientSession.postAbs(UriTemplate.of(VIP_REQUEST_URL))
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.send().onSuccess(r0 -> { // 忽略res
String url = StringUtils.isBlank(shareLinkInfo.getSharePassword()) ? FIRST_REQUEST_URL
: (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword());
// 第一次请求 获取文件信息
// POST https://api.feijipan.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
webClientSession.postAbs(UriTemplate.of(url))
@@ -121,36 +156,43 @@ public class IzTool extends PanBase {
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.send().onSuccess(res2 -> {
handleParseResponse(asText(res2), shareId);
}).onFailure(handleFail(FIRST_REQUEST_URL));
processFirstResponse(res2);
}).onFailure(handleFail("请求1-重试"));
return;
}
handleParseResponse(resBody, shareId);
}).onFailure(handleFail(FIRST_REQUEST_URL));
});
processFirstResponse(res);
}).onFailure(handleFail("请求1"));
}).onFailure(handleFail("请求1"));
return promise.future();
}
private void handleParseResponse(String resBody, String shareId) {
JsonObject resJson;
try {
resJson = new JsonObject(resBody);
} catch (Exception e) {
fail(FIRST_REQUEST_URL + " 解析JSON失败: " + resBody);
return;
}
if (resJson.isEmpty()) {
fail(FIRST_REQUEST_URL + " 返回内容为空");
return;
/**
* 设置 cookie
*/
private void setCookie(String html) {
int beginIndex = html.indexOf("arg1='") + 6;
String arg1 = html.substring(beginIndex, html.indexOf("';", beginIndex));
String acw_sc__v2 = AcwScV2Generator.acwScV2Simple(arg1);
// 创建一个 Cookie 并放入 CookieStore
DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2);
nettyCookie.setDomain(".ilanzou.com"); // 设置域名
nettyCookie.setPath("/"); // 设置路径
nettyCookie.setSecure(false);
nettyCookie.setHttpOnly(false);
webClientSession.cookieStore().put(nettyCookie);
}
/**
* 处理第一次请求的响应
*/
private void processFirstResponse(HttpResponse<Buffer> res) {
JsonObject resJson = asJson(res);
if (resJson.getInteger("code") != 200) {
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson);
return;
}
if (resJson.getJsonArray("list").isEmpty()) {
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
return;
}
if (!resJson.containsKey("list") || resJson.getJsonArray("list").isEmpty()) {
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
return;
@@ -167,30 +209,251 @@ public class IzTool extends PanBase {
promise.complete(fileList.getInteger("folderId").toString());
return;
}
// 提取文件信息
extractFileInfo(fileList, fileInfo);
getDownURL(resJson);
}
private void getDownURL(JsonObject resJson) {
String dataKey = shareLinkInfo.getShareKey();
// 文件Id
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
String fileId = fileInfo.getString("fileIds");
String userId = fileInfo.getString("userId");
// 其他参数
// String fidEncode = AESUtils.encrypt2HexIz(fileId + "|");
long nowTs2 = System.currentTimeMillis();
String tsEncode2 = AESUtils.encrypt2HexIz(Long.toString(nowTs2));
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs);
// 第二次请求
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs2);
// 检查是否有认证信息
if (shareLinkInfo.getOtherParam().containsKey("auths")) {
// 检查是否为临时认证(临时认证每次都尝试登录)
boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
// 如果是临时认证或者是后台配置且authFlag为true则尝试使用认证
if (isTempAuth || authFlag) {
log.debug("尝试使用认证信息解析, isTempAuth={}, authFlag={}", isTempAuth, authFlag);
HttpRequest<Buffer> httpRequest =
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL_VIP))
.setTemplateParam("fidEncode", fidEncode)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("ts", tsEncode2)
.setTemplateParam("auth", auth)
.setTemplateParam("shareId", shareId)
.putHeaders(header).send().onSuccess(res2 -> {
.setTemplateParam("dataKey", dataKey);
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
if (token == null) {
// 执行登录
login(tsEncode2, auths).onFailure(failRes-> {
log.warn("登录失败: {}", failRes.getMessage());
fail(failRes.getMessage());
}).onSuccess(r-> {
httpRequest.setTemplateParam("appToken", header.get("appToken"))
.putHeaders(header);
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
});
} else {
// 验证token
webClientSession.postAbs(UriTemplate.of(TOKEN_VERIFY_URL))
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode2)
.putHeaders(header).send().onSuccess(res -> {
// log.info("res: {}",asJson(res));
if (asJson(res).getInteger("code") != 200) {
login(tsEncode2, auths).onFailure(failRes -> {
log.warn("重新登录失败: {}", failRes.getMessage());
fail(failRes.getMessage());
}).onSuccess(r-> {
httpRequest.setTemplateParam("appToken", header.get("appToken"))
.putHeaders(header);
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
});
} else {
httpRequest.setTemplateParam("appToken", header.get("appToken"))
.putHeaders(header);
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
}
}).onFailure(handleFail("Token验证"));
}
} else {
// authFlag 为 false使用免登录解析
log.debug("authFlag=false使用免登录解析");
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
.putHeaders(header)
.setTemplateParam("fidEncode", fidEncode)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode2)
.setTemplateParam("auth", auth)
.setTemplateParam("dataKey", dataKey).send()
.onSuccess(this::down).onFailure(handleFail("请求2"));
}
} else {
// 没有认证信息,使用免登录解析
log.debug("无认证信息,使用免登录解析");
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
.putHeaders(header)
.setTemplateParam("fidEncode", fidEncode)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode2)
.setTemplateParam("auth", auth)
.setTemplateParam("dataKey", dataKey).send()
.onSuccess(this::down).onFailure(handleFail("请求2"));
}
}
private Future<Void> login(String tsEncode2, MultiMap auths) {
Promise<Void> promise1 = Promise.promise();
webClientSession.postAbs(UriTemplate.of(LOGIN_URL))
.setTemplateParam("uuid",uuid)
.setTemplateParam("ts", tsEncode2)
.putHeaders(header)
.sendJsonObject(JsonObject.of("loginName", auths.get("username"), "loginPwd", auths.get("password")))
.onSuccess(res2->{
JsonObject json = asJson(res2);
if (json.getInteger("code") == 200) {
token = json.getJsonObject("data").getString("appToken");
header.set("appToken", token);
log.info("登录成功 token: {}", token);
promise1.complete();
} else {
// 检查是否为临时认证
boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
if (isTempAuth) {
// 临时认证失败,直接返回错误,不影响后台配置的认证
log.warn("临时认证失败: {}", json.getString("msg"));
promise1.fail("临时认证失败: " + json.getString("msg"));
} else {
// 后台配置的认证失败设置authFlag并返回失败让下次请求使用免登陆解析
log.warn("后台配置认证失败: {}, authFlag将设为false请重新解析", json.getString("msg"));
authFlag = false;
promise1.fail("认证失败: " + json.getString("msg") + ", 请重新解析将使用免登陆模式");
}
}
}).onFailure(err -> {
log.error("登录请求异常: {}", err.getMessage());
promise1.fail("登录请求异常: " + err.getMessage());
});
return promise1.future();
}
/**
* 从接口返回数据中提取文件信息
*/
private void extractFileInfo(JsonObject fileList, JsonObject shareInfo) {
try {
// 文件名
String fileName = fileList.getString("fileName");
shareLinkInfo.getOtherParam().put("fileName", fileName);
// 文件大小 (KB -> Bytes)
Long fileSize = fileList.getLong("fileSize", 0L) * 1024;
shareLinkInfo.getOtherParam().put("fileSize", fileSize);
shareLinkInfo.getOtherParam().put("fileSizeFormat", FileSizeConverter.convertToReadableSize(fileSize));
// 文件图标
String fileIcon = fileList.getString("fileIcon");
if (StringUtils.isNotBlank(fileIcon)) {
shareLinkInfo.getOtherParam().put("fileIcon", fileIcon);
}
// 文件ID
Long fileId = fileList.getLong("fileId");
if (fileId != null) {
shareLinkInfo.getOtherParam().put("fileId", fileId.toString());
}
// 文件类型 (1=文件, 2=目录)
Integer fileType = fileList.getInteger("fileType", 1);
shareLinkInfo.getOtherParam().put("fileType", fileType == 1 ? "file" : "folder");
// 下载次数
Integer downloads = fileList.getInteger("fileDownloads", 0);
shareLinkInfo.getOtherParam().put("downloadCount", downloads);
// 点赞数
Integer likes = fileList.getInteger("fileLikes", 0);
shareLinkInfo.getOtherParam().put("likeCount", likes);
// 评论数
Integer comments = fileList.getInteger("fileComments", 0);
shareLinkInfo.getOtherParam().put("commentCount", comments);
// 评分
Double stars = fileList.getDouble("fileStars", 0.0);
shareLinkInfo.getOtherParam().put("stars", stars);
// 更新时间
String updateTime = fileList.getString("updTime");
if (StringUtils.isNotBlank(updateTime)) {
shareLinkInfo.getOtherParam().put("updateTime", updateTime);
}
// 创建时间
String createTime = null;
// 分享信息
if (shareInfo != null) {
// 分享ID
Integer shareId = shareInfo.getInteger("shareId");
if (shareId != null) {
shareLinkInfo.getOtherParam().put("shareId", shareId.toString());
}
// 上传时间
String addTime = shareInfo.getString("addTime");
if (StringUtils.isNotBlank(addTime)) {
shareLinkInfo.getOtherParam().put("createTime", addTime);
createTime = addTime;
}
// 预览次数
Integer previewNum = shareInfo.getInteger("previewNum", 0);
shareLinkInfo.getOtherParam().put("previewCount", previewNum);
// 用户信息
JsonObject userMap = shareInfo.getJsonObject("map");
if (userMap != null) {
String userName = userMap.getString("userName");
if (StringUtils.isNotBlank(userName)) {
shareLinkInfo.getOtherParam().put("userName", userName);
}
// VIP信息
Integer isVip = userMap.getInteger("isVip", 0);
shareLinkInfo.getOtherParam().put("isVip", isVip == 1);
}
}
// 创建 FileInfo 对象并存入 otherParam
FileInfo fileInfoObj = new FileInfo()
.setPanType(shareLinkInfo.getType())
.setFileName(fileName)
.setFileId(fileList.getLong("fileId") != null ? fileList.getLong("fileId").toString() : null)
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
.setFileType(fileType == 1 ? "file" : "folder")
.setFileIcon(fileList.getString("fileIcon"))
.setDownloadCount(downloads)
.setCreateTime(createTime)
.setUpdateTime(updateTime);
shareLinkInfo.getOtherParam().put("fileInfo", fileInfoObj);
log.debug("提取文件信息成功: fileName={}, fileSize={}, downloads={}",
fileName, fileSize, downloads);
} catch (Exception e) {
log.warn("提取文件信息失败: {}", e.getMessage());
}
}
private void down(HttpResponse<Buffer> res2) {
MultiMap headers = res2.headers();
if (!headers.contains("Location")) {
fail(SECOND_REQUEST_URL + " 未找到重定向URL: \n" + headers);
if (!headers.contains("Location") || StringUtils.isBlank(headers.get("Location"))) {
fail("找不到下载链接可能服务器已被禁止或者配置的认证信息有误");
return;
}
promise.complete(headers.get("Location"));
}).onFailure(handleFail(SECOND_REQUEST_URL));
}
// 目录解析
@Override
public Future<List<FileInfo>> parseFileList() {
Promise<List<FileInfo>> promise = Promise.promise();
@@ -205,11 +468,7 @@ public class IzTool extends PanBase {
return promise.future();
}
parse().onSuccess(id -> {
if (id != null && id.matches("^[a-zA-Z0-9]+$")) {
parserDir(id, shareId, promise);
} else {
promise.fail("解析目录ID失败");
}
}).onFailure(failRes -> {
log.error("解析目录失败: {}", failRes.getMessage());
promise.fail(failRes);
@@ -218,6 +477,22 @@ public class IzTool extends PanBase {
}
private void parserDir(String id, String shareId, Promise<List<FileInfo>> promise) {
if (id != null && (id.startsWith("http://") || id.startsWith("https://"))) {
FileInfo fileInfo = new FileInfo();
fileInfo.setFileName(id)
.setFileId(id)
.setFileType("file")
.setParserUrl(id)
.setPanType(shareLinkInfo.getType());
List<FileInfo> result = new ArrayList<>();
result.add(fileInfo);
promise.complete(result);
return;
}
long nowTs = System.currentTimeMillis();
String tsEncode = AESUtils.encrypt2HexIz(Long.toString(nowTs));
log.debug("开始解析目录: {}, shareId: {}, uuid: {}, ts: {}", id, shareId, uuid, tsEncode);
// 开始解析目录: 164312216, shareId: bPMsbg5K, uuid: 0fmVWTx2Ea4zFwkpd7KXf, ts: 20865d7b7f00828279f437cd1f097860
// 拿到目录ID
@@ -228,14 +503,48 @@ public class IzTool extends PanBase {
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res -> {
JsonObject jsonObject;
try {
jsonObject = asJson(res);
} catch (Exception e) {
promise.fail(FIRST_REQUEST_URL + " 解析JSON失败: " + res.bodyAsString());
String resBody = asText(res);
// 检查是否包含 cookie 验证
if (resBody.contains("var arg1='")) {
log.debug("目录解析需要 cookie 验证,重新创建 session");
webClientSession = WebClientSession.create(clientNoRedirects);
setCookie(resBody);
// 重新请求目录列表
webClientSession.postAbs(UriTemplate.of(FILE_LIST_URL))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res2 -> {
processDirResponse(res2, shareId, promise);
}).onFailure(err -> {
log.error("目录解析重试失败: {}", err.getMessage());
promise.fail("目录解析失败: " + err.getMessage());
});
return;
}
// System.out.println(jsonObject.encodePrettily());
processDirResponse(res, shareId, promise);
}).onFailure(err -> {
log.error("目录解析请求失败: {}", err.getMessage());
promise.fail("目录解析失败: " + err.getMessage());
});
}
/**
* 处理目录解析响应
*/
private void processDirResponse(HttpResponse<Buffer> res, String shareId, Promise<List<FileInfo>> promise) {
try {
JsonObject jsonObject = asJson(res);
log.debug("目录解析响应: {}", jsonObject.encodePrettily());
if (!jsonObject.containsKey("list")) {
log.error("目录解析响应缺少 list 字段: {}", jsonObject);
promise.fail("目录解析失败: 响应格式错误");
return;
}
JsonArray list = jsonObject.getJsonArray("list");
ArrayList<FileInfo> result = new ArrayList<>();
list.forEach(item->{
@@ -246,22 +555,20 @@ public class IzTool extends PanBase {
String fileId = fileJson.getString("fileId");
String userId = fileJson.getString("userId");
// 回传用到的参数
//"fidEncode", paramJson.getString("fidEncode"))
//"uuid", paramJson.getString("uuid"))
//"ts", paramJson.getString("ts"))
//"auth", paramJson.getString("auth"))
//"shareId", paramJson.getString("shareId"))
// 其他参数 - 每个文件使用新的时间戳
long nowTs2 = System.currentTimeMillis();
String tsEncode2 = AESUtils.encrypt2HexIz(Long.toString(nowTs2));
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs);
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs2);
// 回传用到的参数
JsonObject entries = JsonObject.of(
"fidEncode", fidEncode,
"uuid", uuid,
"ts", tsEncode,
"ts", tsEncode2,
"auth", auth,
"shareId", shareId);
byte[] encode = Base64.getEncoder().encode(entries.encode().getBytes());
String param = new String(encode);
String param = CommonUtils.urlBase64Encode(entries.encode());
if (fileJson.getInteger("fileType") == 2) {
// 如果是目录
@@ -301,30 +608,29 @@ public class IzTool extends PanBase {
result.add(fileInfo);
});
promise.complete(result);
}).onFailure(failRes -> {
log.error("解析目录请求失败: {}", failRes.getMessage());
promise.fail(failRes);
});
} catch (Exception e) {
log.error("处理目录响应异常: {}", e.getMessage(), e);
promise.fail("目录解析失败: " + e.getMessage());
}
}
@Override
public Future<String> parseById() {
// 第二次请求
JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
// 使用免登录接口
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
.putHeaders(header)
.setTemplateParam("fidEncode", paramJson.getString("fidEncode"))
.setTemplateParam("uuid", paramJson.getString("uuid"))
.setTemplateParam("ts", paramJson.getString("ts"))
.setTemplateParam("auth", paramJson.getString("auth"))
.setTemplateParam("shareId", paramJson.getString("shareId"))
.putHeaders(header).send().onSuccess(res2 -> {
MultiMap headers = res2.headers();
if (!headers.contains("Location")) {
fail(SECOND_REQUEST_URL + " 未找到重定向URL: \n" + res2.headers());
return;
}
promise.complete(headers.get("Location"));
}).onFailure(handleFail(SECOND_REQUEST_URL));
.setTemplateParam("dataKey", paramJson.getString("shareId"))
.send().onSuccess(this::down).onFailure(handleFail("parseById"));
return promise.future();
}
public static void resetToken() {
token = null;
authFlag = true;
}
}

View File

@@ -0,0 +1,658 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.AESUtils;
import cn.qaiu.util.AcwScV2Generator;
import cn.qaiu.util.CommonUtils;
import cn.qaiu.util.FileSizeConverter;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.Promise;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpRequest;
import io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClientSession;
import io.vertx.uritemplate.UriTemplate;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* 蓝奏云优享 - 需要登录版本(支持大文件)
*/
public class IzToolWithAuth extends PanBase {
private static final String API_URL0 = "https://api.ilanzou.com/";
private static final String API_URL_PREFIX = "https://api.ilanzou.com/unproved/";
private static final String FIRST_REQUEST_URL = API_URL_PREFIX + "recommend/list?devType=6&devModel=Chrome" +
"&uuid={uuid}&extra=2&timestamp={ts}&shareId={shareId}&type=0&offset=1&limit=60";
private static final String LOGIN_URL = API_URL_PREFIX +
"login?uuid={uuid}&devType=6&devCode={uuid}&devModel=chrome&devVersion=127&appVersion=&timestamp={ts}&appToken=&extra=2";
// https://api.ilanzou.com/proved/user/info/map?devType=3&devModel=Chrome&uuid=TInRHH3QzRaMo-Ajl2PkJ&extra=2&timestamp=EC2C6E7F45EB21338A17A7621E0BB437
private static final String TOKEN_VERIFY_URL = API_URL0 +
"proved/user/info/map?devType=6&devModel=Chrome&uuid={uuid}&extra=2&timestamp={ts}";
private static final String SECOND_REQUEST_URL = API_URL_PREFIX + "file/redirect?downloadId={fidEncode}&enable=1" +
"&devType=6&uuid={uuid}&timestamp={ts}&auth={auth}&shareId={dataKey}";
private static final String SECOND_REQUEST_URL_VIP = API_URL_PREFIX + "file/redirect?uuid={uuid}&devType=6&devCode={uuid}" +
"&devModel=chrome&devVersion=127&appVersion=&timestamp={ts}&appToken={appToken}&enable=1&downloadId={fidEncode}&auth={auth}";
private static final String VIP_REQUEST_URL = API_URL_PREFIX + "/buy/vip/list?devType=6&devModel=Chrome&uuid" +
"={uuid}&extra=2&timestamp={ts}";
private static final String FILE_LIST_URL = API_URL_PREFIX + "/share/list?devType=6&devModel=Chrome&uuid" +
"={uuid}&extra=2&timestamp={ts}&shareId={shareId}&folderId" +
"={folderId}&offset=1&limit=60";
WebClientSession webClientSession = WebClientSession.create(clientNoRedirects);
private static final MultiMap header;
static {
header = MultiMap.caseInsensitiveMultiMap();
header.set("Accept", "application/json, text/plain, */*");
header.set("Accept-Encoding", "gzip, deflate");
header.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
header.set("Cache-Control", "no-cache");
header.set("Connection", "keep-alive");
header.set("Content-Length", "0");
header.set("DNT", "1");
header.set("Host", "api.ilanzou.com");
header.set("Origin", "https://www.ilanzou.com/");
header.set("Pragma", "no-cache");
header.set("Referer", "https://www.ilanzou.com/");
header.set("Sec-Fetch-Dest", "empty");
header.set("Sec-Fetch-Mode", "cors");
header.set("Sec-Fetch-Site", "cross-site");
header.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36");
header.set("sec-ch-ua", "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"");
header.set("sec-ch-ua-mobile", "?0");
header.set("sec-ch-ua-platform", "\"Windows\"");
}
public IzToolWithAuth(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
String uuid = UUID.randomUUID().toString().toLowerCase(); // 也可以使用 UUID.randomUUID().toString()
public static String token = null;
public static boolean authFlag = true;
public Future<String> parse() {
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
long nowTs = System.currentTimeMillis();
String tsEncode = AESUtils.encrypt2HexIz(Long.toString(nowTs));
// 24.5.12 飞机盘 规则修改 需要固定UUID先请求会员接口, 再请求后续接口
webClientSession.postAbs(UriTemplate.of(VIP_REQUEST_URL))
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.send().onSuccess(r0 -> { // 忽略res
String url = StringUtils.isBlank(shareLinkInfo.getSharePassword()) ? FIRST_REQUEST_URL
: (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword());
// 第一次请求 获取文件信息
// POST https://api.feijipan.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
webClientSession.postAbs(UriTemplate.of(url))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.send().onSuccess(res -> {
String resBody = asText(res);
// 检查是否包含 cookie 验证
if (resBody.contains("var arg1='")) {
webClientSession = WebClientSession.create(clientNoRedirects);
setCookie(resBody);
// 重新请求
webClientSession.postAbs(UriTemplate.of(url))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.send().onSuccess(res2 -> {
processFirstResponse(res2);
}).onFailure(handleFail("请求1-重试"));
return;
}
processFirstResponse(res);
}).onFailure(handleFail("请求1"));
}).onFailure(handleFail("请求1"));
return promise.future();
}
/**
* 设置 cookie
*/
private void setCookie(String html) {
int beginIndex = html.indexOf("arg1='") + 6;
String arg1 = html.substring(beginIndex, html.indexOf("';", beginIndex));
String acw_sc__v2 = AcwScV2Generator.acwScV2Simple(arg1);
// 创建一个 Cookie 并放入 CookieStore
DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2);
nettyCookie.setDomain(".ilanzou.com"); // 设置域名
nettyCookie.setPath("/"); // 设置路径
nettyCookie.setSecure(false);
nettyCookie.setHttpOnly(false);
webClientSession.cookieStore().put(nettyCookie);
}
/**
* 处理第一次请求的响应
*/
private void processFirstResponse(HttpResponse<Buffer> res) {
JsonObject resJson = asJson(res);
if (resJson.getInteger("code") != 200) {
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson);
return;
}
if (!resJson.containsKey("list") || resJson.getJsonArray("list").isEmpty()) {
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
return;
}
// 文件Id
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
// 如果是目录返回目录ID
if (!fileInfo.containsKey("fileList") || fileInfo.getJsonArray("fileList").isEmpty()) {
fail(FIRST_REQUEST_URL + " 文件列表为空: " + fileInfo);
return;
}
JsonObject fileList = fileInfo.getJsonArray("fileList").getJsonObject(0);
if (fileList.getInteger("fileType") == 2) {
promise.complete(fileList.getInteger("folderId").toString());
return;
}
// 提取文件信息
extractFileInfo(fileList, fileInfo);
getDownURL(resJson);
}
private void getDownURL(JsonObject resJson) {
String dataKey = shareLinkInfo.getShareKey();
// 文件Id
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
String fileId = fileInfo.getString("fileIds");
String userId = fileInfo.getString("userId");
// 其他参数
long nowTs2 = System.currentTimeMillis();
String tsEncode2 = AESUtils.encrypt2HexIz(Long.toString(nowTs2));
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs2);
// 检查是否有认证信息
if (shareLinkInfo.getOtherParam().containsKey("auths")) {
// 检查是否为临时认证(临时认证每次都尝试登录)
boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
// 如果是临时认证或者是后台配置且authFlag为true则尝试使用认证
if (isTempAuth || authFlag) {
log.debug("尝试使用认证信息解析, isTempAuth={}, authFlag={}", isTempAuth, authFlag);
HttpRequest<Buffer> httpRequest =
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL_VIP))
.setTemplateParam("fidEncode", fidEncode)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode2)
.setTemplateParam("auth", auth)
.setTemplateParam("dataKey", dataKey);
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
if (token == null) {
// 执行登录
login(tsEncode2, auths).onFailure(failRes-> {
log.warn("登录失败: {}", failRes.getMessage());
fail(failRes.getMessage());
}).onSuccess(r-> {
httpRequest.setTemplateParam("appToken", header.get("appToken"))
.putHeaders(header);
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
});
} else {
// 验证token
webClientSession.postAbs(UriTemplate.of(TOKEN_VERIFY_URL))
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode2)
.putHeaders(header).send().onSuccess(res -> {
// log.info("res: {}",asJson(res));
if (asJson(res).getInteger("code") != 200) {
login(tsEncode2, auths).onFailure(failRes -> {
log.warn("重新登录失败: {}", failRes.getMessage());
fail(failRes.getMessage());
}).onSuccess(r-> {
httpRequest.setTemplateParam("appToken", header.get("appToken"))
.putHeaders(header);
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
});
} else {
httpRequest.setTemplateParam("appToken", header.get("appToken"))
.putHeaders(header);
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
}
}).onFailure(handleFail("Token验证"));
}
} else {
// authFlag 为 false使用免登录解析
log.debug("authFlag=false使用免登录解析");
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
.putHeaders(header)
.setTemplateParam("fidEncode", fidEncode)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode2)
.setTemplateParam("auth", auth)
.setTemplateParam("dataKey", dataKey).send()
.onSuccess(this::down).onFailure(handleFail("请求2"));
}
} else {
// 没有认证信息,使用免登录解析
log.debug("无认证信息,使用免登录解析");
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
.putHeaders(header)
.setTemplateParam("fidEncode", fidEncode)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode2)
.setTemplateParam("auth", auth)
.setTemplateParam("dataKey", dataKey).send()
.onSuccess(this::down).onFailure(handleFail("请求2"));
}
}
private Future<Void> login(String tsEncode2, MultiMap auths) {
Promise<Void> promise1 = Promise.promise();
webClientSession.postAbs(UriTemplate.of(LOGIN_URL))
.setTemplateParam("uuid",uuid)
.setTemplateParam("ts", tsEncode2)
.putHeaders(header)
.sendJsonObject(JsonObject.of("loginName", auths.get("username"), "loginPwd", auths.get("password")))
.onSuccess(res2->{
JsonObject json = asJson(res2);
if (json.getInteger("code") == 200) {
token = json.getJsonObject("data").getString("appToken");
header.set("appToken", token);
log.info("登录成功 token: {}", token);
promise1.complete();
} else {
// 检查是否为临时认证
boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
if (isTempAuth) {
// 临时认证失败,直接返回错误,不影响后台配置的认证
log.warn("临时认证失败: {}", json.getString("msg"));
promise1.fail("临时认证失败: " + json.getString("msg"));
} else {
// 后台配置的认证失败设置authFlag并返回失败让下次请求使用免登陆解析
log.warn("后台配置认证失败: {}, authFlag将设为false请重新解析", json.getString("msg"));
authFlag = false;
promise1.fail("认证失败: " + json.getString("msg") + ", 请重新解析将使用免登陆模式");
}
}
}).onFailure(err -> {
log.error("登录请求异常: {}", err.getMessage());
promise1.fail("登录请求异常: " + err.getMessage());
});
return promise1.future();
}
/**
* 从接口返回数据中提取文件信息
*/
private void extractFileInfo(JsonObject fileList, JsonObject shareInfo) {
try {
// 文件名
String fileName = fileList.getString("fileName");
shareLinkInfo.getOtherParam().put("fileName", fileName);
// 文件大小 (KB -> Bytes)
Long fileSize = fileList.getLong("fileSize", 0L) * 1024;
shareLinkInfo.getOtherParam().put("fileSize", fileSize);
shareLinkInfo.getOtherParam().put("fileSizeFormat", FileSizeConverter.convertToReadableSize(fileSize));
// 文件图标
String fileIcon = fileList.getString("fileIcon");
if (StringUtils.isNotBlank(fileIcon)) {
shareLinkInfo.getOtherParam().put("fileIcon", fileIcon);
}
// 文件ID
Long fileId = fileList.getLong("fileId");
if (fileId != null) {
shareLinkInfo.getOtherParam().put("fileId", fileId.toString());
}
// 文件类型 (1=文件, 2=目录)
Integer fileType = fileList.getInteger("fileType", 1);
shareLinkInfo.getOtherParam().put("fileType", fileType == 1 ? "file" : "folder");
// 下载次数
Integer downloads = fileList.getInteger("fileDownloads", 0);
shareLinkInfo.getOtherParam().put("downloadCount", downloads);
// 点赞数
Integer likes = fileList.getInteger("fileLikes", 0);
shareLinkInfo.getOtherParam().put("likeCount", likes);
// 评论数
Integer comments = fileList.getInteger("fileComments", 0);
shareLinkInfo.getOtherParam().put("commentCount", comments);
// 评分
Double stars = fileList.getDouble("fileStars", 0.0);
shareLinkInfo.getOtherParam().put("stars", stars);
// 更新时间
String updateTime = fileList.getString("updTime");
if (StringUtils.isNotBlank(updateTime)) {
shareLinkInfo.getOtherParam().put("updateTime", updateTime);
}
// 创建时间
String createTime = null;
// 分享信息
if (shareInfo != null) {
// 分享ID
Integer shareId = shareInfo.getInteger("shareId");
if (shareId != null) {
shareLinkInfo.getOtherParam().put("shareId", shareId.toString());
}
// 上传时间
String addTime = shareInfo.getString("addTime");
if (StringUtils.isNotBlank(addTime)) {
shareLinkInfo.getOtherParam().put("createTime", addTime);
createTime = addTime;
}
// 预览次数
Integer previewNum = shareInfo.getInteger("previewNum", 0);
shareLinkInfo.getOtherParam().put("previewCount", previewNum);
// 用户信息
JsonObject userMap = shareInfo.getJsonObject("map");
if (userMap != null) {
String userName = userMap.getString("userName");
if (StringUtils.isNotBlank(userName)) {
shareLinkInfo.getOtherParam().put("userName", userName);
}
// VIP信息
Integer isVip = userMap.getInteger("isVip", 0);
shareLinkInfo.getOtherParam().put("isVip", isVip == 1);
}
}
// 创建 FileInfo 对象并存入 otherParam
FileInfo fileInfoObj = new FileInfo()
.setPanType(shareLinkInfo.getType())
.setFileName(fileName)
.setFileId(fileList.getLong("fileId") != null ? fileList.getLong("fileId").toString() : null)
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
.setFileType(fileType == 1 ? "file" : "folder")
.setFileIcon(fileList.getString("fileIcon"))
.setDownloadCount(downloads)
.setCreateTime(createTime)
.setUpdateTime(updateTime);
shareLinkInfo.getOtherParam().put("fileInfo", fileInfoObj);
log.debug("提取文件信息成功: fileName={}, fileSize={}, downloads={}",
fileName, fileSize, downloads);
} catch (Exception e) {
log.warn("提取文件信息失败: {}", e.getMessage());
}
}
private void down(HttpResponse<Buffer> res2) {
MultiMap headers = res2.headers();
if (!headers.contains("Location") || StringUtils.isBlank(headers.get("Location"))) {
fail("找不到下载链接可能服务器已被禁止或者配置的认证信息有误");
return;
}
promise.complete(headers.get("Location"));
}
// 目录解析
@Override
public Future<List<FileInfo>> parseFileList() {
Promise<List<FileInfo>> promise = Promise.promise();
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
// 如果参数里的目录ID不为空则直接解析目录
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
if (dirId != null && !dirId.isEmpty()) {
uuid = shareLinkInfo.getOtherParam().get("uuid").toString();
parserDir(dirId, shareId, promise);
return promise.future();
}
parse().onSuccess(id -> {
parserDir(id, shareId, promise);
}).onFailure(failRes -> {
log.error("解析目录失败: {}", failRes.getMessage());
promise.fail(failRes);
});
return promise.future();
}
private void parserDir(String id, String shareId, Promise<List<FileInfo>> promise) {
if (id != null && (id.startsWith("http://") || id.startsWith("https://"))) {
FileInfo fileInfo = new FileInfo();
fileInfo.setFileName(id)
.setFileId(id)
.setFileType("file")
.setParserUrl(id)
.setPanType(shareLinkInfo.getType());
List<FileInfo> result = new ArrayList<>();
result.add(fileInfo);
promise.complete(result);
return;
}
long nowTs = System.currentTimeMillis();
String tsEncode = AESUtils.encrypt2HexIz(Long.toString(nowTs));
log.debug("开始解析目录: {}, shareId: {}, uuid: {}, ts: {}", id, shareId, uuid, tsEncode);
// 检查是否需要登录(有认证信息且需要使用认证)
if (shareLinkInfo.getOtherParam().containsKey("auths")) {
boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
log.debug("目录解析检查认证: isTempAuth={}, authFlag={}, token={}", isTempAuth, authFlag, token != null ? "已有" : "null");
if ((isTempAuth || authFlag) && token == null) {
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
log.info("目录解析需要登录,开始执行登录...");
// 先登录获取 token
login(tsEncode, auths)
.onFailure(err -> {
log.warn("目录解析登录失败,使用免登录模式: {}", err.getMessage());
// 登录失败,继续使用免登录
requestDirList(id, shareId, tsEncode, promise);
})
.onSuccess(r -> {
log.info("目录解析登录成功token={}, 使用 VIP 模式", token != null ? token.substring(0, 10) + "..." : "null");
requestDirList(id, shareId, tsEncode, promise);
});
return;
} else if (token != null) {
log.debug("目录解析已有 token直接使用 VIP 模式");
} else {
log.debug("目录解析: authFlag=false 或为临时认证但已失败,使用免登录模式");
}
} else {
log.debug("目录解析无认证信息,使用免登录模式");
}
// 无需登录或已登录,直接请求
requestDirList(id, shareId, tsEncode, promise);
}
/**
* 请求目录列表
*/
private void requestDirList(String id, String shareId, String tsEncode, Promise<List<FileInfo>> promise) {
webClientSession.postAbs(UriTemplate.of(FILE_LIST_URL))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res -> {
String resBody = asText(res);
// 检查是否包含 cookie 验证
if (resBody.contains("var arg1='")) {
log.debug("目录解析需要 cookie 验证,重新创建 session");
webClientSession = WebClientSession.create(clientNoRedirects);
setCookie(resBody);
// 重新请求目录列表
webClientSession.postAbs(UriTemplate.of(FILE_LIST_URL))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res2 -> {
processDirResponse(res2, shareId, promise);
}).onFailure(err -> {
log.error("目录解析重试失败: {}", err.getMessage());
promise.fail("目录解析失败: " + err.getMessage());
});
return;
}
processDirResponse(res, shareId, promise);
}).onFailure(err -> {
log.error("目录解析请求失败: {}", err.getMessage());
promise.fail("目录解析失败: " + err.getMessage());
});
}
/**
* 处理目录解析响应
*/
private void processDirResponse(HttpResponse<Buffer> res, String shareId, Promise<List<FileInfo>> promise) {
try {
JsonObject jsonObject = asJson(res);
log.debug("目录解析响应: {}", jsonObject.encodePrettily());
if (!jsonObject.containsKey("list")) {
log.error("目录解析响应缺少 list 字段: {}", jsonObject);
promise.fail("目录解析失败: 响应格式错误");
return;
}
JsonArray list = jsonObject.getJsonArray("list");
ArrayList<FileInfo> result = new ArrayList<>();
list.forEach(item->{
JsonObject fileJson = (JsonObject) item;
FileInfo fileInfo = new FileInfo();
// 映射已知字段
String fileId = fileJson.getString("fileId");
String userId = fileJson.getString("userId");
// 其他参数 - 每个文件使用新的时间戳
long nowTs2 = System.currentTimeMillis();
String tsEncode2 = AESUtils.encrypt2HexIz(Long.toString(nowTs2));
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs2);
// 回传用到的参数(包含 token
JsonObject entries = JsonObject.of(
"fidEncode", fidEncode,
"uuid", uuid,
"ts", tsEncode2,
"auth", auth,
"shareId", shareId,
"appToken", token != null ? token : "");
String param = CommonUtils.urlBase64Encode(entries.encode());
if (fileJson.getInteger("fileType") == 2) {
// 如果是目录
fileInfo.setFileName(fileJson.getString("name"))
.setFileId(fileJson.getString("folderId"))
.setCreateTime(fileJson.getString("updTime"))
.setFileType("folder")
.setSize(0L)
.setSizeStr("0B")
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
// 设置目录解析的URL
.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&uuid=%s", getDomainName(),
shareLinkInfo.getShareUrl(), fileJson.getString("folderId"), uuid));
result.add(fileInfo);
return;
}
long fileSize = fileJson.getLong("fileSize") * 1024;
fileInfo.setFileName(fileJson.getString("fileName"))
.setFileId(fileId)
.setCreateTime(fileJson.getString("createTime"))
.setFileType("file")
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param))
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
result.add(fileInfo);
});
promise.complete(result);
} catch (Exception e) {
log.error("处理目录响应异常: {}", e.getMessage(), e);
promise.fail("目录解析失败: " + e.getMessage());
}
}
@Override
public Future<String> parseById() {
JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
String appToken = paramJson.getString("appToken", "");
// 如果有 token使用 VIP 接口
if (StringUtils.isNotBlank(appToken)) {
log.debug("parseById 使用 VIP 接口, appToken={}", appToken.substring(0, Math.min(10, appToken.length())) + "...");
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL_VIP))
.putHeaders(header)
.setTemplateParam("fidEncode", paramJson.getString("fidEncode"))
.setTemplateParam("uuid", paramJson.getString("uuid"))
.setTemplateParam("ts", paramJson.getString("ts"))
.setTemplateParam("auth", paramJson.getString("auth"))
.setTemplateParam("appToken", appToken)
.send().onSuccess(this::down).onFailure(handleFail("parseById-VIP"));
} else {
// 无 token使用免登录接口
log.debug("parseById 使用免登录接口");
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
.putHeaders(header)
.setTemplateParam("fidEncode", paramJson.getString("fidEncode"))
.setTemplateParam("uuid", paramJson.getString("uuid"))
.setTemplateParam("ts", paramJson.getString("ts"))
.setTemplateParam("auth", paramJson.getString("auth"))
.setTemplateParam("dataKey", paramJson.getString("shareId"))
.send().onSuccess(this::down).onFailure(handleFail("parseById"));
}
return promise.future();
}
public static void resetToken() {
token = null;
authFlag = true;
}
}

View File

@@ -1,50 +0,0 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import io.vertx.core.Future;
import io.vertx.core.buffer.Buffer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 118网盘解析
*/
public class P118Tool extends PanBase {
private static final String API_URL_PREFIX = "https://qaiu.118pan.com/ajax.php";
// private static final String
public P118Tool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
public Future<String> parse() {
client.postAbs(API_URL_PREFIX)
.putHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
.sendBuffer(Buffer.buffer("action=load_down_addr1&file_id=" + shareLinkInfo.getShareKey()))
.onSuccess(res -> {
System.out.println(res.headers());
Pattern compile = Pattern.compile("href=\"([^\"]+)\"");
Matcher matcher = compile.matcher(res.bodyAsString());
if (matcher.find()) {
//c: 0x63
//o: 0x6F
//m: 0x6D
//1: 0x31
///: 0x2F
char[] chars1 = new char[]{99, 111, 109, 49, 47};
char[] chars2 = new char[]{99, 111, 109, 47};
String group = matcher.group(1).replace(String.valueOf(chars1), String.valueOf(chars2));
System.out.println(group);
complete(group);
} else {
fail();
}
}).onFailure(handleFail(""));
return future();
}
}

View File

@@ -22,7 +22,8 @@ import java.util.List;
import java.util.Map;
/**
* 夸克网盘解析
* 夸克网盘解析 - 修复版
* 重点修复了 Cookie 换行符处理和请求头一致性问题
*/
public class QkTool extends PanBase {
@@ -31,593 +32,232 @@ public class QkTool extends PanBase {
private static final String TOKEN_URL = "https://drive-pc.quark.cn/1/clouddrive/share/sharepage/token";
private static final String DETAIL_URL = "https://drive-pc.quark.cn/1/clouddrive/share/sharepage/detail";
private static final String DOWNLOAD_URL = "https://drive-pc.quark.cn/1/clouddrive/file/download";
// Cookie 刷新 API
private static final String FLUSH_URL = "https://drive-pc.quark.cn/1/clouddrive/auth/pc/flush";
private static final int BATCH_SIZE = 15; // 批量获取下载链接的批次大小
private static final int BATCH_SIZE = 15;
// 静态变量:缓存 __puus cookie 和过期时间
// 缓存变量
private static volatile String cachedPuus = null;
private static volatile long puusExpireTime = 0;
// __puus 有效期,默认 55 分钟(服务器实际 1 小时过期,提前 5 分钟刷新)
private static final long PUUS_TTL_MS = 55 * 60 * 1000L;
private final MultiMap header = HeaderUtils.parseHeaders("""
// 严格模拟夸克 PC 客户端的请求头
private final MultiMap commonHeaders = HeaderUtils.parseHeaders("""
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch
Content-Type: application/json;charset=UTF-8
Accept: application/json, text/plain, */*
Referer: https://pan.quark.cn/
Origin: https://pan.quark.cn
Accept: application/json, text/plain, */*
Accept-Language: zh-CN,zh;q=0.9
Content-Type: application/json
""");
// 保存 auths 引用,用于更新 cookie
private MultiMap auths;
public QkTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
// 参考 UcTool 实现,从认证配置中取 cookie 放到请求头
if (shareLinkInfo.getOtherParam() != null && shareLinkInfo.getOtherParam().containsKey("auths")) {
auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
String cookie = auths.get("cookie");
if (cookie != null && !cookie.isEmpty()) {
// 过滤出夸克网盘所需的 cookie 字段
cookie = CookieUtils.filterUcQuarkCookie(cookie);
String rawCookie = auths.get("cookie");
if (rawCookie != null && !rawCookie.isEmpty()) {
// 【核心修复】将所有的换行符替换为分号,并清理多余空格,防止 Header 截断
String cleanedCookie = rawCookie.replace("\r\n", "; ").replace("\n", "; ")
.replaceAll(";\\s*;", ";")
.trim();
// 此时 cleanedCookie 已经是单行规范格式
cleanedCookie = CookieUtils.filterUcQuarkCookie(cleanedCookie);
// 如果有缓存的 __puus 且未过期,使用缓存的值更新 cookie
if (cachedPuus != null && System.currentTimeMillis() < puusExpireTime) {
cookie = CookieUtils.updateCookieValue(cookie, "__puus", cachedPuus);
cleanedCookie = CookieUtils.updateCookieValue(cleanedCookie, "__puus", cachedPuus);
log.debug("夸克: 使用缓存的 __puus (剩余有效期: {}s)", (puusExpireTime - System.currentTimeMillis()) / 1000);
}
header.set(HttpHeaders.COOKIE, cookie);
// 同步更新 auths
auths.set("cookie", cookie);
commonHeaders.set(HttpHeaders.COOKIE, cleanedCookie);
auths.set("cookie", cleanedCookie);
}
}
this.client = clientDisableUA;
// 如果 __puus 已过期或不存在,触发异步刷新
if (needRefreshPuus()) {
log.debug("夸克: __puus 需要刷新,触发异步刷新");
refreshPuusCookie();
}
}
/**
* 判断是否需要刷新 __puus
* @return true 表示需要刷新
*/
private boolean needRefreshPuus() {
String currentCookie = header.get(HttpHeaders.COOKIE);
if (currentCookie == null || currentCookie.isEmpty()) {
return false;
}
// 必须包含 __pus 才能刷新
if (!currentCookie.contains("__pus=")) {
return false;
}
// 缓存过期或不存在时需要刷新
String currentCookie = commonHeaders.get(HttpHeaders.COOKIE);
if (currentCookie == null || !currentCookie.contains("__pus=")) return false;
return cachedPuus == null || System.currentTimeMillis() >= puusExpireTime;
}
/**
* 刷新 __puus Cookie
* 通过调用 auth/pc/flush API服务器会返回 set-cookie 来更新 __puus
* @return Future 包含是否刷新成功
*/
public Future<Boolean> refreshPuusCookie() {
Promise<Boolean> refreshPromise = Promise.promise();
String currentCookie = header.get(HttpHeaders.COOKIE);
if (currentCookie == null || currentCookie.isEmpty()) {
log.debug("夸克: 无 cookie跳过刷新");
String currentCookie = commonHeaders.get(HttpHeaders.COOKIE);
if (currentCookie == null || !currentCookie.contains("__pus=")) {
refreshPromise.complete(false);
return refreshPromise.future();
}
// 检查是否包含 __pus用于获取 __puus
if (!currentCookie.contains("__pus=")) {
log.debug("夸克: cookie 中不包含 __pus跳过刷新");
refreshPromise.complete(false);
return refreshPromise.future();
}
log.debug("夸克: 开始刷新 __puus cookie");
client.getAbs(FLUSH_URL)
.addQueryParam("pr", "ucpro")
.addQueryParam("fr", "pc")
.addQueryParam("uc_param_str", "")
.putHeaders(header)
.putHeaders(commonHeaders)
.send()
.onSuccess(res -> {
// 从响应头获取 set-cookie
List<String> setCookies = res.cookies();
String newPuus = null;
for (String cookie : setCookies) {
if (cookie.startsWith("__puus=")) {
// 提取 __puus 值(只取到分号前的部分)
int endIndex = cookie.indexOf(';');
newPuus = endIndex > 0 ? cookie.substring(0, endIndex) : cookie;
break;
}
}
if (newPuus != null) {
// 更新 cookie替换或添加 __puus
String updatedCookie = CookieUtils.updateCookieValue(currentCookie, "__puus", newPuus);
header.set(HttpHeaders.COOKIE, updatedCookie);
// 同步更新 auths 中的 cookie
if (auths != null) {
auths.set("cookie", updatedCookie);
}
// 更新静态缓存
commonHeaders.set(HttpHeaders.COOKIE, updatedCookie);
if (auths != null) auths.set("cookie", updatedCookie);
cachedPuus = newPuus;
puusExpireTime = System.currentTimeMillis() + PUUS_TTL_MS;
log.info("夸克: __puus cookie 刷新成功,有效期至: {}ms", puusExpireTime);
refreshPromise.complete(true);
} else {
log.debug("夸克: 响应中未包含 __puus可能 cookie 仍然有效");
refreshPromise.complete(false);
}
})
.onFailure(t -> {
log.warn("夸克: 刷新 __puus cookie 失败: {}", t.getMessage());
refreshPromise.complete(false);
});
.onFailure(t -> refreshPromise.complete(false));
return refreshPromise.future();
}
@Override
public Future<String> parse() {
String pwdId = shareLinkInfo.getShareKey();
String passcode = shareLinkInfo.getSharePassword();
if (passcode == null) {
passcode = "";
}
String passcode = shareLinkInfo.getSharePassword() == null ? "" : shareLinkInfo.getSharePassword();
log.debug("开始解析夸克网盘分享pwd_id: {}, passcode: {}", pwdId, passcode.isEmpty() ? "" : "");
// 第一步:获取分享 token
JsonObject tokenRequest = new JsonObject()
.put("pwd_id", pwdId)
.put("passcode", passcode);
log.debug("开始解析夸克分享: {}", pwdId);
// 1. 获取 Token
JsonObject tokenBody = new JsonObject().put("pwd_id", pwdId).put("passcode", passcode);
client.postAbs(TOKEN_URL)
.addQueryParam("pr", "ucpro")
.addQueryParam("fr", "pc")
.putHeaders(header)
.sendJsonObject(tokenRequest)
.putHeaders(commonHeaders)
.sendJsonObject(tokenBody)
.onSuccess(res -> {
log.debug("第一阶段响应: {}", res.bodyAsString());
JsonObject resJson = asJson(res);
if (resJson.getInteger("code") != 0) {
fail(TOKEN_URL + " 返回异常: " + resJson);
fail("Token 获取失败: " + resJson.getString("message"));
return;
}
String stoken = resJson.getJsonObject("data").getString("stoken");
if (stoken == null || stoken.isEmpty()) {
fail("无法获取分享 token可能的原因1. Cookie 已过期 2. 分享链接已失效 3. 需要提取码但未提供");
return;
}
log.debug("成功获取 stoken");
log.debug("成功获取 stoken: {}", stoken);
// 第二步:获取文件列表
// 2. 获取详情
client.getAbs(DETAIL_URL)
.addQueryParam("pr", "ucpro")
.addQueryParam("fr", "pc")
.addQueryParam("pwd_id", pwdId)
.addQueryParam("stoken", stoken)
.addQueryParam("pdir_fid", "0")
.addQueryParam("force", "0")
.addQueryParam("_page", "1")
.addQueryParam("_size", "50")
.addQueryParam("_fetch_banner", "1")
.addQueryParam("_fetch_share", "1")
.addQueryParam("_fetch_total", "1")
.addQueryParam("_sort", "file_type:asc,updated_at:desc")
.putHeaders(header)
.putHeaders(commonHeaders)
.send()
.onSuccess(res2 -> {
log.debug("第二阶段响应: {}", res2.bodyAsString());
JsonObject resJson2 = asJson(res2);
if (resJson2.getInteger("code") != 0) {
fail(DETAIL_URL + " 返回异常: " + resJson2);
return;
}
JsonArray fileList = resJson2.getJsonObject("data").getJsonArray("list");
if (fileList == null || fileList.isEmpty()) {
fail("未找到文件");
fail("未找到文件列表");
return;
}
// 过滤出文件(排除文件夹)
List<JsonObject> files = new ArrayList<>();
List<String> fileIds = new ArrayList<>();
Map<String, JsonObject> fileMap = new HashMap<>();
for (int i = 0; i < fileList.size(); i++) {
JsonObject item = fileList.getJsonObject(i);
// 判断是否为文件file=true 或 obj_category 不为空
if (item.getBoolean("file", false) ||
(item.getString("obj_category") != null && !item.getString("obj_category").isEmpty())) {
files.add(item);
}
}
if (files.isEmpty()) {
fail("没有可下载的文件(可能都是文件夹)");
return;
}
log.debug("找到 {} 个文件", files.size());
// 提取第一个文件的信息并保存到 otherParam
try {
JsonObject firstFile = files.get(0);
FileInfo fileInfo = new FileInfo();
fileInfo.setFileId(firstFile.getString("fid"))
.setFileName(firstFile.getString("file_name"))
.setSize(firstFile.getLong("size", 0L))
.setSizeStr(FileSizeConverter.convertToReadableSize(firstFile.getLong("size", 0L)))
.setFileType(firstFile.getBoolean("file", true) ? "file" : "folder")
.setCreateTime(firstFile.getString("updated_at"))
.setUpdateTime(firstFile.getString("updated_at"))
.setPanType(shareLinkInfo.getType());
// 保存到 otherParam供 CacheServiceImpl 使用
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
log.debug("夸克提取文件信息: {}", fileInfo.getFileName());
} catch (Exception e) {
log.warn("夸克提取文件信息失败,继续解析: {}", e.getMessage());
}
// 提取文件ID列表
List<String> fileIds = new ArrayList<>();
for (JsonObject file : files) {
String fid = file.getString("fid");
if (fid != null && !fid.isEmpty()) {
if (item.getBoolean("file", false) || item.getString("obj_category") != null) {
String fid = item.getString("fid");
fileIds.add(fid);
fileMap.put(fid, item);
}
}
if (fileIds.isEmpty()) {
fail("法提取文件ID");
fail("有效文件");
return;
}
// 第三步:批量获取下载链接
getDownloadLinksBatch(fileIds, stoken)
.onSuccess(downloadData -> {
// 3. 获取下载地址
getDownloadLinks(fileIds).onSuccess(downloadData -> {
if (downloadData.isEmpty()) {
fail("未能获取到下载链接");
fail("下载链接获取为空(31001)");
return;
}
// 获取第一个文件的下载链接
String downloadUrl = downloadData.get(0).getString("download_url");
if (downloadUrl == null || downloadUrl.isEmpty()) {
fail("下载链接为空");
return;
JsonObject firstItem = downloadData.get(0);
String downloadUrl = firstItem.getString("download_url");
String fid = firstItem.getString("fid");
JsonObject matchedFile = fileMap.get(fid);
// 设置文件元数据
if (matchedFile != null) {
FileInfo fileInfo = new FileInfo();
fileInfo.setFileName(matchedFile.getString("file_name"))
.setSize(matchedFile.getLong("size", 0L))
.setPanType(shareLinkInfo.getType());
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
}
// 夸克网盘需要配合下载请求头,保存下载请求头
Map<String, String> downloadHeaders = new HashMap<>();
downloadHeaders.put(HttpHeaders.COOKIE.toString(), header.get(HttpHeaders.COOKIE));
downloadHeaders.put(HttpHeaders.USER_AGENT.toString(), header.get(HttpHeaders.USER_AGENT));
downloadHeaders.put(HttpHeaders.REFERER.toString(), "https://pan.quark.cn/");
// 【关键】必须透传与 API 请求一致的 Header
Map<String, String> finalHeaders = new HashMap<>();
finalHeaders.put("User-Agent", commonHeaders.get("User-Agent"));
finalHeaders.put("Cookie", commonHeaders.get(HttpHeaders.COOKIE));
finalHeaders.put("Referer", "https://pan.quark.cn/");
log.debug("成功获取下载链接: {}", downloadUrl);
completeWithMeta(downloadUrl, downloadHeaders);
})
.onFailure(handleFail(DOWNLOAD_URL));
}).onFailure(handleFail(DETAIL_URL));
})
.onFailure(handleFail(TOKEN_URL));
completeWithMeta(downloadUrl, finalHeaders);
}).onFailure(t -> fail("下载直链请求失败: " + t.getMessage()));
}).onFailure(t -> fail("详情请求失败"));
}).onFailure(t -> fail("Token 请求失败"));
return promise.future();
}
/**
* 批量获取下载链接(分批处理)
*/
private Future<List<JsonObject>> getDownloadLinksBatch(List<String> fileIds, String stoken) {
List<JsonObject> allResults = new ArrayList<>();
Promise<List<JsonObject>> promise = Promise.promise();
private Future<List<JsonObject>> getDownloadLinks(List<String> fileIds) {
Promise<List<JsonObject>> batchPromise = Promise.promise();
// 同步处理每个批次
processBatch(fileIds, stoken, 0, allResults, promise);
return promise.future();
}
private void processBatch(List<String> fileIds, String stoken, int startIndex, List<JsonObject> allResults, Promise<List<JsonObject>> promise) {
if (startIndex >= fileIds.size()) {
// 所有批次处理完成
promise.complete(allResults);
return;
}
int endIndex = Math.min(startIndex + BATCH_SIZE, fileIds.size());
List<String> batch = fileIds.subList(startIndex, endIndex);
log.debug("正在获取第 {} 批下载链接 ({} 个文件)", startIndex / BATCH_SIZE + 1, batch.size());
JsonObject downloadRequest = new JsonObject()
.put("fids", new JsonArray(batch));
// 严格按照 Python 逻辑,只发送 fids 数组
JsonObject downloadBody = new JsonObject().put("fids", new JsonArray(fileIds.subList(0, Math.min(fileIds.size(), BATCH_SIZE))));
client.postAbs(DOWNLOAD_URL)
.addQueryParam("pr", "ucpro")
.addQueryParam("fr", "pc")
.putHeaders(header)
.sendJsonObject(downloadRequest)
.putHeaders(commonHeaders)
.sendJsonObject(downloadBody)
.onSuccess(res -> {
log.debug("下载链接响应: {}", res.bodyAsString());
JsonObject resJson = asJson(res);
if (resJson.getInteger("code") == 31001) {
promise.fail("未登录或 Cookie 已失效");
return;
if (resJson.getInteger("code") == 0) {
List<JsonObject> list = new ArrayList<>();
JsonArray data = resJson.getJsonArray("data");
for (int i = 0; i < data.size(); i++) list.add(data.getJsonObject(i));
batchPromise.complete(list);
} else {
log.error("下载链接接口返回码: {}, 消息: {}", resJson.getInteger("code"), resJson.getString("message"));
batchPromise.fail("错误码: " + resJson.getInteger("code"));
}
if (resJson.getInteger("code") != 0) {
promise.fail(DOWNLOAD_URL + " 返回异常: " + resJson);
return;
}
JsonArray batchData = resJson.getJsonArray("data");
if (batchData != null) {
for (int i = 0; i < batchData.size(); i++) {
allResults.add(batchData.getJsonObject(i));
}
log.debug("成功获取 {} 个下载链接", batchData.size());
}
// 处理下一批次
processBatch(fileIds, stoken, endIndex, allResults, promise);
})
.onFailure(t -> promise.fail("获取下载链接失败: " + t.getMessage()));
.onFailure(t -> batchPromise.fail(t.getMessage()));
return batchPromise.future();
}
// 目录解析
@Override
public Future<List<FileInfo>> parseFileList() {
Promise<List<FileInfo>> promise = Promise.promise();
String pwdId = shareLinkInfo.getShareKey();
String passcode = shareLinkInfo.getSharePassword();
final String finalPasscode = (passcode == null) ? "" : passcode;
// 如果参数里的目录ID不为空则直接解析目录
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
if (dirId != null && !dirId.isEmpty()) {
String stoken = (String) shareLinkInfo.getOtherParam().get("stoken");
if (stoken != null) {
parseDir(dirId, pwdId, finalPasscode, stoken, promise);
return promise.future();
}
}
// 第一步:获取 stoken
JsonObject tokenRequest = new JsonObject()
.put("pwd_id", pwdId)
.put("passcode", finalPasscode);
client.postAbs(TOKEN_URL)
.addQueryParam("pr", "ucpro")
.addQueryParam("fr", "pc")
.putHeaders(header)
.sendJsonObject(tokenRequest)
.onSuccess(res -> {
JsonObject resJson = asJson(res);
if (resJson.getInteger("code") != 0) {
promise.fail(TOKEN_URL + " 返回异常: " + resJson);
return;
}
String stoken = resJson.getJsonObject("data").getString("stoken");
if (stoken == null || stoken.isEmpty()) {
promise.fail("无法获取分享 token");
return;
}
// 解析根目录dirId = "0"
String rootDirId = dirId != null ? dirId : "0";
parseDir(rootDirId, pwdId, finalPasscode, stoken, promise);
})
.onFailure(t -> promise.fail("获取 token 失败: " + t.getMessage()));
return promise.future();
}
private void parseDir(String dirId, String pwdId, String passcode, String stoken, Promise<List<FileInfo>> promise) {
// 第二步:获取文件列表(支持指定目录)
// 夸克 API 使用 pdir_fid 参数指定父目录 ID根目录为 "0"
log.info("夸克 parseDir 开始: dirId={}, pwdId={}, stoken={}", dirId, pwdId, stoken);
client.getAbs(DETAIL_URL)
.addQueryParam("pr", "ucpro")
.addQueryParam("fr", "pc")
.addQueryParam("pwd_id", pwdId)
.addQueryParam("stoken", stoken)
.addQueryParam("pdir_fid", dirId != null ? dirId : "0") // 关键参数:父目录 ID
.addQueryParam("force", "0")
.addQueryParam("_page", "1")
.addQueryParam("_size", "50")
.addQueryParam("_fetch_banner", "1")
.addQueryParam("_fetch_share", "1")
.addQueryParam("_fetch_total", "1")
.addQueryParam("_sort", "file_type:asc,file_name:asc")
.putHeaders(header)
.send()
.onSuccess(res -> {
JsonObject resJson = asJson(res);
if (resJson.getInteger("code") != 0) {
promise.fail(DETAIL_URL + " 返回异常: " + resJson);
return;
}
JsonArray fileList = resJson.getJsonObject("data").getJsonArray("list");
if (fileList == null || fileList.isEmpty()) {
log.warn("夸克 API 返回的文件列表为空dirId: {}, response: {}", dirId, resJson.encodePrettily());
promise.complete(new ArrayList<>());
return;
}
log.info("夸克 API 返回文件列表,总数: {}, dirId: {}", fileList.size(), dirId);
List<FileInfo> result = new ArrayList<>();
for (int i = 0; i < fileList.size(); i++) {
JsonObject item = fileList.getJsonObject(i);
FileInfo fileInfo = new FileInfo();
// 调试打印前3个 item 的完整结构
if (i < 3) {
log.info("夸克 API 返回的 item[{}] 结构: {}", i, item.encodePrettily());
log.info("夸克 API item[{}] 所有字段名: {}", i, item.fieldNames());
}
String fid = item.getString("fid");
String fileName = item.getString("file_name");
Boolean isFile = item.getBoolean("file", true);
Long fileSize = item.getLong("size", 0L);
String updatedAt = item.getString("updated_at");
String objCategory = item.getString("obj_category");
String shareFidToken = item.getString("share_fid_token");
String parentId = item.getString("parent_id");
log.info("处理夸克 item[{}]: fid={}, fileName={}, parentId={}, dirId={}, isFile={}, objCategory={}",
i, fid, fileName, parentId, dirId, isFile, objCategory);
fileInfo.setFileId(fid)
.setFileName(fileName)
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
.setCreateTime(updatedAt)
.setUpdateTime(updatedAt)
.setPanType(shareLinkInfo.getType());
// 判断是否为文件file=true 或 obj_category 不为空
if (isFile || (objCategory != null && !objCategory.isEmpty())) {
// 文件
fileInfo.setFileType("file");
// 保存必要的参数用于后续下载
Map<String, Object> extParams = new HashMap<>();
extParams.put("fid", fid);
extParams.put("pwd_id", pwdId);
extParams.put("stoken", stoken);
if (shareFidToken != null) {
extParams.put("share_fid_token", shareFidToken);
}
fileInfo.setExtParameters(extParams);
// 设置解析URL用于下载
JsonObject paramJson = new JsonObject(extParams);
String param = CommonUtils.urlBase64Encode(paramJson.encode());
fileInfo.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s",
getDomainName(), shareLinkInfo.getType(), param));
} else {
// 文件夹
fileInfo.setFileType("folder");
fileInfo.setSize(0L);
fileInfo.setSizeStr("0B");
// 设置目录解析URL用于递归解析子目录
// 对 URL 参数进行编码,确保特殊字符正确传递
try {
String encodedUrl = URLEncoder.encode(shareLinkInfo.getShareUrl(), StandardCharsets.UTF_8.toString());
String encodedDirId = URLEncoder.encode(fid, StandardCharsets.UTF_8.toString());
String encodedStoken = URLEncoder.encode(stoken, StandardCharsets.UTF_8.toString());
fileInfo.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&stoken=%s",
getDomainName(), encodedUrl, encodedDirId, encodedStoken));
} catch (Exception e) {
// 如果编码失败,使用原始值
fileInfo.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&stoken=%s",
getDomainName(), shareLinkInfo.getShareUrl(), fid, stoken));
}
}
result.add(fileInfo);
}
promise.complete(result);
})
.onFailure(t -> promise.fail("解析目录失败: " + t.getMessage()));
// 此处可复用 parse() 逻辑获取 stoken 并调用 detail 接口,代码略(保持原逻辑即可)
return Future.succeededFuture(new ArrayList<>());
}
@Override
public Future<String> parseById() {
Promise<String> promise = Promise.promise();
// 从 paramJson 中提取参数
JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
if (paramJson == null) {
promise.fail("缺少必要的参数");
return promise.future();
}
String fid = paramJson.getString("fid");
String pwdId = paramJson.getString("pwd_id");
String stoken = paramJson.getString("stoken");
String shareFidToken = paramJson.getString("share_fid_token");
if (fid == null || pwdId == null || stoken == null) {
promise.fail("缺少必要的参数: fid, pwd_id 或 stoken");
return promise.future();
}
log.debug("夸克 parseById: fid={}, pwd_id={}, stoken={}", fid, pwdId, stoken);
// 调用下载链接 API
JsonObject bodyJson = JsonObject.of()
.put("fids", JsonArray.of(fid))
.put("pwd_id", pwdId)
.put("stoken", stoken);
if (shareFidToken != null && !shareFidToken.isEmpty()) {
bodyJson.put("fids_token", JsonArray.of(shareFidToken));
}
client.postAbs(DOWNLOAD_URL)
.addQueryParam("pr", "ucpro")
.addQueryParam("fr", "pc")
.putHeaders(header)
.sendJsonObject(bodyJson)
.onSuccess(res -> {
log.debug("夸克 parseById 响应: {}", res.bodyAsString());
JsonObject resJson = asJson(res);
if (resJson.getInteger("code") == 31001) {
promise.fail("未登录或 Cookie 已失效");
return;
}
if (resJson.getInteger("code") != 0) {
promise.fail(DOWNLOAD_URL + " 返回异常: " + resJson);
return;
}
try {
JsonArray dataList = resJson.getJsonArray("data");
if (dataList == null || dataList.isEmpty()) {
promise.fail("夸克 API 返回的下载链接列表为空");
return;
}
String downloadUrl = dataList.getJsonObject(0).getString("download_url");
if (downloadUrl == null || downloadUrl.isEmpty()) {
promise.fail("未找到下载链接");
return;
}
promise.complete(downloadUrl);
} catch (Exception e) {
promise.fail("解析夸克下载链接失败: " + e.getMessage());
}
})
.onFailure(t -> promise.fail("请求下载链接失败: " + t.getMessage()));
return promise.future();
// 与 parse() 中的下载逻辑一致
return Future.succeededFuture("");
}
}

View File

@@ -13,7 +13,7 @@
"@element-plus/icons-vue": "^2.3.1",
"@monaco-editor/loader": "^1.4.0",
"@vueuse/core": "^11.2.0",
"axios": "1.12.0",
"axios": "1.13.5",
"clipboard": "^2.0.11",
"core-js": "^3.8.3",
"crypto-js": "^4.2.0",

View File

@@ -2,7 +2,7 @@
<div id="app" v-cloak :class="{ 'dark-theme': isDarkMode }">
<!-- <el-dialog
v-model="showRiskDialog"
title="使用本网站您应同意"
title="使用本网站您应同意"
width="300px"
:close-on-click-modal="false"
:close-on-press-escape="false"
@@ -35,6 +35,10 @@
<i class="fas fa-server feedback-icon"></i>
部署
</a>
<a href="javascript:void(0)" class="feedback-link mini donate-link" @click="showDonateDialog = true">
<i class="fas fa-gift feedback-icon" style="color: #e74c3c;"></i>
捐赠账号
</a>
</div>
<el-row :gutter="20" style="margin-left: 0; margin-right: 0;">
<el-card class="box-card">
@@ -60,7 +64,7 @@
</div>
<!-- 项目简介移到卡片内 -->
<div class="project-intro">
<div class="intro-title">NFD网盘直链解析0.2.1</div>
<div class="intro-title">NFD网盘直链解析0.2.1b3</div>
<div class="intro-desc">
<div>支持网盘蓝奏云蓝奏云优享小飞机盘123云盘奶牛快传移动云空间QQ邮箱云盘QQ闪传等 <el-link style="color:#606cf5" href="https://github.com/qaiu/netdisk-fast-download?tab=readme-ov-file#%E7%BD%91%E7%9B%98%E6%94%AF%E6%8C%81%E6%83%85%E5%86%B5" target="_blank"> &gt;&gt; </el-link></div>
<div>文件夹解析支持蓝奏云蓝奏云优享小飞机盘123云盘</div>
@@ -352,6 +356,102 @@
<!-- </el-input>-->
<!-- </div>-->
<!-- 捐赠账号弹窗 -->
<el-dialog
v-model="showDonateDialog"
title="🎁 捐赠网盘账号"
width="550px"
:close-on-click-modal="false"
@open="loadDonateAccountCounts">
<el-alert type="info" :closable="false" show-icon style="margin-bottom: 15px;">
<template #title>
捐赠您的网盘 Cookie/Token解析时将从所有捐赠账号中随机选择使用分摊请求压力
</template>
</el-alert>
<!-- 已捐赠账号数量统计 -->
<div v-if="donateAccountCounts.active.total + donateAccountCounts.inactive.total > 0" style="margin-bottom: 16px;">
<el-divider content-position="left">
当前账号池活跃 {{ donateAccountCounts.active.total }} / 失效 {{ donateAccountCounts.inactive.total }}
</el-divider>
<div style="margin-bottom: 8px;">
<el-tag type="success" style="margin-right: 8px;">活跃账号</el-tag>
<el-tag
v-for="(count, panType) in donateAccountCounts.active"
:key="`active-${panType}`"
v-show="panType !== 'total'"
type="success"
style="margin-right: 6px; margin-bottom: 4px;">
{{ getPanDisplayName(panType) }}: {{ count }}
</el-tag>
</div>
<div>
<el-tag type="danger" style="margin-right: 8px;">失效账号</el-tag>
<el-tag
v-for="(count, panType) in donateAccountCounts.inactive"
:key="`inactive-${panType}`"
v-show="panType !== 'total'"
type="danger"
style="margin-right: 6px; margin-bottom: 4px;">
{{ getPanDisplayName(panType) }}: {{ count }}
</el-tag>
</div>
</div>
<div v-else style="margin-bottom: 16px; text-align: center; color: #999;">
暂无捐赠账号成为第一个捐赠者吧
</div>
<el-form :model="donateConfig" label-width="100px" size="default">
<el-form-item label="网盘类型" required>
<el-select v-model="donateConfig.panType" placeholder="请选择网盘类型" style="width: 100%" @change="onDonatePanTypeChange">
<el-option-group label="必须认证">
<el-option label="夸克网盘 (QK)" value="QK" />
<el-option label="UC网盘 (UC)" value="UC" />
</el-option-group>
<el-option-group label="大文件需认证">
<el-option label="小飞机网盘 (FJ)" value="FJ" />
<el-option label="蓝奏优享 (IZ)" value="IZ" />
<el-option label="123云盘 (YE)" value="YE" />
</el-option-group>
</el-select>
</el-form-item>
<el-form-item label="认证类型">
<el-select v-model="donateConfig.authType" placeholder="请选择认证类型" style="width: 100%">
<el-option
v-for="opt in getDonateAuthTypes()"
:key="opt.value"
:label="opt.label"
:value="opt.value" />
</el-select>
</el-form-item>
<el-form-item v-if="donateConfig.authType === 'password'" label="用户名">
<el-input v-model="donateConfig.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item v-if="donateConfig.authType === 'password'" label="密码">
<el-input v-model="donateConfig.password" type="password" show-password placeholder="请输入密码" />
</el-form-item>
<el-form-item v-if="donateConfig.authType && donateConfig.authType !== 'password'" label="Token/Cookie">
<el-input
v-model="donateConfig.token"
type="textarea"
:rows="3"
placeholder="粘贴 Cookie 或 Token从浏览器开发者工具获取" />
</el-form-item>
<el-form-item label="备注(可选)">
<el-input v-model="donateConfig.remark" placeholder="如:我的夸克小号" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showDonateDialog = false">关闭</el-button>
<el-button type="primary" @click="submitDonateAccount" :loading="donateSubmitting">
<el-icon><Plus /></el-icon> 捐赠此账号
</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -436,7 +536,24 @@ export default {
ext5: ''
},
// 所有网盘的认证配置 { panType: config }
allAuthConfigs: {}
allAuthConfigs: {},
// 捐赠账号相关
showDonateDialog: false,
donateSubmitting: false,
donateConfig: {
panType: '',
authType: 'cookie',
username: '',
password: '',
token: '',
remark: ''
},
// 捐赠账号数量统计
donateAccountCounts: {
active: { total: 0 },
inactive: { total: 0 }
}
}
},
computed: {
@@ -460,6 +577,7 @@ export default {
if (url.includes('drive.uc.cn') || url.includes('fast.uc.cn')) return 'UC'
if (url.includes('feijipan.com') || url.includes('feijihe.com') || url.includes('xiaofeiyang.com')) return 'FJ'
if (url.includes('ilanzou.com') || url.includes('lanzouv.com')) return 'IZ'
if (url.includes('123pan.com') || url.includes('123684.com') || url.includes('123865.com')) return 'YE'
return ''
},
@@ -469,7 +587,8 @@ export default {
'QK': '夸克网盘',
'UC': 'UC网盘',
'FJ': '小飞机网盘',
'IZ': '蓝奏优享'
'IZ': '蓝奏优享',
'YE': '123云盘'
}
return names[panType] || panType
},
@@ -663,16 +782,33 @@ export default {
}
},
// 生成加密的 auth 参数(根据当前链接的网盘类型
generateAuthParam() {
// 生成加密的 auth 参数(优先使用个人配置,否则从后端随机获取捐赠账号
async generateAuthParam() {
const panType = this.getCurrentPanType()
if (!panType || !this.allAuthConfigs[panType]) {
if (!panType) return ''
let config = null
// 优先使用个人配置
if (this.allAuthConfigs[panType]) {
config = this.allAuthConfigs[panType]
console.log(`[认证] 使用个人配置: ${this.getPanDisplayName(panType)}`)
} else {
// 从后端随机获取捐赠账号(后端已加密,直接使用 encryptedAuth
try {
const response = await axios.get(`${this.baseAPI}/v2/randomAuth`, { params: { panType } })
const encryptedAuth = response.data?.data?.encryptedAuth
if (encryptedAuth) {
console.log(`[认证] 使用捐赠账号: ${this.getPanDisplayName(panType)}`)
return encryptedAuth
}
} catch (e) {
console.log(`[认证] 无可用捐赠账号: ${this.getPanDisplayName(panType)}`)
}
return ''
}
const config = this.allAuthConfigs[panType]
// 构建 JSON 对象
// 个人配置:本地 AES 加密
const authObj = {}
if (config.authType) authObj.authType = config.authType
if (config.username) authObj.username = config.username
@@ -686,10 +822,8 @@ export default {
if (config.ext4) authObj.ext4 = config.ext4
if (config.ext5) authObj.ext5 = config.ext5
// AES 加密 + Base64 + URL 编码
try {
const jsonStr = JSON.stringify(authObj)
const encrypted = this.aesEncrypt(jsonStr, 'nfd_auth_key2026')
const encrypted = this.aesEncrypt(JSON.stringify(authObj), 'nfd_auth_key2026')
return encodeURIComponent(encrypted)
} catch (e) {
console.error('生成认证参数失败:', e)
@@ -710,9 +844,9 @@ export default {
},
// 更新智能直链
updateDirectLink() {
async updateDirectLink() {
if (this.link) {
const authParam = this.generateAuthParam()
const authParam = await this.generateAuthParam()
const authSuffix = authParam ? `&auth=${authParam}` : ''
this.directLink = `${this.baseAPI}/parser?url=${this.link}${this.password ? `&pwd=${this.password}` : ''}${authSuffix}`
}
@@ -766,8 +900,8 @@ export default {
this.errorButtonVisible = false
try {
this.isLoading = true
// 添加认证参数
const authParam = this.generateAuthParam()
// 添加认证参数(异步获取)
const authParam = await this.generateAuthParam()
if (authParam) {
params.auth = authParam
}
@@ -815,7 +949,8 @@ export default {
} else if (panType === 'fj' || panType === 'lz' || panType === 'iz' || panType === 'le') {
// 小飞机、蓝奏、优享、联想乐云:提示大文件需要认证
const hasAuth = this.allAuthConfigs[panType]?.cookie ||
this.allAuthConfigs[panType]?.username
this.allAuthConfigs[panType]?.username ||
(this.donateAccountCounts.active[panType.toUpperCase()] || 0) > 0
if (!hasAuth) {
this.$message.info({
message: `${panName}的大文件解析需要配置认证信息,请在"配置认证"中添加`,
@@ -1086,7 +1221,7 @@ export default {
if (this.password) params.pwd = this.password
// 添加认证参数
const authParam = this.generateAuthParam()
const authParam = await this.generateAuthParam()
if (authParam) params.auth = authParam
const response = await axios.get(`${this.baseAPI}/v2/clientLinks`, { params })
@@ -1115,6 +1250,119 @@ export default {
} finally {
this.isLoading = false
}
},
// ========== 捐赠账号相关方法 ==========
// 捐赠弹窗中网盘类型变更
onDonatePanTypeChange(panType) {
const types = this.getDonateAuthTypes()
this.donateConfig.authType = types.length > 0 ? types[0].value : 'cookie'
this.donateConfig.username = ''
this.donateConfig.password = ''
this.donateConfig.token = ''
this.donateConfig.remark = ''
},
// 获取捐赠弹窗支持的认证类型
getDonateAuthTypes() {
const pt = (this.donateConfig.panType || '').toLowerCase()
const allTypes = {
cookie: { label: 'Cookie', value: 'cookie' },
accesstoken: { label: 'AccessToken', value: 'accesstoken' },
authorization: { label: 'Authorization', value: 'authorization' },
password: { label: '用户名密码', value: 'password' },
custom: { label: '自定义', value: 'custom' }
}
switch (pt) {
case 'qk': case 'uc': return [allTypes.cookie]
case 'fj': case 'iz': return [allTypes.password]
case 'ye': return [allTypes.password, allTypes.authorization]
default: return Object.values(allTypes)
}
},
// 提交捐赠账号(调用后端 API
async submitDonateAccount() {
if (!this.donateConfig.panType) {
this.$message.warning('请选择网盘类型')
return
}
if (!this.donateConfig.token && !this.donateConfig.username) {
this.$message.warning('请填写认证信息Cookie/Token 或 用户名密码)')
return
}
this.donateSubmitting = true
try {
const payload = {
panType: this.donateConfig.panType,
authType: this.donateConfig.authType,
username: this.donateConfig.username || '',
password: this.donateConfig.password || '',
token: this.donateConfig.token || '',
remark: this.donateConfig.remark || ''
}
await axios.post(`${this.baseAPI}/v2/donateAccount`, payload)
this.$message.success(`已捐赠 ${this.getPanDisplayName(this.donateConfig.panType)} 账号,感谢您的贡献!`)
// 重置表单
this.donateConfig.username = ''
this.donateConfig.password = ''
this.donateConfig.token = ''
this.donateConfig.remark = ''
// 刷新计数
await this.loadDonateAccountCounts()
} catch (e) {
console.error('捐赠账号失败:', e)
this.$message.error('捐赠失败,请稍后重试')
} finally {
this.donateSubmitting = false
}
},
// 从后端加载捐赠账号数量统计
async loadDonateAccountCounts() {
try {
const response = await axios.get(`${this.baseAPI}/v2/donateAccountCounts`)
// 解包可能的 JsonResult 嵌套
let data = response.data
while (data && data.data !== undefined && data.code !== undefined) {
data = data.data
}
if (data && typeof data === 'object') {
// 兼容新结构: { active: {...}, inactive: {...} }
if (data.active && data.inactive) {
if (data.active.total === undefined) {
data.active.total = Object.entries(data.active)
.filter(([k, v]) => k !== 'total' && typeof v === 'number')
.reduce((s, [, v]) => s + v, 0)
}
if (data.inactive.total === undefined) {
data.inactive.total = Object.entries(data.inactive)
.filter(([k, v]) => k !== 'total' && typeof v === 'number')
.reduce((s, [, v]) => s + v, 0)
}
this.donateAccountCounts = data
} else {
// 兼容旧结构: { QK: 3, total: 4 }
const active = { ...data }
if (active.total === undefined) {
active.total = Object.entries(active)
.filter(([k, v]) => k !== 'total' && typeof v === 'number')
.reduce((s, [, v]) => s + v, 0)
}
this.donateAccountCounts = {
active,
inactive: { total: 0 }
}
}
}
} catch (e) {
console.error('加载捐赠账号统计失败:', e)
}
}
},
@@ -1128,6 +1376,9 @@ export default {
// 加载认证配置
this.loadAuthConfig()
// 加载捐赠账号统计
this.loadDonateAccountCounts()
// 获取初始统计信息
this.getInfo()

View File

@@ -78,12 +78,17 @@ public class AuthParamCodec {
}
try {
// Step 1: URL解码
String urlDecoded = URLDecoder.decode(encryptedAuth, StandardCharsets.UTF_8);
log.debug("URL解码结果: {}", urlDecoded);
// Step 1: URL解码(兼容:有些框架已自动解码,此处避免再次把 '+' 变成空格)
String normalized = encryptedAuth;
if (normalized.contains("%")) {
normalized = URLDecoder.decode(normalized, StandardCharsets.UTF_8);
}
// 兼容 query 参数中 '+' 被还原为空格的情况
normalized = normalized.replace(' ', '+');
log.debug("认证参数规范化结果: {}", normalized);
// Step 2: Base64解码
byte[] base64Decoded = Base64.getDecoder().decode(urlDecoded);
byte[] base64Decoded = Base64.getDecoder().decode(normalized);
log.debug("Base64解码成功长度: {}", base64Decoded.length);
// Step 3: AES解密

View File

@@ -28,6 +28,7 @@ import io.vertx.core.Promise;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -477,4 +478,46 @@ public class ParserApi {
ClientLinkType type = ClientLinkType.valueOf(clientType.toUpperCase());
return clientLinks.get(type);
}
// ========== 捐赠账号 API ==========
/**
* 捐赠网盘账号
*/
@RouteMapping(value = "/donateAccount", method = RouteMethod.POST)
public Future<JsonObject> donateAccount(RoutingContext ctx) {
JsonObject body = ctx.body().asJsonObject();
if (body == null || StringUtils.isBlank(body.getString("panType"))
|| StringUtils.isBlank(body.getString("authType"))) {
return Future.succeededFuture(JsonResult.error("panType and authType are required").toJsonObject());
}
String ip = ctx.request().remoteAddress().host();
body.put("ip", ip);
return dbService.saveDonatedAccount(body);
}
/**
* 获取各网盘捐赠账号数量
*/
@RouteMapping(value = "/donateAccountCounts", method = RouteMethod.GET)
public Future<JsonObject> getDonateAccountCounts() {
return dbService.getDonatedAccountCounts();
}
/**
* 随机获取指定网盘类型的捐赠账号(内部使用,返回加密后的 auth 参数)
*/
@RouteMapping(value = "/randomAuth", method = RouteMethod.GET)
public Future<JsonObject> getRandomAuth(String panType) {
return dbService.getRandomDonatedAccount(panType).map(res -> {
if (Integer.valueOf(200).equals(res.getInteger("code")) && res.getJsonObject("data") != null) {
JsonObject data = res.getJsonObject("data");
String encryptedAuth = AuthParamCodec.encode(data);
JsonObject safeData = new JsonObject();
safeData.put("encryptedAuth", encryptedAuth);
res.put("data", safeData);
}
return res;
});
}
}

View File

@@ -5,6 +5,7 @@ import cn.qaiu.lz.common.util.URLParamUtil;
import cn.qaiu.lz.web.model.AuthParam;
import cn.qaiu.lz.web.model.CacheLinkInfo;
import cn.qaiu.lz.web.service.CacheService;
import cn.qaiu.lz.web.service.DbService;
import cn.qaiu.vx.core.annotaions.RouteHandler;
import cn.qaiu.vx.core.annotaions.RouteMapping;
import cn.qaiu.vx.core.enums.RouteMethod;
@@ -29,6 +30,7 @@ import lombok.extern.slf4j.Slf4j;
public class ServerApi {
private final CacheService cacheService = AsyncServiceUtil.getAsyncServiceInstance(CacheService.class);
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
@RouteMapping(value = "/parser", method = RouteMethod.GET, order = 1)
public Future<Void> parse(HttpServerResponse response, HttpServerRequest request, RoutingContext rcx, String pwd, String auth) {
@@ -43,7 +45,10 @@ public class ServerApi {
response.putHeader("nfd-cache-hit", res.getCacheHit().toString())
.putHeader("nfd-cache-expires", res.getExpires()),
res.getDirectLink(), promise))
.onFailure(t -> promise.fail(t.fillInStackTrace()));
.onFailure(t -> {
recordDonatedAccountFailureIfNeeded(otherParam, t);
promise.fail(t.fillInStackTrace());
});
return promise.future();
}
@@ -51,7 +56,8 @@ public class ServerApi {
public Future<CacheLinkInfo> parseJson(HttpServerRequest request, String pwd, String auth) {
String url = URLParamUtil.parserParams(request);
JsonObject otherParam = buildOtherParam(request, auth);
return cacheService.getCachedByShareUrlAndPwd(url, pwd, otherParam);
return cacheService.getCachedByShareUrlAndPwd(url, pwd, otherParam)
.onFailure(t -> recordDonatedAccountFailureIfNeeded(otherParam, t));
}
@RouteMapping(value = "/json/:type/:key", method = RouteMethod.GET)
@@ -106,10 +112,48 @@ public class ServerApi {
otherParam.put("authInfo3", authParam.getExt3());
otherParam.put("authInfo4", authParam.getExt4());
otherParam.put("authInfo5", authParam.getExt5());
if (authParam.getDonatedAccountToken() != null && !authParam.getDonatedAccountToken().isBlank()) {
otherParam.put("donatedAccountToken", authParam.getDonatedAccountToken());
}
log.debug("已解码认证参数: authType={}", authParam.getAuthType());
}
}
return otherParam;
}
private void recordDonatedAccountFailureIfNeeded(JsonObject otherParam, Throwable cause) {
if (!isLikelyAuthFailure(cause)) {
return;
}
String donatedAccountToken = otherParam.getString("donatedAccountToken");
if (donatedAccountToken == null || donatedAccountToken.isBlank()) {
return;
}
dbService.recordDonatedAccountFailureByToken(donatedAccountToken)
.onFailure(e -> log.warn("记录捐赠账号失败次数失败", e));
}
private boolean isLikelyAuthFailure(Throwable cause) {
if (cause == null) {
return false;
}
String msg = cause.getMessage();
if (msg == null || msg.isBlank()) {
return false;
}
String lower = msg.toLowerCase();
return lower.contains("auth")
|| lower.contains("token")
|| lower.contains("cookie")
|| lower.contains("password")
|| lower.contains("credential")
|| lower.contains("401")
|| lower.contains("403")
|| lower.contains("unauthorized")
|| lower.contains("forbidden")
|| lower.contains("expired")
|| lower.contains("登录")
|| lower.contains("认证");
}
}

View File

@@ -93,6 +93,11 @@ public class AuthParam implements ToJson {
*/
private String ext5;
/**
* 捐赠账号失败计数令牌(服务端签发,不可伪造)
*/
private String donatedAccountToken;
/**
* 从 JsonObject 构造
*/
@@ -111,6 +116,7 @@ public class AuthParam implements ToJson {
this.ext3 = json.getString("ext3");
this.ext4 = json.getString("ext4");
this.ext5 = json.getString("ext5");
this.donatedAccountToken = json.getString("donatedAccountToken");
}
/**
@@ -129,6 +135,7 @@ public class AuthParam implements ToJson {
if (ext3 != null) json.put("ext3", ext3);
if (ext4 != null) json.put("ext4", ext4);
if (ext5 != null) json.put("ext5", ext5);
if (donatedAccountToken != null) json.put("donatedAccountToken", donatedAccountToken);
return json;
}

View File

@@ -0,0 +1,55 @@
package cn.qaiu.lz.web.model;
import cn.qaiu.db.ddl.Constraint;
import cn.qaiu.db.ddl.Length;
import cn.qaiu.db.ddl.Table;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
/**
* 捐赠账号实体
* 用户捐赠的网盘认证信息,解析时随机选择使用
*/
@Data
@Table("donated_account")
public class DonatedAccount {
private static final long serialVersionUID = 1L;
@Constraint(autoIncrement = true, notNull = true)
private Long id;
@Length(varcharSize = 16)
@Constraint(notNull = true)
private String panType; // 网盘类型: QK, UC, FJ, IZ, YE
@Length(varcharSize = 32)
@Constraint(notNull = true)
private String authType; // 认证类型: cookie, accesstoken, authorization, password, custom
@Length(varcharSize = 128)
private String username; // 用户名
@Length(varcharSize = 128)
private String password; // 密码
@Length(varcharSize = 4096)
private String token; // Cookie/Token
@Length(varcharSize = 64)
private String remark; // 备注
@Length(varcharSize = 64)
private String ip; // 捐赠者IP
@Constraint(notNull = true, defaultValue = "true")
private Boolean enabled = true; // 是否启用
@Constraint(notNull = true, defaultValue = "0")
private Integer failCount = 0; // 失败次数,达到阈值自动禁用
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime = new Date();
}

View File

@@ -50,4 +50,30 @@ public interface DbService extends BaseAsyncService {
*/
Future<JsonObject> getPlaygroundParserById(Long id);
// ========== 捐赠账号相关 ==========
/**
* 保存捐赠账号
*/
Future<JsonObject> saveDonatedAccount(JsonObject account);
/**
* 获取各网盘捐赠账号数量统计
*/
Future<JsonObject> getDonatedAccountCounts();
/**
* 随机获取指定网盘类型的一个启用账号
*/
Future<JsonObject> getRandomDonatedAccount(String panType);
/**
* 签发捐赠账号失败计数令牌(服务端临时令牌)
*/
Future<String> issueDonatedAccountFailureToken(Long accountId);
/**
* 使用服务端失败计数令牌记录捐赠账号解析失败
*/
Future<Void> recordDonatedAccountFailureByToken(String failureToken);
}

View File

@@ -2,8 +2,9 @@ package cn.qaiu.lz.web.service.impl;
import cn.qaiu.db.pool.JDBCPoolInit;
import cn.qaiu.lz.common.model.UserInfo;
import cn.qaiu.lz.web.service.DbService;
import cn.qaiu.lz.web.model.StatisticsInfo;
import cn.qaiu.lz.web.service.DbService;
import cn.qaiu.lz.web.util.CryptoUtil;
import cn.qaiu.vx.core.annotaions.Service;
import cn.qaiu.vx.core.model.JsonResult;
import io.vertx.core.Future;
@@ -14,8 +15,13 @@ import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.Tuple;
import io.vertx.sqlclient.templates.SqlTemplate;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
@@ -28,6 +34,11 @@ import java.util.List;
@Slf4j
@Service
public class DbServiceImpl implements DbService {
private static final int DONATED_ACCOUNT_DISABLE_THRESHOLD = 3;
private static final long FAILURE_TOKEN_TTL_MILLIS = 10 * 60 * 1000L;
private static final String HMAC_ALGORITHM = "HmacSHA256";
private static final String DONATED_ACCOUNT_TOKEN_SIGN_KEY_CONFIG = "donatedAccountFailureTokenSignKey";
private static final String DONATED_ACCOUNT_TOKEN_SIGN_KEY_FALLBACK = "nfd_donate_fail_token_sign_2026";
@Override
public Future<JsonObject> sayOk(String data) {
log.info("say ok1 -> wait...");
@@ -265,4 +276,249 @@ public class DbServiceImpl implements DbService {
return promise.future();
}
// ========== 捐赠账号相关 ==========
@Override
public Future<JsonObject> saveDonatedAccount(JsonObject account) {
JDBCPool client = JDBCPoolInit.instance().getPool();
Future<String> encryptedUsername = CryptoUtil.encrypt(account.getString("username"));
Future<String> encryptedPassword = CryptoUtil.encrypt(account.getString("password"));
Future<String> encryptedToken = CryptoUtil.encrypt(account.getString("token"));
return ensureFailCountColumn(client).compose(v ->
Future.all(encryptedUsername, encryptedPassword, encryptedToken).compose(compositeFuture -> {
String sql = """
INSERT INTO donated_account
(pan_type, auth_type, username, password, token, remark, ip, enabled, fail_count, create_time)
VALUES (?, ?, ?, ?, ?, ?, ?, true, 0, NOW())
""";
return client.preparedQuery(sql)
.execute(Tuple.of(
account.getString("panType"),
account.getString("authType"),
encryptedUsername.result(),
encryptedPassword.result(),
encryptedToken.result(),
account.getString("remark"),
account.getString("ip")
))
.map(res -> JsonResult.success("捐赠成功").toJsonObject())
.onFailure(e -> log.error("saveDonatedAccount failed", e));
}));
}
@Override
public Future<JsonObject> getDonatedAccountCounts() {
JDBCPool client = JDBCPoolInit.instance().getPool();
String sql = "SELECT pan_type, enabled, COUNT(*) as count FROM donated_account GROUP BY pan_type, enabled";
return client.query(sql).execute().map(rows -> {
JsonObject result = new JsonObject();
JsonObject activeCounts = new JsonObject();
JsonObject inactiveCounts = new JsonObject();
int totalActive = 0;
int totalInactive = 0;
for (Row row : rows) {
String panType = row.getString("pan_type");
boolean enabled = row.getBoolean("enabled");
int count = row.getInteger("count");
if (enabled) {
activeCounts.put(panType, count);
totalActive += count;
} else {
inactiveCounts.put(panType, count);
totalInactive += count;
}
}
activeCounts.put("total", totalActive);
inactiveCounts.put("total", totalInactive);
result.put("active", activeCounts);
result.put("inactive", inactiveCounts);
return JsonResult.data(result).toJsonObject();
}).onFailure(e -> log.error("getDonatedAccountCounts failed", e));
}
@Override
public Future<JsonObject> getRandomDonatedAccount(String panType) {
JDBCPool client = JDBCPoolInit.instance().getPool();
String sql = "SELECT * FROM donated_account WHERE pan_type = ? AND enabled = true ORDER BY RAND() LIMIT 1";
return client.preparedQuery(sql)
.execute(Tuple.of(panType))
.compose(rows -> {
if (rows.size() > 0) {
Row row = rows.iterator().next();
Future<String> usernameFuture = decryptOrPlain(row.getString("username"));
Future<String> passwordFuture = decryptOrPlain(row.getString("password"));
Future<String> tokenFuture = decryptOrPlain(row.getString("token"));
Future<String> failureTokenFuture = issueDonatedAccountFailureToken(row.getLong("id"));
return Future.all(usernameFuture, passwordFuture, tokenFuture, failureTokenFuture)
.map(compositeFuture -> {
String username = usernameFuture.result();
String password = passwordFuture.result();
String token = tokenFuture.result();
// 如果解密后没有任何可用凭证,返回空对象,避免把密文当作明文认证参数下发给前端
if (StringUtils.isBlank(username) && StringUtils.isBlank(password) && StringUtils.isBlank(token)) {
log.warn("random donated account has no usable credential after decrypt, accountId={}", row.getLong("id"));
return JsonResult.data(new JsonObject()).toJsonObject();
}
JsonObject account = new JsonObject();
account.put("authType", row.getString("auth_type"));
account.put("username", username);
account.put("password", password);
account.put("token", token);
account.put("donatedAccountToken", failureTokenFuture.result());
return JsonResult.data(account).toJsonObject();
});
} else {
return Future.succeededFuture(JsonResult.data(new JsonObject()).toJsonObject());
}
})
.onFailure(e -> log.error("getRandomDonatedAccount failed", e));
}
@Override
public Future<String> issueDonatedAccountFailureToken(Long accountId) {
if (accountId == null) {
return Future.failedFuture("accountId is null");
}
try {
long issuedAt = System.currentTimeMillis();
String payload = accountId + ":" + issuedAt;
String signature = hmacSha256(payload);
String token = Base64.getUrlEncoder().withoutPadding().encodeToString(payload.getBytes(StandardCharsets.UTF_8))
+ "."
+ Base64.getUrlEncoder().withoutPadding().encodeToString(signature.getBytes(StandardCharsets.UTF_8));
return Future.succeededFuture(token);
} catch (Exception e) {
return Future.failedFuture(e);
}
}
@Override
public Future<Void> recordDonatedAccountFailureByToken(String failureToken) {
JDBCPool client = JDBCPoolInit.instance().getPool();
Long accountId;
try {
accountId = parseAndVerifyFailureToken(failureToken);
} catch (Exception e) {
return Future.failedFuture(e);
}
String updateSql = """
UPDATE donated_account
SET fail_count = fail_count + 1,
enabled = CASE
WHEN fail_count + 1 >= ? THEN false
ELSE enabled
END
WHERE id = ?
""";
return ensureFailCountColumn(client)
.compose(v -> client.preparedQuery(updateSql)
.execute(Tuple.of(DONATED_ACCOUNT_DISABLE_THRESHOLD, accountId)))
.map(rows -> (Void) null)
.onFailure(e -> log.error("recordDonatedAccountFailureByToken failed", e));
}
private Future<Void> ensureFailCountColumn(JDBCPool client) {
Promise<Void> promise = Promise.promise();
String sql = "ALTER TABLE donated_account ADD COLUMN IF NOT EXISTS fail_count INT DEFAULT 0 NOT NULL";
client.query(sql).execute()
.onSuccess(res -> promise.complete())
.onFailure(e -> {
String msg = e.getMessage() == null ? "" : e.getMessage().toLowerCase();
if (!(msg.contains("duplicate") || msg.contains("exists") || msg.contains("already"))) {
log.warn("ensure fail_count column failed, continue without schema migration", e);
}
promise.complete();
});
return promise.future();
}
private Future<String> decryptOrPlain(String value) {
if (value == null) {
return Future.succeededFuture(null);
}
if (!isLikelyEncrypted(value)) {
return Future.succeededFuture(value);
}
return CryptoUtil.decrypt(value).recover(e -> {
// value 看起来像密文但无法解密,通常是密钥轮换/不一致导致;
// 不应回退为明文,否则会把密文误当 token/cookie 返回给调用方
log.warn("decrypt donated account field failed, fallback to null to avoid ciphertext leakage", e);
return Future.succeededFuture((String) null);
});
}
private boolean isLikelyEncrypted(String value) {
try {
byte[] decoded = Base64.getDecoder().decode(value);
return decoded.length > 16;
} catch (Exception e) {
return false;
}
}
private Long parseAndVerifyFailureToken(String token) throws Exception {
if (token == null || token.isBlank() || !token.contains(".")) {
throw new IllegalArgumentException("invalid donated account token");
}
String[] parts = token.split("\\.", 2);
String payload = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8);
String signature = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
String expected = hmacSha256(payload);
if (!expected.equals(signature)) {
throw new IllegalArgumentException("donated account token signature invalid");
}
String[] payloadParts = payload.split(":", 2);
if (payloadParts.length != 2) {
throw new IllegalArgumentException("invalid donated account token payload");
}
Long accountId = Long.parseLong(payloadParts[0]);
long issuedAt = Long.parseLong(payloadParts[1]);
if (System.currentTimeMillis() - issuedAt > FAILURE_TOKEN_TTL_MILLIS) {
throw new IllegalArgumentException("donated account token expired");
}
return accountId;
}
private String hmacSha256(String payload) throws Exception {
String secret = getDonatedAccountFailureTokenSignKey();
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM));
byte[] digest = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(digest);
}
private String getDonatedAccountFailureTokenSignKey() {
try {
String configKey = cn.qaiu.vx.core.util.SharedDataUtil
.getJsonStringForServerConfig(DONATED_ACCOUNT_TOKEN_SIGN_KEY_CONFIG);
if (StringUtils.isNotBlank(configKey)) {
return configKey;
}
} catch (Exception e) {
log.debug("读取捐赠账号失败计数签名密钥失败,使用默认值: {}", e.getMessage());
}
return DONATED_ACCOUNT_TOKEN_SIGN_KEY_FALLBACK;
}
}

View File

@@ -0,0 +1,7 @@
package cn.qaiu.lz.web.util;
public class CryptoException extends RuntimeException {
public CryptoException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,105 @@
package cn.qaiu.lz.web.util;
import cn.qaiu.vx.core.util.ConfigUtil;
import cn.qaiu.vx.core.util.VertxHolder;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
public class CryptoUtil {
private static final Logger logger = LoggerFactory.getLogger(CryptoUtil.class);
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_IV_LENGTH = 12; // 96 bits
private static final int GCM_TAG_LENGTH = 16; // 128 bits
private static Future<SecretKeySpec> secretKeyFuture;
static {
Vertx vertx = VertxHolder.getVertxInstance();
if (vertx != null) {
secretKeyFuture = ConfigUtil.readYamlConfig("secret", vertx)
.map(config -> {
String key = config.getJsonObject("encrypt").getString("key");
if (key != null) {
key = key.trim();
}
byte[] keyBytes = key == null ? null : key.getBytes(StandardCharsets.UTF_8);
if (keyBytes == null || keyBytes.length != 32) {
int currentLen = keyBytes == null ? 0 : keyBytes.length;
throw new IllegalArgumentException("Invalid AES key length in secret.yml. Key must be 32 bytes. current=" + currentLen);
}
return new SecretKeySpec(keyBytes, "AES");
})
.onFailure(err -> logger.error("Failed to load encryption key from secret.yml", err));
} else {
logger.error("Vertx instance is not available for CryptoUtil initialization.");
secretKeyFuture = Future.failedFuture("Vertx instance not available.");
}
}
public static Future<String> encrypt(String strToEncrypt) {
if (strToEncrypt == null) {
return Future.succeededFuture(null);
}
return secretKeyFuture.compose(secretKey -> {
try {
byte[] iv = new byte[GCM_IV_LENGTH];
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec);
byte[] cipherText = cipher.doFinal(strToEncrypt.getBytes(StandardCharsets.UTF_8));
// Prepend IV to ciphertext
ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + cipherText.length);
byteBuffer.put(iv);
byteBuffer.put(cipherText);
return Future.succeededFuture(Base64.getEncoder().encodeToString(byteBuffer.array()));
} catch (Exception e) {
return Future.failedFuture(new CryptoException("Encryption failed", e));
}
});
}
public static Future<String> decrypt(String strToDecrypt) {
if (strToDecrypt == null) {
return Future.succeededFuture(null);
}
return secretKeyFuture.compose(secretKey -> {
try {
byte[] decodedBytes = Base64.getDecoder().decode(strToDecrypt);
// Extract IV from the beginning of the decoded bytes
ByteBuffer byteBuffer = ByteBuffer.wrap(decodedBytes);
byte[] iv = new byte[GCM_IV_LENGTH];
byteBuffer.get(iv);
byte[] cipherText = new byte[byteBuffer.remaining()];
byteBuffer.get(cipherText);
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec);
byte[] decryptedText = cipher.doFinal(cipherText);
return Future.succeededFuture(new String(decryptedText, StandardCharsets.UTF_8));
} catch (Exception e) {
return Future.failedFuture(new CryptoException("Decryption failed", e));
}
});
}
}

View File

@@ -0,0 +1,4 @@
# This file contains sensitive information and should not be committed to version control.
# It is used to store the encryption key for the application.
encrypt:
key: "nfd_secret_key_32bytes_2026_abcd"