10 Commits
Author SHA1 Message Date
Bao-qing 228b5017c1 fix: 修正 API_BASE_URL 和 SITE_BASE_URL 的使用,增强 JSON 响应解析错误处理 2026-07-05 23:40:29 +08:00
baoqing 4053b0a4f5 feat: add folder upload & make ls auto-refreshing 2026-05-13 00:27:22 +08:00
保清 3f83e4fce6 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	README.md
2026-02-08 17:54:28 +08:00
保清 cb737c0d95 分离出core和cli,更新文档,打包脚本 2026-02-08 17:48:14 +08:00
保清 18fcfc2c01 前后分离 2026-02-08 13:17:56 +08:00
保清 aa9d54390f cdbyid 2026-02-08 01:25:43 +08:00
保清andGitHub 6b01ea3ff7 Add MIT License with No Commercial Use
Updated license text to clarify the scope of use and project reference.
2025-10-13 16:35:44 +08:00
保清andGitHub c62fee2220 Add MIT License with No Commercial Use
Added MIT License with No Commercial Use restrictions.
2025-10-13 16:30:59 +08:00
保清andGitHub 3f83c568ee 更新 README.md 2025-10-13 16:23:18 +08:00
保清 4d402f7889 修改打包命令 2025-10-09 19:40:27 +08:00
8 changed files with 2732 additions and 1217 deletions
+8 -1
View File
@@ -1,3 +1,10 @@
123pan_config.json
download/*
123pan.txt
123pan.txt
dist/*
build/*
123pan.dist/*
.idea/*
123pan.exe
123pan.spec
android.py
-86
View File
@@ -1,86 +0,0 @@
// ==UserScript==
// @name 123云盘下载辅助
// @namespace https://github.com/Bao-qing/123pan
// @version 0.3
// @description 123 Cloud Drive Unlimited Flow
// @match https://www.123pan.com/*
// @match https://www.123pan.cn/*
// @match https://www.123865.com/*
// @match https://www.123684.com/*
// @grant none
// @author Qing
// ==/UserScript==
(function () {
// 重写 XMLHttpRequest
const originalXHR = window.XMLHttpRequest;
function newXHR() {
const realXHR = new originalXHR();
realXHR.open = function (method, url, async, user, password) {
this._url = url; // 记录请求的 URL
return originalXHR.prototype.open.apply(this, arguments);
};
realXHR.setRequestHeader = function (header, value) {
let headers = {
"user-agent": "123pan/v2.4.0(Android_7.1.2;Xiaomi)",
//"loginuuid": generateUUIDHex(),
"platform": "android",
"app-version": "61",
"x-app-version": "2.4.0"
}
// 如果header在列表中,则修改
if (header.toLowerCase() in headers) {
value = headers[header.toLowerCase()];
} else {
console.log('header:', header);
}
return originalXHR.prototype.setRequestHeader.apply(this, arguments);
};
// 拦截响应内容,修改 DownloadUrl以适应网页端下载
realXHR.send = function () {
const xhrInstance = this;
this.addEventListener('readystatechange', function () {
let origin_url;
let new_url_no_redirect;
let base64data;
if (xhrInstance.readyState === 4 && xhrInstance.status === 200) {
// 解析响应的 JSON
let responseText = xhrInstance.responseText;
let responseJSON = JSON.parse(responseText);
console.log('Original Response:', responseJSON);
// 修改 DownloadUrl
if (responseJSON.data && responseJSON.data.DownloadUrl) {
origin_url = responseJSON.data.DownloadUrl;
new_url_no_redirect = origin_url + "&auto_redirect=0";
base64data = btoa(new_url_no_redirect);
responseJSON.data.DownloadUrl = "https://web-pro2.123952.com/download-v2/?params=" + base64data + "&is_s3=0";
console.log('Modified DownloadUrl:', responseJSON.data.DownloadUrl);
}
// 将修改后的 JSON 转为字符串
let modifiedResponseText = JSON.stringify(responseJSON);
// 使用 defineProperty 重写 responseText
Object.defineProperty(xhrInstance, 'responseText', {
get: function () {
return modifiedResponseText;
}
});
console.log('Modified Response:', modifiedResponseText);
}
});
return originalXHR.prototype.send.apply(this, arguments);
};
return realXHR;
}
window.XMLHttpRequest = newXHR;
})();
-1016
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
MIT License with No Commercial Use
Copyright (c) 2023 [Bao-qing]
特此免费授予任何获得本项目及相关文档文件(以下简称"项目")副本的人无限制使用的权利,
包括但不限于使用、复制、修改、合并、发布、分发、再许可的权利,但须满足以下条件:
1. 禁止将本项目用于任何商业目的,包括但不限于售卖、付费授权等
2. 禁止声称与本项目作者有任何官方关联或授权
3. 本软件仅限个人学习和技术研究用途
上述版权声明和本许可声明应包含在软件的所有副本或重要部分中。
本项目按"原样"提供,不提供任何形式的明示或暗示的保证。在任何情况下,作者或版权持有人均不对因软件或使用而引起的任何索赔、损害或其他责任负责。
此许可条款追溯适用于本仓库的所有历史版本,包括首次提交以来的所有代码。
+327 -113
View File
@@ -1,14 +1,43 @@
# 123Pan 下载工具
# 1、123Pan Cli工具
123 网盘命令行工具,支持列出文件、下载、上传、分享、删除、创建目录及回收站管理。Android 客户端或 Web 协议,便于在本地批量管理与下载文件。
123 网盘命令行工具,支持列出文件、下载、上传、分享、删除、创建目录及回收站管理。
安卓客户端协议不受下载流量限制(推荐使用)。
<!-- TOC -->
* [1、123Pan Cli工具](#1123pan-cli工具)
* [1.1 特性](#11-特性)
* [1.2 脚本环境要求](#12-脚本环境要求)
* [1.3 安装与运行](#13-安装与运行)
* [1.3.1 脚本运行](#131-脚本运行)
* [1.3.2 下载release版](#132-下载release版)
* [1.4 配置文件(JSON](#14-配置文件json)
* [1.5 常用命令(交互式)](#15-常用命令交互式)
* [2、123Pan接口模块(pan123_core.py](#2123pan接口模块pan123_corepy)
* [2.1 核心类:`Pan123Core`](#21-核心类pan123core)
* [2.1.1 属性说明](#211-属性说明)
* [2.1.2 方法清单](#212-方法清单)
* [2.1.2.1 1)登录操作](#2121-1登录操作)
* [2.1.2.2 2)配置管理](#2122-2配置管理)
* [2.1.2.3 3)目录操作](#2123-3目录操作)
* [2.1.2.4 4)文件操作](#2124-4文件操作)
* [2.1.2.5 5)用户信息](#2125-5用户信息)
* [2.2 工具类:`Pan123Tool`](#22-工具类pan123tool)
* [2.2.1 属性说明](#221-属性说明)
* [2.2.2 方法清单](#222-方法清单)
* [2.2.2.1 1)配置管理](#2221-1配置管理)
* [2.2.2.2 2)文件下载](#2222-2文件下载)
* [2.2.2.3 3)文件上传](#2223-3文件上传)
* [2.3 全局配置参数](#23-全局配置参数)
* [2.3.1 协议相关](#231-协议相关)
* [2.3.2 设备伪装](#232-设备伪装)
* [2.4 错误码说明](#24-错误码说明)
* [2.5 典型使用示例](#25-典型使用示例)
* [3、下载说明](#3下载说明)
* [4、注意事项](#4注意事项)
* [5、免责声明](#5免责声明)
<!-- TOC -->
123download.js 是网页端下载油猴脚本最初版本,仍然可以使用,仅保留最基本的解锁下载功能,不再更新。
可以参考其他项目:[123云盘解锁 (@QingJ)](https://greasyfork.org/zh-CN/scripts/519353-123%E4%BA%91%E7%9B%98%E8%A7%A3%E9%94%81)
[123 云盘会员青春版 (@hmjz100)](https://greasyfork.org/zh-CN/scripts/513528-123-%E4%BA%91%E7%9B%98%E4%BC%9A%E5%91%98%E9%9D%92%E6%98%A5%E7%89%88)
## 1.1 特性
## 特性
- 登录 / 登出
- 列出当前目录文件(ls
- 切换目录(cd)与刷新(refresh / re
@@ -22,28 +51,36 @@
- 协议切换(protocol android|web
- 支持保存配置到 JSON 文件(authorization、device/os、protocol 等)
## 脚本环境要求
- Python 3.7+
- 依赖库:requests
## 1.2 脚本环境要求
- Python 3
- 依赖库:requests
安装:
```bash
pip install requests
```
```bash
pip install requests
```
## 1.3 安装与运行
### 1.3.1 脚本运行
## 安装与运行
### 脚本运行
1. 克隆或下载本仓库到本地。
2. 进入项目目录。
3. 运行脚本:
```bash
python 123pan.py
```
启动后会提示输入用户名 / 密码,或自动读取配置文件(默认 `123pan_config.json` 或 `123pan.txt`,脚本内部根据传入参数使用该文件)。
### 下载release版
根据系统下载对应的 release 版本,解压后运行 `123pan.exe`Windows)或 `123pan`Linux)。
## 配置文件(JSON
```bash
python pan123_cli.py
```
启动后会提示输入用户名 / 密码,或自动读取配置文件(默认 `123pan_config.json`,脚本内部根据传入参数使用该文件)。
### 1.3.2 下载release版
根据系统下载对应的 release 版本,解压后运行 `123pan.exe`Windows)或 `123pan`Linux)。
## 1.4 配置文件(JSON
脚本会读取并保存一个配置文件(示例 `123pan_config.json`),保存登录状态与偏好,格式示例:
```json
```json
{
"userName": "your_username",
"passWord": "your_password",
@@ -51,112 +88,289 @@
"deviceType": "M2007J20CI",
"osVersion": "Android_10",
"protocol": "android"
}
```
}
```
注意:保存密码或 token 到本地会有安全风险,请在可信环境下使用并妥善保护该文件。
## 常用命令(交互式)
## 1.5 常用命令(交互式)
在脚本交互提示符中输入命令,部分带参数:
- 直接输入编号
- 若编号对应文件夹 → 进入该文件夹
- 若编号对应文件 → 直接下载该文件
- ls
- 显示当前目录的文件与文件夹列表。
| 指令 | 用法示例 | 功能说明 |
|-----------------------------|----------------------------------------|----------------------------------|
| 直接输入编号 | `3` | 若编号对应文件夹 → 进入该文件夹;若为文件 → 直接下载该文件 |
| ls | `ls` | 显示当前目录的文件与文件夹列表 |
| cd [编号&#124;..&#124;/] | `cd 3`、`cd ..`、`cd /` | 切换目录:进入指定编号的文件夹、返回上级、返回根目录 |
| mkdir [名称] | `mkdir test` | 在当前目录创建文件夹 |
| upload [路径] | `upload C:\Users\you\Desktop\file.txt` | 上传文件到当前目录(仅支持单个文件) |
| rm [编号] | `rm 2` | 删除当前列表中指定编号的文件/文件夹(移入回收站) |
| share [编号 ...] | `share 2 4` | 为指定文件创建一个或多个分享链接,可设置提取码(可为空) |
| link [编号] | `link 3` | 获取指定文件的直链地址 |
| download / d [编号] | `download 5` 或 `d 5` | 下载指定编号的文件或文件夹(文件夹将递归下载) |
| recycle | `recycle` | 查看回收站内容,可恢复指定编号项或输入 clear 清空回收站 |
| refresh / re | `refresh` 或 `re` | 刷新当前目录列表 |
| reload | `reload` | 重新加载配置文件并刷新目录 |
| login / logout | `login`、`logout` | 手动登录或登出(清除授权信息) |
| clearaccount | clearaccount | 清除已登录账号(包括用户名和密码) |
| more | `more` | 当目录分页未加载完时,继续加载更多内容 |
| protocol [android&#124;web] | `protocol web` | 切换通信协议(如 android/web),并可选择保存配置 |
| exit | `exit` | 退出程序 |
- cd [编号|..|/]
- 切换目录:
- cd 3 —— 进入当前列表第3项(如果是文件夹)。
- cd .. —— 返回上级。
- cd / —— 返回根目录。
- mkdir [名称]
- 在当前目录创建文件夹。例如:mkdir test
- upload [路径]
- 上传文件到当前目录。例如:upload C:\Users\you\Desktop\file.txt
- 仅支持文件,暂不支持目录批量上传。
- rm [编号]
- 删除当前列表中的文件/文件夹(会移动到回收站)。例如:rm 2
- share [编号 ...]
- 为一个或多个文件创建分享链接,例如:share 2 4
- 程序会提示输入提取码(可留空)。
- link [编号]
- 获取文件直链。例如:link 3
- download / d [编号]
- 下载指定编号的文件或文件夹(如果是文件夹,会递归下载)。
- 例如:download 5
- recycle
- 显示回收站内容,并可恢复或清空。
- 可输入编号恢复,或输入 clear 清空回收站。
- refresh / re
- 刷新当前目录列表。
- reload
- 重新加载配置文件并刷新目录。
- login / logout
- login:手动登录(使用当前配置或提示输入)。
- logout:登出并清除授权信息(保存配置时会写入空 token)。
- more
- 如果当前目录分页未加载完,输入 more 继续加载更多文件。
- protocol [android|web]
- 切换协议。例:protocol web
- 切换后会重新初始化请求头,并可选择保存到配置文件。
- exit
- 退出程序。
---
---
交互示例:
```
/> cd demo
无效命令,使用 '..' 返回上级,'/' 返回根目录,或输入文件夹编号
/> cd 1
当前目录为空
/demo1> ls
当前目录为空
/demo1> mkdir test
目录 'test' 创建成功
/demo1> 1
当前目录为空
/demo1/test> upload 123pan.py
上传进度: 100.0%
上传完成,正在验证...
文件上传成功
------------------------------------------------------------
编号 类型 大小 名称
------------------------------------------------------------
1 文件 38.66 KB 123pan.py
============================================================
```
/> cd demo
无效命令,使用 '..' 返回上级,'/' 返回根目录,或输入文件夹编号
/> cd 1
当前目录为空
/demo1> ls
当前目录为空
/demo1> mkdir test
目录 'test' 创建成功
/demo1/test> 1
开始下载: 123pan.py
进度: 100.0% | 38.66 KB/38.66 KB | 10.29 MB/s
下载完成: 123pan.py
/demo1/test>
/demo1> 1
当前目录为空
/demo1/test> upload 123pan.py
上传进度: 100.0%
上传完成,正在验证...
文件上传成功
------------------------------------------------------------
编号 类型 大小 名称
------------------------------------------------------------
1 文件 38.66 KB 123pan.py
============================================================
/demo1/test> 1
开始下载: 123pan.py
进度: 100.0% | 38.66 KB/38.66 KB | 10.29 MB/s
下载完成: 123pan.py
/demo1/test>
```
## 下载说明
# 2、123Pan接口模块(pan123_core.py
以下是基于代码实现的 **123pan 网盘 API**,按类结构分类说明:
---
### 2.1 核心类:`Pan123Core`
负责与 123pan 服务器的通信,管理认证状态、目录浏览、文件操作等核心逻辑。
#### 2.1.1 属性说明
| 属性名 | 类型 | 描述 |
|-----------------|------------|-----------------------------|
| `user_name` | str | 登录账号(手机号/用户名) |
| `password` | str | 登录密码 |
| `authorization` | str | 认证 Token(登录后自动填充) |
| `protocol` | str | 请求协议(`"android"` 或 `"web"` |
| `cwd_id` | int | 当前工作目录 ID(0 表示根目录) |
| `file_list` | List[dict] | 当前目录文件列表 |
| `nick_name` | str | 当前用户昵称 |
| `uid` | int | 当前用户 UID |
---
#### 2.1.2 方法清单
##### 2.1.2.1 1)登录操作
| 方法名 | 参数说明 | 返回值类型 | 功能描述 |
|-------------------|------|--------|----------------------------|
| `login()` | 无 | Result | 使用 `user_name/password` 登录 |
| `logout()` | 无 | Result | 登出并清除 Token |
| `check_login()` | 无 | Result | 检查当前 Token 是否有效 |
| `clear_account()` | 无 | Result | 清除账号信息(不保存配置) |
##### 2.1.2.2 2)配置管理
| 方法名 | 参数说明 | 返回值类型 | 功能描述 |
|--------------------------|-----------------------------------|--------|----------------------|
| `load_config(cfg)` | `cfg`: 包含账号信息的字典(见下方配置参数) | Result | 加载配置并更新实例状态 |
| `get_current_config()` | 无 | dict | 获取当前配置(账号、Token、协议等) |
| `set_protocol(protocol)` | `protocol`: `"android"` 或 `"web"` | Result | 切换请求协议 |
##### 2.1.2.3 3)目录操作
| 方法名 | 参数说明 | 返回值类型 | 功能描述 |
|-----------------------------------------------|---------------------------------------------------------|--------|--------------|
| `list_dir(parent_id=None, page=1, limit=100)` | `parent_id`: 父目录 ID<br>`page`: 页码<br>`limit`: 单页数量 | Result | 获取单页文件列表 |
| `list_dir_all(parent_id=None, limit=100)` | 同上 | Result | 获取全部文件(自动翻页) |
| `mkdir(name)` | `name`: 目录名 | Result | 在当前目录创建子目录 |
| `cd(folder_index)` | `folder_index`: `file_list` 中的目标文件夹下标 | Result | 进入目标文件夹 |
| `cd_up()` | 无 | Result | 返回上级目录 |
| `cd_root()` | 无 | Result | 返回根目录 |
| `trash(file_data, delete=True)` | `file_data`: 文件信息字典<br>`delete`: 是否删除(True=删除,False=恢复) | Result | 删除或恢复文件 |
| `list_recycle()` | 无 | Result | 获取回收站文件列表 |
##### 2.1.2.4 4)文件操作
| 方法名 | 参数说明 | 返回值类型 | 功能描述 |
|-------------------------------------------------------------------------|---------------------------------------------------------------------------------|--------|-----------------|
| `upload_file(file_path, duplicate=0, on_progress=None)` | `file_path`: 本地文件或文件夹路径<br>`duplicate`: 冲突策略(0=报错,1=覆盖,2=保留)<br>`on_progress`: 进度回调 | Result | 上传文件;传入文件夹时递归上传 |
| `upload_directory(dir_path, duplicate=0, on_progress=None)` | `dir_path`: 本地文件夹路径<br>其他参数同上 | Result | 上传文件夹(保留根目录结构) |
| `get_download_url(index)` | `index`: `file_list` 中的目标文件下标 | Result | 获取文件直链(自动处理重定向) |
| `share(file_ids, share_pwd="", expiration="2099-12-12T08:00:00+08:00")` | `file_ids`: 文件 ID 列表<br>`share_pwd`: 提取码<br>`expiration`: 过期时间 | Result | 创建分享链接 |
##### 2.1.2.5 5)用户信息
| 方法名 | 参数说明 | 返回值类型 | 功能描述 |
|-------------------|------|--------|------------------------|
| `get_user_info()` | 无 | Result | 获取当前用户信息(UID、昵称、空间用量等) |
---
### 2.2 工具类:`Pan123Tool`
基于 `Pan123Core` 提供文件交互功能(依赖文件系统操作)。
#### 2.2.1 属性说明
| 属性名 | 类型 | 描述 |
|---------------|------------|-----------------------------------|
| `core` | Pan123Core | 关联的核心实例 |
| `config_file` | str | 配置文件路径(默认 `"123pan_config.json"` |
---
#### 2.2.2 方法清单
##### 2.2.2.1 1)配置管理
| 方法名 | 参数说明 | 返回值类型 | 功能描述 |
|---------------------------|------|--------|------------|
| `load_config_from_file()` | 无 | Result | 从文件加载配置 |
| `save_config_to_file()` | 无 | Result | 将当前配置保存到文件 |
##### 2.2.2.2 2)文件下载
| 方法名 | 参数说明 | 返回值类型 | 功能描述 |
|--------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|--------|--------|
| `download_file(index, save_dir="download", on_progress=None, overwrite=False, skip_existing=False)` | `index`: 文件列表下标<br>`save_dir`: 保存路径<br>`on_progress`: 进度回调<br>`overwrite`: 是否覆盖<br>`skip_existing`: 是否跳过已存在文件 | Result | 下载单个文件 |
| `download_directory(directory, save_dir="download", on_progress=None, overwrite=False, skip_existing=False)` | `directory`: 目录信息字典<br>其他参数同上 | Result | 递归下载目录 |
##### 2.2.2.3 3)文件上传
| 方法名 | 参数说明 | 返回值类型 | 功能描述 |
|---------------------------------------------------------|----------------------------|--------|-------------------|
| `upload_file(file_path, duplicate=0, on_progress=None)` | 同 `Pan123Core.upload_file` | Result | 上传文件或文件夹(与 Core 方法一致) |
---
### 2.3 全局配置参数
#### 2.3.1 协议相关
| 参数名 | 默认值 | 描述 |
|-----------------------|----------------------------|-----------------|
| `API_BASE_URL` | `"https://www.123pan.com"` | API 根地址 |
| `TIMEOUT_DEFAULT` | `15` | 默认请求超时时间(秒) |
| `UPLOAD_CHUNK_SIZE` | `5*1024*1024` | 分块上传单块大小(5MB) |
| `DOWNLOAD_CHUNK_SIZE` | `8192` | 下载流式读取单块大小(8KB) |
#### 2.3.2 设备伪装
| 参数名 | 默认值 | 描述 |
|----------------|-----|-------------------|
| `DEVICE_TYPES` | 见代码 | 可选 Android 设备型号列表 |
| `OS_VERSIONS` | 见代码 | 可选 Android 系统版本列表 |
---
### 2.4 错误码说明
| 错误码 | 含义 | 可能触发场景 |
|------|--------|----------------------------|
| 0 | 操作成功 | 所有接口成功时返回 |
| -1 | 网络请求失败 | 连接超时、SSL 错误等 |
| 5060 | 文件名冲突 | 上传时 `duplicate=0` 且目标文件已存在 |
| 1 | 本地文件冲突 | 下载时目标文件已存在 |
---
### 2.5 典型使用示例
```python
import json
from pan123_core import Pan123Core, Pan123Tool, Pan123EventType, format_size
# 初始化核心对象(Android 协议)
core = Pan123Core(
user_name="13800138000",
password="your_password",
protocol=Pan123Core.PROTOCOL_ANDROID
)
# 登录
result = core.login()
if result["code"] != 0:
raise Exception("登录失败")
# 创建工具类实例
tool = Pan123Tool(core)
# 下载文件
def _download_progress(data) -> None:
if data.get("type") == Pan123EventType.DOWNLOAD_PROGRESS:
downloaded = data.get("downloaded", 0)
total = data.get("total", 0)
speed = data.get("speed", 0)
if total > 0:
pct = downloaded / total * 100
print(
f"\r进度: {pct:.1f}% | {format_size(downloaded)}/{format_size(total)} | {format_size(int(speed))}/s",
end=" ",
flush=True,
)
elif data.get("type") == Pan123EventType.DOWNLOAD_START_FILE:
print(f"开始下载: {data.get('file_name', '未知文件')} ({format_size(data.get('file_size', 0))})")
elif data.get("type") == Pan123EventType.DOWNLOAD_START_DIRECTORY:
print(f"开始下载目录: {data.get('dir_name', '未知目录')}")
else:
print(json.dumps(data, indent=2))
result = tool.download_file(
index=0,
save_dir="downloads",
on_progress=lambda e: print(f"下载进度: {e['percent']:.2f}%")
)
# 上传文件
result = core.upload_file(
file_path="local_file.txt",
duplicate=1, # 覆盖已有文件
on_progress=None # ...
)
# 创建分享链接
result = core.share(
file_ids=[12345],
share_pwd="123456",
expiration="2026-12-31T23:59:59+08:00"
)
```
---
# 3、下载说明
- 下载到脚本所在目录的 `download` 文件夹,下载过程中使用临时后缀 `.123pan`,下载完成后会重命名为原文件名。
- 如果文件已存在,会提示覆盖 / 跳过 / 全部覆盖 / 全部跳过等选项。
## 注意事项
# 4、注意事项
- 本工具用于学习与自用场景,请勿用于违法用途。对任何滥用造成的后果,本人概不负责。
- 模拟客户端协议可能存在账号或服务端策略风险,请谨慎使用。
- 建议不要在公用或不受信任的机器上保存明文密码或授权信息。
## 免责声明
# 5、免责声明
本工具用于学习场景,请勿用于违法用途。对任何滥用造成的后果,作者概不负责。
本工具用于学习场景,请勿用于违法用途。对任何滥用造成的后果,作者概不负责。
任何未经允许的api调用都是不被官方允许的,对于因此产生的账号风险、数据损失等后果自负。
+1 -1
View File
@@ -1 +1 @@
pyinstaller -F 123pan.py --icon icon.ico --clean --noconfirm
pyinstaller -F pan123_cli.py --icon favicon.ico --clean --noconfirm
+471
View File
@@ -0,0 +1,471 @@
"""
123pan 控制台交互界面 —— 仅负责用户 IO,所有业务调用 Pan123Core。
"""
import json
import os
import sys
from typing import Dict
from pan123_core import Pan123Core, Pan123Tool, Pan123EventType, format_size
# ──────────────── 颜色工具 ────────────────
class Color:
"""ANSI 颜色常量"""
RESET = "\033[0m"
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
PURPLE = "\033[35m"
CYAN = "\033[96m"
def colored(text: str, color: str) -> str:
return f"{color}{text}{Color.RESET}"
# ──────────────── CLI 类 ────────────────
class Pan123CLI:
"""控制台交互界面"""
HELP_TEXT = """可用命令:
ls - 显示当前目录
cd [编号|..|/] - 切换目录
mkdir [名称] - 创建目录
upload [路径] - 上传文件或文件夹
rm [编号] - 删除文件
share [编号 ...] - 创建分享
link [编号] - 获取文件直链
download/d [编号] - 下载文件
recycle - 管理回收站
refresh/re - 刷新目录
reload - 重新加载配置并刷新
login - 登录
logout - 登出并清除 token
clearaccount - 清除已登录账号(包括用户名和密码)
more - 继续加载更多文件
protocol [android|web] - 切换协议
exit - 退出程序"""
def __init__(self, config_file: str = "123pan_config.json"):
self.config_file: str = config_file
self.core = Pan123Core()
self.tool = Pan123Tool(self.core)
self._download_mode: int = 0 # 0=询问, 3=全部覆盖, 4=全部跳过
# ──────────────── 启动 ────────────────
def run(self) -> None:
"""主入口"""
# Windows cmd 颜色支持
if os.name == "nt":
os.system("")
self._print_banner()
if not self._init_login():
print(colored("无法登录", Color.RED))
a = input("输入1重新输入账号和密码,输入2清除登录信息,其他键退出: ")
if a == "1":
user_name = input("请输入用户名: ")
password = input("请输入密码: ")
if not user_name or not password:
print("用户名和密码不能为空,程序退出")
return
self.core.load_config({
"userName": user_name,
"passWord": password,
"authorization": ""
})
self.save_config()
return self.run()
if a == "2":
self._do_clear_account()
return self.run()
return
self.save_config()
self.core.refresh() # 加载文件列表
self.core.get_user_info()
self._show_files()
while True:
try:
prompt = colored(f"{self.core.cwd_path}>", Color.RED) + " "
print(colored(f'用户:{self.core.nick_name}', Color.GREEN))
command = input(prompt).strip()
if not command:
continue
self._dispatch(command)
except KeyboardInterrupt:
print("\n操作已取消")
except EOFError:
break
except Exception as e:
print(colored(f"发生错误: {e}", Color.RED))
# ──────────────── 初始化 ────────────────
def _print_banner(self) -> None:
print("=" * 60)
print("123网盘CLI客户端".center(56))
print("=" * 60)
def _init_login(self) -> bool:
"""尝试加载配置 -> 尝试访问目录 -> 必要时登录"""
res = self.load_config()
r = self.core.init_login_state()
if r["code"] < 0:
print(colored("登录失败", Color.YELLOW))
print(r["message"])
return False
return True
def load_config(self) -> Dict:
"""加载配置"""
try:
with open(self.config_file, "r", encoding="utf-8") as f:
cfg = json.load(f)
except FileNotFoundError:
user_name = input("请输入用户名: ")
password = input("请输入密码: ")
cfg = {
"userName": user_name,
"passWord": password,
"authorization": ""
}
return self.core.load_config(cfg)
def save_config(self) -> None:
"""保存配置"""
cfg = self.core.get_current_config()
with open(self.config_file, "w", encoding="utf-8") as f:
json.dump(cfg, f, indent=2, ensure_ascii=False)
# ──────────────── 命令分发 ────────────────
def _dispatch(self, command: str) -> None:
parts = command.split(maxsplit=1)
cmd = parts[0].lower()
arg = parts[1].strip() if len(parts) > 1 else ""
handler = {
#"ls": lambda: self._show_files(),
"ls": lambda: self._do_refresh(), # 用户希望ls是列出目前文件,故直接刷新并列出
"login": lambda: self._do_login(),
"logout": lambda: self._do_logout(),
"clearaccount": lambda: self._do_clear_account(),
"exit": lambda: sys.exit(0),
"cd": lambda: self._do_cd(arg),
"mkdir": lambda: self._do_mkdir(arg),
"upload": lambda: self._do_upload(arg),
"rm": lambda: self._do_rm(arg),
"share": lambda: self._do_share(arg),
"more": lambda: self._do_more(),
"link": lambda: self._do_link(arg),
"download": lambda: self._do_download(arg),
"d": lambda: self._do_download(arg),
"recycle": lambda: self._do_recycle(),
"refresh": lambda: self._do_refresh(),
"re": lambda: self._do_refresh(),
"reload": lambda: self._do_reload(),
"protocol": lambda: self._do_protocol(arg),
"help": lambda: print(self.HELP_TEXT),
}.get(cmd)
if handler:
handler()
elif cmd.isdigit():
self._do_select(int(cmd))
else:
print(self.HELP_TEXT)
# ──────────────── 显示 ────────────────
def _show_files(self) -> None:
items = self.core.file_list
if not items:
print("当前目录为空")
return
print()
print("=" * 60)
print(f"当前路径: {self.core.cwd_path}")
print("-" * 60)
print(f"{'编号':<6}{'类型':<8}{'大小':<12}{'名称'}")
print("-" * 60)
for idx, item in enumerate(items, 1):
is_dir = item["Type"] == 1
type_str = "文件夹" if is_dir else "文件"
size_str = format_size(item["Size"])
color = Color.PURPLE if is_dir else Color.YELLOW
print(colored(f"{idx:<6}{type_str:<8}{size_str:<12}{item['FileName']}", color))
if not self.core.all_loaded:
remaining = self.core.file_total - len(items)
print(f"\n还有 {remaining} 个文件未加载,输入 'more' 继续加载")
print("=" * 60 + "\n")
def _print_result(self, r: dict) -> None:
"""打印 Result 消息"""
if r["code"] == 0:
print(colored(r["message"], Color.GREEN))
else:
print(colored(f"[错误 {r['code']}] {r['message']}", Color.RED))
# ──────────────── 命令实现 ────────────────
def _do_login(self) -> None:
if not self.core.user_name:
self.core.user_name = input("请输入用户名: ")
if not self.core.password:
self.core.password = input("请输入密码: ")
r = self.core.login()
self._print_result(r)
if r["code"] == 0:
self._do_refresh()
def _do_logout(self) -> None:
r = self.core.logout()
self.save_config()
self._print_result(r)
def _do_clear_account(self) -> None:
"""清除已登录账号:清除用户名、密码、token 等信息"""
confirm = input("确定要清除已登录账号信息吗?(y/N): ").strip().lower()
if confirm == 'y':
r = self.core.clear_account()
self.save_config()
self._print_result(r)
else:
print("操作已取消")
def _do_cd(self, arg: str) -> None:
if arg == "..":
r = self.core.cd_up()
elif arg == "/":
r = self.core.cd_root()
elif arg.isdigit():
r = self.core.cd(int(arg) - 1)
else:
print("用法: cd [编号|..|/]")
return
if r["code"] != 0:
self._print_result(r)
else:
self._show_files()
def _do_mkdir(self, name: str) -> None:
if not name:
name = input("请输入目录名: ")
r = self.core.mkdir(name)
self._print_result(r)
if r["code"] == 0:
self._do_refresh()
def _do_upload(self, path: str) -> None:
if not path:
path = input("请输入文件路径: ")
r = self.core.upload_file(path, on_progress=self._upload_progress)
if r["code"] == 5060:
choice = input("检测到同名文件,输入 1 覆盖,2 保留两者,其他取消: ")
if choice == "1":
r = self.core.upload_file(path, duplicate=1, on_progress=self._upload_progress)
elif choice == "2":
r = self.core.upload_file(path, duplicate=2, on_progress=self._upload_progress)
else:
print("上传取消")
return
print() # 换行
self._print_result(r)
if r["code"] == 0:
self._do_refresh()
def _do_rm(self, arg: str) -> None:
if not arg.isdigit():
print("请提供文件编号")
return
r = self.core.trash_by_index(int(arg) - 1)
self._print_result(r)
if r["code"] == 0:
self._do_refresh()
def _do_share(self, arg: str) -> None:
indices = [int(x) - 1 for x in arg.split() if x.isdigit()]
if not indices:
print("请提供文件编号")
return
# 显示待分享的文件
names = [self.core.file_list[i]["FileName"] for i in indices if 0 <= i < len(self.core.file_list)]
print("分享文件:", ", ".join(names))
pwd = input("输入提取码(留空跳过): ").strip()
r = self.core.share_by_indices(indices, pwd)
self._print_result(r)
if r["code"] == 0:
print(f"链接: {r['data']['share_url']}")
if r["data"]["share_pwd"]:
print(f"提取码: {r['data']['share_pwd']}")
def _do_more(self) -> None:
r = self.core.load_more()
if r["code"] != 0:
self._print_result(r)
else:
self._show_files()
def _do_link(self, arg: str) -> None:
if not arg.isdigit():
print("请提供文件编号")
return
r = self.core.get_download_url(int(arg) - 1)
if r["code"] == 0:
print(f"文件直链: \n{r['data']['url']}")
else:
self._print_result(r)
def _do_download(self, arg: str) -> None:
if not arg.isdigit():
print("请提供文件编号")
return
idx = int(arg) - 1
if not (0 <= idx < len(self.core.file_list)):
print("无效的文件编号")
return
item = self.core.file_list[idx]
print(f"开始下载: {item['FileName']}")
overwrite = self._download_mode == 3
skip = self._download_mode == 4
r = self.tool.download_file(
idx,
on_progress=self._download_progress,
overwrite=overwrite,
skip_existing=skip,
)
# 冲突处理
if r["code"] == 1 and r.get("data", {}).get("conflict"):
print(f"文件已存在: {item['FileName']}")
choice = input("输入 1 覆盖,2 跳过,3 全部覆盖,4 全部跳过: ").strip()
if choice == "4":
self._download_mode = 4
print("跳过下载")
return
elif choice == "2":
print("跳过下载")
return
elif choice == "3":
self._download_mode = 3
r = self.tool.download_file(idx, on_progress=self._download_progress, overwrite=True)
print() # 换行
self._print_result(r)
def _do_recycle(self) -> None:
r = self.core.list_recycle()
if r["code"] != 0:
self._print_result(r)
return
items = r["data"]
if not items:
print("回收站为空")
return
print("\n回收站内容:")
for i, item in enumerate(items, 1):
print(f" {i}. {item['FileName']} ({format_size(item['Size'])})")
action = input("\n输入编号恢复文件,或输入 'clear' 清空回收站: ").strip()
if action.isdigit():
idx = int(action) - 1
if 0 <= idx < len(items):
rr = self.core.restore(items[idx]["FileId"])
self._print_result(rr)
else:
print("无效编号")
elif action == "clear":
for item in items:
self.core.trash(item, delete=True)
print("回收站已清空")
self._do_refresh()
def _do_refresh(self) -> None:
self._download_mode = 0
r = self.core.refresh()
if r["code"] != 0:
self._print_result(r)
else:
self._show_files()
def _do_reload(self) -> None:
r = self.core.load_config()
self._print_result(r)
self._do_refresh()
def _do_protocol(self, arg: str) -> None:
if arg.lower() not in ("android", "web"):
print("请指定协议: android 或 web")
return
r = self.core.set_protocol(arg.lower())
self._print_result(r)
if r["code"] == 0:
self._do_refresh()
def _do_select(self, num: int) -> None:
"""数字选择:文件夹进入,文件下载"""
idx = num - 1
if not (0 <= idx < len(self.core.file_list)):
print("无效的文件编号")
return
if self.core.file_list[idx]["Type"] == 1:
self._do_cd(str(num))
else:
self._do_download(str(num))
# ──────────────── 进度回调 ────────────────
@staticmethod
def _download_progress(data) -> None:
if data.get("type") == Pan123EventType.DOWNLOAD_PROGRESS:
downloaded = data.get("downloaded", 0)
total = data.get("total", 0)
speed = data.get("speed", 0)
if total > 0:
pct = downloaded / total * 100
print(
f"\r进度: {pct:.1f}% | {format_size(downloaded)}/{format_size(total)} | {format_size(int(speed))}/s",
end=" ",
flush=True,
)
elif data.get("type") == Pan123EventType.DOWNLOAD_START_FILE:
print(f"开始下载: {data.get('file_name', '未知文件')} ({format_size(data.get('file_size', 0))})")
elif data.get("type") == Pan123EventType.DOWNLOAD_START_DIRECTORY:
print(f"开始下载目录: {data.get('dir_name', '未知目录')}")
else:
print(json.dumps(data, indent=2))
@staticmethod
def _upload_progress(data) -> None:
uploaded_total = data.get("uploaded_total")
total_size = data.get("total_size")
if uploaded_total is not None and total_size is not None:
pct = data.get("percent", uploaded_total / total_size * 100 if total_size else 100)
file_index = data.get("file_index", 0)
file_count = data.get("file_count", 0)
file_name = data.get("file_name", "")
print(
f"\r上传进度: {pct:.1f}% | {format_size(uploaded_total)}/{format_size(total_size)} | "
f"{file_index}/{file_count} {file_name}",
end=" ",
flush=True,
)
return
uploaded = data.get("uploaded", 0)
total = data.get("total", 0)
if total > 0:
pct = uploaded / total * 100
print(f"\r上传进度: {pct:.1f}%", end="", flush=True)
# ──────────────── 入口 ────────────────
if __name__ == "__main__":
Pan123CLI().run()
+1909
View File
File diff suppressed because it is too large Load Diff