Compare commits

..
Author SHA1 Message Date
whyour 7250883628 添加license 2021-04-21 20:04:33 +08:00
whyour 369cfb825f 更新readme 2021-04-21 14:05:15 +08:00
whyour 1274748eee 更新readme 2021-04-21 14:04:54 +08:00
whyour 6d1b7c51d7 更新readme 2021-04-21 14:02:43 +08:00
whyour 6035a22b4a 更新readme 2021-04-21 13:19:47 +08:00
whyour 8303789b0c 更新readme 2021-04-21 12:28:44 +08:00
whyour 8be9bcfbeb 更新readme 2021-04-20 21:45:09 +08:00
whyour 0be1a8c2a6 更新readme 2021-04-20 21:43:00 +08:00
whyour 350fe2caf4 修复cdn链接 2021-04-20 19:22:12 +08:00
whyour f99ef46e6a 修改cdn链接 2021-04-20 18:51:30 +08:00
whyour 299552de86 更换cdn域名 2021-04-20 18:50:30 +08:00
whyour 86874c0ee2 logo使用cdn 2021-04-19 18:18:24 +08:00
whyour 596f03315c 修复diy参数 2021-04-19 17:20:53 +08:00
whyour c6af75cd70 移除js hangup初始化定时任务 2021-04-19 16:41:02 +08:00
whyour 2645716d49 修复diy命令,decodeURI参数,更换logo 2021-04-19 16:27:04 +08:00
whyour ac5e2e404e 修复添加ck,初始化目录缺失 2021-04-16 19:32:27 +08:00
whyour 0563555a6a diy支持任意仓库地址,修复添加ck报错,移除monk仓库内置 2021-04-16 17:30:22 +08:00
whyour 5de67b9679 增加ck管理昵称默认值 2021-04-16 13:23:03 +08:00
whyour 6b7a64845e ck管理添加异步状态获取 2021-04-16 13:07:57 +08:00
whyour 5f25fcc49f 修复diy命令,添加挂机程序配置 2021-04-16 12:43:26 +08:00
whyour bdfec42652 修复diy脚本语法 2021-04-11 23:32:37 +08:00
whyour c2964ec1bb 增加dust仓库秘钥 2021-04-11 23:21:43 +08:00
whyour 947a753610 修复更新cron , fix #32 2021-04-11 20:46:47 +08:00
whyour 17c50d0c9e 使用jq获取json数据 2021-04-11 18:51:31 +08:00
whyour f706b1aa2e 修复cron日志展示 2021-04-11 18:32:45 +08:00
whyour 688fdbff2a 大幅缩减docker镜像 2021-04-11 18:18:58 +08:00
whyour 5ebb9f4196 更新dockerfile 2021-04-11 17:35:20 +08:00
whyour 39e17db9b1 更新dockerfile 2021-04-11 16:31:32 +08:00
whyour d13e1d84da 更新dockerfile 2021-04-11 16:17:30 +08:00
whyour 2217f200dc 抽离前端依赖,只再rebuild时安装 2021-04-11 11:28:46 +08:00
whyour 2a7d298f05 修复cron日志获取逻辑 2021-04-11 10:31:30 +08:00
whyour 7105952670 修复cron日志 2021-04-10 20:02:55 +08:00
whyour 9a1891c909 修复cron日志显示 2021-04-10 19:50:52 +08:00
whyour f9c299cfbe 修复添加ck和cron未更新列表 2021-04-10 13:48:44 +08:00
whyour fb98bc44e4 ck管理增加排序,禁用,实时状态变成手动获取 2021-04-10 00:21:51 +08:00
whyour ef24fc8354 暂时移除nginx ipv6 2021-04-07 23:53:06 +08:00
whyour 1d1eee3513 修改编辑cron数据交互 2021-04-06 17:18:26 +08:00
whyour a6abc15126 优化运行定时任务后日志查看 2021-04-06 17:06:57 +08:00
whyour f41975860c nginx添加ipv6支持 #14 2021-04-06 12:01:03 +08:00
whyour 2a02111644 git clone使用ghproxy代理 #7 2021-04-06 10:39:19 +08:00
whyour 0333365762 修改操作定时任务交互 2021-04-05 22:49:36 +08:00
whyour 21d164218f 修复修改任务后没有充值模态框字段 2021-04-05 00:13:50 +08:00
whyour a21cfb2880 增加api请求timeout 2021-04-04 23:26:34 +08:00
whyour 8001e61796 更新readme 2021-04-04 21:47:10 +08:00
whyour 1400875449 修复diy命令重复添加脚本 2021-04-04 20:52:47 +08:00
whyour c4eb16b137 修改任务管理表格样式 2021-04-04 20:00:41 +08:00
whyour 9a57c27a4f 任务管理表格样式优化 2021-04-04 19:55:31 +08:00
whyour b6345edd69 修复任务管理列表首次请求两次接口 2021-04-04 19:53:16 +08:00
whyour b08f1ab0c5 修复定时任务搜索 2021-04-04 19:19:20 +08:00
whyour a0c5b33239 修复禁用任务后git_pull会重新添加 2021-04-04 19:13:44 +08:00
whyour 78ab74fe1b 修复删除定时任务 2021-04-04 17:57:03 +08:00
whyour f9f1f9d685 修复diy命令😀 2021-04-04 17:22:25 +08:00
whyour 9839c453db cron列表改为顺序展示 2021-04-04 17:13:02 +08:00
whyour 95b44d915c 修复git_pull和diy命令 2021-04-04 17:10:03 +08:00
whyour b10a2a71d0 修复isSystem变量 2021-04-04 16:11:58 +08:00
whyour 9f7cd91d39 修复git_pull重复添加任务 2021-04-04 16:11:35 +08:00
whyour 262e43ec10 修复初始化数据错别字😀 2021-04-04 14:49:55 +08:00
whyour a7d652c86e 增加cron搜索 2021-04-04 14:27:09 +08:00
whyour f46e20fc81 ck管理取消分页 2021-04-04 13:38:37 +08:00
whyour d7d29c468c 修复任务运行状态展示 2021-04-04 13:32:38 +08:00
whyour b73431a7ab 增加系统初始化任务 2021-04-04 11:57:13 +08:00
whyour df1dc4fcbb 更新手动执行cron参数 2021-04-03 22:19:02 +08:00
whyour 7eba87aa2c 修复git_pull 2021-04-03 21:10:03 +08:00
whyour 79507a2f38 移除variables.sh 2021-04-03 20:57:24 +08:00
whyour a519997da7 更新diy和git_pull 2021-04-03 20:45:07 +08:00
whyour 8a45599919 全新定时任务管理 2021-04-03 16:39:10 +08:00
whyour 32be5a6591 修改容器启动程序顺序,防止更新源代码阻塞面板启动 2021-04-02 22:10:58 +08:00
whyour 81ba9d5e52 修改codemirror容器样式 2021-04-02 13:27:25 +08:00
whyour e3ff6d1ce6 修复codemirror样式 2021-04-02 13:20:03 +08:00
whyour 3fa5956051 修改内容基础样式 2021-04-02 11:31:49 +08:00
whyour 90fd845169 修复默认定时表达式 2021-04-02 10:18:34 +08:00
whyour 870e90fc2a 修复定时任务不展示 2021-04-02 09:40:59 +08:00
whyour 16ad626b43 diy.sh重命名为extra.sh,防止与diy命令混淆 2021-04-01 23:55:32 +08:00
whyour d5b9bca68b git_pull 重构,样式优化 2021-04-01 23:50:24 +08:00
whyour 4ca49edf01 修复git_pull定时任务 2021-04-01 15:17:28 +08:00
whyour a7a6fdd350 修复diy命令无法跳出 2021-03-30 16:45:22 +08:00
whyour 09498766dd 修改git_pull链接 2021-03-30 15:21:04 +08:00
whyour d3211eb564 修改rebuild命令 2021-03-30 10:17:09 +08:00
whyour 45f434df0a 修复 git_pull 参数 2021-03-30 10:07:08 +08:00
whyour e4339ab204 修改 git_pull 逻辑 2021-03-30 10:02:50 +08:00
whyour f0f738f8a2 修复scripts独立造成shell样式丢失 2021-03-29 22:42:46 +08:00
whyour 9f54a15ab8 修复upstream gzip压缩,前端公共依赖采用cdn 2021-03-29 20:27:59 +08:00
whyour f0be863b3f 修复日志列表唯一值和互助码目录 2021-03-29 14:40:39 +08:00
whyour ef586697f0 移除Dockerfile scripts依赖 2021-03-29 12:17:05 +08:00
whyour 7df97a31e1 修复git_pull命令执行顺序 2021-03-29 12:02:32 +08:00
whyour 02ed509a7f 修复git_pull部分命令导致无法添加cron 2021-03-29 11:40:30 +08:00
whyour 306419158e 修复yarn错误 2021-03-29 10:26:44 +08:00
whyour a47d667414 更新 gzip_types 2021-03-29 10:15:17 +08:00
whyour 1cec38243c nginx增加gzip压缩,webpack预压缩gz文件 2021-03-29 10:08:19 +08:00
whyour b10e77dce6 增加文件保存限制,增加接口错误提醒 2021-03-28 23:36:01 +08:00
whyour b8e3a451c3 rebuild命令添加pm2服务无法自动启动时,自动启动 2021-03-28 21:13:23 +08:00
whyour 44addb6ff3 更新diy命令参数识别 2021-03-28 20:07:50 +08:00
whyour 5b874befb8 diy支持指定用户指定仓储指定目录指定文件黑名单 2021-03-28 18:07:08 +08:00
whyour 6dff1e9109 修复互助码目录 2021-03-27 22:39:28 +08:00
whyour 1d585e2619 添加Cron接口,更新Dockerfile 2021-03-27 21:18:44 +08:00
whyour 7ba3e59980 优化登录验证、退出逻辑 2021-03-26 23:57:31 +08:00
whyour 9fd678a7fc 修复git_pull生成crontab 2021-03-26 10:32:44 +08:00
whyour 4f1376e750 更新Dockerfile 2021-03-26 00:35:43 +08:00
whyour 8906472ae0 基础命令重构
1. 移除命令中 bash 依赖
2. jd命令重命名为js
3. 修改默认初始目录
4. 移除冗余代码
2021-03-25 23:44:57 +08:00
whyour 43eab41579 更新readme 2021-03-24 22:51:51 +08:00
whyour d3644c8eb8 修改docker build 触发逻辑 2021-03-24 20:40:17 +08:00
whyour 3c579c10a6 修复ck填写错误,ck列表报错 2021-03-24 19:43:11 +08:00
whyour 64a8acde92 添加Cookie增加每条验证
1. codemirror主题改为默认
2. docker打包触发条件改为tag
2021-03-24 17:38:45 +08:00
whyour 3f2f165d72 cookie添加增加格式验证 2021-03-24 10:14:36 +08:00
whyour 6648013c30 修复互助码页面复制 2021-03-23 23:05:37 +08:00
whyour 879662b7eb 修复日志拷贝问题 2021-03-23 22:30:20 +08:00
whyour cfee47b112 修复diff和系统设置移动端样式 2021-03-23 17:17:19 +08:00
whyour 39ae979953 修改Cookie管理表格最小宽度 2021-03-23 17:04:07 +08:00
whyour a4f06413a3 修复codemirror暗黑模式文本选中颜色 2021-03-23 15:53:03 +08:00
whyour 51b0418fa9 添加退出登录功能 2021-03-23 14:32:17 +08:00
whyour d41eb582e4 首次登录自动生成随机密码 2021-03-22 23:41:26 +08:00
whyour dada261987 修复sharecode.sh 2021-03-22 22:36:24 +08:00
whyour 6d75cf54b8 修复Cookie组合 2021-03-22 22:24:52 +08:00
whyour ad30e6bbeb 重构Cookie管理逻辑和交互 2021-03-22 17:59:21 +08:00
whyour ad513d1eae 精简umi react依赖,缩减镜像至760M左右 2021-03-22 13:30:17 +08:00
whyour 3e8a5bd74c 缩减docker镜像至940M 2021-03-22 13:16:38 +08:00
whyour bdffd7d402 添加docker.yml 2021-03-22 00:24:40 +08:00
whyour 6e4648d709 修复rebuild命令 2021-03-21 20:25:27 +08:00
whyour 4a618ece68 修复右侧内容区样式 2021-03-21 20:11:23 +08:00
whyour b264c91846 修复用户认证字段,添加rebuild命令 2021-03-21 19:34:41 +08:00
whyour baa16d8ab2 修复日志查看和用户认证 2021-03-21 18:21:37 +08:00
whyour 386eff8a0b 修复主题逻辑判断 2021-03-21 14:22:38 +08:00
whyour 3a1de79322 更新git_pull,移除update.js 依赖 2021-03-21 13:12:09 +08:00
whyour ecb5bb1718 修复用户名密码修改,diff视图滚动 2021-03-21 11:57:00 +08:00
whyour f03323e5d8 更新diy和通知脚本 2021-03-20 11:25:51 +08:00
whyour 1da99472b4 修改git_diy脚本仓库路径 2021-03-20 00:14:50 +08:00
whyour 602e2775f7 增加git_diy脚本,支持自定义拉取仓库 2021-03-20 00:13:43 +08:00
whyour 6ad50be6fb 删除console.log 2021-03-19 21:23:54 +08:00
whyour 2b2ddc9b0e 修复初始化主题加载 2021-03-19 21:19:24 +08:00
whyour 8ed071bc29 删除未使用依赖 2021-03-19 21:13:23 +08:00
whyour 9d28a8a0db 支持暗黑模式 2021-03-19 17:04:25 +08:00
whyour c0f09ec5bb 添加log查询api 2021-03-18 23:51:35 +08:00
whyour b2c22327f9 修改Cookie管理刷新逻辑 2021-03-18 23:19:25 +08:00
whyour 041479443e 移除console.log 2021-03-18 21:03:43 +08:00
whyour a2510d6763 修复暗黑模式兼容 2021-03-18 20:11:34 +08:00
whyour d238850b35 修复首次获取Cookie 2021-03-18 19:58:52 +08:00
whyour b1f6d1ce14 更新Dockerfile 2021-03-18 18:58:54 +08:00
whyour f803013846 修改nginx监听端口 2021-03-18 14:51:29 +08:00
whyour f2f4bfbefd 更新nginx配置 2021-03-18 14:32:36 +08:00
whyour c0bb6cddf4 修改dockerfile和默认跳转路径 2021-03-18 11:31:49 +08:00
whyour 4555a24434 添加dockerfile 2021-03-18 10:02:13 +08:00
whyour 361963711e 更新cookie管理,文件保存 2021-03-17 20:43:03 +08:00
whyour 8526a57caf 更新cookie管理添加cookie逻辑 2021-03-17 00:35:14 +08:00
whyour 7f7d4d4f24 添加cookie管理页 2021-03-16 14:46:40 +08:00
whyour 99a8dc78db 初始化api 2021-03-15 21:09:30 +08:00
whyour f1f8ece8a2 初始化项目 2021-03-14 22:06:27 +08:00
74 changed files with 6218 additions and 1 deletions
Executable
+16
View File
@@ -0,0 +1,16 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab
+8
View File
@@ -0,0 +1,8 @@
MONGODB_URI='mongodb://'
YIYAN_MONGODB_URI=''
PORT=5600
LOG_LEVEL='debug'
SECRET='whyour'
-1
View File
@@ -1 +0,0 @@
Gitee Pull mirror notifications.
+54
View File
@@ -0,0 +1,54 @@
name: Publish Docker Image
on:
push:
tags:
- v*
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout base
uses: actions/checkout@v2
with:
fetch-depth: 0
# https://github.com/docker/setup-qemu-action
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
# https://github.com/docker/setup-buildx-action
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Docker login
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin
- name: Docker buildx image and push on master branch
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
if: github.ref == 'refs/heads/master'
run: |
docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v7,linux/arm64 --tag whyour/qinglong:latest docker/
- name: Replace tag without `v`
if: startsWith(github.ref, 'refs/tags/')
uses: actions/github-script@v1
id: version
with:
script: |
return context.payload.ref.replace(/\/?refs\/tags\/v/, '')
result-encoding: string
- name: Docker buildx image and push on release
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
if: startsWith(github.ref, 'refs/tags/')
run: |
docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v7,linux/arm64 --tag whyour/qinglong:${{steps.version.outputs.result}} docker/
docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v7,linux/arm64 --tag whyour/qinglong:latest docker/
+28
View File
@@ -0,0 +1,28 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/npm-debug.log*
/yarn-error.log
/yarn.lock
/package-lock.json
# production
/dist
/build
# misc
.DS_Store
# umi
/src/.umi
/src/.umi-production
/src/.umi-test
/.env.local
.env
.history
/config
/log
/db
/manual_log
+8
View File
@@ -0,0 +1,8 @@
**/*.md
**/*.svg
**/*.ejs
**/*.html
package.json
.umi
.umi-production
.umi-test
+11
View File
@@ -0,0 +1,11 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 80,
"overrides": [
{
"files": ".prettierrc",
"options": { "parser": "json" }
}
]
}
+41
View File
@@ -0,0 +1,41 @@
import { defineConfig } from 'umi';
const CompressionPlugin = require('compression-webpack-plugin');
export default defineConfig({
hash: true,
layout: false,
nodeModulesTransform: {
type: 'none',
},
fastRefresh: {},
favicon: 'https://qinglong.whyour.cn/g5.ico',
proxy: {
'/api': {
target: 'http://127.0.0.1:5678/',
changeOrigin: true,
},
},
chainWebpack: (config) => {
config.plugin('compression-webpack-plugin').use(
new CompressionPlugin({
algorithm: 'gzip',
test: new RegExp('\\.(js|css)$'),
threshold: 10240,
minRatio: 0.6,
}),
);
},
externals: {
react: 'window.React',
'react-dom': 'window.ReactDOM',
codemirror: 'window.CodeMirror',
darkreader: 'window.DarkReader',
},
scripts: [
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
'https://cdn.jsdelivr.net/npm/codemirror@5.60.0/lib/codemirror.min.js',
'https://cdn.jsdelivr.net/npm/darkreader@4.9.27/darkreader.min.js',
'https://cdn.jsdelivr.net/npm/codemirror@5.60.0/mode/shell/shell.js',
],
});
+22
View File
@@ -0,0 +1,22 @@
MIT LICENSE
Copyright (c) 2021-present whyour
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+64
View File
@@ -0,0 +1,64 @@
<p align="center">
<a href="https://github.com/whyour/qinglong">
<img width="150" src="https://qinglong.whyour.cn/qinglong.png">
</a>
</p>
<h1 align="center">青龙(WIP)</h1>
<div align="center">
python和javaScript的定时任务管理面板
[![donate][donate-image]][donate-url] [![build status][build-status-image]][build-status-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker version][docker-version-image]][docker-version-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
[donate-image]: https://img.shields.io/badge/donate-wechat-green?style=for-the-badge
[donate-url]: https://qinglong.whyour.cn/nice.png
[build-status-image]: https://img.shields.io/docker/cloud/build/whyour/qinglong?style=for-the-badge
[build-status-url]: https://img.shields.io/docker/cloud/build/whyour/qinglong
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=for-the-badge
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=for-the-badge
[docker-version-url]: https://hub.docker.com/r/whyour/qinglong/tags?page=1&ordering=last_updated
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=for-the-badge
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=for-the-badge
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
</div>
青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
《后汉书·律历志下》记载:日周于天,一寒一暑,四时备成,万物毕改,摄提迁次,青龙移辰,谓之岁。
在中国[二十八宿](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF)中,青龙是东方七宿(角、亢、氐、房、心、尾、箕)的总称。 在早期星宿信仰中,祂是最尊贵的天神。 但被道教信仰吸纳入其神系后,神格大跌,道教将其称为“孟章”,在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼,与白虎监兵神君一起,是道教的护卫天神。
## 多谢
* [https://github.com/nevinee/jd_shell](https://github.com/nevinee/jd_shell)
* [https://github.com/alseambusher/crontab-ui](https://github.com/alseambusher/crontab-ui)
* [Ant Design](https://ant.design)
* [Ant Design Pro](https://pro.ant.design/)
* [Umijs3.0](https://umijs.org)
* [darkreader](https://github.com/darkreader/darkreader)
## 免责声明
1. 此仓储脚本仅用于学习研究,不保证其合法性、准确性、有效性,请根据情况自行判断,本人对此不承担任何保证责任。
2. 由于此仓储脚本仅用于学习研究,您必须在下载后 24 小时内将所有内容从您的计算机或手机或任何存储设备中完全删除,若违反规定引起任何事件本人对此均不负责。
3. 请勿将此仓储脚本用于任何商业或非法目的,若违反规定请自行对此负责。
4. 此仓储脚本涉及应用与本人无关,本人对因此引起的任何隐私泄漏或其他后果不承担任何责任。
5. 本人对任何脚本引发的问题概不负责,包括但不限于由脚本错误引起的任何损失和损害。
6. 如果任何单位或个人认为此仓储脚本可能涉嫌侵犯其权利,应及时通知并提供身份证明,所有权证明,我们将在收到认证文件确认后删除此仓储脚本。
7. 所有直接或间接使用、查看此仓储脚本的人均应该仔细阅读此声明。本人保留随时更改或补充此声明的权利。一旦您使用或复制了此仓储脚本,即视为您已接受此免责声明。
+128
View File
@@ -0,0 +1,128 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config';
import jwt from 'jsonwebtoken';
import { createPassword } from '../config/util';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.post(
'/login',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let username = req.body.username;
let password = req.body.password;
fs.readFile(config.authConfigFile, 'utf8', function (err, data) {
if (err) console.log(err);
const authInfo = JSON.parse(data);
if (username && password) {
if (
authInfo.username === 'admin' &&
authInfo.password === 'adminadmin'
) {
const newPassword = createPassword(16, 22);
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({
username: authInfo.username,
password: newPassword,
}),
);
return res.send({
code: 100,
msg: '已初始化密码,请前往auth.json查看并重新登录',
});
}
if (
username == authInfo.username &&
password == authInfo.password
) {
let token = jwt.sign(
{ username, password },
config.secret as any,
{ expiresIn: 60 * 60 * 24 * 7, algorithm: 'HS384' },
);
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({
username: authInfo.username,
password: authInfo.password,
token,
}),
);
res.send({ code: 200, token });
} else {
res.send({ code: 400, msg: config.authError });
}
} else {
res.send({ err: 400, msg: '请输入用户名密码!' });
}
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/logout',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
fs.readFile(config.authConfigFile, 'utf8', function (err, data) {
if (err) console.log(err);
const authInfo = JSON.parse(data);
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({
username: authInfo.username,
password: authInfo.password,
}),
);
res.send({ code: 200 });
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/user',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
fs.writeFile(config.authConfigFile, JSON.stringify(req.body), (err) => {
if (err) console.log(err);
res.send({ code: 200, msg: '更新成功' });
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/user',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
if (err) console.log(err);
const authInfo = JSON.parse(data);
res.send({ code: 200, data: { username: authInfo.username } });
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+71
View File
@@ -0,0 +1,71 @@
import { getFileContentByName, getLastModifyFilePath } from '../config/util';
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import config from '../config';
import * as fs from 'fs';
import { celebrate, Joi } from 'celebrate';
import { execSync } from 'child_process';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.get(
'/config/:key',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let content = '未找到文件';
switch (req.params.key) {
case 'config':
content = getFileContentByName(config.confFile);
break;
case 'sample':
content = getFileContentByName(config.sampleFile);
break;
case 'crontab':
content = getFileContentByName(config.crontabFile);
break;
case 'shareCode':
let shareCodeFile = getLastModifyFilePath(config.shareCodeDir);
content = getFileContentByName(shareCodeFile);
break;
case 'extra':
content = getFileContentByName(config.extraFile);
break;
default:
break;
}
res.send({ code: 200, data: content });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/save',
celebrate({
body: Joi.object({
name: Joi.string().required(),
content: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const { name, content } = req.body;
const path = (config.fileMap as any)[name];
fs.writeFileSync(path, content);
if (name === 'crontab.list') {
execSync(`crontab ${path}`);
}
res.send({ code: 200, msg: '保存成功' });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+187
View File
@@ -0,0 +1,187 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import CookieService from '../services/cookie';
import { Logger } from 'winston';
import { celebrate, Joi } from 'celebrate';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.get(
'/cookies',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.cookies();
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/cookies',
celebrate({
body: Joi.array().items(Joi.string().required()).min(1),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.create(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/cookies',
celebrate({
body: Joi.object({
value: Joi.string().required(),
_id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.update(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.delete(
'/cookies/:id',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.remove(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/cookies/:id/move',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
body: Joi.object({
fromIndex: Joi.number().required(),
toIndex: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.move(req.params.id, req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/cookies/:id/refresh',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.refreshCookie(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/cookies/:id/disable',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.disabled(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/cookies/:id/enable',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.enabled(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/cookies/:id',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.get(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+208
View File
@@ -0,0 +1,208 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config';
import CronService from '../services/cron';
import { celebrate, Joi } from 'celebrate';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.get(
'/crons',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.crontabs(
req.query.searchValue as string,
);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/crons',
celebrate({
body: Joi.object({
command: Joi.string().required(),
schedule: Joi.string().required(),
name: Joi.string(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.create(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/crons/:id/run',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.run(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/crons/:id/disable',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.disabled(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/crons/:id/enable',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.enabled(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/crons/:id/log',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.log(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/crons',
celebrate({
body: Joi.object({
command: Joi.string().required(),
schedule: Joi.string().required(),
name: Joi.string(),
_id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.update(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.delete(
'/crons/:id',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.remove(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/crons/import',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.import_crontab();
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/crons/:id',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.get(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+17
View File
@@ -0,0 +1,17 @@
import { Router } from 'express';
import auth from './auth';
import cookie from './cookie';
import config from './config';
import log from './log';
import cron from './cron';
export default () => {
const app = Router();
auth(app);
cookie(app);
config(app);
log(app);
cron(app);
return app;
};
+77
View File
@@ -0,0 +1,77 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config';
import { getFileContentByName } from '../config/util';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.get(
'/logs',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const fileList = fs.readdirSync(config.logPath, 'utf-8');
const dirs = [];
for (let i = 0; i < fileList.length; i++) {
const stat = fs.lstatSync(config.logPath + fileList[i]);
if (stat.isDirectory()) {
const fileListTmp = fs.readdirSync(
`${config.logPath}/${fileList[i]}`,
'utf-8',
);
dirs.push({
name: fileList[i],
isDir: true,
files: fileListTmp.reverse(),
});
} else {
dirs.push({
name: fileList[i],
isDir: false,
files: [],
});
}
}
res.send({ code: 200, dirs });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/logs/:dir/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const { dir, file } = req.params;
const content = getFileContentByName(
`${config.logPath}/${dir}/${file}`,
);
res.send({ code: 200, data: content });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/logs/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const { file } = req.params;
const content = getFileContentByName(`${config.logPath}/${file}`);
res.send({ code: 200, data: content });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+28
View File
@@ -0,0 +1,28 @@
import 'reflect-metadata'; // We need this in order to use @Decorators
import config from './config';
import express from 'express';
import Logger from './loaders/logger';
async function startServer() {
const app = express();
await require('./loaders').default({ expressApp: app });
app
.listen(config.port, () => {
Logger.info(`
################################################
🛡️ Server listening on port: ${config.port} 🛡️
################################################
`);
})
.on('error', (err) => {
Logger.error(err);
process.exit(1);
});
}
startServer();
+59
View File
@@ -0,0 +1,59 @@
import dotenv from 'dotenv';
import path from 'path';
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
const envFound = dotenv.config();
const rootPath = path.resolve(__dirname, '../../');
const cookieFile = path.join(rootPath, 'config/cookie.sh');
const confFile = path.join(rootPath, 'config/config.sh');
const sampleFile = path.join(rootPath, 'sample/config.sh.sample');
const crontabFile = path.join(rootPath, 'config/crontab.list');
const confBakDir = path.join(rootPath, 'config/bak/');
const authConfigFile = path.join(rootPath, 'config/auth.json');
const shareCodeDir = path.join(rootPath, 'log/export_sharecodes/');
const extraFile = path.join(rootPath, 'config/extra.sh');
const logPath = path.join(rootPath, 'log/');
const authError = '错误的用户名密码,请重试';
const loginFaild = '请先登录!';
const configString = 'config sample crontab shareCode diy';
const dbPath = path.join(rootPath, 'db/');
const manualLogPath = path.join(rootPath, 'manual_log/');
const cronDbFile = path.join(rootPath, 'db/crontab.db');
const cookieDbFile = path.join(rootPath, 'db/cookie.db');
if (envFound.error) {
throw new Error("⚠️ Couldn't find .env file ⚠️");
}
export default {
port: parseInt(process.env.PORT as string, 10),
secret: process.env.SECRET,
logs: {
level: process.env.LOG_LEVEL || 'silly',
},
api: {
prefix: '/api',
},
configString,
loginFaild,
authError,
logPath,
extraFile,
shareCodeDir,
authConfigFile,
confBakDir,
crontabFile,
sampleFile,
confFile,
cookieFile,
fileMap: {
'config.sh': confFile,
'crontab.list': crontabFile,
'extra.sh': extraFile,
},
dbPath,
cronDbFile,
cookieDbFile,
manualLogPath,
};
+113
View File
@@ -0,0 +1,113 @@
import * as fs from 'fs';
import * as path from 'path';
export function getFileContentByName(fileName: string) {
if (fs.existsSync(fileName)) {
return fs.readFileSync(fileName, 'utf8');
}
return '';
}
export function getLastModifyFilePath(dir: string) {
let filePath = '';
if (fs.existsSync(dir)) {
const arr = fs.readdirSync(dir);
arr.forEach((item) => {
const fullpath = path.join(dir, item);
const stats = fs.statSync(fullpath);
if (stats.isFile()) {
if (stats.mtimeMs >= 0) {
filePath = fullpath;
}
}
});
}
return filePath;
}
export function createPassword(min: number, max: number): string {
const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const english = [
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
];
const ENGLISH = [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
];
const special = ['-', '_', '#'];
const config = num.concat(english).concat(ENGLISH).concat(special);
const arr = [];
arr.push(getOne(num));
arr.push(getOne(english));
arr.push(getOne(ENGLISH));
arr.push(getOne(special));
const len = min + Math.floor(Math.random() * (max - min + 1));
for (let i = 4; i < len; i++) {
arr.push(config[Math.floor(Math.random() * config.length)]);
}
const newArr = [];
for (let j = 0; j < len; j++) {
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
}
function getOne(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
return newArr.join('');
}
+27
View File
@@ -0,0 +1,27 @@
export class Cookie {
value?: string;
timestamp?: string;
created?: number;
_id?: string;
status?: CookieStatus;
position?: number;
constructor(options: Cookie) {
this.value = options.value;
this._id = options._id;
this.created = options.created || new Date().valueOf();
this.status = options.status || CookieStatus.noacquired;
this.timestamp = new Date().toString();
this.position = options.position;
}
}
export enum CookieStatus {
'noacquired',
'normal',
'disabled',
'invalid',
'abnormal',
}
export const initCookiePosition = 9999999999;
+29
View File
@@ -0,0 +1,29 @@
export class Crontab {
name?: string;
command: string;
schedule: string;
timestamp?: string;
created?: number;
saved?: boolean;
_id?: string;
status?: CrontabStatus;
isSystem?: 1 | 0;
constructor(options: Crontab) {
this.name = options.name;
this.command = options.command;
this.schedule = options.schedule;
this.saved = options.saved;
this._id = options._id;
this.created = options.created;
this.status = options.status || CrontabStatus.idle;
this.timestamp = new Date().toString();
this.isSystem = options.isSystem || 0;
}
}
export enum CrontabStatus {
'running',
'idle',
'disabled',
}
+15
View File
@@ -0,0 +1,15 @@
import { Container } from 'typedi';
import LoggerInstance from './logger';
export default ({ models }: { models: { name: string; model: any }[] }) => {
try {
models.forEach((m) => {
Container.set(m.name, m.model);
});
Container.set('logger', LoggerInstance);
} catch (e) {
LoggerInstance.error('🔥 Error on dependency injector loader: %o', e);
throw e;
}
};
+71
View File
@@ -0,0 +1,71 @@
import { Request, Response, NextFunction, Application } from 'express';
import bodyParser from 'body-parser';
import cors from 'cors';
import routes from '../api';
import config from '../config';
import jwt from 'express-jwt';
import fs from 'fs';
export default ({ app }: { app: Application }) => {
app.enable('trust proxy');
app.use(cors());
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
app.use(
jwt({ secret: config.secret as string, algorithms: ['HS384'] }).unless({
path: ['/api/login'],
}),
);
app.use((req, res, next) => {
if (req.url && req.url.includes('/api/login')) {
return next();
}
const data = fs.readFileSync(config.authConfigFile, 'utf8');
const authHeader = req.headers.authorization;
if (data) {
const { token } = JSON.parse(data);
if (token && authHeader.includes(token)) {
return next();
}
}
const err: any = new Error('UnauthorizedError');
err['status'] = 401;
next(err);
});
app.use(config.api.prefix, routes());
app.use((req, res, next) => {
const err: any = new Error('Not Found');
err['status'] = 404;
next(err);
});
app.use(
(
err: Error & { status: number },
req: Request,
res: Response,
next: NextFunction,
) => {
if (err.name === 'UnauthorizedError') {
return res.status(err.status).send({ message: err.message }).end();
}
return next(err);
},
);
app.use(
(
err: Error & { status: number },
req: Request,
res: Response,
next: NextFunction,
) => {
res.status(err.status || 500);
res.json({
message: err.message,
});
},
);
};
+19
View File
@@ -0,0 +1,19 @@
import expressLoader from './express';
import dependencyInjectorLoader from './dependencyInjector';
import Logger from './logger';
import initData from './initData';
export default async ({ expressApp }: { expressApp: any }) => {
Logger.info('✌️ DB loaded and connected!');
await dependencyInjectorLoader({
models: [],
});
Logger.info('✌️ Dependency Injector loaded');
await expressLoader({ app: expressApp });
Logger.info('✌️ Express loaded');
await initData();
Logger.info('✌️ init data loaded');
};
+99
View File
@@ -0,0 +1,99 @@
import { Container } from 'typedi';
import { Crontab, CrontabStatus } from '../data/cron';
import CronService from '../services/cron';
const initData = [
{
name: '更新面板',
command: `sleep ${randomSchedule(
60,
1,
)} && git_pull >> $QL_DIR/log/git_pull.log 2>&1`,
schedule: `${randomSchedule(60, 1)} ${randomSchedule(
24,
7,
).toString()} * * *`,
status: CrontabStatus.idle,
},
{
name: 'build面板',
command: 'rebuild >> ${QL_DIR}/log/rebuild.log 2>&1',
schedule: '30 7 */7 * *',
status: CrontabStatus.disabled,
},
{
name: '自定义仓库',
command: `sleep ${randomSchedule(
60,
1,
)} && diy https://ghproxy.com/https://github.com/whyour/hundun.git "quanx/jx|quanx/jd" tokens >> $QL_DIR/log/diy_pull.log 2>&1`,
schedule: `${randomSchedule(60, 1)} ${randomSchedule(
24,
6,
).toString()} * * *`,
status: CrontabStatus.idle,
},
{
name: '互助码导出',
command: 'export_sharecodes',
schedule: '48 5 * * *',
status: CrontabStatus.idle,
},
{
name: '删除日志',
command: 'rm_log >/dev/null 2>&1',
schedule: '30 7 */7 * *',
status: CrontabStatus.disabled,
},
{
name: '重置密码',
command: 'js resetpwd',
schedule: '33 6 */7 * *',
status: CrontabStatus.disabled,
},
{
name: '运行所有脚本(慎用)',
command: 'js runall',
schedule: '33 6 */7 * *',
status: CrontabStatus.disabled,
},
];
export default async () => {
const cronService = Container.get(CronService);
const cronDb = cronService.getDb();
cronDb.count({}, async (err, count) => {
const data = initData.map((x) => {
const tab = new Crontab(x);
tab.created = new Date().valueOf();
tab.saved = false;
if (tab.name === '更新面板') {
tab.isSystem = 1;
} else {
tab.isSystem = 0;
}
return tab;
});
if (count === 0) {
cronDb.insert(data);
await cronService.autosave_crontab();
}
});
};
function randomSchedule(from: number, to: number) {
const result = [];
const arr = [...Array(from).keys()];
let count = arr.length;
for (let i = 0; i < to; i++) {
const index = ~~(Math.random() * count) + i;
if (result.includes(arr[index])) {
continue;
}
result[i] = arr[index];
arr[index] = arr[i];
count--;
}
return result;
}
+32
View File
@@ -0,0 +1,32 @@
import winston from 'winston';
import config from '../config';
const transports = [];
if (process.env.NODE_ENV !== 'development') {
transports.push(new winston.transports.Console());
} else {
transports.push(
new winston.transports.Console({
format: winston.format.combine(
winston.format.cli(),
winston.format.splat(),
),
}),
);
}
const LoggerInstance = winston.createLogger({
level: config.logs.level,
levels: winston.config.npm.levels,
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss',
}),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json(),
),
transports,
});
export default LoggerInstance;
+281
View File
@@ -0,0 +1,281 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import fetch from 'node-fetch';
import { getFileContentByName } from '../config/util';
import config from '../config';
import * as fs from 'fs';
import got from 'got';
import DataStore from 'nedb';
import { Cookie, CookieStatus, initCookiePosition } from '../data/cookie';
@Service()
export default class CookieService {
private cronDb = new DataStore({ filename: config.cookieDbFile });
constructor(@Inject('logger') private logger: winston.Logger) {
this.cronDb.loadDatabase((err) => {
if (err) throw err;
});
}
public async getCookies() {
const content = getFileContentByName(config.cookieFile);
return this.formatCookie(content.split('\n').filter((x) => !!x));
}
public async addCookie(cookies: string[]) {
let content = getFileContentByName(config.cookieFile);
const originCookies = content.split('\n').filter((x) => !!x);
const result = originCookies.concat(cookies);
fs.writeFileSync(config.cookieFile, result.join('\n'));
return '';
}
public async updateCookie({ cookie, oldCookie }) {
let content = getFileContentByName(config.cookieFile);
const cookies = content.split('\n');
const index = cookies.findIndex((x) => x === oldCookie);
if (index !== -1) {
cookies[index] = cookie;
fs.writeFileSync(config.cookieFile, cookies.join('\n'));
return '';
} else {
return '未找到要原有Cookie';
}
}
public async deleteCookie(cookie: string) {
let content = getFileContentByName(config.cookieFile);
const cookies = content.split('\n');
const index = cookies.findIndex((x) => x === cookie);
if (index !== -1) {
cookies.splice(index, 1);
fs.writeFileSync(config.cookieFile, cookies.join('\n'));
return '';
} else {
return '未找到要删除的Cookie';
}
}
private async formatCookie(data: any[]) {
const result = [];
for (const x of data) {
const { nickname, status } = await this.getJdInfo(x);
if (/pt_pin=(.+?);/.test(x)) {
result.push({
pin: x.match(/pt_pin=(.+?);/)[1],
cookie: x,
status,
nickname: nickname,
});
} else {
result.push({
pin: 'pin未匹配到',
cookie: x,
status,
nickname: nickname,
});
}
}
return result;
}
public async refreshCookie(_id: string) {
const current = await this.get(_id);
const { status, nickname } = await this.getJdInfo(current.value);
return {
...current,
status,
nickname,
};
}
private getJdInfo(cookie: string) {
return fetch(
`https://me-api.jd.com/user_new/info/GetJDUserInfoUnion?orgFlag=JD_PinGou_New&callSource=mainorder&channel=4&isHomewhite=0&sceneval=2&_=${Date.now()}&sceneval=2&g_login_type=1&g_ty=ls`,
{
method: 'get',
headers: {
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-cn',
Connection: 'keep-alive',
Cookie: cookie,
Referer: 'https://home.m.jd.com/myJd/newhome.action',
'User-Agent':
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1',
Host: 'me-api.jd.com',
},
},
)
.then((x) => x.json())
.then((x) => {
if (x.retcode === '0' && x.data && x.data.userInfo) {
return {
nickname: x.data.userInfo.baseInfo.nickname,
status: CookieStatus.normal,
};
} else if (x.retcode === 13) {
return { status: CookieStatus.invalid, nickname: '-' };
}
return { status: CookieStatus.abnormal, nickname: '-' };
});
}
public async create(payload: string[]): Promise<Cookie[]> {
const cookies = await this.cookies('');
let position = initCookiePosition;
if (cookies && cookies.length > 0) {
position = cookies[cookies.length - 1].position;
}
const tabs = payload.map((x) => {
const cookie = new Cookie({ value: x, position });
position = position / 2;
cookie.position = position;
return cookie;
});
const docs = await this.insert(tabs);
await this.set_cookies();
return docs;
}
public async insert(payload: Cookie[]): Promise<Cookie[]> {
return new Promise((resolve) => {
this.cronDb.insert(payload, (err, docs) => {
if (err) {
this.logger.error(err);
} else {
resolve(docs);
}
});
});
}
public async update(payload: Cookie): Promise<Cookie> {
const { _id, ...other } = payload;
const doc = await this.get(_id);
const tab = new Cookie({ ...doc, ...other });
const newDoc = await this.updateDb(tab);
await this.set_cookies();
return newDoc;
}
public async updateDb(payload: Cookie): Promise<Cookie> {
return new Promise((resolve) => {
this.cronDb.update(
{ _id: payload._id },
payload,
{ returnUpdatedDocs: true },
(err, docs) => {
if (err) {
this.logger.error(err);
} else {
resolve(docs as Cookie);
}
},
);
});
}
public async remove(_id: string) {
this.cronDb.remove({ _id }, {});
await this.set_cookies();
}
public async move(
_id: string,
{
fromIndex,
toIndex,
}: {
fromIndex: number;
toIndex: number;
},
) {
let targetPosition: number;
const isUpward = fromIndex > toIndex;
const cookies = await this.cookies();
if (toIndex === 0 || toIndex === cookies.length - 1) {
targetPosition = isUpward
? cookies[0].position * 2
: cookies[toIndex].position / 2;
} else {
targetPosition = isUpward
? (cookies[toIndex].position + cookies[toIndex - 1].position) / 2
: (cookies[toIndex].position + cookies[toIndex + 1].position) / 2;
}
this.update({
_id,
position: targetPosition,
});
await this.set_cookies();
}
public async cookies(
searchText?: string,
sort: any = { position: -1 },
): Promise<Cookie[]> {
let query = {};
if (searchText) {
const reg = new RegExp(searchText);
query = {
$or: [
{
name: reg,
},
{
command: reg,
},
],
};
}
return new Promise((resolve) => {
this.cronDb
.find(query)
.sort({ ...sort })
.exec((err, docs) => {
resolve(docs);
});
});
}
public async get(_id: string): Promise<Cookie> {
return new Promise((resolve) => {
this.cronDb.find({ _id }).exec((err, docs) => {
resolve(docs[0]);
});
});
}
public async getBySort(sort: any): Promise<Cookie> {
return new Promise((resolve) => {
this.cronDb
.find({})
.sort({ ...sort })
.limit(1)
.exec((err, docs) => {
resolve(docs[0]);
});
});
}
public async disabled(_id: string) {
this.cronDb.update({ _id }, { $set: { status: CookieStatus.disabled } });
await this.set_cookies();
}
public async enabled(_id: string) {
this.cronDb.update({ _id }, { $set: { status: CookieStatus.noacquired } });
}
private async set_cookies() {
const cookies = await this.cookies();
let cookie_string = '';
cookies.forEach((tab) => {
if (tab.status !== CookieStatus.disabled) {
cookie_string += tab.value;
cookie_string += '\n';
}
});
fs.writeFileSync(config.cookieFile, cookie_string);
}
}
+256
View File
@@ -0,0 +1,256 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import DataStore from 'nedb';
import config from '../config';
import { Crontab, CrontabStatus } from '../data/cron';
import { exec, execSync, spawn } from 'child_process';
import fs from 'fs';
import cron_parser from 'cron-parser';
import { getFileContentByName } from '../config/util';
@Service()
export default class CronService {
private cronDb = new DataStore({ filename: config.cronDbFile });
constructor(@Inject('logger') private logger: winston.Logger) {
this.cronDb.loadDatabase((err) => {
if (err) throw err;
});
}
public getDb(): DataStore {
return this.cronDb;
}
public async create(payload: Crontab): Promise<Crontab> {
const tab = new Crontab(payload);
tab.created = new Date().valueOf();
tab.saved = false;
const doc = await this.insert(tab);
await this.set_crontab();
return doc;
}
public async insert(payload: Crontab): Promise<Crontab> {
return new Promise((resolve) => {
this.cronDb.insert(payload, (err, docs) => {
if (err) {
this.logger.error(err);
} else {
resolve(docs);
}
});
});
}
public async update(payload: Crontab): Promise<Crontab> {
const { _id, ...other } = payload;
const doc = await this.get(_id);
const tab = new Crontab({ ...doc, ...other });
tab.saved = false;
const newDoc = await this.updateDb(tab);
await this.set_crontab();
return newDoc;
}
public async updateDb(payload: Crontab): Promise<Crontab> {
return new Promise((resolve) => {
this.cronDb.update(
{ _id: payload._id },
payload,
{ returnUpdatedDocs: true },
(err, num, docs: any) => {
if (err) {
this.logger.error(err);
} else {
resolve(docs);
}
},
);
});
}
public async status(_id: string, stopped: boolean) {
this.cronDb.update({ _id }, { $set: { stopped, saved: false } });
}
public async remove(_id: string) {
this.cronDb.remove({ _id }, {});
await this.set_crontab();
}
public async crontabs(searchText?: string): Promise<Crontab[]> {
let query = {};
if (searchText) {
const reg = new RegExp(searchText);
query = {
$or: [
{
name: reg,
},
{
command: reg,
},
],
};
}
return new Promise((resolve) => {
this.cronDb
.find(query)
.sort({ created: 1 })
.exec((err, docs) => {
resolve(docs);
});
});
}
public async get(_id: string): Promise<Crontab> {
return new Promise((resolve) => {
this.cronDb.find({ _id }).exec((err, docs) => {
resolve(docs[0]);
});
});
}
public async run(_id: string) {
this.cronDb.find({ _id }).exec((err, docs: Crontab[]) => {
let res = docs[0];
this.logger.silly('Running job');
this.logger.silly('ID: ' + _id);
this.logger.silly('Original command: ' + res.command);
let logFile = `${config.manualLogPath}${res._id}.log`;
fs.writeFileSync(logFile, `开始执行...\n\n${new Date().toString()}\n`);
let cmdStr = res.command;
if (res.command.startsWith('js') && !res.command.endsWith('now')) {
cmdStr = `${res.command} now`;
} else if (/&& (.*) >>/.test(res.command)) {
cmdStr = res.command.match(/&& (.*) >>/)[1];
}
const cmd = spawn(cmdStr, { shell: true });
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.running } });
cmd.stdout.on('data', (data) => {
this.logger.silly(`stdout: ${data}`);
fs.appendFileSync(logFile, data);
});
cmd.stderr.on('data', (data) => {
this.logger.error(`stderr: ${data}`);
fs.appendFileSync(logFile, data);
});
cmd.on('close', (code) => {
this.logger.silly(`child process exited with code ${code}`);
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
});
cmd.on('error', (err) => {
this.logger.silly(err);
fs.appendFileSync(logFile, err.stack);
});
cmd.on('exit', (code: number, signal: any) => {
this.logger.silly(`cmd exit ${code}`);
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
fs.appendFileSync(logFile, `\n\n执行结束...`);
});
cmd.on('disconnect', () => {
this.logger.silly(`cmd disconnect`);
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
fs.appendFileSync(logFile, `\n\n连接断开...`);
});
});
}
public async disabled(_id: string) {
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.disabled } });
await this.set_crontab();
}
public async enabled(_id: string) {
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
}
public async log(_id: string) {
let logFile = `${config.manualLogPath}${_id}.log`;
return getFileContentByName(logFile);
}
private make_command(tab: Crontab) {
const crontab_job_string = `ID=${tab._id} ${tab.command}`;
return crontab_job_string;
}
private async set_crontab() {
const tabs = await this.crontabs();
var crontab_string = '';
tabs.forEach((tab) => {
if (tab.status === CrontabStatus.disabled) {
crontab_string += '# ';
crontab_string += tab.schedule;
crontab_string += ' ';
crontab_string += this.make_command(tab);
crontab_string += '\n';
} else {
crontab_string += tab.schedule;
crontab_string += ' ';
crontab_string += this.make_command(tab);
crontab_string += '\n';
}
});
this.logger.silly(crontab_string);
fs.writeFileSync(config.crontabFile, crontab_string);
execSync(`crontab ${config.crontabFile}`);
this.cronDb.update({}, { $set: { saved: true } }, { multi: true });
}
private reload_db() {
this.cronDb.loadDatabase();
}
public import_crontab() {
exec('crontab -l', (error, stdout, stderr) => {
var lines = stdout.split('\n');
var namePrefix = new Date().getTime();
lines.reverse().forEach((line, index) => {
line = line.replace(/\t+/g, ' ');
var regex = /^((\@[a-zA-Z]+\s+)|(([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+))/;
var command = line.replace(regex, '').trim();
var schedule = line.replace(command, '').trim();
var is_valid = false;
try {
is_valid = cron_parser.parseString(line).expressions.length > 0;
} catch (e) {}
if (command && schedule && is_valid) {
var name = namePrefix + '_' + index;
this.cronDb.findOne({ command, schedule }, (err, doc) => {
if (err) {
throw err;
}
if (!doc) {
this.create({ name, command, schedule });
} else {
doc.command = command;
doc.schedule = schedule;
this.update(doc);
}
});
}
});
});
}
public autosave_crontab() {
return this.set_crontab();
}
}
+50
View File
@@ -0,0 +1,50 @@
FROM node:lts-alpine
LABEL maintainer="whyour"
ARG QL_BASE_URL=https://github.com.cnpmjs.org/whyour/qinglong
ARG QL_BASE_BRANCH=master
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
LANG=zh_CN.UTF-8 \
SHELL=/bin/bash \
PS1="\u@\h:\w \$ " \
QL_DIR=/ql
WORKDIR ${QL_DIR}
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk update -f \
&& apk upgrade \
&& apk --no-cache add -f bash \
coreutils \
moreutils \
git \
curl \
tzdata \
perl \
openssl \
nginx \
python3 \
jq \
&& rm -rf /var/cache/apk/* \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& touch ~/.bashrc \
&& mkdir /run/nginx \
&& git clone -b ${QL_BASE_BRANCH} ${QL_BASE_URL} ${QL_DIR} \
&& cd ${QL_DIR} \
&& cp -f .env.example .env \
&& ln -sf ${QL_DIR}/shell/js.sh /usr/local/bin/js \
&& ln -sf ${QL_DIR}/shell/git_pull.sh /usr/local/bin/git_pull \
&& ln -sf ${QL_DIR}/shell/rm_log.sh /usr/local/bin/rm_log \
&& ln -sf ${QL_DIR}/shell/export_sharecodes.sh /usr/local/bin/export_sharecodes \
&& ln -sf ${QL_DIR}/shell/git_diy.sh /usr/local/bin/diy \
&& ln -sf ${QL_DIR}/shell/notify.sh /usr/local/bin/notify \
&& ln -sf ${QL_DIR}/shell/rebuild.sh /usr/local/bin/rebuild \
&& ln -sf ${QL_DIR}/docker/docker-entrypoint.sh /usr/local/bin/entrypoint \
&& chmod 777 ${QL_DIR}/shell/*.sh \
&& chmod 777 ${QL_DIR}/docker/*.sh \
&& npm install -g pm2 \
&& rm -rf /root/.npm \
&& yarn install --network-timeout 100000 \
&& yarn build \
&& yarn build-back \
&& yarn install --production --network-timeout 100000 \
&& yarn cache clean
ENTRYPOINT ["entrypoint"]
+64
View File
@@ -0,0 +1,64 @@
#!/bin/bash
set -e
echo -e "======================1. 检测配置文件========================\n"
[ ! -d ${QL_DIR}/config ] && mkdir -p ${QL_DIR}/config
[ ! -d ${QL_DIR}/log ] && mkdir -p ${QL_DIR}/log
[ ! -d ${QL_DIR}/db ] && mkdir -p ${QL_DIR}/db
[ ! -d ${QL_DIR}/manual_log ] && mkdir -p ${QL_DIR}/manual_log
if [ ! -s ${QL_DIR}/config/crontab.list ]
then
echo -e "检测到config配置目录下不存在crontab.list或存在但文件为空,从示例文件复制一份用于初始化...\n"
cp -fv ${QL_DIR}/sample/crontab.list.sample ${QL_DIR}/config/crontab.list
sed -i "s,MY_PATH,${QL_DIR},g" ${QL_DIR}/config/crontab.list
sed -i "s,ENV_PATH=,PATH=$PATH,g" ${QL_DIR}/config/crontab.list
fi
crond
crontab ${QL_DIR}/config/crontab.list
echo -e "成功添加定时任务...\n"
if [ ! -s ${QL_DIR}/config/cookie.sh ]; then
echo -e "检测到config配置目录下不存在cookie.sh,从示例文件复制一份用于初始化...\n"
touch ${QL_DIR}/config/cookie.sh
echo
fi
if [ ! -s ${QL_DIR}/config/config.sh ]; then
echo -e "检测到config配置目录下不存在config.sh,从示例文件复制一份用于初始化...\n"
cp -fv ${QL_DIR}/sample/config.sh.sample ${QL_DIR}/config/config.sh
echo
fi
if [ ! -s ${QL_DIR}/config/auth.json ]; then
echo -e "检测到config配置目录下不存在auth.json,从示例文件复制一份用于初始化...\n"
cp -fv ${QL_DIR}/sample/auth.json ${QL_DIR}/config/auth.json
echo
fi
if [ -s /etc/nginx/conf.d/default.conf ]; then
echo -e "检测到默认nginx配置文件,删除...\n"
rm -f /etc/nginx/conf.d/default.conf
echo
fi
cp -fv ${QL_DIR}/docker/front.conf /etc/nginx/conf.d/front.conf
echo -e "======================2. 启动nginx========================\n"
nginx -c /etc/nginx/nginx.conf
echo
echo -e "======================3. 启动控制面板========================\n"
pm2 start ${QL_DIR}/build/app.js -n panel
echo -e "控制面板启动成功...\n"
echo -e "\n容器启动成功...\n"
echo -e "\n请先访问5700端口,登录面板成功之后先手动执行一次git_pull命令...\n"
echo -e "\n如果需要启动挂机程序手动执行docker exec -it qinglong js hangup...\n"
echo -e "\n或者去cron管理搜索hangup手动执行挂机任务...\n"
if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ]; then
set -- node "$@"
fi
exec "$@"
+27
View File
@@ -0,0 +1,27 @@
upstream api {
server localhost:5600;
}
server {
listen 5700;
root /ql/dist;
ssl_session_timeout 5m;
location /api {
proxy_pass http://api;
}
gzip on;
gzip_static on;
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
gzip_proxied any;
gzip_vary on;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.0;
location / {
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"watch": ["back", ".env"],
"ext": "js,ts,json",
"ignore": ["src/**/*.spec.ts"],
"exec": "ts-node --transpile-only ./back/app.ts"
}
+75
View File
@@ -0,0 +1,75 @@
{
"private": true,
"scripts": {
"start": "umi dev",
"build": "umi build",
"build-back": "tsc -p tsconfig.back.json",
"start-back": "nodemon",
"pm2": "npm run build-back && node build/app.js",
"prepare": "umi generate tmp",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"test": "umi-test",
"test:coverage": "umi-test --coverage"
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.{js,jsx,less,md,json}": [
"prettier --write"
],
"*.ts?(x)": [
"prettier --parser=typescript --write"
]
},
"dependencies": {
"body-parser": "^1.19.0",
"celebrate": "^13.0.3",
"cors": "^2.8.5",
"cron-parser": "^3.3.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-jwt": "^6.0.0",
"got": "^11.8.2",
"jsonwebtoken": "^8.5.1",
"nedb": "^1.8.0",
"node-fetch": "^2.6.1",
"reflect-metadata": "^0.1.13",
"typedi": "^0.8.0",
"winston": "^3.3.3"
},
"devDependencies": {
"umi": "^3.3.9",
"umi-request": "^1.3.5",
"react-codemirror2": "^7.2.1",
"react-diff-viewer": "^3.1.1",
"react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0",
"qrcode.react": "^1.0.1",
"darkreader": "^4.9.27",
"codemirror": "^5.59.4",
"@ant-design/pro-layout": "^6.5.0",
"@umijs/plugin-antd": "^0.9.1",
"@types/cors": "^2.8.10",
"@types/express": "^4.17.8",
"@types/express-jwt": "^6.0.1",
"@types/jsonwebtoken": "^8.5.0",
"@types/nedb": "^1.8.11",
"@types/node": "^14.11.2",
"@types/node-fetch": "^2.5.8",
"@types/qrcode.react": "^1.0.1",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@umijs/test": "^3.3.9",
"compression-webpack-plugin": "6.1.1",
"lint-staged": "^10.0.7",
"nodemon": "^2.0.4",
"prettier": "^2.2.0",
"react": "17.x",
"react-dom": "17.x",
"ts-node": "^9.0.0",
"typescript": "^4.1.2",
"webpack": "^5.28.0",
"yorkie": "^2.0.0"
}
}
+1
View File
@@ -0,0 +1 @@
{ "username": "admin", "password": "adminadmin" }
+599
View File
@@ -0,0 +1,599 @@
## Version: v3.30.6
## Date: 2021-3-8
## Update Content: 京小兑控制变量,宠汪汪自己内部账户互助变量,环球挑战赛互助变量。
## 上面版本号中,如果第2位数字有变化,那么代表增加了新的参数,如果只有第3位数字有变化,仅代表更新了注释,没有增加新的参数,可更新可不更新
################################## 说明 ##################################
## 以下配置中,带有 export 申明的,均由LXK9301大佬定义,详见:https://gitee.com/lxk0301/jd_scripts/blob/master/githubAction.md
## 其他互助码,考虑到直接按LXK9301大佬定义的变量去填的话,一是不方便记忆,二是容易搞混,所以最终的变量将由脚本去组合,你只要按注释去填即可
## 除此之外,还额外增加了是否自动删除失效任务AutoDelCron、是否自动增加新任务AutoAddCron、删除旧日志时间RmLogDaysAgo、随机延迟启动任务RandomDelay、是否添加自定义脚本EnableExtraShell五个人性化的设置供选择
## 所有赋值等号两边不能有空格,所有的值请一律在两侧添加半角的双引号,如果变量值中含有双引号,则外侧改为一对半角的单引号。
## 所有的赋值都可以参考 “定义东东萌宠是否静默运行” 部分,在不同时间设置不同的值,以达到你想要的效果,具体判断条件如下:
## $(date "+%d") 当前的日期,如:13
## $(date "+%w") 当前是星期几,如:3
## $(date "+%H") 当前的小时数,如:23
## $(date "+%M") 当前的分钟数,如:49
## 其他date命令的更多用法,可以在命令行中输入 date --help 查看
## 判断条件 -eq -ne -gt -ge -lt -le ,具体含义可百度一下
################################## 临时屏蔽某个Cookie(选填) ##################################
## 如果某些Cookie已经失效了,但暂时还没法更新,可以使用此功能在不删除该Cookie和重新修改Cookie编号的前提下,临时屏蔽掉某些编号的Cookie
## 多个Cookie编号以半角的空格分隔,两侧一对半角双引号,使用此功能后,在运行js脚本时账户编号将发生变化
## 举例1:TempBlockCookie="2" 临时屏蔽掉Cookie2
## 举例2:TempBlockCookie="2 4" 临时屏蔽掉Cookie2和Cookie4
## 如果只是想要屏蔽某个账号不玩某些小游戏,可以参考下面 case 这个命令的例子来控制,脚本名称请去掉后缀 “.js”
## case $1 in
## jd_fruit)
## TempBlockCookie="5" # 账号5不玩东东农场
## ;;
## jd_dreamFactory | jd_jdfactory)
## TempBlockCookie="2" # 账号2不玩京喜工厂和东东工厂
## ;;
## jd_jdzz | jd_joy)
## TempBlockCookie="3 6" # 账号3、账号6不玩京东赚赚和宠汪汪
## ;;
## esac
TempBlockCookie=""
################################## 定义是否自动删除失效的脚本与定时任务(选填) ##################################
## 有的时候,某些JS脚本只在特定的时间有效,过了时间就失效了,需要自动删除失效的本地定时任务,则设置为 "true" ,否则请设置为 "false"
## 检测文件:LXK9301/jd_scripts 仓库中的 docker/crontab_list.sh,和 shylocks/Loon 仓库中的 docker/crontab_list.sh
## 当设置为 "true" 时,会自动从检测文件中读取比对删除的任务(识别以“jd_”、“jr_”、“jx_”开头的任务)
## 当设置为 "true" 时,脚本只会删除一整行失效的定时任务,不会修改其他现有任务,所以任何时候,你都可以自己调整你的crontab.list
## 当设置为 "true" 时,如果你有添加额外脚本是以“jd_”“jr_”“jx_”开头的,如检测文件中,会被删除,不是以“jd_”“jr_”“jx_”开头的任务则不受影响
AutoDelCron="true"
################################## 定义是否自动增加新的本地定时任务(选填) ##################################
## LXK9301 大佬会在有需要的时候,增加定时任务,如需要本地自动增加新的定时任务,则设置为 "true" ,否则请设置为 "false"
## 检测文件:LXK9301/jd_scripts 仓库中的 docker/crontab_list.sh,和 shylocks/Loon 仓库中的 docker/crontab_list.sh
## 当设置为 "true" 时,如果检测到检测文件中有增加新的定时任务,那么在本地也增加(识别以“jd_”、“jr_”、“jx_”开头的任务)
## 当设置为 "true" 时,会自动从检测文件新增加的任务中读取时间,该时间为北京时间
## 当设置为 "true" 时,脚本只会增加新的定时任务,不会修改其他现有任务,所以任何时候,你都可以自己调整你的crontab.list
AutoAddCron="true"
################################## 定义删除日志的时间(选填) ##################################
## 定义在运行删除旧的日志任务时,要删除多少天以前的日志,请输入正整数,不填则禁用删除日志的功能
RmLogDaysAgo="7"
################################## 定义随机延迟启动任务(选填) ##################################
## 如果任务不是必须准点运行的任务,那么给它增加一个随机延迟,由你定义最大延迟时间,单位为秒,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行
## 在crontab.list中,在每小时第0-2分、第30-31分、第59分这几个时间内启动的任务,均算作必须准点运行的任务,在启动这些任务时,即使你定义了RandomDelay,也将准点运行,不启用随机延迟
## 在crontab.list中,除掉每小时上述时间启动的任务外,其他任务在你定义了 RandomDelay 的情况下,一律启用随机延迟,但如果你按照Wiki教程给某些任务添加了 "now",那么这些任务也将无视随机延迟直接启动
RandomDelay="300"
################################## 定义User-Agent(选填) ##################################
## 自定义LXK9301大佬仓库里京东系列js脚本的User-Agent,不懂不知不会User-Agent的请不要随意填写内容,随意填写了出错概不负责
## 如需使用,请自行解除下一行注释
# export JD_USER_AGENT=""
################################## 定义通知TOKEN(选填) ##################################
## 想通过什么渠道收取通知,就填入对应渠道的值
## 1. ServerChan,教程:http://sc.ftqq.com/3.version
export PUSH_KEY=""
## 2. BARK,教程(看BARK_PUSH和BARK_SOUND的说明):https://gitee.com/lxk0301/jd_scripts/blob/master/githubAction.md
export BARK_PUSH=""
export BARK_SOUND=""
## 3. Telegram,如需使用,TG_BOT_TOKEN和TG_USER_ID必须同时赋值,教程:https://gitee.com/lxk0301/jd_scripts/blob/master/backUp/TG_PUSH.md
export TG_BOT_TOKEN=""
export TG_USER_ID=""
## 4. 钉钉,教程(看DD_BOT_TOKEN和DD_BOT_SECRET部分):https://gitee.com/lxk0301/jd_scripts/blob/master/githubAction.md
export DD_BOT_TOKEN=""
export DD_BOT_SECRET=""
## 5. iGot聚合推送,支持多方式推送,填写iGot的推送key。教程:https://gitee.com/lxk0301/jd_scripts/blob/master/githubAction.md
export IGOT_PUSH_KEY=""
## 6. Push Plus,微信扫码登录后一对一推送或一对多推送,参考文档:http://pushplus.hxtrip.com/
## 其中PUSH_PLUS_USER是一对多推送的“群组编码”(一对多推送下面->您的群组(如无则新建)->群组编码)注:(1、需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送),只填PUSH_PLUS_TOKEN默认为一对一推送
export PUSH_PLUS_TOKEN=""
export PUSH_PLUS_USER=""
## 7. 企业微信机器人消息推送 webhook 后面的 key,文档:https://work.weixin.qq.com/api/doc/90000/90136/91770
export QYWX_KEY=""
## 8. 企业微信应用消息推送的值,教程:https://note.youdao.com/ynoteshare1/index.html?id=351e08a72378206f9dd64d2281e9b83b&type=note
## 依次填上corpid的值,corpsecret的值,touser的值,agentid,media_id的值,注意用,号隔开,例如:"wwcff56746d9adwers,B-791548lnzXBE6_BWfxdf3kSTMJr9vFEPKAbh6WERQ,mingcheng,1000001,2COXgjH2UIfERF2zxrtUOKgQ9XklUqMdGSWLBoW_lSDAdafat"
export QYWX_AM=""
################################## 定义每日签到的通知形式(选填) ##################################
## js脚本每日签到提供3种通知方式,分别为:
## 关闭通知,那么请在下方填入0
## 简洁通知,那么请在下方填入1,其效果见:https://github.com/LXK9301/jd_scripts/blob/master/icon/bean_sign_simple.jpg
## 原始通知,那么请在下方填入2,如果不填也默认为2,内容比较长,这也是默认通知方式
NotifyBeanSign=""
################################## 定义每日签到每个接口间的延迟时间(选填) ##################################
## 默认每个签到接口并发无延迟,如需要依次进行每个接口,请自定义延迟时间,单位为毫秒,延迟作用于每个签到接口, 如填入延迟则切换顺序签到(耗时较长)
export JD_BEAN_STOP=""
################################## 定义东东农场互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MyFruit1=""
MyFruit2=""
MyFruit3=""
MyFruit4=""
MyFruit5=""
MyFruit6=""
MyFruitA=""
MyFruitB=""
ForOtherFruit1=""
ForOtherFruit2=""
ForOtherFruit3=""
ForOtherFruit4=""
ForOtherFruit5=""
ForOtherFruit6=""
################################## 定义东东萌宠互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MyPet1=""
MyPet2=""
MyPet3=""
MyPet4=""
MyPet5=""
MyPet6=""
MyPetA=""
MyPetB=""
ForOtherPet1=""
ForOtherPet2=""
ForOtherPet3=""
ForOtherPet4=""
ForOtherPet5=""
ForOtherPet6=""
################################## 定义种豆得豆互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MyBean1=""
MyBean2=""
MyBean3=""
MyBean4=""
MyBean5=""
MyBean6=""
MyBeanA=""
MyBeanB=""
ForOtherBean1=""
ForOtherBean2=""
ForOtherBean3=""
ForOtherBean4=""
ForOtherBean5=""
ForOtherBean6=""
################################## 定义京喜工厂互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MyDreamFactory1=""
MyDreamFactory2=""
MyDreamFactory3=""
MyDreamFactory4=""
MyDreamFactory5=""
MyDreamFactory6=""
MyDreamFactoryA=""
MyDreamFactoryB=""
ForOtherDreamFactory1=""
ForOtherDreamFactory2=""
ForOtherDreamFactory3=""
ForOtherDreamFactory4=""
ForOtherDreamFactory5=""
ForOtherDreamFactory6=""
################################## 定义东东工厂互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MyJdFactory1=""
MyJdFactory2=""
MyJdFactory3=""
MyJdFactory4=""
MyJdFactory5=""
MyJdFactory6=""
MyJdFactoryA=""
MyJdFactoryB=""
ForOtherJdFactory1=""
ForOtherJdFactory2=""
ForOtherJdFactory3=""
ForOtherJdFactory4=""
ForOtherJdFactory5=""
ForOtherJdFactory6=""
################################## 定义京东赚赚互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MyJdzz1=""
MyJdzz2=""
MyJdzz3=""
MyJdzz4=""
MyJdzz5=""
MyJdzz6=""
MyJdzzA=""
MyJdzzB=""
ForOtherJdzz1=""
ForOtherJdzz2=""
ForOtherJdzz3=""
ForOtherJdzz4=""
ForOtherJdzz5=""
ForOtherJdzz6=""
################################## 定义疯狂的JOY互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MyJoy1=""
MyJoy2=""
MyJoy3=""
MyJoy4=""
MyJoy5=""
MyJoy6=""
MyJoyA=""
MyJoyB=""
ForOtherJoy1=""
ForOtherJoy2=""
ForOtherJoy3=""
ForOtherJoy4=""
ForOtherJoy5=""
ForOtherJoy6=""
################################## 定义京喜农场互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
## 京喜农场助力码为 JSON 格式因此使用单引号,json 格式如下
## {"smp":"22bdadsfaadsfadse8a","active":"jdnc_1_btorange210113_2","joinnum":"1"}
## 助力码获取可以通过 js jd_get_share_code now 命令获取
## 注意:京喜农场 种植种子发生变化的时候,互助码也会变!!
MyJxnc1=''
MyJxnc2=''
MyJxnc3=''
MyJxnc4=''
MyJxnc5=''
MyJxnc6=''
MyJxncA=''
MyJxncB=''
ForOtherJxnc1=""
ForOtherJxnc2=""
ForOtherJxnc3=""
ForOtherJxnc4=""
ForOtherJxnc5=""
ForOtherJxnc6=""
################################## 定义京喜农场TOKEN(选填) ##################################
## 如果某个Cookie的账号种植的是app种子,则必须填入有效的TOKEN;而种植非app种子则不需要TOKEN
## TOKEN的形式:{"farm_jstoken":"749a90f871adsfads8ffda7bf3b1576760","timestamp":"1610165423873","phoneid":"42c7e3dadfadsfdsaac-18f0e4f4a0cf"}
## 因TOKEN中带有双引号,因此,变量值两侧必须由一对单引号引起来
## TOKEN如何获取请阅读以下文件的注释:https://github.com/LXK9301/jd_scripts/blob/master/jd_jxnc.js
TokenJxnc1=''
TokenJxnc2=''
TokenJxnc3=''
TokenJxnc4=''
TokenJxnc5=''
TokenJxnc6=''
################################## 定义口袋书店互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MyBookShop1=""
MyBookShop2=""
MyBookShop3=""
MyBookShop4=""
MyBookShop5=""
MyBookShop6=""
MyBookShopA=""
MyBookShopB=""
ForOtherBookShop1=""
ForOtherBookShop2=""
ForOtherBookShop3=""
ForOtherBookShop4=""
ForOtherBookShop5=""
ForOtherBookShop6=""
################################## 定义签到领现金互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MyCash1=""
MyCash2=""
MyCash3=""
MyCash4=""
MyCash5=""
MyCash6=""
MyCashA=""
MyCashB=""
ForOtherCash1=""
ForOtherCash2=""
ForOtherCash3=""
ForOtherCash4=""
ForOtherCash5=""
ForOtherCash6=""
################################## 定义闪购盲盒互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MySgmh1=""
MySgmh2=""
MySgmh3=""
MySgmh4=""
MySgmh5=""
MySgmh6=""
MySgmhA=""
MySgmhB=""
ForOtherSgmh1=""
ForOtherSgmh2=""
ForOtherSgmh3=""
ForOtherSgmh4=""
ForOtherSgmh5=""
ForOtherSgmh6=""
################################## 定义京东环球挑战赛活动互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
## 仅2021年2月22日(含)前有效
MyGLOBAL1=""
MyGLOBAL2=""
MyGLOBAL3=""
MyGLOBAL4=""
MyGLOBAL5=""
MyGLOBAL6=""
MyGLOBALA=""
MyGLOBALB=""
ForOtherGLOBAL1=""
ForOtherGLOBAL2=""
ForOtherGLOBAL3=""
ForOtherGLOBAL4=""
ForOtherGLOBAL5=""
ForOtherGLOBAL6=""
################################## 定义惊喜财富岛互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MyJdcfd1=""
MyJdcfd2=""
MyJdcfd3=""
MyJdcfd4=""
MyJdcfd5=""
MyJdcfd6=""
MyJdcfdA=""
MyJdcfdB=""
ForOtherJdcfd1=""
ForOtherJdcfd2=""
ForOtherJdcfd3=""
ForOtherJdcfd4=""
ForOtherJdcfd5=""
ForOtherJdcfd6=""
################################## 定义东东超市蓝币兑换数量(选填) ##################################
## 东东超市蓝币兑换,可用值包括:
## 一、0:表示不兑换京豆,这也是js脚本的默认值
## 二、20:表示兑换20个京豆
## 三、1000:表示兑换1000个京豆
## 四、可兑换清单的商品名称,输入能跟唯一识别出来的关键词即可,比如:MARKET_COIN_TO_BEANS="抽纸"
## 注意:有些比较贵的实物商品京东只是展示出来忽悠人的,即使你零点用脚本去抢,也会提示没有或提示已下架
export MARKET_COIN_TO_BEANS="0"
################################## 定义东东超市蓝币成功兑换奖品是否静默运行(选填) ##################################
## 默认 "false" 关闭(即:奖品兑换成功后会发出通知提示),如需要静默运行不发出通知,请改为 "true"
export MARKET_REWARD_NOTIFY=""
################################## 定义东东超市是否自动升级商品和货架(选填) ##################################
## 升级顺序:解锁升级商品、升级货架,默认 "true" 自动升级,如需关闭自动升级,请改为 "false"
export SUPERMARKET_UPGRADE=""
################################## 定义东东超市是否自动更换商圈(选填) ##################################
## 小于对方300热力值自动更换商圈队伍,默认 "true" 自动更换,如不想更换商圈,请改为 "false"
## 目前已无此功能,申明了也无效
# export BUSINESS_CIRCLE_JUMP=""
################################## 定义东东超市是否自动使用金币去抽奖(选填) ##################################
## 是否用金币去抽奖,默认 "false" 关闭,如需开启,请修改为 "true"
export SUPERMARKET_LOTTERY=""
################################## 定义东东超市是否自动参加PK队伍(选填) ##################################
## 是否每次PK活动参加脚本作者创建的PK队伍,"true" 表示参加,"false" 表示不参加,默认为 "true"
export JOIN_PK_TEAM=""
################################## 定义东东农场是否静默运行(选填) ##################################
## 默认为 "false",不静默,发送推送通知消息,如不想收到通知,请修改为 "true"
## 如果你不想完全关闭或者完全开启通知,只想在特定的时间发送通知,可以参考下面的“定义东东萌宠是否静默运行”部分,设定几个if判断条件
export FRUIT_NOTIFY_CONTROL=""
################################## 定义东东农场是否使用水滴换豆卡(选填) ##################################
## 如果出现限时活动时100g水换20豆,此时比浇水划算,"true" 表示换豆(不浇水),"false" 表示不换豆(继续浇水),默认是"false"
## 如需切换为换豆(不浇水),请修改为 "true"
export FRUIT_BEAN_CARD=""
################################## 定义宠汪汪喂食克数(选填) ##################################
## 你期望的宠汪汪每次喂食克数,只能填入10、20、40、80,默认为10
## 如实际持有食物量小于所设置的克数,脚本会自动降一档,直到降无可降
## 具体情况请自行在宠汪汪游戏中去查阅攻略
export JOY_FEED_COUNT=""
################################## 定义宠汪汪兑换京豆数量(选填) ##################################
## 目前的可用值包括:0、20、500、1000,其中0表示为不自动兑换京豆,如不设置,将默认为"20"
## 不同等级可兑换不同数量的京豆,详情请见宠汪汪游戏中兑换京豆选项
## 500、1000的京豆每天有总量限制,设置了并且你也有足够积分时,也并不代表就一定能抢到
export JD_JOY_REWARD_NAME=""
################################## 定义宠汪汪兑换京豆是否静默运行(选填) ##################################
## 默认为 "false",在成功兑换京豆时将发送推送通知消息(失败不发送),如想要静默不发送通知,请修改为 "true"
export JD_JOY_REWARD_NOTIFY=""
################################## 定义宠汪汪是否自动给好友的汪汪喂食(选填) ##################################
## 默认 "false" 不会自动给好友的汪汪喂食,如想自动喂食,请改成 "true"
export JOY_HELP_FEED=""
################################## 定义宠汪汪是否自动报名宠物赛跑(选填) ##################################
## 默认 "true" 参加宠物赛跑,如需关闭,请改成 "false"
export JOY_RUN_FLAG=""
################################## 定义宠汪汪参加比赛类型(选填) ##################################
## 当JOY_RUN_FLAG不设置或设置为 "true" 时生效
## 可选值:2,10,50,其他值不可以。其中2代表参加双人PK赛,10代表参加10人突围赛,50代表参加50人挑战赛,不填时默认为2
## 各个账号间请使用 & 分隔,比如:JOY_TEAM_LEVEL="2&2&50&10"
## 如果你有5个账号但只写了四个数字,那么第5个账号将默认参加2人赛,账号如果更多,与此类似
export JOY_TEAM_LEVEL=""
################################## 定义宠汪汪赛跑获胜后是否推送通知(选填) ##################################
## 控制jd_joy.js脚本宠汪汪赛跑获胜后是否推送通知,"false" 为否(不推送通知消息),"true" 为是(即:发送推送通知消息),默认为 "true"
export JOY_RUN_NOTIFY=""
################################## 定义摇钱树是否自动将金果卖出变成金币(选填) ##################################
## 金币有时效,默认为 "false",不卖出金果为金币,如想希望自动卖出,请修改为 "true"
export MONEY_TREE_SELL_FRUIT=""
################################## 定义东东萌宠是否静默运行(选填) ##################################
## 默认 "false"(不静默,发送推送通知消息),如想静默请修改为 true
## 每次执行脚本通知太频繁了,改成只在周三和周六中午那一次运行时发送通知提醒
## 除掉上述提及时间之外,均设置为 true,静默不发通知
## 特别说明:针对北京时间有效。
if [ $(date "+%w") -eq 6 ] && [ $(date "+%H") -ge 9 ] && [ $(date "+%H") -lt 14 ]; then
export PET_NOTIFY_CONTROL="false"
elif [ $(date "+%w") -eq 3 ] && [ $(date "+%H") -ge 9 ] && [ $(date "+%H") -lt 14 ]; then
export PET_NOTIFY_CONTROL="false"
else
export PET_NOTIFY_CONTROL="true"
fi
################################## 定义京喜工厂控制哪个京东账号不运行此脚本(选填) ##################################
## 输入"1"代表第一个京东账号不运行,多个使用 & 连接,例:"1&3" 代表账号1和账号3不运行京喜工厂脚本,注:输入"0",代表全部账号不运行京喜工厂脚本
## 如果使用了 “临时屏蔽某个Cookie” TempBlockCookie 功能,编号会发生变化
export DREAMFACTORY_FORBID_ACCOUNT=""
################################## 定义东东工厂控制哪个京东账号不运行此脚本(选填) ##################################
## 输入"1"代表第一个京东账号不运行,多个使用 & 连接,例:"1&3" 代表账号1和账号3不运行东东工厂脚本,注:输入"0",代表全部账号不运行东东工厂脚本
## 如果使用了 “临时屏蔽某个Cookie” TempBlockCookie 功能,编号会发生变化
export JDFACTORY_FORBID_ACCOUNT=""
################################## 定义东东工厂心仪的商品(选填) ##################################
## 只有在满足以下条件时,才自动投入电力:一是存储的电力满足生产商品所需的电力,二是心仪的商品有库存,如果没有输入心仪的商品,那么当前你正在生产的商品视作心仪的商品
## 如果你看不懂上面的话,请去东东工厂游戏中查阅攻略
## 心仪的商品请输入商品的全称或能唯一识别出该商品的关键字
export FACTORAY_WANTPRODUCT_NAME=""
################################## 定义京喜农场通知级别(选填) ##################################
## 可用值: 0(不通知); 1(本次获得水滴>0); 2(任务执行); 3(任务执行+未种植种子),默认为"3"
export JXNC_NOTIFY_LEVEL="3"
################################## 定义取关参数(选填) ##################################
## jd_unsubscribe这个任务是用来取关每天做任务关注的商品和店铺,默认在每次运行时取关20个商品和20个店铺
## 如果取关数量不够,可以根据情况增加,还可以设置 jdUnsubscribeStopGoods 和 jdUnsubscribeStopShop
## 商品取关数量
goodPageSize=""
## 店铺取关数量
shopPageSize=""
## 遇到此商品不再取关此商品以及它后面的商品,需去商品详情页长按拷贝商品信息
jdUnsubscribeStopGoods=""
## 遇到此店铺不再取关此店铺以及它后面的店铺,请从头开始输入店铺名称
jdUnsubscribeStopShop=""
################################## 疯狂的JOY(选填) ##################################
## 疯狂的JOY循环助力,"true" 表示循环助力,"false" 表示不循环助力,默认 "false"
export JDJOY_HELPSELF=""
## 疯狂的JOY京豆兑换,目前最小值为500/1000京豆,默认为 "0" 不开启京豆兑换
export JDJOY_APPLYJDBEAN=""
## 疯狂的JOY自动购买什么等级的JOY,如需要使用请自行解除注释
# export BUY_JOY_LEVEL=""
################################## 定义是否自动加购物车(选填) ##################################
## 口袋书店和东东小窝有些任务需要将商品加进购物车才能完成,默认 "false" 不做这些任务,如想做,请设置为 "true"
export PURCHASE_SHOPS=""
################################## 定义京喜财富岛通知(选填) ##################################
## 输入true为通知,默认 "false",不填则为不通知
export CFD_NOTIFY_CONTROL=""
################################## 定义京小兑是否自动把抽奖卷兑换为兑币,(选填) ##################################
## 输入true为自动兑换,不填则为不兑换
export JD_JXD_EXCHANGE=""
################################## 定义宠汪汪是否开启内部互助,(选填) ##################################
## 输入true为开启内部互助,不填默认关闭
export JOY_RUN_HELP_MYSELF=""
################################## Telegram 代理(选填) ##################################
## Telegram 代理的 IP,代理类型为 http,比如你代理是 http://127.0.0.1:1080,则填写 "127.0.0.1"
## 如需使用,请自行解除下一行的注释
# export TG_PROXY_HOST=""
## Telegram 代理的端口,代理类型为 http,比如你代理是 http://127.0.0.1:1080,则填写 "1080"
## 如需使用,请自行解除下一行的注释
# export TG_PROXY_PORT=""
################################## 是否添加DIY脚本(选填) ##################################
## 如果你自己会写shell脚本,并且希望在每次git_pull.sh这个脚本运行时,额外运行你的DIY脚本,请赋值为 "true"
## 同时,请务必将你的脚本命名为 extra.sh (只能叫这个文件名),放在 config 目录下
## 仓库下已经上传extra.sh模板,如果你想使用;可以参考本仓库下 extra.sh 文件;地址:https://raw.githubusercontent.com/dockere/jd-base/master/sample/diy.sh
EnableExtraShell=""
################################## 互助码填法示例 ##################################
## **互助码是填在My系列变量中的,ForOther系统变量中只要填入My系列的变量名即可,按注释中的例子拼接,以东东农场为例,如下所示。**
## **实际上东东农场一个账号只能给别人助力3次,我多写的话,只有前几个会被助力。但如果前面的账号获得的助力次数已经达到上限了,那么还是会尝试继续给余下的账号助力,所以多填也是有意义的。**
## **ForOther系列变量必须从1开始编号,依次编下去。**
# MyFruit1="e6e04602d5e343258873af1651b603ec" # 这是Cookie1这个账号的互助码
# MyFruit2="52801b06ce2a462f95e1d59d7e856ef4" # 这是Cookie2这个账号的互助码
# MyFruit3="e2fd1311229146cc9507528d0b054da8" # 这是Cookie3这个账号的互助码
# MyFruit4="6dc9461f662d490991a31b798f624128" # 这是Cookie4这个账号的互助码
# MyFruit5="30f29addd75d44e88fb452bbfe9f2110" # 这是Cookie5这个账号的互助码
# MyFruit6="1d02fc9e0e574b4fa928e84cb1c5e70b" # 这是Cookie6这个账号的互助码
# MyFruitA="5bc73a365ff74a559bdee785ea97fcc5" # 这是我和别人交换互助,另外一个用户A的互助码
# MyFruitB="6d402dcfae1043fba7b519e0d6579a6f" # 这是我和别人交换互助,另外一个用户B的互助码
# MyFruitC="5efc7fdbb8e0436f8694c4c393359576" # 这是我和别人交换互助,另外一个用户C的互助码
# ForOtherFruit1="${MyFruit2}@${MyFruitB}@${MyFruit4}" # Cookie1这个账号助力Cookie2的账号的账号、Cookie4的账号以及用户B
# ForOtherFruit2="${MyFruit1}@${MyFruitA}@${MyFruit4}" # Cookie2这个账号助力Cookie1的账号的账号、Cookie4的账号以及用户A
# ForOtherFruit3="${MyFruit1}@${MyFruit2}@${MyFruitC}@${MyFruit4}@${MyFruitA}@${MyFruit6}" # 解释同上,东东农场实际上只能助力3次
# ForOtherFruit4="${MyFruit1}@${MyFruit2}@${MyFruit3}@${MyFruitC}@${MyFruit6}@${MyFruitA}" # 解释同上,东东农场实际上只能助力3次
# ForOtherFruit5="${MyFruit1}@${MyFruit2}@${MyFruit3}@${MyFruitB}@${MyFruit4}@${MyFruit6}@${MyFruitC}@${MyFruitA}"
# ForOtherFruit6="${MyFruit1}@${MyFruit2}@${MyFruit3}@${MyFruitA}@${MyFruit4}@${MyFruit5}@${MyFruitC}"
+27
View File
@@ -0,0 +1,27 @@
# 更新shell及面板代码
55 2,13 * * * git_pull >> ${QL_DIR}/log/git_pull.log 2>&1
# 更新你需要的仓库的代码
# diy test作者 test仓库 test路径1|test路径2 黑名单1文件名|黑名单2文件名
33 * * * * diy whyour hundun "quanx/jx|quanx/jd" tokens >> ${QL_DIR}/log/diy_pull.log 2>&1
31 * * * * diy monk-coder dust i-chenzhe >> ${QL_DIR}/log/diy_pull.log 2>&1
# 重新build面板
# 30 7 */7 * * rebuild >> ${QL_DIR}/log/rebuild.log 2>&1
# 删除 RmLogDaysAgo 指定天数以前的旧日志,本行为不记录日志
57 13 * * * rm_log >/dev/null 2>&1
# 导出所有互助码清单,日志在log/export_sharecodes下
48 5 * * * export_sharecodes
# 重启挂机脚本
# 33 3 * * * js hangup
# 重置密码
# 33 6 */7 * * js resetpwd
# 运行所有脚本(慎用)
# 33 9 */7 * * js runall
# 其他定时任务
Executable
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
get_token() {
token=$(cat $AuthConf | jq -r .token)
}
get_json_value() {
local json=$1
local key=$2
if [[ -z "$3" ]]; then
local num=1
else
local num=$3
fi
local value=$(echo "${json}" | awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/'${key}'\042/){print $(i+1)}}}' | tr -d '"' | sed -n ${num}p)
echo ${value}
}
add_cron_api() {
local currentTimeStamp=$(date +%s)
if [ $# -eq 1 ]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}')
else
local schedule=$1
local command=$2
local name=$3
fi
local api=$(curl "http://localhost:5600/api/crons?t=$currentTimeStamp" \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://localhost:5700" \
-H "Referer: http://localhost:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \
--compressed)
code=$(echo $api | jq -r .code)
if [[ $code == 200 ]]; then
echo -e "$name 添加成功"
else
echo -e "$name 添加失败"
fi
}
del_cron_api() {
local id=$1
local currentTimeStamp=$(date +%s)
local api=$(curl "http://localhost:5600/api/crons/$id?t=$currentTimeStamp" \
-X 'DELETE' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://localhost:5700" \
-H "Referer: http://localhost:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7")
code=$(echo $api | jq -r .code)
if [[ $code == 200 ]]; then
echo -e "$name 删除成功"
else
echo -e "$name 删除失败"
fi
}
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env bash
## 路径、环境判断
ShellDir=${QL_DIR:-$(cd $(dirname $0); pwd)}
LogDir=${ShellDir}/log
ConfigDir=${ShellDir}/config
FileConf=${ConfigDir}/config.sh
CookieConf=${ConfigDir}/cookie.sh
[[ ${ANDROID_RUNTIME_ROOT}${ANDROID_ROOT} ]] && Opt="P" || Opt="E"
Tips="从日志中未找到任何互助码"
## 所有有互助码的活动,只需要把脚本名称去掉前缀 jd_ 后列在 Name1 中,将其中文名称列在 Name2 中,对应 config.sh 中互助码后缀列在 Name3 中即可。
## Name1、Name2 和 Name3 中的三个名称必须一一对应。
Name1=(fruit pet plantBean dreamFactory jdfactory crazy_joy jdzz jxnc bookshop cash sgmh cfd global)
Name2=(东东农场 东东萌宠 京东种豆得豆 京喜工厂 东东工厂 crazyJoy任务 京东赚赚 京喜农场 口袋书店 签到领现金 闪购盲盒 京喜财富岛 环球挑战赛)
Name3=(Fruit Pet Bean DreamFactory JdFactory Joy Jdzz Jxnc BookShop Cash Sgmh Cfd Global)
## 导入 config.sh
function Import_Conf {
if [ -f ${FileConf} ]
then
. ${CookieConf}
. ${FileConf}
if [ ! -s ${CookieConf} ]; then
echo -e "请先在Cookie管理中添加一条Cookie...\n"
exit 1
fi
else
echo -e "配置文件 ${FileConf} 不存在,请先按教程配置好该文件...\n"
exit 1
fi
}
## 用户数量 UserSum
function Count_UserSum {
UserSum=$(awk '{print NR}' ${CookieConf} | tail -n1)
}
## 导出互助码的通用程序
function Cat_Scodes {
if [ -d ${LogDir}/jd_$1 ] && [[ $(ls ${LogDir}/jd_$1) != "" ]]; then
cd ${LogDir}/jd_$1
## 导出助力码变量(My)
for log in $(ls -r); do
case $# in
2)
codes=$(cat ${log} | grep -${Opt} "开始【京东账号|您的(好友)?助力码为" | uniq | perl -0777 -pe "{s|\*||g; s|开始||g; s|\n您的(好友)?助力码为(:)?:?|:|g; s|,.+||g}" | sed -r "s/【京东账号/My$2/;s/】.*?:/='/;s/】.*?/='/;s/$/'/;s/\(每次运行都变化,不影响\)//")
;;
3)
codes=$(grep -${Opt} $3 ${log} | uniq | sed -r "s/【京东账号/My$2/;s/(.*?】/='/;s/$/'/")
;;
esac
if [[ ${codes} ]]; then
## 添加判断,若未找到该用户互助码,则设置为空值
for ((user_num=1;user_num<=${UserSum};user_num++)); do
echo -e "${codes}" | grep -${Opt}q "My$2${user_num}="
if [ $? -eq 1 ]; then
if [ $user_num == 1 ]; then
codes=$(echo "${codes}" | sed -r "1i My${2}1=''")
else
codes=$(echo "${codes}" | sed -r "/My$2$(expr ${user_num} - 1)=/a\My$2${user_num}=''")
fi
fi
done
break
fi
done
## 导出为他人助力变量(ForOther)
if [[ ${codes} ]]; then
help_code=""
for ((user_num=1;user_num<=${UserSum};user_num++)); do
echo -e "${codes}" | grep -${Opt}q "My$2${user_num}=''"
if [ $? -eq 1 ]; then
help_code=${help_code}"\${My"$2${user_num}"}@"
fi
done
## 生成互助规则模板
for_other_codes=""
case $HelpType in
0) ### 统一优先级助力模板
new_code=$(echo ${help_code} | sed "s/@$//")
for ((user_num=1;user_num<=${UserSum};user_num++)); do
if [ $user_num == 1 ]; then
for_other_codes=${for_other_codes}"ForOther"$2${user_num}"=\""${new_code}"\"\n"
else
for_other_codes=${for_other_codes}"ForOther"$2${user_num}"=\"\${ForOther"${2}1"}\"\n"
fi
done
;;
1) ### 均匀助力模板
for ((user_num=1;user_num<=${UserSum};user_num++)); do
echo ${help_code} | grep "\${My"$2${user_num}"}@" > /dev/null
if [ $? -eq 0 ]; then
left_str=$(echo ${help_code} | sed "s/\${My$2${user_num}}@/ /g" | awk '{print $1}')
right_str=$(echo ${help_code} | sed "s/\${My$2${user_num}}@/ /g" | awk '{print $2}')
mark="\${My$2${user_num}}@"
else
left_str=$(echo ${help_code} | sed "s/${mark}/ /g" | awk '{print $1}')${mark}
right_str=$(echo ${help_code} | sed "s/${mark}/ /g" | awk '{print $2}')
fi
new_code=$(echo ${right_str}${left_str} | sed "s/@$//")
for_other_codes=${for_other_codes}"ForOther"$2${user_num}"=\""${new_code}"\"\n"
done
;;
*) ### 普通优先级助力模板
for ((user_num=1;user_num<=${UserSum};user_num++)); do
new_code=$(echo ${help_code} | sed "s/\${My"$2${user_num}"}@//;s/@$//")
for_other_codes=${for_other_codes}"ForOther"$2${user_num}"=\""${new_code}"\"\n"
done
;;
esac
echo -e "${codes}\n\n${for_other_codes}" | sed s/[[:space:]]//g
else
echo ${Tips}
fi
else
echo "未运行过 jd_$1 脚本,未产生日志"
fi
}
## 汇总
function Cat_All {
echo -e "\n从最后一个日志提取互助码,受日志内容影响,仅供参考。"
for ((i=0; i<${#Name1[*]}; i++)); do
echo -e "\n${Name2[i]}:"
[[ $(Cat_Scodes "${Name1[i]}" "${Name3[i]}" "的${Name2[i]}好友互助码") == ${Tips} ]] && Cat_Scodes "${Name1[i]}" "${Name3[i]}" || Cat_Scodes "${Name1[i]}" "${Name3[i]}" "的${Name2[i]}好友互助码"
done
}
## 执行并写入日志
LogTime=$(date "+%Y-%m-%d-%H-%M-%S")
LogFile="${LogDir}/export_sharecodes/${LogTime}.log"
[ ! -d "${LogDir}/export_sharecodes" ] && mkdir -p ${LogDir}/export_sharecodes
Import_Conf && Count_UserSum && Cat_All | perl -pe "{s|京东种豆|种豆|; s|crazyJoy任务|疯狂的JOY|}" | tee ${LogFile}
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env bash
#author:spark thanks to: https://github.com/sparkssssssss/scripts
ShellDir=${QL_DIR:-$(
cd $(dirname $0)
pwd
)}
[[ $QL_DIR ]] && ShellJs=js
ConfigDir=$ShellDir/config
ListCronCurrent=$ConfigDir/crontab.list
AuthConf=$ConfigDir/auth.json
declare -A BlackListDict
url=$1
path=$2
blackword=$3
if [[ $# -lt 2 ]] || [[ $# -gt 4 ]]; then
echo 'Desc: 用户拉取指定用户的指定仓储'
echo 'Usage: diy <repourl> <path> <blacklist>'
echo 'repourl 仓储地址'
echo 'path 需要下载脚本的指定目录,多个目录 | 分割 path1 | path2'
echo 'blacklist 需要排除的脚本名,多个名称 | 分割 blacklist1 | blacklist2'
exit 0
fi
diyscriptsdir=/ql/diyscripts
mkdir -p ${diyscriptsdir}
urlTmp=${url%*/}
repoTmp=${urlTmp##*/}
repo=${repoTmp%.*}
tmp=${url%/*}
authorTmp1=${tmp##*/}
authorTmp2=${authorTmp1##*:}
author=${authorTmp2##*.}
if [ ! -d "$diyscriptsdir/${author}_${repo}" ]; then
echo -e "${author}本地仓库不存在,从远程拉取ing..."
cd ${diyscriptsdir} && git clone $url ${author}_${repo}
gitpullstatus=$?
[ $gitpullstatus -eq 0 ] && echo -e "${author}本地仓库拉取完毕"
[ $gitpullstatus -ne 0 ] && echo -e "${author}本地仓库拉取失败,请检查!" && exit 0
else
cd ${diyscriptsdir}/${author}_${repo}
branch=$(git symbolic-ref --short -q HEAD)
git fetch --all
git reset --hard origin/$branch
git pull
gitpullstatus=$?
fi
rand() {
min=$1
max=$(($2 - $min + 1))
num=$(cat /proc/sys/kernel/random/uuid | cksum | awk -F ' ' '{print $1}')
echo $(($num % $max + $min))
}
addnewcron() {
addname=""
cd ${diyscriptsdir}/${author}_${repo}
express=$(find . -name "*.js")
if [ $path ]; then
express=$(find . -name "*.js" | egrep $path)
fi
if [ $blackword ]; then
express=$(find . -name "*.js" | egrep -v $blackword | egrep $path)
fi
for js in $express; do
base=$(basename $js)
croname=$(echo "${author}_$base" | awk -F\. '{print $1}')
script_date=$(cat $js | grep ^[0-9] | awk '{print $1,$2,$3,$4,$5}' | egrep -v "[a-zA-Z]|:|\." | sort | uniq | head -n 1)
[ -z "${script_date}" ] && script_date=$(cat $js | grep -Eo "([0-9]+|\*|[0-9]+[,-].*) ([0-9]+|\*|[0-9]+[,-].*) ([0-9]+|\*|[0-9]+[,-].*) ([0-9]+|\*|[0-9]+[,-].*) ([0-9]+|\*|[0-9][,-].*)" | sort | uniq | head -n 1)
[ -z "${script_date}" ] && cron_min=$(rand 1 59) && cron_hour=$(rand 7 9) && script_date="${cron_min} ${cron_hour} * * *"
local oldCron=$(grep -c -w "$croname" "$ListCronCurrent")
if [[ oldCron -eq 0 ]]; then
local name=$(cat "$js" | grep -E "new Env\(" | perl -pe "s|(^.+)new Env\(\'*\"*(.+?)'*\"*\).+|\2|")
add_cron_api "$script_date" "js $croname" "$name"
addname="${addname}\n${croname}"
echo -e "添加了新的脚本${croname}."
fi
if [ ! -f "/ql/scripts/${author}_$base" ]; then
\cp $js /ql/scripts/${author}_$base
else
change=$(diff $js /ql/scripts/${author}_$base)
[ -n "${change}" ] && \cp $js /ql/scripts/${author}_$base && echo -e "${author}_$base 脚本更新了."
fi
done
[ "$addname" != "" ] && notify "新增 ${author} 自定义脚本" "${addname}"
}
delcron() {
delname=""
cronfiles=$(grep "$author" /ql/config/crontab.list | grep -v "^#" | perl -pe "s|.*ID=(.*) js (${author}_.*)\.*|\1:\2|")
for filename in $cronfiles; do
local id=$(echo "$1" | awk -F ":" '{print $1}')
local name=$(echo "$1" | awk -F ":" '{print $2}')
hasFile=$(cd ${diyscriptsdir}/${author}_${repo} && find . -name "$filename.js" | wc -l)
if [[ $hasFile != 0 ]]; then
del_cron_api "$id"
echo -e "删除失效脚本${name}."
delname="${delname}\n${author}_${filename}"
fi
done
[ "$delname" != "" ] && notify "删除 ${author} 失效脚本" "${delname}"
}
. $ShellDir/shell/api.sh
get_token
if [[ ${gitpullstatus} -eq 0 ]]; then
addnewcron
delcron
else
echo -e "$author 仓库更新失败了."
notify "自定义仓库更新失败" "$author"
fi
exit 0
+297
View File
@@ -0,0 +1,297 @@
#!/usr/bin/env bash
ShellDir=${QL_DIR:-$(
cd $(dirname $0)
pwd
)}
[[ $QL_DIR ]] && ShellJs=js
LogDir=$ShellDir/log
DbDir=$ShellDir/db
ManualLogDir=$ShellDir/manual_log
ScriptsDir=$ShellDir/scripts
ConfigDir=$ShellDir/config
FileConf=$ConfigDir/config.sh
CookieConf=$ConfigDir/cookie.sh
AuthConf=$ConfigDir/auth.json
ExtraShell=$ConfigDir/extra.sh
FileConfSample=$ShellDir/sample/config.sh.sample
ListCronSample=$ShellDir/sample/crontab.list.sample
ListCronCurrent=$ConfigDir/crontab.list
ListCronRemote=$ScriptsDir/docker/crontab_list.sh
ListCurrentTask=$LogDir/task.list
ListRemoteTask=$LogDir/js.list
ListJsAdd=$LogDir/js-add.list
ListJsDrop=$LogDir/js-drop.list
ContentVersion=$ShellDir/version
ContentNewTask=$ShellDir/new_task
ContentDropTask=$ShellDir/drop_task
SendVersion=$ShellDir/send_version
isTermux=$ANDROID_RUNTIME_ROOT$ANDROID_ROOT
ShellURL=https://ghproxy.com/https://github.com/whyour/qinglong
ScriptsURL=https://ghproxy.com/https://github.com/gossh520/jd_scripts
Import_Conf() {
if [ ! -s $FileConf ]; then
echo -e "复制一份 $FileConfSample 示例配置文件\n\n"
cp -fv $FileConfSample $FileConf
fi
if [ ! -s $ListCronCurrent ]; then
echo -e "复制一份 $ListCronSample 基础定时任务\n\n"
cp -fv $ListCronSample $ListCronCurrent
fi
[ -f $CookieConf ] && . $CookieConf
[ -f $FileConf ] && . $FileConf
}
# 更新shell
Git_Pull_Shell() {
echo -e "更新shell...\n"
cd $ShellDir
git remote set-url origin $ShellURL
git fetch --all
ExitStatusShell=$?
git reset --hard origin/master
git pull
}
Git_Pull_Shell_Next() {
if [[ $ExitStatusShell -eq 0 ]]; then
echo -e "更新shell成功...\n"
[ ! -d $ShellDir/node_modules ] && Npm_Install panel
[ -f $ShellDir/package.json ] && PanelDependNew=$(cat $ShellDir/package.json)
[[ "$PanelDependOld" != "$PanelDependNew" ]] && cd $ShellDir && Npm_Install panel
cp -f $FileConfSample $ConfigDir/config.sh.sample
Notify_Version
else
echo -e "更新shell失败,请检查原因...\n"
fi
}
## npm install
Npm_Install() {
echo -e "检测到 $1 的依赖包有变化,运行 npm install...\n"
Npm_InstallSub
if [ $? -ne 0 ]; then
echo -e "\nnpm install 运行不成功,自动删除 $1/node_modules 后再次尝试一遍..."
rm -rf node_modules
fi
echo
if [ ! -d node_modules ]; then
echo -e "运行 npm install...\n"
Npm_InstallSub
if [ $? -ne 0 ]; then
echo -e "\nnpm install 运行不成功,自动删除 $1/node_modules...\n"
echo -e "请进入 $1 目录后手动运行 npm install...\n"
echo -e "3...\n"
sleep 1
echo -e "2...\n"
sleep 1
echo -e "1...\n"
sleep 1
rm -rf node_modules
fi
fi
}
## npm install 子程序,判断是否为安卓,判断是否安装有yarn
function Npm_InstallSub() {
if [ -n "$isTermux" ]; then
npm install --production --no-save --no-bin-links || npm install --production --no-bin-links --no-save --registry=https://registry.npm.taobao.org
elif ! type yarn >/dev/null 2>&1; then
npm install --production --no-save || npm install --production --no-save --registry=https://registry.npm.taobao.org
else
echo -e "检测到本机安装了 yarn,使用 yarn 替代 npm...\n"
yarn install --production --network-timeout 1000000000 || yarn install --production --registry=https://registry.npm.taobao.org --network-timeout 1000000000
fi
}
## 检测配置文件版本
Notify_Version() {
## 识别出两个文件的版本号
VerConfSample=$(grep " Version: " $FileConfSample | perl -pe "s|.+v((\d+\.?){3})|\1|")
[ -f $FileConf ] && VerConf=$(grep " Version: " $FileConf | perl -pe "s|.+v((\d+\.?){3})|\1|")
## 删除旧的发送记录文件
[ -f "$SendVersion" ] && [[ $(cat $SendVersion) != $VerConfSample ]] && rm -f $SendVersion
## 识别出更新日期和更新内容
UpdateDate=$(grep " Date: " $FileConfSample | awk -F ": " '{print $2}')
UpdateContent=$(grep " Update Content: " $FileConfSample | awk -F ": " '{print $2}')
## 如果是今天,并且版本号不一致,则发送通知
if [ -f $FileConf ] && [[ "$VerConf" != "$VerConfSample" ]] && [[ $UpdateDate == $(date "+%Y-%m-%d") ]]; then
if [ ! -f $SendVersion ]; then
notify "检测到配置文件config.sh.sample有更新" "更新日期: $UpdateDate\n当前版本: $VerConf\n新的版本: $VerConfSample\n更新内容: $UpdateContent\n更新说明: 如需使用新功能请对照config.sh.sample,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。"
fi
else
[ -f $ContentVersion ] && rm -f $ContentVersion
[ -f $SendVersion ] && rm -f $SendVersion
fi
}
## 每天次数随机,更新时间随机,更新秒数随机,至少6次,至多12次,大部分为8-10次,符合正态分布。
Random_Pull_Cron() {
if [[ $(date "+%-H") -le 2 ]]; then
RanMin=$(($RANDOM % 60))
RanSleep=$(($RANDOM % 56))
RanHourArray[0]=$(($RANDOM % 3))
for ((i = 1; i < 14; i++)); do
j=$(($i - 1))
tmp=$(($RANDOM % 3 + ${RanHourArray[j]} + 2))
[[ $tmp -lt 24 ]] && RanHourArray[i]=$tmp || break
done
RanHour=${RanHourArray[0]}
for ((i = 1; i < ${#RanHourArray[*]}; i++)); do
RanHour="$RanHour,${RanHourArray[i]}"
done
perl -i -pe "s|.+(git_pull? .+git_pull\.log.*)|$RanMin $RanHour \* \* \* sleep $RanSleep && \1|" $ListCronCurrent
crontab $ListCronCurrent
fi
}
## 克隆scripts
Git_Clone_Scripts() {
git clone -b master $ScriptsURL $ScriptsDir
ExitStatusScripts=$?
}
## 更新scripts
Git_Pull_Scripts() {
if [ -d $ScriptsDir/.git ]; then
echo -e "更新scripts...\n"
cd $ScriptsDir
git remote set-url origin $ScriptsURL
git fetch --all
ExitStatusScripts=$?
git reset --hard origin/master
git pull
else
Git_Clone_Scripts
fi
}
Git_Pull_Scripts_Next() {
if [[ $ExitStatusScripts -eq 0 ]]; then
echo -e "更新scripts成功...\n"
[ ! -d $ScriptsDir/node_modules ] && Npm_Install scripts
[ -f $ScriptsDir/package.json ] && ScriptsDependNew=$(cat $ScriptsDir/package.json)
[[ "$ScriptsDependOld" != "$ScriptsDependNew" ]] && cd $ScriptsDir && Npm_Install scripts
Diff_Cron
if [ -s $ListJsDrop ]; then
Output_ListJs $ListJsDrop "失效"
Del_Cron
fi
if [ -s $ListJsAdd ]; then
Output_ListJs $ListJsAdd "新"
Add_Cron
fi
else
echo -e "更新scripts失败,请检查原因...\n"
fi
}
Diff_Cron() {
cat $ListCronRemote | grep -E "node.+j[drx]_\w+\.js" | perl -pe "s|.+(j[drx]_\w+)\.js.+|\1|" | sort -u >$ListRemoteTask
cat $ListCronCurrent | grep -E "$ShellJs j[drx]_\w+" | perl -pe "s|.*ID=(.*) $ShellJs (j[drx]_\w+)\.*|\2|" | sort -u >$ListCurrentTask
if [ -s $ListCurrentTask ]; then
grep -vwf $ListCurrentTask $ListRemoteTask >$ListJsAdd
else
cp -f $ListRemoteTask $ListJsAdd
fi
if [ -s $ListRemoteTask ]; then
grep -vwf $ListRemoteTask $ListCurrentTask >$ListJsDrop
else
cp -f $ListCurrentTask $ListJsDrop
fi
}
Del_Cron() {
if [ $AutoDelCron == true ] && [ -s $ListJsDrop ]; then
echo -e "开始尝试自动删除定时任务如下:\n"
cat $ListJsDrop
echo
JsDrop=$(cat $ListJsDrop)
for Cron in $JsDrop; do
local id=$(cat $ListCronCurrent | grep -E "js $Cron" | perl -pe "s|.*ID=(.*) js $Cron|\1|")
del_cron_api "$id"
done
crontab $ListCronCurrent
echo -e "成功删除失效的脚本与定时任务\n"
notify "删除 lxk0301 失效脚本" "$JsDrop"
fi
}
Add_Cron() {
if [ $AutoAddCron == true ] && [ -s $ListJsAdd ]; then
echo -e "开始尝试自动添加定时任务\n"
JsAdd=$(cat $ListJsAdd)
for Cron in $JsAdd; do
if [[ $Cron == jd_bean_sign ]]; then
local name=$(cat "$ScriptsDir/$Cron.js" | grep -E "new Env\(" | perl -pe "s|(^.+)new Env\(\'*\"*(.+?)'*\"*\).+|\2|")
add_cron_api "4 0,9 * * *" "$ShellJs $Cron" "$name"
else
local name=$(cat "$ScriptsDir/$Cron.js" | grep -E "new Env\(" | perl -pe "s|(^.+)new Env\(\'*\"*(.+?)'*\"*\).+|\2|")
local param=$(cat $ListCronRemote | grep -E "\/$Cron\." | perl -pe "s|(^.+) node */scripts/(j[drx]_\w+)\.js.+|\1\:$ShellJs \2|")
add_cron_api "$param:$name"
fi
done
if [ $? -eq 0 ]; then
crontab $ListCronCurrent
echo -e "成功添加新的定时任务...\n"
notify "新增lxk0301脚本" "$JsAdd"
else
echo -e "添加新的定时任务出错,请手动添加...\n"
notify "尝试自动添加lxk0301以下新的定时任务出错,请手动添加:" "$JsAdd"
fi
fi
}
## 输出定时任务变化
Output_ListJs() {
local list=$1
local type=$2
if [ -s $list ]; then
echo -e "检测到有$type的定时任务:\n"
cat $list
echo
fi
}
#################################################################################################################################
echo -e "\n--------------------------------------------------------------\n"
echo -n "系统时间:"
echo $(date "+%Y-%m-%d %H:%M:%S")
if [ "${TZ}" = "UTC" ]; then
echo -n "北京时间:"
echo $(date -d "8 hour" "+%Y-%m-%d %H:%M:%S")
fi
echo -e "\nJS脚本目录:$ScriptsDir\n"
echo -e "--------------------------------------------------------------\n"
. $ShellDir/shell/api.sh
get_token
Import_Conf
# 更新shell
[ -f $ShellDir/package.json ] && PanelDependOld=$(cat $ShellDir/package.json)
Git_Pull_Shell
Git_Pull_Shell_Next
## 更新scripts
[ -f $ScriptsDir/package.json ] && ScriptsDependOld=$(cat $ScriptsDir/package.json)
Git_Pull_Scripts
Git_Pull_Scripts_Next
## 调用用户自定义的extra.sh
if [[ $EnableExtraShell == true ]]; then
if [ -f $ExtraShell ]; then
. $ExtraShell
else
echo -e "$ExtraShell 文件不存在,跳过执行DIY脚本...\n"
fi
fi
Executable
+283
View File
@@ -0,0 +1,283 @@
#!/usr/bin/env bash
## 路径
ShellDir=${QL_DIR:-$(cd $(dirname $0); pwd)}
[[ ${QL_DIR} ]] && ShellJS=js
ScriptsDir=${ShellDir}/scripts
ConfigDir=${ShellDir}/config
FileConf=${ConfigDir}/config.sh
CookieConf=${ConfigDir}/cookie.sh
FileConfSample=${ShellDir}/sample/config.sh.sample
LogDir=${ShellDir}/log
ListScripts=($(cd ${ScriptsDir}; ls *.js | grep -E "j[drx]_"))
ListCron=${ConfigDir}/crontab.list
ListCronLxk=${ScriptsDir}/docker/crontab_list.sh
ListJs=${LogDir}/js.list
## 导入config.sh
function Import_Conf {
if [ -f ${FileConf} ]
then
. ${CookieConf}
. ${FileConf}
if [[ ! -s ${CookieConf} ]]; then
echo -e "请先在Cookie管理中添加一条Cookie...\n"
exit 1
fi
else
echo -e "配置文件 ${FileConf} 不存在,请先按教程配置好该文件...\n"
exit 1
fi
}
## 更新crontab
function Detect_Cron {
if [[ $(cat ${ListCron}) != $(crontab -l) ]]; then
crontab ${ListCron}
fi
}
## 用户数量UserSum
function Count_UserSum {
UserSum=0
for line in `cat $CookieConf`
do
((UserSum++))
eval Cookie${UserSum}="\"${line}\""
done
}
## 组合Cookie和互助码子程序
function Combin_Sub {
CombinAll=""
if [[ ${AutoHelpOther} == true ]] && [[ $1 == ForOther* ]]; then
ForOtherAll=""
MyName=$(echo $1 | perl -pe "s|ForOther|My|")
for ((m=1; m<=${UserSum}; m++)); do
TmpA=${MyName}$m
TmpB=${!TmpA}
ForOtherAll="${ForOtherAll}@${TmpB}"
done
for ((n=1; n<=${UserSum}; n++)); do
for num in ${TempBlockCookie}; do
[[ $n -eq $num ]] && continue 2
done
CombinAll="${CombinAll}&${ForOtherAll}"
done
else
for ((i=1; i<=${UserSum}; i++)); do
for num in ${TempBlockCookie}; do
[[ $i -eq $num ]] && continue 2
done
Tmp1=$1$i
Tmp2=${!Tmp1}
CombinAll="${CombinAll}&${Tmp2}"
done
fi
echo ${CombinAll} | perl -pe "{s|^&||; s|^@+||; s|&@|&|g; s|@+&|&|g; s|@+|@|g; s|@+$||}"
}
## 组合Cookie、Token与互助码
function Combin_All {
export JD_COOKIE=$(Combin_Sub Cookie)
export FRUITSHARECODES=$(Combin_Sub ForOtherFruit)
export PETSHARECODES=$(Combin_Sub ForOtherPet)
export PLANT_BEAN_SHARECODES=$(Combin_Sub ForOtherBean)
export DREAM_FACTORY_SHARE_CODES=$(Combin_Sub ForOtherDreamFactory)
export DDFACTORY_SHARECODES=$(Combin_Sub ForOtherJdFactory)
export JDZZ_SHARECODES=$(Combin_Sub ForOtherJdzz)
export JDJOY_SHARECODES=$(Combin_Sub ForOtherJoy)
export JXNC_SHARECODES=$(Combin_Sub ForOtherJxnc)
export JXNCTOKENS=$(Combin_Sub TokenJxnc)
export BOOKSHOP_SHARECODES=$(Combin_Sub ForOtherBookShop)
export JD_CASH_SHARECODES=$(Combin_Sub ForOtherCash)
export JDSGMH_SHARECODES=$(Combin_Sub ForOtherSgmh)
export JDCFD_SHARECODES=$(Combin_Sub ForOtherCfd)
export JDGLOBAL_SHARECODES=$(Combin_Sub ForOtherGlobal)
}
## 转换JD_BEAN_SIGN_STOP_NOTIFY或JD_BEAN_SIGN_NOTIFY_SIMPLE
function Trans_JD_BEAN_SIGN_NOTIFY {
case ${NotifyBeanSign} in
0)
export JD_BEAN_SIGN_STOP_NOTIFY="true"
;;
1)
export JD_BEAN_SIGN_NOTIFY_SIMPLE="true"
;;
esac
}
## 转换UN_SUBSCRIBES
function Trans_UN_SUBSCRIBES {
export UN_SUBSCRIBES="${goodPageSize}\n${shopPageSize}\n${jdUnsubscribeStopGoods}\n${jdUnsubscribeStopShop}"
}
## 申明全部变量
function Set_Env {
Count_UserSum
Combin_All
Trans_JD_BEAN_SIGN_NOTIFY
Trans_UN_SUBSCRIBES
}
## 随机延迟
function Random_Delay {
if [[ -n ${RandomDelay} ]] && [[ ${RandomDelay} -gt 0 ]]; then
CurMin=$(date "+%-M")
if [[ ${CurMin} -gt 2 && ${CurMin} -lt 30 ]] || [[ ${CurMin} -gt 31 && ${CurMin} -lt 59 ]]; then
CurDelay=$((${RANDOM} % ${RandomDelay} + 1))
echo -e "\n命令未添加 \"now\",随机延迟 ${CurDelay} 秒后再执行任务,如需立即终止,请按 CTRL+C...\n"
sleep ${CurDelay}
fi
fi
}
## 使用说明
function Help {
echo -e "本脚本的用法为:"
echo -e "1. ${ShellJS} xxx # 如果设置了随机延迟并且当时时间不在0-2、30-31、59分内,将随机延迟一定秒数"
echo -e "2. ${ShellJS} xxx now # 无论是否设置了随机延迟,均立即运行"
echo -e "3. ${ShellJS} runall # 运行所有非挂机脚本,非常耗时"
echo -e "4. ${ShellJS} hangup # 重启挂机程序"
echo -e "5. ${ShellJS} resetpwd # 重置控制面板用户名和密码"
echo -e "\n针对用法1、用法2中的\"xxx\",可以不输入后缀\".js\",另外,如果前缀是\"jd_\"的话前缀也可以省略。"
echo -e "当前有以下脚本可以运行(仅列出以jd_、jr_、jx_开头的脚本):"
cd ${ScriptsDir}
for ((i=0; i<${#ListScripts[*]}; i++)); do
Name=$(grep "new Env" ${ListScripts[i]} | awk -F "'|\"" '{print $2}')
echo -e "$(($i + 1)).${Name}:${ListScripts[i]}"
done
}
## nohup
function Run_Nohup {
if [[ $(ps -ef | grep "${js}" | grep -v "grep") != "" ]]; then
ps -ef | grep "${js}" | grep -v "grep" | awk '{print $2}' | xargs kill -9
fi
[ ! -d ${LogDir}/${js} ] && mkdir -p ${LogDir}/${js}
LogTime=$(date "+%Y-%m-%d-%H-%M-%S")
LogFile="${LogDir}/${js}/${LogTime}.log"
nohup node ${js}.js > ${LogFile} &
}
## 运行挂机脚本
function Run_HangUp {
HangUpJs="jd_crazy_joy_coin"
cd ${ScriptsDir}
for js in ${HangUpJs}; do
Import_Conf ${js} && Set_Env
if type pm2 >/dev/null 2>&1; then
pm2 stop ${js}.js 2>/dev/null
pm2 flush
pm2 start -a ${js}.js --watch "${ScriptsDir}/${js}.js" --name="${js}"
else
Run_Nohup >/dev/null 2>&1
fi
done
}
## 重置密码
function Reset_Pwd {
cp -f ${ShellDir}/sample/auth.json ${ConfigDir}/auth.json
echo -e "控制面板重置成功,用户名:admin,密码:adminadmin\n"
}
## 一次性运行所有脚本
function Run_All {
if [ ! -f ${ListJs} ]; then
cat ${ListCronLxk} | grep -E "j[drx]_\w+\.js" | perl -pe "s|.+(j[drx]_\w+)\.js.+|\1|" | sort -u > ${ListJs}
fi
echo -e "\n==================== 开始运行所有非挂机脚本 ====================\n"
echo -e "请注意:本过程将非常非常耗时,一个账号可能长达几小时,账号越多耗时越长,如果是手动运行,退出终端也将终止运行。\n"
echo -e "倒计时5秒...\n"
for ((sec=5; sec>0; sec--)); do
echo -e "$sec...\n"
sleep 1
done
for file in $(cat ${ListJs}); do
echo -e "==================== 运行 $file.js 脚本 ====================\n"
${ShellJS} $file now
done
}
## 正常运行单个脚本
function Run_Normal {
Import_Conf $1 && Detect_Cron && Set_Env
FileNameTmp1=$(echo $1 | perl -pe "s|\.js||")
FileNameTmp2=$(echo $1 | perl -pe "{s|jd_||; s|\.js||; s|^|jd_|}")
SeekDir="${ScriptsDir} ${ScriptsDir}/backUp ${ConfigDir}"
FileName=""
WhichDir=""
for dir in ${SeekDir}
do
if [ -f ${dir}/${FileNameTmp1}.js ]; then
FileName=${FileNameTmp1}
WhichDir=${dir}
break
elif [ -f ${dir}/${FileNameTmp2}.js ]; then
FileName=${FileNameTmp2}
WhichDir=${dir}
break
fi
done
if [ -n "${FileName}" ] && [ -n "${WhichDir}" ]
then
[ $# -eq 1 ] && Random_Delay
LogTime=$(date "+%Y-%m-%d-%H-%M-%S")
LogFile="${LogDir}/${FileName}/${LogTime}.log"
[ ! -d ${LogDir}/${FileName} ] && mkdir -p ${LogDir}/${FileName}
cd ${WhichDir}
node ${FileName}.js 2>&1 | tee ${LogFile}
else
echo -e "\n在${ScriptsDir}、${ScriptsDir}/backUp、${ConfigDir}三个目录下均未检测到 $1 脚本的存在,请确认...\n"
Help
fi
}
## 命令检测
case $# in
0)
echo
Help
;;
1)
case $1 in
hangup)
Run_HangUp
;;
resetpwd)
Reset_Pwd
;;
runall)
Run_All
;;
*)
Run_Normal $1
;;
esac
;;
2)
case $2 in
now)
Run_Normal $1 $2
;;
*)
echo -e "\n命令输入错误...\n"
Help
;;
esac
;;
*)
echo -e "\n命令过多...\n"
Help
;;
esac
+5
View File
@@ -0,0 +1,5 @@
const notify = require('/ql/scripts/sendNotify');
const title = process.argv[2];
const content = process.argv[3];
notify.sendNotify(`${title}`, `${content}`);
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
#author:spark thanks to: https://github.com/sparkssssssss/scripts
. /ql/config/cookie.sh
. /ql/config/config.sh
title=$(echo $1|sed 's/-/_/g')
msg=$(echo -e $2)
node /ql/shell/notify.js "$title" "$msg"
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
ShellDir=${QL_DIR:-$(cd $(dirname $0); pwd)}
echo -e "更新shell...\n"
cd ${ShellDir}
git fetch --all
git pull
echo -e "更新shell完成...\n"
echo -e "重新build...\n"
yarn install --network-timeout 1000000000 || yarn install --registry=https://registry.npm.taobao.org --network-timeout 1000000000
yarn build
yarn build-back
yarn cache clean
echo -e "重新build完成...\n"
echo -e "重启服务...\n"
PIDS=`ps -ef|grep "app.js"|grep -v grep`
if [ "$PIDS" != "" ]; then
pm2 restart panel
else
pm2 start ${QL_DIR}/build/app.js -n panel
fi
nginx -s reload
echo -e "重启服务完成...\n"
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
## 判断环境
ShellDir=${QL_DIR:-$(cd $(dirname $0); pwd)}
LogDir=${ShellDir}/log
## 导入配置文件
. ${ShellDir}/config/cookie.sh
. ${ShellDir}/config/config.sh
## 删除运行js脚本的旧日志
function Rm_JsLog {
LogFileList=$(ls -l ${LogDir}/*/*.log | awk '{print $9}')
for log in ${LogFileList}
do
LogDate=$(echo ${log} | awk -F "/" '{print $NF}' | cut -c1-10) #文件名比文件属性获得的日期要可靠
if [[ $(uname -s) == Darwin ]]
then
DiffTime=$(($(date +%s) - $(date -j -f "%Y-%m-%d" "${LogDate}" +%s)))
else
DiffTime=$(($(date +%s) - $(date +%s -d "${LogDate}")))
fi
[ ${DiffTime} -gt $((${RmLogDaysAgo} * 86400)) ] && rm -vf ${log}
done
}
## 删除git_pull.sh的运行日志
function Rm_GitPullLog {
if [[ $(uname -s) == Darwin ]]
then
DateDelLog=$(date -v-${RmLogDaysAgo}d "+%Y-%m-%d")
else
Stmp=$(($(date "+%s") - 86400 * ${RmLogDaysAgo}))
DateDelLog=$(date -d "@${Stmp}" "+%Y-%m-%d")
fi
LineEndGitPull=$[$(cat ${LogDir}/git_pull.log | grep -n "${DateDelLog} " | head -1 | awk -F ":" '{print $1}') - 3]
[ ${LineEndGitPull} -gt 0 ] && perl -i -ne "{print unless 1 .. ${LineEndGitPull} }" ${LogDir}/git_pull.log
}
## 删除空文件夹
function Rm_EmptyDir {
cd ${LogDir}
for dir in $(ls)
do
if [ -d ${dir} ] && [[ $(ls ${dir}) == "" ]]; then
rm -rf ${dir}
fi
done
}
## 运行
if [ -n "${RmLogDaysAgo}" ]; then
echo -e "查找旧日志文件中...\n"
Rm_JsLog
Rm_GitPullLog
Rm_EmptyDir
echo -e "删除旧日志执行完毕\n"
fi
+24
View File
@@ -0,0 +1,24 @@
import { history } from 'umi';
import { request } from '@/utils/http';
import config from '@/utils/config';
export function render(oldRender: any) {
request
.get(`${config.apiPrefix}user`)
.then((data) => {
if (data.data && data.data.username) {
return oldRender();
}
localStorage.removeItem(config.authKey);
history.push('/login');
oldRender();
})
.catch((e) => {
console.log(e);
if (e.response && e.response.status === 401) {
localStorage.removeItem(config.authKey);
history.push('/login');
oldRender();
}
});
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

+73
View File
@@ -0,0 +1,73 @@
import {
FormOutlined,
FieldTimeOutlined,
DiffOutlined,
SettingOutlined,
CodeOutlined,
FolderOutlined,
RadiusSettingOutlined,
ControlOutlined,
} from '@ant-design/icons';
export default {
route: {
routes: [
{
name: 'login',
path: '/login',
hideInMenu: true,
component: '@/pages/login/index',
},
{
path: '/crontab',
name: '定时任务',
icon: <FieldTimeOutlined />,
component: '@/pages/crontab/index',
},
{
path: '/cookie',
name: 'Cookie管理',
icon: <RadiusSettingOutlined />,
component: '@/pages/cookie/index',
},
{
path: '/config',
name: '配置文件',
icon: <ControlOutlined />,
component: '@/pages/config/index',
},
{
path: '/diy',
name: '自定义脚本',
icon: <FormOutlined />,
component: '@/pages/diy/index',
},
{
path: '/diff',
name: '对比工具',
icon: <DiffOutlined />,
component: '@/pages/diff/index',
},
{
path: '/log',
name: '日志',
icon: <FolderOutlined />,
component: '@/pages/log/index',
},
{
path: '/setting',
name: '系统设置',
icon: <SettingOutlined />,
component: '@/pages/password/index',
},
],
},
location: {
pathname: '/',
},
navTheme: 'light',
fixSiderbar: true,
contentWidth: 'Fixed',
splitMenus: false,
logo: 'https://qinglong.whyour.cn/qinglong.png',
} as any;
+41
View File
@@ -0,0 +1,41 @@
body {
height: 100%;
overflow-y: hidden;
background-color: rgb(248, 248, 248);
}
@import '~codemirror/lib/codemirror.css';
.code-mirror-wrapper .CodeMirror {
position: absolute;
height: calc(100vh - 128px);
width: calc(100% - 32px);
}
.ant-pro-grid-content.wide {
max-width: unset !important;
overflow: auto;
.ant-pro-page-container-children-content {
overflow: auto;
height: calc(100vh - 96px);
background-color: #fff;
padding: 16px;
}
}
.cookie-wrapper {
th {
white-space: nowrap;
}
}
@media (max-width: 768px) {
.ant-pro-grid-content.wide {
.ant-pro-page-container-children-content {
height: calc(100vh - 144px);
}
}
.code-mirror-wrapper .CodeMirror {
height: calc(100vh - 176px);
}
}
+91
View File
@@ -0,0 +1,91 @@
import React, { useEffect, useState } from 'react';
import ProLayout from '@ant-design/pro-layout';
import {
enable as enableDarkMode,
disable as disableDarkMode,
auto as followSystemColorScheme,
setFetchMethod,
} from 'darkreader';
import defaultProps from './defaultProps';
import { Link, history } from 'umi';
import { LogoutOutlined } from '@ant-design/icons';
import config from '@/utils/config';
import 'codemirror/mode/shell/shell.js';
import { request } from '@/utils/http';
import './index.less';
export default function (props: any) {
const logout = () => {
request.post(`${config.apiPrefix}logout`).then(() => {
localStorage.removeItem(config.authKey);
history.push('/login');
});
};
useEffect(() => {
const isAuth = localStorage.getItem(config.authKey);
if (!isAuth) {
history.push('/login');
}
}, []);
useEffect(() => {
if (props.location.pathname === '/') {
history.push('/crontab');
}
}, [props.location.pathname]);
useEffect(() => {
const theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
setFetchMethod(window.fetch);
if (theme === 'dark') {
enableDarkMode({
brightness: 100,
contrast: 90,
sepia: 10,
});
} else if (theme === 'light') {
disableDarkMode();
} else {
followSystemColorScheme({
brightness: 100,
contrast: 90,
sepia: 10,
});
}
}, []);
if (props.location.pathname === '/login') {
return props.children;
}
return (
<ProLayout
selectedKeys={[props.location.pathname]}
title="控制面板"
menuItemRender={(menuItemProps: any, defaultDom: any) => {
if (
menuItemProps.isUrl ||
!menuItemProps.path ||
location.pathname === menuItemProps.path
) {
return defaultDom;
}
return <Link to={menuItemProps.path}>{defaultDom}</Link>;
}}
postMenuData={(menuData) => {
return [
...(menuData || []),
{
icon: <LogoutOutlined />,
name: '退出登录',
path: 'logout',
onTitleClick: () => logout(),
},
];
}}
{...defaultProps}
>
{props.children}
</ProLayout>
);
}
View File
+93
View File
@@ -0,0 +1,93 @@
import React, { PureComponent, Fragment, useState, useEffect } from 'react';
import { Button, notification, Modal } from 'antd';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
import { Controlled as CodeMirror } from 'react-codemirror2';
import { request } from '@/utils/http';
const Config = () => {
const [width, setWdith] = useState('100%');
const [marginLeft, setMarginLeft] = useState(0);
const [marginTop, setMarginTop] = useState(-72);
const [value, setValue] = useState('');
const [loading, setLoading] = useState(true);
const getConfig = () => {
setLoading(true);
request
.get(`${config.apiPrefix}config/config`)
.then((data) => {
setValue(data.data);
})
.finally(() => setLoading(false));
};
const updateConfig = () => {
request
.post(`${config.apiPrefix}save`, {
data: { content: value, name: 'config.sh' },
})
.then((data) => {
notification.success({
message: data.msg,
});
});
};
useEffect(() => {
if (document.body.clientWidth < 768) {
setWdith('auto');
setMarginLeft(0);
setMarginTop(0);
} else {
setWdith('100%');
setMarginLeft(0);
setMarginTop(-72);
}
getConfig();
}, []);
return (
<PageContainer
className="code-mirror-wrapper"
title="config.sh"
extra={[
<Button key="1" type="primary" onClick={updateConfig}>
保存
</Button>,
]}
header={{
style: {
padding: '4px 16px 4px 15px',
position: 'sticky',
top: 0,
left: 0,
zIndex: 20,
marginTop,
width,
marginLeft,
},
}}
style={{
height: '100vh',
}}
>
<CodeMirror
value={value}
options={{
lineNumbers: true,
lineWrapping: true,
styleActiveLine: true,
matchBrackets: true,
mode: 'shell',
}}
onBeforeChange={(editor, data, value) => {
setValue(value);
}}
onChange={(editor, data, value) => {}}
/>
</PageContainer>
);
};
export default Config;
+7
View File
@@ -0,0 +1,7 @@
tr.drop-over-downward td {
border-bottom: 2px dashed #1890ff;
}
tr.drop-over-upward td {
border-top: 2px dashed #1890ff;
}
+486
View File
@@ -0,0 +1,486 @@
import React, { useCallback, useRef, useState, useEffect } from 'react';
import {
Button,
notification,
Modal,
Table,
Tag,
Space,
Typography,
Tooltip,
} from 'antd';
import {
EditOutlined,
DeleteOutlined,
SyncOutlined,
CheckCircleOutlined,
StopOutlined,
} from '@ant-design/icons';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http';
import QRCode from 'qrcode.react';
import CookieModal from './modal';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less';
const { Text } = Typography;
enum Status {
'未获取',
'正常',
'已禁用',
'已失效',
'状态异常',
}
enum StatusColor {
'default',
'success',
'warning',
'error',
}
const type = 'DragableBodyRow';
const DragableBodyRow = ({
index,
moveRow,
className,
style,
...restProps
}: any) => {
const ref = useRef();
const [{ isOver, dropClassName }, drop] = useDrop(
() => ({
accept: type,
collect: (monitor) => {
const { index: dragIndex } = monitor.getItem() || ({} as any);
if (dragIndex === index) {
return {};
}
return {
isOver: monitor.isOver(),
dropClassName:
dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
};
},
drop: (item: any) => {
moveRow(item.index, index);
},
}),
[index],
);
const [, drag, preview] = useDrag(
() => ({
type,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
}),
[index],
);
drop(drag(ref));
return (
<tr
ref={ref}
className={`${className}${isOver ? dropClassName : ''}`}
style={{ cursor: 'move', ...style }}
{...restProps}
>
{restProps.children}
</tr>
);
};
const Config = () => {
const columns = [
{
title: '序号',
align: 'center' as const,
render: (text: string, record: any, index: number) => {
return <span style={{ cursor: 'text' }}>{index + 1} </span>;
},
},
{
title: '用户名',
dataIndex: 'pin',
key: 'pin',
align: 'center' as const,
render: (text: string, record: any) => {
const match = record.value.match(/pt_pin=([^; ]+)(?=;?)/);
const val = (match && match[1]) || '未匹配用户名';
return <span style={{ cursor: 'text' }}>{decodeUrl(val)}</span>;
},
},
{
title: '昵称',
dataIndex: 'nickname',
key: 'nickname',
align: 'center' as const,
width: '15%',
render: (text: string, record: any, index: number) => {
return (
<span style={{ cursor: 'text' }}>{record.nickname || '-'} </span>
);
},
},
{
title: '值',
dataIndex: 'value',
key: 'value',
align: 'center' as const,
width: '50%',
render: (text: string, record: any) => {
return (
<span
style={{
textAlign: 'left',
display: 'inline-block',
wordBreak: 'break-all',
cursor: 'text',
}}
>
{text}
</span>
);
},
},
{
title: '状态',
key: 'status',
dataIndex: 'status',
align: 'center' as const,
width: '15%',
render: (text: string, record: any, index: number) => {
return (
<Space size="middle" style={{ cursor: 'text' }}>
<Tag
color={StatusColor[record.status] || StatusColor[3]}
style={{ marginRight: 0 }}
>
{Status[record.status]}
</Tag>
{record.status !== Status.已禁用 && (
<Tooltip title="刷新">
<a onClick={() => refreshStatus(record, index)}>
<SyncOutlined />
</a>
</Tooltip>
)}
</Space>
);
},
},
{
title: '操作',
key: 'action',
align: 'center' as const,
render: (text: string, record: any, index: number) => (
<Space size="middle">
<Tooltip title="编辑">
<a onClick={() => editCookie(record, index)}>
<EditOutlined />
</a>
</Tooltip>
<Tooltip title={record.status === Status.已禁用 ? '启用' : '禁用'}>
<a onClick={() => enabledOrDisabledCron(record, index)}>
{record.status === Status.已禁用 ? (
<CheckCircleOutlined />
) : (
<StopOutlined />
)}
</a>
</Tooltip>
<Tooltip title="删除">
<a onClick={() => deleteCookie(record, index)}>
<DeleteOutlined />
</a>
</Tooltip>
</Space>
),
},
];
const [width, setWdith] = useState('100%');
const [marginLeft, setMarginLeft] = useState(0);
const [marginTop, setMarginTop] = useState(-72);
const [value, setValue] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [isModalVisible, setIsModalVisible] = useState(false);
const [editedCookie, setEditedCookie] = useState();
const getCookies = () => {
setLoading(true);
request
.get(`${config.apiPrefix}cookies`)
.then((data: any) => {
setValue(data.data);
})
.finally(() => setLoading(false));
};
const decodeUrl = (val: string) => {
try {
const newUrl = decodeURIComponent(val);
return newUrl;
} catch (error) {
return val;
}
};
useEffect(() => {
if (value && loading) {
asyncUpdateStatus();
}
}, [value]);
const asyncUpdateStatus = async () => {
for (let i = 0; i < value.length; i++) {
const cookie = value[i];
if (cookie.status === Status.已禁用) {
continue;
}
await sleep(1000);
location.pathname === '/cookie' && refreshStatus(cookie, i);
}
};
const sleep = (time: number) => {
return new Promise((resolve) => setTimeout(resolve, time));
};
const refreshStatus = (record: any, index: number) => {
request
.get(`${config.apiPrefix}cookies/${record._id}/refresh`)
.then(async (data: any) => {
if (data.data && data.data.value) {
(value as any).splice(index, 1, data.data);
setValue([...(value as any)] as any);
} else {
notification.error({ message: '更新状态失败' });
}
});
};
const enabledOrDisabledCron = (record: any, index: number) => {
Modal.confirm({
title: `确认${record.status === Status.已禁用 ? '启用' : '禁用'}`,
content: (
<>
确认{record.status === Status.已禁用 ? '启用' : '禁用'}
Cookie{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.value}
</Text>{' '}
吗
</>
),
onOk() {
request
.get(
`${config.apiPrefix}cookies/${record._id}/${
record.status === Status.已禁用 ? 'enable' : 'disable'
}`,
{
data: { _id: record._id },
},
)
.then((data: any) => {
if (data.code === 200) {
notification.success({
message: `${
record.status === Status.已禁用 ? '启用' : '禁用'
}成功`,
});
const newStatus =
record.status === Status.已禁用 ? Status.未获取 : Status.已禁用;
const result = [...value];
result.splice(index, 1, {
...record,
status: newStatus,
});
setValue(result);
} else {
notification.error({
message: data,
});
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const addCookie = () => {
setEditedCookie(null as any);
setIsModalVisible(true);
};
const editCookie = (record: any, index: number) => {
setEditedCookie(record);
setIsModalVisible(true);
};
const deleteCookie = (record: any, index: number) => {
Modal.confirm({
title: '确认删除',
content: (
<>
确认删除Cookie{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.value}
</Text>{' '}
吗
</>
),
onOk() {
request
.delete(`${config.apiPrefix}cookies/${record._id}`)
.then((data: any) => {
if (data.code === 200) {
notification.success({
message: '删除成功',
});
const result = [...value];
result.splice(index, 1);
setValue(result);
} else {
notification.error({
message: data,
});
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const handleCancel = (cookies: any[]) => {
setIsModalVisible(false);
if (cookies && cookies.length > 0) {
handleCookies(cookies);
}
};
const handleCookies = (cookies: any[]) => {
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i];
const index = value.findIndex((x) => x._id === cookie._id);
const result = [...value];
if (index === -1) {
result.push(cookie);
} else {
result.splice(index, 1, {
...cookie,
});
}
setValue(result);
refreshStatus(cookie, index);
}
};
const components = {
body: {
row: DragableBodyRow,
},
};
const moveRow = useCallback(
(dragIndex, hoverIndex) => {
if (dragIndex === hoverIndex) {
return;
}
const dragRow = value[dragIndex];
const newData = [...value];
newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, dragRow);
setValue([...newData]);
request
.put(`${config.apiPrefix}cookies/${dragRow._id}/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex },
})
.then((data: any) => {
if (data.code !== 200) {
notification.error({
message: data,
});
}
});
},
[value],
);
useEffect(() => {
if (document.body.clientWidth < 768) {
setWdith('auto');
setMarginLeft(0);
setMarginTop(0);
} else {
setWdith('100%');
setMarginLeft(0);
setMarginTop(-72);
}
getCookies();
}, []);
return (
<PageContainer
className="cookie-wrapper"
title="Cookie管理"
loading={loading}
extra={[
<Button key="2" type="primary" onClick={() => addCookie()}>
添加Cookie
</Button>,
]}
header={{
style: {
padding: '4px 16px 4px 15px',
position: 'sticky',
top: 0,
left: 0,
zIndex: 20,
marginTop,
width,
marginLeft,
},
}}
style={{
height: '100vh',
}}
>
<DndProvider backend={HTML5Backend}>
<Table
columns={columns}
pagination={false}
dataSource={value}
rowKey="value"
size="middle"
bordered
scroll={{ x: 768 }}
components={components}
onRow={(record, index) => {
return {
index,
moveRow,
} as any;
}}
/>
</DndProvider>
<CookieModal
visible={isModalVisible}
handleCancel={handleCancel}
cookie={editedCookie}
/>
</PageContainer>
);
};
export default Config;
+103
View File
@@ -0,0 +1,103 @@
import React, { useEffect, useState } from 'react';
import { Modal, notification, Input, Form } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
const CookieModal = ({
cookie,
handleCancel,
visible,
}: {
cookie?: any;
visible: boolean;
handleCancel: (needUpdate?: boolean) => void;
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const handleOk = async (values: any) => {
const cookies = values.value
.split('\n')
.map((x: any) => x.trim().replace(/\s/g, ''));
let flag = false;
for (const coo of cookies) {
if (!/pt_key=\S*;\s*pt_pin=\S*;\s*/.test(coo)) {
notification.error({ message: `${coo}格式有误` });
flag = true;
break;
}
}
if (flag) {
return;
}
setLoading(true);
const method = cookie ? 'put' : 'post';
const payload = cookie ? { value: cookies[0], _id: cookie._id } : cookies;
const { code, data } = await request[method](`${config.apiPrefix}cookies`, {
data: payload,
});
if (code === 200) {
notification.success({
message: cookie ? '更新Cookie成功' : '添加Cookie成功',
});
} else {
notification.error({
message: data,
});
}
setLoading(false);
handleCancel(data);
};
useEffect(() => {
form.resetFields();
}, [cookie]);
return (
<Modal
title={cookie ? '编辑Cookie' : '新建Cookie'}
visible={visible}
forceRender
onOk={() => {
form
.validateFields()
.then((values) => {
handleOk(values);
})
.catch((info) => {
console.log('Validate Failed:', info);
});
}}
onCancel={() => handleCancel()}
confirmLoading={loading}
destroyOnClose
>
<Form
form={form}
layout="vertical"
name="form_in_modal"
preserve={false}
initialValues={cookie}
>
<Form.Item
name="value"
rules={[
{ required: true, message: '请输入Cookie' },
{
pattern: /pt_key=\S*;\s*pt_pin=\S*;\s*/,
message: 'Cookie格式错误,注意分号(pt_key=***;pt_pin=***;)',
},
]}
>
<Input.TextArea
rows={4}
autoSize={true}
placeholder="请输入cookie,多个cookie换行输入"
/>
</Form.Item>
</Form>
</Modal>
);
};
export default CookieModal;
View File
+470
View File
@@ -0,0 +1,470 @@
import React, { PureComponent, Fragment, useState, useEffect } from 'react';
import {
Button,
notification,
Modal,
Table,
Tag,
Space,
Tooltip,
Dropdown,
Menu,
Typography,
Input,
} from 'antd';
import {
ClockCircleOutlined,
SyncOutlined,
CloseCircleOutlined,
FileTextOutlined,
EllipsisOutlined,
PlayCircleOutlined,
CheckCircleOutlined,
EditOutlined,
StopOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http';
import CronModal from './modal';
import CronLogModal from './logModal';
const { Text } = Typography;
const { Search } = Input;
enum CrontabStatus {
'running',
'idle',
'disabled',
}
const Crontab = () => {
const columns = [
{
title: '任务名',
dataIndex: 'name',
key: 'name',
align: 'center' as const,
render: (text: string, record: any) => (
<span>{record.name || record._id}</span>
),
},
{
title: '任务',
dataIndex: 'command',
key: 'command',
width: '40%',
align: 'center' as const,
render: (text: string, record: any) => {
return (
<span
style={{
textAlign: 'left',
width: '100%',
display: 'inline-block',
wordBreak: 'break-all',
}}
>
{text}
</span>
);
},
},
{
title: '任务定时',
dataIndex: 'schedule',
key: 'schedule',
align: 'center' as const,
},
{
title: '状态',
key: 'status',
dataIndex: 'status',
align: 'center' as const,
render: (text: string, record: any) => (
<>
{record.status === CrontabStatus.idle && (
<Tag icon={<ClockCircleOutlined />} color="default">
空闲中
</Tag>
)}
{record.status === CrontabStatus.running && (
<Tag icon={<SyncOutlined spin />} color="processing">
运行中
</Tag>
)}
{record.status === CrontabStatus.disabled && (
<Tag icon={<CloseCircleOutlined />} color="error">
已禁用
</Tag>
)}
</>
),
},
{
title: '操作',
key: 'action',
align: 'center' as const,
render: (text: string, record: any, index: number) => (
<Space size="middle">
<Tooltip title="运行">
<a
onClick={() => {
runCron(record, index);
}}
>
<PlayCircleOutlined />
</a>
</Tooltip>
<Tooltip title="日志">
<a
onClick={() => {
setLogCron({ ...record, timestamp: Date.now() });
}}
>
<FileTextOutlined />
</a>
</Tooltip>
<MoreBtn key="more" record={record} index={index} />
</Space>
),
},
];
const [width, setWdith] = useState('100%');
const [marginLeft, setMarginLeft] = useState(0);
const [marginTop, setMarginTop] = useState(-72);
const [value, setValue] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [isModalVisible, setIsModalVisible] = useState(false);
const [editedCron, setEditedCron] = useState();
const [searchText, setSearchText] = useState('');
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [logCron, setLogCron] = useState<any>();
const getCrons = () => {
setLoading(true);
request
.get(`${config.apiPrefix}crons?searchValue=${searchText}`)
.then((data: any) => {
setValue(data.data.sort((a: any, b: any) => a.status - b.status));
})
.finally(() => setLoading(false));
};
const addCron = () => {
setEditedCron(null as any);
setIsModalVisible(true);
};
const editCron = (record: any, index: number) => {
setEditedCron(record);
setIsModalVisible(true);
};
const delCron = (record: any, index: number) => {
Modal.confirm({
title: '确认删除',
content: (
<>
确认删除定时任务{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
吗
</>
),
onOk() {
request
.delete(`${config.apiPrefix}crons/${record._id}`)
.then((data: any) => {
if (data.code === 200) {
notification.success({
message: '删除成功',
});
const result = [...value];
result.splice(index, 1);
setValue(result);
} else {
notification.error({
message: data,
});
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const runCron = (record: any, index: number) => {
Modal.confirm({
title: '确认运行',
content: (
<>
确认运行定时任务{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
吗
</>
),
onOk() {
request
.get(`${config.apiPrefix}crons/${record._id}/run`)
.then((data: any) => {
if (data.code === 200) {
const result = [...value];
result.splice(index, 1, {
...record,
status: CrontabStatus.running,
});
setValue(result);
} else {
notification.error({
message: data,
});
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const enabledOrDisabledCron = (record: any, index: number) => {
Modal.confirm({
title: `确认${
record.status === CrontabStatus.disabled ? '启用' : '禁用'
}`,
content: (
<>
确认{record.status === CrontabStatus.disabled ? '启用' : '禁用'}
定时任务{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
吗
</>
),
onOk() {
request
.get(
`${config.apiPrefix}crons/${record._id}/${
record.status === CrontabStatus.disabled ? 'enable' : 'disable'
}`,
{
data: { _id: record._id },
},
)
.then((data: any) => {
if (data.code === 200) {
notification.success({
message: `${
record.status === CrontabStatus.disabled ? '启用' : '禁用'
}成功`,
});
const newStatus =
record.status === CrontabStatus.disabled
? CrontabStatus.idle
: CrontabStatus.disabled;
const result = [...value];
result.splice(index, 1, {
...record,
status: newStatus,
});
setValue(result);
} else {
notification.error({
message: data,
});
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const MoreBtn: React.FC<{
record: any;
index: number;
}> = ({ record, index }) => (
<Dropdown
arrow
trigger={['click', 'hover']}
overlay={
<Menu onClick={({ key }) => action(key, record, index)}>
<Menu.Item key="edit" icon={<EditOutlined />}>
编辑
</Menu.Item>
{record.isSystem !== 1 && (
<>
<Menu.Item
key="enableordisable"
icon={
record.status === CrontabStatus.disabled ? (
<CheckCircleOutlined />
) : (
<StopOutlined />
)
}
>
{record.status === CrontabStatus.disabled ? '启用' : '禁用'}
</Menu.Item>
<Menu.Item key="delete" icon={<DeleteOutlined />}>
删除
</Menu.Item>
</>
)}
</Menu>
}
>
<a>
<EllipsisOutlined />
</a>
</Dropdown>
);
const action = (key: string | number, record: any, index: number) => {
switch (key) {
case 'edit':
editCron(record, index);
break;
case 'enableordisable':
enabledOrDisabledCron(record, index);
break;
case 'delete':
delCron(record, index);
break;
default:
break;
}
};
const handleCancel = (cron?: any) => {
setIsModalVisible(false);
if (cron) {
handleCrons(cron);
}
};
const onSearch = (value: string) => {
setSearchText(value);
};
const handleCrons = (cron: any) => {
const index = value.findIndex((x) => x._id === cron._id);
const result = [...value];
if (index === -1) {
result.push(cron);
} else {
result.splice(index, 1, {
...cron,
});
}
setValue(result);
};
const getCronDetail = (cron: any) => {
request
.get(`${config.apiPrefix}crons/${cron._id}`)
.then((data: any) => {
const index = value.findIndex((x) => x._id === cron._id);
const result = [...value];
result.splice(index, 1, {
...cron,
...data.data,
});
setValue(result);
})
.finally(() => setLoading(false));
};
useEffect(() => {
if (logCron) {
setIsLogModalVisible(true);
}
}, [logCron]);
useEffect(() => {
getCrons();
}, [searchText]);
useEffect(() => {
if (document.body.clientWidth < 768) {
setWdith('auto');
setMarginLeft(0);
setMarginTop(0);
} else {
setWdith('100%');
setMarginLeft(0);
setMarginTop(-72);
}
}, []);
return (
<PageContainer
className="code-mirror-wrapper"
title="定时任务"
loading={loading}
extra={[
<Search
placeholder="请输入名称或者关键词"
style={{ width: 'auto' }}
enterButton
loading={loading}
onSearch={onSearch}
/>,
<Button key="2" type="primary" onClick={() => addCron()}>
添加定时
</Button>,
]}
header={{
style: {
padding: '4px 16px 4px 15px',
position: 'sticky',
top: 0,
left: 0,
zIndex: 20,
marginTop,
width,
marginLeft,
},
}}
>
<Table
columns={columns}
pagination={{
hideOnSinglePage: true,
showSizeChanger: true,
defaultPageSize: 20,
}}
dataSource={value}
rowKey="_id"
size="middle"
bordered
scroll={{ x: 768 }}
/>
<CronLogModal
visible={isLogModalVisible}
handleCancel={() => {
getCronDetail(logCron);
setIsLogModalVisible(false);
}}
cron={logCron}
/>
<CronModal
visible={isModalVisible}
handleCancel={handleCancel}
cron={editedCron}
/>
</PageContainer>
);
};
export default Crontab;
+71
View File
@@ -0,0 +1,71 @@
import React, { useEffect, useState } from 'react';
import { Modal, notification, Input, Form } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
enum CrontabStatus {
'running',
'idle',
'disabled',
}
const CronLogModal = ({
cron,
handleCancel,
visible,
}: {
cron?: any;
visible: boolean;
handleCancel: () => void;
}) => {
const [value, setValue] = useState<string>('启动中...');
const [loading, setLoading] = useState<any>(true);
const getCronLog = (isFirst?: boolean) => {
if (isFirst) {
setLoading(true);
}
request
.get(`${config.apiPrefix}crons/${cron._id}/log`)
.then((data: any) => {
const log = data.data as string;
setValue(log || '暂无日志');
if (log && !log.includes('执行结束')) {
setTimeout(() => {
getCronLog();
}, 2000);
}
})
.finally(() => {
if (isFirst) {
setLoading(false);
}
});
};
const cancel = () => {
handleCancel();
};
useEffect(() => {
if (cron) {
getCronLog(true);
}
}, [cron]);
return (
<Modal
title={`日志-${cron && cron.name}`}
visible={visible}
forceRender
onOk={() => cancel()}
onCancel={() => cancel()}
>
<pre style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word' }}>
{!loading && value}
</pre>
</Modal>
);
};
export default CronLogModal;
+99
View File
@@ -0,0 +1,99 @@
import React, { useEffect, useState } from 'react';
import { Modal, notification, Input, Form } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import cronParse from 'cron-parser';
const CronModal = ({
cron,
handleCancel,
visible,
}: {
cron?: any;
visible: boolean;
handleCancel: (needUpdate?: boolean) => void;
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const handleOk = async (values: any) => {
setLoading(true);
const method = cron ? 'put' : 'post';
const payload = { ...values };
if (cron) {
payload._id = cron._id;
}
const { code, data } = await request[method](`${config.apiPrefix}crons`, {
data: payload,
});
if (code === 200) {
notification.success({
message: cron ? '更新Cron成功' : '添加Cron成功',
});
} else {
notification.error({
message: data,
});
}
setLoading(false);
handleCancel(data);
};
useEffect(() => {
if (cron) {
form.setFieldsValue(cron);
} else {
form.resetFields();
}
}, [cron]);
return (
<Modal
title={cron ? '编辑定时' : '新建定时'}
visible={visible}
forceRender
onOk={() => {
form
.validateFields()
.then((values) => {
handleOk(values);
})
.catch((info) => {
console.log('Validate Failed:', info);
});
}}
onCancel={() => handleCancel()}
confirmLoading={loading}
destroyOnClose
>
<Form form={form} layout="vertical" name="form_in_modal" preserve={false}>
<Form.Item name="name" label="名称">
<Input />
</Form.Item>
<Form.Item name="command" label="任务" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item
name="schedule"
label="时间"
rules={[
{ required: true },
{
validator: (rule, value) => {
if (cronParse.parseString(value).expressions.length > 0) {
return Promise.resolve();
} else {
return Promise.reject('Cron表达式格式有误');
}
},
},
]}
>
<Input />
</Form.Item>
</Form>
</Modal>
);
};
export default CronModal;
+12
View File
@@ -0,0 +1,12 @@
.d2h-files-diff {
height: calc(100vh - 130px);
overflow: auto;
}
.d2h-code-side-linenumber {
position: relative;
}
.d2h-code-side-line {
padding: 0 0.5em;
}
+103
View File
@@ -0,0 +1,103 @@
import React, { PureComponent, Fragment, useState, useEffect } from 'react';
import { Button, notification, Modal } from 'antd';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http';
import ReactDiffViewer from 'react-diff-viewer';
import './index.less';
const Crontab = () => {
const [width, setWdith] = useState('100%');
const [marginLeft, setMarginLeft] = useState(0);
const [marginTop, setMarginTop] = useState(-72);
const [value, setValue] = useState('');
const [sample, setSample] = useState('');
const [loading, setLoading] = useState(true);
const getConfig = () => {
request.get(`${config.apiPrefix}config/config`).then((data) => {
setValue(data.data);
});
};
const getSample = () => {
setLoading(true);
request
.get(`${config.apiPrefix}config/sample`)
.then((data) => {
setSample(data.data);
})
.finally(() => setLoading(false));
};
useEffect(() => {
if (document.body.clientWidth < 768) {
setWdith('auto');
setMarginLeft(0);
setMarginTop(0);
} else {
setWdith('100%');
setMarginLeft(0);
setMarginTop(-72);
}
getConfig();
getSample();
}, []);
return (
<PageContainer
className="code-mirror-wrapper"
title="对比工具"
loading={loading}
header={{
style: {
padding: '4px 16px 4px 15px',
position: 'sticky',
top: 0,
left: 0,
zIndex: 20,
marginTop,
width,
marginLeft,
},
}}
style={{
height: '100vh',
}}
>
<ReactDiffViewer
styles={{
diffContainer: {
overflowX: 'auto',
minWidth: 768,
},
diffRemoved: {
overflowX: 'auto',
maxWidth: 300,
},
diffAdded: {
overflowX: 'auto',
maxWidth: 300,
},
line: {
wordBreak: 'break-word',
},
}}
oldValue={value}
newValue={sample}
splitView={true}
leftTitle="config.sh"
rightTitle="config.sh.sample"
disableWordDiff={true}
/>
{/* <CodeDiff
style={{ height: 'calc(100vh - 72px)', overflowY: 'auto' }}
outputFormat="side-by-side"
oldStr={value}
newStr={sample}
/> */}
</PageContainer>
);
};
export default Crontab;
View File
+93
View File
@@ -0,0 +1,93 @@
import React, { PureComponent, Fragment, useState, useEffect } from 'react';
import { Button, notification, Modal } from 'antd';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
import { Controlled as CodeMirror } from 'react-codemirror2';
import { request } from '@/utils/http';
const Crontab = () => {
const [width, setWdith] = useState('100%');
const [marginLeft, setMarginLeft] = useState(0);
const [marginTop, setMarginTop] = useState(-72);
const [value, setValue] = useState('');
const [loading, setLoading] = useState(true);
const getConfig = () => {
setLoading(true);
request
.get(`${config.apiPrefix}config/extra`)
.then((data) => {
setValue(data.data);
})
.finally(() => setLoading(false));
};
const updateConfig = () => {
request
.post(`${config.apiPrefix}save`, {
data: { content: value, name: 'extra.sh' },
})
.then((data) => {
notification.success({
message: data.msg,
});
});
};
useEffect(() => {
if (document.body.clientWidth < 768) {
setWdith('auto');
setMarginLeft(0);
setMarginTop(0);
} else {
setWdith('100%');
setMarginLeft(0);
setMarginTop(-72);
}
getConfig();
}, []);
return (
<PageContainer
className="code-mirror-wrapper"
title="extra.sh"
extra={[
<Button key="1" type="primary" onClick={updateConfig}>
保存
</Button>,
]}
header={{
style: {
padding: '4px 16px 4px 15px',
position: 'sticky',
top: 0,
left: 0,
zIndex: 20,
marginTop,
width,
marginLeft,
},
}}
style={{
height: '100vh',
}}
>
<CodeMirror
value={value}
options={{
lineNumbers: true,
lineWrapping: true,
styleActiveLine: true,
matchBrackets: true,
mode: 'shell',
}}
onBeforeChange={(editor, data, value) => {
setValue(value);
}}
onChange={(editor, data, value) => {}}
/>
</PageContainer>
);
};
export default Crontab;
View File
+118
View File
@@ -0,0 +1,118 @@
import React, { PureComponent, Fragment, useState, useEffect } from 'react';
import { Button, notification, Modal, TreeSelect } from 'antd';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
import { Controlled as CodeMirror } from 'react-codemirror2';
import { request } from '@/utils/http';
const Log = () => {
const [width, setWdith] = useState('100%');
const [marginLeft, setMarginLeft] = useState(0);
const [marginTop, setMarginTop] = useState(-72);
const [title, setTitle] = useState('log');
const [value, setValue] = useState('请选择日志文件');
const [select, setSelect] = useState();
const [data, setData] = useState();
const [loading, setLoading] = useState(false);
const getConfig = () => {
request.get(`${config.apiPrefix}logs`).then((data) => {
setData(formatData(data.dirs) as any);
});
};
const formatData = (tree: any[]) => {
return tree.map((x) => {
x.title = x.name;
x.value = x.name;
x.disabled = x.isDir;
x.children = x.files.map((y: string) => ({
title: y,
value: `${x.name}/${y}`,
parent: x.name,
}));
return x;
});
};
const getLog = (node: any) => {
console.log(node);
setLoading(true);
request
.get(`${config.apiPrefix}logs/${node.value}`)
.then((data) => {
setValue(data.data);
})
.finally(() => setLoading(false));
};
const onSelect = (value: any, node: any) => {
setSelect(value);
setTitle(node.parent);
getLog(node);
};
useEffect(() => {
if (document.body.clientWidth < 768) {
setWdith('auto');
setMarginLeft(0);
setMarginTop(0);
} else {
setWdith('100%');
setMarginLeft(0);
setMarginTop(-72);
}
getConfig();
}, []);
return (
<PageContainer
className="code-mirror-wrapper"
title={title}
extra={[
<TreeSelect
style={{ width: 280 }}
value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={data}
placeholder="请选择日志文件"
showSearch
key="value"
onSelect={onSelect}
/>,
]}
header={{
style: {
padding: '4px 16px 4px 15px',
position: 'sticky',
top: 0,
left: 0,
zIndex: 20,
marginTop,
width,
marginLeft,
},
}}
style={{
height: '100vh',
}}
>
<CodeMirror
value={value}
options={{
lineNumbers: true,
lineWrapping: true,
styleActiveLine: true,
matchBrackets: true,
mode: 'shell',
readOnly: true,
}}
onBeforeChange={(editor, data, value) => {
setValue(value);
}}
onChange={(editor, data, value) => {}}
/>
</PageContainer>
);
};
export default Log;
+105
View File
@@ -0,0 +1,105 @@
@import '~antd/es/style/themes/default.less';
.container {
display: flex;
flex-direction: column;
height: 100vh;
overflow: auto;
background: @layout-body-background;
}
.lang {
width: 100%;
height: 40px;
line-height: 44px;
text-align: right;
:global(.ant-dropdown-trigger) {
margin-right: 24px;
}
}
.content {
position: absolute;
top: 45%;
left: 50%;
margin: -160px 0 0 -160px;
width: 320px;
height: 320px;
padding: 36px;
box-shadow: 0 0 100px rgba(0, 0, 0, 0.08);
}
@media (min-width: @screen-md-min) {
.container {
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
}
}
.top {
text-align: center;
}
.header {
height: 44px;
line-height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.logo {
width: 30px;
margin-right: 8px;
}
.desc {
margin-top: 12px;
margin-bottom: 40px;
color: @text-color-secondary;
font-size: @font-size-base;
}
.main {
margin: 35px auto 0;
@media screen and (max-width: @screen-sm) {
width: 95%;
max-width: 320px;
}
:global {
.@{ant-prefix}-tabs-nav-list {
margin: auto;
font-size: 16px;
}
}
.icon {
margin-left: 16px;
color: rgba(0, 0, 0, 0.2);
font-size: 24px;
vertical-align: middle;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: @primary-color;
}
}
.other {
margin-top: 24px;
line-height: 22px;
text-align: left;
.register {
float: right;
}
}
.prefixIcon {
color: @primary-color;
font-size: @font-size-base;
}
}
+90
View File
@@ -0,0 +1,90 @@
import React, { Fragment, useEffect } from 'react';
import { Button, Row, Input, Form, notification } from 'antd';
import config from '@/utils/config';
import { history, Link } from 'umi';
import styles from './index.less';
import { request } from '@/utils/http';
const FormItem = Form.Item;
const Login = () => {
const handleOk = (values: any) => {
request
.post(`${config.apiPrefix}login`, {
data: {
username: values.username,
password: values.password,
},
})
.then((data) => {
if (data.code === 200) {
localStorage.setItem(config.authKey, data.token);
history.push('/crontab');
} else if (data.code === 100) {
notification.warn({
message: data.msg,
});
} else {
notification.error({
message: data.msg,
});
}
})
.catch(function (error) {
console.log(error);
});
};
useEffect(() => {
const isAuth = localStorage.getItem(config.authKey);
if (isAuth) {
history.push('/crontab');
}
}, []);
return (
<div className={styles.container}>
<div className={styles.content}>
<div className={styles.top}>
<div className={styles.header}>
<img
alt="logo"
className={styles.logo}
src="https://qinglong.whyour.cn/qinglong.png"
/>
<span className={styles.title}>{config.siteName}</span>
</div>
</div>
<div className={styles.main}>
<Form onFinish={handleOk}>
<FormItem
name="username"
rules={[{ required: true, message: '请输入用户名' }]}
hasFeedback
>
<Input placeholder="用户名" autoFocus />
</FormItem>
<FormItem
name="password"
rules={[{ required: true, message: '请输入密码' }]}
hasFeedback
>
<Input type="password" placeholder="密码" />
</FormItem>
<Row>
<Button
type="primary"
htmlType="submit"
style={{ width: '100%' }}
>
登录
</Button>
</Row>
</Form>
</div>
</div>
</div>
);
};
export default Login;
View File
+156
View File
@@ -0,0 +1,156 @@
import React, { useState, useEffect } from 'react';
import { Button, Input, Form, Radio, Tabs } from 'antd';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http';
import {
enable as enableDarkMode,
disable as disableDarkMode,
auto as followSystemColorScheme,
setFetchMethod,
} from 'darkreader';
import { history } from 'umi';
const optionsWithDisabled = [
{ label: '亮色', value: 'light' },
{ label: '暗色', value: 'dark' },
{ label: '跟随系统', value: 'auto' },
];
const Password = () => {
const [width, setWdith] = useState('100%');
const [marginLeft, setMarginLeft] = useState(0);
const [marginTop, setMarginTop] = useState(-72);
const [value, setValue] = useState('');
const [loading, setLoading] = useState(true);
const defaultDarken = localStorage.getItem('qinglong_dark_theme') || 'auto';
const [theme, setTheme] = useState(defaultDarken);
const handleOk = (values: any) => {
request
.post(`${config.apiPrefix}user`, {
data: {
username: values.username,
password: values.password,
},
})
.then((data: any) => {
localStorage.removeItem(config.authKey);
history.push('/login');
})
.catch((error: any) => {
console.log(error);
});
};
const themeChange = (e: any) => {
setTheme(e.target.value);
localStorage.setItem('qinglong_dark_theme', e.target.value);
};
const importJob = () => {
request.get(`${config.apiPrefix}crons/import`).then((data: any) => {
console.log(data);
});
};
useEffect(() => {
if (document.body.clientWidth < 768) {
setWdith('auto');
setMarginLeft(0);
setMarginTop(0);
} else {
setWdith('100%');
setMarginLeft(0);
setMarginTop(-72);
}
}, []);
useEffect(() => {
setFetchMethod(window.fetch);
if (theme === 'dark') {
enableDarkMode({
brightness: 100,
contrast: 90,
sepia: 10,
});
} else if (theme === 'light') {
disableDarkMode();
} else {
followSystemColorScheme({
brightness: 100,
contrast: 90,
sepia: 10,
});
}
}, [theme]);
return (
<PageContainer
className="code-mirror-wrapper"
title="系统设置"
header={{
style: {
padding: '4px 16px 4px 15px',
position: 'sticky',
top: 0,
left: 0,
zIndex: 20,
marginTop,
width,
marginLeft,
},
}}
style={{
height: '100vh',
}}
>
<Tabs
defaultActiveKey="person"
tabPosition="left"
style={{ height: 'calc(100vh - 128px)' }}
>
<Tabs.TabPane tab="个人设置" key="person">
<Form onFinish={handleOk} layout="vertical">
<Form.Item
label="用户名"
name="username"
rules={[{ required: true }]}
hasFeedback
style={{ maxWidth: 300 }}
>
<Input placeholder="用户名" autoFocus />
</Form.Item>
<Form.Item
label="密码"
name="password"
rules={[{ required: true }]}
hasFeedback
style={{ maxWidth: 300 }}
>
<Input type="password" placeholder="密码" />
</Form.Item>
<Button type="primary" htmlType="submit">
保存
</Button>
</Form>
</Tabs.TabPane>
<Tabs.TabPane tab="其他设置" key="theme">
<Form layout="vertical">
<Form.Item label="主题设置" name="theme" initialValue={theme}>
<Radio.Group
options={optionsWithDisabled}
onChange={themeChange}
value={theme}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>
</Form>
</Tabs.TabPane>
</Tabs>
</PageContainer>
);
};
export default Password;
+37
View File
@@ -0,0 +1,37 @@
export default {
siteName: '青龙控制面板',
apiPrefix: '/api/',
authKey: 'token',
/* Layout configuration, specify which layout to use for route. */
layouts: [
{
name: 'primary',
include: [/.*/],
exclude: [/(\/(en|zh))*\/login/],
},
],
/* I18n configuration, `languages` and `defaultLanguage` are required currently. */
i18n: {
/* Countrys flags: https://www.flaticon.com/packs/countrys-flags */
languages: [
{
key: 'pt-br',
title: 'Português',
flag: '/portugal.svg',
},
{
key: 'en',
title: 'English',
flag: '/america.svg',
},
{
key: 'zh',
title: '中文',
flag: '/china.svg',
},
],
defaultLanguage: 'en',
},
};
+39
View File
@@ -0,0 +1,39 @@
import { extend } from 'umi-request';
import { notification } from 'antd';
import config from './config';
const time = Date.now();
const errorHandler = function (error: any) {
if (error.response) {
const message = error.data
? error.data.message || error.data
: error.response.statusText;
if (error.response.status !== 401) {
notification.error({ message });
}
} else {
console.log(error.message);
}
throw error; // 如果throw. 错误将继续抛出.
};
const _request = extend({ timeout: 60000, params: { t: time }, errorHandler });
_request.interceptors.request.use((url, options) => {
const token = localStorage.getItem(config.authKey);
if (token) {
const headers = {
Authorization: `Bearer ${token}`,
};
return { url, options: { ...options, headers } };
}
return { url, options };
});
_request.interceptors.response.use(async (response) => {
const res = await response.clone();
return response;
});
export const request = _request;
+25
View File
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "es2017",
"lib": ["es2017", "esnext.asynciterable", "DOM"],
"typeRoots": [
"./node_modules/@types",
"./src/types",
"./node_modules/celebrate/lib/index.d.ts"
],
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"module": "commonjs",
"pretty": true,
"sourceMap": true,
"outDir": "./build",
"allowJs": true,
"noEmit": false,
"esModuleInterop": true
},
"include": ["./back/**/*"],
"exclude": ["node_modules", "tests"]
}
+42
View File
@@ -0,0 +1,42 @@
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"moduleResolution": "node",
"importHelpers": true,
"jsx": "react-jsx",
"esModuleInterop": true,
"sourceMap": true,
"baseUrl": "./",
"strict": true,
"paths": {
"@/*": ["src/*"],
"@@/*": ["src/.umi/*"]
},
"lib": ["dom", "es2017", "esnext.asynciterable"],
"typeRoots": [
"./node_modules/@types",
"./back/types",
"./node_modules/celebrate/lib/index.d.ts"
],
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"forceConsistentCasingInFileNames": true,
"pretty": true,
"allowJs": true,
"noEmit": false
},
"include": ["src/**/*", "config/**/*", ".umirc.ts", "typings.d.ts"],
"exclude": [
"node_modules",
"lib",
"es",
"dist",
"typings",
"**/__test__",
"test",
"docs",
"tests"
]
}
+8
View File
@@ -0,0 +1,8 @@
declare module '*.css';
declare module '*.less';
declare module '*.png';
declare module '*.svg' {
export function ReactComponent(props: React.SVGProps<SVGSVGElement>): React.ReactElement
const url: string
export default url
}