aliyunpan 阿里云盘客户端 JavaScript 插件开发指南:回调机制、内置 API 与 8 大实战场景全解析
【免费下载链接】aliyunpan阿里云盘命令行客户端,支持JavaScript插件,支持同步备份功能。项目地址: https://gitcode.com/GitHub_Trending/ali/aliyunpan
本篇技术指南以阿里云盘命令行客户端 aliyunpan 的 JS 插件系统为核心,系统讲解如何通过 JavaScript 脚本定制上传、下载、同步、删除、Token 刷新等关键环节的自动化行为。读完本文你将掌握:插件的文件结构与加载规则、9 类回调函数的参数与返回值契约、10 个内置 PluginUtil API 的用法与底层实现,以及过滤敏感文件、自动改名、云端删源、外部通知、限流下载等 8 个可直接落地的实战脚本模板。
插件系统概述
aliyunpan 是阿里云盘的命令行客户端,其核心特性之一便是内置 JavaScript 插件引擎。通过 JS 插件,用户可以按照自己的需要定制上传、下载、同步备份、删除过程中关键步骤的行为,最大程度满足个性化需求。
插件系统把"上传前/后、下载前/后、同步扫描前、文件删除前、Token 刷新后"等关键节点抽象为回调钩子,开发者只需在脚本中按约定定义同名函数,引擎就会在对应时机自动调用。官方文档(docs/plugin_manual.md)列举了插件能实现的典型能力:
- 排除某个特定敏感文件的上传;
- 上传文件进行改名,但本地文件不做更改;
- 上传完成文件后删除本地文件;
- 修改上传到云盘的文件路径,本地文件保持不变;
- 上传文件成功后通过 HTTP 通知其他服务;
- 排除某些网盘文件的下载;
- 下载的文件进行改名,网盘文件保持不变;
- 修改下载保存的本地路径,网盘文件保持不变;
- 下载文件完成后通过 HTTP 通知其他服务;
- 同步备份功能中过滤本地文件或云盘文件,定制需要同步的文件。
从实现层面看,插件引擎基于 Go 语言编写的goja(ECMAScript 5.1)解释器构建,位于 internal/plugins/js_plugin.go,引擎启动时会把console与PluginUtil两大对象注入到 JS 运行时(见 internal/plugins/js_plugin.go)。因此你不需要在系统上安装 Node.js 或其他 JS 运行时,插件脚本由客户端进程内嵌解释执行。
插件使用方式
插件文件的存放位置与命名
JS 插件的样本文件默认存放在程序所在目录的plugin/js文件夹下(对应仓库中的 assets/plugin/js),共 5 个:
| 样本文件 | 对应功能 |
|---|---|
download_handler.js.sample | 下载插件:定制下载前/后的行为 |
upload_handler.js.sample | 上传插件:定制上传前/后的行为 |
remove_handler.js.sample | 删除插件:定制删除文件前行为 |
sync_handler.js.sample | 同步备份插件:过滤同步文件、同步完成通知 |
token_handler.js.sample | 用户 Token 插件:Token 刷新失败通知 |
关键约定:必须拷贝一份并将后缀名从.sample改为.js插件才会生效,例如upload_handler.js。
此外还有两点需要注意:
- 如果你通过环境变量
ALIYUNPAN_CONFIG_DIR设置了自定义配置目录,则需要将plugin文件夹拷贝到该配置目录中,插件才会被加载。 - 编写插件需要一定的 JavaScript 语言基础;如果不会 JS,也可以到项目 issue 区寻求开发者或网友提供现成脚本。
加载规则(源码级验证)
插件管理器在 internal/plugins/plugin_manager.go 中实现了加载逻辑,可以从源码确认以下规则:
- 插件目录固定为
pluginPath/js,目录不存在时直接回退到空插件(NewIdlePlugin); - 只加载以
.js(不区分大小写)结尾的文件,.sample后缀因此不会被加载,这正是"必须改名"约定的原因; - 以
.或~开头的文件(如临时文件、备份文件)会被跳过; - 多个
.js文件会被依次加载进同一个 goja 运行时,因此多个文件可以共享全局变量与函数,但也要注意避免函数名冲突; - 只要有一个 JS 文件加载成功,插件即生效;全部加载失败则使用空插件,程序照常运行。
回调函数的执行遵循"存在才调用"的原则,例如上传前回调在 internal/plugins/js_plugin.go 中先检查uploadFilePrepareCallback是否存在,不存在则直接返回nil,不影响正常上传流程。这意味着你的脚本不需要定义全部回调,按需定义即可。
回调函数体系:9 类钩子与参数契约
插件的核心是回调函数。所有回调统一签名function xxxCallback(context, params),其中:
- context:当前调用上下文,由
GetContext构造(见 internal/plugins/plugin_manager.go),包含应用名、版本、当前登录用户 ID、昵称,以及备份盘/相册盘/资源盘的网盘 ID; - params:本次回调的业务参数,由各命令模块按对应结构体填充(定义见 internal/plugins/plugin.go)。
9 类回调及其触发时机如下:
| 回调函数 | 触发时机 | 返回结构 |
|---|---|---|
uploadFilePrepareCallback | 每个文件上传前 | {uploadApproved, driveFilePath} |
uploadFileFinishCallback | 每个文件上传结束后 | 无 |
downloadFilePrepareCallback | 每个文件下载前 | {downloadApproved, localFilePath} |
downloadFileFinishCallback | 每个文件下载结束后 | 无 |
syncScanLocalFilePrepareCallback | 同步备份扫描本地文件前 | {syncScanLocalApproved} |
syncScanPanFilePrepareCallback | 同步备份扫描云盘文件前 | {syncScanPanApproved} |
syncFileFinishCallback | 同步备份单个文件同步结束后 | 无 |
syncAllFileFinishCallback | 同步任务全部文件完成后(仅"只运行一次备份"模式) | 无 |
removeFilePrepareCallback | 删除命令执行前 | {result: [{driveId, driveFileId, removeApproved}]} |
userTokenRefreshFinishCallback | 用户 Token 刷新完成后 | 无 |
回调钩子贯穿各功能命令:上传命令在 internal/command/upload.go 中获取插件并调用UploadFilePrepareCallback;下载任务在 internal/functions/pandownload/download_task_unit.go 中调用下载类回调;删除命令则在 internal/command/utils.go 中调用RemoveFilePrepareCallback。这也解释了为什么改动插件后重启客户端即可生效——插件是在每次命令执行时重新加载的。
关键回调参数详解
以上传前回调uploadFilePrepareCallback为例(参数定义见 internal/plugins/plugin.go),params完整字段如下:
{ "localFilePath": "D:\\Program Files\\aliyunpan\\Downloads\\token.bat", // 本地文件绝对完整路径 "localFileName": "token.bat", // 本地文件名 "localFileSize": 125330, // 本地文件大小,单位B "localFileType": "file", // 文件类型:file-文件,folder-文件夹 "localFileUpdatedAt": "2022-04-14 07:05:12", // 文件修改时间 "driveId": "19519221", // 目标网盘ID "driveFilePath": "aliyunpan/Downloads/token.bat" // 目标网盘保存路径(相对路径) }返回结果中:
uploadApproved:yes允许上传,no禁止上传;driveFilePath:修改后的网盘保存路径(相对路径),为空字符串表示保持原本目标路径。注意:修改路径要小心,重名文件可能只会上传一个(该提示同样见于 assets/plugin/js/upload_handler.js.sample 的注释)。
下载前回调downloadFilePrepareCallback的params则包含driveId、driveFileName、driveFilePath、driveFileSha1、driveFileSize、driveFileType、driveFileUpdatedAt、localFilePath以及downloadActionId(本次下载动作的 ID,可用于统计单次下载数量);返回结果downloadApproved(yes/no)与localFilePath(修改后的本地保存路径)。
同步备份相关的两个扫描回调返回syncScanLocalApproved/syncScanPanApproved(yes/no),被禁止扫描的文件不会执行后续的上传或下载动作。样本 assets/plugin/js/sync_handler.js.sample 演示了如何过滤以.开头、以~$开头的 Office 暂存文件、.txt文件及@eadir等。
JS 内置函数(PluginUtil API)
引擎向每个插件脚本注入了console与PluginUtil两大全局对象(注册代码见 internal/plugins/js_plugin.go),用于日志输出、网络请求、文件操作、邮件通知、键值存储与哈希计算,增强插件的扩展性与可玩性。以下逐一说明用法与底层实现。
console.log() / console.println()
console.log("hello world"):打印日志,需要开启 debug 日志才会在控制台窗口显示。底层走logger.Verboseln(详细日志级别),输出带JAVASCRIPT:前缀。console.println("hello world"):打印日志,无需开启 debug 日志,直接显示在控制台窗口,输出带[PLUGIN]前缀。
两个函数的 Go 实现分别见 internal/plugins/js_plugin.go。在调试脚本时建议用console.println观察参数内容,正式环境再改用console.log减少干扰。
PluginUtil.Http.get(header, url)
发起 HTTP GET 请求,返回响应体字符串。
var header = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36", "Content-Type": "application/json", "Accept": "application/json" }; try { var r = PluginUtil.Http.get(header, "https://625f528c53a42eaa07f37e13.mockapi.io/files/1"); console.log(r); } catch (e) { if (e !== "Error") { throw e; } }底层实现(internal/plugins/plugin_util.go)使用requester.NewHTTPClient()发起请求,请求失败时返回空字符串并输出详细日志。
PluginUtil.Http.post(header, url, data)
发起 HTTP POST 请求,data为字符串形式的请求体(通常由JSON.stringify(...)生成),返回响应体字符串。
var header = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36", "Content-Type": "application/json", "Accept": "application/json" }; try { var reqDataStr = JSON.stringify({ "id": "1", "localFilePath": "/usr/local/src/borders_burundi_producer.gram.aab", "localFileSize": 1111, "uploadApproved": false }); var r = PluginUtil.Http.post(header, "https://625f528c53a42eaa07f37e13.mockapi.io/files", reqDataStr); console.log(r); } catch (e) { if (e !== "Error") { throw e; } }这对函数是"上传/下载完成后通知外部服务"(如 Webhook、Server 酱、企业微信机器人等)的基础能力。
PluginUtil.LocalFS.deleteFile(localFilePath)
删除本地指定文件,不支持文件夹,返回布尔值表示是否删除成功。
PluginUtil.LocalFS.deleteFile(localFilePath); 其中: localFilePath - 本地文件的绝对完整路径PluginUtil.LocalFS.deleteFile("/Users/tickstep/Downloads/IMG_0884.HEIC");底层直接调用 Go 的os.Remove(internal/plugins/plugin_util.go),删除失败(文件不存在、权限不足等)返回false。典型用途是"上传完成后删除本地文件"以释放磁盘空间。
PluginUtil.PanFS.deleteFile(userId, driveId, panFileId)
删除云盘指定文件,支持文件和文件夹,返回布尔值。
PluginUtil.PanFS.deleteFile(userId, driveId, panFileId); 其中: userId - 登录的用户ID driveId - 网盘ID panFileId - 网盘文件IDvar userId = context["userId"] var driveId = params["driveId"] var driveFileId = params["driveFileId"] if (PluginUtil.PanFS.deleteFile(userId, driveId, driveFileId)) { console.println("插件删除云盘文件成功") }底层实现(internal/plugins/plugin_util.go)会根据userId从配置的用户列表中查找对应用户,再调用其 OpenAPI 客户端的FileDelete接口。注意userId、driveId、panFileId任一为空都会直接返回false。典型用途是"下载完成后删除云盘源文件",实现类似"搬库"的效果。
PluginUtil.Email.sendTextMail(...) / sendHtmlMail(...)
发送邮件通知,两个函数分别发送纯文本邮件与 HTML 富文本邮件,均返回布尔值。
PluginUtil.Email.sendTextMail(mailServer, userName, password, to, subject, body) PluginUtil.Email.sendHtmlMail(mailServer, userName, password, to, subject, body) 其中: mailServer - smtp服务器+端口,例如 "smtp.qq.com:465" userName - 发件人邮箱地址 password - 发件人邮箱密码(授权码) to - 收件人邮箱地址 subject - 邮件标题 body - 邮件内容,纯文本或HTML富文本纯文本邮件示例:
PluginUtil.Email.sendTextMail("smtp.qq.com:465", "123xxx@qq.com", "pwdxxxxxx", "12545xxx@qq.com", "文件上传通知", "该文件已经上传完毕");HTML 富文本邮件示例:
var html = "<html>" +"<body>" +"<h1>文件上传通知</h1>" +"<h2>该文件已经上传完毕</h2>" +"</body>" +"</html>"; PluginUtil.Email.sendHtmlMail("smtp.qq.com:465", "123xxx@qq.com", "pwdxxxxxx", "12545xxx@qq.com", "文件上传通知", html);底层实现(internal/plugins/plugin_util.go)使用smtp.PlainAuth认证并通过SendWithStartTLS(SSL 端口)发送。请确保发件邮箱具备 SMTP 发送权限,没有的需要找邮箱服务商开启(例如 QQ 邮箱需获取授权码而非登录密码)。
PluginUtil.KV.putString(key, value) / getString(key)
插件的轻量键值存储,用于在多次命令运行之间持久化状态(例如记录上次通知时间、累计下载数量)。
PluginUtil.KV.putString(key, value) // 存储键值对,key/value 均为字符串 PluginUtil.KV.getString(key) // 获取存储的值,如果没有则返回空字符串PluginUtil.KV.putString("mykey", "1670419352"); var value = PluginUtil.KV.getString("mykey");底层使用嵌入式BoltDB持久化(存储文件路径由config.GetPluginKvFile()指定,见 internal/plugins/js_plugin.go),读取与写入均带互斥锁与 JSON 序列化(internal/plugins/plugin_util.go)。这保证了插件状态跨命令、跨进程重启依然存在——注意getString返回的是字符串,做时间比较等运算时需要自行parseInt/Date.parse。
PluginUtil.HashTool.md5Hex(text)
计算指定字符串的 MD5 值,返回 hex 格式字符串。
PluginUtil.HashTool.md5Hex(text) 其中: text - 字符串var md5 = PluginUtil.HashTool.md5Hex("123456");底层即标准库crypto/md5的十六进制输出(internal/plugins/plugin_util.go)。典型用途是以文件路径为维度生成稳定的去重 Key(见下文场景 8)。
常见场景样例
以下 8 个样例直接取自官方文档 docs/plugin_manual.md,均可作为插件定制的模板直接套用。使用前请先按上文规则将对应样本拷贝为.js文件。
1. 禁止特定文件上传
使用上传插件中的uploadFilePrepareCallback,通过修改返回结果的uploadApproved字段决定文件是否上传。本样例演示了三种过滤写法:以点号开头的隐藏文件、.txt后缀文件(正则)、指定文件名。
function uploadFilePrepareCallback(context, params) { console.log(params) var result = { "uploadApproved": "yes", "driveFilePath": "" }; if (params["localFileType"] != "file") { // do nothing return result; } // 禁止点号.开头的文件上传 if (params["localFileName"].indexOf(".") == 0) { result["uploadApproved"] = "no"; } // 禁止.txt文件上传 if (params["localFileName"].search(/.txt$/i) >= 0) { result["uploadApproved"] = "no"; } // 禁止password.key文件上传 if (params["localFileName"] == "password.key") { result["uploadApproved"] = "no"; } return result; }注意:localFileType为folder时脚本直接放行,保证文件夹本身照常创建,只拦截具体文件。
2. 上传文件后删除本地文件
使用上传插件中的uploadFileFinishCallback,在上传成功(uploadResult == "success")后调用PluginUtil.LocalFS.deleteFile删除本地源文件,实现"云盘留档、本地清理"。
function uploadFileFinishCallback(context, params) { console.log(params); if (params["localFileType"] != "file") { // do nothing return; } if (params["uploadResult"] == "success") { PluginUtil.LocalFS.deleteFile(params["localFilePath"]); } }3. 下载文件并截断过长的文件名
有些文件的路径或名称太长,下载时可能因路径过长导致失败。使用下载插件中的downloadFilePrepareCallback定制下载保存的文件名——将路径拆分出目录、主文件名、后缀名,再按需截断主文件名后拼接返回。该方法只影响下载保存路径,网盘上的文件保持不变。
function downloadFilePrepareCallback(context, params) { console.log(params) var result = { "downloadApproved": "yes", "localFilePath": "" }; if (params["driveFileType"] != "file") { return result; } // 下面的代码都是分隔路径,方便后面修改路径时使用 var filePath = params["localFilePath"]; filePath = filePath.replace(/\\/g, "/"); // 目录完整路径 var dirPath = ""; // 文件名,不包括后缀名 var fileName = ""; // 文件后缀名 var fileExt = ""; var idx = filePath.lastIndexOf('/'); if (idx > 0) { dirPath = filePath.substring(0,idx); fileName = filePath.substring(idx+1,filePath.length); } else { fileName = filePath; } idx = fileName.lastIndexOf(".") if (idx > 0) { fileExt = fileName.substring(idx,fileName.length); fileName = fileName.substring(0, fileName.length-fileExt.length) } // 开始按照需要截断太长的文件路径,例如下面的这个例子: // // dirPath + "/" ==> 这个的意思是保留前面的文件夹路径,如果不需要就去掉 // fileName.substr(0,10) ==> 文件名只取前面10个字符,其他的不要了 // + fileExt ==> 把文件的后缀名补上 var saveFilePath = dirPath + "/" + fileName.substr(0,10) + fileExt; // 返回 result["localFilePath"] = saveFilePath; return result; }实际运行效果(取自文档):
- 网盘源路径:
/亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagram( 《金融时报》和麦肯锡2020年度商业书籍!社交应用如何改变世界?解锁打造估值千亿美元爆品的核心方法! ) by 莎拉·弗莱尔(z-lib.org).mobi - 下载后本地保存路径:
D:\test\亚马逊书籍合集\亚马逊 kindle ebook 大合集5289册\经济金融 (2)\解密Instagra.mobi
[1] ---- 文件ID: 630f0623e67c0a1d2e554b1994a29108d089f0d5 文件名: 解密Instagram( 《金融时报》和麦肯锡2020年度商业书籍!社交应用如何改变世界?解锁打造估值千亿美元爆品的核心方法! ) by 莎拉·弗莱尔(z-lib.org).mobi 文件类型: 文件 文件路径: /亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagram( 《金融时报》和麦肯锡2020年度商业书籍!社交应用如何改变世界?解锁打造估值千亿美元爆品的核心方法! ) by 莎拉·弗莱尔(z-lib.org).mobi 插件修改文件下载保存路径为: D:\test\亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagra.mobi [1] 准备下载: /亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagram( 《金融时报》和麦肯锡2020年度商业书籍!社交应用如何改变世界?解锁打造估值千亿美元爆品的核心方法! ) by 莎拉·弗莱尔(z-lib.org).mobi [1] 将会下载到路径: D:\test\亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagra.mobi [1] 下载开始 [1] 下载完成, 保存位置: D:\test\亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagra.mobi [1] 检验文件有效性成功: D:\test\亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagra.mobi4. 上传文件去掉文件名包含的部分字符
例如本地文件夹名称为[周杰伦]范特西[mp3],希望上传到网盘后名称变为[周杰伦]范特西,同时保持本地名称不变。由于文件夹数量多、不想逐个手动改名,可以通过修改driveFilePath批量实现。同样地,只改网盘路径,本地文件保持不变。
function uploadFilePrepareCallback(context, params) { var result = { "uploadApproved": "yes", "driveFilePath": "" }; // 去掉网盘保存路径中包含的[mp3]字段 var filePath = params["driveFilePath"]; filePath = filePath.replace(/\[mp3\]/g, ""); result["driveFilePath"] = filePath; return result; }5. 上传文件时过滤指定目录或者文件路径
upload命令本身支持exn参数排除上传文件或目录,但只能指定文件名称,无法指定文件的路径。如需按绝对路径排除,可通过插件脚本实现。本样例中只需在脚本开头维护forbiddenUploadFolders与forbiddenUploadFiles两个清单即可,其余代码保持不变。
function uploadFilePrepareCallback(context, params) { //(自行配置)禁止上传的本地【目录列表】,使用绝对路径,可以配置多个路径用逗号分隔 var forbiddenUploadFolders = ["/Users/tickstep/Downloads/up/target","/Users/tickstep/Downloads/up/.idea"] //(自行配置)禁止上传的本地【文件列表】,使用绝对路径,可以配置多个路径用逗号分隔 var forbiddenUploadFiles = ["/Users/tickstep/Downloads/up/pom.xml"] // -------------------- 以下代码不要修改 -------------------- var result = { "uploadApproved": "yes", "driveFilePath": "" }; // 下面的代码都是分隔路径,方便后面使用 var filePath = params["localFilePath"]; filePath = filePath.replace(/\\/g, "/"); // 目录完整路径 var dirPath = ""; // 文件名,不包括后缀名 var fileName = ""; // 文件后缀名 var fileExt = ""; var idx = filePath.lastIndexOf('/'); if (idx > 0) { dirPath = filePath.substring(0,idx); fileName = filePath.substring(idx+1,filePath.length); } else { fileName = filePath; } idx = fileName.lastIndexOf(".") if (idx > 0) { fileExt = fileName.substring(idx,fileName.length); fileName = fileName.substring(0, fileName.length-fileExt.length) } if (params["localFileType"] == "file") { for (var i = 0; i < forbiddenUploadFiles.length; i++) { if (forbiddenUploadFiles[i].replace(/\\/g, "/") == params["localFilePath"]) { result["uploadApproved"] = "no"; // 禁止文件上传 break } } for (var i = 0; i < forbiddenUploadFolders.length; i++) { if (forbiddenUploadFolders[i].replace(/\\/g, "/") == dirPath) { result["uploadApproved"] = "no"; // 禁止文件上传 break } } } else if (params["localFileType"] == "folder") { for (var i = 0; i < forbiddenUploadFolders.length; i++) { if (forbiddenUploadFolders[i].replace(/\\/g, "/") == params["localFilePath"]) { result["uploadApproved"] = "no"; // 禁止文件上传 break } } } return result; }6. 下载云盘文件到本地后删除云盘对应的文件
使用下载插件中的downloadFileFinishCallback,在文件成功下载(downloadResult == "success")后,从context与params中取出userId、driveId、driveFileId,调用PluginUtil.PanFS.deleteFile删除云盘源文件,实现"下载即搬离"。
function downloadFileFinishCallback(context, params) { console.log(params) // 云盘文件成功下载到本地后,删除云盘的文件 if (params["downloadResult"] == "success") { if (params["driveFileType"] == "file") { // 文件下载成功,删除该云盘文件 var userId = context["userId"] var driveId = params["driveId"] var driveFileId = params["driveFileId"] if (PluginUtil.PanFS.deleteFile(userId, driveId, driveFileId)) { console.println("插件删除云盘文件成功:" + params["driveFilePath"]) } } } }7. Token 刷新失败发送外部通知
当用户 Token 刷新失败时,可通过userTokenRefreshFinishCallback发送外部通知(如 Server 酱推送或邮件),及时提醒手动处理,避免"静默失效"。
方案一:Server 酱 HTTP 推送。利用PluginUtil.KV记录上次发送时间,10 分钟内只发送一次,避免频繁打扰:
function userTokenRefreshFinishCallback(context, params) { var header = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36", "Content-Type": "application/json", "Accept": "application/json" }; try { if (params["result"] === "fail") { // 避免频繁发送 var ONE_MINUTE = 60 * 1000; var lastSendEmailTime = PluginUtil.KV.getString("email_last_send_time"); if (lastSendEmailTime != "") { if ((Date.now() - lastSendEmailTime) < (10 * ONE_MINUTE)) { console.log("距离上次发送邮件小于10分钟,先不发送了"); return } } PluginUtil.KV.putString("email_last_send_time", Date.now()); var reqData = { "text": "Token刷新失败", "desp": params["message"] }; var r = PluginUtil.Http.post(header, "https://sctapi.ftqq.com/xxxxxkeyxxxxxx.send", JSON.stringify(reqData)); } } catch (e) { if (e !== "Error") { throw e; } } }方案二:发送邮件通知。同样带 10 分钟限频,通过PluginUtil.Email.sendTextMail发送提醒邮件:
function userTokenRefreshFinishCallback(context, params) { try { if (params["result"] === "fail") { // 避免频繁发送 var ONE_MINUTE = 60 * 1000; var lastSendEmailTime = PluginUtil.KV.getString("email_last_send_time"); if (lastSendEmailTime != "") { if ((Date.now() - lastSendEmailTime) < (10 * ONE_MINUTE)) { console.log("距离上次发送邮件小于10分钟,先不发送了"); return } } PluginUtil.KV.putString("email_last_send_time", Date.now()); // 发送通知邮件,请确保你的发送邮箱具备smtp发送权限,没有的需要找邮箱提供商配置开启 console.log("发送通知邮件"); PluginUtil.Email.sendTextMail("smtp.qq.com:465", "123xxx@qq.com", "pwdxxxxxx", "12545xxx@qq.com", "Token刷新失败了", "Token过期了,骚年快去手动恢复吧"); } } catch (e) { if (e !== "Error") { throw e; } } }params["message"]中携带 Token 刷新失败的具体原因(见 internal/plugins/plugin.go 中UserTokenRefreshFinishParams的定义)。
8. 每次只下载指定数量的文件
利用KV存储与HashTool.md5Hex,实现"每次运行 download 命令只下载指定数量的文件,其余文件留到下一次命令再下载"的限流策略。核心思路:以downloadActionId为维度统计本次已放行数量,以driveFilePath的 MD5 为维度标记单个文件是否已处理过。
function downloadFilePrepareCallback(context, params) { var result = { "downloadApproved": "yes", "localFilePath": "" }; // 这次下载限制的最大文件总数量 const maxCountOfDownloadAction = 3; // 只处理文件 if (params["driveFileType"] != "file") { return } // 获取这次下载动作,已经下载的文件数量 var keyOfThisDownloadAction = "download:" + params["downloadActionId"]; var valueOfThisDownloadAction = PluginUtil.KV.getString(keyOfThisDownloadAction); if (valueOfThisDownloadAction == "") { valueOfThisDownloadAction = "0"; } var countOfThisDownloadAction = parseInt(valueOfThisDownloadAction); if (countOfThisDownloadAction >= maxCountOfDownloadAction) { // 下载数量已达到最大,其他文件不再下载 result["downloadApproved"] = "no"; return result; } // 该文件需要下载,记录相关信息 var keyOfThisDownloadFile = "file:" + PluginUtil.HashTool.md5Hex(params["driveFilePath"]); var valueOfThisDownloadFile = PluginUtil.KV.getString(keyOfThisDownloadFile); if (valueOfThisDownloadFile == "finish") { // 已经在下载的文件不做处理 return result; } PluginUtil.KV.putString(keyOfThisDownloadFile, "downloading"); // 更新下载总数量 countOfThisDownloadAction += 1; PluginUtil.KV.putString(keyOfThisDownloadAction, String(countOfThisDownloadAction)); return result; } function downloadFileFinishCallback(context, params) { var keyOfThisDownloadFile = "file:" + PluginUtil.HashTool.md5Hex(params["driveFilePath"]); console.log(keyOfThisDownloadFile) PluginUtil.KV.putString(keyOfThisDownloadFile, "finish"); }由于KV数据持久化在 BoltDB 中,即使中途退出程序,下次运行命令时也能从上次的计数继续,不会重复放行或漏放。
进阶技巧与注意事项
- 多文件合并运行:所有
.js文件会被加载进同一个运行时(见 internal/plugins/plugin_manager.go),你可以在一个文件里定义回调、在另一个文件里定义公共工具函数,它们互相可见。 - 调试三板斧:先用
console.println(params)打印完整参数确认字段取值;HTTP 回调统一用try/catch包裹并判断e !== "Error",避免网络异常导致上传/下载流程中断;注意回调中的 JS 抛错会被引擎捕获并记录详细日志(见 internal/plugins/js_plugin.go 的错误处理路径),插件异常不会导致客户端崩溃。 - 路径分隔符:Windows 路径中的
\在回调参数中会保留,处理时建议先replace(/\\/g, "/")统一为正斜杠再解析,上述场景 3、5 已给出标准写法。 - 同步备份场景:同步插件的过滤能力比
upload/download命令更细粒度——syncScanLocalFilePrepareCallback过滤本地侧文件、syncScanPanFilePrepareCallback过滤云盘侧文件,配合syncFileFinishCallback(单文件完成)与syncAllFileFinishCallback(全任务完成,仅"只运行一次备份"模式),可以实现完整的同步通知与二次处理,详见 assets/plugin/js/sync_handler.js.sample。 - 删除保护:
removeFilePrepareCallback支持按driveId+driveFileId逐条返回removeApproved(yes/no),可用来保护特定文件或目录不被误删,参数定义见 internal/plugins/plugin.go。
总结
aliyunpan 的 JS 插件系统通过 goja 内嵌 JS 引擎 + 生命周期回调钩子 + PluginUtil 工具集三者的组合,把"上传、下载、同步、删除、Token 刷新"五大环节的关键节点完全开放给了用户脚本。本文覆盖了官方文档 docs/plugin_manual.md 的全部内容,并补充了 internal/plugins 源码层的机制解读:回调的加载与调用规则、参数结构定义、内置 API 的 Go 实现细节,以及 5 个样本文件(assets/plugin/js)的完整回调签名。掌握这些之后,无论是过滤敏感文件、自动改名、删除源文件,还是对接外部通知服务、实现下载限流,都可以用几十行 JS 脚本轻松完成。
【免费下载链接】aliyunpan阿里云盘命令行客户端,支持JavaScript插件,支持同步备份功能。项目地址: https://gitcode.com/GitHub_Trending/ali/aliyunpan
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考