Compare commits

...
169 Commits
Author SHA1 Message Date
whyour 81e2bbb14c 更新版本 v2.15.10 2023-03-17 21:30:13 +08:00
whyour 970dff144c 修复订阅下载 http 协议文件 2023-03-15 21:44:11 +08:00
whyour 819b15b15e 容器增加 ipv6 检测,自动配置 nginx 2023-03-14 22:39:10 +08:00
whyour 3fa57c69ac 修改检查 pm2 状态 api 2023-03-13 23:57:56 +08:00
whyour 18689c8119 修复 shell 引入通用变量 2023-03-13 22:30:53 +08:00
adams549659584andGitHub 61e7049baa 修复系统 bark 推送铃声 (#1850) 2023-03-13 22:13:20 +08:00
whyour 39c40a328d 修复检查更新弹框逻辑 2023-03-12 21:48:31 +08:00
whyour 955d815d14 修改 task 获取 token 逻辑 2023-03-12 21:33:46 +08:00
adams549659584andGitHub 3a3b945de8 修复 js 下 bark 推送声音 bug (#1847) 2023-03-12 20:58:26 +08:00
whyour 73c53dbc8d 修改 typescript execute 2023-03-12 20:53:15 +08:00
whyour f76828d4ec 修改 shell 文件权限 2023-03-11 01:10:03 +08:00
whyour 7b2956eb0a 修复日志删除逻辑 2023-03-10 14:21:52 +08:00
whyour b2167ae5e4 修复自定义通知 2023-03-07 22:20:08 +08:00
whyour 98dfa4c425 更新版本 v2.15.9 2023-03-04 15:48:29 +08:00
whyour 5bf08b76de 修复系统更新源判断,回退 @monaco-editor/react 2023-03-03 23:45:38 +08:00
whyour 5c5b4f804e 更新 antd 2023-03-03 22:52:44 +08:00
whyour 6455245d11 更新 @umijs/max 2023-03-03 22:14:03 +08:00
pharaoh2012andGitHub 2ff3d9c601 根据标题跳过消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔 (#1833) 2023-03-03 21:59:44 +08:00
whyour d342fbf29f 修改登录日志表格样式 2023-03-03 11:58:06 +08:00
whyour f865018cd6 修改表格样式 2023-03-02 23:31:46 +08:00
whyour 3b9a4f0834 修复定时任务状态筛选 2023-03-02 23:16:18 +08:00
whyour 67bc305950 修复拉取仓库重置操作 2023-03-01 22:12:49 +08:00
whyour 97f128d59f 修改 ql extra 执行逻辑 2023-02-28 22:56:06 +08:00
whyour e25885c4d9 修改 nginx ipv6 配置 2023-02-27 23:30:00 +08:00
whyour c285026339 修改删除日志逻辑 2023-02-27 23:25:08 +08:00
whyour 6c34045a48 修改订阅名称必填 2023-02-26 23:36:32 +08:00
whyour e1e6261b4f 修复定时任务/视图/环境变量更新逻辑 2023-02-24 23:21:08 +08:00
whyour 697bcb5922 暂时去掉 vlist 2023-02-24 23:04:18 +08:00
whyour e05b0a2491 修复文件类型订阅重复添加任务 2023-02-18 14:38:13 +08:00
whyour e1655455d8 更新版本 v2.15.8 2023-02-17 23:11:41 +08:00
whyour 9921dab064 修复日志文件名格式化 2023-02-17 23:03:06 +08:00
whyour 403ab7f0f0 修改日志名称格式 2023-02-17 22:49:06 +08:00
whyour 9bc7ca4094 增加 NODE_PATH 环境变量 2023-02-15 22:27:40 +08:00
whyour e924aa76b0 修复环境变量位置重置逻辑 2023-02-15 11:54:51 +08:00
whyour 0c46606e14 修复添加文件类型订阅 2023-02-14 21:15:59 +08:00
whyour dbfa4049f5 修复订阅格式化参数 2023-02-14 11:10:25 +08:00
whyour 622fe2a8f8 修复订阅自动增加/删除任务默认值 2023-02-14 11:07:16 +08:00
whyour 7bce5c4f6a 任务增加关联订阅 2023-02-13 23:50:01 +08:00
whyour 1f7f2c8971 修复订阅生成 ssh 配置逻辑,自动添加/删除任务 2023-02-13 23:12:55 +08:00
whyour 437a849c2a 更新版本 v2.15.7 2023-02-12 17:09:07 +08:00
whyour 8f7261dd50 修复 workflow 2023-02-12 17:08:47 +08:00
whyour 76fa82c3a7 订阅支持自动添加和删除任务设置 2023-02-09 00:20:28 +08:00
whyour 46c9774efa 更新 workflow 2023-02-07 21:52:24 +08:00
whyour 1bc64a23e3 修复查看任务日志,列表位置重置 2023-02-07 21:29:56 +08:00
whyour cc9a17afa3 修复定时任务列表格式化 crontab 2023-02-06 23:26:59 +08:00
whyour fcbcfc3a03 更新 sqlite3 2023-02-06 16:24:31 +08:00
whyour 0144f3b131 修改任务日志自动滚动逻辑 2023-02-05 20:57:37 +08:00
whyour 518436d5ca 修复脚本管理跳转逻辑 2023-02-04 00:20:44 +08:00
whyour 1961a6b913 修复定时任务表格自动换行 2023-02-03 23:45:55 +08:00
whyour 5edebdd5f3 修复创建日志目录逻辑 2023-02-03 23:30:02 +08:00
whyour 48edf2c212 增加 ppc64le/s390x/386 镜像 2023-02-03 22:38:00 +08:00
whyour 9b16dca9da 修改定时任务状态宽度 2023-02-02 23:39:18 +08:00
whyour 9479c2498c 移除 console 2023-02-02 13:20:47 +08:00
whyour 4a18bcabe2 修复移动端新建脚本 2023-02-02 13:17:00 +08:00
whyour 34f690c53d 修改 shell 参数处理逻辑 2023-02-02 12:14:35 +08:00
whyour e70cd6579f 更新版本 v2.15.6 2023-02-01 22:46:23 +08:00
whyour 99c047e9c7 修复定时任务表格滚动 2023-02-01 22:45:47 +08:00
whyour 978ae326f8 更新 sqlite3 依赖 2023-02-01 15:30:53 +08:00
whyour b01f27f2d3 修改更新列表后刷新逻辑 2023-01-31 23:52:02 +08:00
whyour 4d34e0dd96 修复环境变量滚动卡顿 2023-01-31 23:46:42 +08:00
whyour 9b0065e533 修复定时任务过多搜索卡顿 2023-01-31 23:30:47 +08:00
whyour 34a51feee9 修改定时任务列表title 2023-01-31 17:42:30 +08:00
whyour 679091de59 修改定时任务输入提示 2023-01-31 17:41:35 +08:00
whyour c6220cb920 修改定时任务查询日志逻辑 2023-01-29 18:07:57 +08:00
whyour 633b30269c 修复定时任务脚本跳转 2023-01-29 10:57:08 +08:00
whyour 8ad26267d8 修改 task before/after 逻辑 2023-01-20 23:38:32 +08:00
OceanJiangandGitHub 7cfb125682 实现 Node.js 部分 SMTP 推送 (#1776) 2023-01-14 19:53:36 +08:00
Wankko ReeandGitHub a2dc5de5ce 添加基于 SMTP 邮件 的消息通知服务 (#1763) 2023-01-14 19:48:41 +08:00
whyour 4f41ff29ec 修改资源通知逻辑 2023-01-14 19:46:33 +08:00
whyour 13d95b8c19 修复日志滚动逻辑 2023-01-11 23:46:47 +08:00
whyour 335c13ab44 修改日志滚动逻辑 2023-01-11 23:44:04 +08:00
whyour 015cdc10a7 publicPath使用相对目录 2023-01-11 22:52:51 +08:00
whyour a5075ab28f 修改新建任务命令提示 2023-01-11 22:14:32 +08:00
whyour 2e3c3274c7 修复antd兼容性,日志详情自动滚动 2023-01-11 22:03:44 +08:00
whyour 443c581955 修改定时任务 shouldCellUpdate 2023-01-08 22:30:29 +08:00
whyour 5e41faf590 修改 monaco-editor 版本 2023-01-08 22:16:22 +08:00
whyour 0579fb83d3 修改环境变量值省略展示 2023-01-08 21:56:29 +08:00
whyour 2864845d5b 优化定时任务表格渲染 2023-01-08 18:15:18 +08:00
whyour 659dddace7 梅子留酸软齿牙,芭蕉分绿与窗纱。 2023-01-08 16:49:21 +08:00
whyour b096f30558 更新 dockerfile 2023-01-08 16:39:01 +08:00
whyour dcfb72143a 修复定时任务每页数据量大时搜索卡顿 2023-01-07 18:19:03 +08:00
whyour c63bcd7706 修改docker编译 2023-01-07 18:16:39 +08:00
whyour bac306f9b3 更新版本 v2.15.5 2023-01-04 21:09:52 +08:00
whyour a6fd96e97b 修复定时规则修改 2023-01-04 21:09:27 +08:00
whyour 7edd91f923 修改服务异常判断逻辑 2023-01-04 19:27:15 +08:00
whyour 66df445bd5 修改添加任务命令提示 2023-01-04 17:28:26 +08:00
whyour 5c03034bb4 修复定时任务不以task开头时,任务无效 2023-01-04 17:25:30 +08:00
whyour 0eab181d46 修改update逻辑 2022-12-29 12:15:25 +08:00
whyour f6f95308c7 monaco editor改为本地加载 2022-12-28 22:12:37 +08:00
whyour e93cad91b4 更新版本 v2.15.4 2022-12-28 11:15:20 +08:00
whyour 08ee61b179 修改 pub 脚本 2022-12-28 11:14:57 +08:00
whyour 0ab756665e 修改版本文件 2022-12-28 11:06:47 +08:00
whyour 3570cddce0 关于增加更新日志查看 2022-12-27 17:10:21 +08:00
whyour a2d9f1a5db 脚本管理支持重命名文件/文件夹 2022-12-27 16:02:15 +08:00
whyour 0c6a214e55 修复启动public服务命令 2022-12-27 12:24:24 +08:00
whyour 095fcdbe69 修复任务脚本快捷跳转 2022-12-17 12:46:26 +08:00
whyour 3afb1d18f6 更新版本 v2.15.3 2022-12-10 19:06:39 +08:00
whyour 4526c84330 任务视图支持标签筛选 2022-12-10 19:00:03 +08:00
whyour a9cc1cb4b9 修改token获取逻辑 2022-12-10 17:12:45 +08:00
whyour 7acf1eace3 修改添加任务视图筛选条件交互 2022-12-09 23:34:23 +08:00
whyour bb2e8cb287 修复调试保存文件默认目录和任务视图创建 2022-12-09 22:59:38 +08:00
whyour 3b389259c1 修复调试脚本日志 2022-12-06 00:22:05 +08:00
whyour 310ce55b09 修改默认基础镜像 2022-12-05 23:49:33 +08:00
CoolBoyandwhyour 4662e3fec2 修复notify.js中智能微秘书 (#1736) 2022-12-05 23:44:34 +08:00
whyour 10fab9d415 修改进程默认退出信号 2022-12-05 21:00:14 +08:00
whyour acecf01cbe 修复 killTask 返回值 2022-12-05 15:38:21 +08:00
whyour b95fb9cda4 修改退出进程逻辑 2022-12-05 15:26:22 +08:00
whyour 23fd595582 修改 timeout 命令默认信号 2022-12-05 13:18:10 +08:00
whyour 468d05cb10 更新版本 v2.15.2 2022-12-02 18:07:54 +08:00
whyour 88c2e5d160 更新 readme 2022-12-02 18:07:21 +08:00
whyour 8cf491b802 task 命令增加 -m 参数,支持设置任务超时时间 2022-12-01 18:26:24 +08:00
whyour 5886e78299 修复获取更新分支 2022-12-01 16:25:02 +08:00
whyour 591ef9781c 修改获取 cpu 占用 2022-12-01 16:16:17 +08:00
whyour 52fef96c1c 修改check逻辑 2022-11-30 21:29:58 +08:00
whyour d5fd9c57ec 修复shell语法 2022-11-29 22:50:11 +08:00
whyour 8a7d6a1c13 修改资源检查逻辑 2022-11-29 21:01:28 +08:00
whyour 1745c72981 修改 check/update 逻辑 2022-11-29 00:05:50 +08:00
whyour b98b7646cb 修复日志样式 2022-11-28 23:59:23 +08:00
whyour 1446e925ec 任务视图增加系统视图 2022-11-26 20:34:07 +08:00
whyour 799cda1e3e 移除未使用依赖 2022-11-24 22:09:33 +08:00
whyour d2e0231341 修改 workflow node_modules 缓存 2022-11-22 21:33:03 +08:00
whyour 9e0aa655a0 修改 workflow 依赖缓存 2022-11-22 21:00:53 +08:00
whyour e303c73d44 workflow 增加 digest 打印 2022-11-22 20:03:56 +08:00
whyour e5e8e66d6b 增加 docker action 缓存 2022-11-22 19:53:02 +08:00
whyour 0bb3381122 修复拉去仓库逻辑 2022-11-22 16:51:57 +08:00
whyour 2c5bf2bd6b 更新 readme 2022-11-22 14:42:27 +08:00
whyour 83763b00ae 上传 pnpm-lock 2022-11-22 13:41:39 +08:00
whyour 47fc795c97 增加action缓存 2022-11-22 13:37:44 +08:00
whyour 6a59bf7af3 更新版本 v2.15.1 2022-11-20 21:04:58 +08:00
whyour 6f185570d6 pushDeer支持自架版 2022-11-20 20:05:02 +08:00
whyour 8b8336d9d0 系统升级增加gitee源 2022-11-17 22:01:47 +08:00
whyour a1e1d6ed3f 更新 readme 2022-11-17 11:47:44 +08:00
whyour df3e209534 修改 workflow 2022-11-17 11:37:45 +08:00
whyour 851408860e 修复 workflow 2022-11-17 11:18:15 +08:00
whyour d92ea23b0e 修改 workflow 2022-11-17 11:11:05 +08:00
whyour d83641db92 修复workflow 2022-11-17 00:28:28 +08:00
whyour 48f262e1f3 增加 workflow 2022-11-17 00:23:24 +08:00
whyour bc49a7db50 同步gitee 2022-11-16 23:04:39 +08:00
whyour 49975b461c 修改typescript版本 2022-11-16 22:52:34 +08:00
whyour 3ccb635956 修复移动端最后更新时间展示 2022-11-16 22:34:28 +08:00
whyour 074a90e0ac 修改同步gitlab私钥 2022-11-16 00:02:40 +08:00
whyour 526f927436 增加 gitlab workflow 2022-11-15 23:55:28 +08:00
whyour fae7fad915 更新版本 v2.15.0 2022-11-15 12:15:06 +08:00
whyour 56132ceb8b 修复初始化新增字段 2022-11-15 12:14:33 +08:00
whyour 3c3fce0a5a 修复初始化表字段 2022-11-15 11:32:42 +08:00
whyour 8139384961 移除filterRelation默认值 2022-11-14 23:31:27 +08:00
whyour 734cc94048 修改filterRelation字段限制 2022-11-14 22:59:01 +08:00
whyour 91dbb7770d 修改订阅github ssh生成逻辑 2022-11-14 22:00:16 +08:00
whyour 707709837d 初始化更新不存在filterRelation的任务视图 2022-11-14 21:36:31 +08:00
whyour e868e65208 修改日志管理默认排序 2022-11-14 15:12:17 +08:00
whyour b19ad2a13b 修复debounce方法引入错误 2022-11-14 11:35:51 +08:00
whyour 98ccccf6ab 修复openapi token生成逻辑 2022-11-14 09:28:30 +08:00
whyour 0a6166c557 订阅增加代理参数 2022-11-13 23:58:42 +08:00
whyour 5ee00e52a8 修复新建视图筛选条件样式 2022-11-13 22:12:03 +08:00
whyour ff2b4e0b2f 订阅支持添加代理 2022-11-12 22:45:46 +08:00
whyour 8fdd8db51b 修改创建任务视图样式 2022-11-11 15:03:27 +08:00
whyour f113eb7ba8 修改系统设置样式 2022-11-11 14:55:09 +08:00
whyour 40bc25ed5f 修复获取最后commit时间 2022-11-10 10:52:57 +08:00
whyour cd3eab5d35 修复版本信息展示 2022-11-10 01:34:18 +08:00
whyour 7038e15ad2 支持定时任务视图筛选条件关系切换 2022-11-10 01:31:21 +08:00
whyour c72abd29ec 任务视图筛选条件增加条件关系 2022-11-09 20:43:22 +08:00
whyour 5efc3d2228 更新版本 v2.14.12 2022-11-05 16:43:28 +08:00
whyour 8ac3f83c79 修复可能出现移动或者创建环境变量出错 2022-11-05 16:41:19 +08:00
whyour b31b054d0c 更新版本 v2.14.11 2022-11-05 15:24:16 +08:00
whyour fb4a87f5ce 修复版本文件缓存 2022-11-05 15:23:17 +08:00
whyour 049c73880b 修改pre样式 2022-11-05 14:57:21 +08:00
whyour 7b2c54f6a6 修改版本信息获取 2022-11-05 12:14:46 +08:00
whyour 23bda39812 关于增加版本信息展示 2022-11-05 11:01:58 +08:00
whyour 90af5801ee 修复表格滚动 2022-11-04 19:14:03 +08:00
96 changed files with 18578 additions and 1533 deletions
+2 -1
View File
@@ -9,4 +9,5 @@ SECRET='whyour'
QINIU_AK='' QINIU_AK=''
QINIU_SK='' QINIU_SK=''
QINIU_SCOPE='' QINIU_SCOPE=''
TEMP=''
+64 -10
View File
@@ -5,28 +5,55 @@ on:
branches: branches:
- 'master' - 'master'
- 'develop' - 'develop'
# Sequence of patterns matched against refs/tags
tags: tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
schedule: schedule:
# 参考 https://jasonet.co/posts/scheduled-actions/ # 参考 https://jasonet.co/posts/scheduled-actions/
# note: 这里是GMT时间,北京时间减去八小时即可。如北京时间 22:30 => GMT 14:30 - cron: '00 14 * * *' # GMT 15:00 => 北京时间 23:00
# minute hour day month dayOfWeek
- cron: '00 14 * * *' # GMT 14:00 => 北京时间 22:00
#- cron: '30 16 * * *' # GMT 16:30(前一天) => 北京时间 00:30
workflow_dispatch: workflow_dispatch:
jobs: jobs:
to_gitlab:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url:
git@gitlab.com:whyour/qinglong.git
ssh_private_key:
${{ secrets.GITLAB_SSH_PK }}
to_gitee:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url:
git@gitee.com:whyour/qinglong.git
ssh_private_key:
${{ secrets.GITLAB_SSH_PK }}
build-static: build-static:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: latest
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with:
cache: 'pnpm'
- name: build front and back - name: build front and back
run: | run: |
npm i -g pnpm pnpm install --frozen-lockfile
pnpm install
pnpm build:front pnpm build:front
pnpm build:back pnpm build:back
@@ -34,6 +61,9 @@ jobs:
env: env:
GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static
GITHUB_BRANCH: ${{ github.ref_name }} GITHUB_BRANCH: ${{ github.ref_name }}
REPO_GITEE: git@gitee.com:whyour/qinglong-static.git
REPO_GITLAB: git@gitlab.com:whyour/qinglong-static.git
PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}
run: | run: |
mkdir -p tmp mkdir -p tmp
cd ./tmp cd ./tmp
@@ -44,7 +74,19 @@ jobs:
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')" git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH} git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH}
mkdir -p ~/.ssh
echo "${PRIVATE_KEY}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
export GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no -l git"
git remote add gitee "${REPO_GITEE}"
git remote add gitlab "${REPO_GITLAB}"
git push --force --quiet gitee ${GITHUB_BRANCH}:${GITHUB_BRANCH}
git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH}
build: build:
if: github.ref != 'refs/heads/master'
needs: build-static needs: build-static
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -55,7 +97,12 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: latest
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with:
cache: 'pnpm'
- name: Setup timezone - name: Setup timezone
uses: zcong1993/setup-timezone@master uses: zcong1993/setup-timezone@master
@@ -98,7 +145,7 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: Build and push - name: Build and push
id: docker_build id: docker_build
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
@@ -106,9 +153,16 @@ jobs:
build-args: | build-args: |
MAINTAINER=${{ github.repository_owner }} MAINTAINER=${{ github.repository_owner }}
QL_BRANCH=${{ github.ref_name }} QL_BRANCH=${{ github.ref_name }}
SOURCE_COMMIT=${{ github.sha }}
network: host network: host
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386
context: docker/ context: .
file: ./docker/Dockerfile
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=whyour/qinglong:cache
cache-to: type=registry,ref=whyour/qinglong:cache,mode=max
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
-1
View File
@@ -5,7 +5,6 @@
/npm-debug.log* /npm-debug.log*
/yarn-error.log /yarn-error.log
/yarn.lock /yarn.lock
/pnpm-lock.yaml
/package-lock.json /package-lock.json
# production # production
+2 -1
View File
@@ -6,10 +6,11 @@ export default defineConfig({
antd: {}, antd: {},
outputPath: 'static/dist', outputPath: 'static/dist',
fastRefresh: true, fastRefresh: true,
favicons: ['/images/favicon.svg'], favicons: ['./images/favicon.svg'],
mfsu: { mfsu: {
strategy: 'eager', strategy: 'eager',
}, },
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
proxy: { proxy: {
'/api/public': { '/api/public': {
target: 'http://127.0.0.1:5400/', target: 'http://127.0.0.1:5400/',
+8 -3
View File
@@ -22,7 +22,7 @@ Timed task management panel with python3, javaScript, shell, typescript support
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong [docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
</div> </div>
[![](https://user-images.githubusercontent.com/22700758/161788855-c4e51cb8-d4e9-44fe-bb17-ee1a56c8549b.png)](https://whyour.cn) [![](https://user-images.githubusercontent.com/22700758/203243067-1a8a570d-b1b4-4837-9f12-d78d83e31f35.jpg)](https://whyour.cn)
[简体中文](./README.md) | English [简体中文](./README.md) | English
@@ -41,7 +41,7 @@ Timed task management panel with python3, javaScript, shell, typescript support
### Local Deployment ### Local Deployment
```bash ```bash
# To be completed # To be refined, see the development steps first (not supported on windows yet)
``` ```
### Podman Deployment ### Podman Deployment
@@ -160,7 +160,11 @@ task <file_path> now
# the foreground does not generate the day, directly recorded in the log file, and can be specified account execution # the foreground does not generate the day, directly recorded in the log file, and can be specified account execution
task <file_path> conc <env_name> <account_number>(Optional) task <file_path> conc <env_name> <account_number>(Optional)
# Specify the account to execute and run immediately regardless of whether a random delay is set # Specify the account to execute and run immediately regardless of whether a random delay is set
task <file_path> desi <env_name> <account_number> task <file_path> desi <env_name> <account_number>
# Set task timeout
task -m <max_time> <file_path>
# Print task log in real time, no need to carry this parameter when creating timed tasks
task -l <file_path>
``` ```
2. parameter description 2. parameter description
@@ -175,6 +179,7 @@ task <file_path> desi <env_name> <account_number>
* file_path: File path for task execution * file_path: File path for task execution
* env_name: The name of the environment variable that needs to be concurrent or specified at the time of task execution * env_name: The name of the environment variable that needs to be concurrent or specified at the time of task execution
* account_number: Specify the account number of an environment variable to be executed when the task is executed * account_number: Specify the account number of an environment variable to be executed when the task is executed
* max_time: Timeout, suffix "s" for seconds (default), "m" for minutes, "h" for hours, "d" for days
## Links ## Links
+8 -3
View File
@@ -22,7 +22,7 @@
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong [docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
</div> </div>
[![](https://user-images.githubusercontent.com/22700758/161788855-c4e51cb8-d4e9-44fe-bb17-ee1a56c8549b.png)](https://whyour.cn) [![](https://user-images.githubusercontent.com/22700758/203243067-1a8a570d-b1b4-4837-9f12-d78d83e31f35.jpg)](https://whyour.cn)
简体中文 | [English](./README-en.md) 简体中文 | [English](./README-en.md)
@@ -41,7 +41,7 @@
### 本机部署 ### 本机部署
```bash ```bash
# 待完善 # 待完善,可先参考开发步骤 (windows暂时不支持)
``` ```
### podman 部署 ### podman 部署
@@ -159,7 +159,11 @@ task <file_path> now
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行 # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
task <file_path> conc <env_name> <account_number>(可选的) task <file_path> conc <env_name> <account_number>(可选的)
# 指定账号执行,无论是否设置了随机延迟,均立即运行 # 指定账号执行,无论是否设置了随机延迟,均立即运行
task <file_path> desi <env_name> <account_number> task <file_path> desi <env_name> <account_number>
# 设置任务超时时间
task -m <max_time> <file_path>
# 实时打印任务日志,创建定时任务时,不用携带此参数
task -l <file_path>
``` ```
2. 参数说明 2. 参数说明
@@ -175,6 +179,7 @@ task <file_path> desi <env_name> <account_number>
* file_path: 任务执行时的文件路径 * file_path: 任务执行时的文件路径
* env_name: 任务执行时需要并发或者指定时的环境变量名称 * env_name: 任务执行时需要并发或者指定时的环境变量名称
* account_number: 任务执行时指定某个环境变量需要执行的账号序号 * account_number: 任务执行时指定某个环境变量需要执行的账号序号
* max_time: 超时时间,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
## 链接 ## 链接
+25 -2
View File
@@ -30,6 +30,7 @@ export default (app: Router) => {
name: Joi.string().required(), name: Joi.string().required(),
sorts: Joi.array().optional().allow(null), sorts: Joi.array().optional().allow(null),
filters: Joi.array().optional(), filters: Joi.array().optional(),
filterRelation: Joi.string().optional(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -51,13 +52,18 @@ export default (app: Router) => {
id: Joi.number().required(), id: Joi.number().required(),
sorts: Joi.array().optional().allow(null), sorts: Joi.array().optional().allow(null),
filters: Joi.array().optional(), filters: Joi.array().optional(),
filterRelation: Joi.string().optional(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
try { try {
const cronViewService = Container.get(CronViewService); const cronViewService = Container.get(CronViewService);
const data = await cronViewService.update(req.body); if (req.body.type === 1) {
return res.send({ code: 200, data }); return res.send({ code: 400, message: '参数错误' });
} else {
const data = await cronViewService.update(req.body);
return res.send({ code: 200, data });
}
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
@@ -146,6 +152,21 @@ export default (app: Router) => {
} }
}); });
route.get(
'/detail',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.find(req.query as any);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post( route.post(
'/', '/',
celebrate({ celebrate({
@@ -154,6 +175,7 @@ export default (app: Router) => {
schedule: Joi.string().required(), schedule: Joi.string().required(),
name: Joi.string().optional(), name: Joi.string().optional(),
labels: Joi.array().optional(), labels: Joi.array().optional(),
sub_id: Joi.number().optional().allow(null),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -310,6 +332,7 @@ export default (app: Router) => {
command: Joi.string().required(), command: Joi.string().required(),
schedule: Joi.string().required(), schedule: Joi.string().required(),
name: Joi.string().optional().allow(null), name: Joi.string().optional().allow(null),
sub_id: Joi.number().optional().allow(null),
id: Joi.number().required(), id: Joi.number().required(),
}), }),
}), }),
+32 -5
View File
@@ -170,7 +170,7 @@ export default (app: Router) => {
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
path: Joi.string().allow(''), path: Joi.string().allow(''),
type: Joi.string().optional() type: Joi.string().optional(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -183,7 +183,7 @@ export default (app: Router) => {
}; };
const filePath = join(config.scriptPath, path, filename); const filePath = join(config.scriptPath, path, filename);
if (type === 'directory') { if (type === 'directory') {
emptyDir(filePath); emptyDir(filePath);
} else { } else {
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
} }
@@ -255,23 +255,50 @@ export default (app: Router) => {
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
content: Joi.string().optional().allow(''),
path: Joi.string().optional().allow(''), path: Joi.string().optional().allow(''),
pid: Joi.number().optional().allow(''),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
let { filename, content, path } = req.body; let { filename, path, pid } = req.body;
const { name, ext } = parse(filename); const { name, ext } = parse(filename);
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`); const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
const scriptService = Container.get(ScriptService); const scriptService = Container.get(ScriptService);
const result = await scriptService.stopScript(filePath); const result = await scriptService.stopScript(filePath, pid);
res.send(result); res.send(result);
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
}, },
); );
route.put(
'/rename',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
newFilename: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
let { filename, path, type, newFilename } = req.body as {
filename: string;
path: string;
type: string;
newFilename: string;
};
const filePath = join(config.scriptPath, path, filename);
const newPath = join(config.scriptPath, path, newFilename);
fs.renameSync(filePath, newPath);
res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
}; };
+6
View File
@@ -49,6 +49,9 @@ export default (app: Router) => {
sub_after: Joi.string().optional().allow('').allow(null), sub_after: Joi.string().optional().allow('').allow(null),
schedule_type: Joi.string().required(), schedule_type: Joi.string().required(),
alias: Joi.string().required(), alias: Joi.string().required(),
proxy: Joi.string().optional().allow('').allow(null),
autoAddCron: Joi.boolean().optional().allow('').allow(null),
autoDelCron: Joi.boolean().optional().allow('').allow(null),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -177,6 +180,9 @@ export default (app: Router) => {
sub_before: Joi.string().optional().allow('').allow(null), sub_before: Joi.string().optional().allow('').allow(null),
sub_after: Joi.string().optional().allow('').allow(null), sub_after: Joi.string().optional().allow('').allow(null),
alias: Joi.string().required(), alias: Joi.string().required(),
proxy: Joi.string().optional().allow('').allow(null),
autoAddCron: Joi.boolean().optional().allow('').allow(null),
autoDelCron: Joi.boolean().optional().allow('').allow(null),
id: Joi.number().required(), id: Joi.number().required(),
}), }),
}), }),
+24 -4
View File
@@ -7,6 +7,9 @@ import SystemService from '../services/system';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import UserService from '../services/user'; import UserService from '../services/user';
import { EnvModel } from '../data/env'; import { EnvModel } from '../data/env';
import { parseVersion, promiseExec } from '../config/util';
import dayjs from 'dayjs';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
@@ -18,10 +21,22 @@ export default (app: Router) => {
const userService = Container.get(UserService); const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo(); const authInfo = await userService.getUserInfo();
const envCount = await EnvModel.count(); const envCount = await EnvModel.count();
const versionRegx = /.*export const version = \'(.*)\'\;/; const { version, changeLog, changeLogLink } = await parseVersion(
config.versionFile,
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8'); );
const version = currentVersionFile.match(versionRegx)![1]; const lastCommitTime = (
await promiseExec(
`cd ${config.rootPath} && git show -s --format=%ai | head -1`,
)
).replace('\n', '');
const lastCommitId = (
await promiseExec(`cd ${config.rootPath} && git rev-parse --short HEAD`)
).replace('\n', '');
const branch = (
await promiseExec(
`cd ${config.rootPath} && git symbolic-ref --short HEAD`,
)
).replace('\n', '');
let isInitialized = true; let isInitialized = true;
if ( if (
@@ -37,6 +52,11 @@ export default (app: Router) => {
data: { data: {
isInitialized, isInitialized,
version, version,
lastCommitTime: dayjs(lastCommitTime).unix(),
lastCommitId,
branch,
changeLog,
changeLogLink,
}, },
}); });
} catch (e) { } catch (e) {
+7 -1
View File
@@ -1 +1,7 @@
export const LOG_END_SYMBOL = '\n          '; export const LOG_END_SYMBOL = '     ';
export const TASK_COMMAND = 'task';
export const QL_COMMAND = 'ql';
export const TASK_PREFIX = `${TASK_COMMAND} `;
export const QL_PREFIX = `${QL_COMMAND} `;
+2 -2
View File
@@ -14,7 +14,7 @@ if (!process.env.QL_DIR) {
process.env.QL_DIR = qlHomePath.replace(/\/$/g, ''); process.env.QL_DIR = qlHomePath.replace(/\/$/g, '');
} }
const lastVersionFile = `https://qn.whyour.cn/version.ts?v=${Date.now()}`; const lastVersionFile = `https://qn.whyour.cn/version.yaml`;
const rootPath = process.env.QL_DIR as string; const rootPath = process.env.QL_DIR as string;
const envFound = dotenv.config({ path: path.join(rootPath, '.env') }); const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
@@ -40,7 +40,7 @@ const sqliteFile = path.join(samplePath, 'database.sqlite');
const authError = '错误的用户名密码,请重试'; const authError = '错误的用户名密码,请重试';
const loginFaild = '请先登录!'; const loginFaild = '请先登录!';
const configString = 'config sample crontab shareCode diy'; const configString = 'config sample crontab shareCode diy';
const versionFile = path.join(rootPath, 'src/version.ts'); const versionFile = path.join(rootPath, 'version.yaml');
if (envFound.error) { if (envFound.error) {
throw new Error("⚠️ Couldn't find .env file ⚠️"); throw new Error("⚠️ Couldn't find .env file ⚠️");
+42
View File
@@ -0,0 +1,42 @@
import { Subscription } from '../data/subscription';
import isNil from 'lodash/isNil';
export function formatUrl(doc: Subscription) {
let url = doc.url;
let host = '';
if (doc.type === 'private-repo') {
if (doc.pull_type === 'ssh-key') {
host = doc.url!.replace(/.*\@([^\:]+)\:.*/, '$1');
url = doc.url!.replace(host, doc.alias);
} else {
host = doc.url!.replace(/.*\:\/\/([^\/]+)\/.*/, '$1');
const { username, password } = doc.pull_option as any;
url = doc.url!.replace(host, `${username}:${password}@${host}`);
}
}
return { url, host };
}
export function formatCommand(doc: Subscription, url?: string) {
let command = `SUB_ID=${doc.id} ql `;
let _url = url || formatUrl(doc).url;
const {
type,
whitelist,
blacklist,
dependences,
branch,
extensions,
proxy,
autoAddCron,
autoDelCron,
} = doc;
if (type === 'file') {
command += `raw "${_url}"`;
} else {
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${dependences || ''
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}" "${isNil(autoAddCron) ? true : Boolean(autoAddCron)
}" "${isNil(autoDelCron) ? true : Boolean(autoDelCron)}"`;
}
return command;
}
+63 -7
View File
@@ -4,6 +4,9 @@ import got from 'got';
import iconv from 'iconv-lite'; import iconv from 'iconv-lite';
import { exec } from 'child_process'; import { exec } from 'child_process';
import FormData from 'form-data'; import FormData from 'form-data';
import psTreeFun from 'pstree.remy';
import { promisify } from 'util';
import { load } from 'js-yaml';
export function getFileContentByName(fileName: string) { export function getFileContentByName(fileName: string) {
if (fs.existsSync(fileName)) { if (fs.existsSync(fileName)) {
@@ -284,6 +287,20 @@ enum FileType {
'file', 'file',
} }
interface IFile {
title: string;
key: string;
type: 'directory' | 'file';
parent: string;
mtime: number;
children?: IFile[];
}
export function dirSort(a: IFile, b: IFile) {
if (a.type !== b.type) return FileType[a.type] < FileType[b.type] ? -1 : 1;
else if (a.mtime !== b.mtime) return a.mtime > b.mtime ? -1 : 1;
}
export function readDirs( export function readDirs(
dir: string, dir: string,
baseDir: string = '', baseDir: string = '',
@@ -303,10 +320,8 @@ export function readDirs(
key, key,
type: 'directory', type: 'directory',
parent: relativePath, parent: relativePath,
children: readDirs(subPath, baseDir).sort( mtime: stats.mtime.getTime(),
(a: any, b: any) => children: readDirs(subPath, baseDir).sort(dirSort),
(FileType as any)[a.type] - (FileType as any)[b.type],
),
}; };
} }
return { return {
@@ -315,11 +330,10 @@ export function readDirs(
isLeaf: true, isLeaf: true,
key, key,
parent: relativePath, parent: relativePath,
mtime: stats.mtime.getTime(),
}; };
}); });
return result.sort( return result.sort(dirSort);
(a: any, b: any) => (FileType as any)[a.type] - (FileType as any)[b.type],
);
} }
export function readDir( export function readDir(
@@ -441,3 +455,45 @@ export function parseBody(
return parsed; return parsed;
} }
export function psTree(pid: number): Promise<number[]> {
return new Promise((resolve, reject) => {
psTreeFun(pid, (err: any, pids: number[]) => {
if (err) {
reject(err);
}
resolve(pids.filter((x) => !isNaN(x)));
});
});
}
export async function killTask(pid: number) {
const pids = await psTree(pid);
// SIGALRM 14 时钟信号
if (pids.length) {
process.kill(pids[0], 14);
} else {
process.kill(pid, 14);
}
}
export async function getPid(name: string) {
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
const execAsync = promisify(exec);
let pid = (await execAsync(taskCommand)).stdout;
return Number(pid);
}
interface IVersion {
version: string;
changeLogLink: string;
changeLog: string;
}
export async function parseVersion(path: string): Promise<IVersion> {
return load(await promisify(fs.readFile)(path, 'utf8')) as IVersion;
}
export async function parseContentVersion(content: string): Promise<IVersion> {
return load(content) as IVersion;
}
+3
View File
@@ -17,6 +17,7 @@ export class Crontab {
labels?: string[]; labels?: string[];
last_running_time?: number; last_running_time?: number;
last_execution_time?: number; last_execution_time?: number;
sub_id?: number;
constructor(options: Crontab) { constructor(options: Crontab) {
this.name = options.name; this.name = options.name;
@@ -37,6 +38,7 @@ export class Crontab {
this.labels = options.labels || []; this.labels = options.labels || [];
this.last_running_time = options.last_running_time || 0; this.last_running_time = options.last_running_time || 0;
this.last_execution_time = options.last_execution_time || 0; this.last_execution_time = options.last_execution_time || 0;
this.sub_id = options.sub_id;
} }
} }
@@ -72,4 +74,5 @@ export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
labels: DataTypes.JSON, labels: DataTypes.JSON,
last_running_time: DataTypes.NUMBER, last_running_time: DataTypes.NUMBER,
last_execution_time: DataTypes.NUMBER, last_execution_time: DataTypes.NUMBER,
sub_id: { type: DataTypes.NUMBER, allowNull: true },
}); });
+16 -1
View File
@@ -1,13 +1,19 @@
import { sequelize } from '.'; import { sequelize } from '.';
import { DataTypes, Model } from 'sequelize'; import { DataTypes, Model } from 'sequelize';
export enum CronViewType {
'系统' = 1,
'个人',
}
interface SortType { interface SortType {
type: 'ASC' | 'DESC'; type: 'ASC' | 'DESC';
value: string; value: string;
} }
interface FilterType { interface FilterType {
type: 'or' | 'and'; property: string;
operation: string;
value: string; value: string;
} }
@@ -18,6 +24,8 @@ export class CrontabView {
isDisabled?: 1 | 0; isDisabled?: 1 | 0;
filters?: FilterType[]; filters?: FilterType[];
sorts?: SortType[]; sorts?: SortType[];
filterRelation?: 'and' | 'or';
type?: CronViewType;
constructor(options: CrontabView) { constructor(options: CrontabView) {
this.name = options.name; this.name = options.name;
@@ -26,6 +34,8 @@ export class CrontabView {
this.isDisabled = options.isDisabled || 0; this.isDisabled = options.isDisabled || 0;
this.filters = options.filters; this.filters = options.filters;
this.sorts = options.sorts; this.sorts = options.sorts;
this.filterRelation = options.filterRelation;
this.type = options.type || CronViewType.;
} }
} }
@@ -43,5 +53,10 @@ export const CrontabViewModel = sequelize.define<CronViewInstance>(
isDisabled: DataTypes.NUMBER, isDisabled: DataTypes.NUMBER,
filters: DataTypes.JSON, filters: DataTypes.JSON,
sorts: DataTypes.JSON, sorts: DataTypes.JSON,
filterRelation: {
type: DataTypes.STRING,
allowNull: true,
},
type: DataTypes.NUMBER,
}, },
); );
+1 -1
View File
@@ -28,7 +28,7 @@ export enum EnvStatus {
export const maxPosition = 9000000000000000; export const maxPosition = 9000000000000000;
export const initPosition = 4500000000000000; export const initPosition = 4500000000000000;
export const stepPosition = 10000000; export const stepPosition = 10000000000;
export const minPosition = 100; export const minPosition = 100;
interface EnvInstance extends Model<Env, Env>, Env {} interface EnvInstance extends Model<Env, Env>, Env {}
+1
View File
@@ -41,6 +41,7 @@ export class ServerChanNotification extends NotificationBaseInfo {
export class PushDeerNotification extends NotificationBaseInfo { export class PushDeerNotification extends NotificationBaseInfo {
public pushDeerKey = ''; public pushDeerKey = '';
public pushDeerUrl = '';
} }
export class ChatNotification extends NotificationBaseInfo { export class ChatNotification extends NotificationBaseInfo {
+9
View File
@@ -28,6 +28,9 @@ export class Subscription {
extensions?: string; extensions?: string;
sub_before?: string; sub_before?: string;
sub_after?: string; sub_after?: string;
proxy?: string;
autoAddCron?: 1 | 0;
autoDelCron?: 1 | 0;
constructor(options: Subscription) { constructor(options: Subscription) {
this.id = options.id; this.id = options.id;
@@ -54,6 +57,9 @@ export class Subscription {
this.extensions = options.extensions; this.extensions = options.extensions;
this.sub_before = options.sub_before; this.sub_before = options.sub_before;
this.sub_after = options.sub_after; this.sub_after = options.sub_after;
this.proxy = options.proxy;
this.autoAddCron = options.autoAddCron ? 1 : 0;
this.autoDelCron = options.autoDelCron ? 1 : 0;
} }
} }
@@ -102,5 +108,8 @@ export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
log_path: DataTypes.STRING, log_path: DataTypes.STRING,
schedule_type: DataTypes.STRING, schedule_type: DataTypes.STRING,
alias: { type: DataTypes.STRING, unique: 'alias' }, alias: { type: DataTypes.STRING, unique: 'alias' },
proxy: { type: DataTypes.STRING, allowNull: true },
autoAddCron: { type: DataTypes.NUMBER, allowNull: true },
autoDelCron: { type: DataTypes.NUMBER, allowNull: true },
}, },
); );
+28 -6
View File
@@ -10,6 +10,7 @@ import { fileExist } from '../config/util';
import { SubscriptionModel } from '../data/subscription'; import { SubscriptionModel } from '../data/subscription';
import { CrontabViewModel } from '../data/cronView'; import { CrontabViewModel } from '../data/cronView';
import config from '../config'; import config from '../config';
import { sequelize } from '../data';
export default async () => { export default async () => {
try { try {
@@ -21,12 +22,33 @@ export default async () => {
await SubscriptionModel.sync(); await SubscriptionModel.sync();
await CrontabViewModel.sync(); await CrontabViewModel.sync();
// try { // 初始化新增字段
// const queryInterface = sequelize.getQueryInterface(); try {
// await queryInterface.addIndex('Crontabs', ['command'], { unique: true }); await sequelize.query(
// await queryInterface.addIndex('Envs', ['name', 'value'], { unique: true }); 'alter table CrontabViews add column filterRelation VARCHAR(255)',
// await queryInterface.addIndex('Apps', ['name'], { unique: true }); );
// } catch (error) { } } catch (error) {}
try {
await sequelize.query(
'alter table Subscriptions add column proxy VARCHAR(255)',
);
} catch (error) {}
try {
await sequelize.query('alter table CrontabViews add column type NUMBER');
} catch (error) {}
try {
await sequelize.query(
'alter table Subscriptions add column autoAddCron NUMBER',
);
} catch (error) {}
try {
await sequelize.query(
'alter table Subscriptions add column autoDelCron NUMBER',
);
} catch (error) {}
try {
await sequelize.query('alter table Crontabs add column sub_id NUMBER');
} catch (error) {}
// 2.10-2.11 升级 // 2.10-2.11 升级
const cronDbFile = path.join(config.rootPath, 'db/crontab.db'); const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
+16
View File
@@ -8,12 +8,28 @@ import groupBy from 'lodash/groupBy';
import { DependenceModel } from '../data/dependence'; import { DependenceModel } from '../data/dependence';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import config from '../config'; import config from '../config';
import { CrontabViewModel, CronViewType } from '../data/cronView';
import { initPosition } from '../data/env';
export default async () => { export default async () => {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
// 初始化新增默认全部任务视图
CrontabViewModel.findAll({
where: { type: CronViewType., name: '全部任务' },
raw: true,
}).then((docs) => {
if (docs.length === 0) {
CrontabViewModel.create({
name: '全部任务',
type: CronViewType.,
position: initPosition / 2,
});
}
});
// 初始化更新所有任务状态为空闲 // 初始化更新所有任务状态为空闲
await CrontabModel.update( await CrontabModel.update(
{ status: CrontabStatus.idle }, { status: CrontabStatus.idle },
+7 -9
View File
@@ -11,7 +11,7 @@ export default async () => {
const subscriptionService = Container.get(SubscriptionService); const subscriptionService = Container.get(SubscriptionService);
// 生成内置token // 生成内置token
let tokenCommand = `ts-node-transpile-only ${config.rootPath}/back/token.ts`; let tokenCommand = `tsx ${config.rootPath}/back/token.ts`;
const tokenFile = `${config.rootPath}static/build/token.js`; const tokenFile = `${config.rootPath}static/build/token.js`;
if (await fileExist(tokenFile)) { if (await fileExist(tokenFile)) {
tokenCommand = `node ${tokenFile}`; tokenCommand = `node ${tokenFile}`;
@@ -21,6 +21,7 @@ export default async () => {
name: '生成token', name: '生成token',
command: tokenCommand, command: tokenCommand,
}; };
await scheduleService.cancelIntervalTask(cron);
scheduleService.createIntervalTask(cron, { scheduleService.createIntervalTask(cron, {
days: 28, days: 28,
}); });
@@ -28,24 +29,21 @@ export default async () => {
// 运行删除日志任务 // 运行删除日志任务
const data = await systemService.getLogRemoveFrequency(); const data = await systemService.getLogRemoveFrequency();
if (data && data.info && data.info.frequency) { if (data && data.info && data.info.frequency) {
const cron = { const rmlogCron = {
id: data.id, id: data.id,
name: '删除日志', name: '删除日志',
command: `ql rmlog ${data.info.frequency}`, command: `ql rmlog ${data.info.frequency}`,
}; };
scheduleService.createIntervalTask(cron, { await scheduleService.cancelIntervalTask(rmlogCron);
scheduleService.createIntervalTask(rmlogCron, {
days: data.info.frequency, days: data.info.frequency,
}); });
} }
// 运行所有订阅 // 运行所有订阅
await subscriptionService.setSshConfig();
const subs = await subscriptionService.list(); const subs = await subscriptionService.list();
for (const sub of subs) { for (const sub of subs) {
await subscriptionService.handleTask( subscriptionService.handleTask(sub, !sub.is_disabled, !sub.is_disabled);
sub,
!sub.is_disabled,
true,
!sub.is_disabled,
);
} }
}; };
+4 -6
View File
@@ -4,13 +4,11 @@ import * as Tracing from '@sentry/tracing';
import Logger from './logger'; import Logger from './logger';
import config from '../config'; import config from '../config';
import fs from 'fs'; import fs from 'fs';
import { parseVersion } from '../config/util';
export default ({ expressApp }: { expressApp: Application }) => { export default async ({ expressApp }: { expressApp: Application }) => {
const versionRegx = /.*export const version = \'(.*)\'\;/; const { version } = await parseVersion(config.versionFile);
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
Sentry.init({ Sentry.init({
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819', dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
integrations: [ integrations: [
@@ -18,7 +16,7 @@ export default ({ expressApp }: { expressApp: Application }) => {
new Tracing.Integrations.Express({ app: expressApp }), new Tracing.Integrations.Express({ app: expressApp }),
], ],
tracesSampleRate: 0.1, tracesSampleRate: 0.1,
release: currentVersion, release: version,
}); });
expressApp.use(Sentry.Handlers.requestHandler()); expressApp.use(Sentry.Handlers.requestHandler());
+6 -9
View File
@@ -6,15 +6,12 @@ import config from './config';
const app = express(); const app = express();
app.get('/api/public/panel/log', (req, res) => { app.get('/api/public/panel/log', (req, res) => {
exec( exec('tail -n 300 ~/.pm2/logs/panel-error.log', (err, stdout, stderr) => {
'pm2 logs panel --lines 500 --nostream --timestamp', if (err || stderr) {
(err, stdout, stderr) => { return res.send({ code: 400, message: (err && err.message) || stderr });
if (err || stderr) { }
return res.send({ code: 400, message: (err && err.message) || stderr }); return res.send({ code: 200, data: stdout });
} });
return res.send({ code: 200, data: stdout });
},
);
}); });
app app
+6 -2
View File
@@ -4,6 +4,7 @@ import { exec } from 'child_process';
import Logger from './loaders/logger'; import Logger from './loaders/logger';
import { CrontabModel, CrontabStatus } from './data/cron'; import { CrontabModel, CrontabStatus } from './data/cron';
import config from './config'; import config from './config';
import { QL_PREFIX, TASK_PREFIX } from './config/const';
const app = express(); const app = express();
@@ -23,8 +24,11 @@ const run = async () => {
) { ) {
schedule.scheduleJob(task.schedule, function () { schedule.scheduleJob(task.schedule, function () {
let command = task.command as string; let command = task.command as string;
if (!command.includes('task ') && !command.includes('ql ')) { if (
command = `task ${command}`; !command.startsWith(TASK_PREFIX) &&
!command.startsWith(QL_PREFIX)
) {
command = `${TASK_PREFIX}${command}`;
} }
exec(`ID=${task.id} ${command}`); exec(`ID=${task.id} ${command}`);
}); });
+71 -157
View File
@@ -5,17 +5,20 @@ import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import { exec, execSync, spawn } from 'child_process'; import { exec, execSync, spawn } from 'child_process';
import fs from 'fs'; import fs from 'fs';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import { getFileContentByName, concurrentRun, fileExist } from '../config/util'; import {
getFileContentByName,
concurrentRun,
fileExist,
killTask,
} from '../config/util';
import { promises, existsSync } from 'fs'; import { promises, existsSync } from 'fs';
import { promisify } from 'util'; import { Op, where, col as colFn } from 'sequelize';
import { Op } from 'sequelize';
import path from 'path'; import path from 'path';
import dayjs from 'dayjs'; import { TASK_PREFIX, QL_PREFIX } from '../config/const';
import { LOG_END_SYMBOL } from '../config/const';
@Service() @Service()
export default class CronService { export default class CronService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) { }
private isSixCron(cron: Crontab) { private isSixCron(cron: Crontab) {
const { schedule } = cron; const { schedule } = cron;
@@ -29,7 +32,7 @@ export default class CronService {
const tab = new Crontab(payload); const tab = new Crontab(payload);
tab.saved = false; tab.saved = false;
const doc = await this.insert(tab); const doc = await this.insert(tab);
await this.set_crontab(this.isSixCron(doc)); await this.set_crontab();
return doc; return doc;
} }
@@ -38,9 +41,10 @@ export default class CronService {
} }
public async update(payload: Crontab): Promise<Crontab> { public async update(payload: Crontab): Promise<Crontab> {
payload.saved = false; const tab = new Crontab(payload);
const newDoc = await this.updateDb(payload); tab.saved = false;
await this.set_crontab(this.isSixCron(newDoc)); const newDoc = await this.updateDb(tab);
await this.set_crontab();
return newDoc; return newDoc;
} }
@@ -79,7 +83,7 @@ export default class CronService {
public async remove(ids: number[]) { public async remove(ids: number[]) {
await CrontabModel.destroy({ where: { id: ids } }); await CrontabModel.destroy({ where: { id: ids } });
await this.set_crontab(true); await this.set_crontab();
} }
public async pin(ids: number[]) { public async pin(ids: number[]) {
@@ -116,8 +120,9 @@ export default class CronService {
private formatViewQuery(query: any, viewQuery: any) { private formatViewQuery(query: any, viewQuery: any) {
if (viewQuery.filters && viewQuery.filters.length > 0) { if (viewQuery.filters && viewQuery.filters.length > 0) {
if (!query[Op.and]) { const primaryOperate = viewQuery.filterRelation === 'or' ? Op.or : Op.and;
query[Op.and] = []; if (!query[primaryOperate]) {
query[primaryOperate] = [];
} }
for (const col of viewQuery.filters) { for (const col of viewQuery.filters) {
const { property, value, operation } = col; const { property, value, operation } = col;
@@ -160,13 +165,27 @@ export default class CronService {
} }
if (operate && operate2) { if (operate && operate2) {
q[property] = { q[property] = {
[operate2]: [ [Op.or]: [
{ [operate]: `%${value}%` }, {
{ [operate]: `%${encodeURIComponent(value)}%` }, [operate2]: [
{ [operate]: `%${value}%` },
{ [operate]: `%${encodeURIComponent(value)}%` },
],
},
{
[operate2]: [
where(colFn(property), operate, `%${value}%`),
where(
colFn(property),
operate,
`%${encodeURIComponent(value)}%`,
),
],
},
], ],
}; };
} }
query[Op.and].push(q); query[primaryOperate].push(q);
} }
} }
} }
@@ -226,8 +245,11 @@ export default class CronService {
const filterKeys: any = Object.keys(filterQuery); const filterKeys: any = Object.keys(filterQuery);
for (const key of filterKeys) { for (const key of filterKeys) {
let q: any = {}; let q: any = {};
if (filterKeys[key]) { if (!filterQuery[key]) continue;
q[key] = filterKeys[key]; if (key === 'status' && filterQuery[key].includes(2)) {
q = { [Op.or]: [{ [key]: filterQuery[key] }, { isDisabled: 1 }] };
} else {
q[key] = filterQuery[key];
} }
query[Op.and].push(q); query[Op.and].push(q);
} }
@@ -242,6 +264,15 @@ export default class CronService {
} }
} }
public async find({ log_path }: { log_path: string }): Promise<Crontab | null> {
try {
const result = await CrontabModel.findOne({ where: { log_path } });
return result;
} catch (error) {
throw error;
}
}
public async crontabs(params?: { public async crontabs(params?: {
searchValue: string; searchValue: string;
page: string; page: string;
@@ -314,31 +345,11 @@ export default class CronService {
for (const doc of docs) { for (const doc of docs) {
if (doc.pid) { if (doc.pid) {
try { try {
process.kill(-doc.pid); await killTask(doc.pid);
} catch (error) { } catch (error) {
this.logger.silly(error); this.logger.silly(error);
} }
} }
const err = await this.killTask(doc.command);
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
const logFileExist = doc.log_path && (await fileExist(absolutePath));
const endTime = dayjs();
const diffTimeStr = doc.last_execution_time
? `,耗时 ${endTime.diff(
dayjs(doc.last_execution_time * 1000),
'second',
)}`
: '';
if (logFileExist) {
const str = err ? `\n${err}` : '';
fs.appendFileSync(
`${absolutePath}`,
`${str}\n## 执行结束... ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)}${diffTimeStr}${LOG_END_SYMBOL}`,
);
}
} }
await CrontabModel.update( await CrontabModel.update(
@@ -347,42 +358,6 @@ export default class CronService {
); );
} }
public async killTask(name: string) {
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
const execAsync = promisify(exec);
try {
let pid = (await execAsync(taskCommand)).stdout;
if (pid) {
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
} else {
return;
}
let pids = pid.match(/\(\d+/g);
const killLogs = [];
if (pids && pids.length > 0) {
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
pids = pids.slice(0, 3);
for (const id of pids) {
const c = `kill -9 ${id.slice(1)}`;
try {
const { stdout, stderr } = await execAsync(c);
if (stderr) {
killLogs.push(stderr);
}
if (stdout) {
killLogs.push(stdout);
}
} catch (error: any) {
killLogs.push(error.message);
}
}
}
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
} catch (e) {
return JSON.stringify(e);
}
}
private async runSingle(cronId: number): Promise<number> { private async runSingle(cronId: number): Promise<number> {
return new Promise(async (resolve: any) => { return new Promise(async (resolve: any) => {
const cron = await this.getDb({ id: cronId }); const cron = await this.getDb({ id: cronId });
@@ -400,8 +375,8 @@ export default class CronService {
this.logger.silly('Original command: ' + command); this.logger.silly('Original command: ' + command);
let cmdStr = command; let cmdStr = command;
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) { if (!cmdStr.startsWith(TASK_PREFIX) && !cmdStr.startsWith(QL_PREFIX)) {
cmdStr = `task ${cmdStr}`; cmdStr = `${TASK_PREFIX}${cmdStr}`;
} }
if ( if (
cmdStr.endsWith('.js') || cmdStr.endsWith('.js') ||
@@ -447,12 +422,12 @@ export default class CronService {
public async disabled(ids: number[]) { public async disabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } }); await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
await this.set_crontab(true); await this.set_crontab();
} }
public async enabled(ids: number[]) { public async enabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } }); await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
await this.set_crontab(true); await this.set_crontab();
} }
public async log(id: number) { public async log(id: number) {
@@ -465,69 +440,26 @@ export default class CronService {
const logFileExist = doc.log_path && (await fileExist(absolutePath)); const logFileExist = doc.log_path && (await fileExist(absolutePath));
if (logFileExist) { if (logFileExist) {
return getFileContentByName(`${absolutePath}`); return getFileContentByName(`${absolutePath}`);
}
const [, commandStr, url] = doc.command.split(/ +/);
let logPath = this.getKey(commandStr);
const isQlCommand = doc.command.startsWith('ql ');
const key =
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
logPath;
if (isQlCommand) {
logPath = 'update';
}
let logDir = `${config.logPath}${logPath}`;
if (existsSync(logDir)) {
let files = await promises.readdir(logDir);
if (isQlCommand) {
files = files.filter((x) => x.includes(key));
}
return getFileContentByName(`${logDir}/${files[files.length - 1]}`);
} else { } else {
return ''; return '任务未运行';
} }
} }
public async logs(id: number) { public async logs(id: number) {
const doc = await this.getDb({ id }); const doc = await this.getDb({ id });
if (!doc) { if (!doc || !doc.log_path) {
return []; return [];
} }
if (doc.log_path) { const relativeDir = path.dirname(`${doc.log_path}`);
const relativeDir = path.dirname(`${doc.log_path}`); const dir = path.resolve(config.logPath, relativeDir);
const dir = path.resolve(config.logPath, relativeDir); if (existsSync(dir)) {
if (existsSync(dir)) { let files = await promises.readdir(dir);
let files = await promises.readdir(dir);
return files
.map((x) => ({
filename: x,
directory: relativeDir.replace(config.logPath, ''),
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time);
}
}
const [, commandStr, url] = doc.command.split(/ +/);
let logPath = this.getKey(commandStr);
const isQlCommand = doc.command.startsWith('ql ');
const key =
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
logPath;
if (isQlCommand) {
logPath = 'update';
}
let logDir = `${config.logPath}${logPath}`;
if (existsSync(logDir)) {
let files = await promises.readdir(logDir);
if (isQlCommand) {
files = files.filter((x) => x.includes(key));
}
return files return files
.map((x) => ({ .map((x) => ({
filename: x, filename: x,
directory: logPath, directory: relativeDir.replace(config.logPath, ''),
time: fs.statSync(`${logDir}/${x}`).mtime.getTime(), time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
})) }))
.sort((a, b) => b.time - a.time); .sort((a, b) => b.time - a.time);
} else { } else {
@@ -535,34 +467,18 @@ export default class CronService {
} }
} }
private getKey(command: string): string {
const start =
command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0;
const end =
command.lastIndexOf('.') !== -1
? command.lastIndexOf('.')
: command.length;
const tmpStr = command.substring(0, start - 1);
let index = 0;
if (tmpStr.lastIndexOf('/') !== -1 && tmpStr.startsWith('http')) {
index = tmpStr.lastIndexOf('/');
} else if (tmpStr.lastIndexOf(':') !== -1 && tmpStr.startsWith('git@')) {
index = tmpStr.lastIndexOf(':');
}
if (index) {
return `${tmpStr.substring(index + 1)}_${command.substring(start, end)}`;
} else {
return command.substring(start, end);
}
}
private make_command(tab: Crontab) { private make_command(tab: Crontab) {
if (
!tab.command.startsWith(TASK_PREFIX) &&
!tab.command.startsWith(QL_PREFIX)
) {
tab.command = `${TASK_PREFIX}${tab.command}`;
}
const crontab_job_string = `ID=${tab.id} ${tab.command}`; const crontab_job_string = `ID=${tab.id} ${tab.command}`;
return crontab_job_string; return crontab_job_string;
} }
private async set_crontab(needReloadSchedule: boolean = false) { private async set_crontab() {
const tabs = await this.crontabs(); const tabs = await this.crontabs();
var crontab_string = ''; var crontab_string = '';
tabs.data.forEach((tab) => { tabs.data.forEach((tab) => {
@@ -585,9 +501,7 @@ export default class CronService {
fs.writeFileSync(config.crontabFile, crontab_string); fs.writeFileSync(config.crontabFile, crontab_string);
execSync(`crontab ${config.crontabFile}`); execSync(`crontab ${config.crontabFile}`);
if (needReloadSchedule) { exec(`pm2 reload schedule`);
exec(`pm2 reload schedule`);
}
await CrontabModel.update({ saved: true }, { where: {} }); await CrontabModel.update({ saved: true }, { where: {} });
} }
+28 -3
View File
@@ -1,7 +1,12 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import { CrontabView, CrontabViewModel } from '../data/cronView'; import { CrontabView, CrontabViewModel } from '../data/cronView';
import { initPosition } from '../data/env'; import {
initPosition,
maxPosition,
minPosition,
stepPosition,
} from '../data/env';
@Service() @Service()
export default class CronViewService { export default class CronViewService {
@@ -16,6 +21,8 @@ export default class CronViewService {
position = position / 2; position = position / 2;
const tab = new CrontabView({ ...payload, position }); const tab = new CrontabView({ ...payload, position });
const doc = await this.insert(tab); const doc = await this.insert(tab);
await this.checkPosition(tab.position!);
return doc; return doc;
} }
@@ -24,7 +31,7 @@ export default class CronViewService {
} }
public async update(payload: CrontabView): Promise<CrontabView> { public async update(payload: CrontabView): Promise<CrontabView> {
const newDoc = await this.updateDb(payload); const newDoc = await this.updateDb(new CrontabView(payload));
return newDoc; return newDoc;
} }
@@ -62,6 +69,22 @@ export default class CronViewService {
await CrontabViewModel.update({ isDisabled: 0 }, { where: { id: ids } }); await CrontabViewModel.update({ isDisabled: 0 }, { where: { id: ids } });
} }
private async checkPosition(position: number) {
const precisionPosition = parseFloat(position.toPrecision(16));
if (precisionPosition < minPosition || precisionPosition > maxPosition) {
const envs = await this.list();
let position = initPosition;
for (const env of envs) {
position = position - stepPosition;
await this.updateDb({ id: env.id, position });
}
}
}
private getPrecisionPosition(position: number): number {
return parseFloat(position.toPrecision(16));
}
public async move({ public async move({
id, id,
fromIndex, fromIndex,
@@ -85,8 +108,10 @@ export default class CronViewService {
} }
const newDoc = await this.update({ const newDoc = await this.update({
id, id,
position: targetPosition, position: this.getPrecisionPosition(targetPosition),
}); });
await this.checkPosition(targetPosition);
return newDoc; return newDoc;
} }
} }
+9 -5
View File
@@ -49,7 +49,7 @@ export default class EnvService {
} }
public async update(payload: Env): Promise<Env> { public async update(payload: Env): Promise<Env> {
const newDoc = await this.updateDb(payload); const newDoc = await this.updateDb(new Env(payload));
await this.set_envs(); await this.set_envs();
return newDoc; return newDoc;
} }
@@ -92,18 +92,22 @@ export default class EnvService {
position: this.getPrecisionPosition(targetPosition), position: this.getPrecisionPosition(targetPosition),
}); });
await this.checkPosition(targetPosition); await this.checkPosition(targetPosition, envs[toIndex].position!);
return newDoc; return newDoc;
} }
private async checkPosition(position: number) { private async checkPosition(position: number, edge: number = 0) {
const precisionPosition = parseFloat(position.toPrecision(16)); const precisionPosition = parseFloat(position.toPrecision(16));
if (precisionPosition < minPosition || precisionPosition > maxPosition) { if (
precisionPosition < minPosition ||
precisionPosition > maxPosition ||
Math.abs(precisionPosition - edge) < minPosition
) {
const envs = await this.envs(); const envs = await this.envs();
let position = initPosition; let position = initPosition;
for (const env of envs) { for (const env of envs) {
position = position - stepPosition; position = position - stepPosition;
await this.updateDb({ ...env, position }); await this.updateDb({ id: env.id, position });
} }
} }
} }
+7 -7
View File
@@ -123,8 +123,8 @@ export default class NotificationService {
} }
private async pushDeer() { private async pushDeer() {
const { pushDeerKey } = this.params; const { pushDeerKey, pushDeerUrl } = this.params;
const url = `https://api2.pushdeer.com/message/push`; const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`;
const res: any = await got const res: any = await got
.post(url, { .post(url, {
...this.gotOption, ...this.gotOption,
@@ -161,7 +161,7 @@ export default class NotificationService {
this.title, this.title,
)}/${encodeURIComponent( )}/${encodeURIComponent(
this.content, this.content,
)}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`; )}?icon=${barkIcon}&sound=${barkSound}&group=${barkGroup}`;
const res: any = await got const res: any = await got
.get(url, { .get(url, {
...this.gotOption, ...this.gotOption,
@@ -484,11 +484,11 @@ export default class NotificationService {
return { return {
formatUrl: url formatUrl: url
.replaceAll('$title', encodeURIComponent(this.title)) ?.replaceAll('$title', encodeURIComponent(this.title))
.replaceAll('$content', encodeURIComponent(this.content)), ?.replaceAll('$content', encodeURIComponent(this.content)),
formatBody: body formatBody: body
.replaceAll('$title', this.title) ?.replaceAll('$title', this.title)
.replaceAll('$content', this.content), ?.replaceAll('$content', this.content),
}; };
} }
} }
+1 -1
View File
@@ -119,7 +119,7 @@ export default class OpenService {
(x) => x.expiration >= timestamp, (x) => x.expiration >= timestamp,
); );
let tokens = invalidTokens; let tokens = invalidTokens;
if (invalidTokens.length > 5) { if (invalidTokens.length >= 5) {
tokens = [ tokens = [
...invalidTokens.slice(0, 4), ...invalidTokens.slice(0, 4),
{ ...invalidTokens[4], expiration }, { ...invalidTokens[4], expiration },
+15 -13
View File
@@ -5,8 +5,8 @@ import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process';
import { import {
ToadScheduler, ToadScheduler,
LongIntervalJob, LongIntervalJob,
AsyncTask,
SimpleIntervalSchedule, SimpleIntervalSchedule,
Task,
} from 'toad-scheduler'; } from 'toad-scheduler';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@@ -42,7 +42,11 @@ export default class ScheduleService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
async runTask(command: string, callbacks: TaskCallbacks = {}) { async runTask(
command: string,
callbacks: TaskCallbacks = {},
completionTime: 'start' | 'end' = 'end',
) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
const startTime = dayjs(); const startTime = dayjs();
@@ -52,14 +56,15 @@ export default class ScheduleService {
// TODO: // TODO:
callbacks.onStart?.(cp, startTime); callbacks.onStart?.(cp, startTime);
completionTime === 'start' && resolve(cp.pid);
cp.stdout.on('data', async (data) => { cp.stdout.on('data', async (data) => {
await callbacks.onLog?.(data.toString()); await callbacks.onLog?.(data.toString());
}); });
cp.stderr.on('data', async (data) => { cp.stderr.on('data', async (data) => {
this.logger.error( this.logger.info(
'执行任务 %s 失败,时间:%s, 错误信息:%j', '[执行任务失败] %s,时间:%s, 错误信息:%j',
command, command,
new Date().toLocaleString(), new Date().toLocaleString(),
data.toString(), data.toString(),
@@ -69,7 +74,7 @@ export default class ScheduleService {
cp.on('error', async (err) => { cp.on('error', async (err) => {
this.logger.error( this.logger.error(
'创建任务 %s 失败,时间:%s, 错误信息:%j', '[创建任务失败] %s,时间:%s, 错误信息:%j',
command, command,
new Date().toLocaleString(), new Date().toLocaleString(),
err, err,
@@ -79,7 +84,7 @@ export default class ScheduleService {
cp.on('exit', async (code, signal) => { cp.on('exit', async (code, signal) => {
this.logger.info( this.logger.info(
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`, `[任务退出] ${command} 进程id: ${cp.pid},退出码 ${code}`,
); );
}); });
@@ -100,7 +105,6 @@ export default class ScheduleService {
error, error,
); );
await callbacks.onError?.(JSON.stringify(error)); await callbacks.onError?.(JSON.stringify(error));
resolve(null);
} }
}); });
} }
@@ -150,12 +154,10 @@ export default class ScheduleService {
name, name,
command, command,
); );
const task = new AsyncTask( const task = new Task(
name, name,
async () => { () => {
return new Promise(async (resolve, reject) => { this.runTask(command, callbacks);
await this.runTask(command, callbacks);
});
}, },
(err) => { (err) => {
this.logger.error( this.logger.error(
@@ -176,7 +178,7 @@ export default class ScheduleService {
this.intervalSchedule.addIntervalJob(job); this.intervalSchedule.addIntervalJob(job);
if (runImmediately) { if (runImmediately) {
await this.runTask(command, callbacks); this.runTask(command, callbacks);
} }
} }
+18 -15
View File
@@ -6,7 +6,8 @@ import SockService from './sock';
import CronService from './cron'; import CronService from './cron';
import ScheduleService, { TaskCallbacks } from './schedule'; import ScheduleService, { TaskCallbacks } from './schedule';
import config from '../config'; import config from '../config';
import { LOG_END_SYMBOL } from '../config/const'; import { TASK_COMMAND } from '../config/const';
import { getPid, killTask } from '../config/util';
@Service() @Service()
export default class ScriptService { export default class ScriptService {
@@ -41,23 +42,25 @@ export default class ScriptService {
public async runScript(filePath: string) { public async runScript(filePath: string) {
const relativePath = path.relative(config.scriptPath, filePath); const relativePath = path.relative(config.scriptPath, filePath);
const command = `task -l ${relativePath} now`; const command = `${TASK_COMMAND} -l ${relativePath} now`;
this.scheduleService.runTask(command, this.taskCallbacks(filePath)); const pid = await this.scheduleService.runTask(
command,
this.taskCallbacks(filePath),
'start',
);
return { code: 200 }; return { code: 200, data: pid };
} }
public async stopScript(filePath: string) { public async stopScript(filePath: string, pid: number) {
const relativePath = path.relative(config.scriptPath, filePath); let str = '';
const err = await this.cronService.killTask(`task -l ${relativePath} now`); if (!pid) {
const relativePath = path.relative(config.scriptPath, filePath);
const str = err ? `\n${err}` : ''; pid = await getPid(`${TASK_COMMAND} -l ${relativePath} now`);
this.sockService.sendMessage({ }
type: 'manuallyRunScript', try {
message: `${str}\n## 执行结束... ${new Date() await killTask(pid);
.toLocaleString('zh', { hour12: false }) } catch (error) {}
.replace(' 24:', ' 00:')}${LOG_END_SYMBOL}`,
});
return { code: 200 }; return { code: 200 };
} }
+48 -14
View File
@@ -1,8 +1,10 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import fs from 'fs'; import fs, { existsSync } from 'fs';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
import { Subscription } from '../data/subscription';
import { formatUrl } from '../config/subscription';
@Service() @Service()
export default class SshKeyService { export default class SshKeyService {
@@ -14,7 +16,7 @@ export default class SshKeyService {
private generatePrivateKeyFile(alias: string, key: string): void { private generatePrivateKeyFile(alias: string, key: string): void {
try { try {
fs.writeFileSync(`${this.sshPath}/${alias}`, key, { fs.writeFileSync(`${this.sshPath}/${alias}`, `${key}${os.EOL}`, {
encoding: 'utf8', encoding: 'utf8',
mode: '400', mode: '400',
}); });
@@ -32,23 +34,32 @@ export default class SshKeyService {
private removePrivateKeyFile(alias: string): void { private removePrivateKeyFile(alias: string): void {
try { try {
fs.unlinkSync(`${this.sshPath}/${alias}`); const filePath = path.join(this.sshPath, alias);
if (existsSync(filePath)) {
fs.unlinkSync(`${this.sshPath}/${alias}`);
}
} catch (error) { } catch (error) {
this.logger.error('删除私钥文件失败', error); this.logger.error('删除私钥文件失败', error);
} }
} }
private generateSingleSshConfig(alias: string, host: string): string { private generateSingleSshConfig(
return `\nHost ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no`; alias: string,
host: string,
proxy?: string,
): string {
if (host === 'github.com') {
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa\n PubkeyAcceptedAlgorithms +ssh-rsa`;
}
const proxyStr = proxy ? ` ProxyCommand nc -v -x ${proxy} %h %p\n` : '';
return `Host ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no\n${proxyStr}`;
} }
private generateSshConfig(configs: string[]) { private generateSshConfig(configs: string[]) {
try { try {
for (const config of configs) { fs.writeFileSync(this.sshConfigFilePath, configs.join('\n'), {
fs.appendFileSync(this.sshConfigFilePath, config, { encoding: 'utf8',
encoding: 'utf8', });
});
}
} catch (error) { } catch (error) {
this.logger.error('写入ssh配置文件失败', error); this.logger.error('写入ssh配置文件失败', error);
} }
@@ -69,16 +80,39 @@ export default class SshKeyService {
} }
} }
public addSSHKey(key: string, alias: string, host: string): void { public addSSHKey(
key: string,
alias: string,
host: string,
proxy?: string,
): void {
this.generatePrivateKeyFile(alias, key); this.generatePrivateKeyFile(alias, key);
const config = this.generateSingleSshConfig(alias, host); const config = this.generateSingleSshConfig(alias, host, proxy);
this.removeSshConfig(alias); this.removeSshConfig(alias);
this.generateSshConfig([config]); this.generateSshConfig([config]);
} }
public removeSSHKey(alias: string, host: string): void { public removeSSHKey(alias: string, host: string, proxy?: string): void {
this.removePrivateKeyFile(alias); this.removePrivateKeyFile(alias);
const config = this.generateSingleSshConfig(alias, host); const config = this.generateSingleSshConfig(alias, host, proxy);
this.removeSshConfig(config); this.removeSshConfig(config);
} }
public setSshConfig(docs: Subscription[]) {
let result = [];
for (const doc of docs) {
if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {
const { alias, proxy } = doc;
const { host } = formatUrl(doc);
this.removePrivateKeyFile(alias);
this.generatePrivateKeyFile(
alias,
(doc.pull_option as any).private_key,
);
const config = this.generateSingleSshConfig(alias, host, proxy);
result.push(config);
}
}
this.generateSshConfig(result);
}
} }
+25 -92
View File
@@ -13,15 +13,14 @@ import {
spawn, spawn,
} from 'child_process'; } from 'child_process';
import fs from 'fs'; import fs from 'fs';
import cron_parser from 'cron-parser';
import { import {
getFileContentByName, getFileContentByName,
concurrentRun, concurrentRun,
fileExist, fileExist,
createFile, createFile,
killTask,
} from '../config/util'; } from '../config/util';
import { promises, existsSync } from 'fs'; import { promises, existsSync } from 'fs';
import { promisify } from 'util';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import path from 'path'; import path from 'path';
import ScheduleService, { TaskCallbacks } from './schedule'; import ScheduleService, { TaskCallbacks } from './schedule';
@@ -30,6 +29,7 @@ import SockService from './sock';
import SshKeyService from './sshKey'; import SshKeyService from './sshKey';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { LOG_END_SYMBOL } from '../config/const'; import { LOG_END_SYMBOL } from '../config/const';
import { formatCommand, formatUrl } from '../config/subscription';
@Service() @Service()
export default class SubscriptionService { export default class SubscriptionService {
@@ -68,62 +68,20 @@ export default class SubscriptionService {
['createdAt', 'DESC'], ['createdAt', 'DESC'],
], ],
}); });
return result as any; return result;
} catch (error) { } catch (error) {
throw error; throw error;
} }
} }
private formatCommand(doc: Subscription, url?: string) {
let command = 'ql ';
let _url = url || this.formatUrl(doc).url;
const { type, whitelist, blacklist, dependences, branch, extensions } = doc;
if (type === 'file') {
command += `raw "${_url}"`;
} else {
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
dependences || ''
}" "${branch || ''}" "${extensions || ''}"`;
}
return command;
}
private formatUrl(doc: Subscription) {
let url = doc.url;
let host = '';
if (doc.type === 'private-repo') {
if (doc.pull_type === 'ssh-key') {
host = doc.url!.replace(/.*\@([^\:]+)\:.*/, '$1');
url = doc.url!.replace(host, doc.alias);
} else {
host = doc.url!.replace(/.*\:\/\/([^\/]+)\/.*/, '$1');
const { username, password } = doc.pull_option as any;
url = doc.url!.replace(host, `${username}:${password}@${host}`);
}
}
return { url, host };
}
public async handleTask( public async handleTask(
doc: Subscription, doc: Subscription,
needCreate = true, needCreate = true,
needAddKey = true,
runImmediately = false, runImmediately = false,
) { ) {
const { url, host } = this.formatUrl(doc); const { url } = formatUrl(doc);
if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {
if (needAddKey) {
this.sshKeyService.addSSHKey(
(doc.pull_option as any).private_key,
doc.alias,
host,
);
} else {
this.sshKeyService.removeSSHKey(doc.alias, host);
}
}
doc.command = this.formatCommand(doc, url as string); doc.command = formatCommand(doc, url as string);
if (doc.schedule_type === 'crontab') { if (doc.schedule_type === 'crontab') {
this.scheduleService.cancelCronTask(doc as any); this.scheduleService.cancelCronTask(doc as any);
@@ -146,6 +104,11 @@ export default class SubscriptionService {
} }
} }
public async setSshConfig() {
const docs = await SubscriptionModel.findAll();
this.sshKeyService.setSshConfig(docs);
}
private async promiseExec(command: string): Promise<string> { private async promiseExec(command: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
exec( exec(
@@ -264,6 +227,7 @@ export default class SubscriptionService {
const tab = new Subscription(payload); const tab = new Subscription(payload);
const doc = await this.insert(tab); const doc = await this.insert(tab);
await this.handleTask(doc); await this.handleTask(doc);
await this.setSshConfig();
return doc; return doc;
} }
@@ -272,8 +236,10 @@ export default class SubscriptionService {
} }
public async update(payload: Subscription): Promise<Subscription> { public async update(payload: Subscription): Promise<Subscription> {
const newDoc = await this.updateDb(payload); const tab = new Subscription(payload);
const newDoc = await this.updateDb(tab);
await this.handleTask(newDoc, !newDoc.is_disabled); await this.handleTask(newDoc, !newDoc.is_disabled);
await this.setSshConfig();
return newDoc; return newDoc;
} }
@@ -316,9 +282,10 @@ export default class SubscriptionService {
public async remove(ids: number[]) { public async remove(ids: number[]) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } }); const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) { for (const doc of docs) {
await this.handleTask(doc, false, false); await this.handleTask(doc, false);
} }
await SubscriptionModel.destroy({ where: { id: ids } }); await SubscriptionModel.destroy({ where: { id: ids } });
await this.setSshConfig();
} }
public async getDb(query: any): Promise<Subscription> { public async getDb(query: any): Promise<Subscription> {
@@ -342,19 +309,18 @@ export default class SubscriptionService {
for (const doc of docs) { for (const doc of docs) {
if (doc.pid) { if (doc.pid) {
try { try {
process.kill(-doc.pid); await killTask(doc.pid);
} catch (error) { } catch (error) {
this.logger.silly(error); this.logger.silly(error);
} }
} }
const command = this.formatCommand(doc);
const err = await this.killTask(command);
const absolutePath = await this.handleLogPath(doc.log_path as string); const absolutePath = await this.handleLogPath(doc.log_path as string);
const str = err ? `\n${err}` : '';
fs.appendFileSync( fs.appendFileSync(
`${absolutePath}`, `${absolutePath}`,
`${str}\n## 执行结束... ${dayjs().format('YYYY-MM-DD HH:mm:ss')}${LOG_END_SYMBOL}`, `\n## 执行结束... ${dayjs().format(
'YYYY-MM-DD HH:mm:ss',
)}${LOG_END_SYMBOL}`,
); );
} }
@@ -364,48 +330,13 @@ export default class SubscriptionService {
); );
} }
public async killTask(name: string) {
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
const execAsync = promisify(exec);
try {
let pid = (await execAsync(taskCommand)).stdout;
if (pid) {
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
} else {
return;
}
let pids = pid.match(/\(\d+/g);
const killLogs = [];
if (pids && pids.length > 0) {
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
for (const id of pids) {
const c = `kill -9 ${id.slice(1)}`;
try {
const { stdout, stderr } = await execAsync(c);
if (stderr) {
killLogs.push(stderr);
}
if (stdout) {
killLogs.push(stdout);
}
} catch (error: any) {
killLogs.push(error.message);
}
}
}
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
} catch (e) {
return JSON.stringify(e);
}
}
private async runSingle(subscriptionId: number) { private async runSingle(subscriptionId: number) {
const subscription = await this.getDb({ id: subscriptionId }); const subscription = await this.getDb({ id: subscriptionId });
if (subscription.status !== SubscriptionStatus.queued) { if (subscription.status !== SubscriptionStatus.queued) {
return; return;
} }
const command = this.formatCommand(subscription); const command = formatCommand(subscription);
await this.scheduleService.runTask( await this.scheduleService.runTask(
command, command,
@@ -414,19 +345,21 @@ export default class SubscriptionService {
} }
public async disabled(ids: number[]) { public async disabled(ids: number[]) {
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
const docs = await SubscriptionModel.findAll({ where: { id: ids } }); const docs = await SubscriptionModel.findAll({ where: { id: ids } });
await this.setSshConfig();
for (const doc of docs) { for (const doc of docs) {
await this.handleTask(doc, false); await this.handleTask(doc, false);
} }
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
} }
public async enabled(ids: number[]) { public async enabled(ids: number[]) {
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
const docs = await SubscriptionModel.findAll({ where: { id: ids } }); const docs = await SubscriptionModel.findAll({ where: { id: ids } });
await this.setSshConfig();
for (const doc of docs) { for (const doc of docs) {
await this.handleTask(doc); await this.handleTask(doc);
} }
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
} }
public async log(id: number) { public async log(id: number) {
+21 -18
View File
@@ -9,6 +9,7 @@ import ScheduleService from './schedule';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import SockService from './sock'; import SockService from './sock';
import got from 'got'; import got from 'got';
import { parseContentVersion, parseVersion } from '../config/util';
@Service() @Service()
export default class SystemService { export default class SystemService {
@@ -78,31 +79,33 @@ export default class SystemService {
public async checkUpdate() { public async checkUpdate() {
try { try {
const versionRegx = /.*export const version = \'(.*)\'\;/; const currentVersionContent = await parseVersion(config.versionFile);
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8'); let lastVersionContent;
const currentVersion = currentVersionFile.match(versionRegx)![1];
let lastVersion = '';
let lastLog = '';
try { try {
const result = await got.get(config.lastVersionFile, { const result = await got.get(
timeout: 30000, `${config.lastVersionFile}?t=${Date.now()}`,
}); {
const lastVersionFileContent = result.body; timeout: 30000,
lastVersion = lastVersionFileContent.match(versionRegx)![1]; },
lastLog = lastVersionFileContent.match(logRegx) );
? lastVersionFileContent.match(logRegx)![1] lastVersionContent = await parseContentVersion(result.body);
: '';
} catch (error) {} } catch (error) {}
if (!lastVersionContent) {
lastVersionContent = currentVersionContent;
}
return { return {
code: 200, code: 200,
data: { data: {
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion), hasNewVersion: this.checkHasNewVersion(
lastVersion, currentVersionContent.version,
lastLog, lastVersionContent.version,
),
lastVersion: lastVersionContent.version,
lastLog: lastVersionContent.changeLog,
lastLogLink: lastVersionContent.changeLogLink,
}, },
}; };
} catch (error: any) { } catch (error: any) {
+9 -3
View File
@@ -5,6 +5,7 @@ import LoggerInstance from './loaders/logger';
import fs from 'fs'; import fs from 'fs';
import config from './config'; import config from './config';
import path from 'path'; import path from 'path';
import os from 'os';
const tokenFile = path.join(config.configPath, 'token.json'); const tokenFile = path.join(config.configPath, 'token.json');
@@ -25,9 +26,14 @@ async function getToken() {
async function writeFile(data: any) { async function writeFile(data: any) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
fs.writeFile(tokenFile, JSON.stringify(data), { encoding: 'utf8' }, () => { fs.writeFile(
resolve(); tokenFile,
}); `${JSON.stringify(data)}${os.EOL}`,
{ encoding: 'utf8' },
() => {
resolve();
},
);
}); });
} }
+23 -10
View File
@@ -1,4 +1,13 @@
FROM python:alpine FROM python:3.10-alpine as builder
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
RUN set -x \
&& apk update \
&& apk add nodejs npm git \
&& npm i -g pnpm \
&& cd /tmp/build \
&& pnpm install --prod
FROM python:3.10-alpine
ARG QL_MAINTAINER="whyour" ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}" LABEL maintainer="${QL_MAINTAINER}"
@@ -7,14 +16,13 @@ ARG QL_BRANCH=develop
ENV PNPM_HOME=/root/.local/share/pnpm \ ENV PNPM_HOME=/root/.local/share/pnpm \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules \
LANG=zh_CN.UTF-8 \ LANG=zh_CN.UTF-8 \
SHELL=/bin/bash \ SHELL=/bin/bash \
PS1="\u@\h:\w \$ " \ PS1="\u@\h:\w \$ " \
QL_DIR=/ql \ QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH} QL_BRANCH=${QL_BRANCH}
WORKDIR ${QL_DIR}
RUN set -x \ RUN set -x \
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ && sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk update -f \ && apk update -f \
@@ -41,20 +49,25 @@ RUN set -x \
&& git config --global user.name "qinglong" \ && git config --global user.name "qinglong" \
&& git config --global http.postBuffer 524288000 \ && git config --global http.postBuffer 524288000 \
&& npm install -g pnpm \ && npm install -g pnpm \
&& pnpm add -g pm2 ts-node typescript tslib \ && pnpm add -g pm2 tsx \
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \ && rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \
&& rm -rf /root/.cache \
&& rm -rf /root/.npm
ARG SOURCE_COMMIT
RUN git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
&& cd ${QL_DIR} \ && cd ${QL_DIR} \
&& cp -f .env.example .env \ && cp -f .env.example .env \
&& chmod 777 ${QL_DIR}/shell/*.sh \ && chmod 777 ${QL_DIR}/shell/*.sh \
&& chmod 777 ${QL_DIR}/docker/*.sh \ && chmod 777 ${QL_DIR}/docker/*.sh \
&& pnpm install --prod \
&& rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \
&& rm -rf /root/.cache \
&& rm -rf /root/.npm \
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \ && git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
&& mkdir -p ${QL_DIR}/static \ && mkdir -p ${QL_DIR}/static \
&& cp -rf /static/* ${QL_DIR}/static \ && cp -rf /static/* ${QL_DIR}/static \
&& rm -rf /static && rm -rf /static
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
WORKDIR ${QL_DIR}
ENTRYPOINT ["./docker/docker-entrypoint.sh"] ENTRYPOINT ["./docker/docker-entrypoint.sh"]
+4 -5
View File
@@ -9,9 +9,8 @@ export isFirstStartServer=true
echo -e "======================1. 检测配置文件========================\n" echo -e "======================1. 检测配置文件========================\n"
make_dir /etc/nginx/conf.d make_dir /etc/nginx/conf.d
make_dir /run/nginx make_dir /run/nginx
cp -fv $nginx_conf /etc/nginx/nginx.conf init_nginx
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
pm2 l &>/dev/null pm2 l &>/dev/null
patch_version &>/dev/null patch_version &>/dev/null
@@ -42,13 +41,13 @@ echo -e "定时任务启动成功...\n"
if [[ $AutoStartBot == true ]]; then if [[ $AutoStartBot == true ]]; then
echo -e "======================7. 启动bot========================\n" echo -e "======================7. 启动bot========================\n"
nohup ql bot >$dir_log/bot.log 2>&1 & nohup ql -l bot >$dir_log/bot.log 2>&1 &
echo -e "bot后台启动中...\n" echo -e "bot后台启动中...\n"
fi fi
if [[ $EnableExtraShell == true ]]; then if [[ $EnableExtraShell == true ]]; then
echo -e "======================8. 执行自定义脚本========================\n" echo -e "======================8. 执行自定义脚本========================\n"
nohup ql extra >$dir_log/extra.log 2>&1 & nohup ql -l extra >$dir_log/extra.log 2>&1 &
echo -e "自定义脚本后台执行中...\n" echo -e "自定义脚本后台执行中...\n"
fi fi
+1
View File
@@ -13,6 +13,7 @@ map $http_upgrade $connection_upgrade {
server { server {
listen 5700; listen 5700;
IPV6_CONFIG
root /ql/static/dist; root /ql/static/dist;
ssl_session_timeout 5m; ssl_session_timeout 5m;
+19 -10
View File
@@ -4,7 +4,8 @@
"start": "concurrently -n w: npm:start:*", "start": "concurrently -n w: npm:start:*",
"start:front": "max dev", "start:front": "max dev",
"start:back": "nodemon", "start:back": "nodemon",
"start:public": "ts-node back/public.ts", "start:public": "ts-node --transpile-only ./back/public.ts",
"start:schedule": "ts-node --transpile-only ./back/schedule.ts",
"build:front": "max build", "build:front": "max build",
"build:back": "tsc -p tsconfig.back.json", "build:back": "tsc -p tsconfig.back.json",
"panel": "npm run build:back && node static/build/app.js", "panel": "npm run build:back && node static/build/app.js",
@@ -42,7 +43,8 @@
"monaco-editor", "monaco-editor",
"rc-field-form", "rc-field-form",
"@types/lodash.merge", "@types/lodash.merge",
"rollup" "rollup",
"styled-components"
], ],
"allowedVersions": { "allowedVersions": {
"react": "18", "react": "18",
@@ -69,18 +71,19 @@
"got": "^11.8.2", "got": "^11.8.2",
"hpagent": "^0.1.2", "hpagent": "^0.1.2",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"multer": "^1.4.4", "multer": "^1.4.4",
"nedb": "^1.8.0", "nedb": "^1.8.0",
"node-schedule": "^2.1.0", "node-schedule": "^2.1.0",
"nodemailer": "^6.7.2", "nodemailer": "^6.7.2",
"p-queue": "7.2.0", "pstree.remy": "^1.1.8",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"sequelize": "^6.25.3", "sequelize": "^6.25.5",
"serve-handler": "^6.1.3", "serve-handler": "^6.1.3",
"sockjs": "^0.3.24", "sockjs": "^0.3.24",
"sqlite3": "npm:@louislam/sqlite3@^15.0.6", "sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.2",
"toad-scheduler": "^1.6.0", "toad-scheduler": "^1.6.0",
"typedi": "^0.10.0", "typedi": "^0.10.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
@@ -89,13 +92,15 @@
}, },
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^4.7.0", "@ant-design/icons": "^4.7.0",
"@ant-design/pro-layout": "^6.33.1", "@ant-design/pro-layout": "6.38.22",
"@monaco-editor/react": "4.2.1", "@monaco-editor/react": "4.2.1",
"@react-hook/resize-observer": "^1.2.6",
"@sentry/react": "^7.12.1", "@sentry/react": "^7.12.1",
"@types/body-parser": "^1.19.2", "@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/express-jwt": "^6.0.4", "@types/express-jwt": "^6.0.4",
"@types/js-yaml": "^4.0.5",
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.185", "@types/lodash": "^4.14.185",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",
@@ -105,20 +110,22 @@
"@types/nodemailer": "^6.4.4", "@types/nodemailer": "^6.4.4",
"@types/qrcode.react": "^1.0.2", "@types/qrcode.react": "^1.0.2",
"@types/react": "^18.0.20", "@types/react": "^18.0.20",
"@types/react-copy-to-clipboard": "^5.0.4",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@types/serve-handler": "^6.1.1", "@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33", "@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1", "@types/sockjs-client": "^1.5.1",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@umijs/max": "^4.0.21", "@umijs/max": "^4.0.55",
"@umijs/ssr-darkreader": "^4.9.45", "@umijs/ssr-darkreader": "^4.9.45",
"ansi-to-react": "^6.1.6", "ansi-to-react": "^6.1.6",
"antd": "^4.23.0", "antd": "^4.24.8",
"antd-img-crop": "^4.2.3", "antd-img-crop": "^4.2.3",
"codemirror": "^5.65.2", "codemirror": "^5.65.2",
"compression-webpack-plugin": "9.2.0", "compression-webpack-plugin": "9.2.0",
"concurrently": "^7.0.0", "concurrently": "^7.0.0",
"lint-staged": "^12.3.4", "lint-staged": "^13.0.3",
"monaco-editor": "0.33.0",
"nodemon": "^2.0.15", "nodemon": "^2.0.15",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"qiniu": "^7.4.0", "qiniu": "^7.4.0",
@@ -127,6 +134,7 @@
"rc-tween-one": "^3.0.6", "rc-tween-one": "^3.0.6",
"react": "18.2.0", "react": "18.2.0",
"react-codemirror2": "^7.2.1", "react-codemirror2": "^7.2.1",
"react-copy-to-clipboard": "^5.1.0",
"react-diff-viewer": "^3.1.1", "react-diff-viewer": "^3.1.1",
"react-dnd": "^14.0.2", "react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0", "react-dnd-html5-backend": "^14.0.0",
@@ -135,7 +143,8 @@
"sockjs-client": "^1.6.0", "sockjs-client": "^1.6.0",
"ts-node": "^10.6.0", "ts-node": "^10.6.0",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"typescript": "^4.6.2", "tsx": "^3.12.3",
"typescript": "4.8.4",
"umi-request": "^1.4.0", "umi-request": "^1.4.0",
"vh-check": "^2.0.5", "vh-check": "^2.0.5",
"webpack": "^5.70.0", "webpack": "^5.70.0",
+15588
View File
File diff suppressed because it is too large Load Diff
+14 -1
View File
@@ -16,7 +16,7 @@ DefaultCronRule=""
## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可 ## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可
RepoFileExtensions="js py" RepoFileExtensions="js py"
## 代理地址,支持http/https/socks,例如 http://127.0.0.1:7890 ## 代理地址,支持HTTP/SOCK5,例如 http://127.0.0.1:7890
ProxyUrl="" ProxyUrl=""
## 资源告警阙值,默认CPU 80%、内存80%、磁盘90% ## 资源告警阙值,默认CPU 80%、内存80%、磁盘90%
@@ -160,4 +160,17 @@ export AIBOTK_TYPE=""
## aibotk_name (必填)填写群名或用户昵称,和上面的type类型要对应 ## aibotk_name (必填)填写群名或用户昵称,和上面的type类型要对应
export AIBOTK_NAME="" export AIBOTK_NAME=""
## 14. SMTP
## 暂时只支持在 Python 中调用 notify.py 以发送 SMTP 邮件通知
## smtp_server 填写 SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
export SMTP_SERVER=""
## smtp_ssl 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
export SMTP_SSL="false"
## smtp_email 填写 SMTP 收发件邮箱,通知将会由自己发给自己
export SMTP_EMAIL=""
## smtp_password 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
export SMTP_PASSWORD=""
## smtp_name 填写 SMTP 收发件人姓名,可随意填写
export SMTP_NAME=""
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可 ## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+91 -2
View File
@@ -38,6 +38,7 @@ let SCKEY = '';
//此处填你申请的PushDeer KEY. //此处填你申请的PushDeer KEY.
//(环境变量名 DEER_KEY) //(环境变量名 DEER_KEY)
let PUSHDEER_KEY = ''; let PUSHDEER_KEY = '';
let PUSHDEER_URL = '';
// =======================================Synology Chat通知设置区域=========================================== // =======================================Synology Chat通知设置区域===========================================
//此处填你申请的CHAT_URL与CHAT_TOKEN //此处填你申请的CHAT_URL与CHAT_TOKEN
@@ -124,6 +125,18 @@ let AIBOTK_NAME = '';
//FSKEY 飞书机器人的 FSKEY //FSKEY 飞书机器人的 FSKEY
let FSKEY = ''; let FSKEY = '';
// =======================================SMTP 邮件设置区域=======================================
// SMTP_SERVER: 填写 SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
// SMTP_SSL: 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
// SMTP_EMAIL: 填写 SMTP 收发件邮箱,通知将会由自己发给自己
// SMTP_PASSWORD: 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
// SMTP_NAME: 填写 SMTP 收发件人姓名,可随意填写
let SMTP_SERVER = '';
let SMTP_SSL = 'false';
let SMTP_EMAIL = '';
let SMTP_PASSWORD = '';
let SMTP_NAME = '';
//==========================云端环境变量的判断与接收========================= //==========================云端环境变量的判断与接收=========================
if (process.env.GOTIFY_URL) { if (process.env.GOTIFY_URL) {
GOTIFY_URL = process.env.GOTIFY_URL; GOTIFY_URL = process.env.GOTIFY_URL;
@@ -151,6 +164,7 @@ if (process.env.PUSH_KEY) {
if (process.env.DEER_KEY) { if (process.env.DEER_KEY) {
PUSHDEER_KEY = process.env.DEER_KEY; PUSHDEER_KEY = process.env.DEER_KEY;
PUSHDEER_URL = process.env.DEER_URL;
} }
if (process.env.CHAT_URL) { if (process.env.CHAT_URL) {
@@ -248,6 +262,22 @@ if (process.env.AIBOTK_NAME) {
if (process.env.FSKEY) { if (process.env.FSKEY) {
FSKEY = process.env.FSKEY; FSKEY = process.env.FSKEY;
} }
if (process.env.SMTP_SERVER) {
SMTP_SERVER = process.env.SMTP_SERVER;
}
if (process.env.SMTP_SSL) {
SMTP_SSL = process.env.SMTP_SSL;
}
if (process.env.SMTP_EMAIL) {
SMTP_EMAIL = process.env.SMTP_EMAIL;
}
if (process.env.SMTP_PASSWORD) {
SMTP_PASSWORD = process.env.SMTP_PASSWORD;
}
if (process.env.SMTP_NAME) {
SMTP_NAME = process.env.SMTP_NAME;
}
//==========================云端环境变量的判断与接收========================= //==========================云端环境变量的判断与接收=========================
/** /**
@@ -266,6 +296,16 @@ async function sendNotify(
) { ) {
//提供6种通知 //提供6种通知
desp += author; //增加作者信息,防止被贩卖等 desp += author; //增加作者信息,防止被贩卖等
// 根据标题跳过一些消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔
let skipTitle = process.env.SKIP_PUSH_TITLE
if(skipTitle) {
if(skipTitle.split('\n').includes(text)) {
console.info(text + "在SKIP_PUSH_TITLE环境变量内,跳过推送!");
return
}
}
await Promise.all([ await Promise.all([
serverNotify(text, desp), //微信server酱 serverNotify(text, desp), //微信server酱
pushPlusNotify(text, desp), //pushplus(推送加) pushPlusNotify(text, desp), //pushplus(推送加)
@@ -285,6 +325,7 @@ async function sendNotify(
PushDeerNotify(text, desp), //PushDeer PushDeerNotify(text, desp), //PushDeer
aibotkNotify(text, desp), //智能微秘书 aibotkNotify(text, desp), //智能微秘书
fsBotNotify(text, desp), //飞书机器人 fsBotNotify(text, desp), //飞书机器人
smtpNotify(text, desp), //SMTP 邮件
]); ]);
} }
@@ -413,7 +454,7 @@ function PushDeerNotify(text, desp) {
// PushDeer 建议对消息内容进行 urlencode // PushDeer 建议对消息内容进行 urlencode
desp = encodeURI(desp); desp = encodeURI(desp);
const options = { const options = {
url: `https://api2.pushdeer.com/message/push`, url: PUSHDEER_URL || `https://api2.pushdeer.com/message/push`,
body: `pushkey=${PUSHDEER_KEY}&text=${text}&desp=${desp}&type=markdown`, body: `pushkey=${PUSHDEER_KEY}&text=${text}&desp=${desp}&type=markdown`,
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
@@ -492,7 +533,7 @@ function BarkNotify(text, desp, params = {}) {
const options = { const options = {
url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent( url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent(
desp, desp,
)}?icon=${BARK_ICON}?sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify( )}?icon=${BARK_ICON}&sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(
params, params,
)}`, )}`,
headers: { headers: {
@@ -978,6 +1019,8 @@ function aibotkNotify(text, desp) {
resolve(data); resolve(data);
} }
}); });
} else {
resolve();
} }
}); });
} }
@@ -1018,6 +1061,52 @@ function fsBotNotify(text, desp) {
}); });
} }
async function smtpNotify(text, desp) {
if (![SMTP_SERVER, SMTP_EMAIL, SMTP_PASSWORD].every(Boolean)) {
return;
}
try {
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport(
`${SMTP_SSL === 'true' ? 'smtps:' : 'smtp:'}//${SMTP_SERVER}`,
{
auth: {
user: SMTP_EMAIL,
pass: SMTP_PASSWORD,
},
},
);
const addr = SMTP_NAME ? `"${SMTP_NAME}" <${SMTP_EMAIL}>` : SMTP_EMAIL;
const info = await transporter.sendMail({
from: addr,
to: addr,
subject: text,
text: desp,
});
if (!!info.messageId) {
console.log('SMTP发送通知消息成功🎉\n');
return true;
}
console.log('SMTP发送通知消息失败!!\n');
} catch (e) {
console.log('SMTP发送通知消息出现错误!!\n');
console.log(e);
}
}
function smtpNotify(text, desp) {
return new Promise((resolve) => {
if (SMTP_SERVER && SMTP_SSL && SMTP_EMAIL && SMTP_PASSWORD && SMTP_NAME) {
// todo: Node.js并没有内置的 smtp 实现,需要调用外部库,因为不清楚这个文件的模块依赖情况,所以留给有缘人实现
} else {
resolve();
}
});
}
module.exports = { module.exports = {
sendNotify, sendNotify,
BARK_PUSH, BARK_PUSH,
+56 -9
View File
@@ -9,6 +9,10 @@ import re
import threading import threading
import time import time
import urllib.parse import urllib.parse
import smtplib
from email.mime.text import MIMEText
from email.header import Header
from email.utils import formataddr
import requests import requests
@@ -61,10 +65,11 @@ push_config = {
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版 'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY 'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY
'DEER_URL': '', # PushDeer 的 PUSHDEER_URL
'CHAT_URL': '', # synology chat url 'CHAT_URL': '', # synology chat url
'CHAT_TOKEN': '', # synology chat token 'CHAT_TOKEN': '', # synology chat token
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌 'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码 'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
@@ -85,6 +90,12 @@ push_config = {
'AIBOTK_KEY': '', # 智能微秘书 个人中心的apikey 文档地址:http://wechat.aibotk.com/docs/about 'AIBOTK_KEY': '', # 智能微秘书 个人中心的apikey 文档地址:http://wechat.aibotk.com/docs/about
'AIBOTK_TYPE': '', # 智能微秘书 发送目标 room 或 contact 'AIBOTK_TYPE': '', # 智能微秘书 发送目标 room 或 contact
'AIBOTK_NAME': '', # 智能微秘书 发送群名 或者好友昵称和type要对应好 'AIBOTK_NAME': '', # 智能微秘书 发送群名 或者好友昵称和type要对应好
'SMTP_SERVER': '', # SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
'SMTP_SSL': 'false', # SMTP 发送邮件服务器是否使用 SSL,填写 true 或 false
'SMTP_EMAIL': '', # SMTP 收发件邮箱,通知将会由自己发给自己
'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
} }
notify_function = [] notify_function = []
# fmt: on # fmt: on
@@ -280,14 +291,17 @@ def pushdeer(title: str, content: str) -> None:
print("PushDeer 服务启动") print("PushDeer 服务启动")
data = {"text": title, "desp": content, "type": "markdown", "pushkey": push_config.get("DEER_KEY")} data = {"text": title, "desp": content, "type": "markdown", "pushkey": push_config.get("DEER_KEY")}
url = 'https://api2.pushdeer.com/message/push' url = 'https://api2.pushdeer.com/message/push'
if push_config.get("DEER_URL"):
url = push_config.get("DEER_URL")
response = requests.post(url, data=data).json() response = requests.post(url, data=data).json()
if len(response.get("content").get("result")) > 0: if len(response.get("content").get("result")) > 0:
print("PushDeer 推送成功!") print("PushDeer 推送成功!")
else: else:
print("PushDeer 推送失败!错误信息:", response) print("PushDeer 推送失败!错误信息:", response)
def chat(title: str, content: str) -> None: def chat(title: str, content: str) -> None:
""" """
通过Chat 推送消息 通过Chat 推送消息
@@ -299,14 +313,14 @@ def chat(title: str, content: str) -> None:
data = 'payload=' + json.dumps({'text': title + '\n' + content}) data = 'payload=' + json.dumps({'text': title + '\n' + content})
url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN") url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN")
response = requests.post(url, data=data) response = requests.post(url, data=data)
if response.status_code == 200: if response.status_code == 200:
print("Chat 推送成功!") print("Chat 推送成功!")
else: else:
print("Chat 推送失败!错误信息:", response) print("Chat 推送失败!错误信息:", response)
def pushplus_bot(title: str, content: str) -> None: def pushplus_bot(title: str, content: str) -> None:
""" """
通过 push+ 推送消息。 通过 push+ 推送消息。
@@ -558,6 +572,30 @@ def aibotk(title: str, content: str) -> None:
print(f'智能微秘书 推送失败!{response["error"]}') print(f'智能微秘书 推送失败!{response["error"]}')
def smtp(title: str, content: str) -> None:
"""
使用 SMTP 邮件 推送消息。
"""
if not push_config.get("SMTP_SERVER") or not push_config.get("SMTP_SSL") or not push_config.get("SMTP_EMAIL") or not push_config.get("SMTP_PASSWORD") or not push_config.get("SMTP_NAME"):
print("SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送")
return
print("SMTP 邮件 服务启动")
message = MIMEText(content, 'plain', 'utf-8')
message['From'] = formataddr((Header(push_config.get("SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL")))
message['To'] = formataddr((Header(push_config.get("SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL")))
message['Subject'] = Header(title, 'utf-8')
try:
smtp_server = smtplib.SMTP_SSL(push_config.get("SMTP_SERVER")) if push_config.get("SMTP_SSL") == 'true' else smtplib.SMTP(push_config.get("SMTP_SERVER"))
smtp_server.login(push_config.get("SMTP_EMAIL"), push_config.get("SMTP_PASSWORD"))
smtp_server.sendmail(push_config.get("SMTP_EMAIL"), push_config.get("SMTP_EMAIL"), message.as_bytes())
smtp_server.close()
print("SMTP 邮件 推送成功!")
except Exception as e:
print(f'SMTP 邮件 推送失败!{e}')
def one() -> str: def one() -> str:
""" """
获取一条一言。 获取一条一言。
@@ -599,7 +637,9 @@ if push_config.get("QYWX_KEY"):
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"): if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
notify_function.append(telegram_bot) notify_function.append(telegram_bot)
if push_config.get("AIBOTK_KEY") and push_config.get("AIBOTK_TYPE") and push_config.get("AIBOTK_NAME"): if push_config.get("AIBOTK_KEY") and push_config.get("AIBOTK_TYPE") and push_config.get("AIBOTK_NAME"):
notify_function.append(aibotk) notify_function.append(aibotk)
if push_config.get("SMTP_SERVER") and push_config.get("SMTP_SSL") and push_config.get("SMTP_EMAIL") and push_config.get("SMTP_PASSWORD") and push_config.get("SMTP_NAME"):
notify_function.append(smtp)
def send(title: str, content: str) -> None: def send(title: str, content: str) -> None:
@@ -607,6 +647,13 @@ def send(title: str, content: str) -> None:
print(f"{title} 推送内容为空!") print(f"{title} 推送内容为空!")
return return
# 根据标题跳过一些消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔
skipTitle = os.getenv("SKIP_PUSH_TITLE")
if skipTitle:
if (title in re.split("\n", skipTitle)):
print(f"{title} 在SKIP_PUSH_TITLE环境变量内,跳过推送!")
return
hitokoto = push_config.get("HITOKOTO") hitokoto = push_config.get("HITOKOTO")
text = one() if hitokoto else "" text = one() if hitokoto else ""
+3 -2
View File
@@ -5,12 +5,13 @@
"dependencies": { "dependencies": {
"crypto-js": "^4.0.0", "crypto-js": "^4.0.0",
"download": "^8.0.0", "download": "^8.0.0",
"http-server": "^0.12.3",
"got": "^11.5.1", "got": "^11.5.1",
"http-server": "^0.12.3",
"nodemailer": "^6.8.0",
"qrcode-terminal": "^0.12.0", "qrcode-terminal": "^0.12.0",
"request": "^2.88.2", "request": "^2.88.2",
"tough-cookie": "^4.0.0", "tough-cookie": "^4.0.0",
"tunnel": "0.0.6", "tunnel": "0.0.6",
"ws": "^7.4.3" "ws": "^7.4.3"
} }
} }
+2 -2
View File
@@ -5,14 +5,14 @@ const envFound = dotenv.config();
const accessKey = process.env.QINIU_AK; const accessKey = process.env.QINIU_AK;
const secretKey = process.env.QINIU_SK; const secretKey = process.env.QINIU_SK;
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey); const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
const key = 'version.ts'; const key = 'version.yaml';
const options = { const options = {
scope: `${process.env.QINIU_SCOPE}:${key}`, scope: `${process.env.QINIU_SCOPE}:${key}`,
}; };
const putPolicy = new qiniu.rs.PutPolicy(options); const putPolicy = new qiniu.rs.PutPolicy(options);
const uploadToken = putPolicy.uploadToken(mac); const uploadToken = putPolicy.uploadToken(mac);
const localFile = 'src/version.ts'; const localFile = 'version.yaml';
const config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z1 }); const config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z1 });
const formUploader = new qiniu.form_up.FormUploader(config); const formUploader = new qiniu.form_up.FormUploader(config);
const putExtra = new qiniu.form_up.PutExtra( const putExtra = new qiniu.form_up.PutExtra(
+61 -14
View File
@@ -1,7 +1,25 @@
#!/usr/bin/env bash #!/usr/bin/env bash
create_token() {
local token_command="tsx ${dir_root}/back/token.ts"
local token_file="${dir_root}static/build/token.js"
if [[ -f $token_file ]]; then
token_command="node ${token_file}"
fi
token=$(eval "$token_command")
}
get_token() { get_token() {
token=$(cat $file_auth_token | jq -r .value) if [[ -f $file_auth_token ]]; then
token=$(cat $file_auth_token | jq -r .value)
local expiration=$(cat $file_auth_token | jq -r .expiration)
local currentTimeStamp=$(date +%s)
if [[ $currentTimeStamp -ge $expiration ]]; then
create_token
fi
else
create_token
fi
} }
add_cron_api() { add_cron_api() {
@@ -10,10 +28,16 @@ add_cron_api() {
local schedule=$(echo "$1" | awk -F ":" '{print $1}') local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}') local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}') local name=$(echo "$1" | awk -F ":" '{print $3}')
local sub_id=$(echo "$1" | awk -F ":" '{print $4}')
else else
local schedule=$1 local schedule=$1
local command=$2 local command=$2
local name=$3 local name=$3
local sub_id=$4
fi
if [[ ! $sub_id ]];then
sub_id="null"
fi fi
local api=$( local api=$(
@@ -25,11 +49,11 @@ add_cron_api() {
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -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\"}" \ --data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"sub_id\":$sub_id}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "$name -> 添加成功" echo -e "$name -> 添加成功"
else else
@@ -64,8 +88,8 @@ update_cron_api() {
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \ --data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "$name -> 更新成功" echo -e "$name -> 更新成功"
else else
@@ -96,8 +120,8 @@ update_cron_command_api() {
--data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \ --data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "$command -> 更新成功" echo -e "$command -> 更新成功"
else else
@@ -121,8 +145,8 @@ del_cron_api() {
--data-raw "[$ids]" \ --data-raw "[$ids]" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "成功" echo -e "成功"
else else
@@ -151,8 +175,8 @@ update_cron() {
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \ --data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code != 200 ]]; then if [[ $code != 200 ]]; then
echo -e "\n## 更新任务状态失败(${message})\n" >>$dir_log/$log_path echo -e "\n## 更新任务状态失败(${message})\n" >>$dir_log/$log_path
fi fi
@@ -175,8 +199,8 @@ notify_api() {
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \ --data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "通知发送成功" echo -e "通知发送成功"
else else
@@ -184,4 +208,27 @@ notify_api() {
fi fi
} }
find_cron_api() {
local params=$1
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/detail?$params&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://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--compressed
)
data=$(echo "$api" | jq -r .data)
if [[ $data == 'null' ]]; then
echo -e ""
else
name=$(echo "$api" | jq -r .data.name)
echo -e "$name"
fi
}
get_token get_token
Regular → Executable
-4
View File
@@ -1,9 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
if [[ -z ${BotRepoUrl} ]]; then if [[ -z ${BotRepoUrl} ]]; then
url="https://github.com/SuMaiKaDe/bot.git" url="https://github.com/SuMaiKaDe/bot.git"
repo_path="${dir_repo}/dockerbot" repo_path="${dir_repo}/dockerbot"
Regular → Executable
+10 -13
View File
@@ -1,9 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
. $dir_shell/api.sh
reset_env() { reset_env() {
echo -e "---> 1. 开始检测配置文件\n" echo -e "---> 1. 开始检测配置文件\n"
fix_config fix_config
@@ -29,9 +25,7 @@ copy_dep() {
echo -e "---> 通知文件复制完成\n" echo -e "---> 通知文件复制完成\n"
echo -e "---> 2. 复制nginx配置文件\n" echo -e "---> 2. 复制nginx配置文件\n"
cp -fv $nginx_conf /etc/nginx/nginx.conf init_nginx
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
echo -e "---> 配置文件复制完成\n" echo -e "---> 配置文件复制完成\n"
} }
@@ -70,9 +64,8 @@ check_pm2() {
pm2_log pm2_log
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/api/user?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/api/system?t=$currentTimeStamp" \
-H 'Accept: */*' \ -H 'Accept: */*' \
-H "Authorization: Bearer $token" \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \ -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \
-H 'Referer: http://0.0.0.0:5700/crontab' \ -H 'Referer: http://0.0.0.0:5700/crontab' \
-H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \ -H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \
@@ -87,13 +80,17 @@ check_pm2() {
fi fi
} }
start_public() {
echo -e "=====> 启动公开服务\n"
pm2 delete public --source-map-support --time &>/dev/null
pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null
}
main() { main() {
echo -e "=====> 开始检测" echo -e "=====> 开始检测"
npm i -g pnpm npm i -g pnpm
pnpm setup &>/dev/null patch_version
source ~/.bashrc start_public
pnpm install -g &>/dev/null
pnpm add -g pm2
copy_dep copy_dep
check_ql check_ql
check_nginx check_nginx
Regular → Executable
+25 -21
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 导入通用变量与函数 trap "single_hanle" 2 20 15 14
dir_shell=$QL_DIR/shell single_hanle() {
. $dir_shell/share.sh handle_task_after "$@"
. $dir_shell/api.sh exit 1
}
random_delay() { random_delay() {
local random_delay_max=$RandomDelay local random_delay_max=$RandomDelay
@@ -29,7 +30,7 @@ random_delay() {
done done
local delay_second=$(($(gen_random_num "$random_delay_max") + 1)) local delay_second=$(($(gen_random_num "$random_delay_max") + 1))
echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后执行任务,如需立即终止,请按 CTRL+C...\n" echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后执行\n"
sleep $delay_second sleep $delay_second
fi fi
} }
@@ -75,17 +76,19 @@ run_nohup() {
} }
check_server() { check_server() {
cpu_use=$(top -b -n 1 | grep CPU | grep -v -E 'grep|PID' | awk '{print $2}' | cut -f 1 -d "%" | head -n 1) if [[ $cpu_warn ]] && [[ $mem_warn ]] && [[ $disk_warn ]]; then
local top_result=$(top -b -n 1)
cpu_use=$(echo "$top_result" | grep CPU | grep -v -E 'grep|PID' | awk '{print $2}' | cut -f 1 -d "%" | head -n 1)
mem_free=$(free -m | grep "Mem" | awk '{print $3}' | head -n 1) mem_free=$(free -m | grep "Mem" | awk '{print $3}' | head -n 1)
mem_total=$(free -m | grep "Mem" | awk '{print $2}' | head -n 1) mem_total=$(free -m | grep "Mem" | awk '{print $2}' | head -n 1)
mem_use=$(printf "%d%%" $((mem_free * 100 / mem_total)) | cut -f 1 -d "%" | head -n 1) mem_use=$(printf "%d%%" $((mem_free * 100 / mem_total)) | cut -f 1 -d "%" | head -n 1)
disk_use=$(df -P | grep /dev | grep -v -E '(tmp|boot|shm)' | awk '{print $5}' | cut -f 1 -d "%" | head -n 1) disk_use=$(df -P | grep /dev | grep -v -E '(tmp|boot|shm)' | awk '{print $5}' | cut -f 1 -d "%" | head -n 1)
if [[ $cpu_use -gt $cpu_warn ]] && [[ $cpu_warn ]] || [[ $mem_free -lt $mem_warn ]] || [[ $disk_use -gt $disk_warn ]]; then
if [[ $cpu_use -gt $cpu_warn ]] || [[ $mem_free -lt $mem_warn ]] || [[ $disk_use -gt $disk_warn ]]; then local resource=$(echo "$top_result" | grep -v -E 'grep|Mem|idle|Load|tr' | awk '{$2="";$3="";$4="";$5="";$7="";print $0}' | head -n 10 | tr '\n' '|' | sed s/\|/\\\\n/g)
local resource=$(top -b -n 1 | grep -v -E 'grep|Mem|idle|Load|tr' | awk '{$2="";$3="";$4="";$5="";$7="";print $0}' | head -n 10 | tr '\n' '|' | sed s/\|/\\\\n/g) notify_api "服务器资源异常警告" "当前CPU占用 $cpu_use% 内存占用 $mem_use% 磁盘占用 $disk_use% \n资源占用详情 \n\n $resource"
notify_api "服务器资源异常警告" "当前CPU占用 $cpu_use% 内存占用 $mem_use% 磁盘占用 $disk_use% \n资源占用详情 \n\n $resource" fi
fi fi
} }
@@ -93,6 +96,8 @@ handle_task_before() {
begin_time=$(format_time "$time_format" "$time") begin_time=$(format_time "$time_format" "$time")
begin_timestamp=$(format_timestamp "$time_format" "$time") begin_timestamp=$(format_timestamp "$time_format" "$time")
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
echo -e "## 开始执行... $begin_time\n" echo -e "## 开始执行... $begin_time\n"
[[ $is_macos -eq 0 ]] && check_server [[ $is_macos -eq 0 ]] && check_server
@@ -103,7 +108,6 @@ handle_task_before() {
eval echo $cmd eval echo $cmd
fi fi
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
. $file_task_before "$@" . $file_task_before "$@"
} }
@@ -114,10 +118,10 @@ handle_task_after() {
local end_time=$(format_time "$time_format" "$etime") local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime") local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp)) local diff_time=$(($end_timestamp - $begin_timestamp))
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time 秒     "
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time" [[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time"
echo -e "\n     "
} }
## 正常运行单个脚本,$1:传入参数 ## 正常运行单个脚本,$1:传入参数
@@ -149,7 +153,7 @@ run_concurrent() {
local envs=$(eval echo "\$${env_param}") local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g')) local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g") local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr)) local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]})) runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
@@ -164,7 +168,7 @@ run_concurrent() {
local envs=$(eval echo "\$${env_param}") local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g')) local array=($(echo $envs | sed 's/&/ /g'))
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N") single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%3N")
cd $dir_scripts cd $dir_scripts
local relative_path="${file_param%/*}" local relative_path="${file_param%/*}"
@@ -197,7 +201,7 @@ run_designated() {
local envs=$(eval echo "\$${env_param}") local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g')) local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g") local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr)) local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]})) runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
Regular → Executable
+2 -2
View File
@@ -11,9 +11,9 @@ echo -e "提交master代码"
git push git push
echo -e "更新cdn文件" echo -e "更新cdn文件"
ts-node sample/tool.ts tsx sample/tool.ts
string=$(cat src/version.ts | grep "version" | egrep "[^\']*" -o | egrep "\d\.*") string=$(cat version.yaml | grep "version" | egrep "[^ ]*" -o | egrep "\d\.*")
version="v$string" version="v$string"
echo -e "当前版本$version" echo -e "当前版本$version"
+12 -6
View File
@@ -1,12 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
days=$1 days=$1
## 删除运行js脚本的旧日志 ## 删除运行脚本的旧日志
remove_js_log() { remove_js_log() {
local log_full_path_list=$(find $dir_log/ -name "*.log") local log_full_path_list=$(find $dir_log/ -name "*.log")
local diff_time local diff_time
@@ -18,7 +14,17 @@ remove_js_log() {
else else
diff_time=$(($(date +%s) - $(date +%s -d "$log_date"))) diff_time=$(($(date +%s) - $(date +%s -d "$log_date")))
fi fi
[[ $diff_time -gt $((${days} * 86400)) ]] && rm -vf $log if [[ $diff_time -gt $((${days} * 86400)) ]]; then
local log_path=$(echo "$log" | sed "s,${dir_log}/,,g")
local result=$(find_cron_api "log_path=$log_path")
echo -e "查询文件 $log_path"
if [[ -z $result ]]; then
echo -e "删除中~"
rm -vf $log
else
echo -e "正在被 $result 使用,跳过~"
fi
fi
fi fi
done done
} }
+44 -29
View File
@@ -79,12 +79,16 @@ import_config() {
default_cron="$(random_range 0 59) $(random_range 0 23) * * *" default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
fi fi
cpu_warn=${CpuWarn:-80} cpu_warn=${CpuWarn}
mem_warn=${MemoryWarn:-80} mem_warn=${MemoryWarn}
disk_warn=${DiskWarn:-90} disk_warn=${DiskWarn}
} }
set_proxy() { set_proxy() {
local proxy="$1"
if [[ $proxy ]]; then
proxy_url="$proxy"
fi
if [[ $proxy_url ]]; then if [[ $proxy_url ]]; then
export http_proxy="${proxy_url}" export http_proxy="${proxy_url}"
export https_proxy="${proxy_url}" export https_proxy="${proxy_url}"
@@ -303,13 +307,14 @@ update_depend() {
} }
git_clone_scripts() { git_clone_scripts() {
local url=$1 local url="$1"
local dir=$2 local dir="$2"
local branch=$3 local branch="$3"
local proxy="$4"
[[ $branch ]] && local part_cmd="-b $branch " [[ $branch ]] && local part_cmd="-b $branch "
echo -e "开始克隆仓库 $url$dir\n" echo -e "开始克隆仓库 $url$dir\n"
set_proxy set_proxy "$proxy"
git clone $part_cmd $url $dir git clone $part_cmd $url $dir
exit_status=$? exit_status=$?
unset_proxy unset_proxy
@@ -319,15 +324,15 @@ git_pull_scripts() {
local dir_current=$(pwd) local dir_current=$(pwd)
local dir_work="$1" local dir_work="$1"
local branch="$2" local branch="$2"
local proxy="$3"
cd $dir_work cd $dir_work
echo -e "开始更新仓库:$dir_work\n" echo -e "开始更新仓库:$dir_work"
set_proxy set_proxy "$proxy"
git fetch --all git fetch --all
git pull 1>/dev/null
exit_status=$? exit_status=$?
git pull &>/dev/null
unset_proxy unset_proxy
reset_branch "$branch"
cd $dir_current cd $dir_current
} }
@@ -338,19 +343,21 @@ reset_romote_url() {
local url=$2 local url=$2
local branch="$3" local branch="$3"
cd $dir_work
if [[ -d "$dir_work/.git" ]]; then if [[ -d "$dir_work/.git" ]]; then
cd $dir_work
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null [[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
git remote set-url origin $url &>/dev/null git remote set-url origin $url &>/dev/null
else
local part_cmd="" git init
reset_branch "$branch" git remote add origin $url &>/dev/null
cd $dir_current
fi fi
reset_branch "$branch"
cd $dir_current
} }
reset_branch() { reset_branch() {
local branch="$1" local branch="$1"
local part_cmd="HEAD"
if [[ $branch ]]; then if [[ $branch ]]; then
part_cmd="origin/${branch}" part_cmd="origin/${branch}"
git checkout -B "$branch" &>/dev/null git checkout -B "$branch" &>/dev/null
@@ -375,10 +382,6 @@ reload_pm2() {
echo -e "启动定时任务服务\n" echo -e "启动定时任务服务\n"
pm2 delete schedule --source-map-support --time &>/dev/null pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
echo -e "启动公开服务\n"
pm2 delete public --source-map-support --time &>/dev/null
pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null
} }
diff_time() { diff_time() {
@@ -410,9 +413,9 @@ format_log_time() {
local time="$2" local time="$2"
if [[ $is_macos -eq 1 ]]; then if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S") echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S-%3N")
else else
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S") echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S-%3N")
fi fi
} }
@@ -428,16 +431,17 @@ format_timestamp() {
} }
patch_version() { patch_version() {
# 兼容pnpm@7
pnpm setup &>/dev/null
source ~/.bashrc
if [[ $PipMirror ]]; then if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror pip3 config set global.index-url $PipMirror
fi fi
if [[ $NpmMirror ]]; then if [[ $NpmMirror ]]; then
npm config set registry $NpmMirror pnpm config set registry $NpmMirror
fi fi
# 兼容pnpm@7
pnpm setup &>/dev/null
source ~/.bashrc
pnpm install -g &>/dev/null pnpm install -g &>/dev/null
if [[ -f "$dir_root/db/cookie.db" ]]; then if [[ -f "$dir_root/db/cookie.db" ]]; then
@@ -447,9 +451,7 @@ patch_version() {
echo echo
fi fi
if ! type ts-node &>/dev/null; then pnpm add -g pm2 tsx
pnpm add -g ts-node typescript tslib
fi
git config --global pull.rebase false git config --global pull.rebase false
@@ -480,6 +482,19 @@ patch_version() {
fi fi
} }
init_nginx() {
cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
ipv6=$(ip a | grep inet6)
ipv6Str=""
if [[ $ipv6 ]]; then
ipv6Str="listen [::]:5700 ipv6only=on;"
fi
sed -i "s,IPV6_CONFIG,${ipv6Str},g" /etc/nginx/conf.d/front.conf
}
init_env init_env
detect_termux detect_termux
detect_macos detect_macos
+25 -11
View File
@@ -15,7 +15,11 @@ define_program() {
elif [[ $file_param == *.sh ]]; then elif [[ $file_param == *.sh ]]; then
which_program="bash" which_program="bash"
elif [[ $file_param == *.ts ]]; then elif [[ $file_param == *.ts ]]; then
which_program="ts-node-transpile-only" if ! type tsx &>/dev/null; then
which_program="ts-node-transpile-only"
else
which_program="tsx"
fi
else else
which_program="" which_program=""
fi fi
@@ -32,8 +36,8 @@ handle_log_path() {
if [[ ! -z $ID ]]; then if [[ ! -z $ID ]]; then
suffix="_${ID}" suffix="_${ID}"
fi fi
time=$(date "+$time_format") time=$(date "+$mtime_format")
log_time=$(format_log_time "$time_format" "$time") log_time=$(format_log_time "$mtime_format" "$time")
log_dir_tmp="${file_param##*/}" log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then if [[ $file_param == /* ]]; then
@@ -48,34 +52,44 @@ handle_log_path() {
log_dir="${log_dir_tmp%.*}${suffix}" log_dir="${log_dir_tmp%.*}${suffix}"
log_path="$log_dir/$log_time.log" log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1" cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd="" if [[ "$show_log" == "true" ]]; then
make_dir "$dir_log/$log_dir" cmd=""
else
make_dir "$dir_log/$log_dir"
fi
} }
format_params() { format_params() {
time_format="%Y-%m-%d %H:%M:%S" time_format="%Y-%m-%d %H:%M:%S"
mtime_format="%Y-%m-%d %H:%M:%S.%3N"
timeoutCmd="" timeoutCmd=""
if type timeout &>/dev/null; then if type timeout &>/dev/null; then
timeoutCmd="timeout -k 10s $command_timeout_time " timeoutCmd="timeout --foreground -s 14 -k 10s $command_timeout_time "
fi fi
params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g') # params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g')
} }
show_log="false" while getopts ":lm:" opt; do
while getopts ":l" opt; do
case $opt in case $opt in
l) l)
show_log="true" show_log="true"
;; ;;
m)
max_time="$OPTARG"
;;
esac esac
done done
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1)) [[ $show_log ]] && shift $(($OPTIND - 1))
if [[ $max_time ]]; then
shift $(($OPTIND - 1))
command_timeout_time="$max_time"
fi
format_params "$@" format_params "$@"
define_program "$@" define_program "$@"
handle_log_path "$@" handle_log_path "$@"
eval . $dir_shell/otask.sh "$params" "$cmd" eval . $dir_shell/otask.sh "$cmd"
[[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path" [[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path"
exit 0 exit 0
+100 -89
View File
@@ -26,33 +26,6 @@ diff_cron() {
fi fi
} }
## 检测配置文件版本
detect_config_version() {
## 识别出两个文件的版本号
ver_config_sample=$(grep " Version: " $file_config_sample | perl -pe "s|.+v((\d+\.?){3})|\1|")
[[ -f $file_config_user ]] && ver_config_user=$(grep " Version: " $file_config_user | perl -pe "s|.+v((\d+\.?){3})|\1|")
## 删除旧的发送记录文件
[[ -f $send_mark ]] && [[ $(cat $send_mark) != $ver_config_sample ]] && rm -f $send_mark
## 识别出更新日期和更新内容
update_date=$(grep " Date: " $file_config_sample | awk -F ": " '{print $2}')
update_content=$(grep " Update Content: " $file_config_sample | awk -F ": " '{print $2}')
## 如果是今天,并且版本号不一致,则发送通知
if [[ -f $file_config_user ]] && [[ $ver_config_user != $ver_config_sample ]] && [[ $update_date == $(date "+%Y-%m-%d") ]]; then
if [[ ! -f $send_mark ]]; then
local notify_title="配置文件更新通知"
local notify_content="更新日期: $update_date\n用户版本: $ver_config_user\n新的版本: $ver_config_sample\n更新内容: $update_content\n更新说明: 如需使用新功能请对照config.sample.sh,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。\n"
echo -e $notify_content
notify_api "$notify_title" "$notify_content"
[[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark
fi
else
[[ -f $send_mark ]] && rm -f $send_mark
fi
}
## 输出是否有新的或失效的定时任务,$1:新的或失效的任务清单文件路径,$2:新/失效 ## 输出是否有新的或失效的定时任务,$1:新的或失效的任务清单文件路径,$2:新/失效
output_list_add_drop() { output_list_add_drop() {
local list=$1 local list=$1
@@ -124,7 +97,7 @@ add_cron() {
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron" [[ -z $cron_line ]] && cron_line="$default_cron"
result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name") result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name:$SUB_ID")
echo -e "$result" echo -e "$result"
if [[ $detail ]]; then if [[ $detail ]]; then
detail="${detail}${result}\n" detail="${detail}${result}\n"
@@ -144,6 +117,9 @@ update_repo() {
local dependence="$4" local dependence="$4"
local branch="$5" local branch="$5"
local extensions="$6" local extensions="$6"
local proxy="$7"
local autoAddCron="$8"
local autoDelCron="$9"
local tmp="${url%/*}" local tmp="${url%/*}"
local authorTmp1="${tmp##*/}" local authorTmp1="${tmp##*/}"
local authorTmp2="${authorTmp1##*:}" local authorTmp2="${authorTmp1##*:}"
@@ -156,13 +132,14 @@ update_repo() {
local formatUrl="$url" local formatUrl="$url"
if [[ -d ${repo_path}/.git ]]; then if [[ -d ${repo_path}/.git ]]; then
reset_romote_url ${repo_path} "${formatUrl}" "${branch}" reset_romote_url ${repo_path} "${formatUrl}" "${branch}"
git_pull_scripts ${repo_path} "${branch}" git_pull_scripts ${repo_path} "${branch}" "${proxy}"
else else
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" rm -rf ${repo_path} &>/dev/null
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" "${proxy}"
fi fi
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新${repo_path}成功...\n" echo -e "\n更新${repo_path}成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" "$autoAddCron" "$autoDelCron"
else else
echo -e "\n更新${repo_path}失败,请检查网络...\n" echo -e "\n更新${repo_path}失败,请检查网络...\n"
fi fi
@@ -170,41 +147,58 @@ update_repo() {
## 更新所有 raw 文件 ## 更新所有 raw 文件
update_raw() { update_raw() {
echo -e "--------------------------------------------------------------\n"
local url="$1" local url="$1"
local proxy="$2"
local autoAddCron="$3"
local autoDelCron="$4"
if [[ ! $autoAddCron ]]; then
autoAddCron=${AutoAddCron}
fi
if [[ ! $autoDelCron ]]; then
autoDelCron=${AutoDelCron}
fi
local proxyStr=""
if [[ $proxy ]]; then
if [[ $url == http:* ]]; then
proxyStr="-e \"http_proxy=${proxy}\""
elif [[ $url == https:* ]]; then
proxyStr="-e \"http_proxy=${proxy};https_proxy=${proxy}\""
fi
fi
local raw_url="$url" local raw_url="$url"
local suffix="${raw_url##*.}" local suffix="${raw_url##*.}"
local raw_file_name="${uniq_path}.${suffix}" local raw_file_name="${uniq_path}.${suffix}"
echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n" echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
set_proxy wget -q --no-check-certificate $proxyStr -O "$dir_raw/${raw_file_name}.new" ${raw_url}
wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
unset_proxy
if [[ $? -eq 0 ]]; then if [[ $? -eq 0 ]]; then
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}" mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
echo -e "下载 ${raw_file_name} 成功...\n" echo -e "下载 ${raw_file_name} 成功...\n"
cd $dir_raw cd $dir_raw
local filename="raw_${raw_file_name}" local filename="raw_${raw_file_name}"
local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task $filename" | perl -pe "s|.*ID=(.*) $cmd_task $filename\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}') local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* $filename" | perl -pe "s|.*ID=(.*) $cmd_task.* $filename\.*|\1|" | head -1 | awk -F " " '{print $1}')
cp -f $raw_file_name $dir_scripts/${filename} cp -f $raw_file_name $dir_scripts/${filename}
cron_line=$( if [[ -z $cron_id ]] && [[ ${autoAddCron} == true ]]; then
perl -ne "{ cron_line=$(
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$raw_file_name/ perl -ne "{
}" $raw_file_name | print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$raw_file_name/
perl -pe "{ }" $raw_file_name |
s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$raw_file_name.*|\1|g; perl -pe "{
s|\*([\d\*])(.*)|\1\2|g; s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$raw_file_name.*|\1|g;
s| | |g; s|\*([\d\*])(.*)|\1\2|g;
}" | sort -u | head -1 s| | |g;
) }" | sort -u | head -1
cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1) )
[[ -z $cron_name ]] && cron_name="$raw_file_name" cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs) [[ -z $cron_name ]] && cron_name="$raw_file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron" [[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
if [[ -z $cron_id ]]; then [[ -z $cron_line ]] && cron_line="$default_cron"
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name") result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name:$SUB_ID")
echo -e "$result\n" echo -e "$result\n"
notify_api "新增任务通知" "\n$result" notify_api "新增任务通知" "\n$result"
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id" # update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
@@ -218,13 +212,10 @@ update_raw() {
## 调用用户自定义的extra.sh ## 调用用户自定义的extra.sh
run_extra_shell() { run_extra_shell() {
if [[ ${EnableExtraShell} == true ]]; then if [[ -f $file_extra_shell ]]; then
if [[ -f $file_extra_shell ]]; then . $file_extra_shell
echo -e "--------------------------------------------------------------\n" else
. $file_extra_shell echo -e "$file_extra_shell文件不存在,跳过执行...\n"
else
echo -e "$file_extra_shell文件不存在,跳过执行...\n"
fi
fi fi
} }
@@ -244,23 +235,27 @@ usage() {
## 更新qinglong ## 更新qinglong
update_qinglong() { update_qinglong() {
patch_version &>/dev/null local mirror="gitee"
local githubStatus=$(curl -s -m 2 -IL "https://google.com" | grep 200)
if [[ ! -z $githubStatus ]]; then
mirror="github"
fi
echo -e "使用 ${mirror} 源更新...\n"
export isFirstStartServer=false export isFirstStartServer=false
local all_branch=$(git branch -a) local all_branch=$(cd ${dir_root} && git branch -a)
local primary_branch="master" local primary_branch="master"
if [[ "${all_branch}" =~ "${current_branch}" ]]; then if [[ "${all_branch}" =~ "${current_branch}" ]]; then
primary_branch="${current_branch}" primary_branch="${current_branch}"
fi fi
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json) [[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
reset_romote_url ${dir_root} "https://github.com/whyour/qinglong.git" ${primary_branch} reset_romote_url ${dir_root} "https://${mirror}.com/whyour/qinglong.git" ${primary_branch}
git_pull_scripts $dir_root ${primary_branch} git_pull_scripts $dir_root ${primary_branch}
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙源文件成功...\n" echo -e "\n更新青龙源文件成功...\n"
reset_romote_url ${dir_root} "https://${mirror}.com/whyour/qinglong.git" ${primary_branch}
cp -f $file_config_sample $dir_config/config.sample.sh cp -f $file_config_sample $dir_config/config.sample.sh
detect_config_version
update_depend update_depend
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json) [[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
@@ -275,17 +270,17 @@ update_qinglong() {
update_qinglong_static() { update_qinglong_static() {
local no_restart="$1" local no_restart="$1"
local primary_branch="$2" local primary_branch="$2"
local url="https://github.com/whyour/qinglong-static.git" local url="https://${mirror}.com/whyour/qinglong-static.git"
if [[ -d ${ql_static_repo}/.git ]]; then if [[ -d ${ql_static_repo}/.git ]]; then
reset_romote_url ${ql_static_repo} ${url} ${primary_branch} reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
git_pull_scripts ${ql_static_repo} ${primary_branch} git_pull_scripts ${ql_static_repo} ${primary_branch}
else else
rm -rf ${ql_static_repo} &>/dev/null
git_clone_scripts ${url} ${ql_static_repo} ${primary_branch} git_clone_scripts ${url} ${ql_static_repo} ${primary_branch}
fi fi
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙静态资源成功...\n" echo -e "\n更新青龙静态资源成功...\n"
local static_version=$(cat $dir_root/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1) reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
echo -e "\n当前版本 $static_version...\n"
rm -rf $dir_static/* rm -rf $dir_static/*
cp -rf $ql_static_repo/* $dir_static cp -rf $ql_static_repo/* $dir_static
@@ -309,6 +304,15 @@ diff_scripts() {
local blackword="$4" local blackword="$4"
local dependence="$5" local dependence="$5"
local extensions="$6" local extensions="$6"
local autoAddCron="$7"
local autoDelCron="$8"
if [[ ! $autoAddCron ]]; then
autoAddCron=${AutoAddCron}
fi
if [[ ! $autoDelCron ]]; then
autoDelCron=${AutoDelCron}
fi
gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions"
@@ -318,13 +322,13 @@ diff_scripts() {
if [[ -s $list_drop ]]; then if [[ -s $list_drop ]]; then
output_list_add_drop $list_drop "失效" output_list_add_drop $list_drop "失效"
if [[ ${AutoDelCron} == true ]]; then if [[ ${autoDelCron} == true ]]; then
del_cron $list_drop $uniq_path del_cron $list_drop $uniq_path
fi fi
fi fi
if [[ -s $list_add ]]; then if [[ -s $list_add ]]; then
output_list_add_drop $list_add "新" output_list_add_drop $list_add "新"
if [[ ${AutoAddCron} == true ]]; then if [[ ${autoAddCron} == true ]]; then
add_cron $list_add $uniq_path add_cron $list_add $uniq_path
fi fi
fi fi
@@ -389,12 +393,12 @@ gen_list_repo() {
filename=$(basename $file) filename=$(basename $file)
cp -f $file "$dir_scripts/${uniq_path}/${filename}" cp -f $file "$dir_scripts/${uniq_path}/${filename}"
echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list" echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
cron_id=$(cat $list_crontab_user | grep -E "$cmd_task ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}') # cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task.* ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
if [[ $cron_id ]]; then # if [[ $cron_id ]]; then
result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id") # result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
fi # fi
done done
grep -E "${cmd_task} ${uniq_path}" ${list_crontab_user} | perl -pe "s|.*ID=(.*) ${cmd_task} (${uniq_path}.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${uniq_path}_user.list" grep -E "${cmd_task}.* ${uniq_path}" ${list_crontab_user} | perl -pe "s|.*ID=(.*) ${cmd_task}.* (${uniq_path}.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${uniq_path}_user.list"
cd $dir_current cd $dir_current
} }
@@ -425,13 +429,16 @@ main() {
done done
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1)) [[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
local p1=$1 local p1="${1}"
local p2=$2 local p2="${2}"
local p3=$3 local p3="${3}"
local p4=$4 local p4="${4}"
local p5=$5 local p5="${5}"
local p6=$6 local p6="${6}"
local p7=$7 local p7="${7}"
local p8="${8}"
local p9="${9}"
local p10="${10}"
local log_dir="${p1}" local log_dir="${p1}"
make_dir "$dir_log/$log_dir" make_dir "$dir_log/$log_dir"
local log_time=$(date "+%Y-%m-%d-%H-%M-%S") local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
@@ -447,7 +454,10 @@ main() {
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp" [[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
local begin_time=$(format_time "$time_format" "$time") local begin_time=$(format_time "$time_format" "$time")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
fi
if [[ -s $task_error_log_path ]]; then if [[ -s $task_error_log_path ]]; then
eval cat $task_error_log_path $cmd eval cat $task_error_log_path $cmd
@@ -470,7 +480,7 @@ main() {
repo) repo)
get_uniq_path "$p2" "$p6" get_uniq_path "$p2" "$p6"
if [[ -n $p2 ]]; then if [[ -n $p2 ]]; then
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" "$p7" update_repo "$p2" "$p3" "$p4" "$p5" "$p6" "$p7" "$p8" "$p9" "$p10"
else else
eval echo -e "命令输入错误...\\\n" $cmd eval echo -e "命令输入错误...\\\n" $cmd
eval usage $cmd eval usage $cmd
@@ -479,7 +489,7 @@ main() {
raw) raw)
get_uniq_path "$p2" get_uniq_path "$p2"
if [[ -n $p2 ]]; then if [[ -n $p2 ]]; then
update_raw "$p2" update_raw "$p2" "$p3" "$p4"
else else
eval echo -e "命令输入错误...\\\n" $cmd eval echo -e "命令输入错误...\\\n" $cmd
eval usage $cmd eval usage $cmd
@@ -497,7 +507,7 @@ main() {
resetlet) resetlet)
auth_value=$(cat $file_auth_user | jq '.retries =0' -c) auth_value=$(cat $file_auth_user | jq '.retries =0' -c)
echo "$auth_value" >$file_auth_user echo "$auth_value" >$file_auth_user
echo -e "重置登录错误次数成功" $cmd eval echo -e "重置登录错误次数成功" $cmd
;; ;;
resettfa) resettfa)
auth_value=$(cat $file_auth_user | jq '.twoFactorActivated =false' | jq '.twoFactorActived =false' -c) auth_value=$(cat $file_auth_user | jq '.twoFactorActivated =false' | jq '.twoFactorActived =false' -c)
@@ -514,10 +524,11 @@ main() {
local end_time=$(format_time "$time_format" "$etime") local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime") local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp)) local diff_time=$(($end_timestamp - $begin_timestamp))
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time" [[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n          " $cmd
if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒     " $cmd
fi
if [[ -f $file_path ]]; then if [[ -f $file_path ]]; then
cat $file_path cat $file_path
+39
View File
@@ -0,0 +1,39 @@
import React, { useRef, useState, useEffect } from 'react';
import { Tooltip, Typography } from 'antd';
import { CopyOutlined, CheckOutlined } from '@ant-design/icons';
import { CopyToClipboard } from 'react-copy-to-clipboard';
const { Link } = Typography;
const Copy = ({ text }: { text: string }) => {
const [copied, setCopied] = useState(false);
const copyIdRef = useRef<number>();
const copyText = (e?: React.MouseEvent) => {
e?.preventDefault();
e?.stopPropagation();
setCopied(true);
cleanCopyId();
copyIdRef.current = window.setTimeout(() => {
setCopied(false);
}, 3000);
};
const cleanCopyId = () => {
window.clearTimeout(copyIdRef.current!);
};
return (
<Link onClick={copyText} style={{ marginLeft: 1 }}>
<CopyToClipboard text={text}>
<Tooltip key="copy" title={copied ? '复制成功' : '复制'}>
{copied ? <CheckOutlined /> : <CopyOutlined />}
</Tooltip>
</CopyToClipboard>
</Link>
);
};
export default Copy;
+1 -1
View File
@@ -1,7 +1,7 @@
import { createFromIconfontCN } from '@ant-design/icons'; import { createFromIconfontCN } from '@ant-design/icons';
const IconFont = createFromIconfontCN({ const IconFont = createFromIconfontCN({
scriptUrl: ['//at.alicdn.com/t/font_3354854_ds8pa06q1qa.js'], scriptUrl: ['//at.alicdn.com/t/c/font_3354854_ob5y15ewlyq.js'],
}); });
export default IconFont; export default IconFont;
+404
View File
@@ -0,0 +1,404 @@
import React, {
useRef,
useEffect,
useContext,
createContext,
useReducer,
useState,
useMemo,
} from 'react';
import { throttle, isNumber, debounce } from 'lodash';
const initialState = {
// 行高度
rowHeight: 0,
// 当前的scrollTop
curScrollTop: 0,
// 总行数
totalLen: 0,
};
function reducer(state, action) {
const { curScrollTop, totalLen, ifScrollTopClear, scrollTop } = action;
let stateScrollTop = state.curScrollTop;
switch (action.type) {
// 改变trs 即 改变渲染的列表trs
case 'changeTrs':
return {
...state,
curScrollTop,
};
// 更改totalLen
case 'changeTotalLen':
if (totalLen === 0) {
stateScrollTop = 0;
}
return {
...state,
totalLen,
curScrollTop: stateScrollTop,
};
case 'reset':
return {
...state,
curScrollTop: ifScrollTopClear ? 0 : scrollTop ?? state.curScrollTop,
};
default:
throw new Error();
}
}
// ==============全局常量 ================== //
const DEFAULT_VID = 'vtable';
const vidMap = new Map();
let preData = 0;
// ===============context ============== //
const ScrollContext = createContext({
dispatch: undefined,
renderLen: 1,
start: 0,
offsetStart: 0,
// =============
rowHeight: initialState.rowHeight,
totalLen: 0,
vid: DEFAULT_VID,
});
// =============组件 =================== //
function VCell(props: any): JSX.Element {
const { children, ...restProps } = props;
return (
<td {...restProps}>
<div>{children[1]}</div>
</td>
);
}
function VRow(props: any, ref: any): JSX.Element {
const { rowHeight } = useContext(ScrollContext);
const { children, style, ...restProps } = props;
const trRef = useRef<HTMLTableRowElement>(null);
return (
<tr
{...restProps}
ref={Object.prototype.hasOwnProperty.call(ref, 'current') ? ref : trRef}
style={{
...style,
height: rowHeight || 'auto',
boxSizing: 'border-box',
}}
>
{children}
</tr>
);
}
function VWrapper(props: any): JSX.Element {
const { children, ...restProps } = props;
const { renderLen, start, dispatch, vid, totalLen } =
useContext(ScrollContext);
const contents = useMemo(() => {
return children[1];
}, [children]);
const contentsLen = useMemo(() => {
return contents?.length ?? 0;
}, [contents]);
useEffect(() => {
if (totalLen !== contentsLen) {
dispatch({
type: 'changeTotalLen',
totalLen: contentsLen ?? 0,
});
}
}, [contentsLen, dispatch, vid, totalLen]);
let tempNode = null;
if (Array.isArray(contents) && contents.length) {
tempNode = [
children[0],
contents.slice(start, start + (renderLen ?? 1)).map((item) => {
if (Array.isArray(item)) {
// 兼容antd v4.3.5 --- rc-table 7.8.1及以下
return item[0];
}
// 处理antd ^v4.4.0 --- rc-table ^7.8.2
return item;
}),
];
} else {
tempNode = children;
}
return <tbody {...restProps}>{tempNode}</tbody>;
}
function VTable(props: any, otherParams): JSX.Element {
const { style, children, ...rest } = props;
const { width, ...rest_style } = style;
const { vid, scrollY, resetScrollTopWhenDataChange, rowHeight, scrollTop } =
otherParams ?? {};
const [state, dispatch] = useReducer(reducer, {
...initialState,
curScrollTop: scrollTop,
rowHeight,
});
const wrap_tableRef = useRef<HTMLDivElement>(null);
const tableRef = useRef<HTMLTableElement>(null);
const ifChangeRef = useRef(false);
// 数据的总条数
const [totalLen, setTotalLen] = useState<number>(
children[1]?.props?.data?.length ?? 0,
);
useEffect(() => {
setTotalLen(state.totalLen);
}, [state.totalLen]);
// 组件卸载的清除操作
useEffect(() => {
return () => {
vidMap.delete(vid);
};
}, [vid]);
// 数据变更
useEffect(() => {
ifChangeRef.current = true;
// console.log('数据变更')
if (isNumber(children[1]?.props?.data?.length)) {
dispatch({
type: 'changeTotalLen',
totalLen: children[1]?.props?.data?.length ?? 0,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [children[1].props.data]);
// table总高度
const tableHeight = useMemo<string | number>(() => {
let temp: string | number = 'auto';
if (rowHeight && totalLen) {
temp = rowHeight * totalLen;
}
return temp;
}, [totalLen]);
// table的scrollY值
const [tableScrollY, setTableScrollY] = useState(0);
// tableScrollY 随scrollY / tableHeight 进行变更
useEffect(() => {
let temp = 0;
if (typeof scrollY === 'string') {
temp =
(wrap_tableRef.current?.parentNode as HTMLElement)?.offsetHeight ?? 0;
} else {
temp = scrollY;
}
// if (isNumber(tableHeight) && tableHeight < temp) {
// temp = tableHeight;
// }
// 处理tableScrollY <= 0的情况
if (temp <= 0) {
temp = 0;
}
setTableScrollY(temp);
}, [scrollY, tableHeight]);
// 渲染的条数
const renderLen = useMemo<number>(() => {
let temp = 1;
if (rowHeight && totalLen && tableScrollY) {
if (tableScrollY <= 0) {
temp = 0;
} else {
const tempRenderLen = ((tableScrollY / rowHeight) | 0) + 10;
// console.log('tempRenderLen', tempRenderLen)
// temp = tempRenderLen > totalLen ? totalLen : tempRenderLen;
temp = tempRenderLen;
}
}
return temp;
}, [totalLen, tableScrollY]);
// 渲染中的第一条
let start = rowHeight ? (state.curScrollTop / rowHeight) | 0 : 0;
start = start < 5 ? 0 : start - 5 + 1;
// 偏移量
let offsetStart = state.curScrollTop % (rowHeight * 5);
if (start > 0) {
offsetStart = offsetStart % rowHeight;
}
// console.log(offsetStart)
// offsetStart= offsetStart%rowHeight
// 用来优化向上滚动出现的空白
if (state.curScrollTop && state.curScrollTop >= rowHeight * 5) {
// start -= 1
// if (offsetStart >= rowHeight) {
// offsetStart +=
// } else {
offsetStart += rowHeight * 4;
// }
} else {
start = 0;
}
// console.log(state.curScrollTop, start, offsetStart)
// 数据变更 操作scrollTop
useEffect(() => {
const scrollNode = wrap_tableRef.current?.parentNode as HTMLElement;
if (ifChangeRef?.current) {
// console.log(scrollNode)
ifChangeRef.current = false;
if (resetScrollTopWhenDataChange) {
// 重置scrollTop
if (scrollNode) {
scrollNode.scrollTop = 0;
}
dispatch({ type: 'reset', ifScrollTopClear: true });
} else {
// console.log(preData)
// scrollNode.scrollTop = preData+53
// 不重置scrollTop 不清空curScrollTop
dispatch({ type: 'reset', ifScrollTopClear: false });
}
}
if (vidMap.has(vid)) {
vidMap.set(vid, {
...vidMap.get(vid),
scrollNode,
});
}
}, [totalLen, resetScrollTopWhenDataChange, vid, children]);
useEffect(() => {
const throttleScroll = throttle((e) => {
const scrollTop: number = e?.target?.scrollTop ?? 0;
// const scrollHeight: number = e?.target?.scrollHeight ?? 0
// const clientHeight: number = e?.target?.clientHeight ?? 0
if (scrollTop) {
preData = scrollTop;
}
// 到底了 没有滚动条就不会触发reachEnd. 建议设置scrolly高度少点或者数据量多点.
// 若renderLen大于totalLen, 置空curScrollTop. => table paddingTop会置空.
dispatch({
type: 'changeTrs',
curScrollTop: renderLen <= totalLen ? scrollTop : 0,
});
}, 60);
const ref = wrap_tableRef?.current?.parentNode as HTMLElement;
if (ref) {
ref.addEventListener('scroll', throttleScroll, { passive: true });
}
return () => {
ref.removeEventListener('scroll', throttleScroll);
};
}, [renderLen, totalLen]);
return (
<div
className="virtuallist"
ref={wrap_tableRef}
style={{
width: '100%',
position: 'relative',
height: tableHeight,
boxSizing: 'border-box',
paddingTop: state.curScrollTop,
}}
>
<ScrollContext.Provider
value={{
dispatch,
start,
offsetStart,
renderLen,
totalLen,
vid,
rowHeight,
}}
>
<table
{...rest}
ref={tableRef}
style={{
...rest_style,
width,
position: 'relative',
transform: `translateY(-${offsetStart}px)`,
}}
>
{children}
</table>
</ScrollContext.Provider>
</div>
);
}
// ================导出===================
export function VList(props: {
height: number;
// 唯一标识
vid?: string;
rowHeight: number | string;
reset?: boolean;
scrollTop?: number | string;
}): any {
const { vid = DEFAULT_VID, rowHeight, height, reset, scrollTop } = props;
const resetScrollTopWhenDataChange = reset ?? true;
if (!vidMap.has(vid)) {
vidMap.set(vid, { _id: vid });
}
return {
table: (p) =>
VTable(p, {
vid,
scrollY: height,
rowHeight,
resetScrollTopWhenDataChange,
scrollTop,
}),
body: {
wrapper: VWrapper,
row: VRow,
cell: VCell,
},
};
}
+19
View File
@@ -0,0 +1,19 @@
import { MutableRefObject, useLayoutEffect, useState } from 'react';
import useResizeObserver from '@react-hook/resize-observer';
import { getTableScroll } from '@/utils';
export default <T extends HTMLElement>(
target: MutableRefObject<T>,
extraHeight?: number,
) => {
const [height, setHeight] = useState<number>(0);
useResizeObserver(target, (entry) => {
let _target = entry.target as any;
if (!_target.classList.contains('ant-table-wrapper')) {
_target = entry.target.querySelector('.ant-table-wrapper');
}
setHeight(getTableScroll({ extraHeight, target: _target as HTMLElement }));
});
return height;
};
+2 -1
View File
@@ -10,6 +10,7 @@ import {
ContainerOutlined, ContainerOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import IconFont from '@/components/iconfont'; import IconFont from '@/components/iconfont';
import { BasicLayoutProps } from '@ant-design/pro-layout';
export default { export default {
route: { route: {
@@ -93,4 +94,4 @@ export default {
contentWidth: 'Fixed', contentWidth: 'Fixed',
splitMenus: false, splitMenus: false,
siderWidth: 180, siderWidth: 180,
} as any; } as BasicLayoutProps;
+47 -3
View File
@@ -9,9 +9,14 @@
url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype'); url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype');
} }
#root {
height: 100vh;
height: calc(100vh - var(--vh-offset, 0px));
}
.ant-modal-body { .ant-modal-body {
max-height: calc(85vh - 110px); max-height: calc(80vh - 110px);
max-height: calc(85vh - var(--vh-offset, 110px)); max-height: calc(80vh - var(--vh-offset, 110px));
overflow-y: auto; overflow-y: auto;
} }
@@ -253,10 +258,11 @@ textarea:-webkit-autofill:focus,
select:-webkit-autofill, select:-webkit-autofill,
select:-webkit-autofill:hover, select:-webkit-autofill:hover,
select:-webkit-autofill:focus { select:-webkit-autofill:focus {
border: none;
box-shadow: none; box-shadow: none;
transition: background-color 5000s ease-in-out 0s; transition: background-color 5000s ease-in-out 0s;
-webkit-text-fill-color: @text-color; -webkit-text-fill-color: @text-color;
caret-color: @text-color;
color: @text-color;
} }
::placeholder { ::placeholder {
@@ -342,3 +348,41 @@ select:-webkit-autofill:focus {
width: auto !important; width: auto !important;
} }
} }
pre {
word-break: break-all !important;
white-space: break-spaces !important;
padding: 0 !important;
}
.virtuallist {
.ant-table-tbody > tr > td > div {
white-space: unset !important;
}
}
.virtuallist .ant-table-tbody > tr > td > div {
box-sizing: border-box;
white-space: nowrap;
vertical-align: middle;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.virtuallist .ant-table-tbody > tr > td.ant-table-row-expand-icon-cell > div {
overflow: inherit;
}
.ant-table-bordered .virtuallist > table > .ant-table-tbody > tr > td {
border-right: 1px solid #f0f0f0;
}
.ant-table-column-title {
flex: unset;
}
.ant-table-column-sorters,
.ant-table-filter-column {
justify-content: unset;
}
+61 -31
View File
@@ -13,9 +13,20 @@ import config from '@/utils/config';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import './index.less'; import './index.less';
import vhCheck from 'vh-check'; import vhCheck from 'vh-check';
import { version, changeLogLink, changeLog } from '../version';
import { useCtx, useTheme } from '@/utils/hooks'; import { useCtx, useTheme } from '@/utils/hooks';
import { message, Badge, Modal, Avatar, Dropdown, Menu, Image } from 'antd'; import {
message,
Badge,
Modal,
Avatar,
Dropdown,
Menu,
Image,
Popover,
Descriptions,
Tooltip,
MenuProps,
} from 'antd';
// @ts-ignore // @ts-ignore
import SockJS from 'sockjs-client'; import SockJS from 'sockjs-client';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
@@ -32,6 +43,17 @@ export interface SharedContext {
reloadUser: (needLoading?: boolean) => void; reloadUser: (needLoading?: boolean) => void;
reloadTheme: () => void; reloadTheme: () => void;
socketMessage: any; socketMessage: any;
systemInfo: TSystemInfo;
}
interface TSystemInfo {
branch: 'develop' | 'master';
isInitialized: boolean;
lastCommitId: string;
lastCommitTime: number;
version: string;
changeLog: string;
changeLogLink: string;
} }
export default function () { export default function () {
@@ -40,7 +62,7 @@ export default function () {
const { theme, reloadTheme } = useTheme(); const { theme, reloadTheme } = useTheme();
const [user, setUser] = useState<any>({}); const [user, setUser] = useState<any>({});
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [systemInfo, setSystemInfo] = useState<{ isInitialized: boolean }>(); const [systemInfo, setSystemInfo] = useState<TSystemInfo>();
const ws = useRef<any>(null); const ws = useRef<any>(null);
const [socketMessage, setSocketMessage] = useState<any>(); const [socketMessage, setSocketMessage] = useState<any>();
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
@@ -69,6 +91,7 @@ export default function () {
if (!data.isInitialized) { if (!data.isInitialized) {
history.push('/initialization'); history.push('/initialization');
} else { } else {
init(data.version);
getUser(); getUser();
} }
} }
@@ -123,7 +146,6 @@ export default function () {
useEffect(() => { useEffect(() => {
vhCheck(); vhCheck();
init();
const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto'; const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
if (typeof window === 'undefined') return; if (typeof window === 'undefined') return;
@@ -201,9 +223,6 @@ export default function () {
} }
if (['/login', '/initialization', '/error'].includes(location.pathname)) { if (['/login', '/initialization', '/error'].includes(location.pathname)) {
document.title = `${
(config.documentTitleMap as any)[location.pathname]
} - 控制面板`;
if (systemInfo?.isInitialized && location.pathname === '/initialization') { if (systemInfo?.isInitialized && location.pathname === '/initialization') {
history.push('/crontab'); history.push('/crontab');
} }
@@ -230,13 +249,17 @@ export default function () {
!navigator.userAgent.includes('Chrome'); !navigator.userAgent.includes('Chrome');
const isQQBrowser = navigator.userAgent.includes('QQBrowser'); const isQQBrowser = navigator.userAgent.includes('QQBrowser');
const menu = ( const menu: MenuProps = {
<Menu items: [
className="side-menu-user-drop-menu" {
items={[{ label: '退出登录', key: 'logout', icon: <LogoutOutlined /> }]} label: '退出登录',
onClick={logout} className: 'side-menu-user-drop-menu',
/> onClick: logout,
); key: 'logout',
icon: <LogoutOutlined />,
},
],
};
return loading ? ( return loading ? (
<PageLoading /> <PageLoading />
) : ( ) : (
@@ -245,28 +268,35 @@ export default function () {
loading={loading} loading={loading}
ErrorBoundary={Sentry.ErrorBoundary} ErrorBoundary={Sentry.ErrorBoundary}
logo={<Image preview={false} src="https://qn.whyour.cn/logo.png" />} logo={<Image preview={false} src="https://qn.whyour.cn/logo.png" />}
// @ts-ignore
title={ title={
<> <>
<span style={{ fontSize: 16 }}></span> <span style={{ fontSize: 16 }}></span>
<a <a
href={changeLogLink} href={systemInfo?.changeLogLink}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
> >
<span <Tooltip
style={{ title={systemInfo?.branch === 'develop' ? '开发版' : '正式版'}
fontSize: isFirefox ? 9 : 12,
color: '#666',
marginLeft: 2,
zoom: isSafari ? 0.66 : 0.8,
letterSpacing: isQQBrowser ? -2 : 0,
}}
> >
v{version} <Badge size="small" dot={systemInfo?.branch === 'develop'}>
</span> <span
style={{
fontSize: isFirefox ? 9 : 12,
color: '#666',
marginLeft: 2,
zoom: isSafari ? 0.66 : 0.8,
letterSpacing: isQQBrowser ? -2 : 0,
}}
>
v{systemInfo?.version}
</span>
</Badge>
</Tooltip>
</a> </a>
</> </>
} }
@@ -281,16 +311,15 @@ export default function () {
return <Link to={menuItemProps.path}>{defaultDom}</Link>; return <Link to={menuItemProps.path}>{defaultDom}</Link>;
}} }}
pageTitleRender={(props, pageName, info) => { pageTitleRender={(props, pageName, info) => {
if (info && typeof info.pageName === 'string') { const title =
return `${info.pageName} - 控制面板`; (config.documentTitleMap as any)[location.pathname] || '未找到';
} return `${title} - 控制面板`;
return '控制面板';
}} }}
onCollapse={setCollapsed} onCollapse={setCollapsed}
collapsed={collapsed} collapsed={collapsed}
rightContentRender={() => rightContentRender={() =>
ctx.isPhone && ( ctx.isPhone && (
<Dropdown overlay={menu} placement="bottomRight" trigger={['click']}> <Dropdown menu={menu} placement="bottomRight" trigger={['click']}>
<span className="side-menu-user-wrapper"> <span className="side-menu-user-wrapper">
<Avatar <Avatar
shape="square" shape="square"
@@ -312,7 +341,7 @@ export default function () {
}} }}
> >
{!collapsed && !ctx.isPhone && ( {!collapsed && !ctx.isPhone && (
<Dropdown overlay={menu} placement="topLeft" trigger={['hover']}> <Dropdown menu={menu} placement="topLeft" trigger={['hover']}>
<span className="side-menu-user-wrapper"> <span className="side-menu-user-wrapper">
<Avatar <Avatar
shape="square" shape="square"
@@ -342,6 +371,7 @@ export default function () {
reloadUser, reloadUser,
reloadTheme, reloadTheme,
socketMessage, socketMessage,
systemInfo,
}} }}
/> />
</ProLayout> </ProLayout>
+18
View File
@@ -0,0 +1,18 @@
import React from 'react';
import { Button, Result, Typography } from 'antd';
const { Link } = Typography;
const NotFound: React.FC = () => (
<Result
status="404"
title="404"
extra={
<Button type="primary">
<Link href="/"></Link>
</Button>
}
/>
);
export default NotFound;
+5 -16
View File
@@ -29,6 +29,7 @@ import config from '@/utils/config';
import CronLogModal from './logModal'; import CronLogModal from './logModal';
import Editor from '@monaco-editor/react'; import Editor from '@monaco-editor/react';
import IconFont from '@/components/iconfont'; import IconFont from '@/components/iconfont';
import { getCommandScript } from '@/utils';
const { Text } = Typography; const { Text } = Typography;
@@ -147,22 +148,10 @@ const CronDetailModal = ({
}; };
const getScript = () => { const getScript = () => {
const cmd = cron.command.split(' ') as string[]; const result = getCommandScript(cron.command);
if (cmd[0] === 'task') { if (Array.isArray(result)) {
setValidTabs(validTabs); setValidTabs(validTabs);
if (cmd[1].startsWith('/ql/data/scripts')) { const [s, p] = result;
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
}
let p: string, s: string;
let index = cmd[1].lastIndexOf('/');
if (index >= 0) {
s = cmd[1].slice(index + 1);
p = cmd[1].slice(0, index);
} else {
s = cmd[1];
p = '';
}
setScriptInfo({ parent: p, filename: s }); setScriptInfo({ parent: p, filename: s });
request request
.get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`) .get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`)
@@ -171,7 +160,7 @@ const CronDetailModal = ({
setValue(data); setValue(data);
} }
}); });
} else { } else if (result) {
setValidTabs([validTabs[0]]); setValidTabs([validTabs[0]]);
} }
}; };
+20 -5
View File
@@ -4,13 +4,10 @@
.crontab-detail { .crontab-detail {
.card-wrapper { .card-wrapper {
height: 80vh;
height: calc(80vh - var(--vh-offset, 0px));
.ant-card:last-child { .ant-card:last-child {
.ant-card-body { .ant-card-body {
height: calc(80vh - 238px); height: calc(80vh - 367px);
height: calc(80vh - var(--vh-offset, 0px) - 238px); height: calc(80vh - var(--vh-offset, 0px) - 367px);
overflow-y: auto; overflow-y: auto;
} }
} }
@@ -176,3 +173,21 @@ tr.drop-over-upward td {
padding-top: 10px; padding-top: 10px;
} }
} }
.view-filters-container.active {
.filter-item > div > .ant-form-item-control {
margin-left: 40px;
}
}
.log-modal {
.log-container {
overflow-y: auto;
min-height: 300px;
max-height: calc(80vh - 110px);
max-height: calc(80vh - var(--vh-offset, 110px));
padding: 24px;
margin: -24px;
}
}
+205 -237
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef, useMemo } from 'react';
import { import {
Button, Button,
message, message,
@@ -14,6 +14,7 @@ import {
Popover, Popover,
Tabs, Tabs,
TablePaginationConfig, TablePaginationConfig,
MenuProps,
} from 'antd'; } from 'antd';
import { import {
ClockCircleOutlined, ClockCircleOutlined,
@@ -43,14 +44,16 @@ import CronLogModal from './logModal';
import CronDetailModal from './detail'; import CronDetailModal from './detail';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import { diffTime } from '@/utils/date'; import { diffTime } from '@/utils/date';
import { getTableScroll } from '@/utils/index';
import { history, useOutletContext } from '@umijs/max'; import { history, useOutletContext } from '@umijs/max';
import './index.less'; import './index.less';
import ViewCreateModal from './viewCreateModal'; import ViewCreateModal from './viewCreateModal';
import ViewManageModal from './viewManageModal'; import ViewManageModal from './viewManageModal';
import pagination from 'antd/lib/pagination';
import { FilterValue, SorterResult } from 'antd/lib/table/interface'; import { FilterValue, SorterResult } from 'antd/lib/table/interface';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import { getCommandScript, parseCrontab } from '@/utils';
import { ColumnProps } from 'antd/lib/table';
import { VList } from '../../components/vlist';
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -82,15 +85,28 @@ enum OperationPath {
'unpin', 'unpin',
} }
export interface ICrontab {
name: string;
command: string;
schedule: string;
id: number;
status: number;
isDisabled?: 1 | 0;
isPinned?: 1 | 0;
labels?: string[];
last_running_time?: number;
last_execution_time?: number;
nextRunTime: Date;
}
const Crontab = () => { const Crontab = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>(); const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const columns: any = [ const columns: ColumnProps<ICrontab>[] = [
{ {
title: '名称', title: '名称',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
width: 150, width: 150,
align: 'center' as const,
render: (text: string, record: any) => ( render: (text: string, record: any) => (
<> <>
<a <a
@@ -138,22 +154,20 @@ const Crontab = () => {
</> </>
), ),
sorter: { sorter: {
compare: (a: any, b: any) => a?.name?.localeCompare(b?.name), compare: (a, b) => a?.name?.localeCompare(b?.name),
}, },
}, },
{ {
title: '命令', title: '命令/脚本',
dataIndex: 'command', dataIndex: 'command',
key: 'command', key: 'command',
width: 300, width: 300,
align: 'center' as const, render: (text, record) => {
render: (text: string, record: any) => {
return ( return (
<Paragraph <Paragraph
style={{ style={{
wordBreak: 'break-all', wordBreak: 'break-all',
marginBottom: 0, marginBottom: 0,
textAlign: 'left',
}} }}
ellipsis={{ tooltip: text, rows: 2 }} ellipsis={{ tooltip: text, rows: 2 }}
> >
@@ -176,23 +190,21 @@ const Crontab = () => {
dataIndex: 'schedule', dataIndex: 'schedule',
key: 'schedule', key: 'schedule',
width: 110, width: 110,
align: 'center' as const,
sorter: { sorter: {
compare: (a: any, b: any) => a.schedule.localeCompare(b.schedule), compare: (a, b) => a.schedule.localeCompare(b.schedule),
}, },
}, },
{ {
title: '最后运行时间', title: '最后运行时间',
align: 'center' as const,
dataIndex: 'last_execution_time', dataIndex: 'last_execution_time',
key: 'last_execution_time', key: 'last_execution_time',
width: 150, width: 150,
sorter: { sorter: {
compare: (a: any, b: any) => { compare: (a, b) => {
return a.last_execution_time - b.last_execution_time; return (a.last_execution_time || 0) - (b.last_execution_time || 0);
}, },
}, },
render: (text: string, record: any) => { render: (text, record) => {
const language = navigator.language || navigator.languages[0]; const language = navigator.language || navigator.languages[0];
return ( return (
<span <span
@@ -213,7 +225,6 @@ const Crontab = () => {
}, },
{ {
title: '最后运行时长', title: '最后运行时长',
align: 'center' as const,
width: 120, width: 120,
dataIndex: 'last_running_time', dataIndex: 'last_running_time',
key: 'last_running_time', key: 'last_running_time',
@@ -222,7 +233,7 @@ const Crontab = () => {
return a.last_running_time - b.last_running_time; return a.last_running_time - b.last_running_time;
}, },
}, },
render: (text: string, record: any) => { render: (text, record) => {
return record.last_running_time return record.last_running_time
? diffTime(record.last_running_time) ? diffTime(record.last_running_time)
: '-'; : '-';
@@ -230,14 +241,13 @@ const Crontab = () => {
}, },
{ {
title: '下次运行时间', title: '下次运行时间',
align: 'center' as const,
width: 150, width: 150,
sorter: { sorter: {
compare: (a: any, b: any) => { compare: (a: any, b: any) => {
return a.nextRunTime - b.nextRunTime; return a.nextRunTime - b.nextRunTime;
}, },
}, },
render: (text: string, record: any) => { render: (text, record) => {
const language = navigator.language || navigator.languages[0]; const language = navigator.language || navigator.languages[0];
return record.nextRunTime return record.nextRunTime
.toLocaleString(language, { .toLocaleString(language, {
@@ -246,13 +256,11 @@ const Crontab = () => {
.replace(' 24:', ' 00:'); .replace(' 24:', ' 00:');
}, },
}, },
{ {
title: '状态', title: '状态',
key: 'status', key: 'status',
dataIndex: 'status', dataIndex: 'status',
align: 'center' as const, width: 88,
width: 85,
filters: [ filters: [
{ {
text: '运行中', text: '运行中',
@@ -271,14 +279,14 @@ const Crontab = () => {
value: 3, value: 3,
}, },
], ],
onFilter: (value: number, record: any) => { onFilter: (value, record) => {
if (record.isDisabled && record.status !== 0) { if (record.isDisabled && record.status !== 0) {
return value === 2; return value === 2;
} else { } else {
return record.status === value; return record.status === value;
} }
}, },
render: (text: string, record: any) => ( render: (text, record) => (
<> <>
{(!record.isDisabled || record.status !== CrontabStatus.idle) && ( {(!record.isDisabled || record.status !== CrontabStatus.idle) && (
<> <>
@@ -313,9 +321,8 @@ const Crontab = () => {
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
align: 'center' as const,
width: 100, width: 100,
render: (text: string, record: any, index: number) => { render: (text, record, index) => {
const isPc = !isPhone; const isPc = !isPhone;
return ( return (
<Space size="middle"> <Space size="middle">
@@ -323,6 +330,7 @@ const Crontab = () => {
<Tooltip title={isPc ? '运行' : ''}> <Tooltip title={isPc ? '运行' : ''}>
<a <a
onClick={(e) => { onClick={(e) => {
setReset(false);
e.stopPropagation(); e.stopPropagation();
runCron(record, index); runCron(record, index);
}} }}
@@ -335,6 +343,7 @@ const Crontab = () => {
<Tooltip title={isPc ? '停止' : ''}> <Tooltip title={isPc ? '停止' : ''}>
<a <a
onClick={(e) => { onClick={(e) => {
setReset(false);
e.stopPropagation(); e.stopPropagation();
stopCron(record, index); stopCron(record, index);
}} }}
@@ -346,6 +355,7 @@ const Crontab = () => {
<Tooltip title={isPc ? '日志' : ''}> <Tooltip title={isPc ? '日志' : ''}>
<a <a
onClick={(e) => { onClick={(e) => {
setReset(false);
e.stopPropagation(); e.stopPropagation();
setLogCron({ ...record, timestamp: Date.now() }); setLogCron({ ...record, timestamp: Date.now() });
}} }}
@@ -376,7 +386,6 @@ const Crontab = () => {
filters: any; filters: any;
}>({} as any); }>({} as any);
const [viewConf, setViewConf] = useState<any>(); const [viewConf, setViewConf] = useState<any>();
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false); const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
const [detailCron, setDetailCron] = useState<any>(); const [detailCron, setDetailCron] = useState<any>();
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
@@ -389,26 +398,19 @@ const Crontab = () => {
const [enabledCronViews, setEnabledCronViews] = useState<any[]>([]); const [enabledCronViews, setEnabledCronViews] = useState<any[]>([]);
const [moreMenuActive, setMoreMenuActive] = useState(false); const [moreMenuActive, setMoreMenuActive] = useState(false);
const tableRef = useRef<any>(); const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef);
const resetRef = useRef<boolean>(true);
const setReset = (v) => {
resetRef.current = v;
};
const goToScriptManager = (record: any) => { const goToScriptManager = (record: any) => {
const cmd = record.command.split(' ') as string[]; const result = getCommandScript(record.command);
if (cmd[0] === 'task') { if (Array.isArray(result)) {
if (cmd[1].startsWith('/ql/data/scripts')) { const [s, p] = result;
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
}
let p: string, s: string;
let index = cmd[1].lastIndexOf('/');
if (index >= 0) {
s = cmd[1].slice(index + 1);
p = cmd[1].slice(0, index);
} else {
s = cmd[1];
p = '';
}
history.push(`/script?p=${p}&s=${s}`); history.push(`/script?p=${p}&s=${s}`);
} else if (cmd[1] === 'repo') { } else if (result) {
location.href = cmd[2]; location.href = result;
} }
}; };
@@ -420,9 +422,9 @@ const Crontab = () => {
}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify( }crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(
filters, filters,
)}`; )}`;
if (sorter && sorter.field) { if (sorter && sorter.column && sorter.order) {
url += `&sorter=${JSON.stringify({ url += `&sorter=${JSON.stringify({
field: sorter.field, field: sorter.column.key,
type: sorter.order === 'ascend' ? 'ASC' : 'DESC', type: sorter.order === 'ascend' ? 'ASC' : 'DESC',
})}`; })}`;
} }
@@ -430,6 +432,7 @@ const Crontab = () => {
url += `&queryString=${JSON.stringify({ url += `&queryString=${JSON.stringify({
filters: viewConf.filters, filters: viewConf.filters,
sorts: viewConf.sorts, sorts: viewConf.sorts,
filterRelation: viewConf.filterRelation || 'and',
})}`; })}`;
} }
request request
@@ -441,10 +444,7 @@ const Crontab = () => {
data.map((x) => { data.map((x) => {
return { return {
...x, ...x,
nextRunTime: cron_parser nextRunTime: parseCrontab(x.schedule),
.parseExpression(x.schedule)
.next()
.toDate(),
}; };
}), }),
); );
@@ -682,15 +682,14 @@ const Crontab = () => {
arrow={{ pointAtCenter: true }} arrow={{ pointAtCenter: true }}
placement="bottomRight" placement="bottomRight"
trigger={['click']} trigger={['click']}
overlay={ menu={{
<Menu items: getMenuItems(record),
items={getMenuItems(record)} onClick: ({ key, domEvent }) => {
onClick={({ key, domEvent }) => { domEvent.stopPropagation();
domEvent.stopPropagation(); setReset(false);
action(key, record, index); action(key, record, index);
}} },
/> }}
}
> >
<a onClick={(e) => e.stopPropagation()}> <a onClick={(e) => e.stopPropagation()}>
<EllipsisOutlined /> <EllipsisOutlined />
@@ -717,34 +716,16 @@ const Crontab = () => {
} }
}; };
const handleCancel = (cron?: any) => { const handleCancel = () => {
setIsModalVisible(false); setIsModalVisible(false);
if (cron) { getCrons();
handleCrons(cron);
}
}; };
const onSearch = (value: string) => { const onSearch = (value: string) => {
setReset(true);
setSearchText(value.trim()); setSearchText(value.trim());
}; };
const handleCrons = (cron: any) => {
const index = value.findIndex((x) => x.id === cron.id);
const result = [...value];
cron.nextRunTime = cron_parser
.parseExpression(cron.schedule)
.next()
.toDate();
if (index === -1) {
result.unshift(cron);
} else {
result.splice(index, 1, {
...cron,
});
}
setValue(result);
};
const getCronDetail = (cron: any) => { const getCronDetail = (cron: any) => {
request request
.get(`${config.apiPrefix}crons/${cron.id}`) .get(`${config.apiPrefix}crons/${cron.id}`)
@@ -752,10 +733,7 @@ const Crontab = () => {
if (code === 200) { if (code === 200) {
const index = value.findIndex((x) => x.id === cron.id); const index = value.findIndex((x) => x.id === cron.id);
const result = [...value]; const result = [...value];
data.nextRunTime = cron_parser data.nextRunTime = parseCrontab(data.schedule);
.parseExpression(data.schedule)
.next()
.toDate();
if (index !== -1) { if (index !== -1) {
result.splice(index, 1, { result.splice(index, 1, {
...cron, ...cron,
@@ -770,16 +748,14 @@ const Crontab = () => {
const onSelectChange = (selectedIds: any[]) => { const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds); setSelectedRowIds(selectedIds);
setTimeout(() => {
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
setTableScrollHeight(getTableScroll());
}
});
}; };
useEffect(() => {
setReset(false);
}, [selectedRowIds]);
const rowSelection = { const rowSelection = {
selectedRowIds, selectedRowKeys: selectedRowIds,
onChange: onSelectChange, onChange: onSelectChange,
}; };
@@ -805,6 +781,7 @@ const Crontab = () => {
}; };
const operateCrons = (operationStatus: number) => { const operateCrons = (operationStatus: number) => {
setReset(false);
Modal.confirm({ Modal.confirm({
title: `确认${OperationName[operationStatus]}`, title: `确认${OperationName[operationStatus]}`,
content: <>{OperationName[operationStatus]}</>, content: <>{OperationName[operationStatus]}</>,
@@ -831,6 +808,7 @@ const Crontab = () => {
sorter: SorterResult<any> | SorterResult<any>[], sorter: SorterResult<any> | SorterResult<any>[],
) => { ) => {
const { current, pageSize } = pagination; const { current, pageSize } = pagination;
setReset(true);
setPageConf({ setPageConf({
page: current as number, page: current as number,
size: pageSize as number, size: pageSize as number,
@@ -863,7 +841,7 @@ const Crontab = () => {
useEffect(() => { useEffect(() => {
if (viewConf && enabledCronViews && enabledCronViews.length > 0) { if (viewConf && enabledCronViews && enabledCronViews.length > 0) {
const view = enabledCronViews.slice(2).find((x) => x.id === viewConf.id); const view = enabledCronViews.slice(4).find((x) => x.id === viewConf.id);
setMoreMenuActive(!!view); setMoreMenuActive(!!view);
} }
}, [viewConf, enabledCronViews]); }, [viewConf, enabledCronViews]);
@@ -878,97 +856,6 @@ const Crontab = () => {
getCronViews(); getCronViews();
}, []); }, []);
useEffect(() => {
if (tableRef.current) {
setTableScrollHeight(getTableScroll());
}
}, []);
const panelContent = (
<>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button type="primary" style={{ marginBottom: 5 }} onClick={delCrons}>
</Button>
<Button
type="primary"
onClick={() => operateCrons(0)}
style={{ marginLeft: 8, marginBottom: 5 }}
>
</Button>
<Button
type="primary"
onClick={() => operateCrons(1)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={() => operateCrons(2)}
>
</Button>
<Button type="primary" onClick={() => operateCrons(3)}>
</Button>
<Button
type="primary"
onClick={() => operateCrons(4)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => operateCrons(5)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => setIsLabelModalVisible(true)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span>
</div>
)}
<Table
ref={tableRef}
columns={columns}
pagination={{
current: pageConf.page,
pageSize: pageConf.size,
showSizeChanger: true,
simple: isPhone,
total,
showTotal: (total: number, range: number[]) =>
`${range[0]}-${range[1]} 条/总共 ${total}`,
pageSizeOptions: [10, 20, 50, 100, 200, 500, total || 10000].sort(
(a, b) => a - b,
),
}}
dataSource={value}
rowKey="id"
size="middle"
scroll={{ x: 1000, y: tableScrollHeight }}
loading={loading}
rowSelection={rowSelection}
rowClassName={getRowClassName}
onChange={onPageChange}
/>
</>
);
const viewAction = (key: string) => { const viewAction = (key: string) => {
switch (key) { switch (key) {
case 'new': case 'new':
@@ -984,41 +871,39 @@ const Crontab = () => {
} }
}; };
const menu = ( const menu: MenuProps = {
<Menu onClick: ({ key, domEvent }) => {
onClick={({ key, domEvent }) => { domEvent.stopPropagation();
domEvent.stopPropagation(); viewAction(key);
viewAction(key); },
}} items: [
items={[ ...[...enabledCronViews].slice(4).map((x) => ({
...[...enabledCronViews].slice(2).map((x) => ({ label: (
label: ( <Space style={{ display: 'flex', justifyContent: 'space-between' }}>
<Space style={{ display: 'flex', justifyContent: 'space-between' }}> <span>{x.name}</span>
<span>{x.name}</span> {viewConf?.id === x.id && (
{viewConf?.id === x.id && ( <CheckOutlined style={{ color: '#1890ff' }} />
<CheckOutlined style={{ color: '#1890ff' }} /> )}
)} </Space>
</Space> ),
), key: x.id,
key: x.id, icon: <UnorderedListOutlined />,
icon: <UnorderedListOutlined />, })),
})), {
{ type: 'divider' as 'group',
type: 'divider', },
}, {
{ label: '新建视图',
label: '新建视图', key: 'new',
key: 'new', icon: <PlusOutlined />,
icon: <PlusOutlined />, },
}, {
{ label: '视图管理',
label: '视图管理', key: 'manage',
key: 'manage', icon: <SettingOutlined />,
icon: <SettingOutlined />, },
}, ],
]} };
/>
);
const getCronViews = () => { const getCronViews = () => {
setLoading(true); setLoading(true);
@@ -1038,10 +923,22 @@ const Crontab = () => {
const tabClick = (key: string) => { const tabClick = (key: string) => {
const view = enabledCronViews.find((x) => x.id == key); const view = enabledCronViews.find((x) => x.id == key);
setSelectedRowIds([]); setSelectedRowIds([]);
setReset(true);
setPageConf({ ...pageConf, page: 1 }); setPageConf({ ...pageConf, page: 1 });
setViewConf(view ? view : null); setViewConf(view ? view : null);
}; };
const vComponents = useMemo(() => {
return VList({
height: tableScrollHeight,
reset: resetRef.current,
rowHeight: 69,
scrollTop: resetRef.current
? 0
: tableRef.current?.querySelector('.ant-table-body')?.scrollTop,
});
}, [tableScrollHeight, resetRef.current]);
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper crontab-wrapper ql-container-wrapper-has-tab" className="ql-container-wrapper crontab-wrapper ql-container-wrapper-has-tab"
@@ -1072,7 +969,7 @@ const Crontab = () => {
className={`crontab-view ${moreMenuActive ? 'more-active' : ''}`} className={`crontab-view ${moreMenuActive ? 'more-active' : ''}`}
tabBarExtraContent={ tabBarExtraContent={
<Dropdown <Dropdown
overlay={menu} menu={menu}
trigger={['click']} trigger={['click']}
overlayStyle={{ minWidth: 200 }} overlayStyle={{ minWidth: 200 }}
> >
@@ -1087,27 +984,98 @@ const Crontab = () => {
} }
onTabClick={tabClick} onTabClick={tabClick}
items={[ items={[
{ ...[...enabledCronViews].slice(0, 4).map((x) => ({
key: 'all',
label: '全部任务',
children: panelContent,
},
...[...enabledCronViews].slice(0, 2).map((x) => ({
key: x.id, key: x.id,
label: x.name, label: x.name,
children: panelContent,
})), })),
]} ]}
> />
<Tabs.TabPane tab="全部任务" key="all"> <div ref={tableRef}>
{panelContent} {selectedRowIds.length > 0 && (
</Tabs.TabPane> <div style={{ marginBottom: 16 }}>
{[...enabledCronViews].slice(0, 2).map((x) => ( <Button
<Tabs.TabPane tab={x.name} key={x.id}> type="primary"
{panelContent} style={{ marginBottom: 5 }}
</Tabs.TabPane> onClick={delCrons}
))} >
</Tabs>
</Button>
<Button
type="primary"
onClick={() => operateCrons(0)}
style={{ marginLeft: 8, marginBottom: 5 }}
>
</Button>
<Button
type="primary"
onClick={() => operateCrons(1)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={() => operateCrons(2)}
>
</Button>
<Button type="primary" onClick={() => operateCrons(3)}>
</Button>
<Button
type="primary"
onClick={() => operateCrons(4)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => operateCrons(5)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => setIsLabelModalVisible(true)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span>
</div>
)}
<Table
columns={columns}
pagination={{
current: pageConf.page,
pageSize: pageConf.size,
showSizeChanger: true,
simple: isPhone,
total,
showTotal: (total: number, range: number[]) =>
`${range[0]}-${range[1]} 条/总共 ${total}`,
pageSizeOptions: [10, 20, 50, 100, 200, 500, total || 10000].sort(
(a, b) => a - b,
),
}}
dataSource={value}
rowKey="id"
size="middle"
scroll={{ x: 1000, y: tableScrollHeight }}
loading={loading}
rowSelection={rowSelection}
rowClassName={getRowClassName}
onChange={onPageChange}
components={vComponents}
/>
</div>
<CronLogModal <CronLogModal
visible={isLogModalVisible} visible={isLogModalVisible}
handleCancel={() => { handleCancel={() => {
+54 -23
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { Modal, message, Input, Form, Statistic, Button } from 'antd'; import { Modal, message, Input, Form, Statistic, Button } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
@@ -34,7 +34,7 @@ const CronLogModal = ({
const [loading, setLoading] = useState<any>(true); const [loading, setLoading] = useState<any>(true);
const [executing, setExecuting] = useState<any>(true); const [executing, setExecuting] = useState<any>(true);
const [isPhone, setIsPhone] = useState(false); const [isPhone, setIsPhone] = useState(false);
const [theme, setTheme] = useState<string>(''); const scrollInfoRef = useRef({ value: 0, down: true });
const getCronLog = (isFirst?: boolean) => { const getCronLog = (isFirst?: boolean) => {
if (isFirst) { if (isFirst) {
@@ -45,14 +45,20 @@ const CronLogModal = ({
.then(({ code, data }) => { .then(({ code, data }) => {
if ( if (
code === 200 && code === 200 &&
localStorage.getItem('logCron') === String(cron.id) localStorage.getItem('logCron') === String(cron.id) &&
data !== value
) { ) {
const log = data as string; const log = data as string;
setValue(log || '暂无日志'); setValue(log || '暂无日志');
setExecuting( const hasNext = Boolean(
log && !logEnded(log) && !log.includes('重启面板'), log &&
!logEnded(log) &&
!log.includes('重启面板') &&
!log.includes('任务未运行'),
); );
if (log && !logEnded(log) && !log.includes('重启面板')) { setExecuting(hasNext);
autoScroll();
if (hasNext) {
setTimeout(() => { setTimeout(() => {
getCronLog(); getCronLog();
}, 2000); }, 2000);
@@ -89,11 +95,33 @@ const CronLogModal = ({
}); });
}; };
const autoScroll = () => {
if (!scrollInfoRef.current.down) {
return;
}
setTimeout(() => {
document
.querySelector('#log-flag')!
.scrollIntoView({ behavior: 'smooth' });
}, 1000);
};
const cancel = () => { const cancel = () => {
localStorage.removeItem('logCron'); localStorage.removeItem('logCron');
handleCancel(); handleCancel();
}; };
const handleScroll = (e) => {
const sTop = e.target.scrollTop;
if (scrollInfoRef.current.down) {
scrollInfoRef.current = {
value: sTop,
down: sTop > scrollInfoRef.current.value,
};
}
};
const titleElement = () => { const titleElement = () => {
return ( return (
<> <>
@@ -138,23 +166,26 @@ const CronLogModal = ({
</Button>, </Button>,
]} ]}
> >
{loading ? ( <div onScroll={handleScroll} className="log-container">
<PageLoading /> {loading ? (
) : ( <PageLoading />
<pre ) : (
style={ <pre
isPhone style={
? { isPhone
fontFamily: 'Source Code Pro', ? {
width: 375, fontFamily: 'Source Code Pro',
zoom: 0.83, width: 375,
} zoom: 0.83,
: {} }
} : {}
> }
{value} >
</pre> {value}
)} </pre>
)}
<div id="log-flag"></div>
</div>
</Modal> </Modal>
); );
}; };
+2 -2
View File
@@ -74,13 +74,13 @@ const CronModal = ({
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="command" name="command"
label="命令" label="命令/脚本"
rules={[{ required: true, whitespace: true }]} rules={[{ required: true, whitespace: true }]}
> >
<Input.TextArea <Input.TextArea
rows={4} rows={4}
autoSize={true} autoSize={true}
placeholder="请输入要执行命令" placeholder="支持输入脚本路径/任意系统可执行命令/task 脚本路径"
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
+172 -70
View File
@@ -12,19 +12,28 @@ import {
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import IconFont from '@/components/iconfont';
import get from 'lodash/get';
const PROPERTIES = [ const PROPERTIES = [
{ name: '命令', value: 'command' }, { name: '命令', value: 'command' },
{ name: '名称', value: 'name' }, { name: '名称', value: 'name' },
{ name: '定时规则', value: 'schedule' }, { name: '定时规则', value: 'schedule' },
{ name: '状态', value: 'status' }, { name: '状态', value: 'status' },
{ name: '标签', value: 'labels' },
]; ];
const EOperation: any = {
Reg: '',
NotReg: '',
In: 'select',
Nin: 'select',
};
const OPERATIONS = [ const OPERATIONS = [
{ name: '包含', value: 'Reg' }, { name: '包含', value: 'Reg' },
{ name: '不包含', value: 'NotReg' }, { name: '不包含', value: 'NotReg' },
{ name: '属于', value: 'In' }, { name: '属于', value: 'In', type: 'select' },
{ name: '不属于', value: 'Nin' }, { name: '不属于', value: 'Nin', type: 'select' },
// { name: '等于', value: 'Eq' }, // { name: '等于', value: 'Eq' },
// { name: '不等于', value: 'Ne' }, // { name: '不等于', value: 'Ne' },
// { name: '为空', value: 'IsNull' }, // { name: '为空', value: 'IsNull' },
@@ -36,11 +45,18 @@ const SORTTYPES = [
{ name: '倒序', value: 'DESC' }, { name: '倒序', value: 'DESC' },
]; ];
const STATUS = [ const STATUS_MAP = {
{ name: '运行中', value: 0 }, status: [
{ name: '空闲中', value: 1 }, { name: '运行中', value: 0 },
{ name: '已禁用', value: 2 }, { name: '空闲中', value: 1 },
]; { name: '已禁用', value: 2 },
],
};
enum ViewFilterRelation {
'and' = '且',
'or' = '或',
}
const ViewCreateModal = ({ const ViewCreateModal = ({
view, view,
@@ -53,10 +69,11 @@ const ViewCreateModal = ({
}) => { }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [operationMap, setOperationMap] = useState<any>(); const [filterRelation, setFilterRelation] = useState<'and' | 'or'>('and');
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
setLoading(true); setLoading(true);
values.filterRelation = filterRelation;
const method = view ? 'put' : 'post'; const method = view ? 'put' : 'post';
try { try {
const { code, data } = await request[method]( const { code, data } = await request[method](
@@ -87,12 +104,7 @@ const ViewCreateModal = ({
}, [view, visible]); }, [view, visible]);
const operationElement = ( const operationElement = (
<Select <Select style={{ width: 80 }}>
style={{ width: 100 }}
onChange={() => {
setOperationMap({});
}}
>
{OPERATIONS.map((x) => ( {OPERATIONS.map((x) => (
<Select.Option key={x.name} value={x.value}> <Select.Option key={x.name} value={x.value}>
{x.name} {x.name}
@@ -123,15 +135,17 @@ const ViewCreateModal = ({
</Select> </Select>
); );
const statusElement = ( const statusElement = (property: keyof typeof STATUS_MAP) => {
<Select mode="multiple" allowClear placeholder="请选择状态"> return (
{STATUS.map((x) => ( <Select mode="tags" allowClear placeholder="输入后回车增加自定义选项">
<Select.Option key={x.name} value={x.value}> {STATUS_MAP[property]?.map((x) => (
{x.name} <Select.Option key={x.name} value={x.value}>
</Select.Option> {x.name}
))} </Select.Option>
</Select> ))}
); </Select>
);
};
return ( return (
<Modal <Modal
@@ -164,57 +178,145 @@ const ViewCreateModal = ({
</Form.Item> </Form.Item>
<Form.List name="filters"> <Form.List name="filters">
{(fields, { add, remove }) => ( {(fields, { add, remove }) => (
<> <div
{fields.map(({ key, name, ...restField }, index) => ( style={{ position: 'relative' }}
<Form.Item className={`view-filters-container ${
label={index === 0 ? '筛选条件' : ''} fields.length > 1 ? 'active' : ''
key={key} }`}
style={{ marginBottom: 0 }} >
required {fields.length > 1 && (
<div
style={{
position: 'absolute',
width: 50,
borderRadius: 10,
border: '1px solid rgb(190, 220, 255)',
borderRight: 'none',
height: 56 * (fields.length - 1),
top: 46,
left: 15,
}}
> >
<Space className="view-create-modal-filters" align="baseline"> <Button
<Form.Item type="primary"
{...restField} size="small"
name={[name, 'property']} style={{
rules={[{ required: true }]} position: 'absolute',
top: '50%',
translate: '-50% -50%',
padding: '0 0 0 3px',
cursor: 'pointer',
}}
onClick={() => {
setFilterRelation(
filterRelation === 'and' ? 'or' : 'and',
);
}}
>
<>
<span>{ViewFilterRelation[filterRelation]}</span>
<IconFont type="ql-icon-d-caret" />
</>
</Button>
</div>
)}
<div>
{fields.map(({ key, name, ...restField }, index) => (
<Form.Item
label={index === 0 ? '筛选条件' : ''}
key={key}
style={{ marginBottom: 0 }}
required
className="filter-item"
>
<Space
className="view-create-modal-filters"
align="baseline"
style={
fields.length > 1 ? { width: 'calc(100% - 40px)' } : {}
}
> >
{propertyElement(PROPERTIES, { width: 120 })} <Form.Item
</Form.Item> {...restField}
<Form.Item name={[name, 'property']}
{...restField} rules={[{ required: true }]}
name={[name, 'operation']} >
rules={[{ required: true }]} {propertyElement(PROPERTIES, { width: 90 })}
> </Form.Item>
{operationElement} <Form.Item
</Form.Item> {...restField}
<Form.Item name={[name, 'operation']}
{...restField} rules={[{ required: true }]}
name={[name, 'value']} >
rules={[{ required: true, message: '请输入内容' }]} {operationElement}
> </Form.Item>
{['In', 'Nin'].includes( <Form.Item
form.getFieldValue(['filters', index, 'operation']), noStyle
) ? ( shouldUpdate={(prevValues, nextValues) => {
statusElement const preOperation =
) : ( EOperation[
<Input placeholder="请输入内容" /> get(prevValues, ['filters', name, 'operation'])
];
const nextOperation =
EOperation[
get(nextValues, ['filters', name, 'operation'])
];
const flag = preOperation !== nextOperation;
if (flag) {
form.setFieldValue(
['filters', name, 'value'],
nextOperation === 'select' ? [] : '',
);
}
return flag;
}}
>
{() => {
const property = form.getFieldValue([
'filters',
index,
'property',
]) as 'status';
const operate = form.getFieldValue([
'filters',
name,
'operation',
]);
return (
<Form.Item
{...restField}
name={[name, 'value']}
rules={[
{ required: true, message: '请输入内容' },
]}
>
{EOperation[operate] === 'select' ? (
statusElement(property)
) : (
<Input placeholder="请输入内容" />
)}
</Form.Item>
);
}}
</Form.Item>
{index !== 0 && (
<MinusCircleOutlined onClick={() => remove(name)} />
)} )}
</Form.Item> </Space>
{index !== 0 && ( </Form.Item>
<MinusCircleOutlined onClick={() => remove(name)} /> ))}
)} <Form.Item>
</Space> <a
onClick={() =>
add({ property: 'command', operation: 'Reg' })
}
>
<PlusOutlined />
</a>
</Form.Item> </Form.Item>
))} </div>
<Form.Item> </div>
<a
onClick={() => add({ property: 'command', operation: 'Reg' })}
>
<PlusOutlined />
</a>
</Form.Item>
</>
)} )}
</Form.List> </Form.List>
<Form.List name="sorts"> <Form.List name="sorts">
+10 -6
View File
@@ -80,13 +80,17 @@ const ViewManageModal = ({
title: '名称', title: '名称',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
align: 'center' as const, },
{
title: '类型',
dataIndex: 'type',
key: 'type',
render: (v) => (v === 1 ? '系统' : '个人'),
}, },
{ {
title: '显示', title: '显示',
key: 'isDisabled', key: 'isDisabled',
dataIndex: 'isDisabled', dataIndex: 'isDisabled',
align: 'center' as const,
width: 100, width: 100,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
return ( return (
@@ -100,10 +104,9 @@ const ViewManageModal = ({
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
width: 140, width: 100,
align: 'center' as const,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
return ( return record.type !== 1 ? (
<Space size="middle"> <Space size="middle">
<a onClick={() => editView(record, index)}> <a onClick={() => editView(record, index)}>
<EditOutlined /> <EditOutlined />
@@ -112,6 +115,8 @@ const ViewManageModal = ({
<DeleteOutlined /> <DeleteOutlined />
</a> </a>
</Space> </Space>
) : (
'-'
); );
}, },
}, },
@@ -155,7 +160,6 @@ const ViewManageModal = ({
}; };
const onShowChange = (checked: boolean, record: any, index: number) => { const onShowChange = (checked: boolean, record: any, index: number) => {
console.log(checked);
request request
.put(`${config.apiPrefix}crons/views/${checked ? 'enable' : 'disable'}`, { .put(`${config.apiPrefix}crons/views/${checked ? 'enable' : 'disable'}`, {
data: [record.id], data: [record.id],
+46 -71
View File
@@ -27,10 +27,10 @@ import DependenceModal from './modal';
import { DndProvider, useDrag, useDrop } from 'react-dnd'; import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less'; import './index.less';
import { getTableScroll } from '@/utils/index';
import DependenceLogModal from './logModal'; import DependenceLogModal from './logModal';
import { useOutletContext } from '@umijs/max'; import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
const { Text } = Typography; const { Text } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -56,7 +56,6 @@ const Dependence = () => {
const columns: any = [ const columns: any = [
{ {
title: '序号', title: '序号',
align: 'center' as const,
width: 50, width: 50,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
return <span style={{ cursor: 'text' }}>{index + 1} </span>; return <span style={{ cursor: 'text' }}>{index + 1} </span>;
@@ -66,13 +65,11 @@ const Dependence = () => {
title: '名称', title: '名称',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
align: 'center' as const,
}, },
{ {
title: '状态', title: '状态',
key: 'status', key: 'status',
dataIndex: 'status', dataIndex: 'status',
align: 'center' as const,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
return ( return (
<Space size="middle" style={{ cursor: 'text' }}> <Space size="middle" style={{ cursor: 'text' }}>
@@ -90,13 +87,11 @@ const Dependence = () => {
title: '备注', title: '备注',
dataIndex: 'remark', dataIndex: 'remark',
key: 'remark', key: 'remark',
align: 'center' as const,
}, },
{ {
title: '创建时间', title: '创建时间',
key: 'timestamp', key: 'timestamp',
dataIndex: 'timestamp', dataIndex: 'timestamp',
align: 'center' as const,
render: (text: string, record: any) => { render: (text: string, record: any) => {
const language = navigator.language || navigator.languages[0]; const language = navigator.language || navigator.languages[0];
const time = record.createdAt || record.timestamp; const time = record.createdAt || record.timestamp;
@@ -119,7 +114,6 @@ const Dependence = () => {
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
align: 'center' as const,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
const isPc = !isPhone; const isPc = !isPhone;
return ( return (
@@ -164,11 +158,11 @@ const Dependence = () => {
const [editedDependence, setEditedDependence] = useState(); const [editedDependence, setEditedDependence] = useState();
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]); const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [logDependence, setLogDependence] = useState<any>(); const [logDependence, setLogDependence] = useState<any>();
const [isLogModalVisible, setIsLogModalVisible] = useState(false); const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [type, setType] = useState('nodejs'); const [type, setType] = useState('nodejs');
const tableRef = useRef<any>(); const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef, 59);
const getDependencies = () => { const getDependencies = () => {
setLoading(true); setLoading(true);
@@ -283,16 +277,10 @@ const Dependence = () => {
const onSelectChange = (selectedIds: any[]) => { const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds); setSelectedRowIds(selectedIds);
setTimeout(() => {
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
setTableScrollHeight(getTableScroll({ extraHeight: 59 }));
}
});
}; };
const rowSelection = { const rowSelection = {
selectedRowIds, selectedRowKeys: selectedRowIds,
onChange: onSelectChange, onChange: onSelectChange,
}; };
@@ -368,12 +356,6 @@ const Dependence = () => {
getDependencies(); getDependencies();
}, [searchText, type]); }, [searchText, type]);
useEffect(() => {
if (tableRef.current) {
setTableScrollHeight(getTableScroll({ extraHeight: 59 }));
}
}, []);
useEffect(() => { useEffect(() => {
if (logDependence) { if (logDependence) {
localStorage.setItem('logDependence', logDependence.id); localStorage.setItem('logDependence', logDependence.id);
@@ -422,53 +404,6 @@ const Dependence = () => {
} }
}, [socketMessage]); }, [socketMessage]);
const panelContent = () => (
<>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => handlereInstallDependencies()}
>
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => delDependencies(false)}
>
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => delDependencies(true)}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span>
</div>
)}
<DndProvider backend={HTML5Backend}>
<Table
ref={tableRef}
columns={columns}
rowSelection={rowSelection}
pagination={false}
dataSource={value}
rowKey="id"
size="middle"
scroll={{ x: 768, y: tableScrollHeight }}
loading={loading}
/>
</DndProvider>
</>
);
const onTabChange = (activeKey: string) => { const onTabChange = (activeKey: string) => {
setSelectedRowIds([]); setSelectedRowIds([]);
setType(activeKey); setType(activeKey);
@@ -503,20 +438,60 @@ const Dependence = () => {
{ {
key: 'nodejs', key: 'nodejs',
label: 'NodeJs', label: 'NodeJs',
children: panelContent(),
}, },
{ {
key: 'python3', key: 'python3',
label: 'Python3', label: 'Python3',
children: panelContent(),
}, },
{ {
key: 'linux', key: 'linux',
label: 'Linux', label: 'Linux',
children: panelContent(),
}, },
]} ]}
/> />
<div ref={tableRef}>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => handlereInstallDependencies()}
>
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => delDependencies(false)}
>
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => delDependencies(true)}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span>
</div>
)}
<DndProvider backend={HTML5Backend}>
<Table
columns={columns}
rowSelection={rowSelection}
pagination={false}
dataSource={value}
rowKey="id"
size="middle"
scroll={{ x: 768, y: tableScrollHeight }}
loading={loading}
/>
</DndProvider>
</div>
<DependenceModal <DependenceModal
visible={isModalVisible} visible={isModalVisible}
handleCancel={handleCancel} handleCancel={handleCancel}
+6
View File
@@ -5,3 +5,9 @@ tr.drop-over-downward td {
tr.drop-over-upward td { tr.drop-over-upward td {
border-top: 2px dashed #1890ff; border-top: 2px dashed #1890ff;
} }
.text-ellipsis {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
+172 -161
View File
@@ -1,4 +1,10 @@
import React, { useCallback, useRef, useState, useEffect } from 'react'; import React, {
useCallback,
useRef,
useState,
useEffect,
useMemo,
} from 'react';
import { import {
Button, Button,
message, message,
@@ -28,11 +34,14 @@ import EditNameModal from './editNameModal';
import { DndProvider, useDrag, useDrop } from 'react-dnd'; import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less'; import './index.less';
import { exportJson, getTableScroll } from '@/utils/index'; import { exportJson } from '@/utils/index';
import { useOutletContext } from '@umijs/max'; import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import Copy from '../../components/copy';
import { VList } from '../../components/vlist';
const { Text, Paragraph } = Typography; const { Text } = Typography;
const { Search } = Input; const { Search } = Input;
enum Status { enum Status {
@@ -57,56 +66,11 @@ enum OperationPath {
const type = 'DragableBodyRow'; 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);
},
});
const [, drag] = useDrag({
type,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
drop(drag(ref));
return (
<tr
ref={ref}
className={`${className}${isOver ? dropClassName : ''}`}
style={{ cursor: 'move', ...style }}
{...restProps}
/>
);
};
const Env = () => { const Env = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>(); const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const columns: any = [ const columns: any = [
{ {
title: '序号', title: '序号',
align: 'center' as const,
width: 60, width: 60,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
return <span style={{ cursor: 'text' }}>{index + 1} </span>; return <span style={{ cursor: 'text' }}>{index + 1} </span>;
@@ -116,28 +80,21 @@ const Env = () => {
title: '名称', title: '名称',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
align: 'center' as const,
sorter: (a: any, b: any) => a.name.localeCompare(b.name), sorter: (a: any, b: any) => a.name.localeCompare(b.name),
}, },
{ {
title: '值', title: '值',
dataIndex: 'value', dataIndex: 'value',
key: 'value', key: 'value',
align: 'center' as const,
width: '35%', width: '35%',
render: (text: string, record: any) => { render: (text: string, record: any) => {
return ( return (
<Paragraph <div style={{ display: 'flex', alignItems: 'center' }}>
style={{ <Tooltip title={text} placement="topLeft">
wordBreak: 'break-all', <div className="text-ellipsis">{text}</div>
marginBottom: 0, </Tooltip>
textAlign: 'left', <Copy text={text} />
}} </div>
ellipsis={{ tooltip: text, rows: 2 }}
copyable
>
{text}
</Paragraph>
); );
}, },
}, },
@@ -145,13 +102,18 @@ const Env = () => {
title: '备注', title: '备注',
dataIndex: 'remarks', dataIndex: 'remarks',
key: 'remarks', key: 'remarks',
align: 'center' as const, render: (text: string, record: any) => {
return (
<Tooltip title={text} placement="topLeft">
<div className="text-ellipsis">{text}</div>
</Tooltip>
);
},
}, },
{ {
title: '更新时间', title: '更新时间',
dataIndex: 'timestamp', dataIndex: 'timestamp',
key: 'timestamp', key: 'timestamp',
align: 'center' as const,
width: 165, width: 165,
ellipsis: { ellipsis: {
showTitle: false, showTitle: false,
@@ -186,7 +148,6 @@ const Env = () => {
title: '状态', title: '状态',
key: 'status', key: 'status',
dataIndex: 'status', dataIndex: 'status',
align: 'center' as const,
width: 70, width: 70,
filters: [ filters: [
{ {
@@ -213,7 +174,6 @@ const Env = () => {
title: '操作', title: '操作',
key: 'action', key: 'action',
width: 120, width: 120,
align: 'center' as const,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
const isPc = !isPhone; const isPc = !isPhone;
return ( return (
@@ -253,9 +213,13 @@ const Env = () => {
const [editedEnv, setEditedEnv] = useState(); const [editedEnv, setEditedEnv] = useState();
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]); const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [importLoading, setImportLoading] = useState(false); const [importLoading, setImportLoading] = useState(false);
const tableRef = useRef<any>(); const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef, 59);
const resetRef = useRef<boolean>(true);
const setReset = (v) => {
resetRef.current = v;
};
const getEnvs = () => { const getEnvs = () => {
setLoading(true); setLoading(true);
@@ -270,6 +234,7 @@ const Env = () => {
}; };
const enabledOrDisabledEnv = (record: any, index: number) => { const enabledOrDisabledEnv = (record: any, index: number) => {
setReset(false);
Modal.confirm({ Modal.confirm({
title: `确认${record.status === Status. ? '启用' : '禁用'}`, title: `确认${record.status === Status. ? '启用' : '禁用'}`,
content: ( content: (
@@ -315,16 +280,19 @@ const Env = () => {
}; };
const addEnv = () => { const addEnv = () => {
setReset(false);
setEditedEnv(null as any); setEditedEnv(null as any);
setIsModalVisible(true); setIsModalVisible(true);
}; };
const editEnv = (record: any, index: number) => { const editEnv = (record: any, index: number) => {
setReset(false);
setEditedEnv(record); setEditedEnv(record);
setIsModalVisible(true); setIsModalVisible(true);
}; };
const deleteEnv = (record: any, index: number) => { const deleteEnv = (record: any, index: number) => {
setReset(false);
Modal.confirm({ Modal.confirm({
title: '确认删除', title: '确认删除',
content: ( content: (
@@ -356,7 +324,7 @@ const Env = () => {
const handleCancel = (env?: any[]) => { const handleCancel = (env?: any[]) => {
setIsModalVisible(false); setIsModalVisible(false);
env && handleEnv(env); getEnvs();
}; };
const handleEditNameCancel = (env?: any[]) => { const handleEditNameCancel = (env?: any[]) => {
@@ -364,28 +332,75 @@ const Env = () => {
getEnvs(); getEnvs();
}; };
const handleEnv = (env: any) => { const vComponents = useMemo(() => {
const result = [...value]; return VList({
const index = value.findIndex((x) => x.id === env.id); height: tableScrollHeight!,
if (index === -1) { reset: resetRef.current,
env = Array.isArray(env) ? env : [env]; rowHeight: 48,
result.push(...env); scrollTop: resetRef.current
} else { ? 0
result.splice(index, 1, { : tableRef.current?.querySelector('.ant-table-body')?.scrollTop,
...env, });
}); }, [tableScrollHeight, resetRef.current]);
}
setValue(result); const DragableBodyRow = (props: any) => {
const { index, moveRow, className, style, ...restProps } = props;
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);
},
});
const [, drag] = useDrag({
type,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
useEffect(() => {
drop(drag(ref));
}, [drag, drop]);
const components = useMemo(() => vComponents.body.row, []);
const tempProps = useMemo(() => {
return {
ref: ref,
className: `${className}${isOver ? dropClassName : ''}`,
style: { cursor: 'move', ...style },
...restProps,
};
}, [className, dropClassName, restProps, style, isOver]);
return <> {components(tempProps, ref)} </>;
}; };
const components = { const components = useMemo(() => {
body: { return {
row: DragableBodyRow, ...vComponents,
}, body: {
}; ...vComponents.body,
row: DragableBodyRow,
},
};
}, [vComponents]);
const moveRow = useCallback( const moveRow = useCallback(
(dragIndex, hoverIndex) => { (dragIndex: number, hoverIndex: number) => {
if (dragIndex === hoverIndex) { if (dragIndex === hoverIndex) {
return; return;
} }
@@ -408,20 +423,19 @@ const Env = () => {
const onSelectChange = (selectedIds: any[]) => { const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds); setSelectedRowIds(selectedIds);
setTimeout(() => {
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
setTableScrollHeight(getTableScroll({ extraHeight: 59 }));
}
});
}; };
useEffect(() => {
setReset(false);
}, [selectedRowIds]);
const rowSelection = { const rowSelection = {
selectedRowIds, selectedRowKeys: selectedRowIds,
onChange: onSelectChange, onChange: onSelectChange,
}; };
const delEnvs = () => { const delEnvs = () => {
setReset(false);
Modal.confirm({ Modal.confirm({
title: '确认删除', title: '确认删除',
content: <></>, content: <></>,
@@ -443,6 +457,7 @@ const Env = () => {
}; };
const operateEnvs = (operationStatus: number) => { const operateEnvs = (operationStatus: number) => {
setReset(false);
Modal.confirm({ Modal.confirm({
title: `确认${OperationName[operationStatus]}`, title: `确认${OperationName[operationStatus]}`,
content: <>{OperationName[operationStatus]}</>, content: <>{OperationName[operationStatus]}</>,
@@ -475,6 +490,7 @@ const Env = () => {
}; };
const onSearch = (value: string) => { const onSearch = (value: string) => {
setReset(true);
setSearchText(value.trim()); setSearchText(value.trim());
}; };
@@ -509,12 +525,6 @@ const Env = () => {
getEnvs(); getEnvs();
}, [searchText]); }, [searchText]);
useEffect(() => {
if (tableRef.current) {
setTableScrollHeight(getTableScroll({ extraHeight: 59 }));
}
}, []);
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper env-wrapper" className="ql-container-wrapper env-wrapper"
@@ -544,69 +554,70 @@ const Env = () => {
style: headerStyle, style: headerStyle,
}} }}
> >
{selectedRowIds.length > 0 && ( <div ref={tableRef}>
<div style={{ marginBottom: 16 }}> {selectedRowIds.length > 0 && (
<Button <div style={{ marginBottom: 16 }}>
type="primary" <Button
style={{ marginBottom: 5 }} type="primary"
onClick={modifyName} style={{ marginBottom: 5 }}
> onClick={modifyName}
>
</Button>
<Button </Button>
type="primary" <Button
style={{ marginBottom: 5, marginLeft: 8 }} type="primary"
onClick={delEnvs} style={{ marginBottom: 5, marginLeft: 8 }}
> onClick={delEnvs}
>
</Button>
<Button </Button>
type="primary" <Button
onClick={() => exportEnvs()} type="primary"
style={{ marginLeft: 8, marginRight: 8 }} onClick={() => exportEnvs()}
> style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button </Button>
type="primary" <Button
onClick={() => operateEnvs(0)} type="primary"
style={{ marginLeft: 8, marginBottom: 5 }} onClick={() => operateEnvs(0)}
> style={{ marginLeft: 8, marginBottom: 5 }}
>
</Button>
<Button </Button>
type="primary" <Button
onClick={() => operateEnvs(1)} type="primary"
style={{ marginLeft: 8, marginRight: 8 }} onClick={() => operateEnvs(1)}
> style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<span style={{ marginLeft: 8 }}> </Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span> <a>{selectedRowIds?.length}</a>
</div> </span>
)} </div>
<DndProvider backend={HTML5Backend}> )}
<Table <DndProvider backend={HTML5Backend}>
ref={tableRef} <Table
columns={columns} columns={columns}
rowSelection={rowSelection} rowSelection={rowSelection}
pagination={false} pagination={false}
dataSource={value} dataSource={value}
rowKey="id" rowKey="id"
size="middle" size="middle"
scroll={{ x: 1000, y: tableScrollHeight }} scroll={{ x: 1000, y: tableScrollHeight }}
components={components} components={components}
loading={loading} loading={loading}
onRow={(record: any, index: number | undefined) => { onRow={(record: any, index: number | undefined) => {
return { return {
index, index,
moveRow, moveRow,
} as any; } as any;
}} }}
/> />
</DndProvider> </DndProvider>
</div>
<EnvModal <EnvModal
visible={isModalVisible} visible={isModalVisible}
handleCancel={handleCancel} handleCancel={handleCancel}
+26 -5
View File
@@ -9,20 +9,37 @@ import './index.less';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
const Error = () => { const Error = () => {
const { user, theme } = useOutletContext<SharedContext>(); const { user, theme, reloadUser } = useOutletContext<SharedContext>();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [data, setData] = useState('暂无日志'); const [data, setData] = useState('暂无日志');
const getLog = () => { const getTimes = () => {
setLoading(true); return parseInt(localStorage.getItem('error_retry_times') || '0', 10);
};
let times = getTimes();
const getLog = (needLoading: boolean = true) => {
needLoading && setLoading(true);
request request
.get(`${config.apiPrefix}public/panel/log`) .get(`${config.apiPrefix}public/panel/log`)
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setData(data); setData(data);
if (!data) {
times = getTimes();
if (times > 5) {
return;
}
localStorage.setItem('error_retry_times', `${times + 1}`);
setTimeout(() => {
reloadUser();
getLog(false);
}, 3000);
}
} }
}) })
.finally(() => setLoading(false)); .finally(() => needLoading && setLoading(false));
}; };
useEffect(() => { useEffect(() => {
@@ -39,7 +56,7 @@ const Error = () => {
<div className="error-wrapper"> <div className="error-wrapper">
{loading ? ( {loading ? (
<PageLoading /> <PageLoading />
) : ( ) : data ? (
<Terminal <Terminal
name="服务错误" name="服务错误"
colorMode={theme === 'vs-dark' ? ColorMode.Dark : ColorMode.Light} colorMode={theme === 'vs-dark' ? ColorMode.Dark : ColorMode.Light}
@@ -55,6 +72,10 @@ const Error = () => {
}, },
]} ]}
/> />
) : times > 5 ? (
<> ql -l check </>
) : (
<PageLoading tip="启动中,请稍后..." />
)} )}
</div> </div>
); );
+1 -1
View File
@@ -21,7 +21,7 @@ import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import { DeleteOutlined } from '@ant-design/icons'; import { DeleteOutlined } from '@ant-design/icons';
import { depthFirstSearch } from '@/utils'; import { depthFirstSearch } from '@/utils';
import debounce from 'lodash/groupBy'; import debounce from 'lodash/debounce';
import uniq from 'lodash/uniq'; import uniq from 'lodash/uniq';
import useFilterTreeData from '@/hooks/useFilterTreeData'; import useFilterTreeData from '@/hooks/useFilterTreeData';
+5 -5
View File
@@ -49,6 +49,7 @@ const EditModal = ({
const { theme } = useTheme(); const { theme } = useTheme();
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const [isRunning, setIsRunning] = useState(false); const [isRunning, setIsRunning] = useState(false);
const [currentPid, setCurrentPid] = useState(null);
const cancel = () => { const cancel = () => {
handleCancel(); handleCancel();
@@ -94,21 +95,21 @@ const EditModal = ({
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setIsRunning(true); setIsRunning(true);
setCurrentPid(data);
} }
}); });
}; };
const stop = () => { const stop = () => {
if (!cNode || !cNode.title) { if (!cNode || !cNode.title || !currentPid) {
return; return;
} }
const content = editorRef.current.getValue().replace(/\r\n/g, '\n');
request request
.put(`${config.apiPrefix}scripts/stop`, { .put(`${config.apiPrefix}scripts/stop`, {
data: { data: {
filename: cNode.title, filename: cNode.title,
path: cNode.parent || '', path: cNode.parent || '',
content, pid: currentPid,
}, },
}) })
.then(({ code, data }) => { .then(({ code, data }) => {
@@ -257,7 +258,6 @@ const EditModal = ({
style={{ style={{
height: '100%', height: '100%',
padding: '0 15px', padding: '0 15px',
whiteSpace: 'break-spaces',
}} }}
> >
{log} {log}
@@ -272,7 +272,7 @@ const EditModal = ({
content: content:
editorRef.current && editorRef.current &&
editorRef.current.getValue().replace(/\r\n/g, '\n'), editorRef.current.getValue().replace(/\r\n/g, '\n'),
filename: cNode?.title, ...cNode,
}} }}
/> />
<SettingModal <SettingModal
+80 -42
View File
@@ -11,6 +11,7 @@ import {
Dropdown, Dropdown,
Menu, Menu,
Empty, Empty,
MenuProps,
} from 'antd'; } from 'antd';
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
@@ -35,10 +36,12 @@ import EditScriptNameModal from './editNameModal';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { history, useOutletContext, useLocation } from '@umijs/max'; import { history, useOutletContext, useLocation } from '@umijs/max';
import { parse } from 'query-string'; import { parse } from 'query-string';
import { depthFirstSearch } from '@/utils'; import { depthFirstSearch, findNode } from '@/utils';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import useFilterTreeData from '@/hooks/useFilterTreeData'; import useFilterTreeData from '@/hooks/useFilterTreeData';
import uniq from 'lodash/uniq'; import uniq from 'lodash/uniq';
import IconFont from '@/components/iconfont';
import RenameModal from './renameModal';
const { Text } = Typography; const { Text } = Typography;
@@ -64,20 +67,23 @@ const Script = () => {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false); const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false);
const [isRenameFileModalVisible, setIsRenameFileModalVisible] =
useState(false);
const [currentNode, setCurrentNode] = useState<any>(); const [currentNode, setCurrentNode] = useState<any>();
const [expandedKeys, setExpandedKeys] = useState<string[]>([]); const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const getScripts = () => { const getScripts = (needLoading: boolean = true) => {
setLoading(true); needLoading && setLoading(true);
request request
.get(`${config.apiPrefix}scripts`) .get(`${config.apiPrefix}scripts`)
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setData(data); setData(data);
initGetScript(); initState();
initGetScript(data);
} }
}) })
.finally(() => setLoading(false)); .finally(() => needLoading && setLoading(false));
}; };
const getDetail = (node: any) => { const getDetail = (node: any) => {
@@ -94,7 +100,7 @@ const Script = () => {
}); });
}; };
const initGetScript = () => { const initGetScript = (_data: any) => {
const { p, s } = parse(history.location.search); const { p, s } = parse(history.location.search);
if (s) { if (s) {
const vkey = `${p}/${s}`; const vkey = `${p}/${s}`;
@@ -105,8 +111,11 @@ const Script = () => {
parent: p, parent: p,
}, },
}; };
setExpandedKeys([p]); const item = findNode(_data, (c) => c.key === obj.node.key);
onTreeSelect([vkey], obj); if (item) {
setExpandedKeys([p as string]);
onTreeSelect([vkey], obj);
}
} }
}; };
@@ -287,6 +296,15 @@ const Script = () => {
}); });
}; };
const renameFile = () => {
setIsRenameFileModalVisible(true);
};
const handleRenameFileCancel = () => {
setIsRenameFileModalVisible(false);
getScripts(false);
};
const addFile = () => { const addFile = () => {
setIsAddFileModalVisible(true); setIsAddFileModalVisible(true);
}; };
@@ -372,7 +390,7 @@ const Script = () => {
const menuAction = (key: string | number) => { const menuAction = (key: string | number) => {
switch (key) { switch (key) {
case 'save': case 'add':
addFile(); addFile();
break; break;
case 'edit': case 'edit':
@@ -381,45 +399,52 @@ const Script = () => {
case 'delete': case 'delete':
deleteFile(); deleteFile();
break; break;
case 'rename':
renameFile();
break;
default: default:
break; break;
} }
}; };
const menu = isEditing ? ( const menu: MenuProps = isEditing
<Menu ? {
items={[ items: [
{ label: '保存', key: 'save', icon: <PlusOutlined /> }, { label: '保存', key: 'save', icon: <PlusOutlined /> },
{ label: '退出编辑', key: 'exit', icon: <EditOutlined /> }, { label: '退出编辑', key: 'exit', icon: <EditOutlined /> },
]} ],
onClick={({ key, domEvent }) => { onClick: ({ key, domEvent }) => {
domEvent.stopPropagation(); domEvent.stopPropagation();
action(key); action(key);
}}
/>
) : (
<Menu
items={[
{ label: '新建', key: 'add', icon: <PlusOutlined /> },
{
label: '编辑',
key: 'edit',
icon: <EditOutlined />,
disabled: !select,
}, },
{ }
label: '删除', : {
key: 'delete', items: [
icon: <DeleteOutlined />, { label: '新建', key: 'add', icon: <PlusOutlined /> },
disabled: !select, {
label: '编辑',
key: 'edit',
icon: <EditOutlined />,
disabled: !select,
},
{
label: '重命名',
key: 'rename',
icon: <IconFont type="ql-icon-rename" />,
disabled: !select,
},
{
label: '删除',
key: 'delete',
icon: <DeleteOutlined />,
disabled: !select,
},
],
onClick: ({ key, domEvent }) => {
domEvent.stopPropagation();
menuAction(key);
}, },
]} };
onClick={({ key, domEvent }) => {
domEvent.stopPropagation();
menuAction(key);
}}
/>
);
return ( return (
<PageContainer <PageContainer
@@ -442,7 +467,7 @@ const Script = () => {
allowClear allowClear
onSelect={onSelect} onSelect={onSelect}
/>, />,
<Dropdown overlay={menu} trigger={['click']}> <Dropdown menu={menu} trigger={['click']}>
<Button type="primary" icon={<EllipsisOutlined />} /> <Button type="primary" icon={<EllipsisOutlined />} />
</Dropdown>, </Dropdown>,
] ]
@@ -471,6 +496,14 @@ const Script = () => {
icon={<EditOutlined />} icon={<EditOutlined />}
/> />
</Tooltip>, </Tooltip>,
<Tooltip title="重命名">
<Button
disabled={!select}
type="primary"
onClick={renameFile}
icon={<IconFont type="ql-icon-rename" />}
/>
</Tooltip>,
<Tooltip title="删除"> <Tooltip title="删除">
<Button <Button
type="primary" type="primary"
@@ -585,6 +618,11 @@ const Script = () => {
treeData={data} treeData={data}
handleCancel={addFileModalClose} handleCancel={addFileModalClose}
/> />
<RenameModal
visible={isRenameFileModalVisible}
handleCancel={handleRenameFileCancel}
currentNode={currentNode}
/>
</div> </div>
</PageContainer> </PageContainer>
); );
+78
View File
@@ -0,0 +1,78 @@
import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
const RenameModal = ({
currentNode,
handleCancel,
visible,
}: {
currentNode?: any;
visible: boolean;
handleCancel: () => void;
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const handleOk = async (values: any) => {
setLoading(true);
try {
const { code, data } = await request.put(
`${config.apiPrefix}scripts/rename`,
{
data: {
filename: currentNode.title,
path: currentNode.parent || '',
newFilename: values.name,
},
},
);
if (code === 200) {
message.success('更新名称成功');
handleCancel();
}
setLoading(false);
} catch (error) {
setLoading(false);
}
};
useEffect(() => {
form.resetFields();
}, [currentNode, visible]);
return (
<Modal
title="重命名"
open={visible}
forceRender
centered
maskClosable={false}
onOk={() => {
form
.validateFields()
.then((values) => {
handleOk(values);
})
.catch((info) => {
console.log('Validate Failed:', info);
});
}}
onCancel={() => handleCancel()}
confirmLoading={loading}
>
<Form form={form} layout="vertical" name="edit_name_modal">
<Form.Item
name="name"
rules={[{ required: true, message: '请输入新名称' }]}
>
<Input placeholder="请输入新名称" />
</Form.Item>
</Form>
</Modal>
);
};
export default RenameModal;
+2 -2
View File
@@ -17,7 +17,7 @@ const SaveModal = ({
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
setLoading(true); setLoading(true);
const payload = { ...file, ...values, originFilename: file.filename }; const payload = { ...file, ...values, originFilename: file.title };
request request
.post(`${config.apiPrefix}scripts`, { .post(`${config.apiPrefix}scripts`, {
data: payload, data: payload,
@@ -60,7 +60,7 @@ const SaveModal = ({
form={form} form={form}
layout="vertical" layout="vertical"
name="script_modal" name="script_modal"
initialValues={file} initialValues={{ filename: file?.title, path: file?.parent || '' }}
> >
<Form.Item <Form.Item
name="filename" name="filename"
+30 -2
View File
@@ -1,10 +1,17 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Typography, Input, Form, Button, message } from 'antd'; import { Typography, Input, Form, Button, message, Descriptions } from 'antd';
import styles from './index.less'; import styles from './index.less';
import { SharedContext } from '@/layouts';
import dayjs from 'dayjs';
const { Link } = Typography; const { Link } = Typography;
const About = () => { enum TVersion {
'develop' = '开发版',
'master' = '正式版',
}
const About = ({ systemInfo }: { systemInfo: SharedContext['systemInfo'] }) => {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<img <img
@@ -19,6 +26,27 @@ const About = () => {
task management panel that supports typescript, javaScript, python3, task management panel that supports typescript, javaScript, python3,
and shell. and shell.
</span> </span>
<Descriptions>
<Descriptions.Item label="版本" span={3}>
{TVersion[systemInfo.branch]} v{systemInfo.version}
</Descriptions.Item>
<Descriptions.Item label="更新时间" span={3}>
{dayjs(systemInfo.lastCommitTime * 1000).format(
'YYYY-MM-DD HH:mm:ss',
)}
</Descriptions.Item>
<Descriptions.Item label="更新ID" span={3}>
{systemInfo.lastCommitId}
</Descriptions.Item>
<Descriptions.Item label="更新日志" span={3}>
<Link
href={`https://qn.whyour.cn/version.yaml?t=${Date.now()}`}
target="_blank"
>
</Link>
</Descriptions.Item>
</Descriptions>
<div> <div>
<Link <Link
href="https://github.com/whyour/qinglong" href="https://github.com/whyour/qinglong"
+16 -24
View File
@@ -2,11 +2,10 @@ import React, { useEffect, useState, useRef } from 'react';
import { Statistic, Modal, Tag, Button, Spin, message } from 'antd'; import { Statistic, Modal, Tag, Button, Spin, message } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
import { version } from '../../version';
const { Countdown } = Statistic; const { Countdown } = Statistic;
const CheckUpdate = ({ socketMessage }: any) => { const CheckUpdate = ({ socketMessage, systemInfo }: any) => {
const [updateLoading, setUpdateLoading] = useState(false); const [updateLoading, setUpdateLoading] = useState(false);
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const modalRef = useRef<any>(); const modalRef = useRef<any>();
@@ -23,7 +22,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
if (data.hasNewVersion) { if (data.hasNewVersion) {
showConfirmUpdateModal(data); showConfirmUpdateModal(data);
} else { } else {
showForceUpdateModal(); showForceUpdateModal(data);
} }
} }
}) })
@@ -36,7 +35,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
}); });
}; };
const showForceUpdateModal = () => { const showForceUpdateModal = (data: any) => {
Modal.confirm({ Modal.confirm({
width: 500, width: 500,
title: '更新', title: '更新',
@@ -44,13 +43,12 @@ const CheckUpdate = ({ socketMessage }: any) => {
<> <>
<div></div> <div></div>
<div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}> <div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>
{version} {data.lastVersion}
</div> </div>
</> </>
), ),
okText: '确认', okText: '强制更新',
cancelText: '强制更新', onOk() {
onCancel() {
showUpdatingModal(); showUpdatingModal();
request request
.put(`${config.apiPrefix}system/update`) .put(`${config.apiPrefix}system/update`)
@@ -70,16 +68,13 @@ const CheckUpdate = ({ socketMessage }: any) => {
<> <>
<div></div> <div></div>
<div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}> <div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>
{lastVersion}使{version} {lastVersion} 使 {systemInfo.version}
</div> </div>
</> </>
), ),
content: ( content: (
<pre <pre
style={{ style={{
wordBreak: 'break-all',
whiteSpace: 'pre-wrap',
paddingTop: 15,
fontSize: 12, fontSize: 12,
fontWeight: 400, fontWeight: 400,
}} }}
@@ -107,20 +102,19 @@ const CheckUpdate = ({ socketMessage }: any) => {
width: 600, width: 600,
maskClosable: false, maskClosable: false,
closable: false, closable: false,
keyboard: false,
okButtonProps: { disabled: true }, okButtonProps: { disabled: true },
title: '更新中...', title: '更新中...',
centered: true, centered: true,
content: ( content: (
<pre <pre
style={{ style={{
wordBreak: 'break-all', fontSize: 12,
whiteSpace: 'pre-wrap', fontWeight: 400,
fontSize: 12, }}
fontWeight: 400, >
}} {value}
> </pre>
{value}
</pre>
), ),
}); });
}; };
@@ -146,8 +140,6 @@ const CheckUpdate = ({ socketMessage }: any) => {
<> <>
<pre <pre
style={{ style={{
wordBreak: 'break-all',
whiteSpace: 'pre-wrap',
fontSize: 12, fontSize: 12,
fontWeight: 400, fontWeight: 400,
}} }}
+15
View File
@@ -18,5 +18,20 @@
.desc { .desc {
margin-bottom: 10px; margin-bottom: 10px;
} }
:global {
.ant-descriptions-row > th,
.ant-descriptions-row > td {
padding-bottom: 10px;
}
}
}
}
.ql-setting-container {
.ant-tabs-content-holder {
max-height: calc(100vh - 114px);
max-height: calc(100vh - var(--vh-offset, 114px));
overflow-y: auto;
} }
} }
+19 -111
View File
@@ -17,7 +17,6 @@ import {
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import * as DarkReader from '@umijs/ssr-darkreader';
import AppModal from './appModal'; import AppModal from './appModal';
import { import {
EditOutlined, EditOutlined,
@@ -27,33 +26,34 @@ import {
import SecuritySettings from './security'; import SecuritySettings from './security';
import LoginLog from './loginLog'; import LoginLog from './loginLog';
import NotificationSetting from './notification'; import NotificationSetting from './notification';
import CheckUpdate from './checkUpdate'; import Other from './other';
import About from './about'; import About from './about';
import { useOutletContext } from '@umijs/max'; import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import './index.less';
const { Text } = Typography; const { Text } = Typography;
const optionsWithDisabled = [
{ label: '亮色', value: 'light' },
{ label: '暗色', value: 'dark' },
{ label: '跟随系统', value: 'auto' },
];
const Setting = () => { const Setting = () => {
const { headerStyle, isPhone, user, reloadUser, reloadTheme, socketMessage } = const {
useOutletContext<SharedContext>(); headerStyle,
isPhone,
user,
reloadUser,
reloadTheme,
socketMessage,
systemInfo,
} = useOutletContext<SharedContext>();
const columns = [ const columns = [
{ {
title: '名称', title: '名称',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
align: 'center' as const,
}, },
{ {
title: 'Client ID', title: 'Client ID',
dataIndex: 'client_id', dataIndex: 'client_id',
key: 'client_id', key: 'client_id',
align: 'center' as const,
render: (text: string, record: any) => { render: (text: string, record: any) => {
return <Text copyable>{record.client_id}</Text>; return <Text copyable>{record.client_id}</Text>;
}, },
@@ -62,7 +62,6 @@ const Setting = () => {
title: 'Client Secret', title: 'Client Secret',
dataIndex: 'client_secret', dataIndex: 'client_secret',
key: 'client_secret', key: 'client_secret',
align: 'center' as const,
render: (text: string, record: any) => { render: (text: string, record: any) => {
return <Text copyable={{ text: record.client_secret }}>*******</Text>; return <Text copyable={{ text: record.client_secret }}>*******</Text>;
}, },
@@ -71,7 +70,7 @@ const Setting = () => {
title: '权限', title: '权限',
dataIndex: 'scopes', dataIndex: 'scopes',
key: 'scopes', key: 'scopes',
align: 'center' as const, width: '40%',
render: (text: string, record: any) => { render: (text: string, record: any) => {
return record.scopes.map((scope: any) => { return record.scopes.map((scope: any) => {
return <Tag key={scope}>{(config.scopesMap as any)[scope]}</Tag>; return <Tag key={scope}>{(config.scopesMap as any)[scope]}</Tag>;
@@ -81,7 +80,6 @@ const Setting = () => {
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
align: 'center' as const,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
const isPc = !isPhone; const isPc = !isPhone;
return ( return (
@@ -108,37 +106,12 @@ const Setting = () => {
]; ];
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto';
const [dataSource, setDataSource] = useState<any[]>([]); const [dataSource, setDataSource] = useState<any[]>([]);
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
const [editedApp, setEditedApp] = useState<any>(); const [editedApp, setEditedApp] = useState<any>();
const [tabActiveKey, setTabActiveKey] = useState('security'); const [tabActiveKey, setTabActiveKey] = useState('security');
const [loginLogData, setLoginLogData] = useState<any[]>([]); const [loginLogData, setLoginLogData] = useState<any[]>([]);
const [notificationInfo, setNotificationInfo] = useState<any>(); const [notificationInfo, setNotificationInfo] = useState<any>();
const [logRemoveFrequency, setLogRemoveFrequency] = useState<number>();
const [form] = Form.useForm();
const {
enable: enableDarkMode,
disable: disableDarkMode,
exportGeneratedCSS: collectCSS,
setFetchMethod,
auto: followSystemColorScheme,
} = DarkReader || {};
const themeChange = (e: any) => {
const _theme = e.target.value;
localStorage.setItem('qinglong_dark_theme', e.target.value);
setFetchMethod(fetch);
if (_theme === 'dark') {
enableDarkMode({});
} else if (_theme === 'light') {
disableDarkMode();
} else {
followSystemColorScheme({});
}
reloadTheme();
};
const getApps = () => { const getApps = () => {
setLoading(true); setLoading(true);
@@ -263,8 +236,6 @@ const Setting = () => {
getLoginLog(); getLoginLog();
} else if (activeKey === 'notification') { } else if (activeKey === 'notification') {
getNotification(); getNotification();
} else if (activeKey === 'other') {
getLogRemoveFrequency();
} }
}; };
@@ -281,40 +252,9 @@ const Setting = () => {
}); });
}; };
const getLogRemoveFrequency = () => {
request
.get(`${config.apiPrefix}system/log/remove`)
.then(({ code, data }) => {
if (code === 200 && data.info) {
const { frequency } = data.info;
setLogRemoveFrequency(frequency);
}
})
.catch((error: any) => {
console.log(error);
});
};
const updateRemoveLogFrequency = () => {
setTimeout(() => {
request
.put(`${config.apiPrefix}system/log/remove`, {
data: { frequency: logRemoveFrequency },
})
.then(({ code, data }) => {
if (code === 200) {
message.success('更新成功');
}
})
.catch((error: any) => {
console.log(error);
});
});
};
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper ql-container-wrapper-has-tab" className="ql-container-wrapper ql-container-wrapper-has-tab ql-setting-container"
title="系统设置" title="系统设置"
header={{ header={{
style: headerStyle, style: headerStyle,
@@ -369,49 +309,17 @@ const Setting = () => {
key: 'other', key: 'other',
label: '其他设置', label: '其他设置',
children: ( children: (
<Form layout="vertical" form={form}> <Other
<Form.Item reloadTheme={reloadTheme}
label="主题设置" socketMessage={socketMessage}
name="theme" systemInfo={systemInfo}
initialValue={defaultTheme} />
>
<Radio.Group
options={optionsWithDisabled}
onChange={themeChange}
value={defaultTheme}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>
<Form.Item
label="日志删除频率"
name="frequency"
tooltip="每x天自动删除x天以前的日志"
>
<Input.Group compact>
<InputNumber
addonBefore="每"
addonAfter="天"
style={{ width: 150 }}
min={0}
value={logRemoveFrequency}
onChange={(value) => setLogRemoveFrequency(value)}
/>
<Button type="primary" onClick={updateRemoveLogFrequency}>
</Button>
</Input.Group>
</Form.Item>
<Form.Item label="检查更新" name="update">
<CheckUpdate socketMessage={socketMessage} />
</Form.Item>
</Form>
), ),
}, },
{ {
key: 'about', key: 'about',
label: '关于', label: '关于',
children: <About />, children: <About systemInfo={systemInfo} />,
}, },
]} ]}
></Tabs> ></Tabs>
+2 -6
View File
@@ -18,7 +18,6 @@ enum LoginStatusColor {
const columns = [ const columns = [
{ {
title: '序号', title: '序号',
align: 'center' as const,
width: 50, width: 50,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
return index + 1; return index + 1;
@@ -28,7 +27,6 @@ const columns = [
title: '登录时间', title: '登录时间',
dataIndex: 'timestamp', dataIndex: 'timestamp',
key: 'timestamp', key: 'timestamp',
align: 'center' as const,
render: (text: string, record: any) => { render: (text: string, record: any) => {
return new Date(record.timestamp).toLocaleString(); return new Date(record.timestamp).toLocaleString();
}, },
@@ -37,25 +35,23 @@ const columns = [
title: '登录地址', title: '登录地址',
dataIndex: 'address', dataIndex: 'address',
key: 'address', key: 'address',
align: 'center' as const,
}, },
{ {
title: '登录IP', title: '登录IP',
dataIndex: 'ip', dataIndex: 'ip',
key: 'ip', key: 'ip',
align: 'center' as const,
}, },
{ {
title: '登录设备', title: '登录设备',
dataIndex: 'platform', dataIndex: 'platform',
key: 'platform', key: 'platform',
align: 'center' as const, width: 80,
}, },
{ {
title: '登录状态', title: '登录状态',
dataIndex: 'status', dataIndex: 'status',
key: 'status', key: 'status',
align: 'center' as const, width: 80,
render: (text: string, record: any) => { render: (text: string, record: any) => {
return ( return (
<Tag color={LoginStatusColor[record.status]} style={{ marginRight: 0 }}> <Tag color={LoginStatusColor[record.status]} style={{ marginRight: 0 }}>
+120
View File
@@ -0,0 +1,120 @@
import React, { useState, useEffect } from 'react';
import { Button, InputNumber, Form, Radio, message, Input } from 'antd';
import * as DarkReader from '@umijs/ssr-darkreader';
import config from '@/utils/config';
import { request } from '@/utils/http';
import CheckUpdate from './checkUpdate';
import { SharedContext } from '@/layouts';
import './index.less';
const optionsWithDisabled = [
{ label: '亮色', value: 'light' },
{ label: '暗色', value: 'dark' },
{ label: '跟随系统', value: 'auto' },
];
const Other = ({
systemInfo,
socketMessage,
reloadTheme,
}: Pick<SharedContext, 'socketMessage' | 'reloadTheme' | 'systemInfo'>) => {
const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto';
const [logRemoveFrequency, setLogRemoveFrequency] = useState<number | null>();
const [form] = Form.useForm();
const {
enable: enableDarkMode,
disable: disableDarkMode,
exportGeneratedCSS: collectCSS,
setFetchMethod,
auto: followSystemColorScheme,
} = DarkReader || {};
const themeChange = (e: any) => {
const _theme = e.target.value;
localStorage.setItem('qinglong_dark_theme', e.target.value);
setFetchMethod(fetch);
if (_theme === 'dark') {
enableDarkMode({});
} else if (_theme === 'light') {
disableDarkMode();
} else {
followSystemColorScheme({});
}
reloadTheme();
};
const getLogRemoveFrequency = () => {
request
.get(`${config.apiPrefix}system/log/remove`)
.then(({ code, data }) => {
if (code === 200 && data.info) {
const { frequency } = data.info;
setLogRemoveFrequency(frequency);
}
})
.catch((error: any) => {
console.log(error);
});
};
const updateRemoveLogFrequency = () => {
setTimeout(() => {
request
.put(`${config.apiPrefix}system/log/remove`, {
data: { frequency: logRemoveFrequency },
})
.then(({ code, data }) => {
if (code === 200) {
message.success('更新成功');
}
})
.catch((error: any) => {
console.log(error);
});
});
};
useEffect(() => {
getLogRemoveFrequency();
}, []);
return (
<Form layout="vertical" form={form}>
<Form.Item label="主题设置" name="theme" initialValue={defaultTheme}>
<Radio.Group
options={optionsWithDisabled}
onChange={themeChange}
value={defaultTheme}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>
<Form.Item
label="日志删除频率"
name="frequency"
tooltip="每x天自动删除x天以前的日志"
>
<Input.Group compact>
<InputNumber
addonBefore="每"
addonAfter="天"
style={{ width: 150 }}
min={0}
value={logRemoveFrequency}
onChange={(value) => setLogRemoveFrequency(value)}
/>
<Button type="primary" onClick={updateRemoveLogFrequency}>
</Button>
</Input.Group>
</Form.Item>
<Form.Item label="检查更新" name="update">
<CheckUpdate systemInfo={systemInfo} socketMessage={socketMessage} />
</Form.Item>
</Form>
);
};
export default Other;
+9
View File
@@ -0,0 +1,9 @@
.inline-form-item {
margin-bottom: 0;
.ant-form-item {
display: inline-block;
width: 50%;
margin-bottom: 0px;
}
}
+22 -41
View File
@@ -29,11 +29,11 @@ import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import SubscriptionModal from './modal'; import SubscriptionModal from './modal';
import { getTableScroll } from '@/utils/index';
import { history, useOutletContext } from '@umijs/max'; import { history, useOutletContext } from '@umijs/max';
import './index.less'; import './index.less';
import SubscriptionLogModal from './logModal'; import SubscriptionLogModal from './logModal';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -68,7 +68,6 @@ const Subscription = () => {
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
width: 150, width: 150,
align: 'center' as const,
sorter: { sorter: {
compare: (a: any, b: any) => a.name.localeCompare(b.name), compare: (a: any, b: any) => a.name.localeCompare(b.name),
multiple: 2, multiple: 2,
@@ -78,7 +77,6 @@ const Subscription = () => {
title: '链接', title: '链接',
dataIndex: 'url', dataIndex: 'url',
key: 'url', key: 'url',
align: 'center' as const,
sorter: { sorter: {
compare: (a: any, b: any) => a.name.localeCompare(b.name), compare: (a: any, b: any) => a.name.localeCompare(b.name),
multiple: 2, multiple: 2,
@@ -89,7 +87,6 @@ const Subscription = () => {
style={{ style={{
wordBreak: 'break-all', wordBreak: 'break-all',
marginBottom: 0, marginBottom: 0,
textAlign: 'left',
}} }}
ellipsis={{ tooltip: text, rows: 2 }} ellipsis={{ tooltip: text, rows: 2 }}
> >
@@ -103,7 +100,6 @@ const Subscription = () => {
dataIndex: 'type', dataIndex: 'type',
key: 'type', key: 'type',
width: 130, width: 130,
align: 'center' as const,
render: (text: string, record: any) => { render: (text: string, record: any) => {
return (SubscriptionType as any)[record.type]; return (SubscriptionType as any)[record.type];
}, },
@@ -113,7 +109,6 @@ const Subscription = () => {
dataIndex: 'branch', dataIndex: 'branch',
key: 'branch', key: 'branch',
width: 130, width: 130,
align: 'center' as const,
render: (text: string, record: any) => { render: (text: string, record: any) => {
return record.branch || '-'; return record.branch || '-';
}, },
@@ -121,7 +116,6 @@ const Subscription = () => {
{ {
title: '定时规则', title: '定时规则',
width: 180, width: 180,
align: 'center' as const,
render: (text: string, record: any) => { render: (text: string, record: any) => {
if (record.schedule_type === 'interval') { if (record.schedule_type === 'interval') {
const { type, value } = record.interval_schedule; const { type, value } = record.interval_schedule;
@@ -134,7 +128,6 @@ const Subscription = () => {
title: '状态', title: '状态',
key: 'status', key: 'status',
dataIndex: 'status', dataIndex: 'status',
align: 'center' as const,
width: 110, width: 110,
filters: [ filters: [
{ {
@@ -189,7 +182,6 @@ const Subscription = () => {
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
align: 'center' as const,
width: 130, width: 130,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
const isPc = !isPhone; const isPc = !isPhone;
@@ -243,11 +235,10 @@ const Subscription = () => {
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(20); const [pageSize, setPageSize] = useState(20);
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [searchValue, setSearchValue] = useState('');
const [isLogModalVisible, setIsLogModalVisible] = useState(false); const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [logSubscription, setLogSubscription] = useState<any>(); const [logSubscription, setLogSubscription] = useState<any>();
const tableRef = useRef<any>(); const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef);
const runSubscription = (record: any, index: number) => { const runSubscription = (record: any, index: number) => {
Modal.confirm({ Modal.confirm({
@@ -428,28 +419,26 @@ const Subscription = () => {
arrow={{ pointAtCenter: true }} arrow={{ pointAtCenter: true }}
placement="bottomRight" placement="bottomRight"
trigger={['click']} trigger={['click']}
overlay={ menu={{
<Menu items: [
items={[ { label: '编辑', key: 'edit', icon: <EditOutlined /> },
{ label: '编辑', key: 'edit', icon: <EditOutlined /> }, {
{ label: record.is_disabled === 1 ? '启用' : '禁用',
label: record.is_disabled === 1 ? '启用' : '禁用', key: 'enableOrDisable',
key: 'enableOrDisable', icon:
icon: record.is_disabled === 1 ? (
record.is_disabled === 1 ? ( <CheckCircleOutlined />
<CheckCircleOutlined /> ) : (
) : ( <StopOutlined />
<StopOutlined /> ),
), },
}, { label: '删除', key: 'delete', icon: <DeleteOutlined /> },
{ label: '删除', key: 'delete', icon: <DeleteOutlined /> }, ],
]} onClick: ({ key, domEvent }) => {
onClick={({ key, domEvent }) => { domEvent.stopPropagation();
domEvent.stopPropagation(); action(key, record, index);
action(key, record, index); },
}} }}
/>
}
> >
<a onClick={(e) => e.stopPropagation()}> <a onClick={(e) => e.stopPropagation()}>
<EllipsisOutlined /> <EllipsisOutlined />
@@ -542,12 +531,6 @@ const Subscription = () => {
setPageSize(parseInt(localStorage.getItem('pageSize') || '20')); setPageSize(parseInt(localStorage.getItem('pageSize') || '20'));
}, []); }, []);
useEffect(() => {
if (tableRef.current) {
setTableScrollHeight(getTableScroll());
}
}, []);
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper subscriptiontab-wrapper" className="ql-container-wrapper subscriptiontab-wrapper"
@@ -559,8 +542,6 @@ const Subscription = () => {
enterButton enterButton
allowClear allowClear
loading={loading} loading={loading}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onSearch={onSearch} onSearch={onSearch}
/>, />,
<Button key="2" type="primary" onClick={() => addSubscription()}> <Button key="2" type="primary" onClick={() => addSubscription()}>
+68 -11
View File
@@ -1,12 +1,22 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { Modal, message, InputNumber, Form, Radio, Select, Input } from 'antd'; import {
Modal,
message,
InputNumber,
Form,
Radio,
Select,
Input,
Switch,
} from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import isNil from 'lodash/isNil';
const { Option } = Select; const { Option } = Select;
const repoUrlRegx = /[^\/\:]+\/[^\/]+(?=\.git)/; const repoUrlRegx = /([^\/\:]+\/[^\/]+)(?=\.git)/;
const fileUrlRegx = /[^\/\:]+\/[^\/]+$/; const fileUrlRegx = /([^\/\:]+\/[^\/\.]+)\.[a-z]+$/;
const SubscriptionModal = ({ const SubscriptionModal = ({
subscription, subscription,
@@ -26,7 +36,11 @@ const SubscriptionModal = ({
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
setLoading(true); setLoading(true);
const method = subscription ? 'put' : 'post'; const method = subscription ? 'put' : 'post';
const payload = { ...values }; const payload = {
...values,
autoAddCron: Boolean(values.autoAddCron),
autoDelCron: Boolean(values.autoDelCron),
};
if (subscription) { if (subscription) {
payload.id = subscription.id; payload.id = subscription.id;
} }
@@ -86,7 +100,7 @@ const SubscriptionModal = ({
let _alias = ''; let _alias = '';
const _regx = _type === 'file' ? fileUrlRegx : repoUrlRegx; const _regx = _type === 'file' ? fileUrlRegx : repoUrlRegx;
if (_regx.test(_url)) { if (_regx.test(_url)) {
_alias = _url.match(_regx)![0].replaceAll('/', '_').replaceAll('.', '_'); _alias = _url.match(_regx)![1].replaceAll('/', '_').replaceAll('.', '_');
} }
if (_branch) { if (_branch) {
_alias = _alias + '_' + _branch; _alias = _alias + '_' + _branch;
@@ -110,8 +124,8 @@ const SubscriptionModal = ({
} }
}; };
const numberChange = (value: number) => { const numberChange = (value: number | null) => {
setIntervalNumber(value); setIntervalNumber(value || 1);
if (!value) { if (!value) {
onChange?.(null); onChange?.(null);
} else { } else {
@@ -125,6 +139,7 @@ const SubscriptionModal = ({
setIntervalNumber(value.value); setIntervalNumber(value.value);
} }
}, [value]); }, [value]);
return ( return (
<Input.Group compact> <Input.Group compact>
<InputNumber <InputNumber
@@ -217,7 +232,7 @@ const SubscriptionModal = ({
dependences, dependences,
branch, branch,
extensions, extensions,
alias: formatAlias(url, branch), alias: formatAlias(url, branch, _type),
}); });
setType(_type); setType(_type);
} }
@@ -228,8 +243,17 @@ const SubscriptionModal = ({
if (text.startsWith('ql ')) { if (text.startsWith('ql ')) {
e.preventDefault(); e.preventDefault();
} }
onPaste(e);
}, []); }, []);
const formatParams = (sub) => {
return {
...sub,
autoAddCron: isNil(sub?.autoAddCron) ? true : Boolean(sub?.autoAddCron),
autoDelCron: isNil(sub?.autoDelCron) ? true : Boolean(sub?.autoDelCron),
};
};
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
window.addEventListener('paste', onPaste); window.addEventListener('paste', onPaste);
@@ -239,7 +263,9 @@ const SubscriptionModal = ({
}, [visible]); }, [visible]);
useEffect(() => { useEffect(() => {
form.setFieldsValue(subscription || {}); form.setFieldsValue(
{ ...subscription, ...formatParams(subscription) } || {},
);
setType((subscription && subscription.type) || 'public-repo'); setType((subscription && subscription.type) || 'public-repo');
setScheduleType((subscription && subscription.schedule_type) || 'crontab'); setScheduleType((subscription && subscription.schedule_type) || 'crontab');
setPullType((subscription && subscription.pull_type) || 'ssh-key'); setPullType((subscription && subscription.pull_type) || 'ssh-key');
@@ -269,9 +295,9 @@ const SubscriptionModal = ({
confirmLoading={loading} confirmLoading={loading}
> >
<Form form={form} name="form_in_modal" layout="vertical"> <Form form={form} name="form_in_modal" layout="vertical">
<Form.Item name="name" label="名称"> <Form.Item name="name" label="名称" rules={[{ required: true }]}>
<Input <Input
placeholder="支持拷贝ql repo/raw命令,粘贴导入" placeholder="支持拷贝 ql repo/raw 命令,粘贴导入"
onPaste={onNamePaste} onPaste={onNamePaste}
/> />
</Form.Item> </Form.Item>
@@ -439,6 +465,37 @@ const SubscriptionModal = ({
</Form.Item> </Form.Item>
</> </>
)} )}
<Form.Item
name="proxy"
label="代理"
tooltip="公开仓库支持HTTP/SOCK5代理,私有仓库支持SOCK5代理"
>
<Input
placeholder={
type === 'private-repo'
? 'SOCK5代理,例如 IP:PORT'
: 'HTTP/SOCK5代理,例如 http://127.0.0.1:1080'
}
/>
</Form.Item>
<Form.Item style={{ marginBottom: 0 }} className="inline-form-item">
<Form.Item
name="autoAddCron"
label="自动添加任务"
valuePropName="checked"
initialValue={true}
>
<Switch />
</Form.Item>
<Form.Item
name="autoDelCron"
label="自动删除任务"
valuePropName="checked"
initialValue={true}
>
<Switch />
</Form.Item>
</Form.Item>
</Form> </Form>
</Modal> </Modal>
); );
+6 -1
View File
@@ -137,6 +137,10 @@ export default {
tip: 'PushDeer的Keyhttps://github.com/easychen/pushdeer', tip: 'PushDeer的Keyhttps://github.com/easychen/pushdeer',
required: true, required: true,
}, },
{
label: 'pushDeerUrl',
tip: 'PushDeer的自架API endpoint,默认是 https://api2.pushdeer.com/message/push',
},
], ],
bark: [ bark: [
{ {
@@ -292,7 +296,7 @@ export default {
documentTitleMap: { documentTitleMap: {
'/login': '登录', '/login': '登录',
'/initialization': '初始化', '/initialization': '初始化',
'/cron': '定时任务', '/crontab': '定时任务',
'/env': '环境变量', '/env': '环境变量',
'/subscription': '订阅管理', '/subscription': '订阅管理',
'/config': '配置文件', '/config': '配置文件',
@@ -301,6 +305,7 @@ export default {
'/log': '日志管理', '/log': '日志管理',
'/setting': '系统设置', '/setting': '系统设置',
'/error': '错误日志', '/error': '错误日志',
'/dependence': '依赖管理',
}, },
dependenceTypes: ['nodejs', 'python3', 'linux'], dependenceTypes: ['nodejs', 'python3', 'linux'],
}; };
+1 -1
View File
@@ -1 +1 @@
export const LOG_END_SYMBOL = '\n          '; export const LOG_END_SYMBOL = '     ';
-1
View File
@@ -56,7 +56,6 @@ _request.interceptors.request.use((url, options) => {
_request.interceptors.response.use(async (response) => { _request.interceptors.response.use(async (response) => {
const responseStatus = response.status; const responseStatus = response.status;
if ([502, 504].includes(responseStatus)) { if ([502, 504].includes(responseStatus)) {
message.error('服务异常,请稍后刷新!');
history.push('/error'); history.push('/error');
} else if (responseStatus === 401) { } else if (responseStatus === 401) {
if (history.location.pathname !== '/login') { if (history.location.pathname !== '/login') {
+67 -9
View File
@@ -1,4 +1,5 @@
import { LOG_END_SYMBOL } from './const'; import { LOG_END_SYMBOL } from './const';
import cron_parser from 'cron-parser';
export default function browserType() { export default function browserType() {
// 权重:系统 + 系统版本 > 平台 > 内核 + 载体 + 内核版本 + 载体版本 > 外壳 + 外壳版本 // 权重:系统 + 系统版本 > 平台 > 内核 + 载体 + 内核版本 + 载体版本 > 外壳 + 外壳版本
@@ -180,19 +181,15 @@ export default function browserType() {
*/ */
export function getTableScroll({ export function getTableScroll({
extraHeight, extraHeight,
id, target,
}: { extraHeight?: number; id?: string } = {}) { }: { extraHeight?: number; target?: HTMLElement } = {}) {
if (typeof extraHeight == 'undefined') { if (typeof extraHeight === 'undefined') {
// 47 + 40 + 12 // 47 + 40 + 12
extraHeight = 99; extraHeight = 99;
} }
let tHeader = null; let tHeader = null;
if (id) { if (target) {
tHeader = document.getElementById(id) tHeader = target;
? document
.getElementById(id)!
.getElementsByClassName('ant-table-thead')[0]
: null;
} else { } else {
tHeader = document.querySelector('.ant-table-wrapper'); tHeader = document.querySelector('.ant-table-wrapper');
} }
@@ -276,7 +273,68 @@ export function depthFirstSearch<
return c; return c;
} }
export function findNode<T extends Record<string, any> & { children?: T[] }>(
children: T[],
condition: (column: T) => boolean,
) {
const c = [...children];
let item;
function find(cls: T[] | undefined): T | undefined {
if (!cls) return;
for (let i = 0; i < cls?.length; i++) {
if (condition(cls[i])) {
item = cls[i];
} else if (cls[i].children) {
find(cls[i].children);
}
}
}
find(c);
return item;
}
export function logEnded(log: string): boolean { export function logEnded(log: string): boolean {
const endTips = [LOG_END_SYMBOL, '执行结束']; const endTips = [LOG_END_SYMBOL, '执行结束'];
return endTips.some((x) => log.includes(x)); return endTips.some((x) => log.includes(x));
} }
export function getCommandScript(
command: string,
): [string, string] | string | undefined {
const cmd = command.split(' ') as string[];
if (cmd[1] === 'repo' || cmd[1] === 'raw') {
return cmd[2];
}
let scriptsPart = cmd.find((x) =>
['.js', '.ts', '.sh', '.py'].some((y) => x.endsWith(y)),
);
if (!scriptsPart) return;
if (scriptsPart.startsWith('/ql/data/scripts')) {
scriptsPart = scriptsPart.replace('/ql/data/scripts/', '');
}
let p: string, s: string;
let index = scriptsPart.lastIndexOf('/');
if (index >= 0) {
s = scriptsPart.slice(index + 1);
p = scriptsPart.slice(0, index);
} else {
s = scriptsPart;
p = '';
}
return [s, p];
}
export function parseCrontab(schedule: string): Date {
try {
const time = cron_parser.parseExpression(schedule);
if (time) {
return time.next().toDate();
}
} catch (error) {}
return new Date('1970');
}
+1 -2
View File
@@ -1,9 +1,8 @@
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing'; import { Integrations } from '@sentry/tracing';
import { loader } from '@monaco-editor/react'; import { loader } from '@monaco-editor/react';
import { version } from '../version';
export function init() { export function init(version: string) {
// sentry监控 init // sentry监控 init
Sentry.init({ Sentry.init({
dsn: 'https://3406424fb1dc4813a62d39e844a9d0ac@o1098464.ingest.sentry.io/6122818', dsn: 'https://3406424fb1dc4813a62d39e844a9d0ac@o1098464.ingest.sentry.io/6122818',
-10
View File
@@ -1,10 +0,0 @@
export const version = '2.14.10';
export const changeLogLink = 'https://t.me/jiao_long/337';
export const changeLog = `2.14.10 版本说明
1.
2.
3. notify.js
4. task
5.
6. bug修复
`;
+2
View File
@@ -8,3 +8,5 @@ declare module '*.svg' {
const url: string; const url: string;
export default url; export default url;
} }
declare module 'pstree.remy';
+11
View File
@@ -0,0 +1,11 @@
version: 2.15.10
changeLogLink: https://t.me/jiao_long/365
changeLog: |
1. 增加 ipv6 自动检测
2. 修复订阅管理下载 http 协议文件
3. 修复日志删除逻辑
4. 修复自定义通知
5. 修复检查更新弹框逻辑
6. 修复 bark 推送声音,感谢 https://github.com/adams549659584
7. 修复 task 命令获取 token
8. 其他 bug 修复