[
  {
    "path": ".gitignore",
    "content": ".git"
  },
  {
    "path": "Readme.md",
    "content": "## elecV2P 文档/例程/通知/反馈 - documents/examples/information/issues\r\n\r\n主项目地址: https://github.com/elecV2/elecV2P\r\n\r\n![](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/overview.png)\r\n\r\nTG 频道: https://t.me/elecV2\r\nTG 交流群: https://t.me/elecV2G\r\n\r\n欢迎提交功能需求或者其他建议。\r\n\r\n### 说明\r\n\r\n- 文档目录：[docs](https://github.com/elecV2/elecV2P-dei/tree/master/docs)\r\n- 例程目录：[examples](https://github.com/elecV2/elecV2P-dei/tree/master/examples)\r\n- 初次使用建议先把 docs 内容浏览一遍，涉及内容较多，大部分可以直接跳过，在使用中碰到问题时再来查看"
  },
  {
    "path": "docs/01-overview.md",
    "content": "```\r\n最近更新： 2022-10-21\r\n适用版本： 3.7.3\r\n```\r\n\r\n*此章节内容同步自 [elecV2P Readme 文档](https://github.com/elecV2/elecV2P)*\r\n\r\n## 简介\r\n\r\nelecV2P - customize personal network.\r\n一款基于 NodeJS，可通过 JS 修改网络请求，以及定时运行脚本或 SHELL 指令的网络工具。\r\n\r\n![elecV2P overview/预览](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/overview.png)\r\n\r\n### 基础功能\r\n\r\n- 查看/修改网络请求 (MITM)\r\n- 定时执行 JS/SHELL 脚本\r\n- FEED/IFTTT/自定义 通知\r\n- EFSS 基础文件管理\r\n\r\n## 安装/INSTALL\r\n\r\n***程序开放权限极大，建议局域网使用。公网部署（务必参考 [Advanced.md](https://github.com/elecV2/elecV2P-dei/blob/master/docs/Advanced.md)），风险自负***\r\n\r\n*elecV2P 所有文件及依赖总大小约 90 M。初始运行时内存占用约 90 M，运行 100 个定时任务时总内存占用约 150 M（仅供参考，不同软硬件条件下程序调用资源可能有所不同）*\r\n\r\n**在可使用 Docker 的情况下，推荐使用方法三进行安装**\r\n\r\n### 方法一：直接 NODEJS 运行\r\n\r\n**需求 NODEJS 版本 (node -v) >= 14.17.0**\r\n\r\n``` sh\r\ngit clone https://github.com/elecV2/elecV2P.git\r\ncd elecV2P\r\n\r\n# 安装依赖库（根据网络环境和硬盘读写速度，需要 1-10 分钟不等\r\nyarn\r\n\r\n# elecV2P 默认以 pm2 的方式启动，需要先安装好 pm2\r\n# pm2 的安装方式:\r\n# 1. 添加 elecV2P 所在目录/node_modules/.bin 到系统环境变量 PATH 中\r\n# 2. 或者直接执行 yarn global add pm2\r\n# 然后执行命令\r\nyarn start\r\n\r\n# 其他基础方式启动命令\r\nnode index.js\r\n# 假如提示 80 端口不可用，尝试命令\r\n# windows 平台 CMD:\r\n# set PORT=8000 && node index.js\r\n# windows 平台 PowerShell:\r\n# $env:PORT=\"8000\";node index.js\r\n# 其他平台：\r\n# PORT=8000 TZ=Asia/Shanghai node index.js\r\n## TZ=Asia/Shanghai 用于设置程序运行时区\r\n```\r\n\r\n#### 升级\r\n\r\n方式一：使用 [softupdate.js](https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/softupdate.js) 软更新升级\r\n\r\n- 首先在 webUI/JSMANAGE 脚本管理中找到 softupdate.js 文件，假如不存在就远程推送或本地上传一下\r\n- 然后按照文件内的说明，根据自身需求更改 CONFIG 设置项\r\n- 最后点击测试运行即可\r\n\r\n方式二：手动升级（不推荐\r\n\r\n- 先备份好个人数据，比如 根证书，以及 efss、script/JSFile、Store、Lists、Shell 等文件夹\r\n- （推荐在 webUI/efss 界面，右键对应文件夹，然后 zip 打包下载。）\r\n- 然后在项目目录下执行命令 git pull，拉取最新的代码进行覆盖升级\r\n- 最后再把备份好的文件上传/复制还原到之前的位置\r\n\r\n### 其他 PM2 相关指令\r\n\r\n``` sh\r\npm2 stop elecV2P  # 停止 elecV2P\r\npm2 stop all      # 停止所有程序\r\n\r\npm2 restart elecV2P   # 重启 elecV2P\r\npm2 restart 0\r\n\r\npm2 ls      # 查看运行状态\r\npm2 logs    # 查看运行日志\r\n\r\npm2 -h      # 查看 PM2 帮助列表\r\n```\r\n\r\n### 方法二：DOCKER\r\n\r\n镜像名称: elecv2/elecv2p\r\n镜像地址: https://hub.docker.com/r/elecv2/elecv2p\r\n\r\n``` sh\r\n# 基础使用命令\r\ndocker run --restart=always -d --name elecv2p -e TZ=Asia/Shanghai -p 80:80 -p 8001:8001 -p 8002:8002 elecv2/elecv2p\r\n\r\n# 推荐使用命令\r\ndocker run --restart=always \\\r\n  -d --name elecv2p \\\r\n  -e TZ=Asia/Shanghai \\\r\n  -p 8100:80 -p 8101:8001 -p 8102:8002 \\\r\n  -v /elecv2p/JSFile:/usr/local/app/script/JSFile \\\r\n  -v /elecv2p/Lists:/usr/local/app/script/Lists \\\r\n  -v /elecv2p/Store:/usr/local/app/script/Store \\\r\n  -v /elecv2p/Shell:/usr/local/app/script/Shell \\\r\n  -v /elecv2p/rootCA:/usr/local/app/rootCA \\\r\n  -v /elecv2p/efss:/usr/local/app/efss \\\r\n  elecv2/elecv2p\r\n\r\n# -p/-v 对应参数 宿主:容器\r\n# 如需更改默认的 80 端口，可在 -e 后面加上 PORT=8000\r\n# 升级 Docker 镜像（如果没有使用 -v 持久化存储，容器内数据会丢失，请提前备份）\r\ndocker rm -f elecv2p           # 先删除旧的容器\r\ndocker pull elecv2/elecv2p     # 再拉取新的镜像\r\n# 再使用之前的 docker run xxxx 命令重新启动一下\r\n# 如果拉取到的镜像不是最新的版本，请修改 Docker 当前使用的仓库地址\r\n```\r\n\r\n- ARM32 平台如果出错，参考 [issues #78](https://github.com/elecV2/elecV2P/issues/78)\r\n\r\n### 方法三：DOCKER-COMPOSE （推荐）\r\n\r\n``` sh\r\n# 创建 elecV2P 持久化数据保存目录\r\nmkdir /elecv2p && cd /elecv2p\r\n# 假如失败，请尝试在其他有权限的目录进行创建\r\n# 后面 docker-compose.yaml 映射目录保持和创建的目录一致\r\n\r\n# 下载 docker-compose.yaml 文件\r\ncurl -sL https://git.io/JLw7s > docker-compose.yaml\r\n# 启动运行 elecV2P\r\ndocker-compose up -d\r\n\r\n# 注意: 需提前安装好 docker-compose 管理器\r\n# 默认将 80/8001/8002 端口分别映射到了宿主机的 8100/8101/8102 端口，以防出现占用的情况\r\n# 如果需要设置为其他端口，请自行修改 docker-compose.yaml 文件内容，然后重新启动\r\n```\r\n\r\n以下为 docker-compose.yaml 文件内容，可根据自身需求进行修改。\r\n\r\n``` yaml\r\nversion: '3.7'\r\nservices:\r\n  elecv2p:\r\n    image: elecv2/elecv2p\r\n    container_name: elecv2p\r\n    restart: always\r\n    environment:\r\n      - TZ=Asia/Shanghai\r\n    ports:\r\n      - \"8100:80\"\r\n      - \"8101:8001\"\r\n      - \"8102:8002\"\r\n    volumes:\r\n      - \"/elecv2p/JSFile:/usr/local/app/script/JSFile\"\r\n      - \"/elecv2p/Lists:/usr/local/app/script/Lists\"\r\n      - \"/elecv2p/Store:/usr/local/app/script/Store\"\r\n      - \"/elecv2p/Shell:/usr/local/app/script/Shell\"\r\n      - \"/elecv2p/rootCA:/usr/local/app/rootCA\"\r\n      - \"/elecv2p/efss:/usr/local/app/efss\"\r\n```\r\n\r\n修改后保存文件，然后在 docker-compose.yaml 文件所在目录下执行以下任一命令\r\n\r\n``` sh\r\n# 直接启动（首次启动命令）\r\ndocker-compose up -d\r\n\r\n# 更新镜像并重新启动\r\ndocker-compose pull elecv2p && docker-compose up -d\r\n```\r\n\r\n- 如果在某些设备上无法启动，尝试把文件开头的 version: '3.7' 更改为 version: '3.3'\r\n- ARM32 平台如果出错，参考 [issues #78](https://github.com/elecV2/elecV2P/issues/78)\r\n\r\n其他 docker 相关指令\r\n\r\n``` sh\r\n# 查看是否启动及对应端口\r\ndocker ps\r\n\r\n# 查看 elecV2P 运行日志\r\ndocker logs elecv2p -f\r\n```\r\n\r\n## 默认端口\r\n\r\n- 80：    webUI 后台管理界面。用于添加规则/管理脚本/定时任务/MITM 证书 等\r\n- 8001：  ANYPROXY HTTP 代理端口。（*代理端口不是网页，不能通过浏览器直接访问*）\r\n- 8002：  ANYPROXY 代理请求查看端口\r\n\r\n**ANYPROXY 相关端口默认关闭。可在 webUI 首页双击 ANYPROXY 临时开启。**\r\n**如需在启动时自动开启，请前往 webUI->SETTING->初始化相关设置 中进行设置。**\r\n**80/8002 对应端口需要用到 websocket，在使用 nginx 等反代工具时注意设置。参考 [ev2p-nginx.conf](https://github.com/elecV2/elecV2P-dei/blob/master/examples/ev2p-nginx.conf)**\r\n\r\n- *80 端口可使用环境变量 **PORT** 进行修改(比如: PORT=8000 node index.js)*\r\n- *在 elecV2P 已经启动时，可在 webUI->SETTING->初始化相关设置 中修改其他端口*\r\n- *在 elecV2P 尚未启动时，可在 script/Lists/config.json 文件中修改对应端口*\r\n\r\n## 根证书相关 - HTTPS 解密\r\n\r\n- *如果不使用 RULES/REWRITE 等 MITM 相关功能，此步骤可跳过。*\r\n- *升级启动后，如果不是使用之前的证书，需要重新下载安装信任根证书。*\r\n- *根证书包含两个文件 rootCA.crt/rootCA.key，文件名不可修改。*\r\n\r\n### 安装证书\r\n\r\n选择以下任意一种方式下载证书，然后安装并信任\r\n\r\n- 直接打开 :80/crt\r\n- :80 -> MITM -> 安装证书\r\n- :8002 -> RootCA\r\n\r\n根证书物理存储目录位于 `$HOME/.anyproxy/certificates`。\r\n\r\n*windows 平台的证书存储位置选择 浏览->受信任的根证书颁发机构*\r\n\r\n### 使用自签根证书\r\n\r\n在 webUI->MITM 界面上传自签根证书，然后重启 elecV2P\r\n\r\n**注意：使用新的证书后，记得重新下载安装信任证书，并清除由之前根证书签发的域名证书。**\r\n\r\n## RULES - 网络请求修改\r\n\r\n![rules](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/rules.png)\r\n\r\n详细说明参考: [docs/03-rules.md](https://github.com/elecV2/elecV2P-dei/tree/master/docs/03-rules.md)\r\n\r\n## 定时任务\r\n\r\n![task](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/taskall.png)\r\n\r\n支持两种定时方式：\r\n\r\n- 倒计时\r\n- cron 定时\r\n\r\n### 时间格式：\r\n\r\n- 倒计时 30 999 3 2  (以空格分开的四个数字，后三项可省略)\r\n\r\n|    30（秒）    |     999（次）   |      3（秒）         |       2（次）       \r\n:--------------: | :-------------: | :------------------: | :------------------:\r\n| 基础倒计时时间 | 重复次数（可选）| 增加随机时间（可选） | 增加随机重复次数（可选）  \r\n\r\n\r\n- *当重复次数大于等于 **999** 时，无限循环*\r\n\r\n示例: 40 8 10 3 ，表示倒计时40秒，随机10秒，所以具体倒计时时间位于 40-50 秒之间，重复运行 8-11 次\r\n\r\n- cron 定时 \r\n\r\n时间格式：* * * * * * （五/六位 cron 时间格式）\r\n\r\n| * (0-59)   |  * (0-59)  |  * (0-23)  |  * (1-31)  |  * (1-12)  |  * (0-7)      \r\n:----------: | :--------: | :--------: | :--------: | :--------: | :---------:\r\n| 秒（可选） |    分      |    小时    |     日     |     月     |    星期\r\n\r\n\r\n### 可执行任务类型\r\n\r\n- 运行 JS\r\n- 开始/停止 其他定时任务\r\n- 基础 shell 指令。比如 *rm -f \\**, *python test.py*, *reboot* 等等\r\n\r\n更多说明参考：[docs/06-task.md](https://github.com/elecV2/elecV2P-dei/tree/master/docs/06-task.md)\r\n\r\n## 通知\r\n\r\n目前支持通知方式：\r\n- FEED/RSS 订阅\r\n- IFTTT WEBHOOK\r\n- BARK 通知\r\n- 自定义通知\r\n\r\nFEED/RSS 订阅地址为 webUI/feed。\r\n\r\n通知内容：\r\n- 定时任务开始/结束\r\n- 定时任务 JS 运行次数\r\n- 脚本中的自主调用通知\r\n\r\nIFTTT/BARK/自定义通知等相关设置参考: [07-feed&notify](https://github.com/elecV2/elecV2P-dei/tree/master/docs/07-feed&notify.md)\r\n\r\n## DOCUMENTS&EXAMPLES\r\n\r\n说明文档及一些例程: [https://github.com/elecV2/elecV2P-dei](https://github.com/elecV2/elecV2P-dei)\r\n\r\n如果遇到问题欢迎 [open a issue](https://github.com/elecV2/elecV2P/issues)。尽量说明使用平台，版本，以及附上相关的错误日志（提供的信息越详细，越有助于解决问题）。\r\n\r\nTG 频道: https://t.me/elecV2\r\nTG 交流群: https://t.me/elecV2G\r\n\r\n## 更新日志\r\n\r\n查看: https://github.com/elecV2/elecV2P/blob/master/logs/update.log\r\n\r\n## 贡献参考\r\n\r\n- [anyproxy](https://github.com/alibaba/anyproxy)\r\n- [axios](https://github.com/axios/axios)\r\n- [expressjs](https://expressjs.com)\r\n- [node-cron](https://github.com/merencia/node-cron)\r\n- [node-rss](https://github.com/dylang/node-rss)\r\n- [pm2](https://pm2.keymetrics.io)\r\n- [vue](https://vuejs.org)\r\n- [vue-draggable-resizable](https://github.com/mauricius/vue-draggable-resizable)\r\n- [ace](https://github.com/ajaxorg/ace)\r\n- [adm-zip](https://github.com/cthackers/adm-zip)\r\n- [Ant Design Vue](https://www.antdv.com)\r\n\r\n### 说明文档列表\r\n\r\n- [overview - 简介及安装](01-overview.md)\r\n- [task - 定时任务](06-task.md)\r\n- [rewrite - 重写网络请求](05-rewrite.md)\r\n- [rules - 网络请求更改规则](03-rules.md)\r\n- [script - 脚本编写及说明](04-JS.md)\r\n- [Docker - Docker 运行相关](02-Docker.md)\r\n- [feed&notify - 通知相关](07-feed&notify.md)\r\n- [logger&efss - 日志和 EFSS 文件管理](08-logger&efss.md)\r\n- [webhook - webhook 使用简介](09-webhook.md)\r\n- [config - 配置文件说明](10-config.md)\r\n- [Advanced - 高级使用篇](Advanced.md)\r\n"
  },
  {
    "path": "docs/02-Docker.md",
    "content": "```\r\n最近更新: 2022-03-15\r\n适用版本: 3.6.3\r\n文档地址: https://github.com/elecV2/elecV2P-dei/blob/master/docs/02-Docker.md\r\n```\r\n\r\n## 简介\r\n\r\nDocker 镜像名称: elecv2/elecv2p\r\nDocker 镜像地址: https://hub.docker.com/r/elecv2/elecv2p\r\n\r\n## docker 及 docker-compose 的安装\r\n\r\n``` sh\r\n# 不同平台的安装方式可能不一样，仅供参考\r\n# docker 安装\r\nwget -qO- https://get.docker.com/ | sh\r\n\r\n# docker-compose 安装。（前往 https://github.com/docker/compose/releases 查看适合自己设备的版本）\r\ncurl -L \"https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)\" -o /usr/local/bin/docker-compose\r\n```\r\n\r\n## Docker 运行 elecV2P\r\n\r\n*以下命令仅供参考，具体映射端口和卷根据实际情况进行调整*\r\n\r\n```sh\r\n# 基础启动命令（重建后数据会丢失）\r\ndocker run --restart=always -d --name elecv2p -e TZ=Asia/Shanghai -p 80:80 -p 8001:8001 -p 8002:8002 elecv2/elecv2p\r\n\r\n# 推荐使用命令\r\ndocker run --restart=always \\\r\n  -d --name elecv2p \\\r\n  -e TZ=Asia/Shanghai \\\r\n  -p 8100:80 -p 8101:8001 -p 8102:8002 \\\r\n  -v /elecv2p/JSFile:/usr/local/app/script/JSFile \\\r\n  -v /elecv2p/Lists:/usr/local/app/script/Lists \\\r\n  -v /elecv2p/Store:/usr/local/app/script/Store \\\r\n  -v /elecv2p/Shell:/usr/local/app/script/Shell \\\r\n  -v /elecv2p/rootCA:/usr/local/app/rootCA \\\r\n  -v /elecv2p/efss:/usr/local/app/efss \\\r\n  elecv2/elecv2p\r\n\r\n# -p/-v 对应环境参数 宿主参数:容器内参数\r\n# 宿主机映射目录尽量填写尚未创建或空的文件夹\r\n# 如需更改默认的 80 端口，可在 -e 后面加上 PORT=8000\r\n# 某些设备上，可能无法在根目录创建 elecv2p 文件夹，这时请根据使用设备搜索可操作的目录，进行替换\r\n# 如果在部分复杂的网络情况下出现无法联网或访问的问题，尝试在命令中添加 --net=host\r\n\r\n# 查看 docker 运行状态\r\ndocker ps\r\n\r\n# 进入容器内部\r\ndocker exec -it elecv2p /bin/sh\r\n\r\n# Docker 的启动暂停\r\ndocker start elecv2p\r\ndocker stop elecv2p\r\ndocker restart elecv2p\r\n\r\n# 查看 Docker 运行日志\r\ndocker logs elecv2p -f\r\ndocker logs elecv2p --tail 20\r\n\r\n# 清除 Docker 运行日志\r\necho \"\" > $(docker inspect --format='{{.LogPath}}' elecv2p)\r\n\r\n# 升级容器\r\n# 先移除容器\r\ndocker rm -f elecv2p\r\n# 再拉取最新的镜像\r\ndocker pull elecv2/elecv2p\r\n# 最后再使用上面的 docker run 命令重新启动\r\n```\r\n\r\n## docker-compose 启动\r\n\r\n``` sh\r\nmkdir /elecv2p && cd /elecv2p\r\ncurl -sL https://git.io/JLw7s > docker-compose.yaml\r\n\r\ndocker-compose up -d\r\n\r\n# 默认把 80/8001/8002 端口分别映射成了 8100/8101/8102，以防出现端口占用的情况，访问时注意\r\n# 如果需要设置为其他端口，可以自行修改下面的内容然后手动保存\r\n```\r\n\r\n或者将以下内容手动保存为 docker-compose.yaml 文件。\r\n\r\n``` yaml\r\nversion: '3.7'\r\nservices:\r\n  elecv2p:\r\n    image: elecv2/elecv2p\r\n    container_name: elecv2p\r\n    restart: always\r\n    environment:\r\n      - TZ=Asia/Shanghai\r\n    ports:\r\n      - \"8100:80\"\r\n      - \"8101:8001\"\r\n      - \"8102:8002\"\r\n    volumes:\r\n      - \"/elecv2p/JSFile:/usr/local/app/script/JSFile\"\r\n      - \"/elecv2p/Lists:/usr/local/app/script/Lists\"\r\n      - \"/elecv2p/Store:/usr/local/app/script/Store\"\r\n      - \"/elecv2p/Shell:/usr/local/app/script/Shell\"\r\n      - \"/elecv2p/rootCA:/usr/local/app/rootCA\"\r\n      - \"/elecv2p/efss:/usr/local/app/efss\"\r\n```\r\n\r\n- *具体使用的映射端口和 volumes 目录，根据个人情况进行调整*\r\n- *如需更改默认的 80 端口，在 environment 下添加一行: - PORT=8000*\r\n- *如果在某些设备上无法启动，尝试把文件开头的 version: '3.7' 更改为 version: '3.3'*\r\n\r\n然后在 docker-compose.yaml 同目录执行命令 **docker-compose up -d** ，启动程序。\r\n\r\n### env 默认环境变量\r\n\r\n在 elecV2P 启动前，可设置部分环境变量\r\n\r\n- TZ: 时区设置 timezone\r\n- PORT: webUI 对应端口，默认为 80\r\n- TOKEN: 启动时指定 WEBHOOK TOKEN\r\n\r\n使用示例：\r\n\r\n``` sh\r\ndocker run --restart=always \\\r\n  -d --name elecv2p \\\r\n  -e TZ=Asia/Shanghai PORT=8000 TOKEN=YOUR-WEBHOOK-TOKEN \\\r\n  -p 8100:8000 -p 8101:8001 -p 8102:8002 \\\r\n  -v /elecv2p/JSFile:/usr/local/app/script/JSFile \\\r\n  -v /elecv2p/Lists:/usr/local/app/script/Lists \\\r\n  -v /elecv2p/Store:/usr/local/app/script/Store \\\r\n  -v /elecv2p/Shell:/usr/local/app/script/Shell \\\r\n  -v /elecv2p/rootCA:/usr/local/app/rootCA \\\r\n  -v /elecv2p/efss:/usr/local/app/efss \\\r\n  elecv2/elecv2p\r\n```\r\n\r\n环境变量可以同时设置部分或全部\r\n\r\n**在 docker-compose 中 env 对应 environment**\r\n\r\n### 其他指令\r\n\r\n``` sh\r\n# 更新升级\r\ndocker-compose pull elecv2p && docker-compose up -d\r\n\r\n# 拉取特定版本的镜像文件。可用版本以 https://hub.docker.com/r/elecv2/elecv2p 的 tag 为准\r\ndocker pull elecv2/elecv2p:3.4.5\r\ndocker pull elecv2/elecv2p:arm64-3.0    # 在使用这些特定版本的镜像时，docker run 后面的镜像名也要记得调整\r\n\r\ndocker image prune       # 清除没有挂载的镜像文件\r\n\r\n# 查看运行日志\r\ndocker logs elecv2p -f\r\n```\r\n\r\n### 一些说明\r\n\r\n- 当使用国内的一些 docker 源，因为缓存问题，更新之后可能不是最新的版本，需要手动更换一下 docker 源。（具体步骤谷歌）\r\n- arm32 平台如果出错，参考 [issues #78](https://github.com/elecV2/elecV2P/issues/78)\r\n\r\n### 说明文档列表\r\n\r\n- [overview - 简介及安装](01-overview.md)\r\n- [task - 定时任务](06-task.md)\r\n- [rewrite - 重写网络请求](05-rewrite.md)\r\n- [rules - 网络请求更改规则](03-rules.md)\r\n- [script - 脚本编写及说明](04-JS.md)\r\n- [Docker - Docker 运行相关](02-Docker.md)\r\n- [feed&notify - 通知相关](07-feed&notify.md)\r\n- [logger&efss - 日志和 EFSS 文件管理](08-logger&efss.md)\r\n- [webhook - webhook 使用简介](09-webhook.md)\r\n- [config - 配置文件说明](10-config.md)\r\n- [Advanced - 高级使用篇](Advanced.md)\r\n"
  },
  {
    "path": "docs/03-rules.md",
    "content": "```\r\n最近更新: 2022-03-24\r\n适用版本: 3.7.8\r\n文档地址: https://github.com/elecV2/elecV2P-dei/blob/master/docs/03-rules.md\r\n```\r\n\r\n## 准备工作\r\n\r\n- **再使用 RULES/REWRITE 相关功能前，请确定 ANYPROXY 已打开**\r\n- 已正确将网络请求代理到 ANYPROXY 端口\r\n- 匹配 https 请求请先添加 MITM host，普通 http 请求无需添加\r\n- *首次命中 https 请求时，系统需要生成中间证书，可能会稍长一点时间返回结果*\r\n\r\n## modify 规则集 格式说明\r\n\r\n|   匹配方式   |    匹配内容（正则）   |  修改方式 |       修改目标      |  修改时间点\r\n :-----------: | --------------------- | :-------: | ------------------- | ----------\r\n| url          | ^https://api.b.com/v2 | JS        | file.js             |  前(req)\r\n| host         | api.bilibili.com      | useragent | iPhone 6s           |  后(res)\r\n| useragent    | neteaseMusic / aliApp | block     | reject|tinyimg      |\r\n| reqmethod    | GET/POST/PUT/DELETE   | $HOLD     | 30\r\n| reqbody      | queryPara/word string |           |\r\n| resstatus    | 200 / 404 / 301 / ... |           |\r\n| restype      | text/html / text/json | -----     |\r\n| resbody      | Keyword(string)       | all - JS  |\r\n\r\n- *实际使用中匹配方式和修改方式可以任意搭配*\r\n\r\n### 匹配方式\r\n\r\n```\r\nurl             // 匹配 url \r\nhost            // 匹配 url host 部分\r\nuseragent       // 匹配 User-Agent \r\nreqmethod       // 匹配 网络请求方式\r\nreqbody         // 匹配 请求体（body）\r\nresstatus       // 匹配 请求返回的状态码\r\nrestype         // 匹配 返回的数据类型\r\nresbody         // 匹配 返回的数据内容\r\n```\r\n\r\n- **v3.7.8 默认不再对 reqbody/resbody 内容进行匹配，以提升 elecV2P MITM 效率。如需开启，请参考下文源文件格式部分，增加属性项 \"enbody\": true。（不匹配不代表不可以修改，仍然可以通过 url/host 等方式进行匹配，然后使用脚本对 body 内容进行修改）**\r\n\r\n### 修改方式\r\n\r\n#### JS\r\n\r\n通过 JS 脚本修改网络请求数据，对应修改内容为 JS 文件名或远程 JS 链接。\r\n\r\n从该模块运行 JS，默认会添加 $request，$response(**数据返回前**) 两个变量，具体参数如下：\r\n\r\n- $request.headers, $request.body, $request.method, $request.hostname, $request.port, $request.path, $request.url\r\n- $response.headers, $response.body, $response.statusCode\r\n\r\n#### 307 重定向\r\n\r\n对应修改内容为重定向目标网址\r\n\r\n#### 阻止\r\n\r\nreject: 返回状态码 200, body 为空。 \r\ntinyimg: 返回状态码为 200, body 为一张 1x1 的图片\r\n\r\n#### $HOLD\r\n\r\n将原网络请求的 header 和 body 发送到前端网页进行修改处理，然后将修改后的数据直接发送给服务器/客户端。\r\n\r\n对应修改内容表示等待前端修改数据的时间，单位秒。当为 **0** 时，表示一直等待。如果为其他值且超时时则直接使用原数据进行下步操作。\r\n\r\n使用该修改方式时，请尽量使用比较详细的匹配规则，匹配单一网络请求，否则后面的 $HOLD 请求会覆盖前面的数据。\r\n\r\n**2020.7.16 2.1.0 更新**\r\n\r\n$HOLD request reject - 直接返回当前数据\r\n\r\n返回默认状态码: 200\r\n\r\n数据包含两部分: header 和 body\r\n\r\n#### User-Agent\r\n\r\n修改请求 header 中的 User-Agent。\r\n\r\n默认 User-Agent 可在 webUI->SETTING->网络请求相关设置进行管理修改\r\n\r\n### 修改时间\r\n\r\n网络请求匹配时间\r\n\r\n#### 网络请求前\r\n\r\nbeforeSendRequest\r\n\r\n#### 数据返回前\r\n\r\nbeforeSendResponse\r\n\r\n## 源文件格式\r\n\r\nRULES 规则列表保存于 **./script/Lists/default.list**，实际格式为严格的 JSON 类型（不包含任何注释）。\r\n*（参考: https://raw.githubusercontent.com/elecV2/elecV2P/master/script/Lists/default.list ）*\r\n\r\n``` JSON\r\n{\r\n  \"rules\": {\r\n    \"note\": \"elecV2P RULES 规则列表\",\r\n    \"enable\": false,         // 是否启用下面列表中的规则，仅在该值为 false 时，表示不启用，默认启用\r\n    \"enbody\": false,         // 是否对请求体(body)进行匹配，仅在该值为 true 时，表示启用（v3.7.8 添加，默认不启用\r\n    \"list\": [\r\n      {\r\n        \"mtype\": \"url\",\r\n        \"match\": \"adtest\",\r\n        \"ctype\": \"block\",\r\n        \"target\": \"reject\",\r\n        \"stage\": \"req\"\r\n      },\r\n      {\r\n        \"mtype\": \"url\",\r\n        \"match\": \"httpbin.org/get\\\\?hold\",\r\n        \"ctype\": \"hold\",\r\n        \"target\": \"0\",\r\n        \"stage\": \"req\",     // enable 可省略。仅在 enable 为 false 的时候表示不启用\r\n        \"enable\": true\r\n      }\r\n    ]\r\n  }\r\n}\r\n```\r\n\r\n*如非必要，请不要手动修改 list 源文件*\r\n\r\n### 说明文档列表\r\n\r\n- [overview - 简介及安装](01-overview.md)\r\n- [task - 定时任务](06-task.md)\r\n- [rewrite - 重写网络请求](05-rewrite.md)\r\n- [rules - 网络请求更改规则](03-rules.md)\r\n- [script - 脚本编写及说明](04-JS.md)\r\n- [Docker - Docker 运行相关](02-Docker.md)\r\n- [feed&notify - 通知相关](07-feed&notify.md)\r\n- [logger&efss - 日志和 EFSS 文件管理](08-logger&efss.md)\r\n- [webhook - webhook 使用简介](09-webhook.md)\r\n- [config - 配置文件说明](10-config.md)\r\n- [Advanced - 高级使用篇](Advanced.md)\r\n"
  },
  {
    "path": "docs/04-JS.md",
    "content": "```\r\n最近更新: 2024-11-10\r\n适用版本: 3.8.1\r\n文档地址: https://github.com/elecV2/elecV2P-dei/blob/master/docs/04-JS.md\r\n```\r\n\r\n**每个脚本理论上都有权限对服务器上的任一文件进行修改，请不要运行不信任的脚本。**\r\n\r\n## 保存目录\r\n\r\n本地脚本物理存储目录位于 **./script/JSFile**，在 RULS/REWRITE/TASK/WEBHOOK 中调用时，直接使用对应的文件名即可。支持多级目录，比如 test/exam.js。\r\n\r\n所有文件名称和内容可在 webUI->JSMANAGE/脚本管理 中查看和修改。\r\n\r\n*如果远程脚本推送失败，尝试在 SETTING/设置->网络请求相关设置 中添加代理，或者下载后使用本地上传。*\r\n\r\n## CONTEXT - 脚本运行环境\r\n\r\n在 elecV2P 中，默认脚本的运行环境是基于 [vm](http://nodejs.org/api/vm.html) 模块的虚拟环境，同时增加了以下一些默认的环境变量及函数。\r\n\r\n### 默认参数/环境变量\r\n\r\n\r\n#### 主要函数\r\n\r\n```\r\n- $axios           // 网络请求\r\n- $cheerio         // HTML处理\r\n- $exec            // 简单 shell 命令执行\r\n- $download        // 文件下载\r\n- $feed            // 通知模块\r\n- $store           // cookie/常量/数据存储\r\n- $evui            // 在前端网页生成 UI 界面\r\n- $message         // 发送网页消息\r\n- $done            // 返回脚本执行结果（后面代码会继续执行）\r\n\r\n- $cache           // 临时数据存储（v3.4.3 添加）\r\n- $task            // 定时任务管理（v3.4.4 添加，sudo 模式下生效）\r\n- $env             // 临时环境变量（v3.4.5 添加，默认包含 process.env 中的所有值）\r\n- $fend            // efh 脚本前后端通信函数（v3.5.5 添加）\r\n- $webhook         // 调用 webhook 接口（v3.5.8 添加，sudo 模式下生效）\r\n```\r\n\r\n可利用这些函数判断当前脚本是否运行在 elecV2P 中，比如：\r\n\r\n``` JS\r\nif (typeof $fend !== 'undefined') {\r\n  console.log('elecV2P 运行环境')\r\n} else {\r\n  console.log('其他运行环境')\r\n}\r\n```\r\n\r\n#### 附加变量 (以两个短下划线开头)\r\n\r\n```\r\n- __version        // 当前 elecV2P 版本\r\n- __vernum         // 当前版本数字表达（v3.4.5 添加）。比如版本 3.4.5 表达为 345\r\n- __home           // 主页地址。 可在 webUI->SETTING 界面设置\r\n- __efss           // efss 目录。 可在 webUI/efss 页面设置\r\n- __name           // 脚本名。 v3.3.0 添加 (如果是多级目录脚本，会包含目录)\r\n- __dirname        // 脚本所在目录 (v3.4.4)\r\n- __filename       // 脚本完整路径 (v3.4.4)\r\n- __userid         // 用户 ID (v3.6.4)\r\n- __md5hash        // 当前脚本内容的 md5 hash 值（v3.6.7）\r\n\r\n- __taskid         // 启动该脚本的任务 id (仅在定时任务触发脚本时有值，其他时候为 undefined)\r\n- __taskname       // 启动该脚本的任务名称 (仅在定时任务触发脚本时有值，其他时候为 undefined)\r\n\r\n// 测试 JS:\r\n// console.log('dirname:', __dirname, 'version:', __version, 'homepage:', __home)\r\n// console.log('当前 efss 目录:', __efss, '当前脚本名称:', __name)\r\n// if (__vernum >= 367) console.log('当前脚本 md5 hash:', __md5hash)\r\n```\r\n\r\n#### 特殊变量函数\r\n\r\n```\r\n- $ws              // 通过 websocket 向前端发送数据\r\n- $request/$response\r\n- require()        // 直接引用其他 nodejs 模块或脚本\r\n- console.clear()  // 清空该脚本的运行日志\r\n- // @grant        // 在单个脚本开启增强功能\r\n```\r\n\r\n### @grant（v3.8.1 后取消脚本环境兼容性判断\r\n\r\nJS 文件开头使用 **// @grant** 开启一些增强功能\r\n\r\n*如果要使用纯 nodejs 环境运行，在 TASK 中请选择 shell 指令模式，然后: node xxxxx.js。 或者在脚本中使用 $exec('node xxxx.js') 来执行*\r\n\r\n```\r\n// @grant nodejs 脚本和直接使用 node xxxx.js 运行脚本的区别:\r\n\r\n- 使用 node xxxxx.js 无法使用 $axios/$store/$feed 等非 nodejs 原生函数/变量\r\n- 默认脚本运行在 vm 的虚拟环境中，而 node xxxx.js 运行在原生系统环境中\r\n```\r\n\r\n日志调整\r\n\r\n* **// @grant  calm**    ;不打印日志，但保留到日志文件中，也不影响通知(console.error 错误日志还是会正常打印)\r\n* **// @grant  still**   ;不输出日志，有通知(即 console 函数无效)\r\n* **// @grant  quiet**   ;输出日志，但不通知\r\n* **// @grant  silent**  ;不输出日志，也不通知\r\n\r\n#### sudo 模式(v3.4.4)\r\n\r\n* **// @grant  sudo** \r\n\r\nsudo 模式下启用功能\r\n\r\n- $task  ;定时任务管理（v3.4.4）\r\n- $webhook   ;调用 webhook 接口功能（v3.5.8）\r\n- 其他功能待添加\r\n\r\n### $axios - 网络请求\r\n\r\n$axios(request)\r\n\r\nrequest 格式 [object/string]\r\n- object：支持参数参考：[axios](https://github.com/axios/axios)\r\n- string：单个 url 链接\r\n\r\n``` JS\r\n// --- example 1 ---\r\n$axios('https://httpbin.org/get?hello=elecV2P').then(res=>console.log(res.data)).catch(e=>console.log(e))\r\n\r\n// --- example 2 ---\r\n$axios({\r\n  url: 'https://httpbin.org/put',\r\n  method: 'put',\r\n  timeout: 6000,\r\n  data: {\r\n    hello: 'elecV2P'\r\n  }\r\n}).then(res=>console.log(res.data)).catch(e=>console.log(e.message))\r\n```\r\n\r\n* **$axios** 无 .put/.post 等方法，使用 { ..., method: 'put/post' } 实现\r\n\r\n**v2.1.8 更新 $axios(request, proxy)**\r\n\r\n增加第二个参数 proxy, 此参数会覆盖 request.proxy, 示例\r\n\r\n``` JS\r\n$axios(request, {\r\n  host: '127.0.0.1',\r\n  port: 9000,\r\n  auth: {\r\n    username: 'hello',\r\n    password: 'elecV2P'\r\n  }\r\n}).then(res=>console.log(res.data)).catch(e=>console.log(e.message))\r\n\r\n// 当 proxy 为 {} 时使用内部 ANYPROXY 代理，为 false 时: 强制跳过使用代理，如省略则使用 webUI->SETTING 网络请求相关设置。\r\n// 其他设置参考 [axios](https://github.com/axios/axios) request/proxy 部分\r\n```\r\n\r\n如在运行脚本时需要访问某些境外网站，可在 webUI->SETTING 网络请求相关设置 中添加代理。\r\n\r\n### $store - store/cookie 常量\r\n\r\n所有存储常量以文件的形式保存在 **script/Store** 目录中。\r\n所有脚本共用存储常量，比如在 a.js 中使用 $store.put('something', 'akey'), 可在 b.js 中使用 $store.get('akey') 来获取存储值。\r\n\r\n``` JS example\r\n$store.get(key, options)             // 获取存储值\r\n// options 可选\r\n// - string 字符串\r\n//   - 'raw': 获取存储文件源内容\r\n//   - 'string/array/object/boolean/number': 以相应格式获取存储值\r\n// - object (v3.6.6 新增)\r\n//   {\r\n//     type: 'string',       // 可选值同上面字符串\r\n//     pass: 'string',       // 获取加密内容时密码\r\n//     algo: 'ebuf',         // 获取加密内容时算法，可省略。默认为 ebuf 自定义算法\r\n//   }\r\n\r\n$store.put(value, key, options)    // 保存\r\n// options 可选\r\n// - string 字符串\r\n//   - 'a': 添加内容。具体参考下面的 JS 实例\r\n//   - 'string/array/object/boolean/number': 保存为对应格式\r\n// - object (v3.3.3 新增)\r\n//   {\r\n//     type: 'a',            // 可选值同上面字符串\r\n//     note: '备注信息',     // 关于 cookie 的一些说明，可省略\r\n//     belong: 'test.js',    // 该 cookie 的归属脚本（调用或写入该 cookie 的脚本），可省略\r\n//     pass: 'string',       // 加密存储内容时的密码(v3.6.6 新增)\r\n//     algo: 'ebuf',         // 加密存储内容时的算法(v3.6.6 新增)，可省略。默认为 ebuf 自定义算法\r\n//   }\r\n$store.set(key, value, options)   // 等同于将上面的 $store.put 交换 key 和 value 的位置。(v3.4.5 添加)\r\n\r\n$store.delete(key)                // 删除\r\n```\r\n\r\n$store 保存时会对数据类型进行简单的判断，当数据类型为 **number/boolean/array/object** 时，会按照数据类型进行保存。\r\n\r\n``` JS store 实例\r\n$store.put(123, 'number')       // put 方法，第一个参数为保存值 value, 第二个参数为关键字 key\r\ntypeof $store.get('number')     // get 方法通过关键字获取保存值，返回数字 123，类型为 number\r\n\r\n$store.get('number', 'raw')     // 返回 { \"type\": \"number\", \"value\": 123 }\r\n\r\n$store.set('newarr', [2,3,4])   // 存储为数组。保存成功返回 true，失败返回 false。 set 方法第一个参数为 key，第二个参数为 value\r\n$store.get('newarr')            // 返回数组: [2,3,4]\r\n\r\n$store.put(5, 'newarr', 'a')\r\n// 根据 newarr 原来保存值的类型，存储新的结果\r\n// 原 newarr 值为 array 数组，则新保存值为: [2,3,4,5]\r\n// 如果原 newarr 的值为数字，比如 8，则新的存储值 13\r\n$store.put([6,7], 'newarr', 'a')   // newarr 新值为：[2,3,4,5,6,7]\r\n\r\n$store.put('a string', 'keystr')   // 不指定类型时 直接保存为 string 的形式\r\n$store.put('add new line', 'keystr', 'a')    // 在原 keystr 的值后面添加新行\r\n$store.get('keystr', 'raw')\r\n// 返回：\r\n// {\"type\":\"string\",\"value\":\"a string\\nadd new line\"}\r\n\r\n$store.put({a: 334, b: 'abc'}, 'keyobj')     // 存储类型为 object。\r\n$store.get('keyobj')               // 返回: {\"a\":334,\"b\":\"abc\"}\r\n$store.get('keyobj', 'raw')                  // 返回: {\"type\":\"object\",\"value\":{\"a\":334,\"b\":\"abc\"}}\r\n$store.put({c: 'new val'}, 'keyobj', 'a')    // 新存储的值为: {\"a\":334,\"b\":\"abc\",\"c\": \"new val\"}\r\n\r\n$store.put('eoooe', 'keybol', 'boolean')     // 强制转化为 boolean 值: true\r\n// 建议在使用 $store.put 并指定 type 的时候，先确定原存储数值和即将保存数值的类型，以免发生未知错误。\r\n\r\n$store.delete('number')           // 删除某个 store/cookie 常量 number\r\n// 等同于 $store.put('', 'number')\r\n\r\n// 特殊情况\r\n$store.put('a string 字符', 'objstr', 'object')   // 强制将字符串保存为 object 格式。存储值为: {0: \"a string 字符\"}。 PS: array 是同样结果\r\n$store.get('keystr', 'array')     // 将字符串以 object 格式取出。结果为: {\"0\":\"a string\"}\r\n\r\n// v2.5.2 更新 $store.get type random。 type 关键字 random 或者 r\r\n$store.get('newarr', 'random')    // 返回 newarr 数组中的一个随机值\r\n$store.get('number', 'r')         // 返回 0 - number 代表值 中间的任一整数\r\n$store.get('keyobj', 'r')         // 返回 object 中的任一 keys 对应值\r\n$store.get('keybol', 'random')    // 返回 随机 true/false\r\n$store.get('keystr', 'r')         // 如果 keystr 存储的是字符串，取随机值时，取随机一行的数据\r\n\r\n// v3.3.3 更新: $store.put 第三个参数改为 options\r\n// 当 options 为字符串类型时表示为之前的普通 type 类型\r\n// 当 options 为 object 类型时，type/note/belong 关键字分别表示该 cookie 的 类型/备注/关联脚本\r\n$store.put('a string 字符', 'objstr', {\r\n  type: 'string',\r\n  note: '关于这个 cookie 一些备注说明',\r\n  belong: __name + ', store.js',            // 调用/写入这个 cookie 的脚本\r\n})\r\n\r\n// v3.6.6 更新 加密存储\r\n$store.put('待加密的内容', 'tstr', {\r\n  pass: '加密密钥 password',\r\n})\r\n// 使用对应密钥获取加密内容\r\n$store.get('tstr', {\r\n  pass: '加密密钥 password',\r\n})\r\n\r\n// 普通 get，获取到的数据是加密后的内容\r\n$store.get('tstr')\r\n\r\n$store.put({ a: '加密其他类型的数据，比如 object' }, 'tobj', {\r\n  type: 'object',\r\n  pass: $env.STORE_KEY || 'testkey',  // 可配合环境变量隐藏使用密钥\r\n})\r\n```\r\n\r\n### $cache - 临时数据存储 (v3.4.3)\r\n\r\n用于存储脚本运行时的一些临时数据，在 elecV2P 重启后会自动丢弃。\r\n所有脚本共享临时数据，比如在 a.js 中使用 $cache.hello = 'elecV2P', 可在 b.js 中使用 $cache.hello 来获取临时值。\r\n\r\n共有五个方法: get(key), put(value, key), delete(key), keys(), clear()\r\nv3.4.5 添加方法: set(key, value)  // 和 put 方法的 key/value 顺序相反\r\n\r\n``` JS\r\n$cache.v = 'hello elecV2P'       // 添加临时变量 v\r\n// 等同于\r\n$cache.put('hello elecV2P', 'v')   // put 方法 value 在前，key 在后\r\n$cache.set('vv', '你好，elecV2P')  // set 方法 key 在前，value 在后（v3.4.5 添加）\r\n\r\nlet val = $cache.get('v')        // 获取临时变量 v 中的内容\r\n// 等同于 let val = $cache.v\r\n\r\n$cache.delete('v')               // 删除临时变量 v\r\nconsole.log($cache.v, $cache.vv)            // 如果临时变量不存在，将会返回 undefined\r\n\r\n$cache.obj = {                   // 临时变量存储对象可以是任意值。包括 object 和 函数等\r\n  num: 1234,\r\n  s(){\r\n    console.log('$cache function', this.num++)\r\n  }\r\n}\r\n$cache.obj.s()\r\nconsole.log($cache.obj.num)\r\n\r\nlet keys = $cache.keys()         // 以数组的形式返回当前所有临时变量关键字。\r\n// [ 'obj' ]\r\n\r\n$cache.clear()                   // 清空存储的临时变量\r\nconsole.log(keys, $cache.keys())\r\n\r\nconsole.log($cache.size)         // 当前临时变量个数。（v3.4.5 添加）\r\n```\r\n\r\n**$cache** 和 **$store** 的区别:\r\n- $cache 数据在内存中读写，$store 数据在硬盘上读写（因此，$cache 存取速度更快，但会占用一些内存，不建议保存大量数据）\r\n- $cache 是临时存储，在 elecV2P 重启后所有数据会丢失。$store 是常量存储，重启后已存储数据依然存在\r\n- $store 只能通过 get/put 方法来存取数据，而 $cache 可以直接通过点引用来存取。（比如: $cache.haha = '哈哈哈哈哈哈哈'）\r\n- $store 没有 keys()/clear()/size 的方法/属性，也不能保存函数类'动态'数据\r\n\r\n### $env - 临时环境变量 (v3.4.5)\r\n\r\n``` JS\r\nconsole.log($env)\r\nconsole.log('version', $env.version)\r\n\r\nlet key = 'name'\r\nif ($env[key]) {\r\n  console.log($env[key])\r\n} else {\r\n  console.log('当前环境变量中暂无', key, '相关值')\r\n}\r\n```\r\n\r\n- 临时环境变量默认包含 process.env 中的所有值，不管当前是否以 nodejs 兼容模式运行\r\n- 临时环境变量仅在当前脚本中有效。比如在 a.js 中设置 $env.version = '1.0.0', 在 b.js 中 $env.version 还是等于 process.env.version\r\n- process.env 的变化会反应到 $env 中，但 $env 的变化不会影响 process.env 的值\r\n- 在使用定时任务或 webhook 等方式运行脚本时，可使用 -env 添加临时环境变量（具体参考 [06-task.md](https://github.com/elecV2/elecV2P-dei/blob/master/docs/06-task.md) 运行 JS 相关部分）\r\n- v3.7.6 增加 $env.lang 获取当前设置的语言偏好，比如 en | zh-CN\r\n\r\n### $feed - 通知模块\r\n\r\n发送一条通知\r\n- $feed.push(tile, description, url)\r\n  - title: 通知标题。 如省略，则会使用 'elecV2P 通知' 替代\r\n  - description: 通知内容。 如省略，则会显示 'a empty message\\n没有任何通知内容'\r\n  - url: 点击通知后的跳转链接。可省略\r\n\r\n``` JS example\r\n// PUSH 通知包括 RSS 和其他手动设置好的通知方式\r\n$feed.push('elecV2P notification', '这是一条来自 elecV2P 的通知', 'https://github.com/elecV2/elecV2P')\r\n\r\n// 单独发送一条 ifttt 通知\r\n$feed.ifttt('elecV2P 通知', '一条来自 $feed.ifttt 的通知', 'https://github.com/elecV2/elecV2P-dei')\r\n\r\n// 单独发送一条 bark 通知\r\n$feed.bark('elecV2P 通知', '一条来自 $feed.bark 的通知', 'https://t.me/elecV2')\r\n\r\n// 在 title 开头添加 $enable$ 强制发送通知\r\n$feed.cust('$enable$自定义通知', '使用 $enable$ 强制发送的一条通知', 'https://github.com/elecV2/elecV2P-dei/tree/master/docs/07-feed&notify.md')\r\n```\r\n\r\n- ifttt/bark 等通知需提前在 webUI->SETTING 页面设置好 TOKEN/KEY\r\n- 如果 SETTING 相关通知为关闭状态，则调用了也不会有通知\r\n\r\n更多相关说明参考: [07-feed&notify](https://github.com/elecV2/elecV2P-dei/tree/master/docs/07-feed&notify.md)\r\n\r\n### $exec - Shell 指令执行函数\r\n\r\n*Shell 指令的运行基于 nodejs 的 [child_process_exec](https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback) 模块*\r\n\r\n#### 基础使用: $exec(command, options = { cwd, env, timeout, call, cb, stdin })\r\n\r\n*options 可省略，opitons 中的每一项参数也都可省略*\r\n\r\n- cwd: (string) 工作目录\r\n  - 当没有设置或设置目录不存在时，如果是 node 命令开头，默认为: script/JSFile，其他情况默认为: script/Shell\r\n  - v3.4.2 之前默认工作目录为: process.cwd()\r\n- env: (object) 环境变量 (默认为: process.env)\r\n- timeout: (number)  超时时间。单位: 毫秒(ms)，0: 表示不设定超时时间。默认为 60000ms(60秒)\r\n- call: (boolean) 是否在命令执行完成后返回所有输出内容\r\n- cb(data, error, finish):  (function)  回调函数\r\n  - data: stdout.on('data')   命令执行时的输出内容\r\n  - error: stderr.on('data')  命令执行出错时的输出内容\r\n  - finish: exec.on('exit')   命令执行完毕信号，最终返回 true\r\n- stdin: (object) { write, delay } 延时输入交互数据(v3.2.6 增加)\r\n  - write: (string) 延时写入的数据\r\n  - delay: (number) 延时时间。单位: 毫秒(ms)，可省略(默认为 2000)\r\n\r\n``` JS example\r\n$exec('ls', {\r\n  cwd: './efss',      // 命令执行目录\r\n  timeout: 5000,\r\n  cb(data, error){\r\n    error ? console.error(error) : console.log(data)\r\n  }\r\n})\r\n\r\n// 如果省略 options 中的所有参数，那么对应输出只能在后台看到\r\n$exec('node -v')\r\n\r\n// 在 Docker 环境在安装 python3，并执行其他 python 文件\r\n$exec('apk add python3', {\r\n  timeout: 0,\r\n  cb(data, error, finish){\r\n    if (finish) {\r\n      // 安装完以后可以直接在 JS 中调用。（pyhton 和库安装完成后可在其他脚本中直接调用，不需要再次安装。）\r\n      $exec('python3 -u test.py', {\r\n        cwd: './script/Shell',      // test.py 文件放置的目录。可修改为其他目录，比如 './efss'\r\n        cb(data, error){\r\n          error ? console.error(error) : console.log(data)\r\n        }\r\n      })\r\n\r\n      // 安装一些 python 库\r\n      $exec('pip3 install you-get youtube-dl numpy requests')\r\n    } else {\r\n      error ? console.error(error) : console.log(data)\r\n    }\r\n  }\r\n})\r\n\r\n// stdin 延迟输入交互内容 简单示例\r\n$exec('python3 -u askinput.py', {\r\n  cwd: './script/Shell',\r\n  stdin: {\r\n    delay: 3000,   // 输入延时时间，单位 ms。可省略\r\n    write: 'elecV2P\\nI am fine, thank you.'     // 具体输入数据。（根据实际情况进行修改）\r\n  },\r\n  cb(data, error){\r\n    if (error) {\r\n      console.error(error)\r\n    } else {\r\n      console.log(data)\r\n    }\r\n  }\r\n})\r\n\r\n/* askinput.py 内容\r\nname = input(\"what is your name?\") \r\nprint('nice to meet you,', name)\r\ngreet = input(f\"how are you, {name}?\")\r\nprint(greet)\r\n */\r\n\r\n// 特别使用: 给当前执行命令设置单独日志文件\r\n$exec('ls', {\r\n  cwd: './efss',           // 命令执行目录\r\n  logname: 'ls执行日志',   // 命令执行日志保存文件名，可自定义为其他值\r\n  from: 'task',            // v3.4.5 之前此项必需，且只能为 'task'。v3.4.5 之后可省略\r\n  // 当 from 设置为 task 时，实时日志可在 TASK 定时任务界面查看\r\n  // cb 函数可省略。日志记录保存在 logname 设置的文件中\r\n})\r\n```\r\n\r\n*如果命令不可执行，尝试先在系统命令行工具下手动输入命令，进行测试*\r\n\r\n**如果 windows 平台出现乱码，尝试命令 *CHCP 65001*。或者修改注册表 Active code page 为 65001（具体操作，善用搜索）** （*尝试过 iconv 转换(< v1.8.2)，弃用*）\r\n\r\n#### **v3.2.8 增加支持运行远程文件**\r\n\r\n``` JS\r\n// 例如:\r\n$exec('python3 -u https://raw.githubusercontent.com/elecV2/elecV2P/master/script/Shell/test.py', {\r\n  cwd: './script/Shell',      // 命令执行目录\r\n  timeout: 5000,\r\n  cb(data, error){\r\n    error ? console.error(error) : console.log(data)\r\n  }\r\n})\r\n/* 说明:\r\n- 当执行命令(command)中包含远程链接时，会自动下载远程文件到 cwd 目录，并将该远程链接替换为下载后的文件地址\r\n  - 比如，上面的代码会先下载远程文件 test.py 到 script/Shell 目录，然后命令自动转化为 python3 -u /xxx/script/Shell/test.py\r\n- 远程链接匹配方式: command.match(/ (https?:\\/\\/\\S{4,})/)\r\n- 远程文件执行时默认每次都会重新下载\r\n  - 可使用 options.rename: xxx 来重命名下载文件\r\n  - 可使用 options.local: true 来跳过下载远程文件，直接以本地文件运行（如果存在的话，如果不存在还是会远程下载\r\n- 如果远程文件下载失败，将会尝试运行本地文件\r\n- 如果不希望对原命令中的远程链接进行下载及替换操作，可使用 options.nohttp: true, 或使用 -http 进行转义\r\n  - 注意: 命令中的所有 ' -http' 字符(不含引号)都会被替换为 ' http'\r\n- 以下常用命令已排除远程下载及替换操作:\r\n  - curl/wget/git/start  (v3.2.9)\r\n  - you-get/youtube-dl   (v3.3.0)\r\n  - aria2c/http/npm/yarn/ping/openssl/telnet/nc/echo    (v3.4.2)\r\n  - *如果还有其他的常用网络相关命令，欢迎反馈添加*\r\n**/\r\n\r\n// 以 git 等其他常用命令开头，远程链接不会进行自动转换，按原命令处理\r\n$exec('git clone https://github.com/elecV2/elecV2P', {\r\n  cwd: 'script/JSFile',\r\n  cb(data, error, finish){\r\n    error ? console.error(error) : console.log(data)\r\n    if (finish) {\r\n      console.log('git clone 完成')\r\n    }\r\n  }\r\n})\r\n\r\n// 其他命令中如果包含远程链接，将会自动下载，并将命令中的远程链接替换为文件下载后的地址\r\n$exec('node https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/webhook.js', {\r\n  // 将自动下载 webhook.js 到 cwd 目录('script/JSFile')，并将远程链接替换为文件下载后的地址 node /xxx/script/JSFile/webhook.js\r\n  cwd: 'script/JSFile',\r\n  local: true,     // true: 当 cwd 目录下存在 webhook.js 时，直接使用本地文件运行，不进行远程下载\r\n  // rename: 'testremote.js',  // 重命名远程下载文件\r\n  cb(data, error){\r\n    error ? console.error(error) : console.log(data)\r\n  }\r\n})\r\n\r\n$exec('echo -http://127.0.0.1')   // 输出结果为: http://127.0.0.1\r\n// 等同于\r\n$exec('echo http://127.0.0.1')    // echo 命令不会下载远程文件及替换\r\n\r\n// 使用 cat 命令下载并查看远程文件\r\n$exec('cat http://127.0.0.1/efss/readme.md', {\r\n  // 如没有指定 cwd，或指定 cwd 目录不存在时，则使用默认 cwd: script/Shell\r\n  // nohttp: true,   // 启用此项，表示不转化 http 远程链接，将会搜索文件 'http://127.0.0.1/efss/readme.md'，然后直接报错\r\n  // local: true,    // 启用此项，表示如果 cwd 目录中存在 readme.md 文件，则不下载(但远程链接会替换为本地文件地址)\r\n  cb(data, error){\r\n    error ? console.error(error) : console.log(data)\r\n  }\r\n})\r\n```\r\n\r\n### $cheerio - HTML处理\r\n\r\n用于对 html 的处理\r\n\r\n``` JS example\r\n// example #1\r\nlet body = $response.body\r\nlet restype = $response.headers['Content-Type']\r\n\r\nif (/html/.test(restype)) {\r\n  const $ = $cheerio.load(body)\r\n  $('body').text('hello cheerio')\r\n  body = $.html()\r\n  console.log(body)\r\n}\r\n\r\n$done(body)\r\n\r\n// example #2\r\nconst $ = $cheerio.load(`<ul id=\"fruits\">\r\n  <li class=\"apple\">Apple</li>\r\n  <li class=\"orange\">Orange</li>\r\n  <li class=\"pear\">Pear</li>\r\n</ul>`);\r\n\r\nconst apple = $('.apple', '#fruits').text()\r\nconsole.log(apple)\r\n\r\nconst attr = $('ul .pear').attr('class');\r\nconsole.log(attr)\r\n\r\nconst html = $('#fruits').html();\r\nconsole.log(html)\r\n\r\n$done($('.pear').text())\r\n```\r\n\r\n更多使用方法参考：[cheerio](https://github.com/cheeriojs/cheerio) 官方说明文档\r\n\r\n### $download - 文件下载\r\n\r\n用于直链文件下载，可指定下载目录。如不指定下载目录则保存到默认目录。\r\n默认保存目录为 efss 虚拟目录，如果 efss 目录为空则保存到 web/dist 目录。\r\n\r\n基础用法：\r\n$download(url, options).then(d=>console.log(d)).catch(e=>console.error(e))\r\n\r\n**options** 变量说明：\r\n- 直接省略，表示使用默认目录保存文件\r\n- 字符类型，分两种情况\r\n  - 字符表示的是一个已存在的文件夹，则下载到该文件夹，并以 url 的结尾命名文件\r\n  - 否则表示的是 **目录(如有)+文件名**\r\n- 对象类型，可接受四个参数: { folder, name, existskip, timeout }\r\n  - folder 表示下载目录\r\n  - name 表示文件名(其中也可包含目录)\r\n  - existskip 当目标文件已存在时不下载(v3.4.9 添加)\r\n  - timeout 本次下载的超时时间，单位 ms(v3.5.0 添加)\r\n\r\n下面以几个具体实例进行说明：\r\n\r\n``` JS example\r\n$download('https://raw.githubusercontent.com/elecV2/elecV2P/master/Todo.md').then(d=>console.log(d)).catch(e=>console.error(e))\r\n\r\n// 指定下载目录及文件名\r\n$download('https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/examples/Shell/exam-request.py', './script/Shell/myreq.py').then(d=>console.log(d)).catch(e=>console.error(e))\r\n// 前面一部分 script/Shell 表示保存目录，后面的 myreq.py 表示文件名\r\n// 如果仅有 myreq.py 的话，文件会以该名字保存到默认目录\r\n\r\n// 以 object 方式指定下载目录\r\n$download('https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/examples/JSTEST/boxjs.ev.js', {\r\n  folder: './script/JSFile',\r\n  name: 'box.js',\r\n  existskip: true,\r\n}).then(d=>console.log('文件已下载至: ' + d)).catch(e=>console.error(e))\r\n// 假如将 name 修改为 'test/box.js', 此时文件名中同时包含目录，则会在 script/JSFile 下新建目录 test，然后将文件(box.js)保存到 test 中\r\n```\r\n\r\n#### 特别使用：获取下载进度\r\n\r\n$download(url, options, cb) - 前两项参数不变，增加第三个参数为 callback 函数，callback 函数在下载期间会被多次调用。\r\n\r\ncb(options = {})，传入的 options 参数有如下值：\r\n\r\n- name<string>: 下载的文件名称\r\n- progress<string>: 下载的进度条\r\n- chunk<number>: 第 n 个下载块（仅在下载中存在\r\n- dsize<number>: 文件已下载大小（downloaded size （v3.7.2 增加\r\n- total<number>: 文件总大小（对应为 response.headers['content-length'] 项，不存在时为 NaN（v3.7.2 增加\r\n- start<string>: 开始下载（仅开始下载时存在（对应内容为下载文件的完整保存路径（v3.7.2\r\n- finish<string>: 下载完成后的消息（仅在下载完成后存在（对应内容为下载后文件的完整路径（v3.7.2\r\n\r\n*start 和 finish 内容一样，都表示文件下载到 elecV2P 服务器的绝对路径，只是存在阶段不一样*\r\n\r\n示例:\r\n\r\n``` JS\r\n$download('https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/examples/Shell/elecV2P.py', {\r\n  folder: 'script/Shell',\r\n  name: 'elecV2P.org.py'\r\n}, (options)=>{\r\n  // callback 函数，支持异步函数\r\n  if (options.start) {\r\n    // 下载开始时存在的参数 opitons.name/progress/chunk/dsize/total/start\r\n    // 开始阶段 chunk<=>dsize<=>0\r\n    console.log(`开始下载文件 ${options.name} 到 ${options.start}, 文件大小 ${kSize(options.total)}`)\r\n  } else if (options.finish) {\r\n    // 下载完成时存在的参数 opitons.name/progress/dsize/total/finish\r\n    // 完成阶段 dsize<=>total<=>response.headers['content-length'] || NaN\r\n    $message.success(`${options.name} 下载完成 ${options.total ? '总大小 ' + kSize(options.total) : ''}`)\r\n    console.log(options.name, '已下载至', options.finish)\r\n  } else {\r\n    // 下载中存在的参数 opitons.name/progress/chunk/dsize/total\r\n    options.chunk % 100 === 0 && $message.success(`正在下载 ${options.name} 第 ${options.chunk} 块数据 ${kSize(options.dsize)}/${kSize(options.total)}`, { mid: 'download' })\r\n    // console.log(options.progress + '\\r')    // 显示下载进度条。（后面的 '/r' 用于在前端页面删除上一条 log 日志）\r\n    console.log(options.progress, '\\x1b[F')    // v3.7.2 后推荐使用该条代码\r\n  }\r\n}).then(d=>console.log(d)).catch(e=>console.error(e))\r\n\r\nfunction kSize(size, k = 1024) {\r\n  if (size < k) {\r\n    return size + ' B'\r\n  }\r\n  if (size < k*k) {\r\n    return (size/k).toFixed(2) + ' K'\r\n  }\r\n  if (size < k*k*k) {\r\n    return (size/(k*k)).toFixed(2) + ' M'\r\n  }\r\n  return (size/(k*k*k)).toFixed(2) + ' G'\r\n}\r\n```\r\n\r\n### $evui - 生成一个 UI 界面\r\n\r\n*$evui 的参数传递基于 websocket*\r\n\r\n可接收两个参数：$evui(option, callback)\r\n- option: UI 界面相关参数\r\n- callback: 用于接收处理 UI 界面提交返回的数据。（可省略）\r\n\r\n$evui 返回的是一个 Promise 函数\r\n- resolve 条件: 当 cbable 为 true 时，直到前端关闭窗口。否则，直接 resolve\r\n- reject 条件: 传递参数有误或者 websocket 尚未连接\r\n\r\n``` JS\r\nconst ui = {\r\n  id: 'ebcaa4ff',      // 给图形界面一个独一无二的 ID。可省略（以下所有参数都可省略，不再重复说明）\r\n  title: 'elecV2P windows',          // 窗口标题\r\n  width: 800,          // 窗口宽度\r\n  height: 600,         // 窗口高度。null 表示自适应高度\r\n  content: `<p>显示一张图片</p><img src='https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/overview.png'>`,            // 图形界面显示内容\r\n  style: {             // 设置一些基础样式\r\n    title: \"background: #6B8E23;\",   // 设置标题样式\r\n    content: \"background: #FF8033; font-size: 32px; text-align: center\",  // 设置中间主体内容样式\r\n    cbdata: \"height: 220px;\",        // 设置返回数据输入框样式\r\n    cbbtn: \"width: 220px;\"           // 设置提交数据按钮的样式\r\n  },\r\n  resizable: true,     // 窗口是否可以缩放\r\n  draggable: true,     // 窗口是否可以拖动\r\n  cbable: true,        // 是否启用 callback 函数，用于接收前端 UI 提交返回的数据\r\n  cb(data){            // callback 函数。此项会被 $evui 的第二个参数覆盖(如有)\r\n    console.log('data from client:', data)\r\n  },\r\n  cbdata: 'hello',     // 提供给前端 UI 界面的初始数据\r\n  cblabel: '提交数据', // 提交按钮显示文字\r\n  script: `console.log('hello $evui');alert('hi, elecV2P')`,     // v3.2.4 增加支持在前端网页中插入 javascript 代码\r\n}\r\n\r\n$evui(ui, data=>{\r\n  // 此为 callback 函数，用于接收处理前端 UI 返回的数据，可省略。如有则会覆盖前一项参数中的 cb 变量(ui.cb)\r\n  if (data == 1) {\r\n    $feed.push('Get a infomation frome $evui', 'message:' + data)\r\n  } else if (/^exec /.test(data)) {\r\n    let command = data.split('exec ').pop()\r\n    $exec(command, {\r\n      cb(data, error){\r\n        console.log(error || data)\r\n      }\r\n    })\r\n  } else {\r\n    console.log('data from client:', data)\r\n  }\r\n}).then(data=>console.log(data)).catch(e=>console.error(e))\r\n\r\n// 发送关闭前端 evui 界面的指令 (v3.4.0 增加)\r\n$ws.send({ type: 'evui', data: { id: 'ebcaa4ff', type: 'close' }})\r\n\r\n// * $ws.send 函数可临时用于服务器通过 websocket 向前端发送数据\r\n```\r\n\r\n效果：\r\n\r\n![](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/evuitest.png)\r\n\r\n### $message - 给前端网页发送一条消息\r\n\r\n*消息传递基于 websocket*\r\n\r\n共有三种方法:\r\n- success  成功\r\n- error    错误\r\n- loading  加载中\r\n\r\nv3.4.2 增加方法\r\n- close    用于关掉网页消息体\r\n\r\n``` JS\r\n// 基础使用\r\n$message.success('一条来自脚本的消息')\r\n// $message 可接受任意个参数(v3.4.0)\r\n// 当最后一个参数为数字时，表示消息显示时间，单位：秒。\r\n// 最后参数为 object 且包含 secd/url/mid/align 等参数时:\r\n// - secd 消息显示时间，单位: 秒（默认: 消息长度/5 + 已有消息数*3 秒）。 0: 一直显示不关闭\r\n// - url  点击消息后跳转 url（当为 reload 或 refresh 时表示点击强制刷新当前网页 v3.5.2）\r\n// - mid  消息体的 id (v3.4.2 增加)\r\n// - align  消息文字对齐方式（v3.4.4 增加）。默认: center，可设置 left: 左对齐，right: 右对齐。其他值使用默认 center\r\n\r\n$message.error('some wrong is happen', 10)\r\n// 一条错误提醒消息，10 秒后自动关闭\r\n// 第二个参数如果是 0: 表示消息不自动关闭\r\n\r\n$message.loading('等待中...', 0)\r\n\r\n// 最后一个参数为 object 且传递 secd 和 url\r\n$message.success(23, '参数类型和数量不限', true, { hello: 'elecV2P' }, '点击消息\\n可打开 elecV2P Github 主页', { secd: 0, url: \"https://github.com/elecV2/elecV2P\" })\r\n\r\n// v3.4.2 增加关掉前端网页已弹出消息体的功能\r\n$message.loading('等待关闭中...\\n请点击消息右侧进行关闭', {\r\n  secd: 0,       // 0: 不主动关闭\r\n  mid: 'auid',   // mid 对应值随意填写\r\n  align: 'left', // 文字左对齐\r\n})\r\n\r\n$message.close('auid')  // 关掉 mid === 'auid' 的消息体\r\n$message.close() // 关掉所有已弹出的消息体\r\n```\r\n\r\n### $ws - 向前端发送临时数据\r\n\r\n一般用于需要异步或持续向前端发送数据的特殊情况。\r\n\r\n- $ws.send({ type, data })  通过 websocket 向前端发送数据\r\n- $ws.sse(sseid, data)  通过 server-sent events 向前端发送数据(v3.7.2 添加)\r\n\r\n$ws.send 可使用的 type 类型已在前端定义，以后会整理开放。该方法仅在前端 webUI websocket 已连接的情况有效。通常用于 evui 界面通信中，参考脚本：https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/examples/JSTEST/evui-chatroom.js\r\n\r\n$ws.sse 需要先在前端生成 new EventSource('/sse/elecV2P/' + sseid) 事件，然后可以在事件上自定义数据接收后的处理函数。通常用于 efh 脚本中，参考脚本：https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/examples/JSTEST/efh/kuwo-music.efh （相关资料：[Using server-sent events - MDN](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)）\r\n\r\n**这两个函数发送的消息/数据，会同步发送到所有已连接的客户端**\r\n\r\n### $request/$response\r\n\r\n$request: 存在于通过 RULES/REWRITE/favend 触发的脚本中\r\n$response: 存在于通过 RULES/REWRITE 触发，并且修改时间段为数据返回前的脚本中\r\n\r\n*有 $request 不一定有 $response, 但有 $response 一定有 $request*\r\n\r\n- $request.url<string> : 网络请求完整 URL\r\n- $request.headers<object> : 不区分大小写。比如 headers.Host === headers.host ==== headers.HOST\r\n- $request.body<string|buffer> : 通常为 string 类型，buffer 类型的条件见下面说明\r\n- $request.bodyBytes<buffer> : 原始 buffer 数据。 favend 模式下该参数不存在\r\n- $request.method<string> : GET|POST|PUT|DELETE 等值，大写\r\n- $request.protocol<string> : http|https 等值，小写\r\n- $request.hostname<string> : 请求域名/IP，比如 127.0.0.1\r\n- $request.port<number> : 请求端口，比如 80|443|8000 等\r\n- $request.path<string> : 请求路径\r\n- $request.pathname<string> : 请求路径，同上\r\n\r\n- $response.status<number> : 网络请求返回状态码。同下\r\n- $response.statusCode<number> : 网络请求返回状态码，比如 200|301|404 等\r\n- $response.headers<object> : 不区分大小写。比如 headers['Content-Type'] === headers['content-type']\r\n- $response.body<string|buffer> : 通常为 string 类型，buffer 类型的条件见下面说明\r\n- $response.bodyBytes<buffer> : 原始 buffer 数据\r\n\r\n**当满足 /^(audio|video|image|multipart|font|model)|(ogg|stream|protobuf)$/.test(headers['Content-Type']) 条件时，$request.body/$response.body 对应类型为 buffer（v3.7.5 增加）**\r\n**$reponse 是服务器返回给 MITM 代理的数据，最终返回给客户端的数据以 $done 结果为准**\r\n\r\n``` JS example\r\n// 可使用的相关参数\r\n// $request.url<string>, $request.headers<object>, $request.body<string|buffer>, $request.bodyBytes<buffer>\r\n// $request.method<string>, $request.protocol<string>, $request.hostname<string>, $request.port<number>, $request.path<string>\r\n// $response.status<number>, $response.statusCode<number>, $response.headers<object>, $response.body<string>, $response.bodyBytes<buffer>\r\n\r\nconsole.log('$request', $request)\r\n\r\nlet body = $response.body\r\n// let obj = JSON.parse(body)\r\nif (/httpbin/.test($request.url)) {\r\n  body += 'change by elecV2P'\r\n}\r\n$done({ body })\r\n```\r\n\r\n- v3.4.8 添加 **$request.bodyBytes/$response.bodyBytes**，对应数据类型为 **Buffer**\r\n- 更多相关说明，可参考脚本 https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/0body.js\r\n\r\n### $task 定时任务管理(v3.4.4)\r\n\r\n该功能仅在开启 sudo 模式的脚本中有效。（sudo 模式开启方式: 在脚本中添加 **// @grant  sudo**）\r\n\r\n$task 拥有的方法/函数:\r\n\r\n- add(taskinfo<object>[, options])  ;添加定时任务\r\n- start(taskid<string|array>)   ;开始某个/些定时任务\r\n- stop(taskid<string|array>)    ;停止某个/些定时任务\r\n- delete(taskid<string|array>)  ;删除某个/些定时任务\r\n- info(taskid<string>)   ;获取某个任务的信息。当省略 taskid 时，返回所有任务信息\r\n- nameList()  ;获取任务名及对应 taskid 列表<object>\r\n- status()    ;返回当前任务数。总任务/运行中的任务/订阅任务数\r\n- save()      ;保存当前任务列表。（用于在 elecV2P 重启后自动恢复）\r\n\r\n具体使用\r\n\r\n``` JS\r\n// @grant  sudo\r\n\r\n// 查看当前任务数\r\nconsole.log($task.status())\r\n\r\n// 添加任务（具体任务格式参考 https://github.com/elecV2/elecV2P-dei/blob/master/docs/06-task.md）\r\nlet res = $task.add({\r\n  name: '$task 添加的任务',\r\n  type: 'cron',\r\n  time: '12 15 18 * * *',\r\n  job: {\r\n    type: 'exec',\r\n    target: 'pm2 ls'\r\n  }\r\n})\r\nconsole.log('$task 添加任务结果', res)\r\n// 如果需要添加多个任务，可使用 array 数组的形式\r\n// 比如 $task.add([{}, {}], { type: 'replace' })\r\n// 第二个参数 options 可省略。\r\n// 如设置 type, 表示同名任务的更新方式。有三个有效值:\r\n// - replace    替换原同名任务\r\n// - addition   新增同名任务\r\n// - skip       跳过添加同名任务\r\n\r\n// 获取任务名及对应 taskid\r\nlet tnlist = $task.nameList()\r\nconsole.log(tnlist)\r\n// 返回的是类似于 { '任务名': taskid } 的 object\r\n// 通过该 object，可使用任务名快速查找任务 id\r\n// 如果任务列表不经常变化的话建议使用 $store.put 或 $cache 保存\r\n\r\n// 开始任务\r\nconsole.log($task.start('m8LWPxDc'))\r\n\r\n// 停止任务\r\nconsole.log($task.stop(tnlist['$task 添加的任务']))   // 通过任务名查找任务 id\r\n\r\n// 删除任务\r\nconsole.log($task.delete('mkl7pwQn'))\r\n\r\n// 使用数组的形式传入 taskid，可批量开始/暂停/删除 定时任务\r\n$task.start(['m8LWPxDc', 'ataskid', tnlist['$task 添加的任务'], 'jxwQOSJZ'])\r\n// $task.stop(['taskid1', 'taskid2', ...])\r\n// $task.delete(['taskid1', 'taskid2', ...])\r\n\r\n// 查看某个任务信息\r\nlet taskinfo = $task.info('m8LWPxDc')   // 查看所有任务信息 $task.info() 或者 $task.info('all')\r\nconsole.log(taskinfo)\r\n\r\n// 查询 __taskid/__taskname （仅在使用定时任务运行脚本时有值，其他情况默认为 undefined\r\nconsole.log('执行该脚本的任务名:', __taskname)\r\nconsole.log('相关任务信息', $task.info(__taskid))\r\n\r\n// 尝试使用 __taskid 来停止自身定时任务\r\nif (__taskid) {\r\n  let stopinfo = $task.stop(__taskid)   // 停止自身定时任务\r\n  console.log(stopinfo)\r\n}\r\n\r\n// 保存当前任务列表\r\nlet saveres = $task.save()\r\nconsole.log(saveres)\r\n```\r\n\r\n### $webhook 调用 webhook 接口(v3.5.8)\r\n\r\n*$webhook 的本质是一个对 localhost/webhook 发起的 POST 网络请求*\r\n\r\n使用前提：\r\n\r\n- 脚本中开启 sudo 模式\r\n- 127.0.0.1 IP 没有被限制\r\n\r\n基础格式：$webhook(type<string|object>, options<object>)\r\n\r\n- 当 type 为字符串时，表示执行 webhook 类型（具体参考 https://github.com/elecV2/elecV2P-dei/blob/master/docs/09-webhook.md ）\r\n- 当 type 为 object 时，表示 POST 请求的 body（无需 token）\r\n- options 可省略，当为 object 时表示附带参数\r\n- 最终 POST 的 body 为 Object.assign({ type }, options)\r\n- 为避免循环调用，通过 webhook 调用的脚本 $webhook type runjs 将不可用\r\n\r\n使用示例：\r\n\r\n``` JS\r\n// @grant sudo\r\n$webhook('status').then(res=>console.log(res.data));\r\n// 完全等同于\r\n$webhook({ type: 'status' }).then(res=>console.log(res.data));\r\n\r\n// 附带 options\r\n$webhook('info', {debug: true})         //  $webhook('runjs', { fn: 'test.js' })\r\n.then(res=>console.log(res.data))\r\n.catch(e=>console.error(e.message));\r\n\r\n// options 中的 type 会覆盖前面的 type\r\n$webhook({ type: 'runjs' }, { type: 'shell', command: 'ls' })\r\n.then(res=>console.log(res.data))\r\n.catch(e=>console.error(e.message));\r\n// 以上等同于\r\n$webhook({ type: 'shell', command: 'ls' }).then(res=>console.log(res.data)).catch(e=>console.error(e.message));\r\n\r\n// 当该脚本从 webhook 触发，且 type 为 runjs 时，会报错\r\n// 比如在 test.js 中使用 $webhook('runjs', {fn: 'test.js'}) 再次调用 test.js\r\n// 仅首次调用有效，之后的调用将不再继续执行\r\n$webhook({ type: 'runjs', fn: 'test.js' })\r\n.then(res=>console.log(res.data))\r\n.catch(e=>console.error(e.message));\r\n\r\n// 其他 $webhook 可执行类型及对应参数，请参考：https://github.com/elecV2/elecV2P-dei/blob/master/docs/09-webhook.md\r\n```\r\n\r\n## require 其他 nodejs module\r\n\r\n``` JS example\r\n// require 公共模块\r\nconst path = require('path')\r\n\r\n// require 相对目录 js\r\nconst rob = require('./requireob')\r\nconst ob2 = require('./requireob2.js')\r\n\r\nconsole.log(ob2, path.join(__dirname))\r\nrob('hello elecV2P')\r\n\r\n// #2 引用 node_modules 目录下的模块\r\nconst axios = require('axios')\r\naxios.get('https://github.com/elecV2/elecV2P').then(res=>console.log(res.data))\r\n\r\n// 删除 require 引用缓存 (v3.4.4)\r\nlet id = require.resolve('./requireob2.js')  // 获取缓存 id\r\ndelete require.cache[id]   // 删除对应 id module 的缓存\r\n\r\n// 或者直接使用 require.clear\r\nrequire.clear('./requireob2')   // 等同于 require.clear('./requireob2.js')\r\n// 注意: require.clear 并不是 nodejs 中的标准函数，目前仅适用于 elecV2P\r\n\r\nlet rob2 = require('./requireob2.js')   // 再次引用时将会重新加载\r\n```\r\n\r\n- *require 函数在兼容其他软件脚本模式下默认不启用，可在文件开头使用 **// @grant   require** 强制开启*\r\n- *require 有缓存，刚修改的脚本在其他模块引用时可能不会更新。可使用 require.clear 函数来清空相应缓存*\r\n- *require 仅可引用 nodejs 原生脚本，如脚本中包含 elecV2P 等其他环境变量时（比如上面的 $store/$feed/$exec 等等）无法解析*\r\n\r\n## $done - 返回 JS 执行结果\r\n\r\n优先级: $done > JS 最后一条语句结果\r\n\r\n``` JS example\r\nlet elecV2P = 'customize personal network'\r\nelecV2P\r\n// 返回字符串 'customize personal network'\r\n// 即：当没有使用 $done 函数时，直接返回最后一条语句的执行结果\r\n\r\nlet elecV2P = 'customize personal network'\r\n$done(123)\r\nconsole.log(elecV2P)\r\nelecV2P\r\n// 返回结果 123，并且后面的 console.log 函数会正常执行\r\n\r\nlet elecV2P = 'customize personal network'\r\n$done({ response: elecV2P })\r\n// 返回对象 {response: 'customize personal network'}\r\n\r\nlet elecV2P = 'customize personal network'\r\nlet promise = Promise.resolve(elecV2P)\r\nconsole.log(typeof promise)\r\npromise\r\n// 如果返回的是一个 Promise 函数，最后结果是 resolve 的值\r\n// 即: customize personal network\r\n```\r\n\r\n- JS 默认 timeout 为 5000ms (5 秒)，该值可在 webUI->SETTING 界面修改。0 表示无限制，没有超时时间\r\n- JS 测试运行 和 webhook 运行 timeout 固定为 5000ms (以防长时间无返回结果而出现的错误)，超时后后面的代码会继续执行(v3.4.0)\r\n- 如果最后结果是异步函数，超时后代码会继续执行，如果是同步函数，会返回一个超时的错误信息\r\n\r\n## $fend - efh 文件前后端数据交互(v3.5.5)\r\n\r\n$fend 函数目前仅适用于 efh 文件。基本用法： $fend(key, data);\r\n（关于 efh 文件的说明, 参考 https://github.com/elecV2/elecV2P-dei/blob/master/docs/08-logger&efss.md 中的相关部分）\r\n\r\n``` JS\r\n// efh 文件前端 script 部分\r\n// 默认 timeout 为 5000ms，可在传输数据中使用 timeout 参数修改(v3.7.2)\r\n// 比如 $fend('akey', {timeout: 1000, hello: 'other data'}).then(...)\r\n$fend('skey', '传输给后台的数据')\r\n.then(res=>res.text())\r\n.then(alert)\r\n.catch(e=>{\r\n  console.error(e);\r\n  alert(e.message);\r\n});\r\n\r\n// efh 文件后台 script 部分\r\n$fend('skey', {\r\n  statusCode: 200,\r\n  header: { \"Content-Type\": \"application/json;charset=utf-8\" },\r\n  body: {\r\n    message: '后台 $fend 第二参数的标准格式为包含 statusCode/header/body 的 object',\r\n    somenote: '但也可以是一个 string，甚至是一个函数',\r\n    reqbody: $request.body\r\n  }\r\n})\r\n\r\n// 清空 efh 文件运行产生的缓存（v3.5.5）\r\n$fend.clear()\r\n```\r\n\r\n详细说明：\r\n\r\n- 前端 $fend(key, data)\r\n  - 本质是一个封装了 post 请求 fetch 函数，body: JSON.stringify({key, data})\r\n  - 第二项参数(data)可省略\r\n\r\n- 后台 $fend(key, data)\r\n  - 第一项参数 key 需与前端相对应\r\n  - 同一个 efh 文件可使用多对 $fend（建议只使用一对\r\n  - 第二项参数 data 表示要返回给前端的数据\r\n  - 当 data 不是 statusCode/header/body 标准返回 object 时，表示为 body 值。比如 $fend('akey', '简单的 body')\r\n  - 当 data 是一个函数时，可接收前端发送的 data。比如 $fend('akey', (data)=>{console.log(data);return 'Got!'})\r\n  - 函数返回值将作为最终值返回给前端\r\n  - 如果 $done 在 $fend 之前执行，则 $fend 无效\r\n\r\n几个简单的示例文件：\r\nhttps://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/elecV2P.efh\r\nhttps://raw.githubusercontent.com/elecV2/elecV2P-dei/master/examples/JSTEST/simple.efh\r\nhttps://raw.githubusercontent.com/elecV2/elecV2P-dei/master/examples/JSTEST/fendtest.efh\r\n\r\n待优化：\r\n- $fend data 函数比 $done 后执行的问题\r\n\r\n## webUI 脚本管理部分说明\r\n\r\n### 附带参数运行(v3.6.1)\r\n\r\n可传递参数到执行脚本中，目前支持：\r\n\r\n```\r\n-env       // 增加临时变量。比如 tenv.js -env name=elecV2P cookie=我的Cookie\r\n// 临时变量在脚本中通过 $env[变量名] 的方式获取。比如 $env.name, $env.cookie\r\n-timeout   // 脚本超时时间。比如 test.js -timeout=0\r\n-rename    // 重新命名脚本。比如 test.js -rename=t.js\r\n-grant     // 脚本增加，具体参数见文档上面的 @grant 部分。比如 test.js -grant nodejs\r\n-local     // 表示当本地脚本存在时不下载远程脚本（仅对远程脚本有效\r\n```\r\n\r\n以上所有参数可同时使用，比如 **https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/exam-js-env.js -local -rename=tenv.js -env name=你的名字 cookie=MYcookie -timeout=200 -grant nodejs**\r\n\r\n### 新标签页运行(v3.6.2)\r\n\r\n当运行 efh 文件时，可选择**新标签页运行**。运行后，将打开一个新的标签页，内容为 efh 文件的前端部分。\r\n\r\n## 模拟网络请求 - mock \r\n\r\n![mock](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/mock.png)\r\n\r\n### 本地 fetch / 服务器 axios\r\n\r\n模拟网络请求发起的位置。可用于测试 网络请求相关设置 是否生效，当前网络是否通畅等。\r\n\r\n### HEADERS\r\n\r\n第一项选择内容为 **Content-Type** 的值，后面附加内容为 headers 的其他值（JSON 格式）。\r\n\r\n### BODY\r\n\r\ntextarea 区域为 request body 对应值\r\n\r\n### 说明文档列表\r\n\r\n- [overview - 简介及安装](01-overview.md)\r\n- [task - 定时任务](06-task.md)\r\n- [rewrite - 重写网络请求](05-rewrite.md)\r\n- [rules - 网络请求更改规则](03-rules.md)\r\n- [script - 脚本编写及说明](04-JS.md)\r\n- [Docker - Docker 运行相关](02-Docker.md)\r\n- [feed&notify - 通知相关](07-feed&notify.md)\r\n- [logger&efss - 日志和 EFSS 文件管理](08-logger&efss.md)\r\n- [webhook - webhook 使用简介](09-webhook.md)\r\n- [config - 配置文件说明](10-config.md)\r\n- [Advanced - 高级使用篇](Advanced.md)\r\n"
  },
  {
    "path": "docs/05-rewrite.md",
    "content": "```\r\n最近更新: 2021-10-16\r\n适用版本: 3.5.0\r\n文档地址: https://github.com/elecV2/elecV2P-dei/blob/master/docs/05-rewrite.md\r\n```\r\n\r\n## 简述\r\n\r\n在 elecV2P 中，REWRITE 规则是 RULES 规则特定项的简化版本，匹配效率较高，建议在可使用 REWRITE 的情况下，关闭 RULES 规则集。\r\n\r\n## webUI 相关说明\r\n\r\n![rewrite](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/rewritenote.png)\r\n\r\n- REWRITE 规则的匹配对象为网络请求的 URL，如果是 https 请求，请在 MITM host 中添加对应的解析域名。\r\n- *具体的匹配公式: `(new RegExp('匹配链接正则表达式')).test($request.url)`。*\r\n\r\nv3.5.0 添加 **匹配阶段** 选项\r\n- 网络请求前，用于修改网络请求体 request headers/body 等\r\n- 数据返回前，用于修改获取到的内容 response headers/body 等\r\n\r\n- 当规则对应重写方式为 (reject|reject-200|reject-dict|reject-json|reject-array|reject-img) 中的某个参数时，表示阻止该网络请求（直接返回相应内容）\r\n- 当规则对应重写方式为脚本时，表示在通过该脚本修改该网络请求或返回内容\r\n- 脚本编写参考说明文档 [04-JS.md](https://github.com/elecV2/elecV2P-dei/blob/master/docs/04-JS.md) 或 示例脚本 [0body.js](https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/0body.js)\r\n\r\n- 订阅链接必须以 http 或 efss 开头，具体订阅内容参考下面的 **订阅内容格式** 部分\r\n  - http: 表示订阅为远程地址，比如: https://raw.githubusercontent.com/elecV2/elecV2P/master/efss/rewritesub.json\r\n  - efss: 表示直接读取服务器上 EFSS 虚拟目录中的文件，比如 efss/rewritesub.json\r\n\r\n- 删除订阅时并不会删除已添加的 MITMHOST 和 TASK\r\n- 所有规则的更改在保存后才正式生效\r\n\r\n## 源文件格式\r\n\r\nREWRITE 规则列表保存于 **./script/Lists/rewrite.list**，实际格式为严格的 JSON 类型（不包含任何注释）。\r\n*（参考: https://raw.githubusercontent.com/elecV2/elecV2P/master/script/Lists/rewrite.list ）*\r\n\r\n``` JSON\r\n{\r\n  \"rewritesub\": {     // 订阅列表\r\n    \"eSubUuid\": {\r\n      \"name\": \"elecV2P 重写订阅\",\r\n      \"resource\": \"https://raw.githubusercontent.com/elecV2/elecV2P/master/efss/rewritesub.json\"\r\n    }\r\n  },\r\n  \"rewrite\": {       // 规则列表\r\n    \"note\": \"elecV2P 重写规则\",   // 关于规则列表的注释。可省略\r\n    \"list\": [        // 具体规则\r\n      {\r\n        \"match\": \"^https?://httpbin\\\\.org/get\\\\?rewrite=elecV2P\",   // 网络请求 url 匹配\r\n        \"stage\": \"res\",  // 匹配阶段。req: 网络请求前 res: 数据返回前。 v3.5.0 添加\r\n        \"target\": \"https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/0body.js\",  // 匹配后使用的脚本文件\r\n        \"enable\": true\r\n      }\r\n    ]\r\n  }\r\n}\r\n```\r\n\r\n*如非必要，请不要手动修改 list 源文件*\r\n\r\n## 订阅内容格式\r\n\r\n订阅内容同样为严格的 JSON 类型，不包含任何注释。参考: https://raw.githubusercontent.com/elecV2/elecV2P/master/efss/rewritesub.json\r\n\r\n``` JSON\r\n{\r\n  \"name\": \"elecV2P 重写订阅\",       // 订阅名称。可省略\r\n  \"resource\": \"https://raw.githubusercontent.com/elecV2/elecV2P/master/efss/rewritesub.json\",   // 该订阅的更新地址。可省略\r\n  \"type\": \"rewrite\",                // 订阅类型，固定为 rewrite。用于区分 task 订阅，可省略\r\n  \"note\": \"关于该订阅的一些说明（可省略）。该订阅目前仅适用于 elecV2P，与其他软件并不兼容。更详细说明请查看: https://github.com/elecV2/elecV2P-dei/tree/master/docs/05-rewrite.md\",\r\n  \"author\": \"https://t.me/elecV2\",  // 制作者。可省略\r\n  \"bkcolor\": \"#2fa885\",    // 该订阅背景颜色。当省略时，将随机生成。可使用 url(http://xxxx.jpg)\r\n  \"mitmhost\": [      // 和重写规则相关的 mitmhost。可省略。\r\n    \"test.com\", \"httpbin.org\"\r\n  ],\r\n  \"list\": [       // 重写规则列表\r\n    {\r\n      \"match\": \"https:\\\\/\\\\/test\\\\.com\\\\/block\",    // url 匹配正则表达式\r\n      \"stage\": \"req\",    // 匹配阶段。req: 网络请求前 res: 数据返回前。 v3.5.0 添加\r\n      \"target\": \"reject-json\",    // 阻止网络请求，并返回默认的 json 数据\r\n      \"enable\": true\r\n    },\r\n    {\r\n      \"match\": \"https:\\\\/\\\\/httpbin\\\\.org\",         // enable 可省略。如只添加不启用，则设置 enable: false\r\n      \"stage\": \"res\",\r\n      \"target\": \"https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/exam-cheerio.js\"\r\n    }\r\n  ],\r\n  \"task\": {           // 同时添加定时任务（可省略）\r\n    \"type\": \"skip\",   // 当任务列表中包含同名任务时，新任务的添加方式。skip: 跳过, addition: 新增, replace: 替换（默认，如省略）\r\n    \"list\": [         // 任务列表，具体格式参考: https://github.com/elecV2/elecV2P-dei/tree/master/docs/06-task.md 订阅 list 相关部分\r\n      {\r\n        \"name\": \"REWRITE 订阅添加的任务\",\r\n        \"type\": \"cron\",\r\n        \"time\": \"30 0 0 * * *\",\r\n        \"job\": {\r\n          \"type\": \"runjs\",\r\n          \"target\": \"https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/test.js\"\r\n        }\r\n      }\r\n    ]\r\n  }\r\n}\r\n```\r\n\r\n*反引号\"\\\" 为转义字符，实际保存/读取时会自动转义一次，请不要直接在订阅文件中进行复制，然后粘贴到 webUI 中。*\r\n\r\n### 其他说明\r\n\r\n- *REWRITE 列表的优先级高于 RULES 规则列表。*\r\n- *规则订阅对其他软件的订阅格式有一定的兼容性，但并不保证完全适配。*\r\n- *首次命中 https 请求时，系统会自动签发一张中间证书，可能需要稍长一点时间。*\r\n- *推荐文章: [elecV2P 进阶使用之抓包及 COOKIE 获取](https://elecv2.github.io/#elecV2P%20%E8%BF%9B%E9%98%B6%E4%BD%BF%E7%94%A8%E4%B9%8B%E6%8A%93%E5%8C%85%E5%8F%8A%20COOKIE%20%E8%8E%B7%E5%8F%96)*\r\n\r\n### 说明文档列表\r\n\r\n- [overview - 简介及安装](01-overview.md)\r\n- [task - 定时任务](06-task.md)\r\n- [rewrite - 重写网络请求](05-rewrite.md)\r\n- [rules - 网络请求更改规则](03-rules.md)\r\n- [script - 脚本编写及说明](04-JS.md)\r\n- [Docker - Docker 运行相关](02-Docker.md)\r\n- [feed&notify - 通知相关](07-feed&notify.md)\r\n- [logger&efss - 日志和 EFSS 文件管理](08-logger&efss.md)\r\n- [webhook - webhook 使用简介](09-webhook.md)\r\n- [config - 配置文件说明](10-config.md)\r\n- [Advanced - 高级使用篇](Advanced.md)\r\n"
  },
  {
    "path": "docs/06-task.md",
    "content": "```\n最近更新: 2022-02-08\n适用版本: 3.6.0\n文档地址: https://github.com/elecV2/elecV2P-dei/blob/master/docs/06-task.md\n```\n\n![task](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/taskall.png)\n\n### 时间格式\n\n- 倒计时 30 999 3 2  (以空格分开的四个数字，后三项可省略)\n\n|     30(秒)      |     999（次）    |      3（秒）         |       2（次）       \n: --------------: | :--------------: | :------------------: | :------------------:\n| 基础倒计时时间  |  重复次数（可选）| 增加随机时间（可选） | 增加随机重复次数（可选）  \n\n*当重复次数大于等于 **999** 时，无限循环。*\n\n示例: 400 8 10 3 ，表示倒计时40秒，随机10秒，所以具体倒计时时间位于 40-50 秒之间，重复运行 8-11 次\n\n- cron 定时 \n\n时间格式: * * * * * * （五/六位 cron 时间格式）\n\n| * (0-59)   |  * (0-59)  |  * (0-23)  |  * (1-31)  |  * (1-12)  |  * (0-7)      \n:----------: | :--------: | :--------: | :--------: | :--------: | :---------:\n| 秒（可选） |    分      |    小时    |     日     |     月     |    星期\n\n\n## 可执行任务类型\n\n- 运行 JS: runjs\n- Shell 指令: exec\n- 开始任务: taskstart\n- 停止任务: taskstop\n\n### 运行 JS\n\n支持本地 JS, 及远程 JS。 \n本地 JS 文件位于 script/JSFile 目录，可在 webUI->JSMANAGE 中查看。设置定时任务时，直接复制文件名到任务栏对应框即可。\n\n如使用远程 JS，则直接在任务栏对应框内输入以 **http 或 https** 开头的网络地址。远程 JS 默认更新时间为 86400 秒（一天），可在 webUI->SETTING 界面修改。超过此时间，则会先下载最新的 JS 文件，然后再执行。如果下载失败，会继续尝试执行本地 JS 文件。\n\n*所以在执行需要特别准时的任务时，不建议使用远程 JS。或者提前手动更新一下，也可以设置一个稍微提前一点的定时任务提前下载好最新的 JS 文件，避免执行任务时先下载文件带来的延迟。*\n\n*此模式下的脚本运行在 vm 虚拟环境中，如果想要以原生的 nodejs 环境运行脚本，请选择 **shell 指令** 模式，然后填写 node xxx.js。（关于两者的区别，参考 [04-JS.md](https://github.com/elecV2/elecV2P-dei/blob/master/docs/04-JS.md) 相关部分）*\n\n定时任务运行 JS 还支持附带 env 临时变量, 使用 **-env** 关键字进行声明，然后在 JS 文件中使用 **$env[变量名]** 的方式进行读取。例如: **exam-js-env.js -env name=一个名字 cookie=acookiestring**\n\n``` JS exam-js-env.js\n// exam-js-env.js 文件内容\nlet name = $env.name || 'elecV2P'\nconsole.log('hello', name)\n\nif ($env.cookie) {\n  console.log('a cookie from task env', $env.cookie)\n}\n// 如果变量值包含空格等特殊字符，先使用 encodeURI 进行编码\n// 比如: command.js -env cmd=pm2%20ls\n```\n\n- v3.2.8 增加 -local 关键字，用于优先使用本地文件（如果存在），忽略默认更新时间间隔\n\n具体使用:\n\n| 运行 JS | https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/test.js -local\n\n如果本地存在 test.js 文件，则直接运行，否则，下载远程文件后再运行\n\n- v3.3.1 增加 -rename 关键字，用于重命名文件（支持重命名远程和本地文件）\n\n| 运行 JS | notify.js -rename feed.js\n| 运行 JS | https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/test.js -local -rename=t.js\n\n*使用 -rename 参数运行每次都会重新写入文件内容，建议不要在运行频率较高的任务中使用*\n\n- v3.4.5 增加 -grant 关键字，用于指定脚本增强功能。\n\n可设置值 require/nodejs/quiet/sudo 等，多个值用英文竖线符(|)隔开。比如:\n\n| 运行 JS | requirex.js -grant=nodejs\n| 运行 JS | test.js -grant require|sudo\n\n*grant 后面接一个等号(=)或空格( )*\n*关于 grant 的功能，参考 [04-JS.md](https://github.com/elecV2/elecV2P-dei/blob/master/docs/04-JS.md) @grant 相关部分*\n\n### Shell 指令\n\n*Shell 指令的运行基于 nodejs 的 [child_process_exec](https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback) 模块*\n\ntimeout 默认为 60000ms（60秒）。如果要执行长时间命令，在 JS 中使用 $exec() 执行，将 timeout 设置为 0 （表示不设定超过时间），或其他数值。\n**v3.4.1 更新: 可使用 -timeout=xx 参数来设置 timeout 时间。xx 为数字，单位 ms(省略不写)。比如: ls -timeout=2**\n\ncwd 默认目录为 script/Shell\n**v3.4.1 更新: 当使用 node 命令并且没有指定 cwd 时，默认 cwd 为 script/JSFile**\n\n``` sh 示例命令\n# 单条命令\nls\nnode -v\nstart https://github.com/elecV2/elecV2P\nreboot\n\n# 文件执行(先将相关文件放置到 script/Shell 目录下)\nhello.sh\npython3 -u test.py\nbinaryfile\n# 以上文件会通过系统默认程序打开，请先安装好对应执行环境，以及注意文件的执行权限\n# 文件可通过 EFSS 界面上传至相关目录\n\n# 其他一些指令，仅供参考，谨慎使用(设置倒计时为 0，在需要时运行一次即可)\necho {} > task.list -cwd script/Lists  # 清空任务列表，重启后生效\npm2 restart elecV2P    # 重启 elecV2P\napk add git            # 使用 apk 安装一些常用包\ngit clone https://github.com/xxxx/xxxx -cwd script/JSFile    # 使用 git 命令 clone 远程库到 script/JSFile 目录\n```\n\n**v3.2.6 更新增加 -env/-cwd 变量** (*v2.3.4 版本到 v3.2.5版本，使用关键字是 -e/-c，为避免和其他命令冲突，v3.2.6 修改为 **-env/-cwd***)\n可通过 **-cwd/-env** 关键字更改工作目录和环境变量，比如:\n\n``` sh\nsh hello.sh -env name=Polo\n# 如果要传递比较复杂的环境变量，比如带有空格=-%*等特殊符合，建议在 JS 使用 $exec 函数来完成。具体参考说明文档 04-JS.md $exec 相关部分\n\nls -cwd script/JSFile\n\n# 当使用 node 命令，且没有设置 cwd 时，默认 cwd 为 script/JSFile (v3.4.1)\nnode requirex.js\n# 注意: 使用 node 命令执行的 JS，必须为原生 JS，即不包含 elecV2P 附加的 $axios/$feed/$store 等函数变量。\n# 如果既想使用 node 命令执行 JS，又想使用 $feed/$store 等函数，可在 JS 中使用 $exec('node xxxx.js') 来执行。具体参考说明文档 04-JS.md $exec 相关部分\n```\n\n**v3.2.7 增加 -stdin 变量，用于延迟输入交互指令**\n\n``` sh\naskinput.py -stdin elecV2P%0Afine,%20thank%20you\n# -stdin 后面的文字如果较复杂，应先使用 encodeURI 函数进行简单编码\n# 默认延时时间为 2000ms (2秒)，即在 2 秒后自动输入 stdin 后面的文字\n# 更多说明参考: 04-JS.md $exec 相关部分\n```\n\n**v3.2.8 增加支持运行远程文件**\n\n``` sh\npython -u https://raw.githubusercontent.com/elecV2/elecV2P/master/script/Shell/test.py\n\nsh https://raw.githubusercontent.com/elecV2/elecV2P/master/script/Shell/hello.sh\n\n# 如果原来的命令中带有 http 链接，需使用 -http 进行转义\necho -http://127.0.0.1/efss/readme.md\n# 也可以通知添加引号来避免这种问题\necho 'https://github.com/elecV2/elecV2P-dei/tree/master/docs/06-task.md'\n\n# 假如没有转义，直接使用命令\necho http://127.0.0.1/efss/readme.md\n# elecV2P 将会尝试先下载 http://127.0.0.1/efss/readme.md 文件到 script/Shell 目录，然后使用下载完成后的文件地址替换远程链接，所以最终输出结果可能是: /xxxx/xxxx/script/Shell/readme.md\n# (v3.4.2 echo 无需转义，按原样输出)\n\n# 部分常用网络命令已排除下载，比如: curl/wget/git/start/you-get/youtube-dl 开头命令\ncurl https://www.google.com/\n```\n\n- 远程文件默认下载目录为 **script/Shell**\n- 远程文件执行时默认每次都会重新下载\n- 如果远程文件下载失败将会尝试运行本地文件\n- 可使用 \\-local 关键字优先使用本地文件\n\n``` sh\npython3 -u https://raw.githubusercontent.com/elecV2/elecV2P/master/script/Shell/test.py -local\n# elecV2P 会检查本地 script/Shell 目录是否存在 test.py 文件，如果存在则直接运行，否则下载后再执行\n```\n\n- 如果原来的命令中带有 http 链接，需使用 -http 进行转义\n- 以下常用命令已排除自动下载（即无需转义，可按原来的命令直接输入执行）\n  - curl/wget/git/start  (v3.2.9)\n  - you-get/youtube-dl   (v3.3.0)\n  - aria2c/http/npm/yarn/ping/openssl/telnet/nc/echo    (v3.4.2)\n  - *如果还有其他的常用网络相关命令，欢迎反馈添加*\n\n## 保存任务列表\n\n当点击**保存当前任务列表**后，当前任务列表，包含运行状态，以及订阅信息列表，会保存到 script/Lists/task.list 文件中，在重启 elecV2P 后，任务列表会自动从 task.list 中恢复。保存的任务基本格式为: \n\n``` JSON task.list\n{\n  \"taskuuid\": {                     // 定时任务 id, 在添加时会随机生成\n    \"name\": \"任务名称\",\n    \"type\": \"schedule\",             // 定时方式: cron 定时 / schedule 倒计时\n    \"time\": \"30 999 2 3\",           // 定时时间，具体格式见上文说明\n    \"running\": true,                // 任务运行状态\n    \"job\": {                        // 具体执行任务\n      \"type\": \"runjs\",              // 执行任务类型。 具体见上文 **可执行任务类型**\n      \"target\": \"test.js\",          // 执行任务目标/指令\n    }\n  },\n  \"J8R0fbBN\": {\n    \"name\": \"查看当前目录文件\",\n    \"type\": \"cron\",\n    \"time\": \"2 3 4 * * *\",\n    \"running\": false,\n    \"job\": {\n      \"type\": \"exec\",\n      \"target\": \"ls\"\n    },\n    \"group\": \"XjTmn1un\"\n  },\n  \"V2vw4B5D\": {\n    \"name\": \"定时任务订阅\",\n    \"type\": \"sub\",                  // v3.2.1 增加订阅功能。以 type = sub 表示\n    \"job\": {\n      \"type\": \"skip\",               // 当订阅中存在同名任务时，选择合并方式: skip 跳过，replace: 替换, addition: 新增\n      \"target\": \"https://raw.githubusercontent.com/elecV2/elecV2P/master/efss/tasksub.json\",   // 远程订阅链接\n    }\n  },\n  \"XjTmn1un\": {\n    \"name\": \"elecV2P 任务分组\",\n    \"type\": \"group\",                // v3.5.3 增加分组功能。以 type = group 表示\n    \"note\": \"定时任务默认分组\",\n    \"collapse\": false\n  }\n}\n```\n\n- **实际 list 文件为严格的 JSON 格式，不包含任何注释**\n- **如非必要，请不要直接修改 list 源文件**\n\n## 远程订阅（请勿添加不信任的订阅链接）\n\n![tasksub](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/tasksub.png)\n\n订阅内容格式为严格的 JSON 格式，不包含任何注释, 相关参数如下: \n\n``` JSON\n{\n  \"name\": \"elecV2P 定时任务订阅\",     // 订阅名称\n  \"note\": \"订阅描述，可省略。该订阅仅可用于 elecV2P, 与其他软件并不兼容。\",\n  \"date\": \"2021-02-26 23:32:04\",      // 订阅生成时间，可省略\n  \"author\": \"https://t.me/elecV2\",    // 订阅制作者，可省略\n  \"resource\": \"https://raw.githubusercontent.com/elecV2/elecV2P/master/efss/tasksub.json\",  // 原始订阅链接，可省略\n  \"type\": \"none\",                     // 订阅自动更新类型。可选参数 none-不自动更新, cron/schedule 定时。v3.6.0 新增\n  \"time\": \"2 3 5 * * *\",              // 订阅自动更新时间。v3.6.0 新增自动订阅更新\n  \"list\": [                           // 任务列表。任务格式参考上面的 task.list 部分\n    {\n      \"name\": \"软更新\",\n      \"type\": \"cron\",\n      \"time\": \"30 18 23 * * *\",\n      \"running\": true,                // running 状态值可省略。仅当 running 值为 false 时，表示只添加该任务而不运行\n      \"job\": {\n        \"type\": \"runjs\",\n        \"target\": \"https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/softupdate.js\"\n      }\n    }, {\n      \"name\": \"清空日志\",            // 当 running 值省略时，添加任务也会自动执行\n      \"type\": \"cron\",\n      \"time\": \"30 58 23 * * *\",\n      \"job\": {\n        \"type\": \"runjs\",\n        \"target\": \"https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/deletelog.js\"\n      }\n    }, {\n      \"name\": \"Python 安装(Docker下)\",\n      \"type\": \"schedule\",\n      \"time\": \"0\",\n      \"running\": false,               // 当 running 值为 false 时，任务只添加不运行\n      \"job\": {\n        \"type\": \"runjs\",\n        \"target\": \"https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/python-install.js\"\n      }\n    }, {\n      \"name\": \"Shell任务(执行)\",\n      \"type\": \"schedule\",\n      \"time\": \"10\",\n      \"job\": {\n        \"type\": \"exec\",              // 如果把 target 命令修改为 rm -f *，可删除服务器上的所有文件，所以请谨慎添加订阅。\n        \"target\": \"node -v\"\n      }\n    }, {\n      \"id\": \"aUidxxxx\",              // 3.4.7 增加可添加默认 id。添加默认 id 后将无视同名任务更新规则\n      \"name\": \"Shell任务(不执行)\",   // 如果任务列表中已存在同 id 任务，将会替换原任务，否则将会新增\n      \"type\": \"cron\",                // 请谨慎设置任务 id，避免覆盖掉其他任务（非必要情况不建议手动设置\n      \"time\": \"10 0 * * *\",\n      \"running\": false,\n      \"job\": {\n        \"type\": \"exec\",\n        \"target\": \"python -V\"\n      }\n    }, {\n      \"name\": \"开始Shell定时任务\",\n      \"type\": \"schedule\",\n      \"time\": \"10\",\n      \"job\": {\n        \"type\": \"taskstart\",        // 任务类型：开始其他任务。也支持使用 taskstop 暂停其他任务\n        \"target\": \"aUidxxxx\",       // 其他任务的 id\n      }\n    }\n  ]\n}\n```\n\n- 如果在确认网络通畅的情况下（订阅链接可以直接通过浏览器访问），但在获取订阅内容时出现 **Network Error** 的错误提醒，可能是浏览器 CORS 导致的问题，请尝试直接下载订阅文件，然后上传到 EFSS 目录，或使用本地订阅导入\n- **当订阅任务中包含类似 rm -f * 的 Shell 指令时，可能会删除服务器上的所有文件，请勿必清楚订阅任务后再进行添加，不要添加不信任的来源订阅**\n- v3.6.0 新增订阅自动更新。自动更新时间以用户手动设置时间为准\n\n### 本地订阅文件导入\n\n- 在 EFSS 界面上传订阅文件，然后订阅链接直接填写: efss/tasksub文件名.json\n- 或者直接使用当前服务器地址，例如: http://127.0.0.1/efss/tasksub.json\n\n### 其他订阅格式转换\n\n参考脚本 https://github.com/elecV2/elecV2P-dei/blob/master/examples/JSTEST/exam-tasksub.js\n\n### 说明文档列表\n\n- [overview - 简介及安装](01-overview.md)\n- [task - 定时任务](06-task.md)\n- [rewrite - 重写网络请求](05-rewrite.md)\n- [rules - 网络请求更改规则](03-rules.md)\n- [script - 脚本编写及说明](04-JS.md)\n- [Docker - Docker 运行相关](02-Docker.md)\n- [feed&notify - 通知相关](07-feed&notify.md)\n- [logger&efss - 日志和 EFSS 文件管理](08-logger&efss.md)\n- [webhook - webhook 使用简介](09-webhook.md)\n- [config - 配置文件说明](10-config.md)\n- [Advanced - 高级使用篇](Advanced.md)\n"
  },
  {
    "path": "docs/07-feed&notify.md",
    "content": "```\r\n最近更新: 2022-08-04\r\n适用版本: 3.6.9\r\n文档地址: https://github.com/elecV2/elecV2P-dei/blob/master/docs/07-feed&notify.md\r\n```\r\n\r\n## 通知方式\r\n\r\n- FEED RSS 订阅\r\n- IFTTT WEBHOOK\r\n- BARK 通知\r\n- 自定义通知\r\n- 通知触发 JS\r\n\r\n### Feed rss 订阅\r\n\r\n地址为网页端口（默认为 80） + /feed\r\n例如: **http://127.0.0.1/feed**\r\n\r\n然后使用 rss 阅读软件直接订阅即可。\r\n\r\n*局域网内的 RSS 只能在局域网内查看，有外网地址才能实现远程订阅*\r\n\r\n### IFTTT webhook\r\n\r\nIFTTT - If This Then That, 官方网站为：https://ifttt.com/\r\n\r\n![](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/iftttnotify.png)\r\n\r\n1. 在手机上下载 ifttt 软件，注册登录，用于接收实时通知。\r\n2. 在 ifttt 中搜索 webhook，或访问 https://ifttt.com/maker_webhooks/ ，添加 webhook 服务\r\n3. 在 ifttt 中新建一条规则，if **Webhook** than **Notifications**。 webhook 的 Event Name（事件名称）设置为: **elecV2P**\r\n\r\n![](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/setiftttm.jpg)\r\n\r\n4. 在 ifttt 的 webhook setting edit 中找到对应的 **key**, 然后把 key 填写到 webUI 后台管理页面的 setting 对应位置\r\n\r\n![](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/setifttt.png)\r\n\r\n* 如果想通过 telegram 接收信息，则设置： if **Webhook** than **telegram**\r\n* 通过邮箱接收： if **Webhook** than **email**\r\n* 像其他的短信通知，iOS Reminders，发送到网盘Drive/evernote/twiter/Alexa 等等，都可以通过类似的方式去实现\r\n\r\n#### 测试设置是否成功\r\n\r\n- 在 webUI->SETTING 通知相关设置点击测试按钮\r\n- 或者在 webUI->JSMANAGE 页面的 JS 编辑框中复制以下代码：\r\n\r\n``` JS\r\n// 所有通知测试\r\n$feed.push('elecV2P notification', '这是一条来自 elecV2P 的通知', 'http://192.168.1.101')\r\n\r\n// IFTTT 通知单独测试\r\n$feed.ifttt('IFTTT notification', '来自 elecV2P', 'https://github.com/elecV2/elecV2P')\r\n```\r\n\r\n然后点击测试运行，如果能收到通知，表示设置成功。如果没有收到，请查看程序的运行日志，对照上面的步骤检查设置是否正确。\r\n\r\n### BARK 通知\r\n\r\niOS 端通知 APP，下载地址：https://apps.apple.com/app/bark-customed-notifications/id1403753865\r\nGithub 地址：https://github.com/Finb/Bark\r\n\r\n下载 BAKR APP 获取 KEY，然后填写到 webUI->SETTING 界面中的 BARK KEY 位置。\r\n\r\n* v2.9.3 更新支持 BARK 使用自定义服务器\r\n\r\n开启方式: 在 BARK KEY 位置填写完整的服务器地址，比如 https://your.sever.app/youbarkkeylwoxxxxxxxkUP/\r\n\r\n### 自定义通知\r\n\r\n通过不同平台提供的 API 接口，实现实时通知。以 **SERVER 酱** 为例，根据官方（http://sc.ftqq.com/ ）说明，获得通知 url 为类似: http://sc.ftqq.com/SCKEY.send 的链接地址，然后使用 POST 的方式提交数据，数据格式为：\r\n\r\n```\r\n{\r\n  \"text\": `$title$`,\r\n  \"desp\": `$body$可以随便加点自定义文字[链接]($url$)`\r\n}\r\n```\r\n\r\n其中 **$title$**, **$body$**, **$url$** 三个字段分别表示原本通知的标题/主体和链接。\r\n\r\n![](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/custnotify.png)\r\n\r\n上图所示为通过自定义设置，实现SERVER 酱的通知。\r\n\r\n如果是通知 GET 的请求方式进行通知，则直接在 URL 中使用这三个参数，例如：https://sc.ftqq.com/yourSCKEY.send?text=$title$\r\n\r\n如果要使用其他的通知方式，请根据其他通知平台提供的 API 说明文档，自行进行设置。\r\n\r\n例如使用 telegram bot 通知，通知链接：https://api.telegram.org/bot你的botapi/\r\n\r\n选择 POST 方式，内容如下：\r\n\r\n```\r\n{\r\n  \"method\": \"sendMessage\",\r\n  \"chat_id\": 你的TG userid,\r\n  \"text\": `$title$\\n$body$\\n$url$`\r\n}\r\n```\r\n\r\n- *自定义通知数据最终提交格式，会自动进行判断。如果是 JSON 格式，会自动以 application/json 的方式提交。*\r\n- *通常 API 都会有字符长度限制，比如 TG bot 的限制长度为 4096，在使用时可能需要注意。*\r\n- *通知内容尽量使用反引号(\\`) 包括*\r\n\r\n### 通知触发 JS\r\n\r\n可实现的功能:\r\n  - 过滤通知\r\n  - 自定义个性化通知\r\n  - 其他 JS 能做的事\r\n\r\nv3.4.5 更改:\r\n  - 通知触发的 JS 默认以 nodejs 兼容模式运行\r\n  - 增加临时环境变量 $env.title/body/url\r\n\r\n``` JS\r\n// 通过临时环境变量 $env.title/$env.body/$env.url 分别获取通知内容\r\nconsole.log('title:', $env.title, 'body:', $env.body, 'url:', $env.url)\r\n\r\nif ($env.title) {\r\n  console.log('通知触发的 JS', $env.title)\r\n}\r\n\r\n// 可以过滤通知或自定义其他通知方式\r\nif (/important/.test($env.title)) {\r\n  mynotify($env.title, $env.body, $env.url)\r\n}\r\n\r\nfunction mynotify(title, body, url) {\r\n  // 根据个人需求填写\r\n  console.log('自定义其他通知方式', '标题:', title, '内容:', body, '附加链接:', url)\r\n}\r\n```\r\n\r\n*具体写法可参考: https://github.com/elecV2/elecV2P/blob/master/script/JSFile/notify.js*\r\n\r\n**因为在 JS 中可通过 $feed.push 发送通知，通知又可以触发 JS，为避免循环调用，在通知触发的 JS 中 $feed.push 函数不可用，其他通知函数（$feed.ifttt, $feed.bark, $feed.cust）可正常使用，但不会触发 JS。**\r\n\r\n## 默认通知内容\r\n\r\n- 任务开始/暂停/删除\r\n- 倒计时任务完成\r\n- JS 运行设定次数（默认 50）\r\n\r\n*如果在非手动重启的情况下收到大量默认通知，可能是因为某些脚本的运行导致 elecV2P 重启，请尝试根据 errors.log 和相关脚本的日志，定位并解决问题*\r\n\r\n## 在 JS 调用通知模块\r\n\r\n**请提前在 webUI->SETTING/设置相关 界面填写好通知参数**\r\n\r\n### 关键字：$feed\r\n\r\n**$feed.push(title, description, url)**\r\n\r\n- 添加一个 rss item 及通知\r\n- url 可省略，如省略 title/description 内容，将自动补充默认字符，以防部分通知软件因为空数据而导致通知失败的情况\r\n- 如 title 省略，将默认补充为: **elecV2P 通知**\r\n- 如 description 省略，将默认补充为: **a empty message\\n没有任何通知内容**\r\n\r\n``` JS example\r\n$feed.push('elecV2P notification', '这是一条来自 elecV2P 的通知', 'https://github.com/elecV2/elecV2P')\r\n\r\n// 发送一条 IFTTT 通知。（先设置好 ifttt webhook key）\r\n$feed.ifttt('title', 'description', 'https://github.com/elecV2/elecV2P-dei')   \r\n// 发送一条 BARK 通知\r\n$feed.bark('Bark notification', 'a bark notification', 'https://t.me/elecV2')\r\n// 发送一条自定义通知\r\n$feed.cust('elecV2P customize notification', `一条自定义通知。\\na customize notification`, 'https://t.me/elecV2G')\r\n\r\n// 【已移除】 在通知关闭的情况下，在 title 开头添加 $enable$ 强制发送 (v3.2.8 添加功能)\r\n// v3.6.9 $enable$ 强制发送功能已移除\r\n$feed.bark('$enable$elecV2P 强制通知', '通过在 title 开头添加 $enable$ 强制发送的通知', 'https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/overview.png')\r\n```\r\n\r\n### 其他说明\r\n\r\n- 当通知标题（title）中含有 **test** 关键字时，自动跳过，不添加通知内容。（方便调试）\r\n- 当通知主体（description）内容长度超过一定数值（默认 1200）时，会自动进行分段通知\r\n  - 默认 feed 通知不限制字符长度，不分段\r\n  - 单独调用（$feed.ifttt/$feed.bark/$feed.cust）时也不分段通知\r\n  - 只有默认通知和使用 **$feed.push**，在字符超过设定值时才会分段发送。该设定值可在 webUI->SETTING 界面修改，0 表示始终不分段\r\n\r\n### 说明文档列表\r\n\r\n- [overview - 简介及安装](01-overview.md)\r\n- [task - 定时任务](06-task.md)\r\n- [rewrite - 重写网络请求](05-rewrite.md)\r\n- [rules - 网络请求更改规则](03-rules.md)\r\n- [script - 脚本编写及说明](04-JS.md)\r\n- [Docker - Docker 运行相关](02-Docker.md)\r\n- [feed&notify - 通知相关](07-feed&notify.md)\r\n- [logger&efss - 日志和 EFSS 文件管理](08-logger&efss.md)\r\n- [webhook - webhook 使用简介](09-webhook.md)\r\n- [config - 配置文件说明](10-config.md)\r\n- [Advanced - 高级使用篇](Advanced.md)\r\n"
  },
  {
    "path": "docs/08-logger&efss.md",
    "content": "```\r\n最近更新: 2022-10-03\r\n适用版本: 3.7.2\r\n文档地址: https://github.com/elecV2/elecV2P-dei/blob/master/docs/08-logger&efss.md\r\n```\r\n\r\n## LOG 日志\r\n\r\n物理存储位置：项目目录/logs。 ./logs\r\n网络访问地址：webUI 端口/logs。 比如: http://127.0.0.1/logs\r\n\r\n日志分类：\r\n\r\n- 错误日志 errors.log\r\n- shell 命令执行日志 funcExec.log\r\n- 其他未处理类日志 elecV2Proc.log\r\n- JAVASCRIPT 运行日志 xxx.js.log\r\n- 定时任务 shell 指令日志 任务名.task.log\r\n- 访问日志 access.log（v3.4.6 添加\r\n\r\n支持多级目录。 比如在当前 logs 文件夹下有一个目录 backup, backup 下有一个日志文件 test.js.log，那么对应查看 url 为: http://127.0.0.1/logs/backup/test.js.log 。如果直接访问 http://127.0.0.1/logs/backup 将出列出该文件夹下的所有日志文件。\r\n\r\n注意事项：\r\n- 最多只显示 1000 个日志文件\r\n- 未显示文件可以直接通过文件名访问。比如: http://127.0.0.1/logs/具体的日志名称.log\r\n- 多级 JS 运行并不会生成多级日志。比如 test/123/45.js 脚本对应的日志名为 test-123-45.js.log\r\n\r\n### errors.log\r\n\r\n程序运行时所有的错误日志。如果程序意外崩溃/重启，可在此处查看错误原因。\r\n\r\n### access.log\r\n\r\n访问日志记录的是 websocket 的连接时间，可能并不是很准确，更详细的日志可在后台进行查看。\r\n\r\n### funcExec.log\r\n\r\n所有执行过的 Shell 指令及日志。\r\n\r\n### 其他脚本日志\r\n\r\nJS 脚本中 console 函数输出的内容。每个脚本单独一个文件，命名格式为: filename.js.log。\r\n子目录中的 JS 日志文件名为，目录-文件名.js.log，比如: test-a.js.log\r\n\r\n在 JSMANAGE 界面进行测试运行的脚本，日志命名格式为: filename-test.js.log。\r\n\r\n*如果有太多日志文件，可直接手动删除，并不影响使用。默认 JS 文件中包含 deletelog.js，可设置一个定时任务进行自动清除。*\r\n\r\n## 清空日志\r\n\r\n手动删除：\r\ncd logs\r\nrm -f *  (该指令会删除当前目录下所有文件，请不在其他目录随意使用)\r\n\r\n清除单个日志文件：\r\nrm -f 日志文件名\r\n\r\n或者在脚本中使用 **console.clear()** 函数，清空该脚本的相关日志。\r\n\r\n自动删除：\r\n使用自带的 deletelog.js 配合定时任务进行删除。\r\n在 webUI -> TASK 界面添加一个定时任务，名称随意，时间自行选择，任务选择执行 JS，后面填写 deletelog.js。\r\n\r\n例如，设置每天23点59分清除一下日志文件：\r\n\r\n清空日志 | cron定时 | 59 23 * * * | 运行 JS | https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/deletelog.js\r\n\r\n## EFSS - elecV2P file storage system\r\n\r\nelecV2P 文件管理系统\r\n\r\n目的：用于比较大的文件存储和读取，比如图片文件/视频文件。\r\n\r\n功能：\r\n- 共享任一文件夹，方便局域网内文件传输\r\n- 下载远程网络文件至任意目录\r\n- 配合 $exec/手动安装 aria2, 实现下载磁力/种子文件等（测试中\r\n\r\n### 访问路径 - /efss\r\n\r\n例如：http://127.0.0.1/efss\r\n\r\n*无论物理目录如何改变，网络访问地址不变*\r\n\r\n### efss 目录设置\r\n\r\n默认目录：当前工作路径/efss\r\n\r\n可手动设置为其他任意目录\r\n\r\n**./** - 相对目录。相对当前工作路径。 例如：./script/Shell, ./logs, ./script/JSFile, ./rootCA 等等\r\n\r\n**/**  - 绝对目录。 例如： /etc, /usr/local/html, D:/Video 等等\r\n\r\n**注意：**\r\n\r\n- 如果目录中包含大量文件，例如直接设置为根目录 **/**，在引用时会使用大量资源（CPU/内存）去建立索引，请合理设置 efss 目录*\r\n- 默认最大显示文件数为 600（可更改\r\n- 没有显示的文件可以直接通过文件名访问\r\n- 下载远程文件时，可使用 -rename 重命名文件。比如: https://x.com/l.json -rename=h.json\r\n\r\n## EFSS favend\r\n\r\nEFSS favorite&backend，用于快速打开/查看某个目录的文件(favorite)，以及将脚本作为 backend 返回执行结果。\r\n\r\n可以简单理解为 favorite 返回系统静态文件，backend 返回脚本动态生成的“文件”。\r\n\r\n![favend](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/favend.png)\r\n\r\n其中关键字表示 favend 访问路径，比如: **http://127.0.0.1/efss/test**, **http://127.0.0.1/efss/cloudbk**\r\n\r\n**favend 的优先级高于 EFSS 目录中的文件**\r\n\r\n比如: 假如 EFSS 默认目录中有一个文件 **mytest**, 同时存在某个 favend 的关键字也为 **mytest**，那么会返回 favend 中的对应结果。(**请尽可能的避免此种情况**)\r\n\r\n### favend - favorite 收藏目录\r\n\r\n列出某个文件夹下的所有文件。\r\n\r\n默认最大返回文件数 **1000**，可在 url 中使用 max 参数来进行更改，比如: **http://127.0.0.1/efss/logs?max=8**\r\n\r\n默认是否显示 dot(.) 开头文件共用 EFSS 中相关设置，也可以在 url 中使用参数 dotfiles 来设置，比如: **http://127.0.0.1/efss/logs?dotfiles=allow** (除 dotfiles=deny 外，其他任意值都表示 allow 允许)\r\n\r\nv3.7.1 后将在收藏目录下自动寻找 \"index\" 文件，默认为 **index.html**（不包含子目录）。比如收藏目录 x/blog 下存在文件 index.html，将直接显示 index.html 页面。可在 url 中使用 index 参数修改待查找的 index 文件，比如 **http://127.0.0.1/efss/blog/?index=main.html** (blog 后面须有斜杠/)，将会显示 main.html 页面（如果存在的话）。如果 index 文件不存在，将像之前一样返回所有文件列表。\r\n\r\n作用：\r\n\r\n- 给 task/rewrite 设置专门的订阅目录\r\n- 搭建临时静态网站\r\n- 小型网盘资源分享\r\n- 记录收藏常用目录\r\n\r\n### favend - backend 运行脚本\r\n\r\n作为 backend 运行的脚本默认 timeout 为 5000ms，也可以在 url 中使用参数 timeout 来修改，比如: **http://127.0.0.1/efss/body?timeout=20000**\r\n\r\n该模式下的 JS 包含 **$request** 默认变量，且应该返回如下 object:\r\n\r\n``` JS\r\nconsole.log($request)   // 查看默认变量 $request 内容（该模式下的 console.log 内容前端不可见，只能在后台看到\r\n// $request.method, $request.protocol, $request.url, $request.hostname, $request.path\r\n// $request.headers<object>, $request.body<string>\r\n\r\n// backend 特有属性 $env.key 表示访问该 backend 的关键字，$env.name 表示该 backend 名称\r\nconsole.log($env.key, $env.name, __version, 'cookieKEY:', $store.get('cookieKEY'))   // 其他默认变量/函数也可直接调用\r\n\r\n// 最终网页返回结果\r\n$done({\r\n  statusCode: 200,    // 网页状态码，其他状态码也可以。比如: 404, 301, 502 等。可省略，默认为: 200\r\n  headers: {          // 网页 response.headers 相关信息。可省略，默认为: {'Content-Type': 'text/html;charset=utf-8'}\r\n    'Content-Type': 'application/json;charset=utf-8'\r\n  },\r\n  body: {             // 网页内容。这里面的内容会直接显示到网页中\r\n    elecV2P: 'hello favend',\r\n    cookieKEY: $store.get('cookieKEY'),\r\n    request: $request,\r\n  }\r\n})\r\n\r\n// === $done({ response: { statusCode, headers, body } })\r\n\r\n// 当$done 是其他结果时，比如: $done('hello favend')\r\n// elecV2P 会把最终结果当作 body 输出，其他项使用默认参数\r\n```\r\n\r\nfavend 也接受 post/put 等请求。支持在 body 中添加临时环境变量(env)参数，比如:\r\n\r\n``` JS\r\n// 需提前在 EFSS 界面中设置好 favend 参数 envtest\r\n// 然后使用浏览器的开发者工具发送如下请求（也可以在 webUI->JSMANAGE 中模拟网络请求\r\nfetch('http://127.0.0.1/efss/envtest', {\r\n  method: 'put',\r\n  headers: {\r\n    'Content-Type': 'application/json'\r\n  },\r\n  body: JSON.stringify({\r\n    \"timeout\": 2000,\r\n    \"env\": {\r\n      \"param\": \"传递一个变量\"\r\n    }\r\n  })\r\n}).then(res=>res.text()).then(s=>console.log(s)).catch(e=>console.error(e))\r\n\r\n// envtest 对应运行的 JS 如下:\r\nconsole.log('临时环境变量', $env.param, 'favend key', $env.key, 'favend name', $env.name)\r\n\r\n// 注意: $request.body 为字符串类型，$env 为 object 类型\r\nconsole.log('request body type:', typeof $request.body, '$env type', typeof $env)\r\n\r\n$done({\r\n  body: {\r\n    'param': `通过 ${ $request.method } 获取到的 param: ${ $env.param }`,\r\n    'request': $request\r\n  }\r\n})\r\n```\r\n\r\n### elecV2P favend html(.efh)  (v3.5.4 新增文件格式)\r\n\r\n一个同时包含前后端运行代码的 html 扩展格式，也可以说是一个文件协议或标准。基础结构如下：\r\n\r\n``` HTML\r\n<div>原来的 html 格式/标签/内容</div>\r\n<script type=\"text/javascript\">\r\n  console.log('原 html 页面中的 script 标签')\r\n</script>\r\n<!-- 上面为原 html 页面，下面为扩展部分 -->\r\n<!-- <script type=\"text/javascript\" runon=\"elecV2P\"> v3.6.7 版本之前的写法 -->\r\n<script favend>\r\n  console.log('efh 文件的扩展部分')\r\n</script>\r\n```\r\n\r\n执行过程/基本原理:\r\n- 首次执行 efh 文件时，将文件分离为**前端和后台**部分，并进行缓存\r\n- 然后当收到 POST 请求时，执行**后台部分**代码，返回执行结果\r\n- 当收到其他请求时，直接返回**前端 HTML** 页面\r\n\r\n优点:\r\n- 前后端代码同一页面，方便开发者统一管理\r\n- 标签内代码高亮（最初要解决的问题\r\n- 沿用 html 语法，没有额外的学习成本\r\n\r\n#### efh 实战测试\r\n\r\n测试文件: https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/elecV2P.efh\r\n适用版本: v3.5.5\r\n\r\n在 EFSS 页面 favend 相关设置中添加新的规则，设置好名称和关键字，**类型** 选择 **运行脚本**, 目标填写 efh 文件远程或本地地址（*本地文件可在 webUI->JSMANAGE 脚本管理界面推送/上传/编辑*），然后点击保存(ctrl+s)，最后在操作栏中点击运行。\r\n\r\n#### efh 格式文件说明\r\n\r\n``` HTML\r\n<h3>一个简单的 efh 格式示例文件</h3>\r\n<div><label>请求后台数据测试</label><button onclick=\"dataFetch()\">获取</button></div>\r\n<input type=\"text\" name=\"data\" class=\"data\" placeholder=\"data\">\r\n\r\n<script type=\"text/javascript\">\r\n  // v3.7.2 增加 $ 简易选择器函数，示例：\r\n  // let data = $('.data').value;   // 等价于 document.querySelector('.data').value\r\n  // let div  = $('div', 'all');    // 等价于 document.querySelectorAll('div')\r\n\r\n  // $fend 默认函数用于前后端数据交互（本质为一个 fetch 的 post 请求\r\n  function dataFetch() {\r\n    $fend('data').then(res=>res.text())\r\n    .then(res=>{\r\n      console.log(res)\r\n      alert(res)\r\n    })\r\n    .catch(e=>{\r\n      console.error(e)\r\n      alert('error: ' + e.message)\r\n    })\r\n  }\r\n</script>\r\n<!-- 前端部分可以使用多个 script 标签\r\n使用 src 引入任意远程文件，比如：\r\n<script src=\"https://unpkg.com/vue@3/dist/vue.global.js\"></script>\r\n\r\n(v3.7.1) 如需引入 elecV2P 服务器上的本地脚本，加上 /script 前缀，比如：\r\n<script src=\"/script/webhook.js\"></script>\r\n支持多级目录，比如：\r\n<script src=\"/script/test/webhook.js\"></script>\r\n前端 src 暂不支持使用相对目录，比如 src='./webhook.js' ，会提示脚本不存在\r\n-->\r\n\r\n<!-- 后台代码仅能使用一个 script 标签，如有其他多的标签将被当作前端代码处理\r\n使用 runon=\"elecV2P\" 属性来表示此部分脚本是运行在后台的代码\r\nv3.6.7 之后可简写为 <script favend>  -->\r\n<script type=\"text/javascript\" runon=\"elecV2P\">\r\n  // 后台 $fend 第一参数需与前端对应，第二参数为返回给前端的数据\r\n  $fend('data', {\r\n    hello: 'elecV2P favend',\r\n    data: $store.get('cookieKEY'),\r\n    reqbody: $request.body\r\n  })\r\n  $done('no $fend match');\r\n</script>\r\n<!-- 后台 script 标签同样支持 src 属性，比如：\r\n<script favend src=\"https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/favend.js\"></script>\r\n\r\n后台 src 在引入 elecV2P 服务器上的本地脚本时，无需添加 /script 前缀，支持相对、绝对目录，比如：\r\n<script favend src=\"favend.js\"></script>\r\n<script favend src=\"./0body.js\"></script>\r\n// 相对目录，表示的是相对于当前 efh 文件\r\n<script favend src=\"/webhook.js\"></script>\r\n// 绝对目录，表示的是服务器上的脚本根目录\r\n```\r\n\r\n其他说明：\r\n- 无后台代码时直接返回前端 html 内容\r\n- 直接运行 efh 脚本时，返回前端 html 内容\r\n- 远程 efh 更新间隔和远程 JS 更新间隔同步\r\n- v3.5.5 增加默认 $fend 函数用于前后端数据交互（具体参考 [04-JS.md](https://github.com/elecV2/elecV2P-dei/blob/master/docs/04-JS.md) $fend 相关部分\r\n- 其他 efh 示例脚本：https://github.com/elecV2/elecV2P-dei/tree/master/examples/JSTEST/efh\r\n- 另一篇相关说明文章：https://elecv2.github.io/#efh：一种简单的%20html%20语法扩展结构\r\n\r\n待优化项：\r\n- 其他类型数据 arrayBuffer/stream 等\r\n- $fend 后台无匹配时返回结果\r\n- 默认前端内置脚本可选择是否使用远程链接\r\n\r\n优化完成：\r\n- 前后台数据的持续交互($ws.sse\r\n- efh 前端调用本地 JS/CSS/图片 等（part done\r\n- $fend key/路由 配对优化\r\n- runJS 直接运行 efh 文件\r\n- 前后台更好/优雅的传输数据($fend（done\r\n- 缓存清理(done) $fend.clear();\r\n\r\n### 说明文档列表\r\n\r\n- [overview - 简介及安装](01-overview.md)\r\n- [task - 定时任务](06-task.md)\r\n- [rewrite - 重写网络请求](05-rewrite.md)\r\n- [rules - 网络请求更改规则](03-rules.md)\r\n- [script - 脚本编写及说明](04-JS.md)\r\n- [Docker - Docker 运行相关](02-Docker.md)\r\n- [feed&notify - 通知相关](07-feed&notify.md)\r\n- [logger&efss - 日志和 EFSS 文件管理](08-logger&efss.md)\r\n- [webhook - webhook 使用简介](09-webhook.md)\r\n- [config - 配置文件说明](10-config.md)\r\n- [Advanced - 高级使用篇](Advanced.md)\r\n"
  },
  {
    "path": "docs/09-webhook.md",
    "content": "```\r\n最近更新: 2022-10-30\r\n适用版本: 3.7.4\r\n文档地址: https://github.com/elecV2/elecV2P-dei/blob/master/docs/09-webhook.md\r\n```\r\n\r\nwebhook 用于使用一个网络请求来调用 elecV2P 的部分功能\r\n\r\n## 可调用功能列表\r\n\r\n- 获取脚本列表/运行脚本\r\n- 获取/删除 日志\r\n- 获取服务器相关信息\r\n- 获取定时任务信息\r\n- 开始/暂停 定时任务\r\n- 添加/保存 定时任务\r\n- 远程下载文件到 EFSS\r\n- 执行 shell 指令\r\n- 查看/修改 store/cookie(v3.3.3)\r\n- 脚本文件获取/新增(v3.4.0)\r\n- IP 限制 黑白名单更新(v3.4.0)\r\n- 打开/关闭代理端口(v3.4.8)\r\n- 全局 CORS 设置(v3.5.4-v3.7.3, v3.7.4 后移除)\r\n- 使用根证书签发任意域名证书(v3.5.8)\r\n\r\n## 使用\r\n\r\n首先在 webUI-> SETTING/设置相关中获取 WEBHOOK TOKEN，然后可通过 GET/PUT/POST 三种请求方式触发相关功能。\r\n\r\n下面以几个简单的例子进行说明。\r\n\r\n### 运行 JS\r\n\r\nGET 方式通过 url 传递相关参数，比如运行 JS，触发的请求链接为：\r\n\r\n**http://192.168.1.102:12521/webhook?token=a8c259b2-67fe-D-7bfdf1f55cb3&type=runjs&fn=webhook.js**\r\n\r\nPUT 或者 POST 以 JSON 的方式传递相关参数, 以在浏览器在使用 fetch 函数为例\r\n\r\n``` JS webhook\r\nfetch('http://192.168.1.102:12521/webhook', {\r\n  method: 'put',     // or post\r\n  headers: {\r\n    'Content-Type': 'application/json'\r\n  },\r\n  body: JSON.stringify({\r\n    token: 'a8c259b2-67fe-D-7bfdf1f55cb3',\r\n    type: 'runjs',\r\n    fn: 'webhook.js'\r\n  })\r\n}).then(res=>res.text()).then(s=>console.log(s))\r\n\r\nfetch('/webhook', {   // 本地服务器可直接用 /webhook\r\n  method: 'post',\r\n  headers: {\r\n    'Content-Type': 'application/json'\r\n  },\r\n  body: JSON.stringify({\r\n    token: 'a8c259b2-67fe-D-7bfdf1f55cb3',\r\n    type: 'runjs',\r\n    fn: 'https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/exam-js-env.js',        // 支持远程 JS\r\n    env: {   // (v3.4.5 支持添加临时环境变量)\r\n      name: 'webhook',\r\n      cookie: '来自 webhook 的临时环境变量'\r\n    },\r\n    grant: 'nodejs',   // (v3.4.5 增加支持 grant，多个 grant 用英文竖线符(|)隔开。具体功能参考 04-JS.md @grant 相关部分)\r\n  })\r\n}).then(res=>res.text()).then(s=>console.log(s))\r\n```\r\n\r\n- 如果是远程脚本, 会强制下载脚本文件并保存\r\n- 支持使用 rename 参数，修改远程脚本下载后的文件名\r\n\r\n## body/query 参数说明\r\n\r\n|  type     |   target 目标  |    功能         |        传递参数\r\n| :-------: | -------------- | --------------- | --------------------\r\n| runjs     | fn=webhook.js  | 运行脚本        |  &type=runjs&fn=webhook.js\r\n| status    | 无 ---         | 服务器运行状态  |  &type=status\r\n| task      | 无 ---         | 获取任务列表    |  &type=task\r\n| tasksave  | 无 ---         | 保存任务列表    |  &type=tasksave\r\n| taskinfo  | tid=all or tid | 获取任务信息    |  &type=taskinfo&tid=all\r\n| taskstart | tid=xxtid      | 开始定时任务    |  &type=taskstart&tid=xxxowoxx\r\n| taskstop  | tid=xxtid      | 暂停定时任务    |  &type=taskstop&tid=xxxowoxx\r\n| getlog    | fn=xxxxxxx.log | 查看日志文件    |  &type=getlog&fn=xxxxxxx.log\r\n| deletelog | fn=file.js.log | 删除日志文件    |  &type=deletelog&fn=file.js.log\r\n| taskadd   | task: {}       | 添加定时任务    |  { type: 'taskadd', task: {}, options: {} }\r\n| download  | url=http://xxx | 下载文件到EFSS  |  &type=download&url=https://rawxxxx\r\n| shell     | command=ls     | 执行 shell 指令 |  &type=shell&command=node%20-v\r\n| info      | debug=1  可选  | 查看服务器信息  |  &type=info or &type=info&debug=true\r\n| jslist    | 无 ---         | 获取脚本列表    |  &type=jslist\r\n| store     | key=cookieKEY  | 获取 cookie 信息|  &type=store&key=cookieKEY\r\n| deljs     | fn=webhook.js  | 删除脚本文件    |  &type=deljs&fn=webhook.js\r\n| jsfile    | fn=test.js     | 获取脚本内容    |  &type=jsfile&fn=test.js\r\n| security  | op=put&enable. | 后台 IP 限制修改|  &type=security\r\n| proxyport | op=open/close  | 打开/关闭代理   |  &type=proxyport&op=open\r\n| blackreset| 无 ---         | 重置非法IP 记录 |  &type=blackreset\r\n| newcrt    | hostname=xx.xx | 签发域名证书    |  &type=newcrt&hostname=myhost.com\r\n\r\n- **每次请求时注意带上 token**\r\n- **如果使用 PUT/POST 方式，body 应为对应的 JSON 格式**\r\n- **command 指令需先使用 encodeURI 进行编码**\r\n- **shell 执行默认 timeout 为 5000ms（以防出现服务器长时间无响应的问题）**\r\n\r\n- (v3.7.1 更新) 当 type 参数缺省，或对应参数不在列表中时，可使用脚本处理 payload（即 request body）。需在 webUI->SETTING 设置界面启用 WEBHOOK SCRIPT。此种方式触发的脚本，通过 **$env.payload** 变量来获取除 token 以外的所有 body 参数\r\n\r\n- (具体使用方法，参考下面的相关示例)\r\n\r\n## 直接 GET 请求\r\n\r\n```\r\n# 获取内存使用信息\r\nhttp://192.168.1.102:12521/webhook?token=a8c259b2-67fe-D-7bfdf1f55cb3&type=status\r\n\r\n# 获取当前所有任务\r\nhttp://192.168.1.102:12521/webhook?token=a8c259b2-67fe-D-7bfdf1f55cb3&type=taskinfo&tid=all\r\n\r\n# 远程离线下载文件到 EFSS 虚拟目录\r\nhttp://192.168.1.102:12521/webhook?token=a8c259b2-67fe-D-7bfdf1f55cb3&type=download&url=https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/overview.png\r\n\r\n## 自定义下载文件保存目录和名称(v3.4.0)\r\nhttp://192.168.1.102:12521/webhook?token=a8c259b2-67fe-D-7bfdf1f55cb3&type=download&url=https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/examples/JSTEST/evui-dou.js&folder=script/JSFile&name=edou.js\r\n\r\n# 列出 script/Shell 目录下的文件\r\nhttp://192.168.1.102:12521/webhook?token=a8c259b2-67fe-D-7bfdf1f55cb3&type=shell&command=ls%20-cwd%20script/Shell\r\n\r\n## shell 使用 cwd 和 timeout 参数\r\nhttp://192.168.1.102:12521/webhook?token=a8c259b2-67fe-D-7bfdf1f55cb3&type=shell&command=ls&cwd=script/JSFile&timeout=2000\r\n\r\n# 获取 elecV2P 及服务器相关信息\r\nhttp://192.168.1.102:12521/webhook?token=a8c259b2-67fe-D-7bfdf1f55cb3&type=info&debug=true\r\n\r\n# 查看 store/cookie 信息\r\nhttp://127.0.0.1/webhook?token=a8c259b2-67fe-D-7bfdf1f55cb3&type=store&op=all          # 获取 cookie 列表\r\nhttp://127.0.0.1/webhook?token=a8c259b2-67fe-D-7bfdf1f55cb3&type=store&key=cookieKEY   # 获取某个 KEY 对应值\r\nhttp://127.0.0.1/webhook?token=a8c259b2-67fe-D-7bfdf1f55cb3&type=store&op=put&key=cookieKEY&value=webhookgetvalue   # 添加一个 cookie\r\n\r\n# 后台 IP 限制查看/修改\r\nhttp://127.0.0.1/webhook?token=a8c259b2-67fe-D-7bfdf1f55cb3&type=security              # 查看当前 SECURITY 设置\r\n\r\n## 修改后台 IP 限制。关键参数 op=put，其他修改参数 enable, blacklist, whitelist 可只设置一项，list 中多个数据用逗号(,)分开。\r\nhttp://127.0.0.1/webhook?token=a8c259b2-67fe-D-7bfdf1f55cb3&type=security&op=put&enable=true&blacklist=*&whitelist=127.0.0.1,192.168.1.1\r\n\r\n## 关闭仅开放 webhook 接口选项(v3.6.4)\r\nhttp://127.0.0.1/webhook?token=a8c259b2-67fe-D-7bfdf1f55cb3&type=security&op=put&webhook_only=false\r\n\r\n# 删除默认脚本文件夹下的所有非脚本文件(v3.4.2)\r\nhttp://127.0.0.1/webhook?token=a8c259b2-67fe-D-7bfdf1f55cb3&type=deljs&op=clear\r\n\r\n# 重置非法访问的 IP 记录(v3.5.5)\r\nhttp://127.0.0.1/webhook?token=a8c259b2-67fe-D-7bfdf1f55cb3&type=blackreset\r\n\r\n# 使用根证书签发任意域名证书(v3.5.8)\r\n# 生成的域名证书位于 当前项目/rootCA 目录下\r\nhttp://127.0.0.1/webhook?token=a8c259b2-67fe-D-7bfdf1f55cb3&type=newcrt&hostname=v2host.io\r\n```\r\n\r\n## 使用 PUT/POST 方法\r\n\r\n``` JS\r\n// 在 webUI 后台管理页面打开浏览器调试工具，输入以下代码，可不输入服务器地址\r\n// # 添加定时任务 2.4.6 更新\r\n// task 格式参考: https://github.com/elecV2/elecV2P-dei/tree/master/docs/06-task.md\r\n\r\nfetch('/webhook', {\r\n  method: 'put',     // or post\r\n  headers: {\r\n    'Content-Type': 'application/json'\r\n  },\r\n  body: JSON.stringify({\r\n    token: 'a8c259b2-67fe-D-7bfdf1f55cb3',\r\n    type: 'taskadd',\r\n    task: {\r\n      name: '新的任务-exam',\r\n      type: 'cron',\r\n      job: {\r\n        type: 'runjs',\r\n        target: 'https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/webhook.js',\r\n      },\r\n      time: '10 8 8 * * *',\r\n      running: false        // 是否自动执行添加的任务\r\n    }\r\n  })\r\n}).then(res=>res.text()).then(s=>console.log(s)).catch(e=>console.log(e))\r\n\r\n// v3.4.2 增加支持批量添加，及 options 参数\r\nfetch('/webhook', {\r\n  method: 'post',\r\n  headers: {\r\n    'Content-Type': 'application/json'\r\n  },\r\n  body: JSON.stringify({\r\n    token: 'a8c259b2-67fe-D-7bfdf1f55cb3',\r\n    type: 'taskadd',\r\n    task: [         // 以数组的格式批量添加任务\r\n      {\r\n        name: '新的任务-exam',\r\n        type: 'cron',\r\n        job: {\r\n          type: 'runjs',\r\n          target: 'https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/webhook.js',\r\n        },\r\n        time: '10 8 8 * * *'\r\n      },\r\n      {\r\n        name: 'apk安装命令',\r\n        type: 'schedule',\r\n        time: '0',\r\n        running: false,\r\n        job: {\r\n          type: 'exec',\r\n          target: 'apk add git'\r\n        }\r\n      }\r\n    ],\r\n    options: {              // v3.4.2 增加。可省略\r\n      type: 'replace',      // 当任务列表中存在同名任务时的更新方式。replace: 替换原有任务，skip: 跳过添加新任务，addition: 新增任务\r\n    }\r\n  })\r\n}).then(res=>res.text()).then(s=>console.log(s)).catch(e=>console.log(e))\r\n\r\n// v3.4.2 同时添加 taskstart/taskstop 的批量处理\r\nfetch('/webhook', {\r\n  method: 'post',\r\n  headers: {\r\n    'Content-Type': 'application/json'\r\n  },\r\n  body: JSON.stringify({\r\n    token: 'a8c259b2-67fe-D-7bfdf1f55cb3',\r\n    type: 'taskstart',   // taskstart: 开始任务 taskstop: 停止任务\r\n    tid: ['Wnj8rMaj', 'nPfq5Td3', 'cKUq3ViR'],     // 对应任务的 id\r\n  })\r\n}).then(res=>res.text()).then(res=>console.log(res)).catch(e=>console.log(e))\r\n\r\n// # 增加/修改 store/cookie (v3.3.3)\r\nfetch('/webhook', {\r\n  method: 'post',\r\n  headers: {\r\n    'Content-Type': 'application/json'\r\n  },\r\n  body: JSON.stringify({\r\n    token: 'a8c259b2-67fe-D-7bfdf1f55cb3',\r\n    type: 'store',\r\n    op: 'put',\r\n    key: 'acookiehook',   // cookie key 关键字\r\n    value: {              // cookie 保存内容。可为 string/number 等其他数据类型\r\n      hello: 'elecV2P'\r\n    },\r\n    options: {            // options 可省略\r\n      type: 'object',     // 指定 value 保存类型，可省略（如省略将根据 value 的类型进行自动判断\r\n      belong: 'webhook.js',        // 指定该 cookie 归属脚本\r\n      note: '一个从 webhook 添加测试 cookie',  // 给 cookie 添加简单备注\r\n    }\r\n  })\r\n}).then(res=>res.text()).then(res=>console.log(res)).catch(e=>console.log(e))\r\n\r\n// 在服务器中新增一个脚本文件(v3.4.0)\r\nfetch('http://192.168.1.3/webhook', {\r\n  method: 'post',\r\n  headers: {\r\n    'Content-Type': 'application/json'\r\n  },\r\n  body: JSON.stringify({\r\n    token: 'a8c259b2-67fe-D-7bfdf1f55cb3',\r\n    type: 'jsfile',\r\n    op: 'put',\r\n    fn: 'awbnew.js',      //脚本文件名\r\n    rawcode: `//脚本文件内容\r\nconsole.log('一个通过 webhook 新添加的文件')`\r\n  })\r\n}).then(res=>res.text()).then(res=>console.log(res)).catch(e=>console.log(e))\r\n\r\n// 更改可访问后台管理页面的 IP(v3.4.0)\r\n// enable, blacklist, whitelist 可只设置其他一项，其他项会自动保持原有参数\r\nfetch('http://172.20.10.1/webhook', {\r\n  method: 'post',\r\n  headers: {\r\n    'Content-Type': 'application/json'\r\n  },\r\n  body: JSON.stringify({\r\n    token: 'a8c259b2-67fe-D-7bfdf1f55cb3',\r\n    type: 'security',\r\n    op: 'put',\r\n    enable: false,\r\n    blacklist: ['*'],\r\n    whitelist: ['127.0.0.1', '172.20.10.1']\r\n  })\r\n}).then(res=>res.text()).then(res=>console.log(res)).catch(e=>console.log(e))\r\n\r\n// 批量删除脚本(v3.4.2)\r\nfetch('/webhook', {\r\n  method: 'post',\r\n  headers: {\r\n    'Content-Type': 'application/json'\r\n  },\r\n  body: JSON.stringify({\r\n    token: 'a8c259b2-67fe-D-7bfdf1f55cb3',\r\n    type: 'deletejs',   // 同等于: deljs === jsdel === jsdelete\r\n    // op: 'clear',     // 启用此项，表示删除默认脚本文件夹下的所有非脚本文件\r\n    fn: ['test/starturl.js', 'test/restart.js', '0body.js', 'test.js']   // 使用数组表示多个要删除的脚本文件\r\n  })\r\n}).then(res=>res.text()).then(res=>console.log(res)).catch(e=>console.log(e))\r\n\r\n// 打开代理端口（默认为 8001\r\nfetch('/webhook', {\r\n  method: 'post',\r\n  headers: {\r\n    'Content-Type': 'application/json'\r\n  },\r\n  body: JSON.stringify({\r\n    token: 'a8c259b2-67fe-D-7bfdf1f55cb3',\r\n    type: 'proxyport',\r\n    op: 'open',     // 仅当该值为 open 时，表示打开。\r\n  })\r\n}).then(res=>res.text()).then(res=>console.log(res)).catch(e=>console.log(e))\r\n\r\n// type 参数缺省，或对应参数不在列表中时，可使用脚本来处理 payload (v3.7.1)\r\nfetch('/webhook?token=a8c259b2-67fe-D-7bfdf1f55cb3', {\r\n  method: 'post',\r\n  headers: {\r\n    'Content-Type': 'application/json'\r\n  },\r\n  body: JSON.stringify({\r\n    // type: 'unknow', // type 缺省\r\n    version: 14,       // payload(即 body 内容) 为其他未知值\r\n    some: 'value',\r\n  })\r\n}).then(res=>res.text()).then(res=>console.log(res)).catch(e=>console.log(e))\r\n\r\n// 在 webUI SETTING 设置好 WEBHOOK SCRIPT 相关内容后，可在对应脚本中使用 $env.payload 来获取任意网络请求的 payload\r\n// 假如设置对应的脚本为 webhook.js ，webhook.js 的内容如下：\r\n// console.log('获取到的 payload', $env.payload);$done($env.payload);\r\n```\r\n\r\n- 假如 elecV2P 可远程访问，可以使用使用其他任意程序发送网络请求进行调用\r\n- webhook 可配合 **telegram bot** 或 **快捷指令** 等其他工具使用，方便快速调用 elecV2P 相关功能\r\n- 通过 webhook 提供的 API，可以自行设计其他 UI 界面，实现与 elecV2P 交互\r\n- v3.5.8 在脚本中增加函数 $webhook(type, options) ，详见 https://github.com/elecV2/elecV2P-dei/blob/master/docs/04-JS.md 相关说明\r\n\r\n### 说明文档列表\r\n\r\n- [overview - 简介及安装](01-overview.md)\r\n- [task - 定时任务](06-task.md)\r\n- [rewrite - 重写网络请求](05-rewrite.md)\r\n- [rules - 网络请求更改规则](03-rules.md)\r\n- [script - 脚本编写及说明](04-JS.md)\r\n- [Docker - Docker 运行相关](02-Docker.md)\r\n- [feed&notify - 通知相关](07-feed&notify.md)\r\n- [logger&efss - 日志和 EFSS 文件管理](08-logger&efss.md)\r\n- [webhook - webhook 使用简介](09-webhook.md)\r\n- [config - 配置文件说明](10-config.md)\r\n- [Advanced - 高级使用篇](Advanced.md)\r\n"
  },
  {
    "path": "docs/10-config.md",
    "content": "```\n最近更新: 2022-11-06\n适用版本: 3.7.5\n文档地址: https://github.com/elecV2/elecV2P-dei/blob/master/docs/10-config.md\n```\n\n## 配置文件说明\n\nelecV2P 配置文件默认保存目录为 **./script/Lists/config.json**。\n\n**请尽量在 webUI->SETTING/设置相关 界面进行修改，而不是手动编辑。**\n\n## 内容\n\n*实际配置文件为严格的 JSON 格式，不包含任何注释。以下注释仅为说明，非注释版本查看 [config.json 示例文件](https://raw.githubusercontent.com/elecV2/elecV2P/master/script/Lists/config.json)*\n\n``` JSON\n{\n  \"anyproxy\": {                // anyproxy 相关设置。用于 MITM\n    \"enable\": false,           // 是否启用。默认 false 关闭\n    \"port\": 8001,              // anyproxy 代理端口\n    \"webPort\": 8002            // anyproxy 网络请求查看端口\n  },\n  \"CONFIG_FEED\": {             // 通知相关设置\n    \"enable\": true,            // 是否开启默认通知\n    \"rss\": {\n      \"enable\": true,          // 是否将通知输出为 feed/rss\n      \"homepage\": \"\"           // feed/rss 主页。同最外层 homepage 参数\n    },\n    \"iftttid\": {\n      \"enable\": false,         // 是否启用 IFTTT 通知\n      \"key\": \"\"                // IFTTT 对应 key 值\n    },\n    \"barkkey\": {\n      \"enable\": false,         // 是否启用 BARK 通知\n      \"key\": \"\"\n    },\n    \"custnotify\": {\n      \"enable\": false,         // 是否启用自定义通知\n      \"url\": \"\",               // 自定义通知 URL\n      \"type\": \"GET\",           // 自定义通知内容发送方式\n      \"data\": \"\"               // 自定义通知内容\n    },\n    \"runjs\": {\n      \"enable\": false,         // 通知时触发脚本，即通过自定义脚本发送通知\n      \"list\": \"notify.js\"      // 通知时运行脚本\n    },\n    \"merge\": {\n      \"enable\": true,          // 是否合并默认通知\n      \"gaptime\": 60,           // 合并该时间段内通知。单位：秒\n      \"number\": 10,            // 合并通知条数\n      \"andor\": false           // 时间段和通知条数合并逻辑。true: 同时满足，false: 满足任一\n    },\n    \"maxbLength\": 1200,        // 最大通知内容长度。超过后将分段发送\n    \"webmessage\": {\n      \"enable\": true           // 是否在 webUI 前端显示通知\n    }\n  },\n  \"CONFIG_RUNJS\": {            // 脚本运行相关设置\n    \"timeout\": 5000,           // 脚本运行时间。单位：毫秒 0 表示不设定超时时间\n    \"intervals\": 86400,        // 远程脚本最低更新时间间隔，单位：秒。 默认：86400(一天)。0 表示有则不更新\n    \"numtofeed\": 50,           // 每运行 { numtofeed } 次脚本, 发送一个默认通知。0 表示不通知\n    \"jslogfile\": true,         // 是否保存脚本运行日志\n    \"eaxioslog\": false,        // 是否保存网络请求 url 到日志中\n    \"proxy\": true,             // 是否应用网络请求相关设置中的代理（如有）\n    \"white\": {\n      \"enable\": false,         // 是否启用白名单脚本。放行脚本内所有网络请求\n      \"list\": [\"softupdate.js\"]\n    }\n  },\n  \"CONFIG_Axios\": {            // 网络请求相关设置\n    \"proxy\": {\n      \"enable\": false,         // 是否使用 http 代理\n      \"host\": \"\",              // 代理服务器\n      \"port\": 8001             // 代理端口\n    },\n    \"timeout\": 5000,           // 网络请求超时时间。单位：毫秒\n    \"uagent\": \"iPhone\",        // 默认 User-Agent，相关列表位于 script/Lists/useragent.list\n    \"block\": {\n      \"enable\": false,         // 是否阻止部分网络请求。匹配方式 new RegExp('regexp').test(url)\n      \"regexp\": \"\"\n    },\n    \"only\": {\n      \"enable\": false,         // 当前启用时，表示仅允许符合该规则的 url 通过\n      \"regexp\": \"\"\n    },\n    \"reject_unauthorized\": true       // process.env['NODE_TLS_REJECT_UNAUTHORIZED'] 设置。仅当为 false 时，对应值为 0。v3.7.4 增加\n  },\n  \"eapp\": {                    // EAPP 相关设置。详见 https://github.com/elecV2/elecV2P-dei/blob/master/docs/dev_note/webUI%20首页快捷运行程序%20eapp.md\n    \"enable\": true,            // EAPP 是否在首页显示\n    \"logo_type\": 1,            // EAPP 默认图标风格\n    \"apps\": [{                 // EAPP 列表\n      \"name\": \"EAPP 名称\",\n      \"type\": \"js\",            // EAPP 类型。共 js|efh|shell|url|eval 五种\n      \"target\": \"efss/efh\",    // EAPP 最终执行内容\n      \"hash\": \"xxxxx\",         // EAPP hash 值，自动生成，手动设置无效\n    }, {\n      \"name\": \"PM2LS\",\n      \"type\": \"shell\",\n      \"target\": \"pm2 ls\",\n      \"run\": \"auto\",           // 首次加载时运行方式。仅当为 auto 时表示自动运行一次（v3.7.3 增加\n      \"note\": \"显示 PM2 信息\"  // EAPP 备注信息（v3.7.3 增加\n    }]\n  },\n  \"efss\": {                    // EFSS 相关设置\n    \"enable\": true,            // 是否启用\n    \"directory\": \"./efss\",     // EFSS 对应文件夹\n    \"dotshow\": {\n      \"enable\": false          // 是否显示以点(.) 开头的文件\n    },\n    \"max\": 600,                // 最大显示文件数\n    \"skip\": {\n      \"folder\": [              // 跳过显示部分文件夹内文件\n        \"node_modules\"\n      ],\n      \"file\": []               // 路过显示部分文件\n    },\n    \"favend\": {\n      \"efh\": {\n        \"key\": \"efh\",          // favend 访问关键字。efss/efh\n        \"name\": \"efh 初版\",\n        \"type\": \"runjs\",       // favend 类型。runjs 执行脚本|favorite 收藏目录\n        \"target\": \"elecV2P.efh\",\n        \"enable\": true         // 是否启用\n      },\n      \"logs\": {\n        \"key\": \"logs\",\n        \"name\": \"查看日志\",\n        \"type\": \"favorite\",    // favorite 列出 target 目录下的所有文件\n        \"target\": \"logs\",\n        \"enable\": true\n      }\n    },\n    \"favendtimeout\": 5000      // favend 脚本运行超时时间\n  },\n  \"env\": {                     // 环境变量相关设置（在 elecV2P 启动后自动添加\n    \"path\": \"\",                // 该项内容会和 process.env.PATH 合并，并自动更新\n    \"acookie\": \"myappcookie\"   // 其他环境变量会自动添加，process.env.acookie = \"myappcookie\"\n  },\n  \"gloglevel\": \"info\",         // 后台日志显示等级。可选值 error|notify|info|debug，默认 info\n  \"glogslicebegin\": 5,         // 日志时间显示格式。0: 默认，5: 不显示年份，11: 不显示年月日\n  \"homepage\": \"http://127.0.0.1\",      // 主页地址。用于 RSS 订阅及脚本中的 __home 参数\n  \"init\": {\n    \"checkupdate\": false,      // elecV2P 启动时，是否检测更新。默认 true\n    \"runjsenable\": true,       // elecV2P 启动时，是否自动运行脚本（v3.7.4 增加）仅在为 false 时，表示不运行\n    \"runjs\": \"\"                // elecV2P 启动时，自动运行脚本。多个脚本使用逗号(,)隔开，比如 test.js, 0body.js\n  },\n  \"lang\": \"zh-CN\",             // 语言偏好。zh-CN 中文|en 英文（多语言翻译龟速进行中... \n  \"minishell\": true,           // 是否开启 minishell。默认 false 关闭\n  \"SECURITY\": {                // 安全相关设置\n    \"enable\": false,           // 默认不开启（建议在首次打开 webUI 后手动启用\n    \"blacklist\": [             // 禁止访问的 IP 列表。* 表示禁止所有访问（除了下面的 whitelist\n      \"*\"\n    ],\n    \"whitelist\": [             // 允许访问的 IP 列表。优先级高于 blacklist\n      \"127.0.0.1\",\n      \"::1\"\n    ],\n    \"cookie\": {                // 是否允许通过 cookie 访问。仅当 enable 对应值为 false 时表示不允许\n      \"enable\": true\n    },\n    \"tokens\": {                // 临时访问 token，可限制访问路径（v3.7.4 增加\n      \"md5(token)\": {          // 临时 token 的 MD5 hash 对应值（启动后会进行自动修复\n        \"enable\": true,        // 是否启用该临时 token\n        \"token\": \"xxxxx\",      // 访问 token。比如：efss/hi?token=xxxx, /logs?token=xxxxx\n        \"path\": \"^/(efss|logs?)\",       // 限制可访问的路径。匹配方式 new RegExp(path, 'i').test(req.path)。留空表示不限制\n        \"note\": \"给 xxx 的\",   // 备注说明\n        \"times\": 0             // 授权访问次数统计\n      },\n      \"token2\": {              // 支持设置多个临时访问 token。直接删除即可取消授权\n        \"enable\": true,\n        \"token\": \"xxxxxx\",     // 首次访问成功后，会生成一个有效期为 7 天的 cookie（增加 &cookie=long 有效期为 365 天\n        \"path\": \"/efss/hi\"     // 生成的 cookie 可访问路径同样受此参数限制\n      }\n    },\n    \"numtofeed\": 1,            // 有几次非法访问时出一个默认通知。0: 表示不通知\n    \"webhook_only\": false      // 仅允许 webhook 接口访问\n  },\n  \"TZ\": \"Asia/Shanghai\",       // 时区设置。将会赋值到 process.env.TZ\n  \"update_check\": false,       // 是否检测更新\n  \"update_check_gap\": 0,       // 检测更新最低时间间隔。单位 ms。默认 1000*60*30（30 分钟\n  \"wbrtoken\": \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",         // webhook token，uuid 格式（建议）。当省略时自动生成。在启动时可通过 env.TOKEN 赋值\n  \"webhook\": {\n    \"script\": {                // 当 webhook type 类型无匹配时的最终处理脚本\n      \"enable\": false,         // 更多说明参考 https://github.com/elecV2/elecV2P-dei/blob/master/docs/09-webhook.md\n      \"target\": \"webhook.js\"\n    },\n    \"token\": \"xxxxxxxx-xxx\"    // 当前 wbrtoken 缺省时，会使用该值代替(v3.7.4 添加\n  },\n  \"webUI\": {                   // webUI 主界面相关设置\n    \"port\": 80,                // webUI 运行端口\n    \"tls\": {\n      \"enable\": false,         // 是否开启 TLS 访问。默认 false 不启用。启用时，建议端口使用 443\n      \"host\": \"e.dev\"          // 自签 TLS 证书域名\n    },\n    \"logo\": {\n      \"enable\": true,          // 是否使用自定义 LOGO\n      \"src\": \"//x.xx/x.png\",   // LOGO 替换。当该 LOGO 加载失败时，将生成默认 LOGO 头像\n      \"name\": \"elecV2\"         // LOGO 旁边显示的文字\n    },\n    \"nav\": {                   // webUI 导航相关设置\n      \"overview\": {\n        \"show\": true,          // 是否显示该导航条目\n        \"name\": \"基础信息\"     // 具体显示的导航文字\n      },\n      \"task\": {\n        \"show\": true,\n        \"name\": \"定时任务\"\n      },\n      \"mitm\": {\n        \"show\": false\n      },\n      \"rules\": {\n        \"show\": false\n      },\n      \"rewrite\": {\n        \"show\": true,\n        \"name\": \"重写请求\"\n      },\n      \"jsmanage\": {\n        \"show\": true,\n        \"name\": \"脚本管理\"\n      },\n      \"setting\": {\n        \"show\": true,          // SETTING/设置 导航项默认不能关闭\n        \"name\": \"设置相关\"\n      },\n      \"cfilter\": {\n        \"show\": false,\n        \"name\": \"\"\n      },\n      \"about\": {\n        \"show\": false,\n        \"name\": \"简介说明\"\n      },\n      \"donation\": {\n        \"show\": true,\n        \"name\": \"赞助打赏\"\n      }\n    },\n    \"theme\": {                 // webUI 自定义主题（测试功能，部分用户可用\n      \"simple\": {              // 当前应用主题\n        \"enable\": false,       // 是否启用\n        \"name\": \"椰树背景\",    // 主题名称\n        \"mainbk\": \"#2E3784\",   // 主要背景色彩\n        \"maincl\": \"#6E77FB\",   // 主要文字色彩\n        \"appbk\": \"url(https://images.unsplash.com/photo-1649256651398-46408de2f095?auto=format&fit=crop&w=1964)\",    // 背景\n        \"style\": \".eapp_item .eapp_name {color: var(--main-fc);}\"       // 其他附加样式\n      },\n      \"list\": [{               // 保存的主题列表。对应值为上面 simple 中除 enable 外的其他值\n      }]\n    }\n  },\n  // 规则/脚本/常量等个人数据保存目录（v3.7.4 增加）\n  // 对应值应为某个文件夹。当不存在时，将自动生成\n  // 支持相对路径和绝对路径 path.resolve(__dirname, path_lists || 'script/Lists')\n  \"path_lists\": \"myLists\",     // 规则、定时任务等保存文件夹\n  \"path_script\": \"/elecv2p/script\",        // 脚本文件保存路径\n  \"path_store\": \"E:\\\\elecV2P\\\\Store\",      // store/cookie 常量保存目录\n  \"path_shell\": \"./efss/myShell\",          // shell 指令默认执行目录\n\n  // 以下为 elecV2P 启动后自动生成的值，预先设置无效（v3.7.4 之后被全部移除\n  \"path\": \"/xx/config.json\",   // 当前配置文件保存路径。path.join('./script/Lists/config.json')\n  \"start\": 1666488300114,      // 当前 elecV2P 启动时间。Date.now()\n  \"userid\": \"md5hash\",         // 用户 ID。对应值为 md5(webhook token)\n  \"version\": \"3.7.3\",          // 当前版本。require('./package.json').version\n  \"vernum\": 373,               // 当前版本的数字表达。Number(version.replace(/\\D/g, ''))\n  \"newversion\": \"3.7.4\"        // 检测到的新版本（如果存在的话\n}\n```\n\n## 其他说明\n\n配置文件可在启动时通过环境变量(env) **CONFIG** 来指定更改，最终路径为 path.resolve('script/Lists', process.env.CONFIG || 'config.json')。比如 **set CONFIG=123.json&&node index.js**, 则最终配置文件为 **xxx/script/Lists/123.json**。支持绝对路径，比如 **CONFIG=/elecV2P/config.json node index.js**，则最终配置文件为 **/elecV2P/config.json**。\n\n### 启动时环境变量\n\n在启动时使用以下环境变量(ENV)，可增加或覆盖配置文件中的相关值。\n\n- CONFIG : 指定配置文件路径\n- PORT : webUI 使用端口\n- PROXYEN : 启动时打开代理 anyproxy enable（v3.7.5 增加）\n- TOKEN : webhook token，对应配置文件中的 wbrtoken 项\n- TZ : 时区设置，默认 Asia/Shanghai\n\n### 说明文档列表\n\n- [overview - 简介及安装](01-overview.md)\n- [task - 定时任务](06-task.md)\n- [rewrite - 重写网络请求](05-rewrite.md)\n- [rules - 网络请求更改规则](03-rules.md)\n- [script - 脚本编写及说明](04-JS.md)\n- [Docker - Docker 运行相关](02-Docker.md)\n- [feed&notify - 通知相关](07-feed&notify.md)\n- [logger&efss - 日志和 EFSS 文件管理](08-logger&efss.md)\n- [webhook - webhook 使用简介](09-webhook.md)\n- [config - 配置文件说明](10-config.md)\n- [Advanced - 高级使用篇](Advanced.md)\n"
  },
  {
    "path": "docs/Advanced.md",
    "content": "```\r\n最近更新: 2023-03-22\r\n适用版本: 3.7.8\r\n文档地址: https://github.com/elecV2/elecV2P-dei/blob/master/docs/Advanced.md\r\n```\r\n\r\n## elecV2P 进阶使用篇\r\n\r\n# 安全访问相关设置\r\n\r\n位于 webUI->SETTING/设置相关 页面\r\n\r\n![limitip](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/security.png)\r\n\r\n- 默认处于关闭状态，所有 IP 可访问\r\n- 该限制仅对 webUI 端口（默认 80）有效，对 8001/8002 对应端口无效\r\n- 白名单的优先级高于黑名单。比如，当同个 IP 同时出现在白名单和黑名单中时，以白名单为准，即: 可访问\r\n- IP 以换行符或英文逗号(,)作为分隔，保存实时生效\r\n- 在黑名单中可用单个星号字符(\\*)表示屏蔽所有不在白名单中的 IP，建议在公网部署的情况下使用\r\n- 设置**仅开放 webhook 接口**后，可通过 webhook?token=xx&type=security&op=put&webhook_only=0 来关闭\r\n\r\nIP 屏蔽后，可通过在请求链接中添加 **?token=webhook token** 的参数来绕过屏蔽，例如: http://你的服务器地址/?token=a8c259b2-67fe-4c64-8700-7bfdf1f55cb3 (服务器的 WEBHOOK TOKEN，首次启动时为随机值)\r\n\r\n- 首次通过 token 访问时会在浏览器端生成一个 cookie，之后访问时不再需要 token（v3.5.1）\r\n- cookie 默认有效期 7 天，在后面添加 ?token=xxx&cookie=long，有效期为 365 天\r\n- 如果不想留下 cookie，请使用无痕模式（在使用他人或公共设备时\r\n- 访问时后面添加 ?cookie=clear 删除当前设备的授权信息（v3.6.4）\r\n- 在设置中取消 **允许 cookie 授权访问** 后，所有 cookie 将不可访问（v3.6.6）\r\n\r\n## 临时访问 TOKEN（v3.7.4 增加\r\n\r\n增加临时可访问 token，可限制访问路径。作用：\r\n\r\n- EFSS 临时文件分享\r\n- 部分脚本、日志临时分享\r\n- 非安全网络下临时访问\r\n- 其他\r\n\r\n![temp_token](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/temp_token.png)\r\n\r\n1. 访问 token。使用示例：efss/hi?token=xxxx, /logs?token=xxxxx\r\n2. 限制可访问的路径。匹配方式 new RegExp(path, 'i').test(req.path)。留空表示不限制\r\n3. 备注信息\r\n4. 授权访问次数统计\r\n\r\n注意事项：\r\n\r\n- 当设置和 webhook token 相同值时，会自动舍弃该临时 TOKEN\r\n- 当设置为空值时，会自动舍弃\r\n- 当临时 token 有相同项时，仅保留最后一项\r\n- 临时访问 token 同样会生成 cookie\r\n  - cookie 有效期同上面的 webhook token（7 天或 365 天）\r\n  - cookie 可访问路径同对应 token 的可访问路径\r\n\r\n### 安全访问检测逻辑\r\n\r\n1. 检测安全访问是否开启\r\n  - 当没有开启时，允许所有请求\r\n  - 当开启时，进入下一步\r\n  - */favicon.ico 不进行安全检测*\r\n2. 检测是否启用 webhook_only 选项（仅开放 webhook 接口\r\n  - 当启用时，仅允许 /webhook 请求，其他请求返回 403 无授权访问提示信息\r\n  - 当没有启用时，进入下一步\r\n3. cookie 检测\r\n  - 检测通过，允许请求\r\n  - 检测失败，进入下一步\r\n4. token 检测\r\n  - 检测通过，允许请求。并将设置一个有效期为 7 天或 365 天的 cookie\r\n  - 检测失败，进入下一步\r\n5. IP 检测\r\n  - 检测通过，允许请求（不会设置 cookie 信息\r\n  - 检测失败，返回 403 无授权访问提示信息\r\n\r\n# minishell\r\n\r\n小型 shell 网页客户端，可执行一些简单的 shell 命令。比如: **ls**、 **python3 -V**、 **rm -rf \\***、 **reboot** 等\r\n\r\n![minishell](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/minishell.png)\r\n\r\n## 开启方式\r\n\r\n- v3.4.4 之后使用 webhook 快速开启关闭\r\n\r\n```\r\n// 查看当前 minishell 状态\r\nhttp://127.0.0.1/webhook?token=xxxxbbff-1043-XXXX-XXXX-xxxxxxdfa05&type=devdebug&get=minishell\r\n\r\n// 打开\r\nhttp://127.0.0.1/webhook?token=xxxxbbff-1043-XXXX-XXXX-xxxxxxdfa05&type=devdebug&get=minishell&op=open\r\n\r\n// 关闭\r\nhttp://127.0.0.1/webhook?token=xxxxbbff-1043-XXXX-XXXX-xxxxxxdfa05&type=devdebug&get=minishell&op=close\r\n```\r\n\r\n- v3.4.4 之前的打开方式\r\n\r\n方法一: 在配置文件中添加如下参数，然后重启 elecV2P。\r\n\r\n``` JSON\r\n{\r\n  // ... 其他保持不变\r\n  \"minishell\": true\r\n}\r\n```\r\n\r\n方法二: 在 webUI 页面，打开浏览器开发者工具，在 Console 中执行以下代码，然后刷新页面。\r\n\r\n``` JS\r\nfetch('/config', {\r\n  method: 'put',\r\n  headers: {\r\n    'Content-Type': 'application/json'\r\n  },\r\n  body: JSON.stringify({\r\n    \"type\": \"config\",\r\n    \"data\": {\r\n      \"minishell\": true\r\n    }\r\n  })\r\n}).then(res=>res.text()).then(s=>console.log(s))\r\n```\r\n\r\n再打开 webUI，在 SETTING/设置相关 界面的右上角有一个蓝黑的小圈，点击即可打开 **minishell** 交互台。\r\n\r\n## 基础使用\r\n\r\n- minishell 的通信基于 websocket，确保执行任何命令前 websocket 是成功连接的\r\n- minishell 的执行基于 nodejs 的 **[child_process exec](https://nodejs.org/api/child_process.html)**\r\n- minishell 命令执行默认 timeout 为 60000ms(1 分钟)。可在结尾增加 -timeout=0 进行调整\r\n\r\nelecV2P 对一些简单的命令会自动进行跨平台转换。比如，在 windows 平台输入 **ls** 命令，会自动转化为 **dir**。**reboot** 自动转化为 **restart-computer** 等。\r\n更多跨平台命令自动转换持续添加中，欢迎反馈。\r\n\r\n另外，如果指令中包含 http 链接，将会自动下载后再执行，比如命令:\r\n\r\n``` sh\r\npython3 -u https://raw.githubusercontent.com/elecV2/elecV2P/master/script/Shell/test.py\r\n# 通过这种方式可以实现直接执行远程脚本\r\n# 部分常用网络命令已排除下载，比如: curl/wget/git/start/you-get/youtube-dl 等\r\n# 更多说明，可参考 06-task.md Shell 指令 部分\r\n```\r\n\r\n*如果在 windows 平台出现乱码，尝试执行命令: CHCP 65001*\r\n\r\n## 特殊指令\r\n\r\n- cls/clear   // 清空屏幕日志\r\n- cwd         // 获取当前工作目录\r\n- cd xxx      // 更改当前工作目录到xxx\r\n- docs        // 打开此 minishell 说明页面(v3.4.7)\r\n- exit        // 最小化 minishell 界面（在子进程交互中输入时表示结束子进程\r\n- run         // RUN 运行指令（v3.6.7 添加\r\n\r\n### 快捷按键\r\n\r\n- esc         // 清空当前输入命令\r\n- ctrl + l    // 清空屏幕日志\r\n- ctrl + a    // 移动光标到命令开始处\r\n- up/down     // 上下查找历史执行命令\r\n- shift + tab // 移动光标到子进程交互输入框（如果存在的话\r\n- 单击上方日志输出部分，停止自动滚动。单击下方命令输入部分，开启自动滚动\r\n\r\n# 根据 MITM HOST 列表自动生成 PAC 文件\r\n\r\n## 简介\r\n\r\nPAC 文件链接: webUI/pac 。 比如 http://127.0.0.1/pac 或者 https://xx.xxx(你的webUI地址)/pac\r\n\r\n代理地址指的是其他设备可以访问到的 ANYPROXY 代理地址及端口，如果 elecV2P 部署在本地，那么可能是 127/172/192/10 等开头的 IP 地址，比如 192.168.1.101:8101。 如果 elecV2P 部署在远程服务器上，那么就应该是一个远程 IP 地址加 ANYPROXY 对应端口。\r\n\r\nPS: 可填写多个代理。比如 \"127.0.0.1:8001; PROXY 1.2.3.4:5678; DIRECT\"，以上内容表示当第一个代理不可用时，使用第二个代理（后面还可以接多个），都不可用时使用直连(DIRECT)。\r\n*注意：第一项可不填写 PROXY 字符，后面的代理必须用分号(;)隔开，且带上 PROXY 字符。*\r\n\r\n未匹配到(NON-MATCHED)的网络请求（不需要 MITM），默认使用直连(DIRECT)，也可以设置使用其他代理(v3.7.8)。比如填写：\"127.0.0.1:7890; DIRECT\"，表示默认使用代理，当代理不可用时直连。\r\n\r\n*更多关于 PAC 的说明，参考 MDN 文档： [代理自动配置文件（PAC）文件](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Proxy_servers_and_tunneling/Proxy_Auto-Configuration_PAC_file)*\r\n\r\n## 使用\r\n\r\n在使用设备的代理设置部分，选择 PAC 自动代理，填写上面的 PAC 文件链接。该链接对应内容为一个根据当前 elecV2P MITM HOST 列表自动生成的 PAC 文件。\r\n\r\n### 提醒\r\n\r\n- 如果 webUI 开启了安全访问，在填写 PAC 链接时注意带上 token，建议设置临时 token 进行访问。比如 http://192.168.1.101/pac?token=1234\r\n- 如果更新 mitmhost 列表后 PAC 文件没有更新，建议在 PAC 链接后添加任意参数进行缓存更新。比如 http://192.168.1.1/pac?token=1234&update=154\r\n- 如果设置 PAC 后无法联网，请确认 PAC 默认代理地址及端口是否填写正确，以及 elecV2P 的 MITM 功能是否开启\r\n\r\n# 其他进阶操作\r\n\r\n- 开启 anyproxy 代理 websocket 请求: 在 script/Lists/config.json 中 anyproxy 部分添加\r\n\r\n``` JSON\r\n{\r\n  \"anyproxy\": {\r\n    // ... 前面保存不变,\r\n    \"wsIntercept\": true\r\n  }\r\n}\r\n```\r\n\r\n- 查看 ANYPROXY 当前所启用的规则\r\n\r\nhttp://127.0.0.1/webhook?token=xxxxbbff-1043-XXXX-XXXX-xxxxxxdfa05&type=devdebug&get=rule\r\n\r\n- 查看当前连接客户端简易信息(v3.5.0)\r\n\r\nhttp://127.0.0.1/webhook?token=xxxxbbff-1043-XXXX-XXXX-xxxxxxdfa05&type=devdebug&get=wsclient\r\n\r\n### 说明文档列表\r\n\r\n- [overview - 简介及安装](01-overview.md)\r\n- [task - 定时任务](06-task.md)\r\n- [rewrite - 重写网络请求](05-rewrite.md)\r\n- [rules - 网络请求更改规则](03-rules.md)\r\n- [script - 脚本编写及说明](04-JS.md)\r\n- [Docker - Docker 运行相关](02-Docker.md)\r\n- [feed&notify - 通知相关](07-feed&notify.md)\r\n- [logger&efss - 日志和 EFSS 文件管理](08-logger&efss.md)\r\n- [webhook - webhook 使用简介](09-webhook.md)\r\n- [config - 配置文件说明](10-config.md)\r\n- [Advanced - 高级使用篇](Advanced.md)\r\n"
  },
  {
    "path": "docs/Readme.md",
    "content": "## [elecV2P](https://github.com/elecV2/elecV2P) 使用说明\n\n*可直接跳转到想了解的部分，不必按顺序查看*\n\n1. [overview - 简介及基础安装](https://github.com/elecV2/elecV2P-dei/tree/master/docs/01-overview.md)\n2. [Docker - Docker 运行相关](https://github.com/elecV2/elecV2P-dei/tree/master/docs/02-Docker.md)\n3. [rules - 网络请求更改规则](https://github.com/elecV2/elecV2P-dei/tree/master/docs/03-rules.md)\n4. [JS - 脚本编写及说明](https://github.com/elecV2/elecV2P-dei/tree/master/docs/04-JS.md)\n5. [rewrite - 重写网络请求](https://github.com/elecV2/elecV2P-dei/tree/master/docs/05-rewrite.md)\n6. [task - 定时任务](https://github.com/elecV2/elecV2P-dei/tree/master/docs/06-task.md)\n7. [feed&notify - 通知相关](https://github.com/elecV2/elecV2P-dei/tree/master/docs/07-feed&notify.md)\n8. [logger&efss - 日志和 EFSS 文件管理](https://github.com/elecV2/elecV2P-dei/tree/master/docs/08-logger&efss.md)\n9. [webhook - webhook 使用简介](https://github.com/elecV2/elecV2P-dei/tree/master/docs/09-webhook.md)\n10. [Advanced - 高级篇（开启 minishell 等）](https://github.com/elecV2/elecV2P-dei/tree/master/docs/Advanced.md)\n\n### 两篇使用教程\n\n- [elecV2P 基础使用之定时运行脚本](https://elecv2.github.io/#elecV2P%20%E5%9F%BA%E7%A1%80%E4%BD%BF%E7%94%A8%E4%B9%8B%E5%AE%9A%E6%97%B6%E8%BF%90%E8%A1%8C%20JS)\n- [elecV2P 进阶使用之抓包及 COOKIE 获取](https://elecv2.github.io/#elecV2P%20%E8%BF%9B%E9%98%B6%E4%BD%BF%E7%94%A8%E4%B9%8B%E6%8A%93%E5%8C%85%E5%8F%8A%20COOKIE%20%E8%8E%B7%E5%8F%96)"
  },
  {
    "path": "docs/dev_note/archive/favend JS 重构-efh.md",
    "content": "```\n近期更新: 2021-12-09 22:10\n```\n\n### 原因\n\nelecV2P favend 中的 JS 可能包含 html 部分，直接使用 JS 进行插入，不够优雅。主要是在写的时候，代码编译器默认是 JS 高亮模式，而 html 部分只能通过注释的方式编写，失去了高亮/补齐等特性。于是想可以通过类似 vue 或 react 的结构来设计此部分代码。\n\n比如，html 部分使用 <template></template> 标签进行包裹，JS/api 返回部分使用 <script></script> 标签。当然具体重构的时候可以使用其他 elecV2P 专用标签，细节问题之后再考虑。该方式的主要问题是，此类 JS 文件是不能直接运行的，比如 .vue 文件需要转化才能执行。在 elecV2P 中这类类似 JS 的文件可以叫做，.euv/.evu/.evs/.ejs 等，同样此类细节问题之后再考虑，假如暂时命名为 .evs ，那么 evs 文件必须满足的一点是：可直接运行，至少是在 node 环境下可直接执行（因为 elecV2P 的 favend 本身就运行在 node 环境中）。\n\n### 初步设计\n\n如果按照 node 可运行的标准，其实 .vue 也可以（要不直接使用 vue？本篇完^\\_^）。但是每次都使用 node 对 vue 文件进行转化，再返回，可能比较耗时和占用资源（没有具体测试过，耗时和资源占用，不过应该不大可行）。\n\n按照最新的 ES6 module 标准，可以直接 import/export。（但这部分还没有仔细研究过 2021-09-20 15:02 ，得专门学习一下）。已知的是这种模式可以直接在浏览器中运行，不需要服务器提前编译。然后问题是：如何优雅的插入 html 代码（我们最初要解决的问题）。可直接 import html 文件吗？或者先载入 JS ，然后再载入 html？又或者直接将 html/css/js 打成一个包(module)，这个包就是 backend 之前对应的 \"JS\"。然后这个包是应该以**文件夹**的形式存在，还是**文件**的形式？比如 .evs。\n\n也许可以是一个以 .evs 结尾的文件夹？那么这个文件夹应该怎么以 **module** 的形式发送给前端页面呢？.evs 包含 xx.json 配置文件，指定 html 入口文件，然后 html 文件引入 js/css？\n\n想得太多，而知道的太少。先去看一下 module 前端打包的相关知识。2021-09-20 15:13\n\n### 待解决问题 2021-09-30 09:24\n\n- JS 或者说模块的生命周期\n\n### 基本模型 2021-10-19 18:43\n\n假设是一种部分化的 HTML 页面，比如 iframe，但比 iframe 更小，只能是一个 div，这些 div 是基本模块，里面包含自身所需 css/js，以及可与后台交互的 js，且可以请求其他 div 模块，并且可与已有的基础页面通信，以及和后来请求的 div 模块通信。\n\n首先基础页面可包含一个通用的 css 文件，比如 ant-design-vue 的 css，这个 css 是所有子 div 共用。然后也可以包含一个共用的 js 函数库，类似于 methods。另外还得包含一个远程函数库 **backend**(之后再找个比较合适的名字)，里面包含的函数实际运行在后端，但在前台无感知。比如点击某个按钮，在后台移动/删除某个文件，但整个过程得让用户感觉和操作本地的数据一样。点击在前端执行 console.log(1)，点击在后台执行 console.log(1)，这两者在用户眼里并无区别（目标是这样的）。\n\n一段伪代码：\n\n``` JS\nhtml: `\n<button data-method='log' data-parm='1'>front 1<button>\n<button data-method='log' data-type='backend' data-parm='2'>back 2<button>\n`\nmethods: {\n  log(data){\n    console.log('front', data)\n  }\n}\nbackend: {\n  log(data){\n    console.log('back', data)\n  }\n}\n```\n\n问题:\n\nhtml 编写仍然没有高亮部分（或许写个编辑器插件来解决？那么别人为什么要用你这个插件呢？进一步，别人为什么要用你这个 backend ？有什么优势？）\n\n到底想实现的是一个什么样的东西？\n\n其实我自己也不是太明白。很喜欢**云原生**这个概念，但这个概念好像都只是概念而已，看过一些所谓的云原生的产品，其实就是一台部署在云端的服务器或软件而已，好像和本地没有一点关联。\n**小程序**把前端的很多部分都*本地化*了，但也产生了很多的屏障，每个平台搞一套语义标签一套 css一套 js 接口，在扩展/发扬 html(前端) 的同时，好像更像是在搞分裂。\n\n**云原生概念** + **小程序概念** 可以搞出一个什么样的东西？\n写程序不分前端和后台（像前面示例的点击运行 log 函数，不分是在前端运行还是后端运行），但这其实还是分前后台。更准备的说应该是不分前端开发和后端开发，开发，写一套程序同时可调用前端数据/函数和后台数据/函数。更进一步说，是写一段代码现时包含前端和后端部分，当需要使用前端相关功能时就使用前端部分，需要后台数据时，就使用后台部分。\n\n那么，为什么要搞这么复杂呢？\n其实在我看来这应该是变简单了。现在全栈开发好像有了一点点的趋势，但实际上的全栈其实就是打两份工而已。这边写后台代码，写完后又去写前端代码，这其实就这两个活是一个人干，还是两个人干的问题。那么这跟前面所说的又有什么有关系？\n\n假如前端要向后台请求一个数据，很多时候，前端其实并不知道后台会返回什么样的数据，甚至可能是一个错误。这需要前后有效的沟通（这不是一件容易的事）。再看一段伪代码\n\n``` JS\nhtml: `\n<button data-method='getUser'>GET user<button>\n`\nmethods: {\n  log(){\n    let user = await fetchData('/username')\n    alert(user)\n  }\n}\nbackend: {\n  router('/username'){\n    // maybe get name from some db\n    // let data = db.get('userid')\n    return 'elecV2P'\n  }\n}\n```\n\n主要目的：实现前后端代码的同时开发，真正的**全栈工程师**。\n\n当然这种方式可能只适用于一些小的项目，但这就是原本的设计场景。一个小的 div 包含自身的 css/js，可与后台进行交互。\n\n2021-10-19 19:33  未完待续\n\n### elecV2P favend html(.efh) 2021-12-01 21:17\n\n经过一段时间的思考，得到一个初步可能可行的方案（等优化完善。\n\n以最初 html 页面为基础，增加标签 <script type=\"text/javascript\" runon=\"elecV2P\">此部分为后台代码(run on elecV2P)</script>\n\n``` xml default.efh\n<div>\n  基础 html 部分\n</div>\n<script type=\"text/javascript\">\n  console.log('前端 JS')\n  <!-- 从后台获取数据 同页面自定义 API -->\n  fetch('?data=json').then(res=>res.json()).then(console.log)\n\n  <!-- 假如引入 $fend(待完成) -->\n  async function main() {\n    let data = await $fend.get('json')\n    console.log(data)\n  }\n</script>\n\n<script type=\"text/javascript\" runon=\"elecV2P\">\n  <!-- 可使用 src 属性引入本地或远程 JS -->\n  console.log('后台 JS')\n\n  if (JSON.parse($request.body).data === 'json')) {\n    $done({\n      statusCode: 200,\n      headers: {\n        'Content-Type': 'application/json;charset=utf-8',\n        'X-Powered-By': 'elecV2P'\n      },\n      body: {\n        'hello': 'elecV2P favend',\n        'note': '这是由 elecV2P favend 返回的 JSON 数据',\n        'docs': 'https://github.com/elecV2/elecV2P/blob/master/efss/readme.md',\n        'request': $request\n      }\n    })\n  } else {\n    $done('get method ' + $request.method)\n  }\n\n  <!-- 假如引入 $fend(还没写) -->\n  $fend.set('json', { hello: 'from elecV2P $fend' })\n</script>\n```\n\n执行过程/基本原理:\n- 首次执行 .efh 文件时，先使用 cheerio 模块将 efh 文件分离为**前端和后端**，并进行缓存\n- 然后当使用 get 请求主页时，直接返回**前端**代码\n- 当前端使用 fetch 或 $fend 请求后台 API/数据时，执行**后端**代码并返回执行结果\n\n\n优点:\n- 前后端代码同一页面，方便开发者进行管理\n- 标签高亮（最初要解决的问题\n- 沿用 html 语法，没有额外的学习成本\n\n缺点:\n- 需要一个后台“引擎”对代码进行分离（开发者不需考虑\n\n待优化部分:\n- 前端 fetch，后台 $request 判断，不够简单/优雅。可引入变量进行统一，比如 $fend, 使用 $fend.get('key') 获取，使用 $fend.set('key', 'data') 绑定赋值\n- 长连接/持续数据传输，关闭后自动清理缓存\n\n问题:\n- 和最初的 PHP/PYHTON 等后台生成前端页面有什么区别？\n\n这是在 html(前端) 插入后台运行的代码\n\n#### efh 细节实现 2021-12-06 20:23\n\n- $fend\n\n前端：$fend(key/attr/arg/params<string>, data<string\\|object>)<promise>\n后台：$fend(key, data<any>)\n\n简单说明：\n- key 可以看成是一个路由，或者是要从后台获取的关键值/API 等，前后端应该配对出现。\n- 前端 $fend 第二参数 data 表示要传输给后台的值，可省略。\n- 后台 $fend 第二个参数为 key 对应的返回值，可以是一个函数。当为函数时，此函数接收的第一项参数为前端传输过来的 data。\n\n使用示例：\n\n``` JS\n// 前端部分\n$fend('newone').then(res=>res.text()).then(console.log);\n\n$fend('skey', '传输给后台的数据').then(res=>res.text()).then(alert).catch(e=>console.error(e));\n\n// 后台部分\n$fend('newone', $store.get('newone'));\n\n$fend('skey', d=>{\n  console.log('收到前台传输数据:', d);\n\n  return {\n    ok: true,\n    data: d,\n    message: '后台返回数据，可以是 string 或 object'\n  }\n})\n```\n\n*通常情况建议只使用一对 $fend 来交互数据，使用第二个参数 data 来确定数据内容。*\n\n底层实现：（这部分由平台开发者完成，在编写 efh 时直接使用上面的示例形式调用即可）\n\n前端 $fend 为封装了 fetch 的函数，后台 $fend 在 context 中实现。\n\n``` JS\n// 前端部分\nfunction $fend(key, data) {\n  return fetch('', {\n    method: 'post',\n    body: JSON.stringify({\n      key, data\n    })\n  })\n}\n\n// 后台部分\nCONTEXT.final.$fend = async function (key, fn) {\n  // 有 bind this, 勿改写为 arrow function\n  if (typeof this.$request === 'undefined') {\n    return this.$done('$request is expect');\n  }\n\n  let body = this.$request.body;\n  if (!key || !body) {\n    return this.$done('$fend key and body is expect');\n  }\n  try {\n    body = JSON.parse(body);\n  } catch(e) {\n    return this.$done('a object string of $request.body is expect');\n  }\n  if (body.key === key) {\n    if (typeof fn === 'function') {\n      try {\n        fn = await fn(body.data);\n      } catch(e) {\n        fn = '$fend ' + key + ' error: ' + e.message;\n        console.error('$fend', key, 'error', e);\n      }\n    }\n    return this.$done(fn);\n  }\n}.bind(CONTEXT.final);\n```\n\n待优化：\n\n- 其他类型数据 arrayBuffer/stream 等\n- $fend 后台无匹配时返回结果\n- $fend key/路由 配对优化\n"
  },
  {
    "path": "docs/dev_note/archive/minishell subprocess.md",
    "content": "### minishell 子进程交互\n\ninit subprocess list\n\n前提：每条 command 起一个 id\n\nsend('shell', {\n\tid,\n\ttype: 'main|sub',\n\tdata: 'command',\n})\n\n子进程为一系列，透明背景，划过可输入背景提醒\n\ncommandId = this.$wsrecv.id _ from _ order\n\nsubprocess {\n\tcommandId: {\n  \tcommand: python3 te.py\n\t  history: {\n      current: -1,\n      lists:[]\n    }\n\t}\n}\n\nchildexec.on('exit', subprocess commandId remove)\n\n### exec 所有进程统计\n\n- from + id"
  },
  {
    "path": "docs/dev_note/archive/webUI.md",
    "content": "# web 重构计划（基本完成）\n\n## 左侧导航栏菜单 menu\n\n- OVERVIEW\n  - Port web/proxy/查看请求（anyproxy）\n  - rules/rewrite/task/mitm list lenght\n- RULES\n  - elecV2P default.list\n- REWRITE\n  - Table url file.js\n- JSMANAGE\n  - JS upload\n  - filelist editor\n- TASK\n  - overview task list/table name,type,time,job,stat/controll\n  - new task form table\n- MITM\n  - rootCA\n  - mitm host\n\n- CFILTER\n- SETTING\n- ABOUT\n- DONATION\n\n## 未来计划\n\n- [x] 去 ant design vue\n- [x] 移动端优化\n\n### 右键菜单 contextmenu.vue\n\n``` XML\n<contextmenu :menus='menu.list' :x='menu.x' :y='menu.y' />\n```\n\n- x <number> : x 坐标\n- y <number> : y 坐标\n- menus <array> : 菜单内容\n  - 菜单选项 <object> (建议始终设置 label，其他项视情况添加)\n    - label <string> : 菜单显示文字\n    - click <function> : 点击文字后执行函数\n    - rclick <function> : 右键菜单后执行函数\n    - dclick <function> : 双击菜单后执行函数\n    - color <string> : 菜单选项颜色\n    - bkcolor <string> : 菜单选项背景颜色\n    - fontsize <string> : 菜单选项文字大小\n  - 菜单选项 <object> 同上\n  - ..."
  },
  {
    "path": "docs/dev_note/archive/websocket 通信协议设计.md",
    "content": "### websocket 通信协议设计\n\n基础: json-RPC\n参考: telegram api\n\n### 内部函数管理/初始化\n\n函数和数据分离\n\n服务器端\n- 处理客户端发送过来的数据，对应 method\n- 更新 method\n- 添加 临时 method / 传输函数\n\n``` JS\nconst wsSer = {\n  methods: {    // 现有方法集\n    add(){      // 添加新的方法\n\n    }\n  }\n}\n```\n\n### 基础数据传输结构:\n\n客户端发送: client.send\n- 发送者 sender\n- 发送模块（单元） unit\n- 数据类型（调用函数）\n- 函数参数\n- 需要返回 ？\n\n服务器接收: server.recv\n- methods\n- reply_to all/sender/unit\n\n服务器发送: server.send\n- 指定接收者 （sender/unit）\n- 数据类型（调用函数）\n- 函数参数\n- 需要返回 ？\n\n客户端接收: client.recv\n- 接收函数 methods\n- reply ?\n\n\n``` 接收\n{\n  methods: 'newmthod',\n  param\n}\n```\n\n## 需要实现的功能\n\n- 生成 send 函数。用于向前端网页的某一个单元发送消息\n- 生成 recv 函数。用于接收消息并处理\n\n- "
  },
  {
    "path": "docs/dev_note/clash delegate efh.md",
    "content": "### clash_delegate.efh\n\nclash webUI for elecV2P, work on favend.\n\n两个模块\n\n- delegate manage\n- delegate rule\n\n默认分流 delegate - elecV2P"
  },
  {
    "path": "docs/dev_note/elecV2P 错误自检指南.md",
    "content": "### 后端运行问题\n\n- 查看 errors.log\n- 查看 pm2-error.log\n- 查看对应脚本的日志文件\n- 查看后台运行日志(docker logs elecv2p)\n- webUI->SETTING->日志等级调整为 debug\n- 在 Google/百度/搜狗 等搜索引擎上输入错误信息\n\n- 重启/重装 elecV2P\n\n### 前端显示问题\n\n尝试升级或切换浏览器（部分老旧的浏览器无法解析一些新的 JS 语法。）\n\n如果问题依然存在，请打开浏览器的**开发者调试工具**（PC 端快捷键通常为 F12，移动端查看各浏览器的说明）。然后将显示的错误信息反馈到 [这里](https://github.com/elecV2/elecV2P/issues)\n\n### 其他常见问题\n\n- anyproxy 网络请求有时候点击无反应（8002 端口\n\n1. anyproxy 缓存已清除\n2. anyproxy 已停止运行\n\n\n- 如果你经常碰过某个问题，欢迎提交 issue 或 pr\n\n### 如果问题依然存在，[open a issue](https://github.com/elecV2/elecV2P/issues)"
  },
  {
    "path": "docs/dev_note/ev 命令行程序.md",
    "content": "### ev binary 可执行程序\n\nelecV2P 可执行程序\n\n形式，要实现的功能\n\n``` sh\nev -help, -h     # 帮助菜单\n\n# elecV2P 全局控制类\nev start     # 开始 elecV2P\nev stop      # 停止 elecV2P\nev update    # 更新 elecV2P\n\n## 可能\nev install   # 可选择 node 安装 还是 Docker\nev i         # 同上\n  # options\n  # -o nodejs | docker\n  # -e Asia/Shanghai\n  # -p 8100/8101/8102\n\n# 状态类\nev status    # 显示状态\nev save      # 保存信息\n\n# 配置类\nev config    # 列出配置\nev config webUI    # 列出某项配置\nev config set minishell 1    # 修改某项配置\n\n# 执行类\nev xxxx.js   # 以 elecV2P 模式运行脚本\nev run xxx.js      # 同上\nev download url    # 下载文件\n```\n\n### 具体实现\n\nnode cmd binary"
  },
  {
    "path": "docs/dev_note/favend JS 重构-efh.md",
    "content": "```\n近期更新: 2021-12-09 22:10\n```\n\n### 原因\n\nelecV2P favend 中的 JS 可能包含 html 部分，直接使用 JS 进行插入，不够优雅。主要是在写的时候，代码编译器默认是 JS 高亮模式，而 html 部分只能通过注释的方式编写，失去了高亮/补齐等特性。于是想可以通过类似 vue 或 react 的结构来设计此部分代码。\n\n比如，html 部分使用 <template></template> 标签进行包裹，JS/api 返回部分使用 <script></script> 标签。当然具体重构的时候可以使用其他 elecV2P 专用标签，细节问题之后再考虑。该方式的主要问题是，此类 JS 文件是不能直接运行的，比如 .vue 文件需要转化才能执行。在 elecV2P 中这类类似 JS 的文件可以叫做，.euv/.evu/.evs/.ejs 等，同样此类细节问题之后再考虑，假如暂时命名为 .evs ，那么 evs 文件必须满足的一点是：可直接运行，至少是在 node 环境下可直接执行（因为 elecV2P 的 favend 本身就运行在 node 环境中）。\n\n### 初步设计\n\n如果按照 node 可运行的标准，其实 .vue 也可以（要不直接使用 vue？本篇完^\\_^）。但是每次都使用 node 对 vue 文件进行转化，再返回，可能比较耗时和占用资源（没有具体测试过，耗时和资源占用，不过应该不大可行）。\n\n按照最新的 ES6 module 标准，可以直接 import/export。（但这部分还没有仔细研究过 2021-09-20 15:02 ，得专门学习一下）。已知的是这种模式可以直接在浏览器中运行，不需要服务器提前编译。然后问题是：如何优雅的插入 html 代码（我们最初要解决的问题）。可直接 import html 文件吗？或者先载入 JS ，然后再载入 html？又或者直接将 html/css/js 打成一个包(module)，这个包就是 backend 之前对应的 \"JS\"。然后这个包是应该以**文件夹**的形式存在，还是**文件**的形式？比如 .evs。\n\n也许可以是一个以 .evs 结尾的文件夹？那么这个文件夹应该怎么以 **module** 的形式发送给前端页面呢？.evs 包含 xx.json 配置文件，指定 html 入口文件，然后 html 文件引入 js/css？\n\n想得太多，而知道的太少。先去看一下 module 前端打包的相关知识。2021-09-20 15:13\n\n### 待解决问题 2021-09-30 09:24\n\n- JS 或者说模块的生命周期\n\n### 基本模型 2021-10-19 18:43\n\n假设是一种部分化的 HTML 页面，比如 iframe，但比 iframe 更小，只能是一个 div，这些 div 是基本模块，里面包含自身所需 css/js，以及可与后台交互的 js，且可以请求其他 div 模块，并且可与已有的基础页面通信，以及和后来请求的 div 模块通信。\n\n首先基础页面可包含一个通用的 css 文件，比如 ant-design-vue 的 css，这个 css 是所有子 div 共用。然后也可以包含一个共用的 js 函数库，类似于 methods。另外还得包含一个远程函数库 **backend**(之后再找个比较合适的名字)，里面包含的函数实际运行在后端，但在前台无感知。比如点击某个按钮，在后台移动/删除某个文件，但整个过程得让用户感觉和操作本地的数据一样。点击在前端执行 console.log(1)，点击在后台执行 console.log(1)，这两者在用户眼里并无区别（目标是这样的）。\n\n一段伪代码：\n\n``` JS\nhtml: `\n<button data-method='log' data-parm='1'>front 1<button>\n<button data-method='log' data-type='backend' data-parm='2'>back 2<button>\n`\nmethods: {\n  log(data){\n    console.log('front', data)\n  }\n}\nbackend: {\n  log(data){\n    console.log('back', data)\n  }\n}\n```\n\n问题:\n\nhtml 编写仍然没有高亮部分（或许写个编辑器插件来解决？那么别人为什么要用你这个插件呢？进一步，别人为什么要用你这个 backend ？有什么优势？）\n\n到底想实现的是一个什么样的东西？\n\n其实我自己也不是太明白。很喜欢**云原生**这个概念，但这个概念好像都只是概念而已，看过一些所谓的云原生的产品，其实就是一台部署在云端的服务器或软件而已，好像和本地没有一点关联。\n**小程序**把前端的很多部分都*本地化*了，但也产生了很多的屏障，每个平台搞一套语义标签一套 css一套 js 接口，在扩展/发扬 html(前端) 的同时，好像更像是在搞分裂。\n\n**云原生概念** + **小程序概念** 可以搞出一个什么样的东西？\n写程序不分前端和后台（像前面示例的点击运行 log 函数，不分是在前端运行还是后端运行），但这其实还是分前后台。更准备的说应该是不分前端开发和后端开发，开发，写一套程序同时可调用前端数据/函数和后台数据/函数。更进一步说，是写一段代码现时包含前端和后端部分，当需要使用前端相关功能时就使用前端部分，需要后台数据时，就使用后台部分。\n\n那么，为什么要搞这么复杂呢？\n其实在我看来这应该是变简单了。现在全栈开发好像有了一点点的趋势，但实际上的全栈其实就是打两份工而已。这边写后台代码，写完后又去写前端代码，这其实就这两个活是一个人干，还是两个人干的问题。那么这跟前面所说的又有什么有关系？\n\n假如前端要向后台请求一个数据，很多时候，前端其实并不知道后台会返回什么样的数据，甚至可能是一个错误。这需要前后有效的沟通（这不是一件容易的事）。再看一段伪代码\n\n``` JS\nhtml: `\n<button data-method='getUser'>GET user<button>\n`\nmethods: {\n  log(){\n    let user = await fetchData('/username')\n    alert(user)\n  }\n}\nbackend: {\n  router('/username'){\n    // maybe get name from some db\n    // let data = db.get('userid')\n    return 'elecV2P'\n  }\n}\n```\n\n主要目的：实现前后端代码的同时开发，真正的**全栈工程师**。\n\n当然这种方式可能只适用于一些小的项目，但这就是原本的设计场景。一个小的 div 包含自身的 css/js，可与后台进行交互。\n\n2021-10-19 19:33  未完待续\n\n### elecV2P favend html(.efh) 2021-12-01 21:17\n\n经过一段时间的思考，得到一个初步可能可行的方案（等优化完善。\n\n以最初 html 页面为基础，增加标签 <script type=\"text/javascript\" runon=\"elecV2P\">此部分为后台代码(run on elecV2P)</script>\n\n``` xml default.efh\n<div>\n  基础 html 部分\n</div>\n<script type=\"text/javascript\">\n  console.log('前端 JS')\n  <!-- 从后台获取数据 同页面自定义 API -->\n  fetch('?data=json').then(res=>res.json()).then(console.log)\n\n  <!-- 假如引入 $fend(待完成) -->\n  async function main() {\n    let data = await $fend.get('json')\n    console.log(data)\n  }\n</script>\n\n<script type=\"text/javascript\" runon=\"elecV2P\">\n  <!-- 可使用 src 属性引入本地或远程 JS -->\n  console.log('后台 JS')\n\n  if (JSON.parse($request.body).data === 'json')) {\n    $done({\n      statusCode: 200,\n      headers: {\n        'Content-Type': 'application/json;charset=utf-8',\n        'X-Powered-By': 'elecV2P'\n      },\n      body: {\n        'hello': 'elecV2P favend',\n        'note': '这是由 elecV2P favend 返回的 JSON 数据',\n        'docs': 'https://github.com/elecV2/elecV2P/blob/master/efss/readme.md',\n        'request': $request\n      }\n    })\n  } else {\n    $done('get method ' + $request.method)\n  }\n\n  <!-- 假如引入 $fend(还没写) -->\n  $fend.set('json', { hello: 'from elecV2P $fend' })\n</script>\n```\n\n执行过程/基本原理:\n- 首次执行 .efh 文件时，先使用 cheerio 模块将 efh 文件分离为**前端和后端**，并进行缓存\n- 然后当使用 get 请求主页时，直接返回**前端**代码\n- 当前端使用 fetch 或 $fend 请求后台 API/数据时，执行**后端**代码并返回执行结果\n\n\n优点:\n- 前后端代码同一页面，方便开发者进行管理\n- 标签高亮（最初要解决的问题\n- 沿用 html 语法，没有额外的学习成本\n\n缺点:\n- 需要一个后台“引擎”对代码进行分离（开发者不需考虑\n\n待优化部分:\n- 前端 fetch，后台 $request 判断，不够简单/优雅。可引入变量进行统一，比如 $fend, 使用 $fend.get('key') 获取，使用 $fend.set('key', 'data') 绑定赋值\n- 长连接/持续数据传输，关闭后自动清理缓存\n\n问题:\n- 和最初的 PHP/PYHTON 等后台生成前端页面有什么区别？\n\n这是在 html(前端) 插入后台运行的代码\n\n#### efh 细节实现 2021-12-06 20:23\n\n- $fend\n\n前端：$fend(key/attr/arg/params<string>, data<string\\|object>)<promise>\n后台：$fend(key, data<any>)\n\n简单说明：\n- key 可以看成是一个路由，或者是要从后台获取的关键值/API 等，前后端应该配对出现。\n- 前端 $fend 第二参数 data 表示要传输给后台的值，可省略。\n- 后台 $fend 第二个参数为 key 对应的返回值，可以是一个函数。当为函数时，此函数接收的第一项参数为前端传输过来的 data。\n\n使用示例：\n\n``` JS\n// 前端部分\n$fend('newone').then(res=>res.text()).then(console.log);\n\n$fend('skey', '传输给后台的数据').then(res=>res.text()).then(alert).catch(e=>console.error(e));\n\n// 后台部分\n$fend('newone', $store.get('newone'));\n\n$fend('skey', d=>{\n  console.log('收到前台传输数据:', d);\n\n  return {\n    ok: true,\n    data: d,\n    message: '后台返回数据，可以是 string 或 object'\n  }\n})\n```\n\n*通常情况建议只使用一对 $fend 来交互数据，使用第二个参数 data 来确定数据内容。*\n\n底层实现：（这部分由平台开发者完成，在编写 efh 时直接使用上面的示例形式调用即可）\n\n前端 $fend 为封装了 fetch 的函数，后台 $fend 在 context 中实现。\n\n``` JS\n// 前端部分\nfunction $fend(key, data) {\n  return fetch('', {\n    method: 'post',\n    body: JSON.stringify({\n      key, data\n    })\n  })\n}\n\n// 后台部分\nCONTEXT.final.$fend = async function (key, fn) {\n  // 有 bind this, 勿改写为 arrow function\n  if (typeof this.$request === 'undefined') {\n    return this.$done('$request is expect');\n  }\n\n  let body = this.$request.body;\n  if (!key || !body) {\n    return this.$done('$fend key and body is expect');\n  }\n  try {\n    body = JSON.parse(body);\n  } catch(e) {\n    return this.$done('a object string of $request.body is expect');\n  }\n  if (body.key === key) {\n    if (typeof fn === 'function') {\n      try {\n        fn = await fn(body.data);\n      } catch(e) {\n        fn = '$fend ' + key + ' error: ' + e.message;\n        console.error('$fend', key, 'error', e);\n      }\n    }\n    return this.$done(fn);\n  }\n}.bind(CONTEXT.final);\n```\n\n待优化：\n\n- 其他类型数据 arrayBuffer/stream 等\n- $fend 后台无匹配时返回结果\n- $fend key/路由 配对优化\n\n### $fend.on('message', ()=>{}) 2022-07-31\n\n接收服务器端发送的数据，基于 sse\n\n*客户端服务器自动协商通信*（how?\n\n``` JS\n$fend.on = (event, fn)=>{\n\n}\nlet ee = new EventSource('/sse/elecV2P/' + this.id)\n```"
  },
  {
    "path": "docs/dev_note/favend 模块化.md",
    "content": "*将 EFSS 的 favorite 和 backend 结合起来有没有搞头*\n\n### 原因\n\n虽然 efh 文件很好的解决了前后端配合的问题，但当大量引用外部文件时，可能比较分散。比如在当前目录下引入 js 文件，在 efss 中引入 css/图像等其他类型的文件。于是考虑将 favorite 做为前端，backend 做为后台，打包成一个模块(module)，放到同一个目录下，方便对各种资源进行管理。\n\n### 大概工作过程\n\n首先设置某个文件夹，假设为 **efssmod**，为模块入口。模块中以 index.html 做为前端入口，这样相关的 js/css/图片等资源都可以理所应当的放到同一目录下。然后再以 favend.js 做为后台入口。\n\n具体的前后端入口文件待定，考虑引入 json 配置文件，类似于 package.json，以 index 关键字指定前端入口文件，以 backend 关键字指定后台入口文件，同时可以添加一些版本、注释、作者等相关信息，总之参考 nodejs 模块 package.json。\n\n### 目前存在的问题\n\n- 后台脚本暂时限制在 script/JSFile 目录，efss 目录的后台脚本无法获取执行\n- 模块中的脚本不方便引入 script/JSFile 目录下的脚本。（可正常引入其他 nodejs 模块\n\n### 假如完成\n\n合并后的模块即 APP，未来可期。\n\n### 其他想法\n\nfavend 片段化，方便结合 **evui**。不必生成整个网页，而只是一部分 html, 或者说 div 代码。方便在 elecV2P 的主界面中进行插入。"
  },
  {
    "path": "docs/dev_note/readme.md",
    "content": "### elecV2P 开发笔记\n\n有时候想到一个功能，得先捋清一下思路，才能开始码代码。有些功能可能比较复杂，所以用笔记的形式记录一下。\n\n当时的笔记可能不是最终实现的样子，仅供参考（其实也没有什么参考意义，如果感兴趣的话，可以稍微看看\n\n总之，这就是在开发过程中，为了捋清楚某些开发步骤而作的一些笔记。\n\nPS: 如果你在代码中看到一些“不合理”的地方，那可能是开发者“学习时”留下的记录。"
  },
  {
    "path": "docs/dev_note/runJSFile 执行逻辑及优化.md",
    "content": "### runJSFile 函数逻辑\n\n执行的脚本类型\n\n- 本地文件\n- 远程文件\n- rawcode\n\n命名\n\nrunJSFile(filename, addContext={})\n\n```\ntype       filename                        rawjs\n- rawjs    rename filename rawcode.js      rawjs\n- local    rename filename                 Jsfile.get\n- remote   rename surlName                 Jsfile.get\n```\n\naddContext.filename vs addContext.rename\nrenmae 会重新写入文件\n\naddContext.timeout: 只提前返回，不限制运行时间（已修改为限制运行时间，同步模式下"
  },
  {
    "path": "docs/dev_note/script_store.efh 应用中心.md",
    "content": "### 基础格式\n\n参考苹果、安卓应用中心的分类模式。\n\n``` JSON\n{\n  \"category\": \"类别\",\n  \"scripts\": [\n    {\n      \"name\": \"name.js\",\n      \"logo\": \"url/to/logo_180x180.png\",\n      \"note\": \"一些备注，关于脚本的说明\",\n      \"tags\": [\"elecV2P\", \"标签\"],\n      \"author\": \"elecV2\",\n      \"homepage\": \"url/to/author\",\n      \"resource\": \"url/to/file.js\",\n      \"thumbnail\": [\"url/to/thum1.jpg\", \"url/to/thum2.jpg\"],\n      \"sponsor_img\": \"url/to/qr.png\",\n      \"sponsor_txt\": \"捐赠支持说明/或其他账号\",\n      \"content_hash\": \"auto content md5 hash\",\n      \"date_created\": \"2022-03-08 20:35\",\n      \"date_updated\": \"2022-03-08 20:36\",\n    }\n  ],\n  \"resources\": [\n    \"https://wogowoodk.json\"\n  ]\n}\n```\n\n``` JSON main.json\n// 主入口文件\n{\n  \"category\": [\n    {\n      \"name\": \"分类名称\",\n      \"note\": \"分类描述\",\n      \"resources\": [\n        \"url1/to/scripts.json\",\n        \"url2/to/scripts.json\"\n      ]\n    }, {\n      \"name\": \"另一分类\",\n      \"note\": \"分类描述\",\n      \"resources\": [],\n    }\n  ]\n}\n```\n\n### 细节实现\n\n- 主入口 main.json\n- 分段获取多 json 文件\n- 智能化标签自动生成 json 片断\n- 新建 GITHUB 库存放 JSON\n- 默认 LOGO（无 logo 时替换\n- 用户上传发布(PR\n- 个人脚本管理(PR\n- 搜索 HASHTREE（自动生成更新\n- 快速定位脚本，获取内容\n- 增加任务/重写订阅的分类\n\n### 工具\n\n- content hash\n- 快速生成 LOGO（基于 hash\n- 自动生成上传日期\n- 自动检测文件大小\n- 自动生成标签列表"
  },
  {
    "path": "docs/dev_note/service workers 开发与优化.md",
    "content": "```\n最近更新: 2022-09-03 09:20\n文档地址: https://github.com/elecV2/elecV2P-dei/blob/master/docs/dev_note/service%20workers%20开发与优化.md\n```\n\n### 缓存策略 cache strategies\n\n0. cache first, fetch fallback\n\n优先使用缓存，缓存不存在则发送网络请求 fetch。\n\n适用于图片等长期不更新的静态资源。\n\n1. cache first, fetch synchronize\n\n优先使用缓存，同时发送网络请求更新缓存数据。\n\n适用于更新较频繁，但前端并不一定要求最新的资源。比如一些广告数据。\n\n2. fetch first, cache fallback\n\n优先使用网络请求，请求失败后使用缓存替换。\n\n适用于要求最新资源，但失败后可使用缓存替换的内容。\n\n3. cache first, random/schedule fetch update\n\n优先使用缓存，随机或者间断固定时间后更新\n\n其他：\n\n- cache only/fetch only 没必要\n- 请求直接返回，不经过 service worker (strategy -1)\n\n### 资源匹配 what to cache\n\n匹配方式：\n- url\n- path\n- host\n- mode\n- search\n- destination\n\n为提高匹配效率，尽量使用全等匹配(includes)，不要使用正则(new RegExp().test())\n\n``` JS\nconst CACHE_URL = new Map([['url', 1]])\n\nlet strategy = -1\n\n// 匹配顺序待研究\nswitch (true) {\ncase CACHE_URL.has(request.url):\n  strategy = CACHE_URL.get(request.url);\n  break;\ncase CACHE_PATH.has(request.pathname):\n  strategy = CACHE_PATH.get(request.pathname);\n  break;\n}\n```\n\n匹配顺序逻辑：按资源精确程度。\n\n比如完整的 url 匹配精确度最高，放在最前面。\n\n接下来是应该是 url 中的 search 部分，然后是 path 部分，然后是 host 部分，然后是 资源类型(destination)，然后是访问模式(mode)。\n\n这是大概是匹配顺序，但应该根据实际项目进行调整。\n\n### 关于 preload\n\n仅在 event.request.mode === 'navigate' 的情况下发生\n\n### 问题记录\n\n- 白屏\n\n网络问题无加载，使用 cache first\n\n- PWA 不更新\n\n\n\n### 参考资料\n\nhttps://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers\nhttps://developer.chrome.com/docs/workbox/\nhttps://googlechrome.github.io/samples/service-worker/basic/\nhttps://web.dev/service-worker-lifecycle/\nhttps://phyks.me/2019/01/manage-expiration-of-cached-assets-with-service-worker-caching.html"
  },
  {
    "path": "docs/dev_note/sse 通信模块.md",
    "content": "### server-sent events\n\n需求分析：\n\n- send message(or no)\n\n问题：\n\n- 多用户（同一个请求路径\n- 多连接（单用户多个请求路径\n- 多请求（单连接发送多次数据\n- 多函数（不同数据对应不同处理函数\n- 多参数（不同处理函数包含不同参数\n\n如何区分？\n\n作以下限制：\n\n单页面只能有一个连接（不同页面的同一路径请求如何区分？\n页面刷新后如何复用的问题\n\n### 要实现的功能\n\n- 服务器端\n\n``` JS\n/** clients 数据结构\n{\n  target1: {\n    euid: res, euid: res, euid: res\n  },\n  target2: {euid: res},\n}\n可使用 query 指定 euid，方便重复使用（断开后重连\n优势：\n- 自定义多路径\n- 同一路径多个请求\n***/\nclass sse {\n  constructor({ app }) {\n    this.clients = new Map();\n    if (app) {\n      app.get('/sse/:target', (req, res)=>{\n        // 同一 target 只能对应一个 clients\n        // main/message 多个对应(取消)\n        if (this.clients.has(req.params)) {\n          clog.info('end old sse connection', req.params);\n          this.clients.get(req.params).end();\n        }\n        res.writeHead(200, {\n          'Content-Type': 'text/event-stream',\n          'Connection': 'keep-alive',\n          'Cache-Control': 'no-cache'\n        });\n        clog.info('new sse connection on', req.params);\n        this.clients.set(req.params, res);\n        req.on('close', ()=>{\n          // this.clients.get(req.params).end();\n          this.clients.delete(req.params);\n        })\n      })\n    } else {\n      clog.error('express app is expect');\n    }\n  }\n\n  sent(target, message) {\n    if (this.clients.has(target)) {\n      let res = this.clients.get(target);\n      if (message === 'end') {\n        res.end();\n        return;\n      }\n      res.write(JSON.stringify(message));\n      return;\n    }\n    clog.error('sse connection', target, 'not ready yet');\n  }\n}\n\nsse.send(target, message);\n// sse.send('main', {})\n```\n\n- 客户端\n\n``` JS\nsse.recv(target, message=>{\n  handle(message);\n  done();\n})\n```\n\n### 其他实现\n\n假如分两种连接类型：\n\n- 单路径单连接 /sse/efss\n- 单路径多连接 /sse"
  },
  {
    "path": "docs/dev_note/store 常量加密存储读取.md",
    "content": "### 目的\n\n常量只属于某个脚本，或者必须提供某个密码才能查看。\n\n$store.put('value 原始', 'secret_key', {\n  pass: 'owowogld',\n  algo: 'ebuf',\n})\n\n$store.get('secret_key', {\n  pass: 'owowogld',\n  algo: 'ebuf',\n})\n\n### 算法基础\n\n参考：https://elecv2.github.io/#算法研究之非对称加密的简单示例\n\n\n\n### 未来计划\n\n- 默认加密存储常量"
  },
  {
    "path": "docs/dev_note/webUI transparent mode.md",
    "content": "## 功能\n\n将 webUI 临时作为一个“透明”代理，转发请求到其他任意端口，甚至任意服务器。\n\n## 配置\n\n``` JSON\n\"transparent\": {\n  \"enable\": true,\n  \"host\": \"127.0.0.1\",    // 转发的目标服务器\n  \"port\": 8001,           // 目标服务器端口\n  \"tls\": false,           // 目标服务器是否通过 tls 连接\n  \"type\": \"proxy\"         // 目标服务器类型 web - 网页, proxy - 代理, transparent - 另一个 elecV2P 透明代理\n}\n```\n\n*（如无特殊说明，以下“透明代理”都特指 elecV2P webUI 透明代理）*\n\n## 使用\n\n直接将 webUI 当作代理使用。例如，假设 webUI 运行在 127.0.0.1:8000，则直接将 127.0.0.1:8000 当作代理地址填写到代理软件进行使用。\n\n### 问题\n\n- 当 webUI 只能通过 https 连接时，无法将这个 https 地址作为代理使用\n\n考虑通过本地的透明代理连接远程透明代理，形成链式转发。\n\n- 转发 http(s) 到另一个 https 透明代理\n\n简单测试，如果另一个透明代理没有使用 nginx 等“网关”工具，好像是可以成功的。（待进一步测试）。\n\n当转发普通 http 请求到有“网关”的透明代理端口时，部分“网关”服务会自动对请求进行一些修改，导致请求转发到了 webUI 端口。\n\n### 分析\n\nwebUI 端口本身有两种情况：http https\n端口透明目标站有三种情况：普通网站(web)，代理(proxy)，另一个透明端口(transparent)\n\n"
  },
  {
    "path": "docs/dev_note/webUI 主题设计.md",
    "content": "### 基本格式\n\n``` JSON\n{\n  \"themeone\": {\n    \"name\": \"主题名称\",\n    \"note\": \"一些说明\",\n    \"color\": {\n      \"--main-bk\": \"#003153\",\n      \"--main-fc\": \"#FBFBFF\",\n      \"--secd-bk\": \"#A7A8BD88\",\n      \"--secd-fc\": \"#003153B8\",\n    },\n    \"logo\": \"url\",\n    \"bkimage\": \"background-image url\",\n  }\n}\n```\n\n### 包含内容\n\n- 颜色（文字/背景等\n\n以下为可选项（可能\n- 圆角大小\n- 图标 logo\n- 标题 title\n\n### 具体实现\n\n*最后实现使用其他的实现方式*\n\n```\n.theme--name {\n  --main-bk: #003153;\n  --main-fc: #FBFBFF;\n  --main-cl: #1890FF;\n  --secd-bk: #A7A8BD88;\n  --secd-fc: #003153B8;\n  ...\n}\n\ndocument.body.className = 'theme--name'\n```\n\n### 自定义主题\n\n``` JS\n// TEST 简单测试代码\ndocument.head.insertAdjacentHTML('beforeend', `<style>\n#app {\n  --main-bk: #326733;\n  background: url(https://images.unsplash.com/photo-1646505183416-f3301d2a8127);\n}\n\n.section > .sider,\n.sider_trigger.sider_trigger--mobile,\n.section, .content, .header, .footer {\n  background: transparent;\n}\n</style>`)\n// #app {--main-bk: #2E3784;--main-fc: #FFCB40;--main-cl: #64AAD0;}\n// #app {--main-bk: #2e1630;--main-fc: #e9bb4c;--main-cl: #d3fd3c;}\n\n// background: #222E\n// background: #0008\napp.style.setProperty('--main-bk', '#222E')\n```\n\n前端设置 UI:\n\nNAEM 主色彩 导出 启用 删除\n\n### 最终实现方案\n\n``` JSON\n\"theme\": {\n  \"simple\": {\n    \"enable\": true,\n    \"mainbk\": \"#123456\",\n    \"appbk\": \"https://xxx\",\n    \"elebk\": \"transparent\",\n    \"style\": \"#app{}\",\n  },\n  \"list\": [{\n    \"name\": \"主题名称\",\n    \"mainbk\": \"#123456\",\n    \"appbk\": \"https://xxx\",\n    \"elebk\": \"transparent\",\n    \"style\": \"#app{}\",\n  }, {\n    \"name\": \"主题名称\",\n    \"mainbk\": \"#2E3784\",\n    \"appbk\": \"https://xxx\",\n    \"elebk\": \"transparent\",\n    \"style\": \"#app{}\",\n  }]\n}\n```\n\n### Todo\n\n- [x] 主题保存切换\n- [x] 主题导入导出\n- [x] 自定义 style"
  },
  {
    "path": "docs/dev_note/webUI 首页快捷运行程序 eapp.md",
    "content": "### 基础说明\n\n![EAPP](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/eapp_overview.png)\n\n一个类似于手机主界面的模块，点击图标快速执行 elecV2P 部分功能。或者作为其他网页应用的一个入口。\n\n暂命名为 eapp - elecV2P application, 大写为 EAPP。\n\n### 功能\n\n- 运行 JS 脚本\n- 运行 EFH 文件\n- 执行 SHELL 指令\n- 打开某个网址\n- 前端执行代码 EVALRUN (v3.7.0 添加)\n\n### 格式\n\n``` JSON\n[{\n  \"name\": \"软更新\",\n  \"logo\": \"efss/logo/soft_update.png\",   // 可省略。省略时将自动生成\n  \"type\": \"js\",\n  \"target\": \"https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/softupdate.js\",\n  \"hash\": \"md5hash\",        // 自动生成\n}, {\n  \"name\": \"显示名称\",\n  \"logo\": \"https://raw.githubusercontent.com/elecV2/elecV2P/master/efss/logo/elecV2P.png\",\n  \"type\": \"efh\",\n  \"target\": \"https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/examples/JSTEST/efh/notepad.efh\",\n}, {\n  \"name\": \"项目主页\",\n  \"type\": \"url\",\n  \"target\": \"https://github.com/elecV2/elecV2P\"\n}, {\n  \"name\": \"Shell 指令\",\n  \"type\": \"shell\",\n  \"target\": \"node -v\"\n}, {\n  \"name\": \"交互输入\",\n  \"type\": \"shell\",\n  \"target\": \"ls -cwd %ei%\"    // v3.6.8 增加 %ei% 占位符，用于简单交互输入。ei(eapp input)\n}, {\n  \"name\": \"eval 执行\",\n  \"type\": \"eval\",             // v3.7.0 增加使用 eval 函数在前端网页上执行 JS 代码\n  \"target\": \"alert('hello elecV2P')\"\n}, {\n  \"name\": \"PM2LS\",\n  \"type\": \"shell\",\n  \"target\": \"pm2 ls\",\n  \"run\": \"auto\",              // v3.7.3 增加在打开 webUI 首页时自动运行的选项。auto: 自动运行 click: 点击运行（默认）\n  \"note\": \"备注信息\"\n}]\n```\n\nname 对应值可以为任意字符。\nlogo 对应值为 img src 属性值，显示大小为 60x60，建议使用图片大小 180x180。可以是一个 http 链接，也可以是 efss 目录中的图片。当省略或加载失败时，将根据 hash 值自动生成 logo，具体的生成算法参考自 https://elecv2.github.io/#算法研究之通过字符串生成艺术图片\ntype 目前支持 **js efh shell url** 四种类型。 v3.7.0 增加 eval\ntarget 为最终执行的内容。\nhash 生成算法，md5(NAME + TYPE + TARGET)。\n\nrun  在打开 webUI 时，该 EAPP 的运行方式(v3.7.3)。共两个选择项 auto: 自动运行，click: 点击运行\n- auto: 自动运行。每次打开 webUI 首页加载 EAPP 列表时，运行一次。不影响之后通过点击再次运行\n- click: 手动点击运行(默认)\n\nnote 备注信息(v3.7.3 增加)。\n\n### 执行\n\n**JS 和 SHELL** 类型点击后，将发送一个 POST 请求到后台，执行对应脚本，然后运行日志通过 websocket 返回给前台。\n\n**EFH 和 打开网址** 两个类型的应用将在浏览器的新标签页中打开。\n\n**EVALRUN** 类型，将直接使用 **eval 函数** 在前端网页中执行对应代码。不支持使用文件，只能是纯 JS 原生代码，仅在前端页面中运行。\n\n![EAPP 编辑](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/eapp_overview.png)\n\n打开编辑模式后，再次点击图标可编辑 EAPP 内容，点击右上角的 X 按钮删除应用。\n\n问题反馈 https://github.com/elecV2/elecV2P/issues\n\n### 待添加\n\n- 小组件\n- 编辑脚本内容快捷\n\n### 小组件 widget\n\n在首页显示 widget 小工具\n\n## 实现\n\nEAPP 图标/LOGO 使用 canvas 显示，方便通过 JS 修改，可做成动态 widget\n\n## 更新/trigger\n\n- 每次打开首页是触发\n- cron\n- JS 内部控制\n\n### elecV2P 全部说明文档\n\nhttps://github.com/elecV2/elecV2P-dei/tree/master/docs"
  },
  {
    "path": "docs/dev_note/webhook token 权限设计.md",
    "content": "### 目的\n\n限制某个 token 可访问的目录/时间/次数等。\n\n### 实现\n\n- 可设置多个 token\n\n每个 token 对应的权限:（返回 info\n- 完整权限（管理员\n- 可访问路径。比如 限制某个 token 除 logs 外其他接口都不可访问\n- 可访问时间。2021-07-20 至 2021-07-21 可精确到秒\n- 可访问次数。访问几次后，该 token 自动失效\n- 其他可能添加\n  - 限制 IP\n\n### 问题\n\n相同 token 授于不同权限的？\n\n1. 并集。提供对应 token 的所有权限\n2. 禁止/覆盖。仅后条 token 对应权限生效（理论上不可设置相同 token\n\n根据逻辑及方便性取第 **2** 种。但会产生问题，假如想限制某个 token 在某个时间段对某个路径的访问次数，第 2 种授权访问明显无法达到效果。所以还是得采用第 1 种吗？\n再仔细想想，此时，可更改 token 授权内容，单个 token 可对应多个权限限制。这里会产生的问题: 如何在前端比较好的表现，让单个 token 可以无限的添加权限？\n\n单 token 单权限可以直接使用一个表格 tr 行进行设置。那单个 token 多个权限呢？ 在保持 token 单元格不动的情况下，增删授权行？\n\n睡了一觉，想法有些改变。应该先设置好相关权限，然后生成相应的 token。甚至更进步一些，设置 token 和权限的对等函数，从 token 中可直接读取到对应权限。这样，在前端方面，可专门设置一个区域用于设置权限，然后生成 token，token 还是以表格的形式进行保存。\n\n于是又产生了一个问题：token 生成函数如何编写/设置呢？得去学习参考 oAuth2.0 相关资料了。\n\n2021-09-18\n\noAuth2.0 不可取，太 heavy 了。要设计另外的 token 模式，基本形式基于 uuid(比如: fcef12cf-6694-48bb-8568-63bd025cf5ad), 然后按位设计权限，部分使用 主 token 进行加密认证。比如 0x01 表示拥有访问路径的权限，0x02 表示限制访问时间，同时权限则为 0x03，依此类推。然后取 01/02/03 或加密后的两位于 uuid 固定位置中。\n\n关于 uuid 的权限加密及具体协议待进一步仔细设计。\n\n## Cookie 授权登录 2021-10-23 18:18\n\n新增 cookie 授权验证。cookie 生成及验证方式:\n\n``` JS\n// 生成\nbtoa((wbtoken + wbtoken ).substr(iRandom(wbtoken.length), 8))\n\n// 验证\nlet cookies = cookie.parse(req.headers.cookie || '')\n(wbtoken + wbtoken).indexOf(atob(cookies.token)) !== -1\n```\n\n优点:\n\n- 和 token 相关联(部分)，切换 token 后 cookie 失效\n- 计算简单，可快速检测 cookie 是否有效\n\n缺点:\n\n- 不够优雅？\n- 如果原来的 token 太简单的话，可能会被碰撞出结果\n\n**v3.7.4 之后 cookie 对应值调整为 userid(基于 webhook token 使用 md5 算法生成)**\n\n## 临时多 TOKEN 设计\n\n``` JSON\n\"tokens\": {\n  \"md5hash(token)\": {\n    \"enable\": true,                     // 快捷开关\n    \"token\": \"9855d6cb-0c70-41d2-a246-54ebb365e9e3\",\n    \"path\": \"/efss/temp|^/logs\",        // 可访问路径。正则表达式字符串，匹配方式 new RegExp(path).test(req.path)。留空表示允许所有\n    \"note\": \"给 XX 的临时 TOKEN\",       // 备注信息\n  }\n}\n```"
  },
  {
    "path": "docs/dev_note/下载其他扩展程序.md",
    "content": "### 简单说明\n\n直接下载其他可执行程序。\n\n### 基础格式\n\n``` JSON\n[{\n  \"name\": \"程序名称\",\n  \"resource\": \"url/to/download\",\n  \"file_type\": \"zip | tar | exe\",\n  \"install_path\": \"/path/to/install\",\n}]\n```"
  },
  {
    "path": "docs/dev_note/关于引入区块链的可行性.md",
    "content": "```\n初始发布: 2021-12-11 14:53\n最近更新: 2022-09-20 11:53\n```\n\n### 目的\n\n- 防止脚本被更改\n- 脚本分布式存储\n- 给予脚本作者一定奖励\n\n### 基本架构\n\n暂命名为 **EVP**，方便下面的说明，之后可能会修改。\n\n可选方案：\n\n1. 完全自定义，基于 node 开发\n2. 基于 ETH/EOS 等，使用 solidity 开发\n\n大概的数据结构：\n\n- 脚本 hash 值（基于脚本内容\n- 脚本名称\n- 脚本类型（本地、远程\n- 脚本地址\n- 脚本内容\n  - 是否加密？\n  - 特定用户可查看？\n- 脚本作者（可选\n- 脚本说明（可选\n- 脚本对应积分/代币地址\n- 更新日期（自动添加\n- 定时发布（可选\n- 失效日期（可选\n  - 自毁或不可见？\n\n## 细节问题\n\n- 如何查找脚本（脚本 md5 hash 值）\n- 引用其他脚本\n- 发布脚本花费 token（gas\n  - 空投 token 给早期脚本开发者\n  - 根据脚本大小确定 gas 费用\n\n### 可能加入的功能\n\n- 脚本修改记录（类似 Git commit\n- 他人修改脚本（类似 PR\n- 引入其他获取 Token 的方式\n  - 传送数据/广播\n  - 提供算力\n    - 每个节点可提交要计算的问题\n    - 每个节点初始化可提供的算力\n\n- 脚本可引用 EVP 钱包，进行一些签名授权操作\n- EDNS/.efh 域名，输入域名打开对应脚本\n- 可选择实名制，方便找回\n- 担保转账。转账给他人时引入第三方担保者\n\n### 一些说明 2021-12-20 15:19\n\n- 与 DAPP 的区别\n\n目前本人对 DAPP 了解的不多，但 DAPP 好像是运行在云端(EVM)，用户通过请求发送参数运行。而 EVP 只是保存代码用户下载后可在本地运行。因此 EVP 最终可能无法运行在 ETH 中，而不得不自己设计一套区块链系统？（希望不要如此\n\n### 可能的模式\n\n只在链上只保存脚本的 hash 值以及所有者，脚本内容还是按之前的方式保存，比如本地或远程服务器（github/自建服务器 等）。运行脚本时将脚本的 hash 值发送到 智能合约/DAPP 中，这一步会产生一个问题，发送指令到智能合约是需要消耗 gas 费的，每运行一次付费一次，显然不合理。可能要在这一步引入链下认证环节（所谓的 layer2/layer3 网络？这部分知识待学习）。\n\n### 开发步骤 2022-02-24 20:42\n\n1. 设计钱包 wallet\n\n可引入公钥/私钥模式，但出于安全式考虑，私钥存储在服务器上可能会被其他脚本读取，所以私钥得再加密一次？\n考虑双层私钥模式，一般操作可由第一层私钥授权完成，当涉及到代币/资金转移等问题时，提供一个交互界面，用户手动授权。类似于目前的 MetaMask 钱包。\n\n钱包中显示自己开发的脚本，“已购买”的脚本。类似于 MeatMask 钱包 NFT 展览的概念。\n\n引入 eth 开源钱包。（如果不复杂的话，考虑自己写一个。）\n\n主要是只要包含原始密码（12 个单词），以及生成公钥即可，不需要链相关的开发，难度应该不大。\n\n2. 智能合约\n\n先在 ETH 链上进行开发，熟悉智能合约开发环境，也方便用户进行理解。\n\nETH 链上开发的优点：\n\n- 相关的学习资料很多\n- 开发者和用户也很多\n\n缺点：\n\n- 使用成本（gas 费用）较高（或许这个问题 ETH 之后会解决\n\n等 ETH 链上的智能合约运行稳定后，再考虑开发原生的公链/私链，将 ETH 上的智能合约进行转移。（这部分可能需要一段很长很长的时间，3-5年？）\n\n自己发公链/私链的优点：\n\n- 自定义程度高（包含运行和费用等各方面\n- 开发者/用户的学习成本增加\n\n缺点：\n\n- 开发难度高\n\n3. 自建主链\n\n主链名称 EVP, 发币为 evcoin (大写 EVCOIN)。\n\n每个上链的内容为一个 JSON 数据，包含：\n(此处应综合参考 telegram api 数据传输格式，bitcoin/ETH block 数据存储格式)\n\n``` json\n{\n  \"idx\": 0,\n  \"hash\": \"\",\n  \"from\": {\n\n  },\n  \"to\": {\n\n  },\n  \"data\": {\n    \"type\": \"msg|nft|coin|script\",\n    \"sign\": \"签名\"\n  }\n}\n```\n\n早期活动：\n\n- 在 elecV2P 首页随机领取 evcoin，相当于空投\n- 引入 主题 NFT，随机空投\n\n\n### 当前区块链的问题\n\n- proof of work 的本质是在做无用功\n- 太过于专注于技术而偏移了实际应用\n- 每个节点都保存所有账本（这是必须，但或许可以进行选择性优化\n  - 分层（当前所有的区块链都是无效的尝试？ 2022-01-26 11:53\n  - 中心化不可避免（选择可信任节点\n- 可靠的数据结构大于所有区块链项目（？区块链的本质是一个动态的数据结构\n  - 早期的电子/程序设备是不可更改的，动态可编程让互联网有了可能\n  - 区块链不可更改，智能合约（动态可编程）让这项技术有了更进一步的发展\n- 人人都可以发布主链，人人都可以给主链分层，人人都可以编写智能合约，中心化吗？\n\n\n## 自定义区块链 2022-05-08 16:53\n\n- 主链及分层\n- 不需要 gas 调用合约\n  - 由合约指定 gas 费用\n- 中心化的区块链\n- 用户不需要投票，反正他们也不会去知道投票内容，以及会产生的影响（待考虑\n- 官方网站发布最终的 blockhash, 以及对应时间\n- 引入零知识量证明机制。layer2，本地计算，提交结果及公钥到主链\n- 创世块设置超级管理员（God），赋予超级管理员设置其他管理员的权限\n\n- message is the money. 信息即金钱。转移/传递信息就是转移/传递金钱\n- All Buffer, All binary?\n\n**先做出来，不要考虑效率、内存、安全方面的问题。做出来->测试->修改->测试->修改->测试... 毕竟 what to lose?**\n\n## NFT 相关设计  2022-09-20 11:49\n\n种类：\n- 脚本\n- 主题\n- 头像（包含生成头像的算法\n- 任意文本（其他类型的程序、脚本）\n\n基础结构：（待优化（可参考 git api info\n\n``` JSON\n{\n  \"name\": \"名称\",\n  \"note\": \"一些相关说明\",\n  \"owner\": \"所有者 id\",     // 考虑显示为 Object，展示用户部分基础信息\n  \"type\": \"js\",\n  \"data\": \"binary data\",\n  \"size\": \"data size\",\n  \"hash\": \"hash of data\",\n}\n```\n\n内部可包含函数，用于修改 NFT 的数据（类似于 contract）。这部分参考 wasm 实现。\n\nNFT 可转移所有权（出售\nNFT 可仅授权他人使用，但保留所有权。\n\n授权内容：\n\n- 费用（以 evcoin 结算，可为 0\n- 时间（开始及结束"
  },
  {
    "path": "docs/dev_note/可能永不执行的长期计划.md",
    "content": "### 重命名\n\n原因：现在的名称 elecV2P 不好发音，考虑取一个方便读写的名字。\n\n备选：\n\n- moefi\n- moeku\n- kumoe\n- kufee"
  },
  {
    "path": "docs/dev_note/启动器快捷方式 $run.md",
    "content": "## 目的\n\n用一个 .json 文件作为启动的快捷方式，方便将多个动作进行组合，及一键运行。\n\n## 运行\n\n应该包含的动作，或者说功能：\n\n- 关键字: 对应功能\n\n具体任务类\n- script: 运行脚本\n- shell: 执行 shell 指令\n- task:  开始/暂停任务\n- download:  下载文件（可能增加可选择下载方式 type git/wget/aria2c 等\n- notify: 发送通知\n- open:  直接打开一个 url 或 文件\n- efh: 打开 efh 文件\n\n执行逻辑类\n- if:    如果逻辑 （每个任务执行后判断？\n- next:  下一个任务（配合 if 使用\n- done:  结束语句\n- for:   for 循环逻辑\n- while: while 循环逻辑\n- wait:  等待\n- aski:  ask for input 要求输入\n\n## 格式 2021-12-16 18:06\n\n（研究中，非最终版本\n\n``` JSON\n{\n  \"log\": \"path/to/my.log\",                  // 启动后日志保存路径。可省略\n  \"name\": \"一个启动文件\",\n  \"note\": \"关于该启动器的一些说明\",\n  \"logo\": \"https://xxx.com/xxx.png\",        // 对应图标\n  \"author\": \"作者 elecV2\",\n  \"resouce\": \"https://xxxxxx/xxxx.json\",    // 远程更新地址\n  \"actions\": [     // 待执行的系列动作\n    {\n      \"name\": \"任务一\",\n      \"id\": \"taskone\",           // 使用 id 方便跳转\n      \"type\": \"script\",          // 可以使用远程脚本\n      \"args\": [\"test.js\", \"-grant\", \"nodejs\"],\n    }, {\n      \"name\": \"任务二\",\n      \"type\": \"shell\",\n      \"args\": [\"node\", \"-v\"]\n    }, {\n      \"name\": \"停止任务\",\n      \"type\": \"task\",\n      \"next\": \"anot\",          // 使用 next + id 进行跳转\n      \"args\": [\"stop\", \"taskid\"]\n    }, {\n      \"name\": \"下载文件一\",\n      \"type\": \"download\",\n      \"args\": [\"https://resouce.url\", \"path/to/save\", \"type|wget|...\"]\n    }, {\n      \"name\": \"efh 测试\",\n      \"type\": \"efh\",\n      \"args\": [\"path/to/router\", \"type|wget|...\"]\n    }, {\n      \"id\": \"anot\",\n      \"name\": \"发送通知\",\n      \"type\": \"notify\",\n      \"args\": [\"title\", \"body\", \"url\", \"bark|ifttt|cust|或省略\"]\n    }\n  ],\n  \"mixif\": \"amixif.js\",        // 每次任务完成后的判断脚本。可省略\n}\n```\n\n说明:\n\nmixif: \n使用某个脚本对每次执行的任务结果进行判断。传入如下几个参数:\n- 任务结果  $env.acres\n- 任务 id/或顺序  $env.acid\n- 任务名称  $env.acname\n\n返回执行结果\n- 不作任何处理\n- 跳转到一下任务 next\n- 结束运行 done\n\n### 最小单元测试\n\n``` JS\nfunction run({\n  id, name,\n  type, args,\n}) {\n  // some code\n}\n// run test.js\n// run test.js -grant nodejs\n// run ls -cwd efss\n// run test.py\n// run bash.sh\n// task start tid\n// download url/path\n```\n\n### 待解决的问题\n\n- 获取上一个任务的执行结果\n- 加入 IF/WHILE/FOR 等逻辑\n- .JSON 文件的可视化编辑\n- run bash 文件 process stdin\n\n#### 非问题的问题\n\n其实这些都可以在一个 JS 里面完成，所以有必要吗？\n\n有。可以组合不同的脚本使用，方便进行整合。最主要是可以为以后的拖动/可视化编辑打好基础。\n\n没有。纯属闲得蛋疼，会有人用这个吗？闲着没事做吗？纯属浪费时间。会获得什么吗？值得吗？"
  },
  {
    "path": "docs/dev_note/开发者激励计划.md",
    "content": "## 开发者激励计划\n\n简单说明：给予 elecV2P 脚本开发者一定的现金奖励。\n\n奖励金额：激励计划总金额 10000 元，发完即止。\n\n活动截止时间：2022-06-30\n\n奖励说明：单个脚本奖励 10-100 元不等，本次活动每个作者最高奖励 200 元，希望能把机会留给更多的人。欢迎下期继续参加。\n\n**最终说明以 TG 频道 @elecV2 的通知为准**\n\n### 具体实施\n\n提交方式：\n\n- Github 主动提交（推荐\n  - https://github.com/elecV2/elecV2P-scripts\n  - 提交格式参考仓库 readme 文档\n- E-mail 发送脚本\n  - elecV2#icloud.com (#->@)\n\n提交内容：\n\n- 脚本链接\n- 脚本的简单说明\n- 赞赏 QR 或者账号\n\n可能问题：他人冒领（推荐使用 Github 账号直接 PR，如发送邮箱尽量附带一张后台截图。）\n\n### 参考示例\n\nelecV2P 默认自带的脚本：[elecV2P 默认脚本](https://github.com/elecV2/elecV2P/tree/master/script/JSFile)\n\nJS 脚本编写参考：[说明文档 04-JS.md](https://github.com/elecV2/elecV2P-dei/blob/master/docs/04-JS.md)\n\n推荐编写 efh 脚本。\n文档参考：[efss.md](https://github.com/elecV2/elecV2P-dei/blob/master/docs/08-logger&efss.md) 中 efh 相关部分。\n实例脚本：[efh 测试脚本](https://github.com/elecV2/elecV2P-dei/tree/master/examples/JSTEST/efh)\n\n### 脚本限制\n\n- 不可加密\n- 长期可用（至少一个月内有效，可调整）\n- 非 VIP 破解类\n- 脚本内注明：兼容 elecV2P\n\n### 获取奖励的时间\n\n提交脚本的下一个周五前。比如这周星期一到星期日提交，下周五前将发送奖励现金到指定账号。\n\n### 之后的计划：\n\n- script_store.efh 应用中心"
  },
  {
    "path": "docs/dev_note/引入广告系统.md",
    "content": "### 目的\n\n盈利。\n让项目能更好的发展，以及支持**开发者激励计划**。\n让在 elecV2P 编写脚本的开发者可以受益。脚本开发者有动力写脚本，用户就有更多的脚本可用，实现一个正向循环。\n\n### 广告位\n\n在 webUI 首页底部增加 2-3 个广告位。待实现其他盈利方式后，考虑完全取消。\n\n### 价格\n\n正常价位：\n\n- 图片 1800 元/月\n- 文字  500 元/月\n\n测试阶段：\n\n- 图片 600/月\n- 文字 200/月\n\n**测试阶段最多租用 3 个月，测试结束时间待定**\n\n简单说明：广告是为了盈利及发展，如果实在不想看到，可考虑赞助开发者以进行屏蔽\n\n### 联系方式\n\nE-mail: elecV2#icloud.com (#->@)\nTelegram: @elecv7\n\n### 需要提供的内容\n\n- 广告性质（非黄赌毒类\n- 显示图片或文字\n  - 图片要求：长 600-1000 高 40-60  推荐 844x50\n- 落地页链接\n\n### 可能增加的计划\n\n- 提供广告接口给第三方开发者\n- 广告显示给用户一定代币奖励\n\n### 开发计划/要解决的问题\n\n- 域名（很好解决\n- 服务器（done\n  - 广告数据的存储形式（文字、图片（重点\n- 数据统计（以 cloud flare 为准\n\n### 数据结构\n\n广告数据结构\n\n``` JSON\n{\n  \"adid\": \"广告 ID\",\n  \"link\": \"url/to/ad\",\n  \"type\": \"txt|pic\",\n  \"text\": \"显示/说明文字\",\n  \"show\": \"url/to/show.png\",\n  \"note\": \"更详细的说明\",\n  \"rand\": \"出现概率\",\n  \"position\": \"广告位\",\n  \"sponsors\": \"广告商\",\n  \"date_start\": \"开始时间\",\n  \"date_end\": \"结束时间\",\n}\n```\n\n问题：\n\n- [x] 多 sponsors 自动挑选\n- [x] 展示时间开始|结果自动化\n- [x] 部分数据只可展示给后台\n\nsponsors 数据结构\n\n```\n{\n  sid: {\n    name: '名称',\n    note: '留言',\n    date: '日期',\n    amount: '资金',\n    channel: 'alipay',\n    homepage: '主页',\n    public: true\n  }\n}\n```"
  },
  {
    "path": "docs/dev_note/待深度优化部分.md",
    "content": "### elecV2P 中还可以进行深度优化的地方\n\n- runJSFile context 抽离共用的部分\n- EFSS 文件列表 hash table or Map\n- runJSFile cache JS 内容\n- 脚本运行资源释放的问题\n- 脚本运行停止 $done 的问题\n\n### done\n\n- rule/rewrite 正则匹配优化(hash (cache"
  },
  {
    "path": "docs/dev_note/根据 mitmhost 生成 pac 文件.md",
    "content": "### 说明\n\nPAC 文件地址: webUI/pac 。 比如 http://127.0.0.1/pac 或者 https://xx.xxx(你的webUI地址)/pac\n\n*[PAC 是什么？](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Proxy_servers_and_tunneling/Proxy_Auto-Configuration_PAC_file)*\n\n### 使用\n\n在使用设备的代理设置部分，选择 PAC 自动代理，填写上面的 PAC 文件地址，保存即可。\n\n### 功能\n\n根据当前 mitmhost 列表自动生成 PAC 文件。\n\n#### 提醒\n\n- 如果 webUI 开启了安全访问，在填写 PAC 链接时注意带上 token，建议设置临时 token 进行访问。比如 http://192.168.1.101/pac?token=1234\n- 如果更新 mitmhost 列表后 PAC 文件没有更新，建议在 PAC 链接后添加任意参数进行缓存更新。比如 http://192.168.1.1/pac?token=1234&update=154\n- 如果设置 PAC 后无法联网，请确认代理地址及端口是否填写正确，以及 elecV2P 的 MITM 功能是否开启"
  },
  {
    "path": "docs/dev_note/脚本缓存_内容结果等.md",
    "content": "### 缓存结构\n\n``` JS\nconst script_cache = new Map();\nscript_cache.set('script_name', {\n  name: \"name\",\n  hash: md5(code),\n  time: \"2022-04-17 21:25\",\n  type: \"JS\",\n  grant: true,\n  sudo: false,\n  compatible: 'nodejs',\n  // context: CONTEXT.final,\n  code: \"...\",\n  done: \"res\",\n})\n\n// 简版\nlet scache = {\n  name: \"name\",\n  hash: md5(code),\n  time: \"2022-04-17 21:25\",\n  code: \"...\",\n  // 以下内容每次运行可能不一样，没有缓存的必要\n  // type: \"JS\",\n  // grant: true,\n  // sudo: false,\n  // compatible: 'nodejs',\n  // context: CONTEXT.final,\n  // done: \"res\",\n}\n```\n\n### 目的、优点\n\n- 加快脚本载入速度\n- 加快脚本处理速度\n\n### 问题\n\n- 占用内存\n- 部分资源无法释放\n\n### 考虑添加\n\n- 极速模式（不运行脚本，直接返回上次执行结果"
  },
  {
    "path": "docs/dev_note/节点互联.md",
    "content": "### 简介\n\n不同的 elecV2P 服务器通过 websocket 连接。\n\n### 形式\n\nminishell connect userid\n\n### 功能 - 连接后可以做什么\n\n- 脚本传输"
  },
  {
    "path": "docs/dev_note/通过脚本管理规则 $rewrite.md",
    "content": "### 前期准备\n\nclass rewrite {\n  list()\n  add()\n  remove()\n  update()\n  find()\n}\n\n### 基础使用\n\n- $rewrite.list/add/remove/update/find\n\n### 问题\n\n- $rewrite 管理正在使用的规则还是规则文件？"
  },
  {
    "path": "docs/res/logo/readme.md",
    "content": "### elecV2P logo 文件夹\n\n地址: https://github.com/elecV2/elecV2P-dei/tree/master/docs/res/logo\n\n主色彩值: #003153\n\nfavicon-32x32.png\n\n![favicon-32x32](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/logo/favicon-32x32.png)\n\nelecV2P-180x180.png\n\n![elecV2P-180x180](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/logo/elecV2P-180x180.png)\n\nelecV2PBR-720x720.png\n\n![elecV2PBR-720x720](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/logo/elecV2PBR-720x720.png)\n\nelecV2Pall-720x720.png\n\n![elecV2Pall-720x720](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/logo/elecV2Pall-720x720.png)\n\n欢迎 PR 提交自行设计的 logo 图标\n\n#### 其他说明\n\nsvg 文件使用 [Inkscape](http://www.inkscape.org/) 制作"
  },
  {
    "path": "examples/JS-elecV2P.sublime-build",
    "content": "// sublimet text build system 文件\r\n// 功能：在 sublime 编辑器中使用 ctrl + B 快速运行测试脚本\r\n// 使用：\r\n//   - 复制本文件到 sublime xxxx\\Data\\Packages\\User\\ 文件夹下\r\n//   - 然后根据 elecV2P 的具体运行，修改 cmd 命令中的 http 地址和 token 值，保存\r\n//   - 然后在 sublime 的菜单栏选择 工具(tools)->Build System->JS-elecV2P\r\n//   - 或者使用 ctrl+shift+b 快捷键切换默认 build system\r\n//   \r\n// 确保待运行 JS 文件位于 script/JSFile 目录，并且非子目录\r\n\r\n{\r\n  \"cmd\": \"curl -s http://127.0.0.1:12521/webhook?token=a8c259b2-67fe-D-7bfdf1f5&type=runjs&fn=$file_name\",\r\n  // \"cmd\": \"powershell curl '\\\"http://127.0.0.1/webhook?token=a8c259b2-67fe-D-7bfdf1f55cb3&type=runjs&fn=test/$file_name\\\"' | Select-Object -Expand Content\"\r\n}"
  },
  {
    "path": "examples/JSTEST/0body.js",
    "content": "$done({ response: { body: 'elecV2P body' }})"
  },
  {
    "path": "examples/JSTEST/TGbotonFavend.js",
    "content": "/**\n * 功能: elecV2P TGbot on favend\n * 参考修改自: https://github.com/elecV2/elecV2P-dei/blob/master/examples/TGbotonCFworker2.0.js\n * 最近更新: 2021-11-13\n * 更新地址: https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/examples/JSTEST/TGbotonFavend.js\n * 问题反馈: https://github.com/elecV2/elecV2P-dei/issues\n * \n * 与 CFworker 版相比较的优点:\n * - 支持使用 IP，无需域名\n * - 无需注册 cloudflare\n * \n * 使用方式: \n * 1. 准备工作\n *  - elecV2P 服务器外网可访问（测试: http://你的 elecV2P 服务器地址/webhook?token=你的webhook token&type=status ）\n *  - 在 https://t.me/botfather 申请一个 TG BOT，记下 api token\n *\n * 2. 部署代码\n *  - 根据下面代码中 CONFIG_EV2P 的注释，填写好相关内容\n *  - 然后把修改后的 JS 文件上传到 elecV2P（也可以上传后再修改\n *  - 然后在 elecV2P EFSS 界面 favend 相关设置中设置 关键字 tgbot(可自行设置为其他) | 运行 JS | TGbotonFavend.js(修改后的脚本名)\n *  - 接着在浏览器中打开链接: https://api.telegram.org/bot(tgbot api token)/setWebhook?url=http://服务器地址/efss/tgbot?token=你的 webhook token（连接 TGbot 和 favend）\n *  - 最后，打开 TGbot 对话框，输入下面的相关指令（比如 status），测试 TGbot 是否部署成功\n *\n * 实现功能及相关指令: \n * 查看 elecV2P 运行状态\n * status === /status\n *\n * 查看服务器相关信息\n * /info\n * /info debug\n * \n * 删除 log 文件\n * /deletelog file === /deletelog file.js.log === /dellog file\n * /dellog all  ;删除使用 log 文件\n *\n * 查看 log 文件\n * /log                 ;进入日志查看模式\n * /log 文件名称\n *\n * 定时任务相关\n * /task                ;进入任务管理模式\n * /taskinfo all        ;获取所有任务信息\n * /taskinfo taskid     ;获取单个任务信息\n * /taskstart taskid    ;开始任务\n * /taskstop taskid     ;停止任务\n * /taskdel taskid      ;删除任务\n * /tasksave            ;保存当前任务列表\n * \n * 脚本相关\n * /runjs               ;进入脚本运行模式\n * /runjs file.js       ;运行脚本\n * /runjs https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/webhook.js\n * ;运行远程脚本同时重命名保存为 anotify.js\n * /runjs https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/feed.js anotify.js\n * /deljs file.js       ;删除脚本\n *\n * shell 指令相关\n * /shell               ;进入 shell 指令模式\n * /exec ls  ===  /shell ls  ===  exec ls\n * exec pm2 ls\n * \n * bot commands 2.0\nrunjs - 运行 JS\ntask - 任务管理模式\nstatus - 内存使用状态\nshell - shell 命令执行模式\nstore - store/cookie 管理\ntasksave - 保存任务列表\nlog - 查看日志文件\ncontext - 查看当前执行环境\nend - 退出当前执行环境\ninfo - 查看服务器信息\ncommand - 列出所有指令\n\n * 更新方式: \n * - 如果在 CONFIG_EV2P 中设置了 store，直接覆盖该脚本即可\n * - 如果没有设置 store，则复制覆盖除了开头的 CONFIG_EV2P 外其他所有内容到\n *\n * 适用版本: elecV2P v3.3.6 (低版本下部分指令可能无法正常处理)\n *\n * 待实现功能:\n * - 普通 Get 请求配置 UI\n * - 结合 favend 优化相关逻辑\n**/\n\nlet CONFIG_EV2P = {\n  name: 'elecV2P',              // bot 名称。可省略\n  store: 'elecV2PBot_CONFIG',   // 常量储存 CONFIG_EV2P 配置。建议调试时留空，调试完成后再设置回 'elecV2PBot_CONFIG' ）\n  storeforce: false,     // true: 使用当前设置强制覆盖常量储存中的数据，false: 常量储存中有配置相关数据则读取，没有则使用当前设置运行并保存\n  url: '/',    // elecV2P 服务器地址(非 80 端口填写 http://你的 elecV2P 服务器地址)\n  wbrtoken: 'xxxxxx-xxxxxxxxxxxx-xxxx',      // elecV2P 服务器 webhook token(在 webUI->SETTING 界面查看)\n  token: 'xxxxxxxx:xxxxxxxxxxxxxxxxxxx',     // telegram bot api token\n  userid: [],            // 只对该列表中的 userid 发出的指令进行回应。默认: 回应所有用户的指令（高风险！）\n  slice: -1200,          // 截取部分返回结果的最后 1200 个字符，以防太长无法传输（可自行修改）\n  shell: {\n    timeout: 1000*6,     // shell exec 超时时间，单位: ms\n    contexttimeout: 1000*60*5,               // shell 模式自动退出时间，单位: ms\n  },\n  timeout: 5000,         // runjs 请求超时时间，以防脚本运行时间过长，无回应导致反复请求，bot 被卡死\n  mycommand: {           // 自定义快捷命令，比如 restart: 'exec pm2 restart elecV2P'\n    rtest: '/runjs test.js',    // 表示当输入命令 /rtest 或 rtest 时会自动替换成命令 '/runjs test.js' 运行 JS 脚本 test.js\n    execls: 'exec ls -al',      // 同上，表示自动将命令 /execls 替换成 exec ls -al。 其他命令可参考自行添加\n    update: {                   // 当为 object 类型时，note 表示备注显示信息， command 表示实际执行命令\n      note: '软更新升级',\n      command: 'runjs https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/softupdate.js'\n    }\n  },\n  mode: {\n    storemanage: false,         // 是否开启 store/cookie 管理模式。false: 不开启（默认），true: 开启\n  }\n}\n\nlet modenv = 'auto'    // 运行环境。默认为 auto，可选 'worker' || 'favend'\n\nif (modenv !== 'worker' && modenv !== 'favend') {\n  // 自动判断当前运行环境\n  modenv = typeof $axios === 'undefined' ? 'worker' : 'favend'\n}\nconst kvname = modenv === 'favend' ? $store : elecV2P  // elecV2P 为在 cf 上创建并绑定的 kv namespace\nconsole.log('TGbot start on mode', modenv)\n\n/************ 后面部分为主运行代码，若没有特殊情况，无需改动 ****************/\n\n/************ 简易的转化为 elecV2P favend 可用模式 ************/\nif (typeof fetch === 'undefined') {\n  function fetch(url) {\n    return $axios(url).then(res=>({\n      text(){\n        return typeof res.data === 'string' ? res.data : JSON.stringify(res.data, null, 2)\n      },\n      json(){\n        return res.data\n      }\n    }))\n  }\n}\n\nif (typeof Request === 'undefined') {\n  function Request(url, init = {}) {\n    return {\n      ...init, url\n    }\n  }\n}\n\nif (typeof Response === 'undefined') {\n  function Response(body, header) {\n    return $done({\n      response: {\n        status: 200,\n        header, body\n      }\n    })\n  }\n}\n\nconst store = {\n  put: async (key, value)=>{\n    if (modenv === 'favend') {\n      return kvname.put(value, key)\n    }\n    return await kvname.put(key, value)\n  },\n  get: async (key, type)=>{\n    if (modenv === 'favend') {\n      if (type == 'json') {\n        type = 'object'\n      }\n    }\n    return await kvname.get(key, type)\n  },\n  delete: async (key)=>{\n    await kvname.delete(key)\n  }\n}\n\nconst context = {\n  get: async (uid) => {\n    return await store.get(uid, 'json')\n  },\n  put: async (uid, uenv, command) => {\n    let ctx = await context.get(uid)\n    if (ctx === null || typeof ctx !== 'object') {\n      ctx = {\n        command: []\n      }\n    }\n    if (uenv) {\n      ctx.context = uenv\n    }\n    if (command) {\n      ctx.command ? ctx.command.push(command) : ctx.command = [command]\n    }\n    ctx.active = Date.now()\n    await store.put(uid, JSON.stringify(ctx))\n  },\n  end: async (uid) => {\n    await store.put(uid, JSON.stringify({}))\n  }\n}\n\nfunction surlName(url) {\n  if (!url) {\n    return ''\n  }\n  let name = ''\n  let sdurl = url.split(/\\/|\\?|#/)\n  while (name === '' && sdurl.length) {\n    name = sdurl.pop()\n  }\n  return name\n}\n\nfunction timeoutPromise({ timeout = CONFIG_EV2P.timeout || 5000, fn }) {\n  if (!/\\.js$/.test(fn)) {\n    fn += '.js'\n  }\n  return new Promise(resolve => setTimeout(resolve, timeout, '请求超时 ' + timeout + ' ms，相关请求应该已发送至 elecV2P，这里提前返回结果，以免发送重复请求' + `${fn ? ('\\n\\n运行日志: ' + CONFIG_EV2P.url + 'logs/' + surlName(fn) + '.log') : '' }`))\n}\n\nfunction getLogs(s){\n  if (s !== 'all' && !/\\.log$/.test(s)) {\n    s = s + '.js.log'\n  }\n  return new Promise((resolve,reject)=>{\n    fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=getlog&fn=' + s).then(res=>res.text()).then(r=>{\n      resolve(s === 'all' ? r : r.slice(CONFIG_EV2P.slice))\n    }).catch(e=>{\n      reject(e)\n    })\n  })\n}\n\nfunction delLogs(logn) {\n  return new Promise((resolve,reject)=>{\n    fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=deletelog&fn=' + logn).then(res=>res.text()).then(r=>{\n      resolve(r)\n    }).catch(e=>{\n      reject(e)\n    })\n  })\n}\n\nfunction getStatus() {\n  return new Promise((resolve,reject)=>{\n    fetch(CONFIG_EV2P.url + 'webhook?type=status&token=' + CONFIG_EV2P.wbrtoken).then(res=>res.text()).then(r=>{\n      resolve(r)\n    }).catch(e=>{\n      reject(e)\n    })\n  })\n}\n\nfunction getInfo(debug) {\n  return fetch(CONFIG_EV2P.url + 'webhook?type=info&token=' + CONFIG_EV2P.wbrtoken + (debug ? '&debug=true' : '')).then(res=>res.text())\n}\n\nfunction getTaskinfo(tid) {\n  tid = tid.replace(/^\\//, '')\n  return fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=taskinfo&tid=' + tid).then(res=>res.text())\n}\n\nfunction opTask(tid, op) {\n  if (!/start|stop|del|delete/.test(op)) {\n    return 'unknow operation' + op\n  }\n  tid = tid.replace(/^\\//, '')\n  if (/^\\/?stop/.test(tid)) {\n    op = 'stop'\n    tid = tid.replace(/^\\/?stop/, '')\n  }\n  return fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=task' + op + '&tid=' + tid).then(res=>res.text())\n}\n\nfunction saveTask() {\n  return fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=tasksave').then(res=>res.text())\n}\n\nfunction taskNew(taskinfo) {\n  // 新建任务\n  if (!taskinfo) {\n    return '没有任何任务信息'\n  }\n  let finfo = taskinfo.split(/\\r|\\n/)\n  if (finfo.length < 2) {\n    return '任务信息输入有误 '\n  }\n  taskinfo = {\n    name: finfo[2] || '新的任务' + Math.ceil(Math.random()*100),\n    type: finfo[0].split(' ').length > 4 ? 'cron' : 'schedule',\n    time: finfo[0],\n    job: {\n      type: finfo[3] || 'runjs',\n      target: finfo[1],\n    },\n    running: finfo[4] !== 'false'\n  }\n  return fetch(CONFIG_EV2P.url + 'webhook', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json'\n      },\n      body: JSON.stringify({\n        token: CONFIG_EV2P.wbrtoken,\n        type: 'taskadd',\n        task: taskinfo\n      })\n    }).then(res=>res.text())\n}\n\nfunction jsRun(fn) {\n  // 支持参数运行，参考说明文档 06-task.md 运行 JS 相关部分（elecV2P 需大于 v3.6.0\n  return Promise.race([new Promise((resolve,reject)=>{\n    fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=runjs&fn=' + encodeURI(fn)).then(res=>res.text()).then(r=>{\n      resolve(r)\n    }).catch(e=>{\n      reject(e)\n    })\n  }), timeoutPromise({ fn })])\n}\n\nfunction getJsLists() {\n  return new Promise((resolve,reject)=>{\n    fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=jslist').then(res=>res.json()).then(r=>{\n      resolve(r.rescode === 0 ? r.resdata : r)\n    }).catch(e=>{\n      reject(e)\n    })\n  })\n}\n\nfunction deleteJS(name) {\n  return fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=deletejs&fn=' + name).then(res=>res.text())\n}\n\nfunction shellRun(command) {\n  if (command) {\n    command = encodeURI(command)\n  } else {\n    return '请输入 command 指令，比如: ls'\n  }\n  return fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + `&type=shell&timeout=${CONFIG_EV2P.shell && CONFIG_EV2P.shell.timeout || 3000}&command=` + command).then(res=>res.text())\n}\n\nfunction storeManage(keyvt) {\n  if (!keyvt) {\n    return '请输入要获取的 cookie/store 相关的 key 值'\n  }\n\n  let keys = keyvt.split(' ')\n  if (keys.length === 1) {\n    return new Promise((resolve,reject)=>{\n      fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + `&type=store&key=${keyvt}`).then(res=>res.text()).then(r=>{\n        if (r) {\n          resolve(r.slice(CONFIG_EV2P.slice))\n        } else {\n          resolve(keyvt + ' 暂不存在')\n        }\n      }).catch(e=>{\n        reject(e)\n      })\n    })\n  } else {\n    let body = {\n      token: CONFIG_EV2P.wbrtoken,\n      type: 'store'\n    }\n    if (keys[0] === 'delete') {\n      body.op = 'delete'\n      body.key = keys[1]\n    } else {\n      body.op = 'put'\n      body.key = keys[0]\n      body.value = decodeURI(keys[1])\n      body.options = {\n        type: keys[2]\n      }\n    }\n    return fetch(CONFIG_EV2P.url + 'webhook', {\n        method: 'PUT',\n        headers: {\n          'Content-Type': 'application/json'\n        },\n        body: JSON.stringify(body)\n      }).then(res=>res.text())\n  }\n}\n\nfunction storeList() {\n  return new Promise((resolve,reject)=>{\n    fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=store&op=all').then(res=>res.json()).then(r=>{\n      resolve(r.rescode === 0 ? r.resdata : r)\n    }).catch(e=>{\n      reject(e)\n    })\n  })\n}\n\nfunction getFile(file_id) {\n  return new Promise((resolve,reject)=>{\n    fetch(`https://api.telegram.org/bot${CONFIG_EV2P.token}/getFile?file_id=${file_id}`).then(res=>res.json()).then(r=>{\n      if (r.ok) {\n        resolve(`https://api.telegram.org/file/bot${CONFIG_EV2P.token}/${r.result.file_path}`)\n      } else {\n        resolve(r.description)\n      }\n    }).catch(e=>{\n      reject(e)\n    })\n  })\n}\n\nasync function handlePostRequest(request) {\n  // console.log(request.body)\n  if (CONFIG_EV2P.store) {\n    let config = await store.get(CONFIG_EV2P.store, 'json')\n    if (!CONFIG_EV2P.storeforce && config) {\n      Object.assign(CONFIG_EV2P, config)\n    } else {\n      await store.put(CONFIG_EV2P.store, JSON.stringify(CONFIG_EV2P))\n    }\n  }\n  if (!CONFIG_EV2P.url.endsWith('/')) {\n    CONFIG_EV2P.url = CONFIG_EV2P.url + '/'\n  }\n  CONFIG_EV2P.timeout = CONFIG_EV2P.timeout || 5000\n\n  let bodyString = await readRequestBody(request)\n  let payload = {\n    \"method\": \"sendMessage\",\n    \"chat_id\": CONFIG_EV2P.userid[0],\n    \"parse_mode\": \"html\",\n    \"disable_web_page_preview\": true,\n  }\n\n  try {\n    let body = typeof bodyString === 'string' ? JSON.parse(bodyString) : bodyString\n    if (!body.message) {\n      payload.text = 'elecV2P bot get unknow message:\\n' + bodyString\n      await tgPush(payload)\n      return new Response(\"OK\")\n    }\n    payload[\"chat_id\"] = body.message.chat.id\n    if (body.message.document) {\n      let bodydoc = body.message.document\n      payload.text = `文件名称: ${bodydoc.file_name}\\n文件类型: ${bodydoc.mime_type}\\n文件 id: ${bodydoc.file_id}\\n`\n      let fpath = await getFile(bodydoc.file_id)\n      payload.text += `文件地址: ${fpath}\\n\\n（进一步功能待完成）`\n      await tgPush(payload)\n      return new Response(\"OK\")\n    }\n    if (body.message.text) {\n      let bodytext = body.message.text.trim()\n      let uid = 'u' + payload['chat_id']\n\n      if (CONFIG_EV2P.mycommand && Object.keys(CONFIG_EV2P.mycommand).length) {\n        let tcom = bodytext.replace(/^\\//, '')\n        if (CONFIG_EV2P.mycommand[tcom]) {\n          bodytext = CONFIG_EV2P.mycommand[tcom].command || CONFIG_EV2P.mycommand[tcom]\n        }\n      }\n      if (bodytext === 'sudo clear') {\n        await store.delete(uid)\n        payload.text = 'current context is cleared.'\n        await tgPush(payload)\n        return new Response(\"OK\")\n      } else if (bodytext === '/command') {\n        payload.text = `/runjs - 运行 JS\n/task - 任务管理模式\n/status - 内存使用状态\n/shell - shell 指令执行模式\n/store - store/cookie 管理\n/tasksave - 保存任务列表\n/taskdel + tid - 删除任务\n/deljs + JS 文件名 - 删除 JS\n/log - 获取日志\n/dellog + 日志名 - 删除日志\n/context - 查看当前执行环境\n/end - 退出当前执行环境\n/info - 查看服务器信息\n/command - 列出所有指令`\n\n        if (CONFIG_EV2P.mycommand && Object.keys(CONFIG_EV2P.mycommand).length) {\n          payload.text += '\\n\\n自定义快捷命令'\n          for (let x in CONFIG_EV2P.mycommand) {\n            payload.text += '\\n' + (x.startsWith('/') ? '' : '/') + x + ' - ' + (CONFIG_EV2P.mycommand[x].note || CONFIG_EV2P.mycommand[x])\n          }\n        }\n        await tgPush(payload)\n        return new Response(\"OK\")\n      }\n      let userenv = await context.get(uid)\n      \n      if (CONFIG_EV2P.userid && CONFIG_EV2P.userid.length && CONFIG_EV2P.userid.indexOf(body.message.chat.id) === -1) {\n        payload.text = \"这是 \" + CONFIG_EV2P.name + \" 私人 bot，不接受其他人的指令。\\n如果有兴趣可以自己搭建一个: https://github.com/elecV2/elecV2P-dei\\n\\n频道: @elecV2 | 交流群: @elecV2G\"\n        await tgPush({\n          ...payload,\n          \"chat_id\": CONFIG_EV2P.userid[0],\n          \"text\": `用户: ${body.message.chat.username}，ID: ${body.message.chat.id} 正在连接 elecV2P bot，发出指令为: ${bodytext}`\n        })\n      } else if (/^\\/?end/.test(bodytext)) {\n        await context.end(uid)\n        payload.text = `退出上文执行环境${(userenv && userenv.context) || ''}，回到普通模式`\n      } else if (/^\\/?context$/.test(bodytext)) {\n        if (userenv && userenv.context) {\n          payload.text = '当前执行环境为: ' + userenv.context + '\\n输入 /end 回到普通模式'\n        } else {\n          payload.text = '当前执行环境为: 普通模式'\n        }\n      } else if (/^\\/?status/.test(bodytext)) {\n        payload.text = await getStatus()\n      } else if (/^\\/?info/.test(bodytext)) {\n        let cont = bodytext.trim().split(' ')\n        if (cont.length === 1) {\n          payload.text = await getInfo()\n        } else if (cont.pop() === 'debug') {\n          payload.text = await getInfo('debug')\n        } else {\n          payload.text = 'unknow info command'\n        }\n      } else if (/^\\/?(dellog|deletelog) /.test(bodytext)) {\n        let cont = bodytext.replace(/^\\/?(dellog|deletelog) /, '')\n        if (!(cont === 'all' || /\\.log$/.test(cont))) cont = cont + '.js.log'\n        payload.text = await delLogs(cont)\n      } else if (/^\\/?taskinfo/.test(bodytext)) {\n        let cont = bodytext.replace(/^\\/?taskinfo/, '')\n        payload.text = await getTaskinfo(cont.trim() || 'all')\n      } else if (/^\\/?taskstart /.test(bodytext)) {\n        let cont = bodytext.replace(/^\\/?taskstart /, '')\n        payload.text = await opTask(cont, 'start')\n      } else if (/^\\/?taskstop /.test(bodytext)) {\n        let cont = bodytext.replace(/^\\/?taskstop /, '')\n        payload.text = await opTask(cont, 'stop')\n      } else if (/^\\/?taskdel /.test(bodytext)) {\n        let cont = bodytext.replace(/^\\/?taskdel /, '')\n        payload.text = await opTask(cont, 'del')\n      } else if (/^\\/?tasksave/.test(bodytext)) {\n        payload.text = await saveTask()\n      } else if (/^\\/?deljs /.test(bodytext)) {\n        let cont = bodytext.replace(/^\\/?deljs /, '')\n        payload.text = await deleteJS(cont)\n      } else if (/^\\/?task/.test(bodytext)) {\n        let cont = bodytext.trim().split(' ')\n        if (cont.length === 1) {\n          try {\n            await context.put('u' + payload['chat_id'], 'task')\n            let tasklists = await getTaskinfo('all')\n            let tlist = JSON.parse(tasklists)\n            let tlstr = []\n            for (let tid in tlist.info) {\n              tlstr.push(`${tlist.info[tid].running ? '🐢' : '🐰'} ${tlist.info[tid].name} /${tid}  |  /stop${tid}`)\n              if (tlstr.length > 80) {\n                payload.text = tlstr.join('\\n')\n                await tgPush(payload)\n                tlstr = []\n              }\n            }\n\n            payload.text = `\\n${tlstr.join('\\n')}\\n当前 elecV2P 定时任务共 ${tlist.total} 个，运行中(🐢)的任务 ${tlist.running} 个\\n点击任务名后面的 /+tid 开始任务，/+stoptid 停止任务\\n也可以手动输入对应的 tid 开始任务, stop tid 停止任务\\ntaskinfo tid 查看任务信息`\n            await tgPush(payload)\n\n            payload.text = `按照下面格式多行输入可直接添加新的任务（每行表示一个任务参数）\\n\n任务时间(cron 定时，比如: 8 0,8 * * * ，倒计时，比如: 1 10 6)\n任务目标(test.js，node -v, LOlxkcdI(某个任务的 tid)，远程 JS 链接等)\n任务名称(可省略，默认为 新的任务+随机参数)\n任务类型(可省略，默认为 运行 JS，shell: 运行 shell 指令，taskstart：开始其他任务，taskstop：停止其他任务)\n是否执行(可省略，默认为 true，当且仅当该值为 false 时，表示只添加任务信息而不运行)\n\n示例一：添加一个 cron 定时任务\n\n30 20 * * *\nhttps://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/deletelog.js\n删除日志\n\n示例二：添加一个倒计时任务，运行 test.js，每次倒计时 1 秒，执行 3 次\n\n1 3\ntest.js`\n          } catch(e) {\n            payload.text = e.message\n          }\n        } else {\n          payload.text = 'unknow task operation'\n        }\n      } else if (/^\\/?runjs/.test(bodytext)) {\n        let cont = bodytext.trim().split(/ +/)\n        if (cont.length === 1) {\n          try {\n            await context.put('u' + payload['chat_id'], 'runjs')\n            let jslists = await getJsLists()\n            let keyb = {\n              keyboard: [],\n              resize_keyboard: false,\n              one_time_keyboard: true,\n              selective: true\n            }\n            let over = ''\n            if (jslists.length >= 200) {\n              over = '\\n\\n文件数超过 200，以防 reply_keyboard 过长 TG 无返回，剩余 JS 以文字形式返回\\n\\n'\n            }\n            for (let ind in jslists) {\n              let s = jslists[ind]\n              if (ind >= 200) {\n                over += s + '  '\n                continue\n              }\n\n              let row = parseInt(ind/2)\n              keyb.keyboard[row]\n              ? keyb.keyboard[row].push({\n                text: s.replace(/\\.js$/, '')\n              })\n              : keyb.keyboard[row] = [{\n                text: s.replace(/\\.js$/, '')\n              }]\n            }\n            payload.text = '进入 RUNJS 模式，当前 elecV2P 上 JS 文件数: ' + jslists.length + '\\n点击交互键盘可直接运行 JS，也可以输入文件名或者远程链接运行其他脚本\\n后面可附带 -env/-rename 等参数(v3.6.0)，比如\\nhttps://远程JSname.js -rename=t.js' + over.trimRight()\n            payload.reply_markup = keyb\n          } catch(e) {\n            payload.text = e.message\n          }\n        } else {\n          payload.text = await jsRun(bodytext.replace(/^\\/?runjs /, ''))\n        }\n      } else if (/^\\/?(shell|exec)/.test(bodytext)) {\n        let cont = bodytext.trim().split(' ')\n        if (cont.length === 1) {\n          try {\n            await context.put('u' + payload['chat_id'], 'shell')\n            let keyb = {\n              keyboard: [\n                [{text: 'ls'}, {text: 'node -v'}],\n                [{text: 'apk add python3 ffmpeg'}],\n                [{text: 'python3 -V'}, {text: 'pm2 ls'}]\n              ],\n              resize_keyboard: false,\n              one_time_keyboard: true,\n              selective: true\n            }\n            payload.text = '进入 SHELL 模式，可执行简单 shell 指令，比如: ls, node -v 等'\n            payload.reply_markup = keyb\n          } catch(e) {\n            payload.text = e.message\n          }\n        } else {\n          payload.text = await shellRun(bodytext.replace(/^\\/?(shell|exec) /, ''))\n        }\n      } else if (/^\\/?store/.test(bodytext)) {\n        if (CONFIG_EV2P.mode && CONFIG_EV2P.mode.storemanage) {\n          let cont = bodytext.trim().split(' ')\n          if (cont.length === 1) {\n            try {\n              await context.put('u' + payload['chat_id'], 'store')\n              let storelists = await storeList()\n              let keyb = {\n                keyboard: [],\n                resize_keyboard: false,\n                one_time_keyboard: true,\n                selective: true\n              }\n              let over = ''\n              if (storelists.length >= 200) {\n                over = '\\n\\nCookie 数超过 200，以防 reply_keyboard 过长 TG 无返回，剩余 Cookie KEY 以文字形式返回\\n\\n'\n              }\n              for (let ind in storelists) {\n                let s = storelists[ind]\n                if (ind >= 200) {\n                  over += s + '  '\n                  continue\n                }\n\n                let row = parseInt(ind/2)\n                keyb.keyboard[row]\n                ? keyb.keyboard[row].push({\n                  text: s\n                })\n                : keyb.keyboard[row] = [{\n                  text: s\n                }]\n              }\n              payload.reply_markup = keyb\n              payload.text = '进入 cookie/store 管理模式，当前 elecV2P 上 Cookie 数: ' + storelists.length + '\\n\\n点击或者直接输入关键字(key)查看 store 内容，比如 cookieKEY\\n\\n输入 delete key 删除某个 Cookie。比如: delete cookieKEY\\n\\n输入 key value type(可省略) 修改 store 内容(以空格进行分隔)。如果 value 中包含空格等其他特殊字符，请先使用 encodeURI 函数进行转换。比如:\\n\\nCookieJD pt_pin=xxx;%20pt_key=app_xxxxxxx;\\n\\ntype 可省略，也可设定为:\\nstring 表示将 value 保存为普通字符(默认)\\nobject 表示将 value 保存为 json 格式\\na 表示在原来的值上新增。（更多说明可参考 https://github.com/elecV2/elecV2P-dei/tree/master/docs/04-JS.md $store 部分）' + over\n            } catch(e) {\n              payload.text = e.message\n            }\n          } else {\n            payload.text = await storeManage(bodytext.replace(/^\\/?store /, ''))\n          }\n        } else {\n          payload.text = 'store/cookie 管理模式处于关闭状态'\n        }\n      } else if (/^\\/?log/.test(bodytext)) {\n        let cont = bodytext.trim().split(' ')\n        if (cont.length === 1) {\n          try {\n            await context.put('u' + payload['chat_id'], 'log')\n            let res = await getLogs('all')\n            let map = JSON.parse(res)\n            let keyb = {\n                  inline_keyboard: [ ],\n                }\n\n            map.forEach((s, ind)=> {\n              let row = parseInt(ind/2)\n              keyb.inline_keyboard[row]\n              ? keyb.inline_keyboard[row].push({\n                text: s.replace(/\\.js\\.log$/g, ''),\n                url: CONFIG_EV2P.url + 'logs/' + s\n              }) \n              : keyb.inline_keyboard[row] = [{\n                text: s.replace(/\\.js\\.log$/g, ''),\n                url: CONFIG_EV2P.url + 'logs/' + s\n              }]\n            })\n            payload.text = \"开始日志查看模式，当前 elecV2P 上日志文件数: \" + map.length + \"\\n点击查看日志或者直接输入 log 文件名进行查看\"\n            payload.reply_markup = keyb\n          } catch(e) {\n            payload.text = e.message\n          }\n        } else {\n          payload.text = await getLogs(bodytext.replace(/^\\/?log /, ''))\n        }\n      } else if (userenv && userenv.context) {\n        switch (userenv.context) {\n          case 'log':\n            payload.text = await getLogs(bodytext)\n            break\n          case 'runjs':\n            payload.text = await jsRun(bodytext)\n            break\n          case 'task':\n            if (bodytext.trim().split(/\\r|\\n/).length > 1) {\n              payload.text = await taskNew(bodytext)\n            } else {\n              payload.text = await opTask(bodytext.split(' ').pop(), /^(🐢|\\/?stop)/.test(bodytext) ? 'stop' : 'start')\n            }\n            break\n          case 'shell':\n            if (Date.now() - userenv.active > (CONFIG_EV2P.shell && CONFIG_EV2P.shell.contexttimeout)) {\n              payload.text = '已经超过 ' + CONFIG_EV2P.shell.contexttimeout/1000/60 + ' 分钟没有执行 shell 指令，自动退出 shell 模式\\n使用 /shell 命令重新进入\\n/end 回到普通模式\\n/command 查看所有指令'\n              payload.reply_markup = JSON.stringify({\n                remove_keyboard: true\n              })\n              userenv.context = 'normal'\n            } else {\n              payload.text = await shellRun(bodytext)\n            }\n            break\n          case 'store':\n            if (CONFIG_EV2P.mode && CONFIG_EV2P.mode.storemanage) {\n              payload.text = await storeManage(bodytext)\n            } else {\n              payload.text = 'store/cookie 管理模式处于关闭状态'\n            }\n            break\n          default: {\n            payload.text = '当前执行环境: ' + userenv.context + ' 无法处理指令: ' + bodytext\n          }\n        }\n        await context.put(uid, userenv.context, bodytext)\n      } else {\n        payload.text = 'TGbot 部署成功，可以使用相关指令和 elecV2P 服务器进行交互了\\nPowered By: https://github.com/elecV2/elecV2P\\n\\n频道: @elecV2 | 交流群: @elecV2G'\n        if (CONFIG_EV2P.userid.length === 0) {\n          payload.text += '\\n（❗️危险⚠️）当前 elecV2P bot 并没有设置 userid，所有人可进行交互'\n        }\n        if (bodytext === '/start') {\n          let status = ''\n          try {\n            status = await getStatus()\n            status = '当前 bot 与 elecV2P 连接成功 ' + status\n          } catch(e) {\n            status = (e.message || e) + '\\nelecV2P 服务器没有响应，请检查服务器地址和 webhook token 是否设置正确。'\n          }\n          payload.text += '\\n' + status\n        }\n      }\n\n      await tgPush(payload)\n      return new Response(\"OK\")\n    }\n    return new Response(JSON.stringify(body), {\n      headers: { 'content-type': 'application/json' },\n    })\n  } catch(e) {\n    console.error(e)\n    console.log('payload', payload)\n    payload.text = e.message || e\n    await tgPush(payload)\n    return new Response(\"OK\")\n  }\n}\n\nasync function handleRequest(request) {\n  let retBody = `welcome to elecV2P Bot\\n\\n请根据 https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/examples/JSTEST/TGbotonFavend.js 中的注释进行配置\\n\\nPowered By: https://github.com/elecV2/elecV2P\\n\\nTG 频道: https://t.me/elecV2 | TG 交流群: https://t.me/elecV2G`\n  return new Response(retBody)\n}\n\nif (modenv === 'worker') {\n  addEventListener('fetch', event => {\n    const { request } = event\n    // const { url } = request\n    if (request.method === 'POST') {\n      return event.respondWith(handlePostRequest(request))\n    } else if (request.method === 'GET') {\n      return event.respondWith(handleRequest(request))\n    }\n  })\n} else {\n  if ($request.method === 'GET') {\n    handleRequest()\n  } else {\n    handlePostRequest($request)\n  }\n}\n\n/**\n * readRequestBody reads in the incoming request body\n * Use await readRequestBody(..) in an async function to get the string\n * @param {Request} request the incoming request to read from\n */\nasync function readRequestBody(request) {\n  if (modenv === 'favend') {\n    return request.body\n  }\n  const { headers } = request\n  const contentType = headers['Content-Type'] || headers.get('content-type')\n  if (contentType.includes('application/json')) {\n    const body = await request.json()\n    return JSON.stringify(body)\n  } else if (contentType.includes('application/text')) {\n    const body = await request.text()\n    return body\n  } else if (contentType.includes('text/html')) {\n    const body = await request.text()\n    return body\n  } else if (contentType.includes('form')) {\n    const formData = await request.formData()\n    let body = {}\n    for (let entry of formData.entries()) {\n      body[entry[0]] = entry[1]\n    }\n    return JSON.stringify(body)\n  } else {\n    let myBlob = await request.blob()\n    var objectURL = URL.createObjectURL(myBlob)\n    return objectURL\n  }\n}\n\nasync function tgPush(payload) {\n  const myInit = {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json;charset=UTF-8'\n    }\n  };\n  let maxbLength = 1200;\n  if (payload.text && payload.text.length > maxbLength) {\n    let reply_text = payload.text\n    let pieces = Math.ceil(reply_text.length / maxbLength);\n    for (let i=0; i<pieces; i++) {\n      payload.text = reply_text.slice(i*maxbLength, (i+1)*maxbLength);\n      myInit.body = JSON.stringify(payload)\n      let myRequest = new Request(`https://api.telegram.org/bot${CONFIG_EV2P.token}/`, myInit);\n      await fetch(myRequest);\n      if (payload.reply_markup) {\n        delete payload.reply_markup\n      }\n    }\n  } else {\n    myInit.body = JSON.stringify(payload);\n    let myRequest = new Request(`https://api.telegram.org/bot${CONFIG_EV2P.token}/`, myInit);\n    await fetch(myRequest);\n  }\n}"
  },
  {
    "path": "examples/JSTEST/aria2-env.js",
    "content": "// 需提前配置好 aria2 使用环境, 使命令 aria2c 在系统 Shell 环境中可用\r\n\r\n// task runjs example:\r\n// 运行 JS: aria2-env.js -e dlink=https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/webhook.js\r\n\r\nlet dlink = 'https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/0body.js'\r\nif (typeof($dlink) !== \"undefined\") {\r\n  dlink = $dlink\r\n}\r\n\r\n$exec(`aria2c ${dlink} -d ${__efss}`, {\r\n  call: true,\r\n  cb(data, error, success){\r\n    if (success) {\r\n      console.log('aria2 download complete!', 'dlink:' + dlink)\r\n      $feed.ifttt('aria2 download complete!', 'dlink:' + dlink, __home + '/efss')\r\n    } else {\r\n      error ? console.error(error) : console.log(data)\r\n    }\r\n  }\r\n})"
  },
  {
    "path": "examples/JSTEST/asyncPool.js",
    "content": "/**\n * 异步并行执行函数及限制（待优化）\n * 已实现功能:\n * - 可在 cb 函数中动态添加参数\n * - 可在 cb 中提前结束运行\n * 待优化部分:\n * - 逻辑再写得清晰/简洁一点？\n * author     https://t.me/elecV2\n * update     https://github.com/elecV2/elecV2P-dei/blob/master/examples/JSTEST/asyncPool.js\n * @param     {Function}    fn       待执行的异步函数\n * @param     {Array}       params   函数传入参数\n * @param     {Function}    cb       回调函数\n * @param     {Number}      limit    同时并发执行数\n * @return    {Promise}\n */\nfunction promisePool(fn, params, { cb, limit = 6, log = false }) {\n  if (typeof(fn) !== 'function') {\n    return Promise.reject('a function is expect')\n  }\n  if (!Array.isArray(params)) {\n    return Promise.reject('a array of params is expect')\n  }\n  let cnlog = (...args)=>{\n    if (log) {\n      console.log.apply(null, args)\n    }\n  }\n  let cback = async (options = {}) => {\n    // callback 可能会加入新的 params\n    if (typeof(cb) === 'function') {\n      return await cb(options)\n    } else {\n      cnlog(options)\n    }\n  }\n  let last  = 0, fail = [], cbdone = false\n  let orbit = new Map()\n  let isCbDone = (flag = false)=>{\n    // call force done 只生效一次\n    if (flag === 'done') {\n      cbdone = true\n    }\n    return cbdone\n  }\n  let nTask = async (idx) => {\n      let curt = last++, orbitdone = orbit.get(idx) || []\n      await cback({ message: `orbit ${idx} start`, orbit: idx, running: curt, done: orbitdone })\n      cnlog('orbit', idx, 'start task', curt)\n      let res = null, tempdone = false\n      try {\n        res = await fn(params[curt])\n        orbitdone.push(curt)\n        orbit.set(idx, orbitdone)\n        tempdone = await cback({ message: `task ${curt} finish`, orbit: idx, done: orbitdone, res })\n      } catch(err) {\n        console.error('task', curt, 'fail, data', params[curt])\n        fail.push(curt)\n        tempdone = await cback({ message: `task ${curt} fail with data ${params[curt]}`, orbit: idx, fail: err.message || err })\n      }\n      if (last >= params.length) {\n        orbit.delete(idx)\n        if (orbit.size === 0) {\n          cnlog('all task done')\n          await cback({ message: `total tasks ${last}, all finished`, finish: true, done: last, fail })\n        }\n      } else {\n        if (isCbDone(tempdone)) {\n          if (tempdone) {\n            cnlog('force done by callback, fail', fail, 'current task', curt, 'with param', params[curt])\n          }\n          throw Error('force done by callback')\n        } else {\n          await nTask(idx)\n        }\n      }\n    }\n\n  return Promise.all(new Array(Math.min(limit, params.length)).fill(1).map((s, idx)=>nTask(idx)))\n}"
  },
  {
    "path": "examples/JSTEST/boxjs.ev.js",
    "content": "// elecV2P v3.2.3 版本后，可直接使用 chavyleung 的原版 boxjs。本脚本不再维护\n// \n// BoxJs elecV2P 兼容版。修改自：https://github.com/chavyleung/scripts/tree/master/box\n// 简易修改，测试使用，不保证原 BoxJs 的所有功能能正常工作。\n// 使用方法：\n// - 在 webUI->REWRITE/重写请求 添加规则: ^http://boxjs\\.com 网络请求前 https://raw.githubusercontent.com/chavyleung/scripts/master/box/chavy.boxjs.js\n// - 然后将 boxjs.com 这个域名代理到 ANYPROXY 端口（确保端口已打开，默认为 127.0.0.1:8001）\n// （如果使用 chrome 浏览器推荐使用 SwitchyOmega 插件来进行分流设置，也可以直接使用系统代理）\n// - 最后浏览器打开 http://boxjs.com (http 访问无需安装证书，如果是 https 访问，在 MITM 页面下载安装证书)\n// \n// 说明事项：\n// - boxjs.com 可替换为任一域名，比如 e.com\n// - 访问一个网址后浏览器会有缓存，如果首次测试失败，建议修改域名后再次尝试。（比如 e1.com/e2.cn/e3.org 等等）\n// - 如果在 boxjs 中无法运行脚本，尝试在右上角菜单中清空 HTTP-API 内容，或者直接在左上角调整为 QuanX或Loon 模式\n\nconst $ = new Env('BoxJs')\n\n// 为 eval 准备的上下文环境\nconst $eval_env = {}\n\n$.version = '0.7.69'\n$.versionType = 'beta'\n\n// 发出的请求需要需要 Surge、QuanX 的 rewrite\n$.isNeedRewrite = true\n\n/**\n * ===================================\n * 持久化属性: BoxJs 自有的数据结构\n * ===================================\n */\n\n// 存储`用户偏好`\n$.KEY_usercfgs = 'chavy_boxjs_userCfgs'\n// 存储`应用会话`\n$.KEY_sessions = 'chavy_boxjs_sessions'\n// 存储`页面缓存`\n$.KEY_web_cache = 'chavy_boxjs_web_cache'\n// 存储`应用订阅缓存`\n$.KEY_app_subCaches = 'chavy_boxjs_app_subCaches'\n// 存储`全局备份`\n$.KEY_globalBaks = 'chavy_boxjs_globalBaks'\n// 存储`当前会话` (配合切换会话, 记录当前切换到哪个会话)\n$.KEY_cursessions = 'chavy_boxjs_cur_sessions'\n\n/**\n * ===================================\n * 持久化属性: BoxJs 公开的数据结构\n * ===================================\n */\n\n// 存储用户访问`BoxJs`时使用的域名\n$.KEY_boxjs_host = 'boxjs_host'\n\n// 请求响应体 (返回至页面的结果)\n$.json = $.name // `接口`类请求的响应体\n$.html = $.name // `页面`类请求的响应体\n\n// 页面源码地址\n$.web = `https://cdn.jsdelivr.net/gh/chavyleung/scripts@${$.version}/box/chavy.boxjs.html?_=${new Date().getTime()}`\n// 版本说明地址 (Release Note)\n$.ver = 'https://cdn.jsdelivr.net/gh/chavyleung/scripts@${$.version}/box/release/box.release.tf.json';\n\n(async () => {\n  // 勿扰模式\n  $.isMute = [true, 'true'].includes($.getdata('@chavy_boxjs_userCfgs.isMute'))\n\n  // 请求路径\n  $.path = getPath($request.url)\n\n  // 请求类型: GET\n  $.isGet = $request.method === 'GET'\n  // 请求类型: POST\n  $.isPost = $request.method === 'POST'\n  // 请求类型: OPTIONS\n  $.isOptions = $request.method === 'OPTIONS'\n\n  // 请求类型: page、api、query\n  $.type = 'page'\n  // 查询请求: /query/xxx\n  $.isQuery = $.isGet && /^\\/query\\/.*?/.test($.path)\n  // 接口请求: /api/xxx\n  $.isApi = $.isPost && /^\\/api\\/.*?/.test($.path)\n  // 页面请求: /xxx\n  $.isPage = $.isGet && !$.isQuery && !$.isApi\n\n  // 升级用户数据\n  upgradeUserData()\n\n  // 处理预检请求\n  if ($.isOptions) {\n    $.type = 'options'\n    await handleOptions()\n  }\n  // 处理`页面`请求\n  else if ($.isPage) {\n    $.type = 'page'\n    await handlePage()\n  }\n  // 处理`查询`请求\n  else if ($.isQuery) {\n    $.type = 'query'\n    await handleQuery()\n  }\n  // 处理`接口`请求\n  else if ($.isApi) {\n    $.type = 'api'\n    await handleApi()\n  }\n})()\n  .catch((e) => $.logErr(e))\n  .finally(() => doneBox())\n\n/**\n * http://boxjs.com/ => `http://boxjs.com`\n * http://boxjs.com/app/jd => `http://boxjs.com`\n */\nfunction getHost(url) {\n  return url.slice(0, url.indexOf('/', 8))\n}\n\n/**\n * http://boxjs.com/ => ``\n * http://boxjs.com/api/getdata => `/api/getdata`\n */\nfunction getPath(url) {\n  // 如果以`/`结尾, 去掉最后一个`/`\n  const end = url.lastIndexOf('/') === url.length - 1 ? -1 : undefined\n  // slice第二个参数传 undefined 会直接截到最后\n  // indexOf第二个参数用来跳过前面的 \"https://\"\n  return url.slice(url.indexOf('/', 8), end)\n}\n\n/**\n * ===================================\n * 处理前端请求\n * ===================================\n */\n\n/**\n * 处理`页面`请求\n */\nasync function handlePage() {\n  // 获取 BoxJs 数据\n  const boxdata = getBoxData()\n  boxdata.syscfgs.isDebugMode = false\n\n  // 调试模式: 是否每次都获取新的页面\n  const isDebugWeb = [true, 'true'].includes($.getdata('@chavy_boxjs_userCfgs.isDebugWeb'))\n  const debugger_web = $.getdata('@chavy_boxjs_userCfgs.debugger_web')\n  const cache = $.getjson($.KEY_web_cache, null)\n\n  // 如果没有开启调试模式，且当前版本与缓存版本一致，且直接取缓存\n  if (!isDebugWeb && cache && cache.version === $.version) {\n    $.html = cache.cache\n  }\n  // 如果开启了调试模式，并指定了 `debugger_web` 则从指定的地址获取页面\n  else {\n    if (isDebugWeb && debugger_web) {\n      // 调试地址后面拼时间缀, 避免 GET 缓存\n      const isQueryUrl = debugger_web.includes('?')\n      $.web = `${debugger_web}${isQueryUrl ? '&' : '?'}_=${new Date().getTime()}`\n      boxdata.syscfgs.isDebugMode = true\n      console.log(`[WARN] 调试模式: $.web = : ${$.web}`)\n    }\n    // 如果调用这个方法来获取缓存, 且标记为`非调试模式`\n    const getcache = () => {\n      console.log(`[ERROR] 调试模式: 正在使用缓存的页面!`)\n      boxdata.syscfgs.isDebugMode = false\n      return $.getjson($.KEY_web_cache).cache\n    }\n    await $.http.get($.web).then(\n      (resp) => {\n        if (/<title>BoxJs<\\/title>/.test(resp.body)) {\n          // 返回页面源码, 并马上存储到持久化仓库\n          $.html = resp.body\n          const cache = { version: $.version, cache: $.html }\n          $.setjson(cache, $.KEY_web_cache)\n        } else {\n          // 如果返回的页面源码不是预期的, 则从持久化仓库中获取\n          $.html = getcache()\n        }\n      },\n      // 如果获取页面源码失败, 则从持久化仓库中获取\n      () => ($.html = getcache())\n    )\n  }\n  // 根据偏好设置, 替换首屏颜色 (如果是`auto`则交由页面自适应)\n  const theme = $.getdata('@chavy_boxjs_userCfgs.theme')\n  if (theme === 'light') {\n    $.html = $.html.replace('#121212', '#fff')\n  } else if (theme === 'dark') {\n    $.html = $.html.replace('#fff', '#121212')\n  }\n  /**\n   * 后端渲染数据, 感谢 https://t.me/eslint 提供帮助\n   *\n   * 如果直接渲染到 box: null 会出现双向绑定问题\n   * 所以先渲染到 `boxServerData: null` 再由前端 `this.box = this.boxServerData` 实现双向绑定\n   */\n  $.html = $.html.replace('boxServerData: null', 'boxServerData:' + JSON.stringify(boxdata))\n\n  // 调试模式支持 vue Devtools (只有在同时开启调试模式和指定了调试地址才生效)\n  // vue.min.js 生效时, 会导致 @click=\"window.open()\" 报 \"window\" is not defined 错误\n  if (isDebugWeb && debugger_web) {\n    $.html = $.html.replace('vue.min.js', 'vue.js')\n  }\n}\n\n/**\n * 处理`查询`请求\n */\nasync function handleQuery() {\n  const [, query] = $.path.split('/query')\n  if (/^\\/boxdata/.test(query)) {\n    $.json = getBoxData()\n  } else if (/^\\/baks/.test(query)) {\n    const globalbaks = getGlobalBaks(true)\n    $.json = { globalbaks }\n  } else if (/^\\/versions$/.test(query)) {\n    await getVersions(true)\n  }\n}\n\n/**\n * 处理 API 请求\n */\nasync function handleApi() {\n  const [, api] = $.path.split('/api')\n\n  if (api === '/save') {\n    await apiSave()\n  } else if (api === '/addAppSub') {\n    await apiAddAppSub()\n  } else if (api === '/reloadAppSub') {\n    await apiReloadAppSub()\n  } else if (api === '/delGlobalBak') {\n    await apiDelGlobalBak()\n  } else if (api === '/updateGlobalBak') {\n    await apiUpdateGlobalBak()\n  } else if (api === '/saveGlobalBak') {\n    await apiSaveGlobalBak()\n  } else if (api === '/impGlobalBak') {\n    await apiImpGlobalBak()\n  } else if (api === '/revertGlobalBak') {\n    await apiRevertGlobalBak()\n  } else if (api === '/runScript') {\n    await apiRunScript()\n  }\n}\n\nasync function handleOptions() {}\n\n/**\n * ===================================\n * 获取基础数据\n * ===================================\n */\n\nfunction getBoxData() {\n  const datas = {}\n  const usercfgs = getUserCfgs()\n  const sessions = getAppSessions()\n  const curSessions = getCurSessions()\n  const sysapps = getSystemApps()\n  const syscfgs = getSystemCfgs()\n  const appSubCaches = getAppSubCaches()\n  const globalbaks = getGlobalBaks()\n\n  // 把 `内置应用`和`订阅应用` 里需要持久化属性放到`datas`\n  sysapps.forEach((app) => Object.assign(datas, getAppDatas(app)))\n  usercfgs.appsubs.forEach((sub) => {\n    const subcache = appSubCaches[sub.url]\n    if (subcache && subcache.apps && Array.isArray(subcache.apps)) {\n      subcache.apps.forEach((app) => Object.assign(datas, getAppDatas(app)))\n    }\n  })\n\n  const box = { datas, usercfgs, sessions, curSessions, sysapps, syscfgs, appSubCaches, globalbaks }\n  return box\n}\n\n/**\n * 获取系统配置\n */\nfunction getSystemCfgs() {\n  // prettier-ignore\n  return {\n    env: $.isLoon() ? 'Loon' : $.isQuanX() ? 'QuanX' : $.isSurge() ? 'Surge' : 'Node',\n    version: $.version,\n    versionType: $.versionType,\n    envs: [\n      { id: 'Surge', icons: ['https://raw.githubusercontent.com/Orz-3/mini/none/surge.png', 'https://raw.githubusercontent.com/Orz-3/mini/master/Color/surge.png'] },\n      { id: 'QuanX', icons: ['https://raw.githubusercontent.com/Orz-3/mini/none/quanX.png', 'https://raw.githubusercontent.com/Orz-3/mini/master/Color/quantumultx.png'] },\n      { id: 'Loon', icons: ['https://raw.githubusercontent.com/Orz-3/mini/none/loon.png', 'https://raw.githubusercontent.com/Orz-3/mini/master/Color/loon.png'] }\n    ],\n    chavy: { id: 'ChavyLeung', icon: 'https://avatars3.githubusercontent.com/u/29748519', repo: 'https://github.com/chavyleung/scripts' },\n    senku: { id: 'GideonSenku', icon: 'https://avatars1.githubusercontent.com/u/39037656', repo: 'https://github.com/GideonSenku' },\n    id77: { id: 'id77', icon: 'https://avatars0.githubusercontent.com/u/9592236', repo: 'https://github.com/id77' },\n    orz3: { id: 'Orz-3', icon: 'https://raw.githubusercontent.com/Orz-3/mini/master/Color/Orz-3.png', repo: 'https://github.com/Orz-3/' },\n    boxjs: { id: 'BoxJs', show: false, icon: 'https://raw.githubusercontent.com/Orz-3/mini/master/Color/box.png', icons: ['https://raw.githubusercontent.com/Orz-3/mini/master/Alpha/box.png', 'https://raw.githubusercontent.com/Orz-3/mini/master/Color/box.png'], repo: 'https://github.com/chavyleung/scripts' },\n    defaultIcons: ['https://raw.githubusercontent.com/Orz-3/mini/master/Alpha/appstore.png', 'https://raw.githubusercontent.com/Orz-3/mini/master/Color/appstore.png']\n  }\n}\n\n/**\n * 获取内置应用\n */\nfunction getSystemApps() {\n  // prettier-ignore\n  const sysapps = [\n    {\n      id: 'BoxSetting',\n      name: '偏好设置',\n      descs: ['可设置 http-api 地址 & 超时时间 (Surge TF)', '可设置明暗两种主题下的主色调'],\n      keys: [\n        '@chavy_boxjs_userCfgs.httpapi', \n        '@chavy_boxjs_userCfgs.bgimg', \n        '@chavy_boxjs_userCfgs.color_dark_primary', \n        '@chavy_boxjs_userCfgs.color_light_primary'\n      ],\n      settings: [\n        { id: '@chavy_boxjs_userCfgs.httpapis', name: 'HTTP-API (Surge TF)', val: '', type: 'textarea', placeholder: ',examplekey@127.0.0.1:6166', autoGrow: true, rows: 2, persistentHint:true, desc: '示例: ,examplekey@127.0.0.1:6166! 注意: 以逗号开头, 逗号分隔多个地址, 可加回车' },\n        { id: '@chavy_boxjs_userCfgs.httpapi_timeout', name: 'HTTP-API Timeout (Surge TF)', val: 20, type: 'number', persistentHint:true, desc: '如果脚本作者指定了超时时间, 会优先使用脚本指定的超时时间.' },\n        { id: '@chavy_boxjs_userCfgs.bgimgs', name: '背景图片清单', val: '无,\\n跟随系统,跟随系统\\nlight,http://api.btstu.cn/sjbz/zsy.php\\ndark,https://uploadbeta.com/api/pictures/random\\n妹子,http://api.btstu.cn/sjbz/zsy.php', type: 'textarea', placeholder: '无,{回车} 跟随系统,跟随系统{回车} light,图片地址{回车} dark,图片地址{回车} 妹子,图片地址', persistentHint:true, autoGrow: true, rows: 2, desc: '逗号分隔名字和链接, 回车分隔多个地址' },\n        { id: '@chavy_boxjs_userCfgs.bgimg', name: '背景图片', val: '', type: 'text', placeholder: 'http://api.btstu.cn/sjbz/zsy.php', persistentHint:true, desc: '输入背景图标的在线链接' },\n        { id: '@chavy_boxjs_userCfgs.color_light_primary', name: '明亮色调', canvas: true, val: '#F7BB0E', type: 'colorpicker', desc: '' },\n        { id: '@chavy_boxjs_userCfgs.color_dark_primary', name: '暗黑色调', canvas: true, val: '#2196F3', type: 'colorpicker', desc: '' }\n      ],\n      author: '@chavyleung',\n      repo: 'https://github.com/chavyleung/scripts/blob/master/box/switcher/box.switcher.js',\n      icons: [\n        'https://raw.githubusercontent.com/chavyleung/scripts/master/box/icons/BoxSetting.mini.png', \n        'https://raw.githubusercontent.com/chavyleung/scripts/master/box/icons/BoxSetting.png'\n      ]\n    },\n    {\n      id: 'BoxSwitcher',\n      name: '会话切换',\n      desc: '打开静默运行后, 切换会话将不再发出系统通知 \\n注: 不影响日志记录',\n      keys: [],\n      settings: [{ id: 'CFG_BoxSwitcher_isSilent', name: '静默运行', val: false, type: 'boolean', desc: '切换会话时不发出系统通知!' }],\n      author: '@chavyleung',\n      repo: 'https://github.com/chavyleung/scripts/blob/master/box/switcher/box.switcher.js',\n      icons: [\n        'https://raw.githubusercontent.com/chavyleung/scripts/master/box/icons/BoxSwitcher.mini.png', \n        'https://raw.githubusercontent.com/chavyleung/scripts/master/box/icons/BoxSwitcher.png'\n      ],\n      script: 'https://raw.githubusercontent.com/chavyleung/scripts/master/box/switcher/box.switcher.js'\n    }\n  ]\n  return sysapps\n}\n\n/**\n * 获取用户配置\n */\nfunction getUserCfgs() {\n  const defcfgs = { favapps: [], appsubs: [], isPinedSearchBar: true, httpapi: 'examplekey@127.0.0.1:6166' }\n  const usercfgs = Object.assign(defcfgs, $.getjson($.KEY_usercfgs, {}))\n\n  // 处理异常数据：删除所有为 null 的订阅\n  if (usercfgs.appsubs.includes(null)) {\n    usercfgs.appsubs = usercfgs.appsubs.filter((sub) => sub)\n    $.setjson(usercfgs, $.KEY_usercfgs)\n  }\n\n  return usercfgs\n}\n\n/**\n * 获取`应用订阅`缓存\n */\nfunction getAppSubCaches() {\n  return $.getjson($.KEY_app_subCaches, {})\n}\n\n/**\n * 获取全局备份\n * 默认只获取备份的基础信息, 如: id, name……\n *\n * @param {boolean} isComplete 是否获取完整的备份数据\n */\nfunction getGlobalBaks(isComplete = false) {\n  const globalbaks = $.getjson($.KEY_globalBaks, [])\n  if (isComplete) {\n    return globalbaks\n  } else {\n    // isComplete === false: 不返回备份体\n    globalbaks.forEach((bak) => delete bak.bak)\n    return globalbaks\n  }\n}\n/**\n * 获取版本清单\n */\nfunction getVersions() {\n  return $.http.get($.ver).then(\n    (resp) => {\n      try {\n        $.json = $.toObj(resp.body)\n      } catch {\n        $.json = {}\n      }\n    },\n    () => ($.json = {})\n  )\n}\n\n/**\n * 获取用户应用\n */\nfunction getUserApps() {\n  // TODO 用户可在 BoxJs 中自定义应用, 格式与应用订阅一致\n  return []\n}\n\n/**\n * 获取应用会话\n */\nfunction getAppSessions() {\n  return $.getjson($.KEY_sessions, [])\n}\n\n/**\n * 获取当前切换到哪个会话\n */\nfunction getCurSessions() {\n  return $.getjson($.KEY_cursessions, {})\n}\n\n/**\n * ===================================\n * 接口类函数\n * ===================================\n */\n\nfunction getAppDatas(app) {\n  const datas = {}\n  const nulls = [null, undefined, 'null', 'undefined']\n  if (app.keys && Array.isArray(app.keys)) {\n    app.keys.forEach((key) => {\n      const val = $.getdata(key)\n      datas[key] = nulls.includes(val) ? null : val\n    })\n  }\n  if (app.settings && Array.isArray(app.settings)) {\n    app.settings.forEach((setting) => {\n      const key = setting.id\n      const val = $.getdata(key)\n      datas[key] = nulls.includes(val) ? null : val\n    })\n  }\n  return datas\n}\n\nasync function apiSave() {\n  const data = $.toObj($request.body)\n  if (Array.isArray(data)) {\n    data.forEach((dat) => $.setdata(dat.val, dat.key))\n  } else {\n    $.setdata(data.val, data.key)\n  }\n  $.json = getBoxData()\n}\n\nasync function apiAddAppSub() {\n  const sub = $.toObj($request.body)\n  // 添加订阅\n  const usercfgs = getUserCfgs()\n  usercfgs.appsubs.push(sub)\n  $.setjson(usercfgs, $.KEY_usercfgs)\n  // 加载订阅缓存\n  await reloadAppSubCache(sub.url)\n  $.json = getBoxData()\n}\n\nasync function apiReloadAppSub() {\n  const sub = $.toObj($request.body)\n  if (sub) {\n    await reloadAppSubCache(sub.url)\n  } else {\n    await reloadAppSubCaches()\n  }\n  $.json = getBoxData()\n}\n\nasync function apiDelGlobalBak() {\n  const bak = $.toObj($request.body)\n  const globalbaks = $.getjson($.KEY_globalBaks, [])\n  const bakIdx = globalbaks.findIndex((b) => b.id === bak.id)\n  if (bakIdx > -1) {\n    globalbaks.splice(bakIdx, 1)\n    $.setjson(globalbaks, $.KEY_globalBaks)\n  }\n  $.json = getBoxData()\n}\n\nasync function apiUpdateGlobalBak() {\n  const { id: bakId, name: bakName } = $.toObj($request.body)\n  const globalbaks = $.getjson($.KEY_globalBaks, [])\n  const bak = globalbaks.find((b) => b.id === bakId)\n  if (bak) {\n    bak.name = bakName\n    $.setjson(globalbaks, $.KEY_globalBaks)\n  }\n  $.json = { globalbaks }\n}\n\nasync function apiRevertGlobalBak() {\n  const { id: bakId } = $.toObj($request.body)\n  const globalbaks = $.getjson($.KEY_globalBaks, [])\n  const bak = globalbaks.find((b) => b.id === bakId)\n  if (bak && bak.bak) {\n    const {\n      chavy_boxjs_sysCfgs,\n      chavy_boxjs_sysApps,\n      chavy_boxjs_sessions,\n      chavy_boxjs_userCfgs,\n      chavy_boxjs_cur_sessions,\n      chavy_boxjs_app_subCaches,\n      ...datas\n    } = bak.bak\n    $.setdata(JSON.stringify(chavy_boxjs_sessions), $.KEY_sessions)\n    $.setdata(JSON.stringify(chavy_boxjs_userCfgs), $.KEY_usercfgs)\n    $.setdata(JSON.stringify(chavy_boxjs_cur_sessions), $.KEY_cursessions)\n    $.setdata(JSON.stringify(chavy_boxjs_app_subCaches), $.KEY_app_subCaches)\n    const isNull = (val) => [undefined, null, 'null', 'undefined', ''].includes(val)\n    Object.keys(datas).forEach((datkey) => $.setdata(isNull(datas[datkey]) ? '' : `${datas[datkey]}`, datkey))\n  }\n  const boxdata = getBoxData()\n  boxdata.globalbaks = globalbaks\n  $.json = boxdata\n}\n\nasync function apiSaveGlobalBak() {\n  let globalbaks = $.getjson($.KEY_globalBaks, [])\n  const bak = $.toObj($request.body)\n  const box = getBoxData()\n  const bakdata = {}\n  bakdata['chavy_boxjs_userCfgs'] = box.usercfgs\n  bakdata['chavy_boxjs_sessions'] = box.sessions\n  bakdata['chavy_boxjs_cur_sessions'] = box.curSessions\n  bakdata['chavy_boxjs_app_subCaches'] = box.appSubCaches\n  Object.assign(bakdata, box.datas)\n  bak.bak = bakdata\n  globalbaks.push(bak)\n  if (!$.setjson(globalbaks, $.KEY_globalBaks)) {\n    globalbaks = $.getjson($.KEY_globalBaks, [])\n  }\n  $.json = { globalbaks }\n}\n\nasync function apiImpGlobalBak() {\n  let globalbaks = $.getjson($.KEY_globalBaks, [])\n  const bak = $.toObj($request.body)\n  globalbaks.push(bak)\n  $.setjson(globalbaks, $.KEY_globalBaks)\n  $.json = { globalbaks }\n}\n\nasync function apiRunScript() {\n  // 取消勿扰模式\n  $.isMute = false\n  const opts = $.toObj($request.body)\n  const httpapi = $.getdata('@chavy_boxjs_userCfgs.httpapi')\n  const ishttpapi = /.*?@.*?:[0-9]+/.test(httpapi)\n  let script_text = null\n  if (opts.isRemote) {\n    await $.getScript(opts.url).then((script) => (script_text = script))\n  } else {\n    script_text = opts.script\n  }\n  if (0 && $.isSurge() && ishttpapi) {\n    const runOpts = { timeout: opts.timeout }\n    await $.runScript(script_text, runOpts).then((resp) => ($.json = JSON.parse(resp)))\n  } else {\n    await new Promise((resolve) => {\n      $eval_env.resolve = resolve\n      // 避免被执行脚本误认为是 rewrite 环境\n      // 所以需要 `$request = undefined`\n      $eval_env.request = $request\n      $request = undefined\n      // 重写 console.log, 把日志记录到 $.cached_logs\n      $.cached_logs = []\n      console.cloned_log = console.log\n      console.log = (l) => {\n        console.cloned_log(l)\n        $.cached_logs.push(l)\n      }\n      // 重写脚本内的 $done, 调用 $done() 即是调用 $eval_env.resolve()\n      script_text = script_text.replace(/\\$done/g, '$eval_env.resolve')\n      script_text = script_text.replace(/\\$\\.done/g, '$eval_env.resolve')\n      try {\n        eval(script_text)\n      } catch (e) {\n        $.cached_logs.push(e)\n        resolve()\n      }\n    })\n    // 还原 console.log\n    console.log = console.cloned_log\n    // 还原 $request\n    $request = $eval_env.request\n    // 返回数据\n    $.json = {\n      result: '',\n      output: $.cached_logs.join('\\n')\n    }\n  }\n}\n\n/**\n * ===================================\n * 工具类函数\n * ===================================\n */\n\nfunction reloadAppSubCache(url) {\n  // 地址后面拼时间缀, 避免 GET 缓存\n  const requrl = `${url}${url.includes('?') ? '&' : '?'}_=${new Date().getTime()}`\n  return $.http.get(requrl).then((resp) => {\n    try {\n      const subcaches = getAppSubCaches()\n      subcaches[url] = $.toObj(resp.body)\n      subcaches[url].updateTime = new Date()\n      $.setjson(subcaches, $.KEY_app_subCaches)\n      $.log(`更新订阅, 成功! ${url}`)\n    } catch (e) {\n      $.logErr(e)\n      $.log(`更新订阅, 失败! ${url}`)\n    }\n  })\n}\n\nasync function reloadAppSubCaches() {\n  $.msg($.name, '更新订阅: 开始!')\n  const reloadActs = []\n  const usercfgs = getUserCfgs()\n  usercfgs.appsubs.forEach((sub) => {\n    reloadActs.push(reloadAppSubCache(sub.url))\n  })\n  await Promise.all(reloadActs)\n  $.log(`全部订阅, 完成!`)\n  const endTime = new Date().getTime()\n  const costTime = (endTime - $.startTime) / 1000\n  $.msg($.name, `更新订阅: 完成! 🕛 ${costTime} 秒`)\n}\n\nfunction upgradeUserData() {\n  const usercfgs = getUserCfgs()\n  // 如果存在`usercfgs.appsubCaches`则需要升级数据\n  const isNeedUpgrade = !!usercfgs.appsubCaches\n  if (isNeedUpgrade) {\n    // 迁移订阅缓存至独立的持久化空间\n    $.setjson(usercfgs.appsubCaches, $.KEY_app_subCaches)\n    // 移除用户偏好中的订阅缓存\n    delete usercfgs.appsubCaches\n    usercfgs.appsubs.forEach((sub) => {\n      delete sub._raw\n      delete sub.apps\n      delete sub.isErr\n      delete sub.updateTime\n    })\n  }\n  if (isNeedUpgrade) {\n    $.setjson(usercfgs, $.KEY_usercfgs)\n  }\n}\n\n/**\n * ===================================\n * 结束类函数\n * ===================================\n */\nfunction doneBox() {\n  // 记录当前使用哪个域名访问\n  $.setdata(getHost($request.url), $.KEY_boxjs_host)\n  if ($.isOptions) doneOptions()\n  else if ($.isPage) donePage()\n  else if ($.isQuery) doneQuery()\n  else if ($.isApi) doneApi()\n  else $.done()\n}\n\nfunction getBaseDoneHeaders(mixHeaders = {}) {\n  return Object.assign(\n    {\n      'Access-Control-Allow-Origin': '*',\n      'Access-Control-Allow-Methods': 'POST,GET,OPTIONS,PUT,DELETE',\n      'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept'\n    },\n    mixHeaders\n  )\n}\n\nfunction getHtmlDoneHeaders() {\n  return getBaseDoneHeaders({\n    'Content-Type': 'text/html;charset=UTF-8'\n  })\n}\nfunction getJsonDoneHeaders() {\n  return getBaseDoneHeaders({\n    'Content-Type': 'text/json; charset=utf-8'\n  })\n}\n\nfunction doneOptions() {\n  const headers = getBaseDoneHeaders()\n  if ($.isSurge() || $.isLoon()) {\n    $.done({ response: { headers } })\n  } else if ($.isQuanX()) {\n    $.done({ headers })\n  }\n}\n\nfunction donePage() {\n  const headers = getHtmlDoneHeaders()\n  if ($.isSurge() || $.isLoon()) {\n    $.done({ response: { status: 200, headers, body: $.html } })\n  } else if ($.isQuanX()) {\n    $.done({ status: 'HTTP/1.1 200', headers, body: $.html })\n  }\n}\n\nfunction doneQuery() {\n  $.json = $.toStr($.json)\n  const headers = getJsonDoneHeaders()\n  if ($.isSurge() || $.isLoon()) {\n    $.done({ response: { status: 200, headers, body: $.json } })\n  } else if ($.isQuanX()) {\n    $.done({ status: 'HTTP/1.1 200', headers, body: $.json })\n  }\n}\n\nfunction doneApi() {\n  $.json = $.toStr($.json)\n  const headers = getJsonDoneHeaders()\n  if ($.isSurge() || $.isLoon()) {\n    $.done({ response: { status: 200, headers, body: $.json } })\n  } else if ($.isQuanX()) {\n    $.done({ status: 'HTTP/1.1 200', headers, body: $.json })\n  }\n}\n\n/**\n * GistBox by https://github.com/Peng-YM\n */\n// prettier-ignore\nfunction GistBox(e){const t=function(e,t={}){const{isQX:s,isLoon:n,isSurge:o}=function(){const e=\"undefined\"!=typeof $task,t=\"undefined\"!=typeof $loon,s=\"undefined\"!=typeof $httpClient&&!this.isLoon,n=\"function\"==typeof require&&\"undefined\"!=typeof $jsbox;return{isQX:e,isLoon:t,isSurge:s,isNode:\"function\"==typeof require&&!n,isJSBox:n}}(),r={};return[\"GET\",\"POST\",\"PUT\",\"DELETE\",\"HEAD\",\"OPTIONS\",\"PATCH\"].forEach(i=>r[i.toLowerCase()]=(r=>(function(r,i){(i=\"string\"==typeof i?{url:i}:i).url=e?e+i.url:i.url;const a=(i={...t,...i}).timeout,u={onRequest:()=>{},onResponse:e=>e,onTimeout:()=>{},...i.events};let c,d;u.onRequest(r,i),c=s?$task.fetch({method:r,...i}):new Promise((e,t)=>{(o||n?$httpClient:require(\"request\"))[r.toLowerCase()](i,(s,n,o)=>{s?t(s):e({statusCode:n.status||n.statusCode,headers:n.headers,body:o})})});const f=a?new Promise((e,t)=>{d=setTimeout(()=>(u.onTimeout(),t(`${r} URL: ${i.url} exceeds the timeout ${a} ms`)),a)}):null;return(f?Promise.race([f,c]).then(e=>(clearTimeout(d),e)):c).then(e=>u.onResponse(e))})(i,r))),r}(\"https://api.github.com\",{headers:{Authorization:`token ${e}`,\"User-Agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36\"},events:{onResponse:e=>String(e.statusCode).startsWith(\"4\")?Promise.reject(`ERROR: ${JSON.parse(e.body).message}`):e}}),s=e=>`boxjs.bak.${e}.json`,n=e=>e.match(/boxjs\\.bak\\.(\\d+)\\.json/)[1];return new class{async findDatabase(){return t.get(\"/gists\").then(e=>{const t=JSON.parse(e.body);for(let e of t)if(\"BoxJs Gist\"===e.description)return e.id;return-1})}async createDatabase(e){e instanceof Array||(e=[e]);const n={};return e.forEach(e=>{n[s(e.time)]={content:e.content}}),t.post({url:\"/gists\",body:JSON.stringify({description:\"BoxJs Gist\",public:!1,files:n})}).then(e=>JSON.parse(e.body).id)}async deleteDatabase(e){return t.delete(`/gists/${e}`)}async getBackups(e){const s=await t.get(`/gists/${e}`).then(e=>JSON.parse(e.body)),{files:o}=s,r=[];for(let e of Object.keys(o))r.push({time:n(e),url:o[e].raw_url});return r}async addBackups(e,t){t instanceof Array||(t=[t]);const n={};return t.forEach(e=>n[s(e.time)]={content:e.content}),this.updateBackups(e,n)}async deleteBackups(e,t){t instanceof Array||(t=[t]);const n={};return t.forEach(e=>n[s(e)]={}),this.updateBackups(e,n)}async updateBackups(e,s){return t.patch({url:`/gists/${e}`,body:JSON.stringify({files:s})})}}}\n\n/**\n * EnvJs\n */\n// prettier-ignore\nfunction Env(t,e){class s{constructor(t){this.env=t}send(t,e=\"GET\"){t=\"string\"==typeof t?{url:t}:t;let s=this.get;return\"POST\"===e&&(s=this.post),new Promise((e,i)=>{s.call(this,t,(t,s,r)=>{t?i(t):e(s)})})}get(t){return this.send.call(this.env,t)}post(t){return this.send.call(this.env,t,\"POST\")}}return new class{constructor(t,e){this.name=t,this.http=new s(this),this.data=null,this.dataFile=\"box.dat\",this.logs=[],this.isMute=!1,this.isNeedRewrite=!1,this.logSeparator=\"\\n\",this.startTime=(new Date).getTime(),Object.assign(this,e),this.log(\"\",`\\ud83d\\udd14${this.name}, \\u5f00\\u59cb!`)}isNode(){return\"undefined\"!=typeof module&&!!module.exports}isQuanX(){return\"undefined\"!=typeof $task}isSurge(){return\"undefined\"!=typeof $httpClient&&\"undefined\"==typeof $loon}isLoon(){return\"undefined\"!=typeof $loon}toObj(t,e=null){try{return JSON.parse(t)}catch{return e}}toStr(t,e=null){try{return JSON.stringify(t)}catch{return e}}getjson(t,e){let s=e;const i=this.getdata(t);if(i)try{s=JSON.parse(this.getdata(t))}catch{}return s}setjson(t,e){try{return this.setdata(JSON.stringify(t),e)}catch{return!1}}getScript(t){return new Promise(e=>{this.get({url:t},(t,s,i)=>e(i))})}runScript(t,e){return new Promise(s=>{let i=this.getdata(\"@chavy_boxjs_userCfgs.httpapi\");i=i?i.replace(/\\n/g,\"\").trim():i;let r=this.getdata(\"@chavy_boxjs_userCfgs.httpapi_timeout\");r=r?1*r:20,r=e&&e.timeout?e.timeout:r;const[o,h]=i.split(\"@\"),a={url:`http://${h}/v1/scripting/evaluate`,body:{script_text:t,mock_type:\"cron\",timeout:r},headers:{\"X-Key\":o,Accept:\"*/*\"}};this.post(a,(t,e,i)=>s(i))}).catch(t=>this.logErr(t))}loaddata(){if(!this.isNode())return{};{this.fs=this.fs?this.fs:require(\"fs\"),this.path=this.path?this.path:require(\"path\");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),i=!s&&this.fs.existsSync(e);if(!s&&!i)return{};{const i=s?t:e;try{return JSON.parse(this.fs.readFileSync(i))}catch(t){return{}}}}}writedata(){if(this.isNode()){this.fs=this.fs?this.fs:require(\"fs\"),this.path=this.path?this.path:require(\"path\");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),i=!s&&this.fs.existsSync(e),r=JSON.stringify(this.data);s?this.fs.writeFileSync(t,r):i?this.fs.writeFileSync(e,r):this.fs.writeFileSync(t,r)}}lodash_get(t,e,s){const i=e.replace(/\\[(\\d+)\\]/g,\".$1\").split(\".\");let r=t;for(const t of i)if(r=Object(r)[t],void 0===r)return s;return r}lodash_set(t,e,s){return Object(t)!==t?t:(Array.isArray(e)||(e=e.toString().match(/[^.[\\]]+/g)||[]),e.slice(0,-1).reduce((t,s,i)=>Object(t[s])===t[s]?t[s]:t[s]=Math.abs(e[i+1])>>0==+e[i+1]?[]:{},t)[e[e.length-1]]=s,t)}getdata(t){let e=this.getval(t);if(/^@/.test(t)){const[,s,i]=/^@(.*?)\\.(.*?)$/.exec(t),r=s?this.getval(s):\"\";if(r)try{const t=JSON.parse(r);e=t?this.lodash_get(t,i,\"\"):e}catch(t){e=\"\"}}return e}setdata(t,e){let s=!1;if(/^@/.test(e)){const[,i,r]=/^@(.*?)\\.(.*?)$/.exec(e),o=this.getval(i),h=i?\"null\"===o?null:o||\"{}\":\"{}\";try{const e=JSON.parse(h);this.lodash_set(e,r,t),s=this.setval(JSON.stringify(e),i)}catch(e){const o={};this.lodash_set(o,r,t),s=this.setval(JSON.stringify(o),i)}}else s=this.setval(t,e);return s}getval(t){return this.isSurge()||this.isLoon()?$persistentStore.read(t):this.isQuanX()?$prefs.valueForKey(t):this.isNode()?(this.data=this.loaddata(),this.data[t]):this.data&&this.data[t]||null}setval(t,e){return this.isSurge()||this.isLoon()?$persistentStore.write(t,e):this.isQuanX()?$prefs.setValueForKey(t,e):this.isNode()?(this.data=this.loaddata(),this.data[e]=t,this.writedata(),!0):this.data&&this.data[e]||null}initGotEnv(t){this.got=this.got?this.got:require(\"got\"),this.cktough=this.cktough?this.cktough:require(\"tough-cookie\"),this.ckjar=this.ckjar?this.ckjar:new this.cktough.CookieJar,t&&(t.headers=t.headers?t.headers:{},void 0===t.headers.Cookie&&void 0===t.cookieJar&&(t.cookieJar=this.ckjar))}get(t,e=(()=>{})){t.headers&&(delete t.headers[\"Content-Type\"],delete t.headers[\"Content-Length\"]),this.isSurge()||this.isLoon()?(this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{\"X-Surge-Skip-Scripting\":!1})),$httpClient.get(t,(t,s,i)=>{!t&&s&&(s.body=i,s.statusCode=s.status),e(t,s,i)})):this.isQuanX()?(this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then(t=>{const{statusCode:s,statusCode:i,headers:r,body:o}=t;e(null,{status:s,statusCode:i,headers:r,body:o},o)},t=>e(t))):this.isNode()&&(this.initGotEnv(t),this.got(t).on(\"redirect\",(t,e)=>{try{const s=t.headers[\"set-cookie\"].map(this.cktough.Cookie.parse).toString();this.ckjar.setCookieSync(s,null),e.cookieJar=this.ckjar}catch(t){this.logErr(t)}}).then(t=>{const{statusCode:s,statusCode:i,headers:r,body:o}=t;e(null,{status:s,statusCode:i,headers:r,body:o},o)},t=>e(t)))}post(t,e=(()=>{})){if(t.body&&t.headers&&!t.headers[\"Content-Type\"]&&(t.headers[\"Content-Type\"]=\"application/x-www-form-urlencoded\"),t.headers&&delete t.headers[\"Content-Length\"],this.isSurge()||this.isLoon())this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{\"X-Surge-Skip-Scripting\":!1})),$httpClient.post(t,(t,s,i)=>{!t&&s&&(s.body=i,s.statusCode=s.status),e(t,s,i)});else if(this.isQuanX())t.method=\"POST\",this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then(t=>{const{statusCode:s,statusCode:i,headers:r,body:o}=t;e(null,{status:s,statusCode:i,headers:r,body:o},o)},t=>e(t));else if(this.isNode()){this.initGotEnv(t);const{url:s,...i}=t;this.got.post(s,i).then(t=>{const{statusCode:s,statusCode:i,headers:r,body:o}=t;e(null,{status:s,statusCode:i,headers:r,body:o},o)},t=>e(t))}}time(t){let e={\"M+\":(new Date).getMonth()+1,\"d+\":(new Date).getDate(),\"H+\":(new Date).getHours(),\"m+\":(new Date).getMinutes(),\"s+\":(new Date).getSeconds(),\"q+\":Math.floor(((new Date).getMonth()+3)/3),S:(new Date).getMilliseconds()};/(y+)/.test(t)&&(t=t.replace(RegExp.$1,((new Date).getFullYear()+\"\").substr(4-RegExp.$1.length)));for(let s in e)new RegExp(\"(\"+s+\")\").test(t)&&(t=t.replace(RegExp.$1,1==RegExp.$1.length?e[s]:(\"00\"+e[s]).substr((\"\"+e[s]).length)));return t}msg(e=t,s=\"\",i=\"\",r){const o=t=>{if(!t||!this.isLoon()&&this.isSurge())return t;if(\"string\"==typeof t)return this.isLoon()?t:this.isQuanX()?{\"open-url\":t}:void 0;if(\"object\"==typeof t){if(this.isLoon()){let e=t.openUrl||t[\"open-url\"],s=t.mediaUrl||t[\"media-url\"];return{openUrl:e,mediaUrl:s}}if(this.isQuanX()){let e=t[\"open-url\"]||t.openUrl,s=t[\"media-url\"]||t.mediaUrl;return{\"open-url\":e,\"media-url\":s}}}};this.isMute||(this.isSurge()||this.isLoon()?$notification.post(e,s,i,o(r)):this.isQuanX()&&$notify(e,s,i,o(r)));let h=[\"\",\"==============\\ud83d\\udce3\\u7cfb\\u7edf\\u901a\\u77e5\\ud83d\\udce3==============\"];h.push(e),s&&h.push(s),i&&h.push(i),console.log(h.join(\"\\n\")),this.logs=this.logs.concat(h)}log(...t){t.length>0&&(this.logs=[...this.logs,...t]),console.log(t.join(this.logSeparator))}logErr(t,e){const s=!this.isSurge()&&!this.isQuanX()&&!this.isLoon();s?this.log(\"\",`\\u2757\\ufe0f${this.name}, \\u9519\\u8bef!`,t.stack):this.log(\"\",`\\u2757\\ufe0f${this.name}, \\u9519\\u8bef!`,t)}wait(t){return new Promise(e=>setTimeout(e,t))}done(t={}){const e=(new Date).getTime(),s=(e-this.startTime)/1e3;this.log(\"\",`\\ud83d\\udd14${this.name}, \\u7ed3\\u675f! \\ud83d\\udd5b ${s} \\u79d2`),this.log(),(this.isSurge()||this.isQuanX()||this.isLoon())&&$done(t)}}(t,e)}\n"
  },
  {
    "path": "examples/JSTEST/cheerio-hbin.js",
    "content": "let body = $response.body\nlet restype = $response.headers['Content-Type']\n\nif (/html/.test(restype)) {\n  const $ = $cheerio.load(body)\n  if ($('.version').length) {\n    $('.version').text('cheerio')\n    body = $.html()\n    console.log(body)\n  }\n}\n\n$done(body)"
  },
  {
    "path": "examples/JSTEST/efh/kuwo-music.efh",
    "content": "<!-- 适用于 elecV2P v3.7.2 及以上版本 -->\n<!-- 脚本仅供测试使用，请勿用于其他用途 -->\n<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"theme-color\" content=\"var(--main-bk)\">\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1,viewport-fit=cover\">\n    <title>Kuwo Music Download - elecV2P</title>\n    <style type=\"text/css\">\n      :root {\n        --main-bk: #003153;\n        --main-fc: #FAFAFD;\n        --main-cl: #2890EE;\n        --note-bk: #EF7A82;\n        --secd-bk: #2890EEB8;\n        --green-bk: #66FF0088;\n      }\n\n      html, body {\n        margin: 0;\n        padding: 0;\n      }\n\n      #app {\n        width: 100%;\n        min-height: 100vh;\n        padding: 8px;\n        box-sizing: border-box;\n        word-break: break-all;\n        color: var(--main-fc);\n        background-color: var(--main-bk);\n      }\n\n      .eflex {\n        display: flex;\n        align-items: center;\n        justify-content: space-around;\n      }\n      .eflex--wrap {\n        flex-flow: wrap;\n      }\n      .yflex_f1 {\n        flex: 1;\n      }\n\n      .eul {\n        list-style: none;\n        padding: 0;\n        margin: 0;\n        border-top: 1px solid;\n      }\n      .eli {\n        border-bottom: 1px solid;\n        padding: 3px 0px;\n      }\n      .eli--down {\n        display: flex;\n        justify-content: space-between;\n        background-image: linear-gradient(to right, var(--green-bk), var(--green-bk));\n        background-repeat: no-repeat;\n        transition: background-size 1s linear;\n      }\n      .eres {\n        background: var(--main-fc);\n        color: var(--main-cl);\n        padding: 6px 3px;\n      }\n      .ehistory {\n        text-align: center;\n        margin: 6px;\n      }\n      .espan {\n        display: inline-block;\n        margin: 2px 8px;\n      }\n      .espan--border {\n        border: 2px solid var(--main-cl);\n        padding: 4px 8px;\n        margin: 0;\n        border-radius: 8px;\n        color: var(--main-fc);\n      }\n      .eli--down .espan {\n        min-width: 80px;\n        text-align: right;\n      }\n      .ehistory .espan, .ecursor {\n        cursor: pointer;\n      }\n      .espan--btn {\n        background: var(--main-cl);\n        padding: 0 8px;\n        border-radius: 8px;\n        cursor: pointer;\n        user-select: none;\n      }\n      .epadding0 {\n        padding: 0;\n      }\n      .edown {\n        display: inline-block;\n        padding: 0 8px;\n        border-radius: 8px 0 0 8px;\n      }\n      .eplay {\n        padding: 0 4px;\n        color: var(--main-cl);\n        background: var(--main-fc);\n        border-radius: 0 8px 8px 0;\n      }\n      .yselect, .yinput {\n        height: 40px;\n        min-width: 80px;\n        font-size: 20px;\n        border-radius: 8px;\n        padding: 0 6px;\n        color: var(--main-cl);\n        border: none;\n        box-sizing: border-box;\n        font-family: var(--font-fm);\n      }\n      .yselect--short, .yinput--short {\n        width: 80px;\n        margin: 0 0.5em;\n      }\n\n      .ybutton {\n        width: 120px;\n        height: 40px;\n        border: none;\n        border-radius: 0.5em;\n        font-size: 20px;\n        color: var(--main-fc);\n        background: var(--main-cl);\n        cursor: pointer;\n      }\n      .ybutton--h36, .epreview {\n        height: 36px;\n      }\n      .ebk--green {\n        background: var(--green-bk);\n      }\n      .ebk--note {\n        background-color: var(--note-bk);\n      }\n      .ebk--blue {\n        background: var(--secd-bk);\n      }\n      .ebk--loading {\n        background-image: linear-gradient(to right, transparent, var(--note-bk), transparent);\n        background-size: 10%;\n        background-repeat: no-repeat;\n        animation: loading 5s ease-in-out infinite;\n      }\n      @keyframes loading {\n        0% {\n          background-position-x: 0%;\n        }\n        50% {\n          background-position-x: 100%;\n        }\n        100% {\n          background-position-x: 0%;\n        }\n      }\n      .ehidden {\n        display: none;\n      }\n      .emedia {\n        position: fixed;\n        top: 60px;\n        right: 8px;\n        display: inline-flex;\n        min-width: 300px;\n        max-width: 50%;\n        max-height: 60%;\n        border-radius: 1.5em;\n        background: var(--secd-bk);\n      }\n      .emedia_delete {\n        position: absolute;\n        right: .5em;\n        top: .5em;\n        padding: 6px;\n        background: var(--main-bk);\n        border-radius: 8px;\n        opacity: 0;\n        cursor: pointer;\n      }\n      .emedia:hover .emedia_delete, .emedia[data-delshow=true] .emedia_delete {\n        opacity: 1;\n      }\n      .emeida_name {\n        position: absolute;\n        margin: .5em 1em;\n        word-break: break-word;\n      }\n      .emax-wp100 {\n        max-width: 100%;\n      }\n      .emin-w300 {\n        min-width: 300px;\n      }\n    </style>\n    <!-- Note: when deploying, replace \"development.js\" with \"production.min.js\". -->\n    <script src=\"https://unpkg.com/react@18/umd/react.production.min.js\"></script>\n    <script src=\"https://unpkg.com/react-dom@18/umd/react-dom.production.min.js\"></script>\n\n    <!-- Don't use this in production: -->\n    <script src=\"https://unpkg.com/@babel/standalone/babel.min.js\"></script>\n  </head>\n  <body><noscript><strong>Please enable JavaScript to continue.</strong></noscript>\n    <div id=\"app\"></div>\n    <script type=\"text/javascript\">\n      function kSize(size = 0, k = 1024) {\n        if (size < k) {\n          return size + ' B'\n        }\n        if (size < k*k) {\n          return (size/k).toFixed(2) + ' K'\n        }\n        if (size < k*k*k) {\n          return (size/(k*k)).toFixed(2) + ' M'\n        }\n        return (size/(k*k*k)).toFixed(2) + ' G'\n      }\n      function htmlDecode(input) {\n        const tagsToReplace = {\n          '&nbsp;': ' ',\n          '&amp;': '&',\n          '&lt;em&gt;': '',\n          '&lt;/em&gt;': '',\n          '&lt;': '《',\n          '&gt;': '》',\n          '&quot;': '\"',\n          '?': '？',\n          '&apos;': '\\'',\n        }\n        return input.replace(/&nbsp;|&amp;|&lt;em&gt;|&lt;\\/em&gt;|&lt;|&gt;|&quot;|\\?|&apos;/g, tag=>tagsToReplace[tag] ?? tag);\n      }\n      function setTitle(title = 'elecV2P'){\n        document.title = title\n      }\n      function streamUrl(url){\n        return location.protocol === 'https:' ? `/data?type=stream&url=${encodeURIComponent(url)}` : url\n      }\n    </script>\n    <script type=\"text/babel\">\n    // sublime 用户推荐安装 Package: Naomi 进行高亮显示\n      // Todo:\n      // - 删除已下载文件\n      // Done:\n      // - 移动端 preview 问题\n      // - 进度条拖动问题\n      // - preview 移动\n      // - getOrgUrl(isHttps)\n      // - 预览后选择下载\n      // - 子目录预览问题\n      // - 右键复制链接\n      // - sse 断开重连提醒\n      // - 删除单个搜索历史\n      // - 下载完播放测试\n      // - sse id 优化\n      // - 搜索历史\n      // - 显示 总大小\n      // - skip download 提醒\n      // - 下载目录设置\n      // - 超时下载提醒\n\n      const useState = React.useState\n      const useEffect = React.useEffect\n\n      function MyApp() {\n        const [search, setSearch] = useState({ word: '', history: [] })\n        const [reslist, setReslist] = useState([])\n        const [resraw, setResraw] = useState('')\n        const [page, setPage] = useState(0)\n        const [subfold, setSubfold] = useState('')\n        const [media, setMedia] = useState({ url: '', name: '' })\n\n        const handleChange = (e)=>{\n          setSearch({ word: e.target.value.trim(), history: search.history })\n          setPage(0)\n        }\n        const searchRes = ()=>{\n          const word = search.word, history = [...new Set([word, ...search.history])]\n          setSearch({ word, history })\n          localStorage.setItem('search_history', JSON.stringify(history))\n          const search_url = `http://search.kuwo.cn/r.s?all=${word}&ft=music&itemset=web_2013&client=kt&pn=${page}&rn=10&rformat=json&encoding=utf8`\n          console.debug('search url:', search_url)\n          setResraw(`正在搜索 ${word}...`)\n          fetch(streamUrl(search_url)).then(res=>res.text()).then(res=>{\n            let obj = (new Function(\"return \" + res))();\n            setReslist(obj.abslist.map(f=>[htmlDecode(f.ARTIST), htmlDecode(f.SONGNAME), f.MUSICRID]))\n            if (obj.abslist.length) {\n              setPage(Number(page)+1)\n              setResraw(`成功获取 ${word} 相关搜索结果`)\n              setSubfold(word)\n            } else {\n              setResraw(`没有找到 ${word} 相关数据`)\n            }\n          }).catch(e=>{\n            setResraw(`${search_url} error ${e.message}`)\n            console.error(e)\n          })\n        }\n        const getUrl = (e, name, rid)=>{\n          if (e.target.dataset.d === 'finish') {\n            setResraw(`${name} 已下载`)\n            return\n          }\n          if (e.target.dataset.d === 'start') {\n            setResraw(`${name} 下载中`)\n            return\n          }\n          if (e.target.dataset.m === 'preview') {\n            const name = e.target.dataset.name\n            setMedia({ url: streamUrl(e.target.dataset.url), name })\n            setResraw(`开始播放 ${name}`)\n            setTitle(name + ' - elecV2P Player')\n            return\n          }\n          const type = e.target.dataset.type || 'mp3'\n          if (e.target.dataset.m === 'notfound') {\n            setResraw(`${name} ${type} 对应资源无结果`)\n            return\n          }\n          if (e.target.dataset.m === 'download') {\n            e.target.dataset.d = 'start'\n            const url = e.target.innerText\n            if (!/^http:\\/\\//.test(url)) {\n              setResraw(`${url} 并不是 http 链接`)\n              return\n            }\n            name += '.' + url.split('.').pop()\n            if (subfold) {\n              name = `${subfold}/${name}`\n            }\n            setResraw(`准备下载 ${name}...`)\n            $fend('download', {\n              name, url,\n              timeout: 15000,\n            }).then(res=>res.text()).then(res=>{\n              if (/download fail/.test(res)) {\n                e.target.dataset.d = ''\n                setResraw(`${name} 下载结果 ${res}`)\n                return\n              }\n              e.target.dataset.d = 'finish'\n              e.target.style.background = 'var(--green-bk)'\n              if (!/still running/.test(res)) {\n                setResraw(`${name} 下载结果 ${res}`)\n              }\n            }).catch(e=>{\n              setResraw(`${name} 下载 error ${e.message}`)\n              console.error(e)\n            })\n            return\n          }\n          getRes({e, rid, name, type})\n        }\n        const getRes = ({e, rid, name, type})=>{\n          const search_url = `http://antiserver.kuwo.cn/anti.s?type=convert_url&rid=${rid}&format=${type}&response=url`\n          setResraw(`${name} ${type} 资源链接获取中...`)\n          fetch(streamUrl(search_url)).then(res=>res.text()).then(res=>{\n            if (/^http:\\/\\//.test(res)) {\n              e.target.dataset.d = 'finish'\n              e.target.classList.add('epadding0')\n              e.target.innerHTML = `<span data-m=\"download\" class=\"edown\">${res}</span><span class=\"eplay\" data-name=\"${name}.${type}\" data-m=\"preview\" data-url=\"${res}\">⏵</span>`\n              setResraw(`${name} ${type} 资源链接已获取，再次点击下载至 elecV2P 服务器，右键复制`)\n            } else {\n              e.target.dataset.m = 'notfound'\n              e.target.innerText = '资源不存在'\n              setResraw(`没有找到 ${name} ${type} 对应资源`)\n            }\n          }).catch(e=>{\n            setResraw(`${search_url} error ${e.message}`)\n            console.error(e)\n          })\n        }\n        const copyToClipboard = e => {\n          e.preventDefault();\n          if (e.target.dataset.m !== 'download') return;\n          const url = e.target.innerText;\n          navigator.clipboard.writeText(url).then(()=>setResraw(url + ' 已复制到粘贴板')).catch(error=>{\n            setResraw(url + ' 复制失败 ' + error.message);\n          });\n        }\n\n        useEffect(\n          ()=>{\n            const history = JSON.parse(localStorage.getItem('search_history')||'[]')\n            setSearch({ word: history[0] || '王菲', history })\n          }, []\n        )\n        return (\n          <>\n            <div className=\"eflex\">\n              <input className=\"yinput yflex_f1\" value={search.word} onChange={handleChange} onKeyDown={(e)=>{\n                if (e.keyCode === 13) {\n                  searchRes()\n                }\n              }}/>\n              <input class=\"yinput yinput--short\" type=\"number\" value={page} onChange={e=>setPage(e.target.value)} placeholder=\"page\" />\n              <button onClick={searchRes} className=\"ybutton\">SEARCH</button>\n              <input class=\"yinput yinput--short\" value={subfold} onChange={e=>setSubfold(e.target.value)} placeholder=\"子目录\" title=\"文件保存子目录\" />\n            </div>\n            <div className=\"ehistory\">\n              <span className=\"espan\" onClick={()=>{setSearch({ ...search, history: [] });localStorage.removeItem('search_history')}}>搜索历史：</span>\n              {\n                search.history.map(h=>(<span className=\"espan\" onClick={()=>{\n                  const idx = search.history.indexOf(h)\n                  search.history.splice(idx, 1)\n                  setSearch({ word: h, history: [...search.history] })\n                }}>{h}</span>))\n              }\n            </div>\n            <ul className=\"eul\">{reslist.map(item=>(\n              <li className=\"eli\" key={item[2]} data-rid={item[2]}>\n                <span className=\"espan\">{item[0]}</span>\n                <span className=\"espan\">{item[1]}</span>\n                <span className=\"espan espan--btn\" data-type=\"mp3\" onClick={e=>getUrl(e, item[1], item[2])} onContextMenu={copyToClipboard}>MP3</span>\n                <span className=\"espan espan--btn\" data-type=\"mp4\" onClick={e=>getUrl(e, item[1], item[2])} onContextMenu={copyToClipboard}>MP4</span>\n              </li>\n            ))}</ul>\n            <div className=\"eres\">{resraw}</div>\n            <DownList setResraw={setResraw} />\n            <MediaPreview media={media} setMedia={setMedia} />\n          </>\n        )\n      }\n      function MediaPreview({ media, setMedia }) {\n        const [position, setPosition] = useState([0, 0, 0, 0])\n        const volumeSet = (e)=>{\n          if (e.target.dataset.vol === '0') {\n            e.target.volume = 0.3\n            e.target.dataset.vol = '1'\n          }\n        }\n        const dragStart = (e)=>{\n          // e.preventDefault();\n          // console.debug('drag start', ...position);\n          e.dataTransfer.effectAllowed = 'move';\n          setPosition([e.clientX, e.clientY, position[2], position[3]]);\n        }\n        const dragEnd = (e)=>{\n          e.preventDefault();\n          const endp = [position[2] + e.clientX - position[0], position[3] + e.clientY - position[1]];\n          endp[0] = endp[0] < 0 ? Math.max(e.target.clientWidth-window.innerWidth+16, endp[0]) : 0;\n          endp[1] = endp[1] > 0 ? Math.min(window.innerHeight-e.target.clientHeight-60, endp[1]) : 0;\n          e.dataTransfer.dropEffect = 'move';\n          e.target.style = `transform: translate(${endp[0]}px, ${endp[1]}px);`;\n          setPosition([position[0], position[1], endp[0], endp[1]]);\n        }\n        return (\n          <div className={'ehidden' + (media.url?' emedia':'')}\n            draggable=\"true\"\n            onDragStart={dragStart}\n            onDragEnd={dragEnd}\n            onClick={(e)=>{\n              e.currentTarget.dataset.delshow = e.currentTarget.dataset.delshow !== 'true' ? 'true' : '';\n              e.preventDefault();\n            }}\n          >\n            <span className=\"emeida_name\">{media.name}</span>\n            <video className=\"emax-wp100 emin-w300\" src={media.url} autoplay=\"true\" controls onEnded={()=>{\n              setTitle('Kuwo Music Download - elecV2P');\n            }} onLoadedMetadata={volumeSet} data-vol=\"0\"></video>\n            <span className=\"emedia_delete\" onClick={()=>{\n              setMedia({url: '', name: ''});\n              setTitle('Kuwo Music Download - elecV2P');\n            }}>❌</span>\n          </div>\n        )\n      }\n\n      function DownList({ setResraw }){\n        const [preurl, setPreurl] = useState('')\n        const [downlist, setDownlist] = useState({})\n        const [sseonline, setSseonline] = useState(false)\n\n        const statueUpdate = (name, { status, total, skip })=>{\n          setDownlist((prevList)=>((status && !skip) ? { ...prevList, [name]: { status, total } } : { [name]: { status: status||'0.00%', total, skip }, ...prevList }))\n        }\n        const sseNew = ()=>{\n          if (sseonline) {\n            setResraw('elecV2P SSE 已连接');\n            return;\n          }\n          const ssevent = new EventSource('/sse/elecV2P/kuwomusic');\n          // 所有客户端会同步接收 SSE 同一路径下的消息\n          ssevent.onopen = (event) => {\n            setResraw('elecV2P SSE 连接成功')\n          }\n          ssevent.onmessage = (event)=>{\n            const { name, dsize, total, start, skip, type } = JSON.parse(event.data);\n            if (type === 'init') {\n              // SSE 初始连接返回的数据 { type, data }\n              setSseonline(true);\n              return;\n            }\n            if (skip) {\n              statueUpdate(name, { status: '100.00%', total: '已存在，跳过下载', skip: true });\n            } else if (start) {\n              statueUpdate(name, { status: 0, total });\n            } else {\n              statueUpdate(name, { status: (dsize/total*100).toFixed(2) + '%', total });\n            }\n          }\n          ssevent.onerror = (error)=>{\n            setSseonline(false);\n            ssevent.close();\n            console.error('sse close', error);\n            setResraw(`SSE 已断开, 无法接收后台文件下载进度`);\n            // alert('SSE 已断开, 无法接收后台文件下载进度\\n点击 SSE 已断开 按钮进行重连');\n          }\n        }\n        useEffect(sseNew, [])\n        const volumeSet = (e)=>{\n          if (e.target.dataset.vol === '0') {\n            e.target.volume = 0.3\n            e.target.dataset.vol = '1'\n          }\n        }\n        return (\n          <ul className={'eul' + (!sseonline?' ebk--loading':'')}>\n            <li className=\"eli eflex eflex--wrap\" key=\"downlist\">\n              {preurl && <audio className=\"epreview\" src={preurl} autoplay=\"true\" controls onLoadedMetadata={volumeSet} data-vol=\"0\"></audio>}\n              <span className=\"espan--border\">下载列表 {Object.keys(downlist).length}</span>\n              <button className=\"ybutton ybutton--h36\" onClick={()=>setDownlist({})}>清空</button>\n              <a className=\"espan--border\" href=\"/efss/\" target=\"elecV2PEFSS\">EFSS/music 查看全部</a>\n              <button className={'ybutton ybutton--h36 ' + (sseonline?'ebk--green':'ebk--note')} onClick={()=>{\n                if (sseonline) {\n                  setResraw('elecV2P SSE 已连接');\n                } else {\n                  setResraw('elecV2P SSE 重连中...');\n                  sseNew();\n                }\n              }} title=\"用于接收后台下载进度\">SSE {sseonline?'已连接':'已断开'}</button>\n            </li>\n            {Object.keys(downlist).map(key=>{\n              const { status, total, skip } = downlist[key]\n              return (\n                <li className={'eli eli--down' + (skip?' ebk--blue':'')} style={{backgroundSize: status}} key={key}>\n                  <span>{key}</span>\n                  <div className=\"downstatus\">\n                    {status==='100.00%' && <sapn className=\"espan ecursor\" onClick={(e)=>{\n                      if (e.target.innerText === '⏵') {\n                        setPreurl('/efss/music/'+key)\n                        setTitle(key + ' - elecV2P Player')\n                        e.target.innerText = '⏸︎'\n                      } else {\n                        e.target.innerText = '⏵'\n                        setTitle('Kuwo Music Download - elecV2P')\n                        setPreurl('')\n                      }\n                    }}>⏵</sapn>}\n                    <span className=\"espan\">{!skip?kSize(total):total}</span>\n                    <span className=\"espan\">{status}</span>\n                  </div>\n                </li>\n              )}\n            )}\n          </ul>\n        )\n      }\n\n      const app = document.getElementById('app');\n      const root = ReactDOM.createRoot(app);\n      root.render(<MyApp />);\n    </script>\n    <!-- 以下为在 elecV2P 后台运行的脚本 -->\n    <script favend>\n      // console.clear();     // 是否清除之前的日志\n      function kSize(size, k = 1024) {\n        if (size < k) {\n          return size + ' B'\n        }\n        if (size < k*k) {\n          return (size/k).toFixed(2) + ' K'\n        }\n        if (size < k*k*k) {\n          return (size/(k*k)).toFixed(2) + ' M'\n        }\n        return (size/(k*k*k)).toFixed(2) + ' G'\n      }\n      $fend('download', data=>{\n        console.log('download name', data.name, 'url', data.url)\n        return new Promise((resolve, reject)=>{\n          $download(data.url, {\n            folder: 'efss/music',\n            name: data.name,\n            existskip: true,\n          }, (options)=>{\n            // callback 函数\n            if (options.start) {\n              $ws.sse('kuwomusic', { name: data.name, start: options.start, total: Number(options.total) })\n              console.log(`${options.name} 下载开始 ${options.total ? '总大小 ' + kSize(options.total) : ''}`)\n            } else if (options.finish) {\n              // $ws.sse('kuwomusic', { name: data.name, finish: options.finish, total: Number(options.total) })\n              console.log(`${options.name} 下载完成 ${options.total ? '总大小 ' + kSize(options.total) : ''}`)\n            } else {\n              $ws.sse('kuwomusic', { name: data.name, dsize: options.dsize, total: Number(options.total) })\n              options.chunk % 100 === 0 && console.log(`${options.name} ${options.chunk } ${kSize(options.dsize)}/${kSize(options.total)}\\x1b[J`)\n            }\n          })\n          .then(d=>{\n            resolve(d)\n            if (/skip download/.test(d)) {\n              $ws.sse('kuwomusic', { name: data.name, skip: true })\n            }\n          })\n          .catch(e=>resolve(e.message || e))\n        })\n      })\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/JSTEST/efh/markdown.efh",
    "content": "<!-- efh 小应用，适用于 elecV2P\n功能：一个简单的 markdonw 文件阅读器\n使用：\n- 打开 webUI/efss -> favend 设置\n- 填写任意名称/关键字 | 运行脚本 | https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/examples/JSTEST/efh/markdown.efh\n更新：2022-10-12 13:42\n-->\n<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"theme-color\" content=\"#009688\">\n  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1,viewport-fit=cover\">\n  <title>a simple markdown reader</title>\n  <style type=\"text/css\">\n    .mflex{display:flex;justify-content:center;align-items:center;flex-wrap: wrap;}.minput{min-width:320px;height:32px;font-size:1em;padding:0 .4em;border-radius:.5em;box-sizing: border-box;border: 1px solid #003153;}.mbutton{height:32px;padding:0 1em;margin:3px 1em;border:0;color:#fbfbff;background:#003153;border-radius:.5em;font-size:1em;cursor:pointer}html,body{font-family:'Microsoft YaHei',-apple-system,Helvetica,Arial}body[theme-color=black]{background: #080808;color: #e8e8e8;}pre, code {font-size: 16px;font-family: Consolas, Inconsolata, Courier, monospace;margin: auto 5px;}code {white-space: pre-wrap;border-radius: 2px;display: inline;}pre {font-size: 18px;line-height: 1.4em;display: block;border: 1px solid #ccc;border-radius: 8px;padding: 10px;color: #333;background: #f8f8f8;overflow-x: auto;}pre code {display: block;padding: 0;white-space: pre-wrap;background-color: transparent;border-radius: 0;}strong, b{color: #BF360C;}em, i {color: #009688;}hr {border: 1px solid #BF360C;margin: 1.5em auto;}p {margin: 1.5em 5px;word-break: break-word;text-align: justify;letter-spacing: 1px;line-height: 1.5em;}table, pre, dl, blockquote, q, ul, ol {margin: 10px 5px;}ul, ol {padding-left: 15px;}li {margin: 10px;}li p {margin: 10px 0 !important;}ul ul, ul ol, ol ul, ol ol {margin: 0;padding-left: 10px;}ul {list-style-type: circle;}dl {padding: 0;}dl dt {font-size: 1em;font-weight: bold;font-style: italic;}dl dd {margin: 0 0 10px;padding: 0 10px;}blockquote, q {border-left: 2px solid #009688;padding: 0 10px;color: #777;quotes: none;margin-left: 1em;}blockquote::before, blockquote::after, q::before, q::after {content: none;}h1, h2, h3, h4, h5, h6 {margin: 20px 0 10px;padding: 0.5em 1em;text-align: center;font-weight: bold;color: #009688;}h1 {font-size: 24px;border-bottom: 1px solid #A8A8A8;}h2 {font-size: 20px;border-bottom: 1px solid #A8A8A8;}h3 {font-size: 18px;border-bottom: 1px solid #A8A8A8;}h4 {font-size: 16px;}img {display: flex;max-width: 100%;margin: auto;}table {padding: 0;border-collapse: collapse;border-spacing: 0;font-size: 1em;font: inherit;border: 0;margin: 0 auto;}tbody {margin: 0;padding: 0;border: 0;}table tr {border: 0;border-top: 1px solid #CCC;background-color: white;margin: 0;padding: 0;}table tr:nth-child(2n) {background-color: #F8F8F8;}table tr th, table tr td {font-size: 16px;border: 1px solid #CCC;margin: 0;padding: 5px 10px;}table tr th {font-weight: bold;color: #eee;border: 1px solid #009688;background-color: #009688;}\n  </style>\n</head>\n<body>\n  <div class=\"mflex\">\n    <input class=\"minput\" type=\"text\" placeholder=\"markdown file url\">\n    <button class=\"mbutton\" onclick=\"dataFetch()\">显示</button>\n    <button class=\"mbutton\" onclick=\"dataLocal()\">本地 .md 文件</button>\n  </div>\n  <div class=\"markdown_content\"></div>\n  <script type=\"text/javascript\" src=\"https://cdn.jsdelivr.net/npm/marked/marked.min.js\" async></script>\n  <script type=\"text/javascript\">\n    function mInit(){\n      let initurl = ''\n      if (location.search) {\n        initurl = new URL(location.href).searchParams.get('file')\n      }\n      if (!initurl) {\n        initurl = location.pathname.replace(/\\/efss\\/\\w+\\/?/, '')\n      }\n      if (initurl) {\n        dataFile(initurl)\n      }\n    }\n    mInit()\n    let minput = document.querySelector('.minput')\n    let mcontent = document.querySelector('.markdown_content')\n\n    function dataFetch() {\n      if (minput.value) {\n        dataFile(minput.value)\n      } else {\n        alert('input first')\n      }\n    }\n    function dataFile(file) {\n      $fend('data', file).then(res=>res.text()).then(res=>{\n        parseContent(res)\n      }).catch(e=>{\n        console.error(e)\n        alert(e.message)\n      })\n    }\n    function dataLocal() {\n      getFile({ accept: '.md' }).then(data=>{\n        parseContent(data.content, data.name)\n      })\n    }\n    function parseContent(content, title = '') {\n      mcontent.innerHTML = marked.parse(content)\n      if (!location.href.endsWith('/') && content.startsWith('<li><a')) {\n        history.replaceState({},'',location.href + '/')\n      }\n      if (title) {\n        document.title = title + ' - elecV2P markdown reader'\n        return\n      }\n      const titleH = document.querySelector('h1') || document.querySelector('h2')\n      if (titleH) {\n        document.title = titleH.innerText + ' - elecV2P markdown reader'\n      }\n    }\n    function getFile({ accept = '*', type = 'text', multiple = false } = {}) {\n      let input = document.createElement('input')\n      input.type = 'file'\n      input.accept = accept\n      if (multiple) {\n        input.multiple = true\n      }\n\n      return new Promise((resolve, reject)=>{\n        input.onchange = e => {\n          let file = e.target.files[0]\n          if (!file) {\n            reject('请先选择文件')\n            return\n          }\n          console.debug('get file', file.name, file.type, file.size);\n          if (type === 'file') {\n            resolve(file)\n          } else {\n            let reader = new FileReader()\n            reader.readAsText(file, 'UTF-8')\n\n            reader.onload = readerEvent => {\n              resolve({\n                name: file.name,\n                type: file.type,\n                size: file.size,\n                content: readerEvent.target.result\n              })\n            }\n          }\n        }\n        input.click()\n      })\n    }\n  </script>\n<script favend runon=\"elecV2P\">\n  $fend('data', async (url)=>{\n    if (!/^https?:\\/\\/\\S{4}/.test(url)) {\n      const fs = require('fs')\n      url = decodeURI(url)\n      if (!fs.existsSync(url)) {\n        return url + ' 暂不存在'\n      } else if (fs.statSync(url).isDirectory()) {\n        return fs.readdirSync(url).filter(fo=>/md$/.test(fo)).map(fo=>`<li><a href=\"${fo}\">${fo}</a></li>`).join('')\n      } else {\n        return fs.readFileSync(url)\n      }\n    }\n    try {\n      let md = await $axios(url)\n      return md.data\n    } catch(e) {\n      return e.message\n    }\n  })\n</script>\n</body>\n</html>"
  },
  {
    "path": "examples/JSTEST/efh/notepad.efh",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"theme-color\" content=\"#003153\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <title>Cloud Notepad</title>\n  <style type=\"text/css\">\n    body {\n      background: #eee;\n      box-sizing: border-box;\n    }\n    .main {\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n    }\n    .note {\n      width: 98%;\n      height: 90vh;\n      margin-bottom: 1em;\n      min-height: 680px;\n      border-radius: 8px;\n      padding: 3px 5px;\n      font-size: 18px;\n    }\n    .notebtn {\n      width: 120px;\n      padding: 3px;\n      font-size: 20px;\n      border: none;\n      border-radius: 6px;\n      color: #eee;\n      background: #2f50c6;\n      cursor: pointer;\n    }\n  </style>\n</head>\n<body>\n  <div class=\"main\">\n    <textarea class=\"note\" onkeydown=\"event.ctrlKey&&event.key==='s'&&!event.preventDefault()&&save()\"></textarea>\n    <div>\n      <button class=\"notebtn\" onclick=\"save()\">保存</button>\n      <button class=\"notebtn\" onclick=\"init()\">刷新</button>\n    </div>\n  </div>\n  <script type=\"text/javascript\">\n    let note = document.querySelector('.note')\n    init()\n    function init(){\n      $fend('init').then(res=>res.text()).then(data=>{\n        note.value = data\n      })\n    }\n    function save() {\n      $fend('save', note.value).then(res=>res.text()).then(res=>{\n        alert(res)\n      })\n    }\n  </script>\n</body>\n</html>\n\n<script favend runon=\"elecV2P\">\n  const store_key = 'cloud_notepad'\n  $fend('init', ()=>{\n    return $store.get(store_key)\n  })\n  // add 部分可配合捷径等工具实现分享上传\n  // http.post({ body: { key: \"add\", data: \"some string\", token: \"xxxx-xx\" }})\n  $fend('add', (data)=>{\n    if ($store.put(data, store_key, { type: 'a' })) {\n      return 'success add new data to note ' + store_key\n    }\n    return 'fail to add new data to note ' + store_key\n  })\n  $fend('save', (data)=>{\n    if ($store.put(data, store_key)) {\n      return 'success saved ' + store_key\n    }\n    return 'fail to save ' + store_key\n  })\n</script>"
  },
  {
    "path": "examples/JSTEST/efh/readme.md",
    "content": "### efh 文件适用于 elecV2P favend\n\n更多说明，参考说明文档 [08-logger&efss.md](https://github.com/elecV2/elecV2P-dei/blob/master/docs/08-logger&efss.md) 相关部分。\n\n其他说明：[efh：一种简单的 html 语法扩展结构](https://elecv2.github.io/#efh：一种简单的%20html%20语法扩展结构)\n\n### 基础使用\n\n- 打开 elecV2P webUI/efss -> favend 设置\n- 填写任意名称/关键字 | 运行脚本 | 脚本文件\n- 然后在浏览器打开 http://服务器地址/efss/关键字 页面\n\n### 测试文件\n\n- [notepad.efh](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/examples/JSTEST/efh/notepad.efh) \\- cloud notepad 一个简单的云记事本\n- [markdown.efh](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/examples/JSTEST/efh/markdown.efh) \\- 一个简单的 markdown 阅读器"
  },
  {
    "path": "examples/JSTEST/evui-chatroom.js",
    "content": "// 两个设备分别运行此脚本，可实现一个简单的聊天室。\n// 关于 $evui 的更多说明参考 https://github.com/elecV2/elecV2P-dei/tree/master/docs/04-JS.md 相关部分\n\nlet id = 'aa3a9f9fcc'      // 给聊天室设置一个 ID\n$evui({\n  id,\n  title: 'elecV2P $evui test',\n  width: 800,\n  height: 400,\n  content: \"<p>a simple chatroom</p>\",\n  style: {\n    title: \"background: #6B8E23;\",\n    content: \"background: #FF8033; font-size: 32px; text-align: center\",\n    cbdata: \"height: 320px;\",\n    cbbtn: \"width: 220px;\"\n  },\n  resizable: true,\n  cbable: true,\n  cbdata: 'hello',\n  cblabel: '发送'\n}, data=>{\n  console.log('get new data', data)\n  // 通过 ID 使用 websocket 发送数据到指定客户端\n  $ws.send({ type: 'evui', data: { id, data: data + '\\n' }})\n}).then(data=>console.log(data))\n\nconsole.log(id, 'chatroom is ready')"
  },
  {
    "path": "examples/JSTEST/evui-dou.js",
    "content": "// (!!测试脚本)该脚本用于在前端网页显示近几天的京豆变化。适用环境： elecV2P\n// 参考修改自：https://github.com/dompling/Scriptable/blob/master/Scripts/JDDouK.js\n// 首次运行时耗时较长，请耐心等待\n// 脚本地址：https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/examples/JSTEST/evui-dou.js\n\nclass Widget {\n  constructor() {\n    this.name = \"京东豆收支\";\n    this.JDCookie = {\n      cookie: $store.get('CookieJD'),\n      userName: '',  // 设置显示的用户名，如果为空将使用京东默认昵称代替  \n    };   \n    this.rangeDay = 5;   // 天数范围配置\n    this.cache = true;   // true: 只在每天首次运行时请求新的数据。 false: 每次运行都获取最新数据\n    this.notify = true;  // 是否发送通知\n  }\n\n  rangeTimer = {};\n  timerKeys = [];\n  beanCount = 0;\n  beanChange = [];\n\n  chartConfig = (labels = [], datas = [], datas2 = []) => {\n    const color = `#003153`;\n    let template = `\n{\n  'type': 'bar',\n  'data': {\n    'labels': __LABELS__,\n    'datasets': [\n      {\n        type: 'line',\n        backgroundColor: '#fff',\n        borderColor: getGradientFillHelper('vertical', ['#c8e3fa', '#e62490']),\n        'borderWidth': 2,\n        pointRadius: 5,\n        'fill': false,\n        'data': __DATAS__,\n      },\n      {\n        type: 'line',\n        backgroundColor: '#88f',\n        borderColor: getGradientFillHelper('vertical', ['#c8e3fa', '#0624e9']),\n        'borderWidth': 2,\n        pointRadius: 5,\n        'fill': false,\n        'data': __DATAS2__,\n      },\n    ],\n  },\n  'options': {\n      plugins: {\n        datalabels: {\n          display: true,\n          align: 'top',\n          color: __COLOR__,\n          font: {\n             size: '16'\n          }\n        },\n      },\n      layout: {\n          padding: {\n              left: 0,\n              right: 0,\n              top: 30,\n              bottom: 5\n          }\n      },\n      responsive: true,\n      maintainAspectRatio: true,\n      'legend': {\n        'display': false,\n      },\n      'title': {\n        'display': false,\n      },\n      scales: {\n        xAxes: [\n          {\n            gridLines: {\n              display: false,\n              color: __COLOR__,\n            },\n            ticks: {\n              display: true, \n              fontColor: __COLOR__,\n              fontSize: '16',\n            },\n          },\n        ],\n        yAxes: [\n          {\n            ticks: {\n              display: false,\n              beginAtZero: true,\n              fontColor: __COLOR__,\n            },\n            gridLines: {\n              borderDash: [7, 5],\n              display: false,\n              color: __COLOR__,\n            },\n          },\n        ],\n      },\n    },\n }`;\n\n    template = template.replaceAll(\"__COLOR__\", `'${color}'`);\n    template = template.replace(\"__LABELS__\", `${JSON.stringify(labels)}`);\n    template = template.replace(\"__DATAS__\", `${JSON.stringify(datas)}`);\n    template = template.replace(\"__DATAS2__\", `${JSON.stringify(datas2)}`);\n    return template;\n  };\n\n  init = async () => {\n    try {\n      if (!this.JDCookie.cookie) return;\n      this.rangeTimer = this.getDay(this.rangeDay);\n      this.rangeTimerd = this.getDay(this.rangeDay);\n      this.timerKeys = Object.keys(this.rangeTimer);\n      await this.getAmountData();\n      await this.TotalBean();\n    } catch (e) {\n      console.log(e);\n    }\n  };\n\n  getAmountData = async () => {\n    let i = 0,\n      page = 1;\n    do {\n      let response = await this.getJingBeanBalanceDetail(page);\n      // console.debug(response.data)\n      response = response.data\n      const result = response.code === \"0\";\n      console.log(`正在获取京豆收支明细，第${page}页：${result ? \"请求成功\" : \"请求失败\"}`);\n      if (response.code === \"3\") {\n        i = 1;\n        console.log(response);\n      }\n      if (response && result) {\n        page++;\n        let detailList = response.jingDetailList;\n        if (detailList && detailList.length > 0) {\n          for (let item of detailList) {\n            const dates = item.date.split(\" \");\n            if (this.timerKeys.indexOf(dates[0]) > -1) {\n              const amount = Number(item.amount);\n              if (amount > 0) this.rangeTimer[dates[0]] += amount;\n              else this.rangeTimerd[dates[0]] += amount\n            } else {\n              i = 1;\n              break;\n            }\n          }\n        }\n      }\n    } while (i === 0);\n  };\n\n  getDay(dayNumber) {\n    let data = {};\n    let i = dayNumber;\n    do {\n      const today = new Date();\n      const year = today.getFullYear();\n      const targetday_milliseconds = today.getTime() - 1000 * 60 * 60 * 24 * i;\n      today.setTime(targetday_milliseconds); //注意，这行是关键代码\n      let month = today.getMonth() + 1;\n      month = month >= 10 ? month : `0${month}`;\n      let day = today.getDate();\n      day = day >= 10 ? day : `0${day}`;\n      data[`${year}-${month}-${day}`] = 0;\n      i--;\n    } while (i >= 0);\n    return data;\n  }\n\n  getJingBeanBalanceDetail = async (page) => {\n    try {\n      const options = {\n        url: `https://bean.m.jd.com/beanDetail/detail.json`,\n        body: `page=${page}`,\n        headers: {\n          Accept: \"application/json,text/plain, */*\",\n          \"Content-Type\": \"application/x-www-form-urlencoded\",\n          \"Accept-Encoding\": \"gzip, deflate, br\",\n          \"Accept-Language\": \"zh-cn\",\n          Connection: \"keep-alive\",\n          Cookie: this.JDCookie.cookie,\n          Referer: \"https://wqs.jd.com/my/jingdou/my.shtml?sceneval=2\",\n          \"User-Agent\": \"Mozilla/5.0 (iPhone; CPU iPhone OS 14_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1\",\n        },\n        method: 'post'\n      };\n      return await $axios(options);\n    } catch (e) {\n      console.log(e);\n    }\n  };\n\n  TotalBean = async () => {\n    const options = {\n      \"url\": `https://wq.jd.com/user/info/QueryJDUserInfo?sceneval=2`,\n      \"headers\": {\n        \"Accept\": \"application/json,text/plain, */*\",\n        \"Content-Type\": \"application/x-www-form-urlencoded\",\n        \"Accept-Encoding\": \"gzip, deflate, br\",\n        \"Accept-Language\": \"zh-cn\",\n        \"Connection\": \"keep-alive\",\n        \"Cookie\": this.JDCookie.cookie,\n        \"Referer\": \"https://wqs.jd.com/my/jingdou/my.shtml?sceneval=2\",\n        \"User-Agent\": \"Mozilla/5.0 (iPhone; CPU iPhone OS 14_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1\"\n      }\n    }\n    let res = await $axios(options)\n    if (res && res.data) {\n      let data = res.data\n      if (data.retcode === 0 && data.base) {\n        this.JDCookie.userName = this.JDCookie.userName || data.base.nickname\n        this.beanCount = data.base.jdNum\n      }\n    }\n  }\n\n  createChart = async () => {\n    let labels = [],\n        data = [], data2 = [];\n    Object.keys(this.rangeTimer).forEach((month) => {\n      const value = this.rangeTimer[month];\n      const arrMonth = month.split(\"-\");\n      labels.push(`${arrMonth[1]}.${arrMonth[2]}`);\n      data.push(value);\n      data2.push(this.rangeTimerd[month])\n    });\n    this.beanChange.push(data)\n    this.beanChange.push(data2)\n    const chartStr = this.chartConfig(labels, data, data2);\n    console.debug(chartStr);\n\n    return await this.chartUrl(chartStr)\n  };\n\n  chartUrl = async (data) => {\n    const req = {\n      url: 'https://quickchart.io/chart/create',\n      headers: {\n        'Content-Type': 'application/json'\n      },\n      method: 'post',\n      data: { \n        \"backgroundColor\": \"transparent\",\n        \"width\": 580,\n        \"height\": 320,\n        \"format\": \"png\", \n        \"chart\": data\n      }\n    }\n    return await $axios(req)\n  }\n}\n\n!(async ()=>{\n  let evdou = $store.get('evdou'),\n      today = new Date().getDay()\n  const eDou = new Widget()\n  if (eDou.cache && evdou && evdou.day === today && evdou.imgurl) {\n    console.log('使用 cache 数据显示', eDou.name)\n  } else {\n    await eDou.init()\n    let res = await eDou.createChart()\n    let data = res.data\n    if (data && data.success) {\n      evdou = {\n        day: today,\n        userName: eDou.JDCookie.userName,\n        total: eDou.beanCount,\n        change: eDou.beanChange,\n        imgurl: data.url,\n      }\n      $store.put(evdou, 'evdou')\n    } else {\n      console.log(data)\n    }\n  }\n\n  if (evdou.imgurl) {\n    showChart(evdou.imgurl, evdou.userName, evdou.total, eDou.name)\n    if (eDou.notify) {\n      let body = evdou.userName + ': ' + evdou.total\n      if (evdou.change) {\n        body += '\\n' + '近期收入：' + evdou.change[0].join(', ')\n        body += '\\n' + '近期支出：' + evdou.change[1].join(', ')\n      }\n      $feed.push(eDou.name, evdou.userName + ': ' + evdou.total, evdou.imgurl)\n    }\n  }\n})().catch(e=>console.log(e))\n\nfunction showChart(imgurl, userName, total, title) {\n  $evui({\n    title,\n    width: 640,\n    height: 389,\n    content: `<div style=\"filter: blur(3px);-webkit-filter: blur(3px);background: url(https://bing.ioliu.cn/v1/rand);height: 100%;\"></div><div style=\"position: absolute;right: 12px;top: 46px;padding: 8px;border: 1px solid #003153;border-radius: 20px;\">${userName}: ${total}</div><img style=\"background: #ffffff88;position: absolute;top: 36px;left: 0;\" src=\"${imgurl}\">`,\n    style: {\n      title: \"background: #6B8E23;\",\n      content: \"text-align: center\"\n    },\n    resizable: true,\n  }).then(data=>console.log(data)).catch(e=>console.log(e))\n}"
  },
  {
    "path": "examples/JSTEST/exam-ahk-send.js",
    "content": "// 通过 JS 配合 autohotkey 发送文字和按键信息\n\n$exec('sendkey.ahk \"english or 中文 ^v ctrl+v is send\"')"
  },
  {
    "path": "examples/JSTEST/exam-ahk.js",
    "content": "// 一个配合 autohotkey 移动鼠标的小脚本\n\n$exec('mousemove.ahk left', {\n  cwd: './script/Shell'\n})"
  },
  {
    "path": "examples/JSTEST/exam-chcp.js",
    "content": "const command = 'CHCP 65001'\r\n\r\n$exec(command, {\r\n  cb(data, error){\r\n    error ? console.error(error) : console.log(data)\r\n  }\r\n})"
  },
  {
    "path": "examples/JSTEST/exam-clipboard.js",
    "content": "$exec('powershell -command \"Get-Clipboard | echo\"', {\n  cb(data, error){\n    error ? console.error(error) : console.log(data)\n  }\n})"
  },
  {
    "path": "examples/JSTEST/exam-rss.js",
    "content": "// 使用 cheerio 解析 rss 的小例子\n// iOS 限免软件推送。需要先设置好 IFTTT\n\nconst feedurl = 'https://rsshub.app/telegram/channel/BaccanoSoul/%23%E9%99%90%E5%85%8D%E6%9B%B4%E6%96%B0%E6%9D%BFiOS'\n// rss 来源： https://docs.rsshub.app/\n\n$axios(feedurl).then(res=>{\n  const $ = $cheerio.load(res.data, {\n    xml: {\n      normalizeWhitespace: true,\n      xmlMode: true,\n    },\n  })\n  const pubDate = $('item pubDate').eq(0).text()\n  console.log('last item publish date:', pubDate)\n  const lastdate = new Date(pubDate).getTime()\n  const laststore = $store.get('lastrss')\n  if (laststore && laststore >= lastdate) {\n    console.log('no new item')\n    return\n  }\n  $store.put(lastdate, 'lastrss')\n  const items = $('item')\n  for (var i = 0; i < items.length; i++) {\n    const itemdate = new Date($('pubDate', items[i]).text()).getTime()\n    if (laststore && laststore >= itemdate) {\n      return\n    }\n    const title = $('title', items[i]).text().split(' ¥')\n    console.log('new item', title)\n    const description = $('description', items[i]).text()\n    const content = $cheerio.load(description)\n    const link = content('a[href*=apple]').attr('href')\n    $feed.ifttt(title[0], title[1], link)\n    console.log(content.text())\n  }\n  console.log('all new item pushed')\n}).catch(e=>console.error(e.stack))"
  },
  {
    "path": "examples/JSTEST/exam-tasksub.js",
    "content": "// 通过 webhook 添加定时任务订阅。运行前根据具体情况修改 suburl 和 webhook 里面的内容\n// 每次运行都会添加新任务，请不要多次运行\n// 这只是一个简单的范例，如果出现未知问题，手动修正一下代码\n\nconst suburl = 'https://raw.githubusercontent.com/nzw9314/QuantumultX/master/Task_Remote.conf'\n\nconst webhook = {\n  url: '/webhook',              // 远程： http://sss.xxxx.com/webhook\n  token: 'a8c259b2-67fe-4c64-8700-7bfdf1f55cb3',     // 在 webUI->SETTING 界面查找\n}\n\n$axios(suburl).then(res=>{\n  const body = res.data\n  const mastr = body.matchAll(/([0-9\\-\\*\\/]+ [0-9\\-\\*\\/]+ [0-9\\-\\*\\/]+ [0-9\\-\\*\\/]+ [0-9\\-\\*\\/]+( [0-9\\-\\*\\/]+)?) ([^ ,]+), ?tag=([^, \\n\\r]+)/g)\n\n  ;[...mastr].forEach(mr=>{\n    if (mr[3] && mr[1]) {\n      $axios({\n        url: webhook.url,\n        method: 'post',\n        data: {\n          token: webhook.token,\n          type: 'taskadd',\n          task: {\n            name: mr[4] || 'tasksub-新的任务',\n            type: 'cron',\n            job: {\n              type: 'runjs',\n              target: mr[3],\n            },\n            time: mr[1],\n            running: true        // 是否自动执行添加的任务\n          }\n        }\n      }).then(res=>console.log(res.data))\n    }\n  })\n}).catch(e=>console.error(e))"
  },
  {
    "path": "examples/JSTEST/example-cheerio.js",
    "content": "// a simply $cheerio eaxmple. modify from cheerio readme.md\n\nconst $ = $cheerio.load(`<ul id=\"fruits\">\n  <li class=\"apple\">Apple</li>\n  <li class=\"orange\">Orange</li>\n  <li class=\"pear\">Pear</li>\n</ul>`);\n\nconst apple = $('.apple', '#fruits').text()\nconsole.log(apple)\n\nconst attr = $('ul .pear').attr('class');\nconsole.log(attr)\n\nconst html = $('#fruits').html();\nconsole.log(html)\n\n$done($('.pear').text())"
  },
  {
    "path": "examples/JSTEST/example-rule.js",
    "content": "// a example for rule\r\n\r\n// $request.headers, $request.body, $request.method, $request.hostname, $request.port, $request.path, $request.url\r\n// $response.headers, $response.body, $response.statusCode\r\n\r\nlet body = $response.body\r\n// let obj = JSON.parse(body)\r\nif (/httpbin/.test($request.url)) {\r\n  body += 'change by elecV2P' + body\r\n}\r\n$done({ body })"
  },
  {
    "path": "examples/JSTEST/fendtest.efh",
    "content": "<title>efh fend 测试</title>\n<h3>efh - elecV2P favend html, 一个同时包含前后端运行代码的 html 扩展格式。</h3>\n<p>目前仅可运行于 elecV2P favend, 相关说明参考：<a href=\"https://github.com/elecV2/elecV2P-dei/blob/master/docs/08-logger&efss.md\" target=\"_blank\">elecV2P-dei/efss.md</a> 相关部分</p>\n<div>\n  <label>请求后台数据测试</label>\n  <input type=\"text\" name=\"test\" class=\"fend\">\n  <button onclick=\"dataFetch()\">获取</button>\n</div>\n<p>\n  <label>该 efh 文件地址: </label>\n  <a href='https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/elecV2P.efh' target='_blank'>https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/elecV2P.efh</a>\n</p>\n\n<script type=\"text/javascript\">\n  // 前端部分可使用多个 script 标签引入远程 axios/vue/react 等文件\n  // 总之这部分完全和之前的 html 一样\n  async function dataFetch() {\n    let data = await $fend('newone').then(res=>res.text()).catch(e=>console.error(e))\n    console.log(data)\n    // alert(data)\n    let sd = document.querySelector('.fend').value;\n    console.log('send data to eo', sd);\n    $fend('eo', sd).then(res=>res.text()).then(console.log);\n  }\n</script>\n\n<script type=\"text/javascript\" runon=\"elecV2P\" srcd=\"favend.js\">\n  // 使用 runon=\"elecV2P\" 属性来表示此部分是运行在后台的代码\n  // 使用 src 属性表示使用服务器上的 JS 作为后台代码（支持远程\n  // 当有 src 属性时下面的代码无效（建议测试时去掉\n  $fend('newone', {\n    body: {\n      hello: 'elecV2P favend',\n      data: 'wowo',\n      reqbody: $request.body\n    }\n  })\n\n  $fend('eo', d=>{\n    console.log('收到前台传输数据:', d);\n\n    return {\n      // statusCode: 404,\n      // header: {},\n      // body: ''\n      ok: true,\n      data: d,\n      message: '后台返回数据，可以是 string 或 object'\n    }\n  })\n</script>"
  },
  {
    "path": "examples/JSTEST/github-subdownload.js",
    "content": "// 功能: github 子目录文件下载（仅适用于 elecV2P\n// 作者: https://t.me/elecV2\n// 文件地址: https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/github-subdownload.js\n// 最近更新: 2022-10-05\n\nconst config = {\n  repos: $env.repos || 'elecV2/elecV2P-dei',     // github 仓库名。比如 elecV2/elecV2P\n  folder: $env.folder || 'examples/JSTEST',      // 子目录。比如 script/JSFile\n  dest: $env.dest || 'script/JSFile',            // 下载到此目录。\n  options: {\n    recursive: $env.recursive ?? true,    // 是否下载 folder 下的子目录文件\n    onlyreg: $env.onlyreg || '',          // 只下载文件名满足该正则表达式的文件\n    skipreg: $env.skipreg || '',          // 跳过下载文件名满足该正则表达式的文件\n    sizemax: $env.sizemax || 0,           // 当文件大小超过此设置值时，不下载。0: 表示不限制\n  }\n}\n\ngetTree(`https://api.github.com/repos/${config.repos}/contents/${config.folder}`).then(tree=>{\n  main(tree, config.dest, config.options)\n}).catch(e=>console.error(e.message))\n\nasync function getTree(apigit) {\n  try {\n    console.log('start get', apigit, 'tree')\n    return await $axios(apigit).then(res=>res.data)\n  } catch(e) {\n    console.error('get', apigit, 'error', e.message)\n    return []\n  }\n}\n\nasync function main(tree, dest, options = { recursive: true, onlyreg: '', skipreg: '', sizemax: 0 }) {\n  if (!(typeof tree === 'object' && tree.length > 0 && typeof dest === 'string')) {\n    console.log('输入参数有误', tree, dest)\n    return\n  }\n  const onlyreg = new RegExp(options.onlyreg), skipreg = new RegExp(options.skipreg)\n  for (let file of tree) {\n    if (file.type === 'file') {\n      if (options.sizemax > 0 && file.size > options.sizemax) {\n        console.log('skip download', file.name, 'file size:', file.size, 'is big than', options.sizemax)\n        continue\n      }\n      if (options.onlyreg) {\n        if (!onlyreg.test(file.name)) {\n          console.log('skip download', file.name, 'for onlyreg', onlyreg)\n          continue\n        }\n      }\n      if (options.skipreg) {\n        if (skipreg.test(file.name)) {\n          console.log('skip download', file.name, 'for skipreg', skipreg)\n          continue\n        }\n      }\n\n      mulDownload(file['download_url'], dest, file.name)\n    } else {\n      if (options.recursive) {\n        await main(await getTree(file.url), dest + '/' + file.name, options)\n      } else {\n        console.log(`不下载 ${file.name}, 类型：${file.type}`)\n      }\n    }\n  }\n}\n\n// 简异版多线程下载\nlet count = 0, todownlist = [];\nfunction mulDownload(url, dest, name) {\n  if (count > 5) {\n    todownlist.push([url, dest, name])\n    return\n  }\n  count++\n  console.log('当前下载文件数:', count, '等待下载数:', todownlist.length)\n  $download(url, {\n    folder: dest,\n    name, existskip: true,\n  }, (d) => {\n    if (d.progress) {\n      console.log(d.progress + '\\r\\x1b[F')\n    }\n  }).then(res=>{\n    console.log(name, '下载结果', res)\n  }).catch(e=>{\n    console.error(name, '下载错误', e.message || e)\n  }).finally(()=>{\n    count--\n    if (todownlist.length) {\n      mulDownload(...todownlist.shift())\n    } else if (count === 0) {\n      console.log('所有文件下载完成（如有错误或漏下，可以重新运行一次脚本）')\n    } else {\n      console.log('当前下载文件数:', count)\n    }\n  })\n}"
  },
  {
    "path": "examples/JSTEST/markdown.efh",
    "content": "<!-- efh 小应用，适用于 elecV2P\n功能：一个简单的 markdonw 文件阅读器\n使用：\n- 打开 webUI/efss -> favend 设置\n- 填写任意名称/关键字 | 运行脚本 | https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/examples/JSTEST/markdown.efh\n更新：2022-10-03 21:30\n-->\n<!DOCTYPE html>\n<html>\n<head>\n  <title>a simple markdown reader</title>\n  <style type=\"text/css\">\n    .mflex{display:flex;justify-content:center;align-items:center;flex-wrap: wrap;}.minput{min-width:320px;height:32px;font-size:1em;padding:0 .4em;border-radius:.5em;box-sizing: border-box;border: 1px solid #003153;}.mbutton{height:32px;padding:0 1em;margin:3px 1em;border:0;color:#fbfbff;background:#003153;border-radius:.5em;font-size:1em;cursor:pointer}html,body{font-family:'Microsoft YaHei',-apple-system,Helvetica,Arial}pre, code {font-size: 16px;font-family: Consolas, Inconsolata, Courier, monospace;margin: auto 5px;}code {white-space: pre-wrap;border-radius: 2px;display: inline;}pre {font-size: 18px;line-height: 1.4em;display: block;border: 1px solid #ccc;border-radius: 8px;padding: 10px;color: #333;background: #f8f8f8;overflow-x: auto;}pre code {display: block;padding: 0;white-space: pre-wrap;background-color: transparent;border-radius: 0;}strong, b{color: #BF360C;}em, i {color: #009688;}hr {border: 1px solid #BF360C;margin: 1.5em auto;}p {margin: 1.5em 5px;word-break: break-word;text-align: justify;letter-spacing: 1px;line-height: 1.5em;}table, pre, dl, blockquote, q, ul, ol {margin: 10px 5px;}ul, ol {padding-left: 15px;}li {margin: 10px;}li p {margin: 10px 0 !important;}ul ul, ul ol, ol ul, ol ol {margin: 0;padding-left: 10px;}ul {list-style-type: circle;}dl {padding: 0;}dl dt {font-size: 1em;font-weight: bold;font-style: italic;}dl dd {margin: 0 0 10px;padding: 0 10px;}blockquote, q {border-left: 2px solid #009688;padding: 0 10px;color: #777;quotes: none;margin-left: 1em;}blockquote::before, blockquote::after, q::before, q::after {content: none;}h1, h2, h3, h4, h5, h6 {margin: 20px 0 10px;padding: 0;font-style: bold;color: #009688;text-align: center;margin: 1.5em 5px;padding: 0.5em 1em;}h1 {font-size: 24px;border-bottom: 1px solid #A8A8A8;}h2 {font-size: 20px;border-bottom: 1px solid #A8A8A8;}h3 {font-size: 18px;border-bottom: 1px solid #A8A8A8;}h4 {font-size: 16px;}img {display: flex;max-width: 100%;margin: auto;}table {padding: 0;border-collapse: collapse;border-spacing: 0;font-size: 1em;font: inherit;border: 0;margin: 0 auto;}tbody {margin: 0;padding: 0;border: 0;}table tr {border: 0;border-top: 1px solid #CCC;background-color: white;margin: 0;padding: 0;}table tr:nth-child(2n) {background-color: #F8F8F8;}table tr th, table tr td {font-size: 16px;border: 1px solid #CCC;margin: 0;padding: 5px 10px;}table tr th {font-weight: bold;color: #eee;border: 1px solid #009688;background-color: #009688;}\n  </style>\n</head>\n<body>\n  <div class=\"mflex\">\n    <input class=\"minput\" type=\"text\" placeholder=\"markdown file url\">\n    <button class=\"mbutton\" onclick=\"dataFetch()\">显示</button>\n    <button class=\"mbutton\" onclick=\"dataLocal()\">本地 .md 文件</button>\n  </div>\n  <div class=\"markdown_content\"></div>\n  <script type=\"text/javascript\" src=\"https://cdn.jsdelivr.net/npm/marked/marked.min.js\" defer></script>\n  <script type=\"text/javascript\">\n    let minput = document.querySelector('.minput')\n    let mcontent = document.querySelector('.markdown_content')\n\n    function dataFetch() {\n      if (minput.value) {\n        $fend('data', minput.value).then(res=>res.text()).then(res=>{\n          parseContent(res)\n        }).catch(e=>{\n          console.error(e)\n          alert(e.message)\n        })\n      } else {\n        alert('input first')\n      }\n    }\n    function dataLocal() {\n      getFile({ accept: '.md' }).then(data=>{\n        parseContent(data.content)\n      })\n    }\n    function parseContent(content) {\n      mcontent.innerHTML = marked.parse(content)\n      const titleH = document.querySelector('h1') || document.querySelector('h2')\n      if (titleH) {\n        document.title = titleH.innerText\n      }\n    }\n    function getFile({ accept = '*', type = 'text', multiple = false } = {}) {\n      let input = document.createElement('input')\n      input.type = 'file'\n      input.accept = accept\n      if (multiple) {\n        input.multiple = true\n      }\n\n      return new Promise((resolve, reject)=>{\n        input.onchange = e => {\n          let file = e.target.files[0]\n          if (!file) {\n            reject('请先选择文件')\n            return\n          }\n          console.debug('get file', file.name, file.type, file.size);\n          if (type === 'file') {\n            resolve(file)\n          } else {\n            let reader = new FileReader()\n            reader.readAsText(file, 'UTF-8')\n\n            reader.onload = readerEvent => {\n              resolve({\n                name: file.name,\n                type: file.type,\n                size: file.size,\n                content: readerEvent.target.result\n              })\n            }\n          }\n        }\n        input.click()\n      })\n    }\n  </script>\n<script type=\"text/javascript\" runon=\"elecV2P\">\n  $fend('data', async (url)=>{\n    let md = await $axios(url)\n    return md.data\n  })\n</script>\n</body>\n</html>"
  },
  {
    "path": "examples/JSTEST/reboot.js",
    "content": "// 系统重启脚本，谨慎使用，无法取消\n\nconst countdown = 30              // 重启等待时间，单位：秒\nconsole.log('操作系统将在', countdown, '后重启')\n\nsetTimeout(()=>{\n  $exec('reboot', data=>console.log(data))\n}, countdown*1000)"
  },
  {
    "path": "examples/JSTEST/simple.efh",
    "content": "<h3>一个简单的 efh 格式示例文件</h3>\n<div><label>请求后台数据测试</label><button onclick=\"dataFetch()\">获取</button></div>\n\n<script type=\"text/javascript\">\n  // 前端部分可使用多个 script 标签引入远程 axios/vue/UI 框架等文件\n  // $fend 默认函数用于前后端数据交互（本质为一个 fetch 的 post 请求\n  function dataFetch() {\n    $fend('data').then(res=>res.text())\n    .then(res=>{\n      console.log(res);\n      alert(res);\n    })\n    .catch(e=>{\n      console.error(e);\n      alert('error: ' + e.message);\n    })\n  }\n</script>\n\n<script type=\"text/javascript\" runon=\"elecV2P\" srcf=\"favend.js\">\n  // 使用 runon=\"elecV2P\" 属性来表示此部分是运行在后台的代码\n  // 使用 src 属性表示使用服务器上的 JS 作为后台代码（支持远程\n  // 当有 src 属性时下面的代码无效（建议测试时去掉\n  // 后台 $fend 第一参数需与前端对应，第二参数为返回数据\n  $fend('data', {\n    hello: 'elecV2P favend',\n    data: $store.get('cookieKEY'),\n    reqbody: $request.body\n  })\n  $done('no $fend match');\n  $fend('no', '$done 后面的 $fend 无任何意义，因为 $done 会提前返回结果，这部分数据永远不会传输给前端。');\n</script>"
  },
  {
    "path": "examples/JSTEST/starturl.js",
    "content": "/**\r\n * windows 上通过默认浏览器打开对应 url.\r\n */\r\n\r\n$exec('start https://github.com/elecV2/elecV2P', {})"
  },
  {
    "path": "examples/JSTEST/tgbotmessage.js",
    "content": "/**\r\n * 功能：使用TG bot 给频道或他人发信息\r\n * 使用方法：\r\n * 先使用 https://t.me/BotFather 创建一个机器人，获取 bot token\r\n * 然后获取目标用户的 chatid，填写到下面对应位置。 然后start bot\r\n *\r\n * 如果要发送到频道，先将机器人拉到频道并给予管理员权限\r\n *\r\n * cron  8 8 8 * * * https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/examples/JSTEST/tgbotmessage.js\r\n */\r\n\r\n\r\nconst CONFIG = {\r\n  chatid: '8xxxxxxxxxx',      // 接受信息的用户 id\r\n  token: '8161xxx-xxxxxx'     // tg bot took\r\n}\r\n\r\n// message 可根据自己的需求进行修改，支持 markdown 语法\r\nlet message = `[必应随机壁纸](https://bing.ioliu.cn/v1/rand?${Date.now()})`   // api 来源： https://github.com/xCss/bing\r\n\r\n\r\nconst payload = {\r\n  \"method\": \"sendMessage\",\r\n  \"chat_id\": CONFIG.chatid,\r\n  \"parse_mode\": \"markdown\",\r\n  \"disable_web_page_preview\": false,\r\n  \"text\": 'hello world!'\r\n}\r\n\r\npayload.text = message\r\n\r\nconst myRequest = {\r\n  url: `https://api.telegram.org/bot${CONFIG.token}/`,\r\n  method: 'POST',\r\n  headers: {\r\n    'Content-Type': 'application/json;charset=UTF-8'\r\n  },\r\n  body: JSON.stringify(payload)\r\n}\r\n\r\n$task.fetch(myRequest).then(res => {\r\n  try {\r\n    let body = JSON.parse(res.body)\r\n    if (body.ok) {\r\n      let result = body.result\r\n      console.log('send', result.chat.username, result.chat.first_name, result.chat.last_name, 'message:', result.text)\r\n    } else {\r\n      console.log(body)\r\n    }\r\n  } catch {\r\n    console.log(res.body)\r\n  }\r\n}, error => {\r\n  console.log(error)\r\n})"
  },
  {
    "path": "examples/JSTEST/webonlinetest.js",
    "content": "// 网站是否在线监控\r\n\r\nurl = 'https://github.com/favicon.ico'  // 监控网址\r\n\r\nnew Promise(resolve => {\r\n  let note = ''\r\n  $axios(url).then(res=>{\r\n    if (res.status !== 200) {\r\n      $feed.ifttt('网站下线 - ' + res.status, '网址：' + url)\r\n      console.log('网站下线', url, res.status)\r\n      note = `网站下线 - ${res.status} ${url}`\r\n    } else {\r\n      console.log(url, '稳定运行中')\r\n      note = url + ' 稳定运行中'\r\n    }\r\n  }).catch(e=>{\r\n    $feed.ifttt(e.message, '无法访问网站: ' + url)\r\n    console.log('无法访问网站', url, e.message)\r\n    note = '无法访问网站: ' + url + e.message\r\n  }).finally(()=>{\r\n    resolve(note)\r\n  })\r\n})"
  },
  {
    "path": "examples/Readme.md",
    "content": "## 主要用于一些测试和备份（仅供参考）\n\n### [JSTEST](https://github.com/elecV2/elecV2P-dei/tree/master/examples/JSTEST)\n\n一些测试 JS 文件，比如：\n\n- [boxjs.ev.js](https://github.com/elecV2/elecV2P-dei/blob/master/examples/JSTEST/boxjs.ev.js) \\- boxjs elecV2P 兼容版 (v3.2.3 版本后可直接使用 chavyleung 的原版)\n\n![](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/examples/res/boxjs-test.png)\n\n- [github-subdownload.js](https://github.com/elecV2/elecV2P-dei/blob/master/examples/JSTEST/github-subdownload.js) \\- github 子目录文件下载\n- [exam-rss.js](https://github.com/elecV2/elecV2P-dei/blob/master/examples/JSTEST/exam-rss.js) \\- 使用 cheerio 解析 rss 实现限免软件推送\n- [reboot.js](https://github.com/elecV2/elecV2P-dei/blob/master/examples/JSTEST/reboot.js) \\- 通过 JS 重启服务器\n- [evui-dou.js](https://github.com/elecV2/elecV2P-dei/blob/master/examples/JSTEST/evui-dou.js) \\- 在前端网页显示京东豆收支图表\n\n![](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/examples/res/evuidou.png)\n\n等等\n\n*如果有其他比较好玩或有用的脚本，欢迎 Pull Request*\n\n### efh 实际应用系列\n\n关于 efh 格式文件的说明，参考说明文档 [08-logger&efss.md](https://github.com/elecV2/elecV2P-dei/blob/master/docs/08-logger&efss.md) 中的相关部分。\n\n- [markdown.efh](https://github.com/elecV2/elecV2P-dei/blob/master/examples/JSTEST/efh/markdown.efh) \\- 一个简单的 markdown 阅读器\n\n- [notepad.efh](https://github.com/elecV2/elecV2P-dei/blob/master/examples/JSTEST/efh/notepad.efh) \\- 一个简单的云端记事本\n- [kuwo-music.efh](https://github.com/elecV2/elecV2P-dei/blob/master/examples/JSTEST/efh/kuwo-music.efh) \\- 酷我音乐下载\n\n- [其他 efh 脚本](https://github.com/elecV2/elecV2P-dei/tree/master/examples/JSTEST/efh)\n\n### [TGbotonCFworker.js](https://github.com/elecV2/elecV2P-dei/blob/master/examples/TGbotonCFworker.js) - 通过 TG bot 控制 elecV2P\n\n2.0 版本(新增上下文执行环境): https://github.com/elecV2/elecV2P-dei/blob/master/examples/TGbotonCFworker2.0.js\n\n![](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/examples/res/tgbot.png)\n\n可实现功能：（所有操作可在 telegram 上完成）\n- 运行 JS\n- 获取/删除 日志\n- 获取服务内存占用信息\n- 获取定时任务信息\n- 开始/暂停 定时任务\n- 删除/保存 定时任务\n- 执行 shell 指令\n- store/cookie 常量管理\n\n前提: elecV2P 服务器可通过外网访问\n\n具体使用见脚本内注释内容"
  },
  {
    "path": "examples/Shell/elecV2P-runjs.ahk",
    "content": "﻿; 功能：\n;   使用 win + j 快捷键快速执行选择代码或远程 JS\n; 使用： (提前安装好 autohotkey)\n;   1. 修改 webhook url 和 token 为 elecV2P 实际运行值\n;   2. 通过 autohotkey 运行该脚本\n;   3. 选择一段 JS 代码（比如：console.log(\"a autokey test\");$result=\"hello ahk\"），然后按 win+j。 相关代码会上传到 elecV2P 并执行，并返回相关结果。\n;   -. 也可以选择一个远程 JS 链接（比如：https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/webhook.js），然后按 win+j。 elecV2P 会自动执行远程 JS，并返回相关结果\n\n#j::\nwebhook := \"http://127.0.0.1/webhook\"        ; or https://remote.xxxx.com/webhook\ntoken   := \"a8c259b2-67fe-4c64-8700-7bfdf1f55cb3\"\n\nSend, ^c        ; 复制选择内容\nif clipboard = \"\"\n  return\nclipboard := RegExReplace(clipboard, \"\"\"\", \"\\\"\"\")\nclipboard := RegExReplace(clipboard, \"`r`n|`r|`n\", \"\\n\")\nif (RegExMatch(url, \"^http\")){\n  body = {\"token\":\"%token%\", \"type\":\"runjs\", \"fn\":\"%clipboard%\"}\n} else {\n  body = {\"token\":\"%token%\", \"type\":\"runjs\", \"rawcode\":\"%clipboard%\"}\n}\nreq(webhook, \"POST\", body)\nReturn\n\nReq(url, method, body) {\n  WinHTTP := ComObjCreate(\"WinHTTP.WinHttpRequest.5.1\")\n  ;~ WinHTTP.SetProxy(0)\n  ; MsgBox % body\n  WinHTTP.Open(method, url)\n  WinHTTP.SetRequestHeader(\"Content-Type\", \"application/json;charset=utf-8\")\n  WinHTTP.Send(body)\n  Result := WinHTTP.ResponseText\n  Status := WinHTTP.Status\n\n  msgbox % \"status: \" status \"`n`nresult: \" result\n}"
  },
  {
    "path": "examples/Shell/exam-request.py",
    "content": "import requests, json\r\n\r\nre = requests.get(\"http://httpbin.org/json\")\r\n# re.encoding = 'UTF-8'\r\n\r\njsre = json.loads(re.text)\r\nprint(jsre)"
  },
  {
    "path": "examples/Shell/mousemove.ahk",
    "content": "﻿; 通过附带参数移动鼠标。 windows 平台使用，且已安装好 autohotkey\r\n; 示例：\r\n; mousemove.ahk left 400  ; 鼠标左移 400 Pixel\r\n; mousemove.ahk click 300 400 4   ; 在屏幕 300，400 的位置点击鼠标 4 次\r\n\r\n; MsgBox Parameter number %1%\r\n\r\nif %1%\r\n  direction = %1%\r\nelse\r\n  direction := \"left\"\r\n\r\nif %2%\r\n  movestep = %2%\r\nelse\r\n  movestep := 30\r\n\r\nif (direction = \"left\")\r\n  MouseMove, -movestep, 0, 0, R\r\nelse if (direction = \"right\")\r\n  MouseMove, movestep, 0, 0, R\r\nelse if (direction = \"up\")\r\n  MouseMove, 0, -movestep, 0, R\r\nelse if (direction = \"down\")\r\n  MouseMove, 0, movestep, 0, R\r\nelse if (direction = \"click\")\r\n  MouseClick, left, %2%, %3%, %4%"
  },
  {
    "path": "examples/Shell/sendkey.ahk",
    "content": "﻿#NoEnv  ; Recommended for performance and compatibility with future AutoHotkey releases.\r\n; #Warn  ; Enable warnings to assist with detecting common errors.\r\nSendMode Input  ; Recommended for new scripts due to its superior speed and reliability.\r\nSetWorkingDir %A_ScriptDir%  ; Ensures a consistent starting directory.\r\n\r\n; 发送按键的小脚本\r\n; senkey.ahk \"english or 中文 ^v ctrl+v is send\"\r\n\r\nSend, %1%"
  },
  {
    "path": "examples/TGbotonCFworker.js",
    "content": "/**\r\n * 说明：可部署到 cloudfalre worker 的 TGbot 后台代码，用于通过 telegram 查看/控制 elecV2P\r\n * 地址：https://github.com/elecV2/elecV2P-dei/blob/master/examples/TGbotonCFworker.js\r\n * (该版本基本不再更新，最新功能见 2.0 版本 https://github.com/elecV2/elecV2P-dei/blob/master/examples/TGbotonCFworker2.0.js)\r\n *\r\n * 使用方式：\r\n * 先申请好 TG BOT(https://t.me/botfather)，然后设置好 CONFIG 内容\r\n * tgbot token: 在 telegram botfather 中找到 api token, 然后填写到相应位置\r\n * 然后把修改后的整个 JS 内容粘贴到 cloudfalre worker 代码框，保存即可。得到一个类似 https://xx.xxxxx.workers.dev 的网址\r\n * 接着使用 https://api.telegram.org/bot(你的 tgbot token)/setWebhook?url=https://xx.xxxxx.workers.dev 给 tg bot 添加 webhook，部署完成\r\n * 最后，打开 tgbot 对话框，输入下面的相关指令，测试 TGbot 是否成功\r\n *\r\n * *假如返回数据有问题，先直接访问 elecV2P webhook 看是否正常。http://你的 elecV2P 服务器地址/webhook?token=你的webhook token&type=status*\r\n * \r\n * 实现功能及相关指令：\r\n * 查看服务器资源使用状态\r\n * status === /status  ;任何包含 status 关键字的指令\r\n * \r\n * 删除 log 文件\r\n * /delete file === /delete file.js.log === /del file\r\n * /delete all  ;删除使用 log 文件\r\n *\r\n * 查看 log 文件\r\n * /log file === file === file.js.log\r\n * all    ;返回所有 log 文件列表\r\n *\r\n * 任务相关\r\n * /taskinfo taskid     ;获取任务信息\r\n * /taskinfo all        ;获取所有任务信息\r\n * /taskstart taskid    ;开始任务\r\n * /taskstop taskid     ;停止任务\r\n * /taskdel taskid      ;删除任务\r\n * /tasksave            ;保存当前任务列表\r\n * \r\n * 脚本相关\r\n * /listjs              ;列出所有 JS 脚本。\r\n * /runjs file.js       ;运行脚本\r\n * /runjs https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/webhook.js\r\n * /deljs file.js       ;删除脚本\r\n *\r\n * bot commands\r\nrunjs - runjs\r\ndeljs - delete js\r\nlistjs - list all js\r\nstatus - memory usage status\r\ntasksave - save task\r\ntaskinfo - get task info\r\ntaskstop - stop a task\r\ntaskstart - start a task\r\ntaskdel - delete a task\r\ndelete - delete logs\r\nlog - get log file\r\n**/\r\n\r\nconst CONFIG_EV2P = {\r\n  url: \"http://你的 elecV2P 服务器地址/\",    // elecV2P 服务器地址(必须是域名，cf worker 不支持 IP 直接访问)\r\n  wbrtoken: 'xxxxxx-xxxxxxxxxxxx-xxxx',      // elecV2P 服务器 webhook token(在 webUI->SETTING 界面查看)\r\n  token: \"xxxxxxxx:xxxxxxxxxxxxxxxxxxx\",     // teleram bot token\r\n  slice: -800,           // 截取日志最后 800 个字符，以防太长无法传输（可自行调整）\r\n  userid: null           // 只对该 userid 发出的指令进行回应。null：回应所有用户的指令\r\n}\r\n\r\nif (!CONFIG_EV2P.url.endsWith('/')) {\r\n  CONFIG_EV2P.url = CONFIG_EV2P.url + '/'\r\n}\r\n\r\nfunction getLogs(s){\r\n  return new Promise((resolve,reject)=>{\r\n    fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=getlog&fn=' + s).then(res=>res.text()).then(r=>{\r\n      resolve(r)\r\n    }).catch(e=>{\r\n      reject(e)\r\n    })\r\n  })\r\n}\r\n\r\nfunction delLogs(logn) {\r\n  return new Promise((resolve,reject)=>{\r\n    fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=deletelog&fn=' + logn).then(res=>res.text()).then(r=>{\r\n      resolve(r)\r\n    }).catch(e=>{\r\n      reject(e)\r\n    })\r\n  })\r\n}\r\n\r\nfunction getStatus() {\r\n  return new Promise((resolve,reject)=>{\r\n    fetch(CONFIG_EV2P.url + 'webhook?type=status&token=' + CONFIG_EV2P.wbrtoken).then(res=>res.text()).then(r=>{\r\n      resolve(r)\r\n    }).catch(e=>{\r\n      reject(e)\r\n    })\r\n  })\r\n}\r\n\r\nfunction getTaskinfo(tid) {\r\n  return new Promise((resolve,reject)=>{\r\n    fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=taskinfo&tid=' + tid).then(res=>res.text()).then(r=>{\r\n      resolve(r)\r\n    }).catch(e=>{\r\n      reject(e)\r\n    })\r\n  })\r\n}\r\n\r\nfunction opTask(tid, op) {\r\n  if (!/start|stop|del|delete/.test(op)) {\r\n    return 'unknow operation' + op\r\n  }\r\n  return new Promise((resolve,reject)=>{\r\n    fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=task' + op + '&tid=' + tid).then(res=>res.text()).then(r=>{\r\n      resolve(r)\r\n    }).catch(e=>{\r\n      reject(e)\r\n    })\r\n  })\r\n}\r\n\r\nfunction saveTask() {\r\n  return new Promise((resolve,reject)=>{\r\n    fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=tasksave').then(res=>res.text()).then(r=>{\r\n      resolve(r)\r\n    }).catch(e=>{\r\n      reject(e)\r\n    })\r\n  })\r\n}\r\n\r\nfunction jsRun(fn) {\r\n  if (!fn.startsWith('http') && !/\\.js$/.test(fn)) fn += '.js'\r\n  return new Promise((resolve,reject)=>{\r\n    fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=runjs&fn=' + fn).then(res=>res.text()).then(r=>{\r\n      resolve(r)\r\n    }).catch(e=>{\r\n      reject(e)\r\n    })\r\n  })\r\n}\r\n\r\nfunction getJsLists() {\r\n  return new Promise((resolve,reject)=>{\r\n    fetch(CONFIG_EV2P.url + 'jsmanage?token=' + CONFIG_EV2P.wbrtoken).then(res=>res.json()).then(r=>{\r\n      resolve(r.jslists.join('    ') + '\\ntotal: ' + r.jslists.length)\r\n    }).catch(e=>{\r\n      reject(e)\r\n    })\r\n  })\r\n}\r\n\r\nfunction deleteJS(name) {\r\n  return new Promise((resolve,reject)=>{\r\n    fetch(CONFIG_EV2P.url + 'jsfile?token=' + CONFIG_EV2P.wbrtoken, {\r\n      method: 'DELETE',\r\n      headers: {\r\n        'Content-Type': 'application/json'\r\n      },\r\n      body: JSON.stringify({\r\n        jsfn: name\r\n      })\r\n    }).then(res=>res.text()).then(r=>{\r\n      resolve(r)\r\n    }).catch(e=>{\r\n      reject(e)\r\n    })\r\n  })\r\n}\r\n\r\nasync function handlePostRequest(request) {\r\n  let bodyString = await readRequestBody(request)\r\n\r\n  try {\r\n    let body = JSON.parse(bodyString);\r\n\r\n    if (body.message) {\r\n      let payload = {\r\n        \"method\": \"sendMessage\",\r\n        \"chat_id\": body.message.chat.id,\r\n        \"parse_mode\": \"html\",\r\n        \"disable_web_page_preview\": true,\r\n      };\r\n      if (body.message.text) {\r\n        let bodytext = body.message.text\r\n        if (CONFIG_EV2P.userid && body.message.chat.id !== CONFIG_EV2P.userid ) {\r\n          payload.text = \"check the project: https://github.com/elecV2/elecV2P\"\r\n        } else if (/^\\/?status/.test(bodytext)) {\r\n          payload.text = await getStatus()\r\n        } else if (/^\\/?(del|delete) /.test(bodytext)) {\r\n          let cont = bodytext.split(' ').pop()\r\n          if (!(cont === 'all' || /\\.log$/.test(cont))) cont = cont + '.js.log'\r\n          payload.text = await delLogs(cont)\r\n        } else if (/^\\/?taskinfo /.test(bodytext)) {\r\n          let cont = bodytext.split(' ').pop()\r\n          payload.text = await getTaskinfo(cont)\r\n        } else if (/\\.log$/.test(bodytext) || /^\\/?log /.test(bodytext)) {\r\n          let cont = bodytext.split(' ').pop()\r\n          if (!/\\.log$/.test(cont)) cont = cont + '.js.log'\r\n          payload.text = await getLogs(cont)\r\n        } else if (/^\\/?taskstart /.test(bodytext)) {\r\n          let cont = bodytext.split(' ').pop()\r\n          payload.text = await opTask(cont, 'start')\r\n        } else if (/^\\/?taskstop /.test(bodytext)) {\r\n          let cont = bodytext.split(' ').pop()\r\n          payload.text = await opTask(cont, 'stop')\r\n        } else if (/^\\/?taskdel /.test(bodytext)) {\r\n          let cont = bodytext.split(' ').pop()\r\n          payload.text = await opTask(cont, 'del')\r\n        } else if (/^\\/?tasksave/.test(bodytext)) {\r\n          payload.text = await saveTask()\r\n        } else if (/^\\/?listjs/.test(bodytext)) {\r\n          payload.text = await getJsLists()\r\n        } else if (/^\\/?deljs /.test(bodytext)) {\r\n          let cont = bodytext.split(' ').pop()\r\n          payload.text = await deleteJS(cont)\r\n        } else if (/^\\/?runjs /.test(bodytext)) {\r\n          let cont = bodytext.split(' ').pop()\r\n          payload.text = await jsRun(cont)\r\n        } else if (/^\\/?all/.test(bodytext)) {\r\n          bodytext = 'all'\r\n          let res = await getLogs(bodytext)\r\n          let map = JSON.parse(res)\r\n          let keyb = {\r\n                keyboard:[\r\n                  [\r\n                    { text: 'all - ' + map.length },\r\n                    { text: 'status' }\r\n                  ]\r\n                ],\r\n                resize_keyboard: false,\r\n                one_time_keyboard: true,\r\n                selective: true\r\n              }\r\n\r\n          map.forEach((s, ind)=> {\r\n            let row = parseInt(ind/2) + 1\r\n            keyb.keyboard[row]\r\n            ? keyb.keyboard[row].push({\r\n              text: s.replace(/\\.js\\.log$/g, '')\r\n            }) \r\n            : keyb.keyboard[row] = [{\r\n              text: s.replace(/\\.js\\.log$/g, '')\r\n            }]\r\n          })\r\n          payload.text = \"点击查看日志\"\r\n          payload.reply_markup = keyb\r\n        } else {\r\n          payload.text = 'TGbot 部署成功，可以使用相关指令和 elecV2P 服务器进行交互了\\nPowered By: https://github.com/elecV2/elecV2P\\n\\n频道: @elecV2 | 群组: @elecV2G'\r\n        }\r\n\r\n        const myInit = {\r\n          method: 'POST',\r\n          headers: {\r\n            'Content-Type': 'application/json;charset=UTF-8'\r\n          },\r\n          body: JSON.stringify(payload)\r\n        };\r\n\r\n        let myRequest = new Request(`https://api.telegram.org/bot${CONFIG_EV2P.token}/`, myInit)\r\n\r\n        fetch(myRequest).then(function(x) {\r\n          console.log(x);\r\n        });\r\n        return new Response(\"OK\")\r\n      } else {\r\n        return new Response(\"OK\")\r\n      }\r\n    } else {\r\n      return new Response(JSON.stringify(body), {\r\n        headers: { 'content-type': 'application/json' },\r\n      })\r\n    }\r\n  } catch(e) {\r\n    return new Response(e)\r\n  }\r\n}\r\n\r\nasync function handleRequest(request) {\r\n  let retBody = `The request was a GET `\r\n  return new Response(retBody)\r\n}\r\n\r\naddEventListener('fetch', event => {\r\n  const { request } = event\r\n  if (request.method === 'POST') {\r\n    return event.respondWith(handlePostRequest(request))\r\n  } else if (request.method === 'GET') {\r\n    return event.respondWith(handleRequest(request))\r\n  }\r\n})\r\n\r\n/**\r\n * readRequestBody reads in the incoming request body\r\n * Use await readRequestBody(..) in an async function to get the string\r\n * @param {Request} request the incoming request to read from\r\n */\r\nasync function readRequestBody(request) {\r\n  const { headers } = request\r\n  const contentType = headers.get('content-type')\r\n  if (contentType.includes('application/json')) {\r\n    const body = await request.json()\r\n    return JSON.stringify(body)\r\n  } else if (contentType.includes('application/text')) {\r\n    const body = await request.text()\r\n    return body\r\n  } else if (contentType.includes('text/html')) {\r\n    const body = await request.text()\r\n    return body\r\n  } else if (contentType.includes('form')) {\r\n    const formData = await request.formData()\r\n    let body = {}\r\n    for (let entry of formData.entries()) {\r\n      body[entry[0]] = entry[1]\r\n    }\r\n    return JSON.stringify(body)\r\n  } else {\r\n    let myBlob = await request.blob()\r\n    var objectURL = URL.createObjectURL(myBlob)\r\n    return objectURL\r\n  }\r\n}"
  },
  {
    "path": "examples/TGbotonCFworker2.0.js",
    "content": "/**\n * 功能: 部署在 cloudflare worker 的 TGbot 后台代码，用于通过 telegram 查看/控制 elecV2P\n * 地址: https://github.com/elecV2/elecV2P-dei/blob/master/examples/TGbotonCFworker2.0.js\n * 更新: 2022-02-19\n * 说明: 功能实现主要基于 elecV2P 的 webhook（https://github.com/elecV2/elecV2P-dei/tree/master/docs/09-webhook.md）\n * \n * 使用方式: \n * 1. 准备工作\n *    - elecV2P 服务器配置域名访问（测试: http://你的 elecV2P 服务器地址/webhook?token=你的webhook token&type=status ）\n *    - 注册并登录 https://dash.cloudflare.com/ ，创建一个 workers 和 KV Namespace(建议命名: elecV2P)，并进行绑定\n *    - 在 https://t.me/botfather 申请一个 TG BOT，记下 api token\n *\n * 2. 部署代码\n *    - 根据下面代码中 CONFIG_EV2P 的注释，填写好相关内容\n *    - 然后把修改后的整个 JS 内容粘贴到 cloudflare worker 代码框，保存并部署。得到一个类似 https://xx.xxxxx.workers.dev 的网址\n *    - 接着在浏览器中打开链接: https://api.telegram.org/bot(你的 tgbot token)/setWebhook?url=https://xx.xxxxx.workers.dev （连接 TGbot 和 CFworkers）\n *    - 最后，打开 TGbot 对话框，输入下面的相关指令（比如 status），测试 TGbot 是否部署成功\n *\n * 2.0 更新: 添加上下文执行环境\n * - /runjs   进入脚本执行环境，接下来直接输入文件名或远程链接则可直接运行\n * - /task    进入任务操作环境，获取相关任务的 taskid 可暂停/开始/添加定时任务\n * - /shell   进入 shell 执行环境，默认 timeout 为 3000ms（elecV2P v3.2.4 版本后生效）\n * - /log     进入 日志查看模式\n * - /store   进入 store/cookie 管理模式。默认处于关闭状态，可在 CONFIG_EV2P mode 设置开启\n * - /context 获取当前执行环境，如果没有，则为普通模式\n * 其它模式完善中...\n * \n * 特殊指令 sudo clear ; 用于清空当前 context 值（以防出现服务器长时间无返回而卡死的问题）\n *\n * 下面 /command 命令的优先级高于当前执行环境\n *\n * 实现功能及相关指令: \n * 查看 elecV2P 运行状态\n * status === /status  ;任何包含 status 关键字的指令\n *\n * 查看服务器相关信息（elecV2P v3.2.6 版本后适用）\n * /info\n * /info debug\n * \n * 删除 log 文件\n * /deletelog file === /deletelog file.js.log === /dellog file\n * /dellog all  ;删除使用 log 文件\n *\n * 查看 log 文件\n * /log file\n *\n * 定时任务相关\n * /taskinfo all        ;获取所有任务信息\n * /taskinfo taskid     ;获取单个任务信息\n * /taskstart taskid    ;开始任务\n * /taskstop taskid     ;停止任务\n * /taskdel taskid      ;删除任务\n * /tasksave            ;保存当前任务列表\n * \n * 脚本相关\n * /runjs file.js       ;运行脚本\n * /runjs https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/webhook.js\n * /runjs https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/feed.js anotify.js  ;运行远程脚本同时重命名保存为 anotify.js\n * /deljs file.js       ;删除脚本\n *\n * shell 指令相关\n * /exec ls  ===  /shell ls  ===  exec ls\n * exec pm2 ls\n * \n * bot commands 2.0\nrunjs - 运行 JS\ntask - 任务管理模式\nstatus - 内存使用状态\nshell - shell 命令执行模式\nstore - store/cookie 管理\ntasksave - 保存任务列表\nlog - 查看日志文件\ncontext - 查看当前执行环境\nend - 退出当前执行环境\ninfo - 查看服务器信息\ncommand - 列出所有指令\n\n * 更新方式: \n * - 如果在 CONFIG_EV2P 中设置了 store，直接复制当前整个文件到 cf worker 即可\n * - 如果没有设置 store，则复制除了开头的 CONFIG_EV2P 外其他所有内容到 cf worker\n *\n * 适用版本: elecV2P v3.3.6 (低版本下部分指令可能无法正常处理)\n**/\n\nconst kvname = elecV2P   // 保存上下文内容的 kv namespace。在 cf 上创建并绑定后自行更改\n\nlet CONFIG_EV2P = {\n  name: 'elecV2P',              // bot 名称。可省略\n  store: 'elecV2PBot_CONFIG',   // 是否将当前 CONFIG 设置保存到 kv 库（运行时会自动读取并覆盖下面的设置，即下面的设置更改无效（方便更新)。建议调试时留空，调试完成后再设置回 'elecV2PBot_CONFIG' ）\n  storeforce: false,     // true: 使用当前设置强制覆盖 cf kv 库中的数据，false: kv 库中有配置相关数据则读取，没有则使用当前设置运行并保存\n  url: \"http://你的 elecV2P 服务器地址/\",    // elecV2P 服务器地址(必须是域名，cf worker 不支持 IP 直接访问)\n  wbrtoken: 'xxxxxx-xxxxxxxxxxxx-xxxx',      // elecV2P 服务器 webhook token(在 webUI->SETTING 界面查看)\n  token: \"xxxxxxxx:xxxxxxxxxxxxxxxxxxx\",     // telegram bot api token\n  userid: [],            // 只对该列表中的 userid 发出的指令进行回应。默认: 回应所有用户的指令（高风险！）\n  slice: -1200,          // 截取部分返回结果的最后 1200 个字符，以防太长无法传输（可自行修改）\n  shell: {\n    timeout: 1000*6,     // shell exec 超时时间，单位: ms\n    contexttimeout: 1000*60*5,               // shell 模式自动退出时间，单位: ms\n  },\n  timeout: 5000,         // runjs 请求超时时间，以防脚本运行时间过长，无回应导致反复请求，bot 被卡死\n  mycommand: {           // 自定义快捷命令，比如 restart: 'exec pm2 restart elecV2P'。前面的属性值请不要包含空格。\n    rtest: '/runjs test.js',    // 表示当输入命令 /rtest 或 rtest 时会自动替换成命令 '/runjs test.js' 运行 JS 脚本 test.js\n    execls: 'exec ls -al',      // 同上，表示自动将命令 /execls 替换成 exec ls -al。 其他命令可参考自行添加\n    update: {                   // 当为 object 类型时，note 表示备注显示信息， command 表示实际执行命令\n      note: '软更新升级',\n      command: 'runjs https://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/softupdate.js'\n    },\n    renv: {\n      note: '带参数运行',       // 在 command 中用 $1 作为占位符\n      command: 'runjs exam-js-env.js -env name=$1',  // 使用示例: renv 参数elecV2PTGbot。目前仅支持单个参数\n    },\n  },\n  mode: {\n    storemanage: false,         // 是否开启 store/cookie 管理模式。false: 不开启（默认），true: 开启\n  }\n}\n\n/************ 后面部分为主运行代码，若没有特殊情况，无需改动 ****************/\n\nconst store = {\n  put: async (key, value)=>{\n    return await kvname.put(key, value)\n  },\n  get: async (key, type)=>{\n    return await kvname.get(key, type)\n  },\n  delete: async (key)=>{\n    await kvname.delete(key)\n  },\n  list: async ()=>{\n    const val = await kvname.list()\n    return val.keys\n  }\n}\n\nconst context = {\n  get: async (uid) => {\n    return await store.get(uid, 'json')\n  },\n  put: async (uid, uenv, command) => {\n    let ctx = await context.get(uid)\n    if (ctx === null || typeof ctx !== 'object') {\n      ctx = {\n        command: []\n      }\n    }\n    if (uenv) {\n      ctx.context = uenv\n    }\n    if (command) {\n      ctx.command ? ctx.command.push(command) : ctx.command = [command]\n    }\n    ctx.active = Date.now()\n    await store.put(uid, JSON.stringify(ctx))\n  },\n  end: async (uid) => {\n    await store.put(uid, JSON.stringify({}))\n  }\n}\n\nfunction surlName(url) {\n  if (!url) {\n    return ''\n  }\n  let name = ''\n  let sdurl = url.split(/\\/|\\?|#/)\n  while (name === '' && sdurl.length) {\n    name = sdurl.pop()\n  }\n  return name\n}\n\nfunction timeoutPromise({ timeout = CONFIG_EV2P.timeout || 5000, fn }) {\n  if (!/\\.js$/.test(fn)) {\n    fn += '.js'\n  }\n  return new Promise(resolve => setTimeout(resolve, timeout, '请求超时 ' + timeout + ' ms，相关请求应该已发送至 elecV2P，这里提前返回结果，以免发送重复请求' + `${fn ? ('\\n\\n运行日志: ' + CONFIG_EV2P.url + 'logs/' + surlName(fn) + '.log') : '' }`))\n}\n\nfunction getLogs(s){\n  if (s !== 'all' && !/\\.log$/.test(s)) {\n    s = s + '.js.log'\n  }\n  return new Promise((resolve,reject)=>{\n    fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=getlog&fn=' + s).then(res=>res.text()).then(r=>{\n      resolve(s === 'all' ? r : r.slice(CONFIG_EV2P.slice))\n    }).catch(e=>{\n      reject(e)\n    })\n  })\n}\n\nfunction delLogs(logn) {\n  return new Promise((resolve,reject)=>{\n    fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=deletelog&fn=' + logn).then(res=>res.text()).then(r=>{\n      resolve(r)\n    }).catch(e=>{\n      reject(e)\n    })\n  })\n}\n\nfunction getStatus() {\n  return new Promise((resolve,reject)=>{\n    fetch(CONFIG_EV2P.url + 'webhook?type=status&token=' + CONFIG_EV2P.wbrtoken).then(res=>res.text()).then(r=>{\n      resolve(r)\n    }).catch(e=>{\n      reject(e)\n    })\n  })\n}\n\nfunction getInfo(debug) {\n  return new Promise((resolve,reject)=>{\n    fetch(CONFIG_EV2P.url + 'webhook?type=info&token=' + CONFIG_EV2P.wbrtoken + (debug ? '&debug=true' : '')).then(res=>res.text()).then(r=>{\n      resolve(r)\n    }).catch(e=>{\n      reject(e)\n    })\n  })\n}\n\nfunction getTaskinfo(tid) {\n  tid = tid.replace(/^\\//, '')\n  return new Promise((resolve,reject)=>{\n    fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=taskinfo&tid=' + tid).then(res=>res.text()).then(r=>{\n      resolve(r)\n    }).catch(e=>{\n      reject(e)\n    })\n  })\n}\n\nfunction opTask(tid, op) {\n  if (!/start|stop|del|delete/.test(op)) {\n    return 'unknow operation' + op\n  }\n  tid = tid.replace(/^\\//, '')\n  if (/^\\/?stop/.test(tid)) {\n    op = 'stop'\n    tid = tid.replace(/^\\/?stop/, '')\n  }\n  return new Promise((resolve,reject)=>{\n    fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=task' + op + '&tid=' + tid).then(res=>res.text()).then(r=>{\n      resolve(r)\n    }).catch(e=>{\n      reject(e)\n    })\n  })\n}\n\nfunction saveTask() {\n  return new Promise((resolve,reject)=>{\n    fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=tasksave').then(res=>res.text()).then(r=>{\n      resolve(r)\n    }).catch(e=>{\n      reject(e)\n    })\n  })\n}\n\nfunction taskNew(taskinfo) {\n  // 新建任务\n  if (!taskinfo) {\n    return '没有任何任务信息'\n  }\n  let finfo = taskinfo.split(/\\r|\\n/)\n  if (finfo.length < 2) {\n    return '任务信息输入有误 '\n  }\n  taskinfo = {\n    name: finfo[2] || '新的任务' + Math.ceil(Math.random()*100),\n    type: finfo[0].split(' ').length > 4 ? 'cron' : 'schedule',\n    time: finfo[0],\n    job: {\n      type: finfo[3] || 'runjs',\n      target: finfo[1],\n    },\n    running: finfo[4] !== 'false'\n  }\n  return new Promise((resolve,reject)=>{\n    fetch(CONFIG_EV2P.url + 'webhook', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json'\n      },\n      body: JSON.stringify({\n        token: CONFIG_EV2P.wbrtoken,\n        type: 'taskadd',\n        task: taskinfo\n      })\n    }).then(res=>res.text()).then(r=>{\n      resolve(r)\n    }).catch(e=>{\n      reject(e)\n    })\n  })\n}\n\nfunction jsRun(fn) {\n  // 支持参数运行，参考说明文档 06-task.md 运行 JS 相关部分（elecV2P 需大于 v3.6.0\n  return Promise.race([new Promise((resolve,reject)=>{\n    fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=runjs&fn=' + encodeURI(fn)).then(res=>res.text()).then(r=>{\n      resolve(r)\n    }).catch(e=>{\n      reject(e)\n    })\n  }), timeoutPromise({ fn })])\n}\n\nfunction getJsLists() {\n  return new Promise((resolve,reject)=>{\n    fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=jslist').then(res=>res.json()).then(r=>{\n      resolve(r.rescode === 0 ? r.resdata : r)\n    }).catch(e=>{\n      reject(e)\n    })\n  })\n}\n\nfunction deleteJS(name) {\n  return new Promise((resolve,reject)=>{\n    fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=deletejs&fn=' + name).then(res=>res.text()).then(r=>{\n      resolve(r)\n    }).catch(e=>{\n      reject(e)\n    })\n  })\n}\n\nfunction shellRun(command) {\n  if (command) {\n    command = encodeURI(command)\n  } else {\n    return '请输入 command 指令，比如: ls'\n  }\n  return new Promise((resolve,reject)=>{\n    fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + `&type=shell&timeout=${CONFIG_EV2P.shell && CONFIG_EV2P.shell.timeout || 3000}&command=` + command).then(res=>res.text()).then(r=>{\n      resolve(r.slice(CONFIG_EV2P.slice))\n    }).catch(e=>{\n      reject(e)\n    })\n  })\n}\n\nfunction storeManage(keyvt) {\n  if (!keyvt) {\n    return '请输入要获取的 cookie/store 相关的 key 值'\n  }\n\n  let keys = keyvt.split(' ')\n  if (keys.length === 1) {\n    return new Promise((resolve,reject)=>{\n      fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + `&type=store&key=${keyvt}`).then(res=>res.text()).then(r=>{\n        if (r) {\n          resolve(r.slice(CONFIG_EV2P.slice))\n        } else {\n          resolve(keyvt + ' 暂不存在')\n        }\n      }).catch(e=>{\n        reject(e)\n      })\n    })\n  } else {\n    let body = {\n      token: CONFIG_EV2P.wbrtoken,\n      type: 'store'\n    }\n    if (keys[0] === 'delete') {\n      body.op = 'delete'\n      body.key = keys[1]\n    } else {\n      body.op = 'put'\n      body.key = keys[0]\n      body.value = decodeURI(keys[1])\n      body.options = {\n        type: keys[2]\n      }\n    }\n    return new Promise((resolve,reject)=>{\n      fetch(CONFIG_EV2P.url + 'webhook', {\n        method: 'PUT',\n        headers: {\n          'Content-Type': 'application/json'\n        },\n        body: JSON.stringify(body)\n      }).then(res=>res.text()).then(r=>{\n        resolve(r)\n      }).catch(e=>{\n        reject(e)\n      })\n    })\n  }\n}\n\nfunction storeList() {\n  return new Promise((resolve,reject)=>{\n    fetch(CONFIG_EV2P.url + 'webhook?token=' + CONFIG_EV2P.wbrtoken + '&type=store&op=all').then(res=>res.json()).then(r=>{\n      resolve(r.rescode === 0 ? r.resdata : r)\n    }).catch(e=>{\n      reject(e)\n    })\n  })\n}\n\nfunction getFile(file_id) {\n  return new Promise((resolve,reject)=>{\n    fetch(`https://api.telegram.org/bot${CONFIG_EV2P.token}/getFile?file_id=${file_id}`).then(res=>res.json()).then(r=>{\n      if (r.ok) {\n        resolve(`https://api.telegram.org/file/bot${CONFIG_EV2P.token}/${r.result.file_path}`)\n      } else {\n        resolve(r.description)\n      }\n    }).catch(e=>{\n      reject(e)\n    })\n  })\n}\n\nasync function handlePostRequest(request) {\n  if (CONFIG_EV2P.store) {\n    let config = await store.get(CONFIG_EV2P.store, 'json')\n    if (!CONFIG_EV2P.storeforce && config) {\n      Object.assign(CONFIG_EV2P, config)\n    } else {\n      await store.put(CONFIG_EV2P.store, JSON.stringify(CONFIG_EV2P))\n    }\n  }\n  if (!CONFIG_EV2P.url.endsWith('/')) {\n    CONFIG_EV2P.url = CONFIG_EV2P.url + '/'\n  }\n  CONFIG_EV2P.timeout = CONFIG_EV2P.timeout || 5000\n\n  let bodyString = await readRequestBody(request)\n  let payload = {\n    \"method\": \"sendMessage\",\n    \"chat_id\": CONFIG_EV2P.userid[0],\n    \"parse_mode\": \"html\",\n    \"disable_web_page_preview\": true,\n  }\n\n  try {\n    let body = JSON.parse(bodyString)\n    if (!body.message) {\n      payload.text = 'elecV2P bot get unknow message:\\n' + bodyString\n      await tgPush(payload)\n      return new Response(\"OK\")\n    }\n    payload[\"chat_id\"] = body.message.chat.id\n    if (body.message.document) {\n      let bodydoc = body.message.document\n      payload.text = `文件名称: ${bodydoc.file_name}\\n文件类型: ${bodydoc.mime_type}\\n文件 id: ${bodydoc.file_id}\\n`\n      let fpath = await getFile(bodydoc.file_id)\n      payload.text += `文件地址: ${fpath}\\n\\n（进一步功能待完成）`\n      await tgPush(payload)\n      return new Response(\"OK\")\n    }\n    if (body.message.text) {\n      let bodytext = body.message.text.trim()\n      let uid = 'u' + payload['chat_id']\n\n      if (CONFIG_EV2P.mycommand && Object.keys(CONFIG_EV2P.mycommand).length) {\n        let tcom = bodytext.replace(/^\\//, '')\n        if (CONFIG_EV2P.mycommand[tcom]) {\n          bodytext = CONFIG_EV2P.mycommand[tcom].command || CONFIG_EV2P.mycommand[tcom]\n        } else if (tcom.indexOf(' ') !== -1) {\n          let tind = tcom.indexOf(' ')\n          let fcom = tcom.slice(0, tind)\n          if (CONFIG_EV2P.mycommand[fcom]) {\n            let otext = CONFIG_EV2P.mycommand[fcom].command || CONFIG_EV2P.mycommand[fcom]\n            if (/\\$1/.test(otext)) {\n              bodytext = otext.replace(/\\$1/, tcom.slice(tind+1))\n            }\n          }\n        }\n      }\n      if (bodytext === 'sudo clear') {\n        await store.delete(uid)\n        payload.text = 'current context is cleared.'\n        await tgPush(payload)\n        return new Response(\"OK\")\n      } else if (bodytext === '/command') {\n        payload.text = `/runjs - 运行 JS\n/task - 任务管理模式\n/status - 内存使用状态\n/shell - shell 指令执行模式\n/store - store/cookie 管理\n/tasksave - 保存任务列表\n/taskdel + tid - 删除任务\n/deljs + JS 文件名 - 删除 JS\n/log - 获取日志\n/dellog + 日志名 - 删除日志\n/context - 查看当前执行环境\n/end - 退出当前执行环境\n/info - 查看服务器信息\n/command - 列出所有指令`\n\n        if (CONFIG_EV2P.mycommand && Object.keys(CONFIG_EV2P.mycommand).length) {\n          payload.text += '\\n\\n自定义快捷命令'\n          for (let x in CONFIG_EV2P.mycommand) {\n            payload.text += '\\n' + (x.startsWith('/') ? '' : '/') + x + ' - ' + (CONFIG_EV2P.mycommand[x].note || CONFIG_EV2P.mycommand[x])\n          }\n        }\n        await tgPush(payload)\n        return new Response(\"OK\")\n      }\n      let userenv = await context.get(uid)\n      \n      if (CONFIG_EV2P.userid && CONFIG_EV2P.userid.length && CONFIG_EV2P.userid.indexOf(body.message.chat.id) === -1) {\n        payload.text = \"这是 \" + CONFIG_EV2P.name + \" 私人 bot，不接受其他人的指令。\\n如果有兴趣可以自己搭建一个: https://github.com/elecV2/elecV2P-dei\\n\\n频道: @elecV2 | 交流群: @elecV2G\"\n        await tgPush({\n          ...payload,\n          \"chat_id\": CONFIG_EV2P.userid[0],\n          \"text\": `用户: ${body.message.chat.username}，ID: ${body.message.chat.id} 正在连接 elecV2P bot，发出指令为: ${bodytext}`\n        })\n      } else if (/^\\/?end/.test(bodytext)) {\n        await context.end(uid)\n        payload.text = `退出上文执行环境${(userenv && userenv.context) || ''}，回到普通模式`\n      } else if (/^\\/?context$/.test(bodytext)) {\n        if (userenv && userenv.context) {\n          payload.text = '当前执行环境为: ' + userenv.context + '\\n输入 /end 回到普通模式'\n        } else {\n          payload.text = '当前执行环境为: 普通模式'\n        }\n      } else if (/^\\/?status/.test(bodytext)) {\n        payload.text = await getStatus()\n      } else if (/^\\/?info/.test(bodytext)) {\n        let cont = bodytext.trim().split(' ')\n        if (cont.length === 1) {\n          payload.text = await getInfo()\n        } else if (cont.pop() === 'debug') {\n          payload.text = await getInfo('debug')\n        } else {\n          payload.text = 'unknow info command'\n        }\n      } else if (/^\\/?(dellog|deletelog) /.test(bodytext)) {\n        let cont = bodytext.replace(/^\\/?(dellog|deletelog) /, '')\n        if (!(cont === 'all' || /\\.log$/.test(cont))) cont = cont + '.js.log'\n        payload.text = await delLogs(cont)\n      } else if (/^\\/?taskinfo/.test(bodytext)) {\n        let cont = bodytext.replace(/^\\/?taskinfo/, '')\n        payload.text = await getTaskinfo(cont.trim() || 'all')\n      } else if (/^\\/?taskstart /.test(bodytext)) {\n        let cont = bodytext.replace(/^\\/?taskstart /, '')\n        payload.text = await opTask(cont, 'start')\n      } else if (/^\\/?taskstop /.test(bodytext)) {\n        let cont = bodytext.replace(/^\\/?taskstop /, '')\n        payload.text = await opTask(cont, 'stop')\n      } else if (/^\\/?taskdel /.test(bodytext)) {\n        let cont = bodytext.replace(/^\\/?taskdel /, '')\n        payload.text = await opTask(cont, 'del')\n      } else if (/^\\/?tasksave/.test(bodytext)) {\n        payload.text = await saveTask()\n      } else if (/^\\/?deljs /.test(bodytext)) {\n        let cont = bodytext.replace(/^\\/?deljs /, '')\n        payload.text = await deleteJS(cont)\n      } else if (/^\\/?task/.test(bodytext)) {\n        let cont = bodytext.trim().split(' ')\n        if (cont.length === 1) {\n          try {\n            await context.put('u' + payload['chat_id'], 'task')\n            let tasklists = await getTaskinfo('all')\n            let tlist = JSON.parse(tasklists)\n            let tlstr = []\n            for (let tid in tlist.info) {\n              tlstr.push(`${tlist.info[tid].running ? '🐢' : '🐰'} ${tlist.info[tid].name} /${tid}  |  /stop${tid}`)\n              if (tlstr.length > 80) {\n                payload.text = tlstr.join('\\n')\n                await tgPush(payload)\n                tlstr = []\n              }\n            }\n\n            payload.text = `\\n${tlstr.join('\\n')}\\n当前 elecV2P 定时任务共 ${tlist.total} 个，运行中(🐢)的任务 ${tlist.running} 个\\n点击任务名后面的 /+tid 开始任务，/+stoptid 停止任务\\n也可以手动输入对应的 tid 开始任务, stop tid 停止任务\\ntaskinfo tid 查看任务信息`\n            await tgPush(payload)\n\n            payload.text = `按照下面格式多行输入可直接添加新的任务（每行表示一个任务参数）\\n\n任务时间(cron 定时，比如: 8 0,8 * * * ，倒计时，比如: 1 10 6)\n任务目标(test.js，node -v, LOlxkcdI(某个任务的 tid)，远程 JS 链接等)\n任务名称(可省略，默认为 新的任务+随机参数)\n任务类型(可省略，默认为 运行 JS，shell: 运行 shell 指令，taskstart：开始其他任务，taskstop：停止其他任务)\n是否执行(可省略，默认为 true，当且仅当该值为 false 时，表示只添加任务信息而不运行)\n\n示例一：添加一个 cron 定时任务\n\n30 20 * * *\nhttps://raw.githubusercontent.com/elecV2/elecV2P/master/script/JSFile/deletelog.js\n删除日志\n\n示例二：添加一个倒计时任务，运行 test.js，每次倒计时 1 秒，执行 3 次\n\n1 3\ntest.js`\n          } catch(e) {\n            payload.text = e.message\n          }\n        } else {\n          payload.text = 'unknow task operation'\n        }\n      } else if (/^\\/?runjs/.test(bodytext)) {\n        let cont = bodytext.trim().split(/ +/)\n        if (cont.length === 1) {\n          try {\n            await context.put('u' + payload['chat_id'], 'runjs')\n            let jslists = await getJsLists()\n            let keyb = {\n              keyboard: [],\n              resize_keyboard: false,\n              one_time_keyboard: true,\n              selective: true\n            }\n            let over = ''\n            if (jslists.length >= 200) {\n              over = '\\n\\n文件数超过 200，以防 reply_keyboard 过长 TG 无返回，剩余 JS 以文字形式返回\\n\\n'\n            }\n            for (let ind in jslists) {\n              let s = jslists[ind]\n              if (ind >= 200) {\n                over += s + '  '\n                continue\n              }\n\n              let row = parseInt(ind/2)\n              keyb.keyboard[row]\n              ? keyb.keyboard[row].push({\n                text: s.replace(/\\.js$/, '')\n              })\n              : keyb.keyboard[row] = [{\n                text: s.replace(/\\.js$/, '')\n              }]\n            }\n            payload.text = '进入 RUNJS 模式，当前 elecV2P 上 JS 文件数: ' + jslists.length + '\\n点击交互键盘可直接运行 JS，也可以输入文件名或者远程链接运行其他脚本\\n后面可附带 -env/-rename 等参数(v3.6.0)，比如\\nhttps://远程JSname.js -rename=t.js' + over.trimRight()\n            payload.reply_markup = keyb\n          } catch(e) {\n            payload.text = e.message\n          }\n        } else {\n          payload.text = await jsRun(bodytext.replace(/^\\/?runjs /, ''))\n        }\n      } else if (/^\\/?(shell|exec)/.test(bodytext)) {\n        let cont = bodytext.trim().split(' ')\n        if (cont.length === 1) {\n          try {\n            await context.put('u' + payload['chat_id'], 'shell')\n            let keyb = {\n              keyboard: [\n                [{text: 'ls'}, {text: 'node -v'}],\n                [{text: 'apk add python3 ffmpeg'}],\n                [{text: 'python3 -V'}, {text: 'pm2 ls'}]\n              ],\n              resize_keyboard: false,\n              one_time_keyboard: true,\n              selective: true\n            }\n            payload.text = '进入 SHELL 模式，可执行简单 shell 指令，比如: ls, node -v 等'\n            payload.reply_markup = keyb\n          } catch(e) {\n            payload.text = e.message\n          }\n        } else {\n          payload.text = await shellRun(bodytext.replace(/^\\/?(shell|exec) /, ''))\n        }\n      } else if (/^\\/?store/.test(bodytext)) {\n        if (CONFIG_EV2P.mode && CONFIG_EV2P.mode.storemanage) {\n          let cont = bodytext.trim().split(' ')\n          if (cont.length === 1) {\n            try {\n              await context.put('u' + payload['chat_id'], 'store')\n              let storelists = await storeList()\n              let keyb = {\n                keyboard: [],\n                resize_keyboard: false,\n                one_time_keyboard: true,\n                selective: true\n              }\n              let over = ''\n              if (storelists.length >= 200) {\n                over = '\\n\\nCookie 数超过 200，以防 reply_keyboard 过长 TG 无返回，剩余 Cookie KEY 以文字形式返回\\n\\n'\n              }\n              for (let ind in storelists) {\n                let s = storelists[ind]\n                if (ind >= 200) {\n                  over += s + '  '\n                  continue\n                }\n\n                let row = parseInt(ind/2)\n                keyb.keyboard[row]\n                ? keyb.keyboard[row].push({\n                  text: s\n                })\n                : keyb.keyboard[row] = [{\n                  text: s\n                }]\n              }\n              payload.reply_markup = keyb\n              payload.text = '进入 cookie/store 管理模式，当前 elecV2P 上 Cookie 数: ' + storelists.length + '\\n\\n点击或者直接输入关键字(key)查看 store 内容，比如 cookieKEY\\n\\n输入 delete key 删除某个 Cookie。比如: delete cookieKEY\\n\\n输入 key value type(可省略) 修改 store 内容(以空格进行分隔)。如果 value 中包含空格等其他特殊字符，请先使用 encodeURI 函数进行转换。比如:\\n\\nCookieJD pt_pin=xxx;%20pt_key=app_xxxxxxx;\\n\\ntype 可省略，也可设定为:\\nstring 表示将 value 保存为普通字符(默认)\\nobject 表示将 value 保存为 json 格式\\na 表示在原来的值上新增。（更多说明可参考 https://github.com/elecV2/elecV2P-dei/tree/master/docs/04-JS.md $store 部分）' + over\n            } catch(e) {\n              payload.text = e.message\n            }\n          } else {\n            payload.text = await storeManage(bodytext.replace(/^\\/?store /, ''))\n          }\n        } else {\n          payload.text = 'store/cookie 管理模式处于关闭状态'\n        }\n      } else if (/^\\/?log/.test(bodytext)) {\n        let cont = bodytext.trim().split(' ')\n        if (cont.length === 1) {\n          try {\n            await context.put('u' + payload['chat_id'], 'log')\n            let res = await getLogs('all')\n            let map = JSON.parse(res)\n            if (map.rescode === 0) {\n              map = map.resdata\n            }\n            let keyb = {\n                  inline_keyboard: [ ],\n                }\n\n            map.forEach((s, ind)=> {\n              let row = parseInt(ind/2)\n              keyb.inline_keyboard[row]\n              ? keyb.inline_keyboard[row].push({\n                text: s.replace(/\\.js\\.log$/g, ''),\n                url: CONFIG_EV2P.url + 'logs/' + s\n              }) \n              : keyb.inline_keyboard[row] = [{\n                text: s.replace(/\\.js\\.log$/g, ''),\n                url: CONFIG_EV2P.url + 'logs/' + s\n              }]\n            })\n            payload.text = \"开始日志查看模式，当前 elecV2P 上日志文件数: \" + map.length + \"\\n点击查看日志或者直接输入 log 文件名进行查看\"\n            payload.reply_markup = keyb\n          } catch(e) {\n            payload.text = e.message\n          }\n        } else {\n          payload.text = await getLogs(bodytext.replace(/^\\/?log /, ''))\n        }\n      } else if (userenv && userenv.context) {\n        switch (userenv.context) {\n          case 'log':\n            payload.text = await getLogs(bodytext)\n            break\n          case 'runjs':\n            payload.text = await jsRun(bodytext)\n            break\n          case 'task':\n            if (bodytext.trim().split(/\\r|\\n/).length > 1) {\n              payload.text = await taskNew(bodytext)\n            } else {\n              payload.text = await opTask(bodytext.split(' ').pop(), /^(🐢|\\/?stop)/.test(bodytext) ? 'stop' : 'start')\n            }\n            break\n          case 'shell':\n            if (Date.now() - userenv.active > (CONFIG_EV2P.shell && CONFIG_EV2P.shell.contexttimeout)) {\n              payload.text = '已经超过 ' + CONFIG_EV2P.shell.contexttimeout/1000/60 + ' 分钟没有执行 shell 指令，自动退出 shell 模式\\n使用 /shell 命令重新进入\\n/end 回到普通模式\\n/command 查看所有指令'\n              payload.reply_markup = JSON.stringify({\n                remove_keyboard: true\n              })\n              userenv.context = 'normal'\n            } else {\n              payload.text = await shellRun(bodytext)\n            }\n            break\n          case 'store':\n            if (CONFIG_EV2P.mode && CONFIG_EV2P.mode.storemanage) {\n              payload.text = await storeManage(bodytext)\n            } else {\n              payload.text = 'store/cookie 管理模式处于关闭状态'\n            }\n            break\n          default: {\n            payload.text = '当前执行环境: ' + userenv.context + ' 无法处理指令: ' + bodytext\n          }\n        }\n        await context.put(uid, userenv.context, bodytext)\n      } else {\n        payload.text = 'TGbot 部署成功，可以使用相关指令和 elecV2P 服务器进行交互了\\nPowered By: https://github.com/elecV2/elecV2P\\n\\n频道: @elecV2 | 交流群: @elecV2G'\n        if (CONFIG_EV2P.userid.length === 0) {\n          payload.text += '\\n（❗️危险⚠️）当前 elecV2P bot 并没有设置 userid，所有人可进行交互'\n        }\n        if (bodytext === '/start') {\n          let status = ''\n          try {\n            status = await getStatus()\n            status = '当前 bot 与 elecV2P 连接成功 ' + status\n          } catch(e) {\n            status = (e.message || e) + '\\nelecV2P 服务器没有响应，请检查服务器地址和 webhook token 是否设置正确。'\n          }\n          payload.text += '\\n' + status\n        }\n      }\n\n      await tgPush(payload)\n      return new Response(\"OK\")\n    }\n    return new Response(JSON.stringify(body), {\n      headers: { 'content-type': 'application/json' },\n    })\n  } catch(e) {\n    console.error(e)\n    console.log('payload', payload)\n    payload.text = JSON.stringify({\n      rescode: -1,\n      message: e.message || e,\n      resdata: bodyString\n    }, null, 2)\n    await tgPush(payload)\n    return new Response(\"OK\")\n  }\n}\n\nasync function handleRequest(request) {\n  return new Response(JSON.stringify({\n    rescode: 0,\n    message: `welcome to elecV2P.\\n\\nPowered By: https://github.com/elecV2/elecV2P\\n\\nTG 频道: https://t.me/elecV2 | TG 交流群: @elecV2G`\n  }, null, 2))\n}\n\naddEventListener('fetch', event => {\n  const { request } = event\n  // const { url } = request\n  if (request.method === 'POST') {\n    return event.respondWith(handlePostRequest(request))\n  } else if (request.method === 'GET') {\n    return event.respondWith(handleRequest(request))\n  }\n})\n\n/**\n * readRequestBody reads in the incoming request body\n * Use await readRequestBody(..) in an async function to get the string\n * @param {Request} request the incoming request to read from\n */\nasync function readRequestBody(request) {\n  const { headers } = request\n  const contentType = headers.get('content-type')\n  if (contentType.includes('application/json')) {\n    const body = await request.json()\n    return JSON.stringify(body)\n  } else if (contentType.includes('application/text')) {\n    const body = await request.text()\n    return body\n  } else if (contentType.includes('text/html')) {\n    const body = await request.text()\n    return body\n  } else if (contentType.includes('form')) {\n    const formData = await request.formData()\n    let body = {}\n    for (let entry of formData.entries()) {\n      body[entry[0]] = entry[1]\n    }\n    return JSON.stringify(body)\n  } else {\n    let myBlob = await request.blob()\n    var objectURL = URL.createObjectURL(myBlob)\n    return objectURL\n  }\n}\n\nasync function tgPush(payload) {\n  const myInit = {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json;charset=UTF-8'\n    }\n  };\n  let maxbLength = 1200;\n  if (payload.text && payload.text.length > maxbLength) {\n    let reply_text = payload.text\n    let pieces = Math.ceil(reply_text.length / maxbLength);\n    for (let i=0; i<pieces; i++) {\n      payload.text = reply_text.slice(i*maxbLength, (i+1)*maxbLength);\n      myInit.body = JSON.stringify(payload)\n      let myRequest = new Request(`https://api.telegram.org/bot${CONFIG_EV2P.token}/`, myInit);\n      await fetch(myRequest);\n      if (payload.reply_markup) {\n        delete payload.reply_markup\n      }\n    }\n  } else {\n    myInit.body = JSON.stringify(payload);\n    let myRequest = new Request(`https://api.telegram.org/bot${CONFIG_EV2P.token}/`, myInit);\n    await fetch(myRequest);\n  }\n}"
  },
  {
    "path": "examples/archive/Caddyfile",
    "content": "{\r\nhttp_port   80\r\n}\r\n\r\nhttp://e2p.xxxxxxx.com {\r\n  log stdout\r\n  encode gzip\r\n  reverse_proxy 127.0.0.1:8100 {\r\n    header_up Host {http.reverse_proxy.upstream.hostport}\r\n  }\r\n}\r\n\r\nhttp://dtest.xxxxxxx.com {\r\n  log stdout\r\n  reverse_proxy 127.0.0.1:8101 {\r\n#    header_up Host {http.reverse_proxy.upstream.hostport}\r\n  }\r\n}\r\n\r\nhttp://test.xxxxxxx.com {\r\n  log stdout\r\n  encode gzip\r\n  reverse_proxy * 127.0.0.1:8102 {\r\n    # 请求头Host设置\r\n    header_up Host {http.reverse_proxy.upstream.hostport}\r\n    # 请求头transparent设置\r\n    header_up X-Real-IP {http.request.remote.host}\r\n    header_up X-Forwarded-For {http.request.remote.host}\r\n    header_up X-Forwarded-Port {http.request.port}\r\n    header_up X-Forwarded-Proto {http.request.scheme}\r\n  }\r\n}"
  },
  {
    "path": "examples/docker-compose-clash.yaml",
    "content": "version: '3.7'\r\nservices:\r\n  elecv2p:\r\n    image: elecv2/elecv2p\r\n    container_name: elecv2p\r\n    restart: always\r\n    logging:\r\n      driver: \"json-file\"\r\n      options:\r\n        max-size: \"10m\"\r\n        max-file: \"3\"\r\n    environment:\r\n      - TZ=Asia/Shanghai\r\n    ports:\r\n      - \"8100:80\"\r\n      - \"8101:8001\"\r\n      - \"8102:8002\"\r\n    volumes:\r\n      - \"/elecv2p/JSFile:/usr/local/app/script/JSFile\"\r\n      - \"/elecv2p/Lists:/usr/local/app/script/Lists\"\r\n      - \"/elecv2p/Store:/usr/local/app/script/Store\"\r\n      - \"/elecv2p/Shell:/usr/local/app/script/Shell\"\r\n      - \"/elecv2p/rootCA:/usr/local/app/rootCA\"\r\n      - \"/elecv2p/efss:/usr/local/app/efss\"\r\n\r\n  clash:\r\n    image: elecv2/clash\r\n    container_name: clash\r\n    restart: always\r\n    ports:\r\n      - \"8008:8008\"\r\n      - \"8090:8090\"\r\n    volumes:\r\n      - \"/clash/config.yaml:/clash/config.yaml\""
  },
  {
    "path": "examples/docker-compose.yaml",
    "content": "version: '3.7'\r\nservices:\r\n  elecv2p:\r\n    image: elecv2/elecv2p\r\n    container_name: elecv2p\r\n    restart: always\r\n    logging:\r\n      driver: \"json-file\"\r\n      options:\r\n        max-size: \"10m\"\r\n        max-file: \"3\"\r\n    environment:\r\n      - TZ=Asia/Shanghai\r\n    ports:\r\n      - \"8100:80\"\r\n      - \"8101:8001\"\r\n      - \"8102:8002\"\r\n    volumes:\r\n      - \"/elecv2p/JSFile:/usr/local/app/script/JSFile\"\r\n      - \"/elecv2p/Lists:/usr/local/app/script/Lists\"\r\n      - \"/elecv2p/Store:/usr/local/app/script/Store\"\r\n      - \"/elecv2p/Shell:/usr/local/app/script/Shell\"\r\n      - \"/elecv2p/rootCA:/usr/local/app/rootCA\"\r\n      - \"/elecv2p/efss:/usr/local/app/efss\""
  },
  {
    "path": "examples/ev2p-nginx.conf",
    "content": "server {\r\n  listen 80;\r\n  server_name e2p.xxxxxxx.com;\r\n  location / {\r\n    proxy_pass          http://127.0.0.1:8100;\r\n  }\r\n\r\n  // websocket\r\n  location /elecV2P {\r\n    proxy_pass          http://127.0.0.1:8100;\r\n    proxy_http_version  1.1;\r\n    proxy_set_header    Upgrade    $http_upgrade;\r\n    proxy_set_header    Connection \"upgrade\";\r\n\r\n    proxy_connect_timeout      36000s;\r\n    proxy_send_timeout         36000s;\r\n    proxy_read_timeout         36000s;\r\n  }\r\n}\r\n\r\nserver {\r\n  listen 80;\r\n  server_name dtest.xxxxxxx.com;\r\n  location / {\r\n    proxy_set_header   Host             $host;\r\n    proxy_set_header   X-Real-IP        $remote_addr;\r\n    proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;\r\n    proxy_pass         http://localhost:8101;\r\n    proxy_redirect off;\r\n  }\r\n}\r\n\r\nserver {\r\n  listen 80;\r\n  server_name test.xxxxxxx.com;\r\n  location / {\r\n    proxy_pass http://127.0.0.1:8102;\r\n    proxy_set_header   Host             $host;\r\n    proxy_set_header   X-Real-IP        $remote_addr;\r\n    proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;\r\n\r\n    # WebSocket support\r\n    proxy_http_version  1.1;\r\n    proxy_set_header    Upgrade    $http_upgrade;\r\n    proxy_set_header    Connection \"upgrade\";\r\n  }\r\n}"
  },
  {
    "path": "examples/theme/elecV2P_theme.20220420.json",
    "content": "[\n  {\n    \"name\": \"我的主题\",\n    \"mainbk\": \"#326733dd\",\n    \"appbk\": \"url(https://images.unsplash.com/photo-1646505183416-f3301d2a8127)\",\n    \"elebk\": \"#0006\",\n    \"style\": \"\"\n  },\n  {\n    \"name\": \"主题常用\",\n    \"mainbk\": \"#326733DD\",\n    \"appbk\": \"url(https://cdn.pixabay.com/photo/2022/03/01/20/58/peace-genius-7042013_960_720.jpg)\",\n    \"elebk\": \"#0003\",\n    \"style\": \".header, .footer {background: #3338;}\"\n  },\n  {\n    \"name\": \"暗黑简单\",\n    \"mainbk\": \"#2E3784\",\n    \"appbk\": \"\",\n    \"elebk\": \"#000C\",\n    \"style\": \"\"\n  },\n  {\n    \"name\": \"常用2\",\n    \"mainbk\": \"#2E3784\",\n    \"appbk\": \"url(https://images.unsplash.com/photo-1646505183416-f3301d2a8127)\",\n    \"elebk\": \"#0000\",\n    \"style\": \".header, .footer {background: #3338;}\"\n  },\n  {\n    \"name\": \"椰树背景\",\n    \"mainbk\": \"#2E3784\",\n    \"appbk\": \"url(https://images.unsplash.com/photo-1649256651398-46408de2f095?auto=format&fit=crop&w=1964)\",\n    \"elebk\": \"#0000\",\n    \"style\": \"\"\n  },\n  {\n    \"name\": \"透明主题\",\n    \"mainbk\": \"#0000\",\n    \"appbk\": \"url(https://images.unsplash.com/photo-1646505183416-f3301d2a8127?auto=format)\",\n    \"elebk\": \"#0000\",\n    \"style\": \".content>div,.elecBtn--long,.efssset_container,.efsslist{border: 1px solid var(--tras-bk);}.header,.footer,.efsslist_content .efssa{color: var(--main-cl);}.content .about,.content .donation{color: var(--main-fc)}.loginfo.loginfo--full{background: var(--secd-fc);}.logs_item{border: 1px solid;}\"\n  }\n]"
  },
  {
    "path": "examples/theme/readme.md",
    "content": "### elecV2P 测试主题\n\n![elecV2P 测试主题](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/theme_preview_01.jpg)\n![elecV2P 测试主题](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/theme_preview_02.png)\n![elecV2P 测试主题](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/docs/res/theme_preview_03.png)\n\n一些用于 elecV2P 的测试主题。\n\n使用方式\n\n1. 下载该目录下的主题文件，比如 elecV2P_theme.json\n2. 打开 elecV2P webUI，SETTING/设置->webUI 相关设置->导入常用主题，选择下载的主题文件\n3. 点击**预览**进行查看，确定后点击**保存**正式启用\n\n*主题功能仅部分用户可用*"
  },
  {
    "path": "information/readme.md",
    "content": "## elecV2P 活动详情\n\n用于发布一些 elecV2P 活动规则\n\n[广告位招租](https://github.com/elecV2/elecV2P-dei/blob/master/information/广告位招租.md)\n\n[开发者激励计划](https://github.com/elecV2/elecV2P-dei/blob/master/information/开发者激励计划.md)"
  },
  {
    "path": "information/广告位招租.md",
    "content": "### 预览图\n\n![elecV2P 广告位](https://raw.githubusercontent.com/elecV2/elecV2P-dei/master/information/res/sponsors_overview.png)\n\n### 价格\n\n平常价位：\n\n- a1 图片广告位 1800 元/月\n- a2 文字广告位  600 元/月\n\n测试阶段（进行中...）：\n\n- a1 图片广告位 600 元/月\n- a2 文字广告位 200 元/月\n\n**测试阶段最多租用 3 个月，开始时间：2022 年 4 月 1 日，结束时间待定**\n\n### 广告展示时间\n\n以北京时间为准，以月为单位。\n\n比如 2022-04-01 开始展示广告，则展示的具体时间为 2022-04-01 00:00:00 到 2022-04-30 23:59:59。\n如果 2022-05-14 开始，则展示的具体时间为 2022-05-14 00:00:00 到 2022-06-13 23:59:59。\n\n*假如出现特殊情况，导致广告掉线，将以天为单位补偿展示时间。*\n\n### 需要提供的内容\n\n- 广告性质及简单说明\n- 显示图片或文字\n  - 图片要求：长 600-1000 高 40-60  推荐 844x50\n  - 文字要求：文字数不超过 20\n- 落地页链接\n\n### 联系方式\n\nE-mail: elecV2#icloud.com (#->@)\nTelegram: @elecv7\n\n### 其他说明\n\n- 落地页不可包含病毒类脚本\n- 落地页不可包含黄赌毒类非法内容\n- 广告推广期间，假如落地页内容有较大变更，请提前通知。如果在没有通知的情况，擅自大幅度更改落地页内容，开发者有权利直接下线广告，且费用不退\n- 长期优质赞助商可展示在 elecV2P 项目首页(Github)\n- 最终解释权归 elecV2P 开发者所有\n\nTelegram 通知频道：https://t.me/elecV2"
  },
  {
    "path": "information/开发者激励计划.md",
    "content": "## 简单说明\n\n每个兼容 elecV2P 的脚本，给予脚本作者 10-100 元不等的人民币奖励。首期总金额 10000 元，发完即止。\n\n截止时间：2022 年 6 月 30 号\n\n本次活动每个作者最高奖励 200 元，可以把机会留给更多的人。\n\n*最终解释权归 elecV2P 开发者所有*\n\n### 示例脚本\n\nelecV2P 默认自带的脚本：[elecV2P 默认脚本](https://github.com/elecV2/elecV2P/tree/master/script/JSFile)\n\nJS 脚本编写参考：[说明文档 04-JS.md](https://github.com/elecV2/elecV2P-dei/blob/master/docs/04-JS.md)\n\n推荐编写 efh 脚本。\n文档参考：[efss.md](https://github.com/elecV2/elecV2P-dei/blob/master/docs/08-logger&efss.md) 中 efh 相关部分。\n实例脚本：[efh 测试脚本](https://github.com/elecV2/elecV2P-dei/tree/master/examples/JSTEST/efh)\n\n### 脚本要求\n\n- 不可加密\n- 长期可用（至少一个月内有效，特殊情况下可放宽此限制）\n- 非 VIP 破解类\n- 脚本内注明：兼容 elecV2P\n\n## 提交脚本\n\n- Github PR（推荐\n  - https://github.com/elecV2/elecV2P-scripts\n  - 提交格式/内容参考仓库 readme 中的相关说明\n- E-mail 提交\n  - elecV2#icloud.com (#->@)\n  - 标题格式：elecV2P 脚本提交-脚本名称-分类\n  - 为避免冒领，请务必附带一张后台截图\n\n**暂时只接受远程脚本，假如脚本只有本地版本，请先 PR 到 https://github.com/elecV2/elecV2P-scripts scripts 目录**\n\n## 奖励发放时间\n\n脚本正式收录时间的下一个周五前。\n比如这周星期一到星期日正式收录到仓库的脚本，将在下周五前发送奖励到指定账号。\n\n### 其他说明\n\n- 最终解释权归 elecV2P 开发者所有\n- Telegram 通知频道：https://t.me/elecV2"
  }
]