Compare commits

...
Author SHA1 Message Date
whyour ddee69d291 更新版本 v2.10.13 2021-12-25 21:31:52 +08:00
whyour 28960c9fab 添加sentry-cli cdn 2021-12-25 20:55:13 +08:00
whyour 449301e519 修改热更新目录 2021-12-25 20:13:49 +08:00
whyour 28dab3dc2f 修改两步验证界面逻辑 2021-12-25 20:09:02 +08:00
whyour 7995d98db5 修改批量运行任务逻辑 2021-12-24 23:31:11 +08:00
whyour 1f319f3533 调试脚本增加停止,修复调试交互 2021-12-23 23:25:39 +08:00
whyour 7d2dbaa62d 修改文本 2021-12-23 13:39:16 +08:00
whyour ed5999526b 优化初始化通知设置交互 2021-12-21 23:49:52 +08:00
whyour 2409f75491 修复登录退出接口路径 2021-12-21 23:42:05 +08:00
whyour 0c840095d1 增加系统openapi 2021-12-21 23:22:34 +08:00
whyour 3eaf0b4856 system接口增加版本号,应用权限增加依赖管理 2021-12-21 22:53:19 +08:00
琚涛涛andGitHub 394735a5d9 优化定时任务名称展示效果,提升用户体验 (#1017) 2021-12-21 22:39:17 +08:00
whyour 5bfa10bfe7 修复任务跳转脚本参数,task运行脚本支持更多参数 2021-12-21 00:23:20 +08:00
whyour 3f12d9cbd5 更新版本号 v2.10.12 2021-12-09 23:24:10 +08:00
whyour 54d89754df 修复darkreader最新版bug 2021-12-09 23:22:17 +08:00
whyour 9470524615 更新readme 2021-12-06 23:42:14 +08:00
whyour b5faa6eaaa 修复logo样式 2021-12-06 11:18:19 +08:00
whyour eab476cfe8 更新版本号 v2.10.11 2021-12-05 23:15:49 +08:00
whyour b773def012 修复搜索环境变量urlencode 2021-12-05 23:04:26 +08:00
whyour a9f8b23a7a 修复内容区高度和logo样式 2021-11-29 23:02:42 +08:00
whyour 61bb6535fb 修复最后运行时长被覆盖 2021-11-29 21:49:32 +08:00
雪狐andGitHub eb0bdffe06 避免环境变量中出现"字符的时候提前截断字符串 (#966) 2021-11-29 21:43:29 +08:00
whyour e129485285 更新版本 v2.10.10 2021-11-27 20:43:18 +08:00
colinxuandGitHub aac7de172e 修复使用旧pushplus推送后结果解析失败的问题 (#958) 2021-11-27 20:33:49 +08:00
whyour e19d2412fc 检测更新增加强制更新 2021-11-27 20:19:49 +08:00
whyour 7cba2cf76c 依赖目录通知文件没有时,拷贝示例的通知文件 2021-11-27 17:41:26 +08:00
whyour 55d7a49dbc 修复deps映射目录 2021-11-27 17:19:50 +08:00
whyour f695864fd8 修复build args 2021-11-27 17:12:45 +08:00
whyour ae4883fbe7 测试yml 2021-11-27 17:05:55 +08:00
whyour 836dfd2861 添加deps目录软链 2021-11-27 16:20:24 +08:00
whyour 6e0523c6d7 修复删除JavaScript进程,修改依赖重新安装逻辑 2021-11-27 13:41:38 +08:00
whyour cd9ba084ae 修复调试运行时文件路径 2021-11-23 23:36:43 +08:00
phoenixandGitHub c8a0c0de67 fix nginx can not run without /run/nginx (#944)
alpine3.12 和 alpine3.14 兼容性问题
2021-11-23 18:32:22 +08:00
whyour 5dd8d9df91 更新版本 v2.10.9 2021-11-22 22:27:15 +08:00
whyour 2dbee94e85 默认拷贝deps目录下所有文件 2021-11-22 22:20:13 +08:00
风之凌殇andGitHub 9001797037 表格选择pageSize增加200/500/1000 (#934) 2021-11-22 19:26:02 +08:00
kilo5hzandGitHub 5e7104d5a3 系统通知增加gotify (#936) 2021-11-22 19:23:40 +08:00
whyour 7c33b22d63 修改docker基础镜像为alpine3.12,解决arm32无法启动 2021-11-22 19:22:39 +08:00
whyour b7cb1e379c 更新readme 2021-11-21 22:26:22 +08:00
whyour 274a6ce52e 修复调试功能 2021-11-20 01:06:25 +08:00
kilo5hzandwhyour 8079e4e7b8 点击任务名跳转脚本管理编辑脚本 (#929)
* 增加捷径:点击任务名跳转脚本管理编辑脚本

* 修正树节点选中,父节点自动展开
2021-11-20 01:06:25 +08:00
whyour 920efb8fab 更新readme图片链接 2021-11-20 01:06:25 +08:00
whyour 92aeb2b9da 更新readme 2021-11-20 01:06:25 +08:00
whyour ced76cf343 更新readme 2021-11-20 01:06:25 +08:00
whyour 3750d6ecd5 增加初始化依赖目录及文件 2021-11-20 01:06:25 +08:00
风之凌殇andwhyour 7444f30384 移除dockerfile中不必要的环境变量 (#925)
* 移除上次pr新增的不必要的环境变量
2021-11-20 01:06:16 +08:00
whyour b0fd5ba47f 更新readme 2021-11-17 19:22:40 +08:00
whyour cc098bc2e5 修复拷贝deps已存在依赖 2021-11-17 10:23:45 +08:00
whyour 1cfcf8d194 更新版本 v2.10.8 2021-11-16 14:36:13 +08:00
whyour d1e9f741d0 修复脚本管理删除、新建增加选择父目录 2021-11-16 14:18:02 +08:00
kilo5hzandGitHub 0cde9d07ee 增加gotify推送 (#905)
* 增加gotify推送
2021-11-15 23:04:55 +08:00
whyour a266425348 修复拷贝deps 2021-11-13 22:05:54 +08:00
whyour 6dbe02e321 修复dep目录 2021-11-13 21:54:45 +08:00
whyour 39979879c9 更新版本 v2.10.7 2021-11-13 21:21:35 +08:00
whyour ba3f604a5c 修复bot安装依赖判断 2021-11-13 20:58:27 +08:00
whyour 9572582401 修改拷贝依赖 2021-11-13 19:05:34 +08:00
whyour 2bcef0ba75 修复脚本管理搜索 2021-11-13 17:42:06 +08:00
whyour 31cd7c4007 外部依赖改为系统直接分发通知文件js和py版 2021-11-13 17:21:27 +08:00
whyour 1c4c1799b0 修复环境变量表格样式 2021-11-13 16:44:21 +08:00
whyour 2d558df15e 修复schedule服务 2021-11-13 12:34:49 +08:00
whyour 7b769da4e2 修复脚本管理树结构 2021-11-13 11:14:43 +08:00
whyour 4e95d62933 更新版本 v2.10.6 2021-11-13 00:03:12 +08:00
whyour 83255280b9 修复检查更新 2021-11-12 23:55:57 +08:00
whyour 2546cebdea 修复定时匹配 2021-11-12 23:42:11 +08:00
whyour f14a3fe0f1 修复脚本管理数据 2021-11-12 23:18:24 +08:00
whyour c3eb845be4 修改workflow变量 2021-11-12 22:09:02 +08:00
MiniersandGitHub 1eef77dca5 增加环境变量更新时间 (#887) 2021-11-12 21:55:03 +08:00
风之凌殇andGitHub d7d9f47fe5 增加在添加语义化版本tag时自动生成static并构建多个不同tag的容器的workflow,简化版本发布流程 (#888) 2021-11-12 21:54:42 +08:00
whyour fce6a996d9 更新版本号 v2.10.5 2021-11-12 14:23:16 +08:00
whyour 48a484d7de 修复文件判断引起服务异常 2021-11-12 14:22:21 +08:00
whyour 4b81d3c612 更新版本号 2021-11-12 13:18:32 +08:00
whyour 21b6edd7eb 修复entrypoint启动失败 2021-11-12 13:16:53 +08:00
whyour 9a43e022e9 修改版本说明序号 2021-11-11 23:04:29 +08:00
whyour 62bef3f13d 更新版本号 v2.10.3 2021-11-11 23:03:28 +08:00
whyour c87f5fa004 服务重启时依赖改为单独安装,避免同时失败 2021-11-11 22:56:47 +08:00
whyour 8d99f3ffbb 修复获取日志判断 2021-11-11 22:50:28 +08:00
whyour 100ca7b54b 单个数据库文件大于500兆时,裁剪文件,避免服务无法启动 2021-11-11 22:41:12 +08:00
whyour 4a2624f12a 脚本管理改成树结构 2021-11-11 00:10:05 +08:00
whyour 21b01a85a8 修复pushplus通知设置 2021-11-10 23:33:19 +08:00
whyour e039ae436e 修复依赖文件获取 2021-11-10 23:26:21 +08:00
whyour b13a1a45f8 修复repo依赖路径 2021-11-10 10:35:53 +08:00
whyour 667fe2bbba 修复shell语法 2021-11-09 23:55:50 +08:00
whyour f083871ca0 修复repo文件比对 2021-11-09 23:23:07 +08:00
whyour 782e1ba235 修改ql repo逻辑 2021-11-09 23:07:43 +08:00
whyour b92c6edc4e 添加动态引入 2021-11-09 22:34:24 +08:00
whyour bdf4657c73 修改退出下拉样式 2021-11-08 22:56:53 +08:00
whyour 8559438611 修改数据库加载 2021-11-08 22:47:46 +08:00
whyour 04045fd410 修复centos -x兼容性 2021-11-07 21:54:31 +08:00
whyour 1c71a9d8a8 回退shell [ 2021-11-07 19:18:36 +08:00
whyour 9a9ae9782c 修改pnpm安装源 2021-11-07 12:51:10 +08:00
whyour da2569850b 修复entrypoint 2021-11-07 11:25:18 +08:00
whyour b475035415 修复 arm 没有nginx conf.d 目录 2021-11-07 11:08:21 +08:00
OreoandGitHub 7baee27585 更新 Python 通知文件 (#832)
已在 Python3.6.x(云函数平台),3.8.x,3.10.0 测试通过
特性如下
1. 支持 11 种推送方式,未来还会继续添加
2. 多线程通知
3. 支持一言(一句话)和控制台输出开关
4. 对于现有推送方式,补充了配置变量
2021-11-06 20:45:53 +08:00
whyour 2cb2f1ce0a 更新dockerfile 2021-11-06 20:19:07 +08:00
whyour 8c1407e2a9 修改dockerfile 2021-11-06 19:04:01 +08:00
whyour 7bcb113085 更新docker yml 2021-11-06 18:58:18 +08:00
whyour 42dc27addf 修复任务日志结束时间展示 2021-11-06 00:50:44 +08:00
whyour aac30398c4 修复toLocaleString 0点显示24点 2021-11-05 23:56:16 +08:00
whyour ceae888372 修改shell语法 2021-11-05 23:39:22 +08:00
whyour 61df7050db 修复移动端展开导航 2021-11-04 19:24:55 +08:00
whyour af5561cbbf 更新版本号 v2.10.2 2021-11-04 18:10:43 +08:00
whyour bce30f922d 修复基础镜像pip3依赖 2021-11-04 18:10:14 +08:00
whyour d6ed394557 修复openapi路由 2021-11-04 18:07:34 +08:00
whyour 7b512a1681 更新版本号 v2.10.1 2021-11-04 15:09:07 +08:00
whyour 7af0597cd5 添加白屏时间及错误上报 2021-11-04 15:05:34 +08:00
whyour 2f5cf6d93c 修复check命令 2021-11-03 16:35:18 +08:00
whyour d672d2d104 修改失败任务日志 2021-11-01 23:06:23 +08:00
whyour c0d1fb1ef4 shell 中 [ 改为 [[ 2021-11-01 23:04:10 +08:00
whyour 3c882ad0ce 移动退出登录位置 2021-11-01 15:23:06 +08:00
whyour 58acd2eb68 修复表格滚动高度 2021-10-31 19:45:51 +08:00
whyour 590473e7d2 修复无法覆盖安装依赖 2021-10-31 17:34:19 +08:00
21139157c8 修改错误文字 (#809)
Co-authored-by: 14K <9112790+the14k@user.noreply.gitee.com>
2021-10-29 23:26:44 +08:00
whyour 8f8296ab64 支持服务端端口直接访问前端 2021-10-28 23:35:21 +08:00
whyour 40bf07d2a4 修复置顶后编辑置顶消失 2021-10-28 22:38:34 +08:00
whyour fd2b4fb86d 更新版本号 v2.10.0 2021-10-24 17:51:20 +08:00
whyour 783e867ef6 修复python移除依赖命令 2021-10-24 17:19:14 +08:00
whyour 3873d9f978 添加依赖管理 2021-10-24 17:07:16 +08:00
whyour 7dece938c8 依赖安装日志增加安装时间 2021-10-24 11:11:23 +08:00
whyour 0842d66c79 修复表格初次获取滚动高度 2021-10-23 23:44:11 +08:00
whyour d071e50d62 由于浏览器兼容性,移除dynamicImport 2021-10-23 23:31:46 +08:00
whyour 337f01aff1 修复python3任务无法停止 2021-10-23 23:22:08 +08:00
whyour 5614e41eb0 修复service引入路径 2021-10-23 18:46:24 +08:00
whyour 4f0fdbe453 初始化安装所有依赖 2021-10-23 18:44:25 +08:00
whyour 49c068db78 修改依赖安装结束标识 2021-10-23 18:27:20 +08:00
whyour fbba44d482 添加依赖管理 2021-10-23 18:23:32 +08:00
whyour 10c07b0a09 两步验证输入框自动聚焦 2021-10-21 22:25:15 +08:00
whyour 20893698ba 更新版本号 v2.9.10 2021-10-21 21:59:24 +08:00
longjuanandGitHub 31de659ed3 更新install.md (#789) 2021-10-21 21:55:26 +08:00
whyour 6c08a16c66 修改websocket日志 2021-10-21 21:52:06 +08:00
whyour 21e7178f48 修复table滚动高度 2021-10-21 19:13:38 +08:00
whyour aefd267eb2 更新monaco 2021-10-19 22:41:59 +08:00
whyour fb1a3980d4 检查密码默认值 2021-10-19 21:46:24 +08:00
whyour cfc467f8a5 修复tg系统通知认证参数 2021-10-18 19:18:46 +08:00
whyour 48120f012d 修复检查更新日志展示 2021-10-18 17:05:38 +08:00
whyour c907a78437 修复shell变量重复 2021-10-16 19:32:23 +08:00
whyour 915c0d09a9 升级umi 2021-10-16 17:21:19 +08:00
whyour 80638862cc 添加 yarn.lock 2021-10-16 17:07:05 +08:00
whyour a7350948cd 更新版本号 v2.9.9 2021-10-16 15:19:53 +08:00
whyour 1e1be013b5 脚本管理移动端支持添加脚本、编辑脚本 2021-10-16 15:11:23 +08:00
hanhh 4062584eae 移除无用代码 2021-10-16 13:39:18 +08:00
hanhh abedddffa6 修复表格样式 2021-10-16 10:53:14 +08:00
hanhh 38858ccdb2 修改pm2任务日志 2021-10-15 22:32:32 +08:00
hanhh d56e6359d0 修复更新日志匹配正则 2021-10-15 22:14:42 +08:00
hanhh 5886991464 放开ql update删除操作 2021-10-15 22:01:34 +08:00
hanhh 838326616b 更新版本号 v2.9.8-001 2021-10-15 11:47:32 +08:00
hanhh fcf80fe946 修复设置删除日志 2021-10-15 10:53:27 +08:00
phoenixandGitHub 2e63a7056c 修复nginx.conf错误 (#776)
Fix nginx: [emerg] unknown directive.
Log
`nginx: [emerg] unknown directive "client_body_buffer_size:" in /etc/nginx/nginx.conf:18
`
2021-10-15 10:40:58 +08:00
hanhh 7c807e3ec8 更新版本 2.9.8 2021-10-14 22:07:21 +08:00
hanhh b558aa0934 修复初始化流程 2021-10-14 22:00:36 +08:00
hanhh 38757758cb 增加日志删除频率设置 2021-10-14 10:58:54 +08:00
hanhh 84dd3d8fd1 修改版本更新执行命令 2021-10-13 23:50:25 +08:00
hanhh 19423c2d30 添加ql -l update 2021-10-13 23:45:46 +08:00
hanhh 98bba7d7fa 更新nginx配置文件 2021-10-13 23:31:38 +08:00
hanhh dcaa9ce000 修复websocket连接域名 2021-10-13 23:09:48 +08:00
hanhh 3cae44a136 修复版本号匹配正则 2021-10-13 22:53:10 +08:00
hanhh df8a3d2692 修复版本号匹配 2021-10-13 22:45:24 +08:00
hanhh 56e43f1808 移除远程版本更新链接 2021-10-13 22:41:59 +08:00
hanhh 2f6b95cccf 修复获取最后版本文件 2021-10-13 22:39:59 +08:00
hanhh ad9f6b2b64 修复获取最新版本号 2021-10-13 22:05:54 +08:00
hanhh 165c3dc976 容器启动忽略config和env中的错误 2021-10-13 14:25:38 +08:00
hanhh cbe6390b41 修复版本检查 2021-10-12 23:06:28 +08:00
hanhh 026eaf6606 完善检查更新操作 2021-10-12 22:54:17 +08:00
hanhh ee184f8e62 并发命令支持指定账号 2021-10-12 21:58:41 +08:00
hanhh 5745231dc9 添加系统更新操作和设置删除日志频率 2021-10-12 00:27:42 +08:00
hanhh cbdc3a9861 添加检查更新和删除日志频率操作 2021-10-08 23:02:45 +08:00
hanhh ba5c42ad6d 修复表格滚动样式和交互 2021-10-08 22:39:17 +08:00
hanhh b056a731a1 增加新版本检测 2021-10-07 15:26:25 +08:00
hanhh b4cd94f2d9 增加环境变量名称正则校验 2021-10-06 16:38:46 +08:00
hanhh 3b8cb43f02 添加新版本提醒 2021-10-06 16:22:53 +08:00
hanhh 82cb11c3c4 更新版本号 v2.9.7 2021-10-03 22:26:16 +08:00
SowevoandGitHub eb7522a90b 增加企业微信机器人推送的python版本 (#748) 2021-10-03 22:16:07 +08:00
hanhh 3898201b1e 修复登录两步提醒样式 2021-10-03 22:05:56 +08:00
hanhh bb6ab0c17b 修改初始化判断条件 2021-10-03 22:01:44 +08:00
hanhh 652c40ab1f 增加初始化过程 2021-10-03 20:58:55 +08:00
hanhh ca0d6215a9 修复entrypoint启动nginx配置 2021-09-30 23:49:19 +08:00
hanhh 61d155257c ql update添加patch 2021-09-30 19:14:40 +08:00
hanhh 91399f1a1f 移除check中init git 2021-09-30 19:08:13 +08:00
hanhh 2e738c5977 更新版本号 v2.9.6 2021-09-29 23:16:59 +08:00
hanhh c0bd51cf03 修复类型检查 2021-09-29 23:15:09 +08:00
hanhh ca5afe04f8 修复退出登录获取token出错 2021-09-29 23:09:13 +08:00
hanhh 47db0b9f4e 对比工具支持对比config目录下的文件 2021-09-29 22:48:49 +08:00
hanhh 27f17a377a 重启服务时自动压缩db 2021-09-29 21:40:35 +08:00
hanhh ecaf731ecf 修复loading状态位置 2021-09-29 21:19:25 +08:00
hanhh 9fba2a85f6 修复清空文件操作 2021-09-27 22:04:47 +08:00
hanhh 93f75be182 task和ql命令添加基础目录检测 2021-09-27 19:38:06 +08:00
hanhh d537901f59 测试动态加载 2021-09-25 12:11:13 +08:00
hanhh 260c1ed4a4 修改定时任务列表滚动宽度 2021-09-25 11:40:55 +08:00
hanhh ac2bd52463 修复status接口参数 2021-09-25 02:00:50 +08:00
hanhh 16c6f8fee1 修复并发参数 2021-09-25 01:51:18 +08:00
hanhh 8a0db21f17 更新版本号 2021-09-25 00:43:05 +08:00
hanhh 9ab08a71c0 增加登录设备展示 2021-09-25 00:42:03 +08:00
hanhh 381c32a3b4 修复更新任务状态api 2021-09-25 00:28:18 +08:00
hanhh 350c0f10c4 修复ql resettfa命令 2021-09-25 00:15:23 +08:00
hanhh d5b982f813 ql resettfa增加兼容字段 2021-09-24 23:33:52 +08:00
hanhh 6587375ff2 移除定时任务左右列固定n 2021-09-24 23:31:50 +08:00
hanhh f5ecb81bad ql bot移除自动更新远程仓库 2021-09-24 23:22:40 +08:00
hanhh 0141cd81c4 修复服务端ts类型检查 2021-09-24 22:49:14 +08:00
hanhh ff412c7497 更新版本号 2021-09-24 22:30:09 +08:00
hanhh 623f239f91 修复定时任务操作宽度 2021-09-24 22:28:25 +08:00
hanhh c7ba304c6a 支持多端登录 2021-09-24 22:24:39 +08:00
hanhh c8393604d1 修复task运行指定账号 2021-09-24 00:24:54 +08:00
hanhh c5d47a67aa 修复定时任务列表下次运行时间 2021-09-24 00:11:14 +08:00
hanhh 3a1423cd59 task支持指定账号运行 2021-09-23 23:59:53 +08:00
hanhh d57f4cf6cd 修改定时任务表格布局 2021-09-23 22:12:19 +08:00
hanhh af420942c4 添加最后运行时间和运行时长 2021-09-23 13:42:32 +08:00
hanhh eac4e5e66f 修复主题切换全局class未变化 2021-09-21 23:29:38 +08:00
hanhh b8822a98f7 定时任务添加下次运行时间 2021-09-21 20:11:39 +08:00
hanhh 95322b42c0 修改登录通知消息 2021-09-20 23:56:20 +08:00
hanhh 33f5a06d32 修复pushplus通知参数 2021-09-20 22:23:56 +08:00
hanhh a7e5126c92 修复新建文件 2021-09-20 00:59:38 +08:00
hanhh c62f3e5b8e 添加bak目录ignore 2021-09-20 00:47:58 +08:00
hanhh 5d3f413a05 修改测试通知内容 2021-09-19 23:10:58 +08:00
hanhh 644154ca38 更新版本号 2021-09-19 23:00:53 +08:00
hanhh cccce5ff74 修复通知 2021-09-19 23:00:21 +08:00
hanhh aa1fe98188 定时任务增加置顶功能 2021-09-19 22:25:57 +08:00
hanhh b6a78550d6 添加全局主题className 2021-09-19 21:27:20 +08:00
hanhh 70b7f2c493 脚本管理支持新增文件 2021-09-19 21:23:03 +08:00
hanhh ea44e5f209 修复pageTitle 2021-09-18 23:27:50 +08:00
hanhh 41ef87d9f8 任务详情日志添加loading 2021-09-18 16:33:12 +08:00
hanhh c0b4a70aa0 添加全局loading 2021-09-18 15:08:45 +08:00
hanhh 955875cb7a 修复关闭通知 2021-09-18 02:09:06 +08:00
hanhh 827e8571aa 添加通知设置页面 2021-09-18 01:44:55 +08:00
whyour aed08b0f59 添加通知api 2021-09-17 18:06:14 +08:00
hanhh 3ddaf41e27 更新通知服务 2021-09-17 00:35:27 +08:00
whyour 38a9cce6bf 更新通知服务 2021-09-16 21:01:29 +08:00
whyour ae255ef381 添加通知服务 2021-09-16 18:01:25 +08:00
prcxuandGitHub 4316bef01e 更新安装说明 (#705) 2021-09-15 11:18:58 +08:00
JH2628andGitHub 8491c5706d 修复config sample序号 (#694) 2021-09-14 19:25:00 +08:00
hanhh 7b7fab1f32 修改系统异常提醒 2021-09-13 00:12:44 +08:00
hanhh 5a6fccdc2f 更新tg链接 2021-09-12 20:28:43 +08:00
hanhh 23668be46d 更新版本号 2021-09-11 13:53:58 +08:00
hanhh f92f2c1666 修复写入任务错误日志逻辑 2021-09-11 13:53:24 +08:00
hanhh 895740a3cb 修改获取用户数据逻辑 2021-09-11 02:03:31 +08:00
hanhh 4c76c3231f 移除自动压缩db数据 2021-09-09 13:02:49 +08:00
hanhh ca9d3f9559 更新版本号 2021-09-08 12:26:21 +08:00
hanhh 3ca3c5e75c 修复开启两步失败提醒 2021-09-08 12:26:05 +08:00
hanhh c2c0b1dfdc 添加应用名称改为选填 2021-09-08 12:15:30 +08:00
hanhh c633774487 修复两步验证逻辑 2021-09-08 12:13:19 +08:00
hanhh c2d86fc517 更新版本号 2021-09-07 23:53:01 +08:00
hanhh d7c8d150ca 修改登陆日志title 2021-09-07 23:52:40 +08:00
L1n3andGitHub 6b8a4e65f2 添加安装说明 (#667) 2021-09-07 17:46:53 +08:00
LeonandGitHub 482e43dd1f 修复 Python 和 JavaScript Server酱和企业微信推送的环境变量不一致 (#673) 2021-09-07 17:46:14 +08:00
hanhh 1437babc6c 增加登录日志 2021-09-07 01:40:41 +08:00
Zy143LandGitHub 5ed882943a 修复python和JavaScript钉钉推送的环境变量不一致 (#659) 2021-09-05 23:53:18 +08:00
hanhh e678b3fb3a config文件读取添加黑名单 2021-09-05 23:48:58 +08:00
hanhh a5770bc4e4 添加环境变量命令input改为textarea 2021-09-05 23:38:36 +08:00
hanhh 59821bb939 修改ql check依赖安装逻辑 2021-09-03 22:30:24 +08:00
hanhh b58040be44 修改两步验证逻辑 2021-09-03 19:09:09 +08:00
hanhh be3b8ad52e 修复环境变量拖拽 2021-09-02 19:16:28 +08:00
hanhh 0fdaf97461 更新两步验证逻辑 2021-09-01 23:03:27 +08:00
L1n3andGitHub f5f036372f 添加 docker-compose.yml (#629) 2021-08-31 15:52:24 +08:00
hanhh 3fceb983e6 增加上次登录状态 2021-08-31 15:29:23 +08:00
hanhh ae6a39196d 增加ql resettfa禁用两步验证 2021-08-31 11:06:20 +08:00
hanhh 2810ff2753 更新版本号 2021-08-31 10:56:37 +08:00
hanhh 205c0cf8d3 两步验证失败增加ip记录 2021-08-31 10:54:46 +08:00
hanhh d9c801a091 添加两步验证 2021-08-30 23:37:26 +08:00
hanhh 0833aa3b30 增加nedb数据自定比对,测试对db文件不断增大的影响 2021-08-29 00:52:21 +08:00
hanhh 9c90bbc146 修复移动端无法打开脚本管理、任务日志 2021-08-29 00:34:47 +08:00
hanhh 231b387076 更换ip地址查询接口 2021-08-27 14:00:11 +08:00
hanhh 3de432652f 更换ip查询接口 2021-08-27 13:27:32 +08:00
hanhh cad714493e 更新版本号 2021-08-27 11:08:08 +08:00
hanhh 6a690a6108 更新nginx配置 2021-08-27 11:04:31 +08:00
hanhh 1b85c52557 修改新建应用placeholder 2021-08-27 10:52:49 +08:00
hanhh b37be92ea9 搜索时去掉前后空格 2021-08-27 10:47:30 +08:00
HeroWongandGitHub f95979f445 fix: 只有一页时不隐藏分页栏,确保用户可以更改 pagesize (#613) 2021-08-26 21:27:22 +08:00
hanhh 09e07218ae 添加openapi模块 2021-08-26 19:01:39 +08:00
hanhh 085f5a9451 更新版本号 2021-08-25 23:55:16 +08:00
hanhh eb4e935554 修复cron分隔符,修复登录获取ip逻辑 2021-08-25 23:54:42 +08:00
hanhh 57986cf425 修复登录倒计时颜色 2021-08-25 18:58:45 +08:00
hanhh 7bf14b9acc 更新版本号 2021-08-25 18:20:29 +08:00
hanhh 3ba0be34d9 修复6位cron判断 2021-08-25 18:19:22 +08:00
hanhh 0f828a3c75 修复群辉update之后403 2021-08-25 17:58:21 +08:00
hanhh 16a768a142 更新版本号 2021-08-24 18:21:19 +08:00
hanhh f380d20279 添加重置登录错误次数命令 2021-08-24 18:20:36 +08:00
hanhh be908ab36c 添加默认定时规则配置 2021-08-23 19:10:57 +08:00
hanhh b545454537 task传递参数给task_before和task_after 2021-08-23 15:58:25 +08:00
hanhh a5ff8dd1d6 更新readme图片 2021-08-23 15:31:13 +08:00
hanhh 59f23e518b 修复readme图片 2021-08-23 15:26:30 +08:00
hanhh 858b3f0176 更新版本号 2021-08-23 13:50:44 +08:00
hanhh 89b8494ad0 修复获取ip api失败时,无法登陆
修复禁用或者运行定时任务出现重复
2021-08-23 13:44:54 +08:00
hanhh 64ecd75bd5 修复定时任务搜索后页码 2021-08-23 12:29:29 +08:00
hanhh 138eb3393d 修复error_log文件读取 2021-08-23 11:41:34 +08:00
hanhh 1ad51d2925 更新版本号 2021-08-23 10:47:11 +08:00
hanhh 317e175bca 回退update.sh 2021-08-23 10:43:26 +08:00
hanhh 0e287aa7d1 修复task_before eval 2021-08-22 22:33:07 +08:00
hanhh 28018b705b 修复未产生日志文件cat报错 2021-08-22 22:02:31 +08:00
hanhh 46ec72d6be ql命令增加-l参数 2021-08-22 21:57:10 +08:00
hanhh 82494a87af 修改task.sh任务顺序 2021-08-22 20:54:40 +08:00
hanhh 95c19b447a 修复task语法 2021-08-22 18:57:52 +08:00
hanhh 9c6eae936a 修复task.sh执行语法 2021-08-22 18:21:31 +08:00
hanhh 77605a9360 修改task.sh中参数名 2021-08-22 16:05:58 +08:00
hanhh 82880c3f06 修复最后登录ip读取 2021-08-22 15:25:16 +08:00
hanhh 11143c771b 更新版本号 2021-08-22 15:23:58 +08:00
hanhh 0968eaa5a8 修复任务执行参数出错 2021-08-22 15:23:12 +08:00
hanhh 0687f9d472 修改登陆成功提示文本 2021-08-22 14:47:07 +08:00
hanhh 25f0f5951a 更新版本号 2021-08-22 14:07:43 +08:00
hanhh eec0ce65ad 修复获取访问ip逻辑 2021-08-22 14:07:00 +08:00
hanhh be4e100168 更新版本号 2021-08-22 12:59:54 +08:00
hanhh 59e4f1063c 修复登录重试判断 2021-08-22 12:59:34 +08:00
hanhh 359e21f02b 登录增加错误延迟提醒 2021-08-22 12:28:45 +08:00
hanhh 6894bd9c0e 登录增加最后登录时间地点,登录失败增加重试间隔 2021-08-22 01:22:23 +08:00
hanhh 7040f02823 默认定时改为随机时间 2021-08-21 19:26:37 +08:00
hanhh 41408de337 修复更新环境变量备注无法删除 2021-08-21 18:34:53 +08:00
hanhh 72e243dce8 增加命令行运行日志实时打印 2021-08-21 17:26:41 +08:00
hanhh 77d4840e2a 定时任务,环境变量添加状态筛选 2021-08-20 16:31:24 +08:00
hanhh 0d5961821c 更新版本号 2021-08-20 16:17:04 +08:00
hanhh f34980b173 移除console 2021-08-20 14:26:12 +08:00
hanhh 5b21a6e529 手动运行任务添加错误捕获 2021-08-20 10:59:59 +08:00
hanhh 6dbb737d4b 修复脚本管理编辑交互,修复ico无法加载 2021-08-19 23:46:26 +08:00
hanhh 76ecfbaaa2 更新版本号 2021-08-19 14:47:07 +08:00
hanhh d9aacbcb92 定时任务支持任务名,定时,命令排序
环境变量支持名称排序
2021-08-19 14:45:42 +08:00
hanhh e088325d95 脚本管理支持编辑删除文件 2021-08-19 14:18:25 +08:00
hanhh b29dd2c580 修复未勾选拆分环境变量参数验证失败 2021-08-17 22:31:39 +08:00
hanhh 89e82fd5b0 修复未勾选拆分环境变量参数验证失败 2021-08-17 22:26:33 +08:00
hanhh 9f0b671d42 修复手动运行任务进程 2021-08-17 22:06:04 +08:00
hanhh 7de7c724dc 更新版本号 2021-08-17 18:35:53 +08:00
hanhh 3525b1d92a 支持批量创建同名称环境变量,日志和脚本管理PC页支持宽度拖拽 2021-08-17 18:34:55 +08:00
hanhh 30cea84886 修改暗黑模式默认值 2021-08-16 18:05:52 +08:00
DerekandGitHub 0c2cc7f575 更正ql"本脚本用法"中的序号 (#552) 2021-08-16 17:17:45 +08:00
115 changed files with 20072 additions and 11299 deletions
-34
View File
@@ -1,34 +0,0 @@
name: BuildStatic
on:
push:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '14'
- name: build front and back
run: |
yarn install
yarn build
yarn build-back
- name: copy to static repo
env:
# GITHUB_REPO: gitee.com/whyour/qinglong-static
GITHUB_REPO: github.com/whyour/qinglong-static
run: |
mkdir -p static
cd ./static
cp -rf ../dist ./ && cp -rf ../build ./
git init && git add .
git config user.name "whyour"
git config user.email "imwhyour@gmail.com"
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
git push --force --quiet "https://whyour:${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" master:master
+115
View File
@@ -0,0 +1,115 @@
name: Build And Push Docker Image
on:
push:
branches:
- 'master'
- 'develop'
# Sequence of patterns matched against refs/tags
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
schedule:
# 参考 https://jasonet.co/posts/scheduled-actions/
# note: 这里是GMT时间,北京时间减去八小时即可。如北京时间 22:30 => GMT 14:30
# minute hour day month dayOfWeek
- cron: '00 14 * * *' # GMT 14:00 => 北京时间 22:00
#- cron: '30 16 * * *' # GMT 16:30(前一天) => 北京时间 00:30
workflow_dispatch:
jobs:
build-static:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '14'
- name: build front and back
run: |
yarn install
yarn build
yarn build-back
- name: copy to static repo
env:
GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static
GITHUB_BRANCH: ${{ github.ref_name }}
run: |
mkdir -p static
cd ./static
cp -rf ../dist ./ && cp -rf ../build ./
git init -b ${GITHUB_BRANCH} && git add .
git config --local user.name 'github-actions[bot]'
git config --local user.email 'github-actions[bot]@users.noreply.github.com'
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}
build:
needs: build-static
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v2
- name: Set time zone
uses: szenius/set-timezone@v1.0
with:
timezoneLinux: "Asia/Shanghai"
timezoneMacos: "Asia/Shanghai"
timezoneWindows: "China Standard Time"
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v3
with:
images: |
${{ github.repository }}
ghcr.io/${{ github.repository }}
# generate Docker tags based on the following events/attributes
# nightly, master, pr-2, 1.2.3, 1.2, 1
tags: |
type=schedule,pattern=nightly
type=edge
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
build-args: |
MAINTAINER=${{ github.repository_owner }}
QL_BRANCH=${{ github.ref_name }}
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x
context: docker/
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
-7
View File
@@ -1,13 +1,6 @@
name: PublishDockerImage name: PublishDockerImage
on: on:
workflow_dispatch: workflow_dispatch:
# workflow_run:
# workflows:
# - BuildStatic
# branches:
# - master
# types:
# - completed
jobs: jobs:
build: build:
+3 -1
View File
@@ -4,7 +4,7 @@
/node_modules /node_modules
/npm-debug.log* /npm-debug.log*
/yarn-error.log /yarn-error.log
/yarn.lock /pnpm-lock.yaml
/package-lock.json /package-lock.json
# production # production
@@ -21,9 +21,11 @@
/.env.local /.env.local
.env .env
.history .history
.version.ts
/config /config
/log /log
/db /db
/manual_log /manual_log
/scripts /scripts
/bak
+1
View File
@@ -0,0 +1 @@
sentrycli_cdnurl=https://cdn.npm.taobao.org/dist/sentry-cli
+13 -7
View File
@@ -8,11 +8,15 @@ export default defineConfig({
type: 'none', type: 'none',
}, },
fastRefresh: {}, fastRefresh: {},
favicon: 'https://qinglong.whyour.cn/g5.ico', dynamicImport: {
loading: '@/components/pageLoading',
},
favicon: '/images/g5.ico',
proxy: { proxy: {
'/api': { '/api': {
target: 'http://127.0.0.1:5678/', target: 'http://127.0.0.1:5600/',
changeOrigin: true, changeOrigin: true,
ws: true,
}, },
}, },
chainWebpack: (config) => { chainWebpack: (config) => {
@@ -30,14 +34,16 @@ export default defineConfig({
'react-dom': 'window.ReactDOM', 'react-dom': 'window.ReactDOM',
darkreader: 'window.DarkReader', darkreader: 'window.DarkReader',
codemirror: 'window.CodeMirror', codemirror: 'window.CodeMirror',
'sockjs-client': 'window.SockJS',
}, },
scripts: [ scripts: [
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js', 'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js', 'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
'https://cdn.jsdelivr.net/npm/darkreader@4.9.34/darkreader.min.js', 'https://cdn.jsdelivr.net/npm/darkreader@4.9.40/darkreader.min.js',
'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/lib/codemirror.min.js', 'https://cdn.jsdelivr.net/npm/codemirror@5/lib/codemirror.min.js',
'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/mode/shell/shell.js', 'https://cdn.jsdelivr.net/npm/codemirror@5/mode/shell/shell.js',
'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/mode/python/python.js', 'https://cdn.jsdelivr.net/npm/codemirror@5/mode/python/python.js',
'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/mode/javascript/javascript.js', 'https://cdn.jsdelivr.net/npm/codemirror@5/mode/javascript/javascript.js',
'https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js',
], ],
}); });
+145 -23
View File
@@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<a href="https://github.com/whyour/qinglong"> <a href="https://github.com/whyour/qinglong">
<img width="150" src="https://qinglong.whyour.cn/qinglong.png"> <img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
</a> </a>
</p> </p>
@@ -8,12 +8,10 @@
<div align="center"> <div align="center">
Python/JavaScript/Shell/Typescript 定时任务管理 支持python3、javaScript、shell、typescript 定时任务管理面板(A timed task management panel that supports typescript, javaScript, python3, and shell.
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url] [![donate][donate-image]][donate-url] [![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
[donate-image]: https://img.shields.io/badge/donate-wechat-green?style=flat
[donate-url]: https://qinglong.whyour.cn/nice.png
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat [docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong [docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat [docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
@@ -24,27 +22,151 @@ Python/JavaScript/Shell/Typescript 定时任务管理
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong [docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
</div> </div>
<p align="center"> [![](https://z3.ax1x.com/2021/11/18/I7KrTg.jpg)](https://whyour.cn)
<img width="49%" src="https://qinglong.whyour.cn/login.png">
<img width="49%" src="https://qinglong.whyour.cn/home.png"> 简体中文 | [English](./README-en.md)
</p>
## 功能
- 支持多种脚本语言(python3、javaScript、shell、typescript
- 支持在线管理脚本、环境变量、配置文件
- 支持在线查看任务日志
- 支持秒级任务设置
- 支持系统级通知
- 支持暗黑模式
- 支持手机端操作
## 部署
### 本机部署
```bash
# 待完善
```
### docker 部署
1. docker 安装
```bash
sudo curl -sSL get.docker.com | sh
```
2. 启动容器
```bash
docker run -dit \
-v $PWD/ql/config:/ql/config \
-v $PWD/ql/log:/ql/log \
-v $PWD/ql/db:/ql/db \
-v $PWD/ql/repo:/ql/repo \
-v $PWD/ql/raw:/ql/raw \
-v $PWD/ql/scripts:/ql/scripts \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
whyour/qinglong:latest
```
### docker-compose 部署
1. docker-compose 安装
```bash
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
```
2. 启动容器
```bash
mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker-compose.yml
# 启动
docker-compose up -d
# 停止
docker-compose down
```
## 使用
1. 内置命令
```bash
# 更新并重启青龙
ql update
# 运行自定义脚本extra.sh
ql extra
# 添加单个脚本文件
ql raw <file_url>
# 添加单个仓库的指定脚本
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch>
# 删除旧日志
ql rmlog <days>
# 启动tg-bot
ql bot
# 检测青龙环境并修复
ql check
# 重置登录错误次数
ql resetlet
# 禁用两步登录
ql resettfa
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
task <file_path>
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
task <file_path> now
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
task <file_path> conc <env_name> <account_number>(可选的)
# 指定账号执行,无论是否设置了随机延迟,均立即运行
task <file_path> desi <env_name> <account_number>
```
2. 参数说明
* file_url: 脚本地址
* repo_url: 仓库地址
* whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串
* blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串
* dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响
* branch: 拉取仓库的分支
* days: 需要保留的日志的天数
* file_path: 任务执行时的文件路径
* env_name: 任务执行时需要并发或者指定时的环境变量名称
* account_number: 任务执行时指定某个环境变量需要执行的账号序号
## 链接
- [nevinee](https://gitee.com/evine)
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/)
- [Umijs3.0](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server)
## 开发
```bash
$ git clone git@github.com:whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
$ yarn install
$ yarn start-back
$ yarn start
```
打开你的浏览器,访问 http://127.0.0.1:5601
## 交流
[telegram频道](https://t.me/jiao_long)
## 名称来源
青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。 青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
《后汉书·律历志下》记载:日周于天,一寒一暑,四时备成,万物毕改,摄提迁次,青龙移辰,谓之岁。 《后汉书·律历志下》记载:日周于天,一寒一暑,四时备成,万物毕改,摄提迁次,青龙移辰,谓之岁。
在中国[二十八宿](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF)中,青龙是东方七宿(角、亢、氐、房、心、尾、箕)的总称。 在早期星宿信仰中,祂是最尊贵的天神。 但被道教信仰吸纳入其神系后,神格大跌,道教将其称为“孟章”,在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼,与白虎监兵神君一起,是道教的护卫天神。 在中国[二十八宿](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF)中,青龙是东方七宿(角、亢、氐、房、心、尾、箕)的总称。 在早期星宿信仰中,祂是最尊贵的天神。 但被道教信仰吸纳入其神系后,神格大跌,道教将其称为“孟章”,在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼,与白虎监兵神君一起,是道教的护卫天神。
## 多谢
* [nevinee](https://gitee.com/evine)
* [crontab-ui](https://github.com/alseambusher/crontab-ui)
* [Ant Design](https://ant.design)
* [Ant Design Pro](https://pro.ant.design/)
* [Umijs3.0](https://umijs.org)
* [darkreader](https://github.com/darkreader/darkreader)
-129
View File
@@ -1,129 +0,0 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config';
import jwt from 'jsonwebtoken';
import { createRandomString } from '../config/util';
import crypto from 'crypto';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.post(
'/login',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let username = req.body.username;
let password = req.body.password;
fs.readFile(config.authConfigFile, 'utf8', function (err, data) {
if (err) console.log(err);
const authInfo = JSON.parse(data);
if (username && password) {
if (
authInfo.username === 'admin' &&
authInfo.password === 'adminadmin'
) {
const newPassword = createRandomString(16, 22);
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({
username: authInfo.username,
password: newPassword,
}),
);
return res.send({
code: 100,
message: '已初始化密码,请前往auth.json查看并重新登录',
});
}
if (
username == authInfo.username &&
password == authInfo.password
) {
const data = createRandomString(50, 100);
let token = jwt.sign({ data }, config.secret as any, {
expiresIn: 60 * 60 * 24 * 3,
algorithm: 'HS384',
});
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({
username: authInfo.username,
password: authInfo.password,
token,
}),
);
res.send({ code: 200, token });
} else {
res.send({ code: 400, message: config.authError });
}
} else {
res.send({ err: 400, message: '请输入用户名密码!' });
}
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/logout',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
fs.readFile(config.authConfigFile, 'utf8', function (err, data) {
if (err) console.log(err);
const authInfo = JSON.parse(data);
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({
username: authInfo.username,
password: authInfo.password,
}),
);
res.send({ code: 200 });
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/user',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
fs.writeFile(config.authConfigFile, JSON.stringify(req.body), (err) => {
if (err) console.log(err);
res.send({ code: 200, message: '更新成功' });
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/user',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
if (err) console.log(err);
const authInfo = JSON.parse(data);
res.send({ code: 200, data: { username: authInfo.username } });
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+10 -4
View File
@@ -8,10 +8,10 @@ import { celebrate, Joi } from 'celebrate';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/configs', route);
route.get( route.get(
'/configs/files', '/files',
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 {
@@ -32,11 +32,14 @@ export default (app: Router) => {
); );
route.get( route.get(
'/configs/:file', '/:file',
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 content = ''; let content = '';
if (config.blackFileList.includes(req.params.file)) {
res.send({ code: 403, message: '文件无法访问' });
}
if (req.params.file.includes('sample')) { if (req.params.file.includes('sample')) {
content = getFileContentByName( content = getFileContentByName(
`${config.samplePath}${req.params.file}`, `${config.samplePath}${req.params.file}`,
@@ -55,7 +58,7 @@ export default (app: Router) => {
); );
route.post( route.post(
'/configs/save', '/save',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
name: Joi.string().required(), name: Joi.string().required(),
@@ -66,6 +69,9 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const { name, content } = req.body; const { name, content } = req.body;
if (config.blackFileList.includes(name)) {
res.send({ code: 403, message: '文件无法访问' });
}
const path = `${config.configPath}${name}`; const path = `${config.configPath}${name}`;
fs.writeFileSync(path, content); fs.writeFileSync(path, content);
res.send({ code: 200, message: '保存成功' }); res.send({ code: 200, message: '保存成功' });
+54 -20
View File
@@ -7,26 +7,22 @@ import cron_parser from 'cron-parser';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/crons', route);
route.get(
'/crons', route.get('/', 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 {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const data = await cronService.crontabs( const data = await cronService.crontabs(req.query.searchValue as string);
req.query.searchValue as string,
);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, });
);
route.post( route.post(
'/crons', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
command: Joi.string().required(), command: Joi.string().required(),
@@ -52,7 +48,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/crons/run', '/run',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.string().required()),
}), }),
@@ -70,7 +66,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/crons/stop', '/stop',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.string().required()),
}), }),
@@ -88,7 +84,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/crons/disable', '/disable',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.string().required()),
}), }),
@@ -106,7 +102,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/crons/enable', '/enable',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.string().required()),
}), }),
@@ -124,7 +120,7 @@ export default (app: Router) => {
); );
route.get( route.get(
'/crons/:id/log', '/:id/log',
celebrate({ celebrate({
params: Joi.object({ params: Joi.object({
id: Joi.string().required(), id: Joi.string().required(),
@@ -144,7 +140,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/crons', '',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
command: Joi.string().optional(), command: Joi.string().optional(),
@@ -174,7 +170,7 @@ export default (app: Router) => {
); );
route.delete( route.delete(
'/crons', '/',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.string().required()),
}), }),
@@ -191,8 +187,44 @@ export default (app: Router) => {
}, },
); );
route.put(
'/pin',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.pin(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/unpin',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.unPin(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get( route.get(
'/crons/import', '/import',
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 {
@@ -207,7 +239,7 @@ export default (app: Router) => {
); );
route.get( route.get(
'/crons/:id', '/:id',
celebrate({ celebrate({
params: Joi.object({ params: Joi.object({
id: Joi.string().required(), id: Joi.string().required(),
@@ -227,13 +259,15 @@ export default (app: Router) => {
); );
route.put( route.put(
'/crons/status', '/status',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
ids: Joi.array().items(Joi.string().required()), ids: Joi.array().items(Joi.string().required()),
status: Joi.string().required(), status: Joi.string().required(),
pid: Joi.string().optional(), pid: Joi.string().optional(),
log_path: Joi.string().optional(), log_path: Joi.string().optional(),
last_running_time: Joi.number().optional(),
last_execution_time: Joi.number().optional(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
+143
View File
@@ -0,0 +1,143 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import DependenceService from '../services/dependence';
import { Logger } from 'winston';
import { celebrate, Joi } from 'celebrate';
const route = Router();
export default (app: Router) => {
app.use('/dependencies', route);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const dependenceService = Container.get(DependenceService);
const data = await dependenceService.dependencies(req.query as any);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.post(
'/',
celebrate({
body: Joi.array().items(
Joi.object({
name: Joi.string().required(),
type: Joi.number().required(),
remark: Joi.number().optional().allow(''),
}),
),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const dependenceService = Container.get(DependenceService);
const data = await dependenceService.create(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/',
celebrate({
body: Joi.object({
name: Joi.string().required(),
_id: Joi.string().required(),
type: Joi.number().required(),
remark: Joi.number().optional().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const dependenceService = Container.get(DependenceService);
const data = await dependenceService.update(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.delete(
'/',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const dependenceService = Container.get(DependenceService);
const data = await dependenceService.remove(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.delete(
'/force',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const dependenceService = Container.get(DependenceService);
const data = await dependenceService.removeDb(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/:id',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const dependenceService = Container.get(DependenceService);
const data = await dependenceService.get(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/reinstall',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const dependenceService = Container.get(DependenceService);
const data = await dependenceService.reInstall(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+17 -17
View File
@@ -6,10 +6,9 @@ import { celebrate, Joi } from 'celebrate';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/envs', route);
route.get(
'/envs', route.get('/', 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 {
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
@@ -19,17 +18,18 @@ export default (app: Router) => {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, });
);
route.post( route.post(
'/envs', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.array().items(
Joi.object({
value: Joi.string().required(), value: Joi.string().required(),
name: Joi.string().required(), name: Joi.string().required(),
remarks: Joi.string().optional(), remarks: Joi.string().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');
@@ -45,12 +45,12 @@ export default (app: Router) => {
); );
route.put( route.put(
'/envs', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
value: Joi.string().required(), value: Joi.string().required(),
name: Joi.string().required(), name: Joi.string().required(),
remarks: Joi.string().optional(), remarks: Joi.string().optional().allow(''),
_id: Joi.string().required(), _id: Joi.string().required(),
}), }),
}), }),
@@ -68,7 +68,7 @@ export default (app: Router) => {
); );
route.delete( route.delete(
'/envs', '/',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.string().required()),
}), }),
@@ -86,7 +86,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/envs/:id/move', '/:id/move',
celebrate({ celebrate({
params: Joi.object({ params: Joi.object({
id: Joi.string().required(), id: Joi.string().required(),
@@ -110,7 +110,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/envs/disable', '/disable',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.string().required()),
}), }),
@@ -128,7 +128,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/envs/enable', '/enable',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.string().required()),
}), }),
@@ -146,7 +146,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/envs/name', '/name',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
ids: Joi.array().items(Joi.string().required()), ids: Joi.array().items(Joi.string().required()),
@@ -167,7 +167,7 @@ export default (app: Router) => {
); );
route.get( route.get(
'/envs/:id', '/:id',
celebrate({ celebrate({
params: Joi.object({ params: Joi.object({
id: Joi.string().required(), id: Joi.string().required(),
+8 -2
View File
@@ -1,19 +1,25 @@
import { Router } from 'express'; import { Router } from 'express';
import auth from './auth'; import user from './user';
import env from './env'; import env from './env';
import config from './config'; import config from './config';
import log from './log'; import log from './log';
import cron from './cron'; import cron from './cron';
import script from './script'; import script from './script';
import open from './open';
import dependence from './dependence';
import system from './system';
export default () => { export default () => {
const app = Router(); const app = Router();
auth(app); user(app);
env(app); env(app);
config(app); config(app);
log(app); log(app);
cron(app); cron(app);
script(app); script(app);
open(app);
dependence(app);
system(app);
return app; return app;
}; };
+6 -8
View File
@@ -7,10 +7,9 @@ import { getFileContentByName } from '../config/util';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/logs', route);
route.get(
'/logs', route.get('/', 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 {
const fileList = fs.readdirSync(config.logPath, 'utf-8'); const fileList = fs.readdirSync(config.logPath, 'utf-8');
@@ -40,11 +39,10 @@ export default (app: Router) => {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, });
);
route.get( route.get(
'/logs/:dir/:file', '/:dir/:file',
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 {
@@ -61,7 +59,7 @@ export default (app: Router) => {
); );
route.get( route.get(
'/logs/:file', '/:file',
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 {
+126
View File
@@ -0,0 +1,126 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import OpenService from '../services/open';
import { Logger } from 'winston';
import { celebrate, Joi } from 'celebrate';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.get(
'/apps',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const openService = Container.get(OpenService);
const data = await openService.list();
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/apps',
celebrate({
body: Joi.object({
name: Joi.string().optional().allow(''),
scopes: Joi.array().items(Joi.string().required()),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const openService = Container.get(OpenService);
const data = await openService.create(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/apps',
celebrate({
body: Joi.object({
name: Joi.string().optional().allow(''),
scopes: Joi.array().items(Joi.string()),
_id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const openService = Container.get(OpenService);
const data = await openService.update(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.delete(
'/apps',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const openService = Container.get(OpenService);
const data = await openService.remove(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/apps/:id/reset-secret',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const openService = Container.get(OpenService);
const data = await openService.resetSecret(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/auth/token',
celebrate({
query: {
client_id: Joi.string().required(),
client_secret: Joi.string().required(),
},
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const openService = Container.get(OpenService);
const result = await openService.authToken(req.query as any);
return res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+227 -23
View File
@@ -1,30 +1,76 @@
import { getFileContentByName, getLastModifyFilePath } from '../config/util'; import {
fileExist,
getFileContentByName,
getLastModifyFilePath,
} from '../config/util';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
import config from '../config'; import config from '../config';
import * as fs from 'fs'; import * as fs from 'fs';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import path, { join } from 'path';
import ScriptService from '../services/script';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/scripts', route);
route.get( route.get(
'/scripts/files', '/files',
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 {
const fileList = fs.readdirSync(config.scriptPath, 'utf-8'); const fileList = fs.readdirSync(config.scriptPath, 'utf-8');
let result = [];
for (let i = 0; i < fileList.length; i++) {
const fileOrDir = fileList[i];
const fPath = path.join(config.scriptPath, fileOrDir);
const dirStat = fs.statSync(fPath);
if (['node_modules'].includes(fileOrDir)) {
continue;
}
if (dirStat.isDirectory()) {
const childFileList = fs.readdirSync(fPath, 'utf-8');
let children = [];
for (let j = 0; j < childFileList.length; j++) {
const childFile = childFileList[j];
const sPath = path.join(config.scriptPath, fileOrDir, childFile);
const _fileExist = await fileExist(sPath);
if (_fileExist && fs.statSync(sPath).isFile()) {
const statObj = fs.statSync(sPath);
children.push({
title: childFile,
value: childFile,
key: `${fileOrDir}/${childFile}`,
mtime: statObj.mtimeMs,
parent: fileOrDir,
});
}
}
result.push({
title: fileOrDir,
value: fileOrDir,
key: fileOrDir,
mtime: dirStat.mtimeMs,
disabled: true,
children: children.sort((a, b) => b.mtime - a.mtime),
});
} else {
result.push({
title: fileOrDir,
value: fileOrDir,
key: fileOrDir,
mtime: dirStat.mtimeMs,
});
}
}
res.send({ res.send({
code: 200, code: 200,
data: fileList data: result,
.filter((x) => {
return !fs.lstatSync(config.scriptPath + x).isDirectory();
})
.map((x) => {
return { title: x, value: x, key: x };
}),
}); });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
@@ -34,13 +80,16 @@ export default (app: Router) => {
); );
route.get( route.get(
'/scripts/:file', '/:file',
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 {
const content = getFileContentByName( const filePath = join(
`${config.scriptPath}${req.params.file}`, config.scriptPath,
req.query.path as string,
req.params.file,
); );
const content = getFileContentByName(filePath);
res.send({ code: 200, data: content }); res.send({ code: 200, data: content });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
@@ -50,35 +99,55 @@ export default (app: Router) => {
); );
route.post( route.post(
'/scripts', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
path: Joi.string().required(), path: Joi.string().optional().allow(''),
content: Joi.string().required(), content: Joi.string().allow(''),
originFilename: Joi.string().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, path, content } = req.body as { let { filename, path, content, originFilename } = req.body as {
filename: string; filename: string;
path: string; path: string;
content: string; content: string;
originFilename: string;
}; };
if (!path) {
path = config.scriptPath;
}
if (!path.endsWith('/')) { if (!path.endsWith('/')) {
path += '/'; path += '/';
} }
if (!path.startsWith('/')) {
path = `${config.scriptPath}${path}`;
}
if (config.writePathList.every((x) => !path.startsWith(x))) { if (config.writePathList.every((x) => !path.startsWith(x))) {
return res.send({ code: 400, data: '文件路径错误,可保存目录/ql/scripts、/ql/config、/ql/jbot、/ql/bak' }); return res.send({
code: 430,
data: '文件路径禁止访问',
});
} }
if (!originFilename) {
originFilename = filename;
}
const originFilePath = `${path}${originFilename.replace(/\//g, '')}`;
const filePath = `${path}${filename.replace(/\//g, '')}`; const filePath = `${path}${filename.replace(/\//g, '')}`;
const bakPath = '/ql/bak'; if (fs.existsSync(originFilePath)) {
if (fs.existsSync(filePath)) { if (!fs.existsSync(config.bakPath)) {
if (!fs.existsSync(bakPath)) { fs.mkdirSync(config.bakPath);
fs.mkdirSync(bakPath); }
fs.copyFileSync(
originFilePath,
`${config.bakPath}${originFilename.replace(/\//g, '')}`,
);
if (filename !== originFilename) {
fs.unlinkSync(originFilePath);
} }
fs.copyFileSync(filePath, bakPath);
} }
fs.writeFileSync(filePath, content); fs.writeFileSync(filePath, content);
return res.send({ code: 200 }); return res.send({ code: 200 });
@@ -88,4 +157,139 @@ export default (app: Router) => {
} }
}, },
); );
route.put(
'/',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().optional().allow(''),
content: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, content, path } = req.body as {
filename: string;
content: string;
path: string;
};
const filePath = join(config.scriptPath, path, filename);
fs.writeFileSync(filePath, content);
return res.send({ code: 200 });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.delete(
'/',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, path } = req.body as {
filename: string;
path: string;
};
const filePath = join(config.scriptPath, path, filename);
fs.unlinkSync(filePath);
res.send({ code: 200 });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/download',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename } = req.body as {
filename: string;
};
const filePath = `${config.scriptPath}${filename}`;
// const stats = fs.statSync(filePath);
// res.set({
// 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件
// 'Content-Disposition': 'attachment; filename=' + filename, //告诉浏览器这是一个需要下载的文件
// 'Content-Length': stats.size //文件大小
// });
// fs.createReadStream(filePath).pipe(res);
return res.download(filePath, filename, (err) => {
return next(err);
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/run',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().optional().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, path } = req.body as {
filename: string;
path: string;
};
const filePath = join(path, filename);
const scriptService = Container.get(ScriptService);
const result = await scriptService.runScript(filePath);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/stop',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().optional().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, path } = req.body as {
filename: string;
path: string;
};
const filePath = join(path, filename);
const scriptService = Container.get(ScriptService);
const result = await scriptService.stopScript(filePath);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
}; };
+114
View File
@@ -0,0 +1,114 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config';
import SystemService from '../services/system';
import { celebrate, Joi } from 'celebrate';
import { getFileContentByName } from '../config/util';
import UserService from '../services/user';
const route = Router();
export default (app: Router) => {
app.use('/system', route);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
const envDbContent = getFileContentByName(config.envDbFile);
const versionRegx = /.*export const version = \'(.*)\'\;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const version = currentVersionFile.match(versionRegx)![1];
let isInitialized = true;
if (
Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' &&
authInfo.password === 'admin' &&
envDbContent.length === 0
) {
isInitialized = false;
}
res.send({
code: 200,
data: {
isInitialized,
version,
},
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.get(
'/log/remove',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const data = await systemService.getLogRemoveFrequency();
res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/log/remove',
celebrate({
body: Joi.object({
frequency: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const result = await systemService.updateLogRemoveFrequency(
req.body.frequency,
);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/update-check',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const result = await systemService.checkUpdate();
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/update',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const result = await systemService.updateSystem();
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+241
View File
@@ -0,0 +1,241 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config';
import UserService from '../services/user';
import { celebrate, Joi } from 'celebrate';
import { getFileContentByName } from '../config/util';
const route = Router();
export default (app: Router) => {
app.use('/user', route);
route.post(
'/login',
celebrate({
body: Joi.object({
username: Joi.string().required(),
password: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const data = await userService.login({ ...req.body }, req);
return res.send(data);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/logout',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
await userService.logout(req.platform);
res.send({ code: 200 });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/',
celebrate({
body: Joi.object({
username: Joi.string().required(),
password: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
await userService.updateUsernameAndPassword(req.body);
res.send({ code: 200, message: '更新成功' });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
res.send({
code: 200,
data: {
username: authInfo.username,
twoFactorActivated: authInfo.twoFactorActivated,
},
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.get(
'/two-factor/init',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const data = await userService.initTwoFactor();
res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/two-factor/active',
celebrate({
body: Joi.object({
code: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const data = await userService.activeTwoFactor(req.body.code);
res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/two-factor/deactive',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const data = await userService.deactiveTwoFactor();
res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/two-factor/login',
celebrate({
body: Joi.object({
code: Joi.string().required(),
username: Joi.string().required(),
password: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const data = await userService.twoFactorLogin(req.body, req);
res.send(data);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/login-log',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const data = await userService.getLoginLog();
res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/notification',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const data = await userService.getNotificationMode();
res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/notification',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const result = await userService.updateNotificationMode(req.body);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/init',
celebrate({
body: Joi.object({
username: Joi.string().required(),
password: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
await userService.updateUsernameAndPassword(req.body);
res.send({ code: 200, message: '更新成功' });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/notification/init',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const result = await userService.updateNotificationMode(req.body);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+10 -2
View File
@@ -9,9 +9,15 @@ import Logger from './loaders/logger';
async function startServer() { async function startServer() {
const app = express(); const app = express();
await require('./loaders').default({ expressApp: app }); await require('./loaders/initFile').default();
app await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default();
await require('./loaders/app').default({ expressApp: app });
const server = app
.listen(config.port, () => { .listen(config.port, () => {
Logger.info(` Logger.info(`
################################################ ################################################
@@ -23,6 +29,8 @@ async function startServer() {
Logger.error(err); Logger.error(err);
process.exit(1); process.exit(1);
}); });
await require('./loaders/server').default({ server });
} }
startServer(); startServer();
+24 -11
View File
@@ -4,8 +4,11 @@ import { createRandomString } from './util';
process.env.NODE_ENV = process.env.NODE_ENV || 'development'; process.env.NODE_ENV = process.env.NODE_ENV || 'development';
const lastVersionFile =
'https://raw.githubusercontent.com/whyour/qinglong/master/src/version.ts';
const envFound = dotenv.config(); const envFound = dotenv.config();
const rootPath = path.resolve(__dirname, '../../'); const rootPath = process.cwd();
const envFile = path.join(rootPath, 'config/env.sh'); const envFile = path.join(rootPath, 'config/env.sh');
const confFile = path.join(rootPath, 'config/config.sh'); const confFile = path.join(rootPath, 'config/config.sh');
const sampleFile = path.join(rootPath, 'sample/config.sample.sh'); const sampleFile = path.join(rootPath, 'sample/config.sample.sh');
@@ -15,6 +18,7 @@ const authConfigFile = path.join(rootPath, 'config/auth.json');
const extraFile = path.join(rootPath, 'config/extra.sh'); const extraFile = path.join(rootPath, 'config/extra.sh');
const configPath = path.join(rootPath, 'config/'); const configPath = path.join(rootPath, 'config/');
const scriptPath = path.join(rootPath, 'scripts/'); const scriptPath = path.join(rootPath, 'scripts/');
const bakPath = path.join(rootPath, 'bak/');
const samplePath = path.join(rootPath, 'sample/'); const samplePath = path.join(rootPath, 'sample/');
const logPath = path.join(rootPath, 'log/'); const logPath = path.join(rootPath, 'log/');
const authError = '错误的用户名密码,请重试'; const authError = '错误的用户名密码,请重试';
@@ -23,16 +27,15 @@ const configString = 'config sample crontab shareCode diy';
const dbPath = path.join(rootPath, 'db/'); const dbPath = path.join(rootPath, 'db/');
const cronDbFile = path.join(rootPath, 'db/crontab.db'); const cronDbFile = path.join(rootPath, 'db/crontab.db');
const envDbFile = path.join(rootPath, 'db/env.db'); const envDbFile = path.join(rootPath, 'db/env.db');
const configFound = dotenv.config({ path: confFile }); const appDbFile = path.join(rootPath, 'db/app.db');
const authDbFile = path.join(rootPath, 'db/auth.db');
const dependenceDbFile = path.join(rootPath, 'db/dependence.db');
const versionFile = path.join(rootPath, 'src/version.ts');
if (envFound.error) { if (envFound.error) {
throw new Error("⚠️ Couldn't find .env file ⚠️"); throw new Error("⚠️ Couldn't find .env file ⚠️");
} }
if (configFound.error) {
throw new Error("⚠️ Couldn't find config.sh file ⚠️");
}
export default { export default {
port: parseInt(process.env.PORT as string, 10), port: parseInt(process.env.PORT as string, 10),
cronPort: parseInt(process.env.CRON_PORT as string, 10), cronPort: parseInt(process.env.CRON_PORT as string, 10),
@@ -43,6 +46,7 @@ export default {
api: { api: {
prefix: '/api', prefix: '/api',
}, },
rootPath,
configString, configString,
loginFaild, loginFaild,
authError, authError,
@@ -57,6 +61,9 @@ export default {
dbPath, dbPath,
cronDbFile, cronDbFile,
envDbFile, envDbFile,
appDbFile,
authDbFile,
dependenceDbFile,
configPath, configPath,
scriptPath, scriptPath,
samplePath, samplePath,
@@ -67,10 +74,16 @@ export default {
'crontab.list', 'crontab.list',
'env.sh', 'env.sh',
], ],
writePathList: [ writePathList: [configPath, scriptPath],
'/ql/scripts/', bakPath,
'/ql/config/', apiWhiteList: [
'/ql/jbot/', '/api/user/login',
'/ql/bak/', '/open/auth/token',
'/api/user/two-factor/login',
'/api/system',
'/api/user/init',
'/api/user/notification/init',
], ],
versionFile,
lastVersionFile,
}; };
+152 -3
View File
@@ -1,5 +1,7 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import got from 'got';
import iconv from 'iconv-lite';
export function getFileContentByName(fileName: string) { export function getFileContentByName(fileName: string) {
if (fs.existsSync(fileName)) { if (fs.existsSync(fileName)) {
@@ -85,7 +87,7 @@ export function createRandomString(min: number, max: number): string {
'Y', 'Y',
'Z', 'Z',
]; ];
const special = ['-', '_', '#']; const special = ['-', '_'];
const config = num.concat(english).concat(ENGLISH).concat(special); const config = num.concat(english).concat(ENGLISH).concat(special);
const arr = []; const arr = [];
@@ -113,9 +115,156 @@ export function createRandomString(min: number, max: number): string {
} }
export function getToken(req: any) { export function getToken(req: any) {
const { authorization } = req.headers; const { authorization = '' } = req.headers;
if (authorization && authorization.split(' ')[0] === 'Bearer') { if (authorization && authorization.split(' ')[0] === 'Bearer') {
return authorization.split(' ')[1]; return (authorization as string)
.replace('Bearer ', '')
.replace('mobile-', '')
.replace('desktop-', '');
} }
return ''; return '';
} }
export async function getNetIp(req: any) {
const ipArray = [
...new Set([
...(req.headers['x-real-ip'] || '').split(','),
...(req.headers['x-forwarded-for'] || '').split(','),
req.ip,
...req.ips,
req.socket.remoteAddress,
]),
];
let ip = ipArray[0];
if (ipArray.length > 1) {
for (let i = 0; i < ipArray.length; i++) {
const ipNumArray = ipArray[i].split('.');
const tmp = ipNumArray[0] + '.' + ipNumArray[1];
if (
tmp === '192.168' ||
(ipNumArray[0] === '172' &&
ipNumArray[1] >= 16 &&
ipNumArray[1] <= 32) ||
tmp === '10.7' ||
tmp === '127.0'
) {
continue;
}
ip = ipArray[i];
break;
}
}
ip = ip.substr(ip.lastIndexOf(':') + 1, ip.length);
if (ip.includes('127.0') || ip.includes('192.168') || ip.includes('10.7')) {
ip = '';
}
try {
const baiduApi = got
.get(`https://www.cip.cc/${ip}`, { timeout: 10000, retry: 0 })
.text();
const ipApi = got
.get(`https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`, {
timeout: 10000,
retry: 0,
})
.buffer();
const [data, ipApiBody] = await await Promise.all<any>([baiduApi, ipApi]);
const ipRegx = /.*IP :(.*)\n/;
const addrRegx = /.*数据二 :(.*)\n/;
if (data && ipRegx.test(data) && addrRegx.test(data)) {
const ip = data.match(ipRegx)[1];
const addr = data.match(addrRegx)[1];
return { address: addr, ip };
} else if (ipApiBody) {
const { addr, ip } = JSON.parse(iconv.decode(ipApiBody, 'GBK'));
return { address: `${addr}`, ip };
} else {
return { address: `获取失败`, ip };
}
} catch (error) {
return { address: `获取失败`, ip };
}
}
export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
const ua = userAgent.toLowerCase();
const testUa = (regexp: RegExp) => regexp.test(ua);
const testVs = (regexp: RegExp) =>
(ua.match(regexp) || [])
.toString()
.replace(/[^0-9|_.]/g, '')
.replace(/_/g, '.');
// 系统
let system = 'unknow';
if (testUa(/windows|win32|win64|wow32|wow64/g)) {
system = 'windows'; // windows系统
} else if (testUa(/macintosh|macintel/g)) {
system = 'macos'; // macos系统
} else if (testUa(/x11/g)) {
system = 'linux'; // linux系统
} else if (testUa(/android|adr/g)) {
system = 'android'; // android系统
} else if (testUa(/ios|iphone|ipad|ipod|iwatch/g)) {
system = 'ios'; // ios系统
}
let platform = 'desktop';
if (system === 'windows' || system === 'macos' || system === 'linux') {
platform = 'desktop';
} else if (system === 'android' || system === 'ios' || testUa(/mobile/g)) {
platform = 'mobile';
}
return platform as 'mobile' | 'desktop';
}
export async function fileExist(file: any) {
return new Promise((resolve) => {
try {
fs.accessSync(file);
resolve(true);
} catch (error) {
resolve(false);
}
});
}
export async function concurrentRun(
fnList: Array<() => Promise<any>> = [],
max = 5,
) {
if (!fnList.length) return;
const replyList: any[] = []; // 收集任务执行结果
const startTime = new Date().getTime(); // 记录任务执行开始时间
// 任务执行程序
const schedule = async (index: number) => {
return new Promise(async (resolve) => {
const fn = fnList[index];
if (!fn) return resolve(null);
// 执行当前异步任务
const reply = await fn();
replyList[index] = reply;
// 执行完当前任务后,继续执行任务池的剩余任务
await schedule(index + max);
resolve(null);
});
};
// 任务池执行程序
const scheduleList = new Array(max)
.fill(0)
.map((_, index) => schedule(index));
// 使用 Promise.all 批量执行
const r = await Promise.all(scheduleList);
const cost = (new Date().getTime() - startTime) / 1000;
return replyList;
}
+25
View File
@@ -0,0 +1,25 @@
export class AuthInfo {
ip?: string;
type: AuthDataType;
info?: any;
_id?: string;
constructor(options: AuthInfo) {
this.ip = options.ip;
this.info = options.info;
this.type = options.type;
this._id = options._id;
}
}
export enum LoginStatus {
'success',
'fail',
}
export enum AuthDataType {
'loginLog' = 'loginLog',
'authToken' = 'authToken',
'notification' = 'notification',
'removeLogFrequency' = 'removeLogFrequency',
}
+2
View File
@@ -11,6 +11,7 @@ export class Crontab {
pid?: number; pid?: number;
isDisabled?: 1 | 0; isDisabled?: 1 | 0;
log_path?: string; log_path?: string;
isPinned?: 1 | 0;
constructor(options: Crontab) { constructor(options: Crontab) {
this.name = options.name; this.name = options.name;
@@ -27,6 +28,7 @@ export class Crontab {
this.pid = options.pid; this.pid = options.pid;
this.isDisabled = options.isDisabled || 0; this.isDisabled = options.isDisabled || 0;
this.log_path = options.log_path || ''; this.log_path = options.log_path || '';
this.isPinned = options.isPinned || 0;
} }
} }
+48
View File
@@ -0,0 +1,48 @@
export class Dependence {
timestamp?: string;
created?: number;
_id?: string;
status?: DependenceStatus;
type?: DependenceTypes;
name?: number;
log?: string[];
remark?: string;
constructor(options: Dependence) {
this._id = options._id;
this.created = options.created || new Date().valueOf();
this.status = options.status || DependenceStatus.installing;
this.type = options.type || DependenceTypes.nodejs;
this.timestamp = new Date().toString();
this.name = options.name;
this.log = options.log || [];
this.remark = options.remark || '';
}
}
export enum DependenceStatus {
'installing',
'installed',
'installFailed',
'removing',
'removed',
'removeFailed',
}
export enum DependenceTypes {
'nodejs',
'python3',
'linux',
}
export enum InstallDependenceCommandTypes {
'npm i -g --force',
'pip3 install',
'apk add --no-cache -f',
}
export enum unInstallDependenceCommandTypes {
'npm uninstall -g --force',
'pip3 uninstall -y',
'apk del -f',
}
+89
View File
@@ -0,0 +1,89 @@
export enum NotificationMode {
'gotify' = 'gotify',
'goCqHttpBot' = 'goCqHttpBot',
'serverChan' = 'serverChan',
'bark' = 'bark',
'telegramBot' = 'telegramBot',
'dingtalkBot' = 'dingtalkBot',
'weWorkBot' = 'weWorkBot',
'weWorkApp' = 'weWorkApp',
'iGot' = 'iGot',
'pushPlus' = 'pushPlus',
'email' = 'email',
}
abstract class NotificationBaseInfo {
public type!: NotificationMode;
}
export class GotifyNotification extends NotificationBaseInfo {
public gotifyUrl = '';
public gotifyToken = '';
public gotifyPriority = 0;
}
export class GoCqHttpBotNotification extends NotificationBaseInfo {
public goCqHttpBotUrl = '';
public goCqHttpBotToken = '';
public goCqHttpBotQq = '';
}
export class ServerChanNotification extends NotificationBaseInfo {
public serverChanKey = '';
}
export class BarkNotification extends NotificationBaseInfo {
public barkPush = '';
public barkSound = '';
public barkGroup = 'qinglong';
}
export class TelegramBotNotification extends NotificationBaseInfo {
public telegramBotToken = '';
public telegramBotUserId = '';
public telegramBotProxyHost = '';
public telegramBotProxyPort = '';
public telegramBotProxyAuth = '';
public telegramBotApiHost = 'api.telegram.org';
}
export class DingtalkBotNotification extends NotificationBaseInfo {
public dingtalkBotToken = '';
public dingtalkBotSecret = '';
}
export class WeWorkBotNotification extends NotificationBaseInfo {
public weWorkBotKey = '';
}
export class WeWorkAppNotification extends NotificationBaseInfo {
public weWorkAppKey = '';
}
export class IGotNotification extends NotificationBaseInfo {
public iGotPushKey = '';
}
export class PushPlusNotification extends NotificationBaseInfo {
public pushPlusToken = '';
public pushPlusUser = '';
}
export class EmailNotification extends NotificationBaseInfo {
public emailService: string = '';
public emailUser: string = '';
public emailPass: string = '';
}
export interface NotificationInfo
extends GoCqHttpBotNotification,
GotifyNotification,
ServerChanNotification,
BarkNotification,
TelegramBotNotification,
DingtalkBotNotification,
WeWorkBotNotification,
WeWorkAppNotification,
IGotNotification,
PushPlusNotification,
EmailNotification {}
+31
View File
@@ -0,0 +1,31 @@
export class App {
name: string;
scopes: AppScope[];
client_id: string;
client_secret: string;
tokens?: AppToken[];
_id?: string;
constructor(options: App) {
this.name = options.name;
this.scopes = options.scopes;
this.client_id = options.client_id;
this.client_secret = options.client_secret;
this._id = options._id;
}
}
export interface AppToken {
value: string;
type: 'Bearer';
expiration: number;
}
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
export enum CrontabStatus {
'running',
'idle',
'disabled',
'queued',
}
+17
View File
@@ -0,0 +1,17 @@
export class SockMessage {
message?: string;
type?: SockMessageType;
references?: string[];
constructor(options: SockMessage) {
this.type = options.type;
this.message = options.message;
this.references = options.references;
}
}
export type SockMessageType =
| 'ping'
| 'installDependence'
| 'updateSystemVersion'
| 'manuallyRunScript';
+5
View File
@@ -0,0 +1,5 @@
declare namespace Express {
interface Request {
platform: 'desktop' | 'mobile';
}
}
@@ -2,10 +2,10 @@ import expressLoader from './express';
import dependencyInjectorLoader from './dependencyInjector'; import dependencyInjectorLoader from './dependencyInjector';
import Logger from './logger'; import Logger from './logger';
import initData from './initData'; import initData from './initData';
import { Application } from 'express';
import linkDeps from './deps';
export default async ({ expressApp }: { expressApp: any }) => { export default async ({ expressApp }: { expressApp: Application }) => {
Logger.info('✌️ DB loaded and connected!');
await dependencyInjectorLoader({ await dependencyInjectorLoader({
models: [], models: [],
}); });
@@ -16,4 +16,7 @@ export default async ({ expressApp }: { expressApp: any }) => {
await initData(); await initData();
Logger.info('✌️ init data loaded'); Logger.info('✌️ init data loaded');
await linkDeps();
Logger.info('✌️ link deps');
}; };
+63
View File
@@ -0,0 +1,63 @@
import DataStore from 'nedb';
import config from '../config';
import Logger from './logger';
import fs from 'fs';
import { fileExist } from '../config/util';
interface Dbs {
cronDb: DataStore;
dependenceDb: DataStore;
envDb: DataStore;
appDb: DataStore;
authDb: DataStore;
}
const db: Dbs = {} as any;
async function truncateDb() {
return new Promise(async (resolve) => {
const files = [
config.cronDbFile,
config.dependenceDbFile,
config.envDbFile,
config.appDbFile,
config.authDbFile,
];
for (const file of files) {
const _fileExist = await fileExist(file);
if (_fileExist && fs.statSync(file).size >= 1024 * 1024 * 500) {
fs.truncateSync(file, 1024 * 1024 * 500);
}
}
resolve(null);
});
}
export default async () => {
try {
await truncateDb();
db.cronDb = new DataStore({ filename: config.cronDbFile, autoload: true });
db.dependenceDb = new DataStore({
filename: config.dependenceDbFile,
autoload: true,
});
db.envDb = new DataStore({ filename: config.envDbFile, autoload: true });
db.appDb = new DataStore({ filename: config.appDbFile, autoload: true });
db.authDb = new DataStore({ filename: config.authDbFile, autoload: true });
// compaction data file
db.cronDb.persistence.compactDatafile();
db.envDb.persistence.compactDatafile();
db.dependenceDb.persistence.compactDatafile();
db.appDb.persistence.compactDatafile();
db.authDb.persistence.compactDatafile();
Logger.info('✌️ DB loaded');
} catch (error) {
Logger.info('✌️ DB load failed');
Logger.info(error);
}
};
export const dbs: Dbs = db;
+31
View File
@@ -0,0 +1,31 @@
import path from 'path';
import fs from 'fs';
import chokidar from 'chokidar';
import config from '../config/index';
function linkToNodeModule(src: string, dst?: string) {
const target = path.join(config.rootPath, 'node_modules', dst || src);
const source = path.join(config.rootPath, src);
fs.lstat(target, (err, stat) => {
if (!stat) {
fs.symlink(source, target, 'dir', (err) => {
if (err) throw err;
});
}
});
}
export default async (src: string = 'deps') => {
linkToNodeModule(src);
const source = path.join(config.rootPath, src);
const watcher = chokidar.watch(source, {
ignored: /(^|[\/\\])\../, // ignore dotfiles
persistent: true,
});
watcher
.on('add', (path) => linkToNodeModule(src))
.on('change', (path) => linkToNodeModule(src));
};
+96 -10
View File
@@ -5,29 +5,75 @@ import routes from '../api';
import config from '../config'; import config from '../config';
import jwt from 'express-jwt'; import jwt from 'express-jwt';
import fs from 'fs'; import fs from 'fs';
import { getToken } from '../config/util'; import { getFileContentByName, getPlatform, getToken } from '../config/util';
import Container from 'typedi';
import OpenService from '../services/open';
import rewrite from 'express-urlrewrite';
import UserService from '../services/user';
import handler from 'serve-handler';
import * as Sentry from '@sentry/node';
export default ({ app }: { app: Application }) => { export default ({ app }: { app: Application }) => {
app.enable('trust proxy'); app.enable('trust proxy');
app.use(cors()); app.use(cors());
app.use((req, res, next) => {
if (req.path.startsWith('/api') || req.path.startsWith('/open')) {
next();
} else {
return handler(req, res, {
public: 'dist',
rewrites: [{ source: '**', destination: '/index.html' }],
});
}
});
app.use(bodyParser.json({ limit: '50mb' })); app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
app.use( app.use(
jwt({ secret: config.secret as string, algorithms: ['HS384'] }).unless({ jwt({
path: ['/api/login', '/api/crons/status'], secret: config.secret as string,
algorithms: ['HS384'],
}).unless({
path: [...config.apiWhiteList, /^\/open\//],
}), }),
); );
app.use((req, res, next) => {
const data = fs.readFileSync(config.authConfigFile, 'utf8'); app.use((req: Request, res, next) => {
if (!req.headers) {
req.platform = 'desktop';
} else {
const platform = getPlatform(req.headers['user-agent'] || '');
req.platform = platform;
}
return next();
});
app.use(async (req, res, next) => {
const headerToken = getToken(req); const headerToken = getToken(req);
if (data) { if (req.path.startsWith('/open/')) {
const { token } = JSON.parse(data); const openService = Container.get(OpenService);
if (token && headerToken === token) { const doc = await openService.findTokenByValue(headerToken);
if (doc && doc.tokens && doc.tokens.length > 0) {
const currentToken = doc.tokens.find((x) => x.value === headerToken);
const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/);
const key = keyMatch && keyMatch[1];
if (
doc.scopes.includes(key as any) &&
currentToken &&
currentToken.expiration >= Math.round(Date.now() / 1000)
) {
return next(); return next();
} }
} }
if (!headerToken && req.path && req.path === '/api/login') { }
if (
!headerToken &&
req.path &&
config.apiWhiteList.includes(req.path) &&
req.path !== '/api/crons/status'
) {
return next(); return next();
} }
const remoteAddress = req.socket.remoteAddress; const remoteAddress = req.socket.remoteAddress;
@@ -37,10 +83,46 @@ export default ({ app }: { app: Application }) => {
) { ) {
return next(); return next();
} }
const data = fs.readFileSync(config.authConfigFile, 'utf8');
if (data) {
const { token = '', tokens = {} } = JSON.parse(data);
if (headerToken === token || tokens[req.platform] === headerToken) {
return next();
}
}
const err: any = new Error('UnauthorizedError'); const err: any = new Error('UnauthorizedError');
err['status'] = 401; err.status = 401;
next(err); next(err);
}); });
app.use(async (req, res, next) => {
if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) {
return next();
}
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
const envDbContent = getFileContentByName(config.envDbFile);
let isInitialized = true;
if (
Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' &&
authInfo.password === 'admin' &&
envDbContent.length === 0
) {
isInitialized = false;
}
if (isInitialized) {
return res.send({ code: 450, message: '未知错误' });
} else {
return next();
}
});
app.use(rewrite('/open/*', '/api/$1'));
app.use(config.api.prefix, routes()); app.use(config.api.prefix, routes());
app.use((req, res, next) => { app.use((req, res, next) => {
@@ -49,6 +131,8 @@ export default ({ app }: { app: Application }) => {
next(err); next(err);
}); });
app.use(Sentry.Handlers.errorHandler());
app.use( app.use(
( (
err: Error & { status: number }, err: Error & { status: number },
@@ -73,6 +157,8 @@ export default ({ app }: { app: Application }) => {
res: Response, res: Response,
next: NextFunction, next: NextFunction,
) => { ) => {
Sentry.captureException(err);
res.status(err.status || 500); res.status(err.status || 500);
res.json({ res.json({
code: err.status || 500, code: err.status || 500,
+21 -38
View File
@@ -1,49 +1,18 @@
import DependenceService from '../services/dependence';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Crontab, CrontabStatus } from '../data/cron'; import { Crontab, CrontabStatus } from '../data/cron';
import CronService from '../services/cron'; import CronService from '../services/cron';
import EnvService from '../services/env'; import EnvService from '../services/env';
import _ from 'lodash';
const initData = [ import { dbs } from '../loaders/db';
{
name: '更新面板',
command: `ql update`,
schedule: `${randomSchedule(60, 1)} ${randomSchedule(
6,
1,
).toString()} * * *`,
isDisabled: 1,
},
{
name: '删除日志',
command: 'ql rmlog 7',
schedule: '30 7 */7 * *',
isDisabled: 1,
},
];
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 cronDb = cronService.getDb(); const dependenceService = Container.get(DependenceService);
const cronDb = dbs.cronDb;
cronDb.count({}, async (err, count) => { const dependenceDb = dbs.dependenceDb;
if (count === 0) {
const data = initData.map((x: any) => {
const tab = new Crontab(x);
tab.created = new Date().valueOf();
tab.saved = false;
if (tab.name === '更新面板') {
tab.isSystem = 1;
} else {
tab.isSystem = 0;
}
return tab;
});
cronDb.insert(data);
await cronService.autosave_crontab();
}
});
// 初始化更新所有任务状态为空闲 // 初始化更新所有任务状态为空闲
cronDb.update( cronDb.update(
@@ -52,6 +21,20 @@ export default async () => {
{ multi: true }, { multi: true },
); );
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
dependenceDb.find({ status: { $in: [0, 1, 2] } }).exec(async (err, docs) => {
const groups = _.groupBy(docs, 'type');
for (const key in groups) {
if (Object.prototype.hasOwnProperty.call(groups, key)) {
const group = groups[key];
const depIds = group.map((x) => x._id);
for (const dep of depIds) {
await dependenceService.reInstall([dep]);
}
}
}
});
// 初始化时执行一次所有的ql repo 任务 // 初始化时执行一次所有的ql repo 任务
cronDb cronDb
.find({ .find({
@@ -92,7 +75,7 @@ export default async () => {
}; };
function randomSchedule(from: number, to: number) { function randomSchedule(from: number, to: number) {
const result = []; const result: any[] = [];
const arr = [...Array(from).keys()]; const arr = [...Array(from).keys()];
let count = arr.length; let count = arr.length;
for (let i = 0; i < to; i++) { for (let i = 0; i < to; i++) {
+46
View File
@@ -0,0 +1,46 @@
import fs from 'fs';
import path from 'path';
import dotenv from 'dotenv';
import Logger from './logger';
import { fileExist } from '../config/util';
const rootPath = process.cwd();
const confFile = path.join(rootPath, 'config/config.sh');
const sampleConfigFile = path.join(rootPath, 'sample/config.sample.sh');
const sampleAuthFile = path.join(rootPath, 'sample/auth.sample.json');
const authConfigFile = path.join(rootPath, 'config/auth.json');
const configPath = path.join(rootPath, 'config/');
const scriptPath = path.join(rootPath, 'scripts/');
const logPath = path.join(rootPath, 'log/');
export default async () => {
const authFileExist = await fileExist(authConfigFile);
const confFileExist = await fileExist(confFile);
const scriptDirExist = await fileExist(scriptPath);
const logDirExist = await fileExist(logPath);
const configDirExist = await fileExist(configPath);
if (!configDirExist) {
fs.mkdirSync(configPath);
}
if (!authFileExist) {
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
}
if (!confFileExist) {
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
}
if (!scriptDirExist) {
fs.mkdirSync(scriptPath);
}
if (!logDirExist) {
fs.mkdirSync(logPath);
}
dotenv.config({ path: confFile });
Logger.info('✌️ Init file down');
};
+21
View File
@@ -0,0 +1,21 @@
import { Application } from 'express';
import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing';
import Logger from './logger';
export default ({ expressApp }: { expressApp: Application }) => {
Sentry.init({
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
new Tracing.Integrations.Express({ app: expressApp }),
],
tracesSampleRate: 1.0,
});
expressApp.use(Sentry.Handlers.requestHandler());
expressApp.use(Sentry.Handlers.tracingHandler());
Logger.info('✌️ Sentry loaded');
};
+16
View File
@@ -0,0 +1,16 @@
import { Server } from 'http';
import Logger from './logger';
import Sock from './sock';
export default async ({ server }: { server: Server }) => {
await Sock({ server });
Logger.info('✌️ Sock loaded');
process.on('SIGINT', () => {
Logger.info('✌️ Server need close');
server.close(() => {
Logger.info('✌️ Server closed');
process.exit(0);
});
});
};
+46
View File
@@ -0,0 +1,46 @@
import sockJs from 'sockjs';
import { Server } from 'http';
import Logger from './logger';
import { Container } from 'typedi';
import SockService from '../services/sock';
import config from '../config/index';
import fs from 'fs';
import { getPlatform } from '../config/util';
export default async ({ server }: { server: Server }) => {
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
const sockService = Container.get(SockService);
echo.on('connection', (conn) => {
if (!conn.headers || !conn.url || !conn.pathname) {
conn.close('404');
}
const data = fs.readFileSync(config.authConfigFile, 'utf8');
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
if (data) {
const { token = '', tokens = {} } = JSON.parse(data);
if (headerToken === token || tokens[platform] === headerToken) {
conn.write(JSON.stringify({ type: 'ping', message: 'hanhh' }));
sockService.addClient(conn);
conn.on('data', (message) => {
conn.write(message);
});
conn.on('close', function () {
sockService.removeClient(conn);
});
return;
} else {
conn.write(JSON.stringify({ type: 'ping', message: 'whyour' }));
}
}
conn.close('404');
});
echo.installHandlers(server);
};
+8 -7
View File
@@ -2,16 +2,14 @@ import schedule from 'node-schedule';
import express from 'express'; import express from 'express';
import { exec } from 'child_process'; import { exec } from 'child_process';
import Logger from './loaders/logger'; import Logger from './loaders/logger';
import { Container } from 'typedi';
import CronService from './services/cron';
import { CrontabStatus } from './data/cron'; import { CrontabStatus } from './data/cron';
import config from './config'; import config from './config';
import { dbs } from './loaders/db';
const app = express(); const app = express();
const run = async () => { const run = async () => {
const cronService = Container.get(CronService); const cronDb = dbs.cronDb;
const cronDb = cronService.getDb();
cronDb cronDb
.find({}) .find({})
@@ -25,7 +23,7 @@ const run = async () => {
if (docs && docs.length > 0) { if (docs && docs.length > 0) {
for (let i = 0; i < docs.length; i++) { for (let i = 0; i < docs.length; i++) {
const task = docs[i]; const task = docs[i];
const _schedule = task.schedule && task.schedule.split(' '); const _schedule = task.schedule && task.schedule.split(/ +/);
if ( if (
_schedule && _schedule &&
_schedule.length > 5 && _schedule.length > 5 &&
@@ -46,8 +44,11 @@ const run = async () => {
}; };
app app
.listen(config.cronPort, () => { .listen(config.cronPort, async () => {
run(); await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default();
await run();
Logger.info(` Logger.info(`
################################################ ################################################
🛡️ Schedule listening on port: ${config.cronPort} 🛡️ 🛡️ Schedule listening on port: ${config.cronPort} 🛡️
+104 -33
View File
@@ -6,32 +6,25 @@ import { Crontab, 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 } from '../config/util'; import { getFileContentByName, concurrentRun } from '../config/util';
import PQueue from 'p-queue'; import PQueue from 'p-queue';
import { promises, existsSync } from 'fs'; import { promises, existsSync } from 'fs';
import { promisify } from 'util'; import { promisify } from 'util';
import { dbs } from '../loaders/db';
@Service() @Service()
export default class CronService { export default class CronService {
private cronDb = new DataStore({ filename: config.cronDbFile }); private cronDb = dbs.cronDb;
private queue = new PQueue({ private queue = new PQueue({
concurrency: parseInt(process.env.MaxConcurrentNum) || 5, concurrency: parseInt(process.env.MaxConcurrentNum as string) || 5,
}); });
constructor(@Inject('logger') private logger: winston.Logger) { constructor(@Inject('logger') private logger: winston.Logger) {}
this.cronDb.loadDatabase((err) => {
if (err) throw err;
});
}
public getDb(): DataStore {
return this.cronDb;
}
private isSixCron(cron: Crontab) { private isSixCron(cron: Crontab) {
const { schedule } = cron; const { schedule } = cron;
if (schedule.split(' ').length === 6) { if (schedule.split(/ +/).length === 6) {
return true; return true;
} }
return false; return false;
@@ -90,16 +83,38 @@ export default class CronService {
status, status,
pid, pid,
log_path, log_path,
last_running_time = 0,
last_execution_time = 0,
}: { }: {
ids: string[]; ids: string[];
status: CrontabStatus; status: CrontabStatus;
pid: number; pid: number;
log_path: string; log_path: string;
last_running_time: number;
last_execution_time: number;
}) { }) {
const options: any = {
status,
pid,
log_path,
last_execution_time,
};
if (last_running_time > 0) {
options.last_running_time = last_running_time;
}
return new Promise((resolve) => {
this.cronDb.update( this.cronDb.update(
{ _id: { $in: ids } }, { _id: { $in: ids } },
{ $set: { status, pid, log_path } }, {
$set: options,
},
{ multi: true, returnUpdatedDocs: true },
(err) => {
resolve(null);
},
); );
});
} }
public async remove(ids: string[]) { public async remove(ids: string[]) {
@@ -115,10 +130,38 @@ export default class CronService {
}); });
} }
public async pin(ids: string[]) {
return new Promise((resolve: any) => {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { isPinned: 1 } },
{ multi: true },
async (err) => {
resolve();
},
);
});
}
public async unPin(ids: string[]) {
return new Promise((resolve: any) => {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { isPinned: 0 } },
{ multi: true },
async (err) => {
resolve();
},
);
});
}
public async crontabs(searchText?: string): Promise<Crontab[]> { public async crontabs(searchText?: string): Promise<Crontab[]> {
let query = {}; let query = {};
if (searchText) { if (searchText) {
const reg = new RegExp(searchText, 'i'); const encodeText = encodeURIComponent(searchText);
const reg = new RegExp(`${searchText}|${encodeText}`, 'i');
query = { query = {
$or: [ $or: [
{ {
@@ -157,10 +200,10 @@ export default class CronService {
{ $set: { status: CrontabStatus.queued } }, { $set: { status: CrontabStatus.queued } },
{ multi: true }, { multi: true },
); );
for (let i = 0; i < ids.length; i++) { concurrentRun(
const id = ids[i]; ids.map((id) => () => this.runSingle(id)),
this.queue.add(() => this.runSingle(id)); 10,
} );
} }
public async stop(ids: string[]) { public async stop(ids: string[]) {
@@ -181,7 +224,9 @@ export default class CronService {
const str = err ? `\n${err}` : ''; const str = err ? `\n${err}` : '';
fs.appendFileSync( fs.appendFileSync(
`${doc.log_path}`, `${doc.log_path}`,
`${str}\n## 执行结束... ${new Date().toLocaleString()} `, `${str}\n## 执行结束... ${new Date()
.toLocaleString('zh', { hour12: false })
.replace(' 24:', ' 00:')} `,
); );
} }
} }
@@ -196,20 +241,23 @@ export default class CronService {
}); });
} }
private async killTask(name: string) { public async killTask(name: string) {
let taskCommond = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`; let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
const execAsync = promisify(exec); const execAsync = promisify(exec);
try { try {
let pid = (await execAsync(taskCommond)).stdout; let pid = (await execAsync(taskCommand)).stdout;
if (pid) { if (pid) {
pid = (await execAsync(`pstree -p ${pid}`)).stdout; pid = (await execAsync(`pstree -p ${pid}`)).stdout;
} else { } else {
return; return;
} }
const pids = pid.match(/\d+/g); let pids = pid.match(/\(\d+/g);
const killLogs = []; const killLogs = [];
if (pids && pids.length > 0) {
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
pids = pids.slice(0, 3);
for (const id of pids) { for (const id of pids) {
const c = `kill -9 ${id}`; const c = `kill -9 ${id.slice(1)}`;
const { stdout, stderr } = await execAsync(c); const { stdout, stderr } = await execAsync(c);
if (stderr) { if (stderr) {
killLogs.push(stderr); killLogs.push(stderr);
@@ -218,6 +266,7 @@ export default class CronService {
killLogs.push(stdout); killLogs.push(stdout);
} }
} }
}
return killLogs.length > 0 ? JSON.stringify(killLogs) : ''; return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
} catch (e) { } catch (e) {
return JSON.stringify(e); return JSON.stringify(e);
@@ -232,7 +281,7 @@ export default class CronService {
return; return;
} }
let { _id, command } = cron; let { _id, command, log_path } = cron;
this.logger.silly('Running job'); this.logger.silly('Running job');
this.logger.silly('ID: ' + _id); this.logger.silly('ID: ' + _id);
@@ -246,12 +295,34 @@ export default class CronService {
cmdStr = `${cmdStr} now`; cmdStr = `${cmdStr} now`;
} }
const cp = spawn(cmdStr, { shell: '/bin/bash', detached: true }); const cp = spawn(cmdStr, { shell: '/bin/bash' });
this.cronDb.update( this.cronDb.update(
{ _id }, { _id },
{ $set: { status: CrontabStatus.running, pid: cp.pid } }, { $set: { status: CrontabStatus.running, pid: cp.pid } },
); );
cp.stderr.on('data', (data) => {
if (log_path) {
fs.appendFileSync(`${log_path}`, `${data}`);
}
});
cp.on('error', (err) => {
if (log_path) {
fs.appendFileSync(`${log_path}`, `${JSON.stringify(err)}`);
}
});
cp.on('exit', (code, signal) => {
this.logger.info(
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
);
this.cronDb.update(
{ _id },
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
);
resolve();
});
cp.on('close', (code) => { cp.on('close', (code) => {
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
this.cronDb.update( this.cronDb.update(
{ _id }, { _id },
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } }, { $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
@@ -291,10 +362,14 @@ export default class CronService {
public async log(_id: string) { public async log(_id: string) {
const doc = await this.get(_id); const doc = await this.get(_id);
if (!doc) {
return '';
}
if (doc.log_path) { if (doc.log_path) {
return getFileContentByName(`${doc.log_path}`); return getFileContentByName(`${doc.log_path}`);
} }
const [, commandStr, url] = doc.command.split(' '); const [, commandStr, url] = doc.command.split(/ +/);
let logPath = this.getKey(commandStr); let logPath = this.getKey(commandStr);
const isQlCommand = doc.command.startsWith('ql '); const isQlCommand = doc.command.startsWith('ql ');
const key = const key =
@@ -334,7 +409,7 @@ export default class CronService {
const tabs = await this.crontabs(); const tabs = await this.crontabs();
var crontab_string = ''; var crontab_string = '';
tabs.forEach((tab) => { tabs.forEach((tab) => {
const _schedule = tab.schedule && tab.schedule.split(' '); const _schedule = tab.schedule && tab.schedule.split(/ +/);
if (tab.isDisabled === 1 || _schedule.length !== 5) { if (tab.isDisabled === 1 || _schedule.length !== 5) {
crontab_string += '# '; crontab_string += '# ';
crontab_string += tab.schedule; crontab_string += tab.schedule;
@@ -359,10 +434,6 @@ export default class CronService {
this.cronDb.update({}, { $set: { saved: true } }, { multi: true }); this.cronDb.update({}, { $set: { saved: true } }, { multi: true });
} }
private reload_db() {
this.cronDb.loadDatabase();
}
public import_crontab() { public import_crontab() {
exec('crontab -l', (error, stdout, stderr) => { exec('crontab -l', (error, stdout, stderr) => {
var lines = stdout.split('\n'); var lines = stdout.split('\n');
+286
View File
@@ -0,0 +1,286 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import config from '../config';
import DataStore from 'nedb';
import {
Dependence,
InstallDependenceCommandTypes,
DependenceStatus,
DependenceTypes,
unInstallDependenceCommandTypes,
} from '../data/dependence';
import _ from 'lodash';
import { spawn } from 'child_process';
import SockService from './sock';
import { dbs } from '../loaders/db';
@Service()
export default class DependenceService {
private dependenceDb = dbs.dependenceDb;
constructor(
@Inject('logger') private logger: winston.Logger,
private sockService: SockService,
) {}
public async create(payloads: Dependence[]): Promise<Dependence[]> {
const tabs = payloads.map((x) => {
const tab = new Dependence({ ...x, status: DependenceStatus.installing });
return tab;
});
const docs = await this.insert(tabs);
this.installOrUninstallDependencies(docs);
return docs;
}
public async insert(payloads: Dependence[]): Promise<Dependence[]> {
return new Promise((resolve) => {
this.dependenceDb.insert(payloads, (err, docs) => {
if (err) {
this.logger.error(err);
} else {
resolve(docs);
}
});
});
}
public async update(
payload: Dependence & { _id: string },
): Promise<Dependence> {
const { _id, ...other } = payload;
const doc = await this.get(_id);
const tab = new Dependence({
...doc,
...other,
status: DependenceStatus.installing,
});
const newDoc = await this.updateDb(tab);
this.installOrUninstallDependencies([newDoc]);
return newDoc;
}
private async updateDb(payload: Dependence): Promise<Dependence> {
return new Promise((resolve) => {
this.dependenceDb.update(
{ _id: payload._id },
payload,
{ returnUpdatedDocs: true },
(err, num, doc) => {
if (err) {
this.logger.error(err);
} else {
resolve(doc as Dependence);
}
},
);
});
}
public async remove(ids: string[]) {
return new Promise((resolve: any) => {
this.dependenceDb.update(
{ _id: { $in: ids } },
{ $set: { status: DependenceStatus.removing, log: [] } },
{ multi: true, returnUpdatedDocs: true },
async (err, num, docs: Dependence[]) => {
this.installOrUninstallDependencies(docs, false);
resolve(docs);
},
);
});
}
public async removeDb(ids: string[]) {
return new Promise((resolve: any) => {
this.dependenceDb.remove(
{ _id: { $in: ids } },
{ multi: true },
async (err) => {
resolve();
},
);
});
}
public async dependencies(
{ searchValue, type }: { searchValue: string; type: string },
sort: any = { position: -1 },
query: any = {},
): Promise<Dependence[]> {
let condition = { ...query, type: DependenceTypes[type as any] };
if (searchValue) {
const reg = new RegExp(searchValue);
condition = {
...condition,
$or: [
{
name: reg,
},
],
};
}
const newDocs = await this.find(condition, sort);
return newDocs;
}
public async reInstall(ids: string[]): Promise<Dependence[]> {
return new Promise((resolve: any) => {
this.dependenceDb.update(
{ _id: { $in: ids } },
{ $set: { status: DependenceStatus.installing, log: [] } },
{ multi: true, returnUpdatedDocs: true },
async (err, num, docs: Dependence[]) => {
await this.installOrUninstallDependencies(docs);
resolve(docs);
},
);
});
}
private async find(query: any, sort: any): Promise<Dependence[]> {
return new Promise((resolve) => {
this.dependenceDb
.find(query)
.sort({ ...sort })
.exec((err, docs) => {
resolve(docs);
});
});
}
public async get(_id: string): Promise<Dependence> {
return new Promise((resolve) => {
this.dependenceDb.find({ _id }).exec((err, docs) => {
resolve(docs[0]);
});
});
}
private async updateLog(ids: string[], log: string): Promise<void> {
return new Promise((resolve) => {
this.dependenceDb.update(
{ _id: { $in: ids } },
{ $push: { log } },
{ multi: true },
(err, num, doc) => {
if (err) {
this.logger.error(err);
} else {
resolve();
}
},
);
});
}
public installOrUninstallDependencies(
dependencies: Dependence[],
isInstall: boolean = true,
) {
return new Promise((resolve) => {
if (dependencies.length === 0) {
resolve(null);
return;
}
const depNames = dependencies.map((x) => x.name).join(' ');
const depRunCommand = (
isInstall
? InstallDependenceCommandTypes
: unInstallDependenceCommandTypes
)[dependencies[0].type as any];
const actionText = isInstall ? '安装' : '删除';
const depIds = dependencies.map((x) => x._id) as string[];
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
const startTime = Date.now();
this.sockService.sendMessage({
type: 'installDependence',
message: `开始${actionText}依赖 ${depNames},开始时间 ${new Date(
startTime,
).toLocaleString()}`,
references: depIds,
});
this.updateLog(
depIds,
`开始${actionText}依赖 ${depNames},开始时间 ${new Date(
startTime,
).toLocaleString()}\n`,
);
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'installDependence',
message: data.toString(),
references: depIds,
});
this.updateLog(depIds, data.toString());
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'installDependence',
message: data.toString(),
references: depIds,
});
this.updateLog(depIds, data.toString());
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'installDependence',
message: JSON.stringify(err),
references: depIds,
});
this.updateLog(depIds, JSON.stringify(err));
resolve(null);
});
cp.on('close', (code) => {
const endTime = Date.now();
const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败';
this.sockService.sendMessage({
type: 'installDependence',
message: `依赖${actionText}${resultText},结束时间 ${new Date(
endTime,
).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`,
references: depIds,
});
this.updateLog(
depIds,
`依赖${actionText}${resultText},结束时间 ${new Date(
endTime,
).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`,
);
let status = null;
if (isSucceed) {
status = isInstall
? DependenceStatus.installed
: DependenceStatus.removed;
} else {
status = isInstall
? DependenceStatus.installFailed
: DependenceStatus.removeFailed;
}
this.dependenceDb.update(
{ _id: { $in: depIds } },
{
$set: { status },
$unset: { pid: true },
},
{ multi: true },
);
// 如果删除依赖成功,3秒后删除数据库记录
if (isSucceed && !isInstall) {
setTimeout(() => {
this.removeDb(depIds);
}, 5000);
}
resolve(null);
});
});
}
}
+43 -33
View File
@@ -6,35 +6,37 @@ import * as fs from 'fs';
import DataStore from 'nedb'; import DataStore from 'nedb';
import { Env, EnvStatus, initEnvPosition } from '../data/env'; import { Env, EnvStatus, initEnvPosition } from '../data/env';
import _ from 'lodash'; import _ from 'lodash';
import { dbs } from '../loaders/db';
@Service() @Service()
export default class EnvService { export default class EnvService {
private cronDb = new DataStore({ filename: config.envDbFile }); private envDb = dbs.envDb;
constructor(@Inject('logger') private logger: winston.Logger) {
this.cronDb.loadDatabase((err) => {
if (err) throw err;
});
}
public async create(payload: Env): Promise<Env> { constructor(@Inject('logger') private logger: winston.Logger) {}
public async create(payloads: Env[]): Promise<Env[]> {
const envs = await this.envs(); const envs = await this.envs();
let position = initEnvPosition; let position = initEnvPosition;
if (envs && envs.length > 0 && envs[envs.length - 1].position) { if (envs && envs.length > 0 && envs[envs.length - 1].position) {
position = envs[envs.length - 1].position; position = envs[envs.length - 1].position as number;
} }
const tab = new Env({ ...payload, position: position / 2 }); const tabs = payloads.map((x) => {
const doc = await this.insert(tab); position = position / 2;
const tab = new Env({ ...x, position });
return tab;
});
const docs = await this.insert(tabs);
await this.set_envs(); await this.set_envs();
return doc; return docs;
} }
public async insert(payload: Env): Promise<Env> { public async insert(payloads: Env[]): Promise<Env[]> {
return new Promise((resolve) => { return new Promise((resolve) => {
this.cronDb.insert(payload, (err, doc) => { this.envDb.insert(payloads, (err, docs) => {
if (err) { if (err) {
this.logger.error(err); this.logger.error(err);
} else { } else {
resolve(doc); resolve(docs);
} }
}); });
}); });
@@ -51,7 +53,7 @@ export default class EnvService {
private async updateDb(payload: Env): Promise<Env> { private async updateDb(payload: Env): Promise<Env> {
return new Promise((resolve) => { return new Promise((resolve) => {
this.cronDb.update( this.envDb.update(
{ _id: payload._id }, { _id: payload._id },
payload, payload,
{ returnUpdatedDocs: true }, { returnUpdatedDocs: true },
@@ -68,14 +70,10 @@ export default class EnvService {
public async remove(ids: string[]) { public async remove(ids: string[]) {
return new Promise((resolve: any) => { return new Promise((resolve: any) => {
this.cronDb.remove( this.envDb.remove({ _id: { $in: ids } }, { multi: true }, async (err) => {
{ _id: { $in: ids } },
{ multi: true },
async (err) => {
await this.set_envs(); await this.set_envs();
resolve(); resolve();
}, });
);
}); });
} }
@@ -88,7 +86,7 @@ export default class EnvService {
fromIndex: number; fromIndex: number;
toIndex: number; toIndex: number;
}, },
) { ): Promise<Env> {
let targetPosition: number; let targetPosition: number;
const isUpward = fromIndex > toIndex; const isUpward = fromIndex > toIndex;
const envs = await this.envs(); const envs = await this.envs();
@@ -101,11 +99,11 @@ export default class EnvService {
? (envs[toIndex].position + envs[toIndex - 1].position) / 2 ? (envs[toIndex].position + envs[toIndex - 1].position) / 2
: (envs[toIndex].position + envs[toIndex + 1].position) / 2; : (envs[toIndex].position + envs[toIndex + 1].position) / 2;
} }
this.update({ const newDoc = await this.update({
_id, _id,
position: targetPosition, position: targetPosition,
}); });
await this.set_envs(); return newDoc;
} }
public async envs( public async envs(
@@ -115,7 +113,9 @@ export default class EnvService {
): Promise<Env[]> { ): Promise<Env[]> {
let condition = { ...query }; let condition = { ...query };
if (searchText) { if (searchText) {
const reg = new RegExp(searchText); const encodeText = encodeURIComponent(searchText);
const reg = new RegExp(`${searchText}|${encodeText}`, 'i');
condition = { condition = {
$or: [ $or: [
{ {
@@ -136,7 +136,7 @@ export default class EnvService {
private async find(query: any, sort: any): Promise<Env[]> { private async find(query: any, sort: any): Promise<Env[]> {
return new Promise((resolve) => { return new Promise((resolve) => {
this.cronDb this.envDb
.find(query) .find(query)
.sort({ ...sort }) .sort({ ...sort })
.exec((err, docs) => { .exec((err, docs) => {
@@ -147,7 +147,7 @@ export default class EnvService {
public async get(_id: string): Promise<Env> { public async get(_id: string): Promise<Env> {
return new Promise((resolve) => { return new Promise((resolve) => {
this.cronDb.find({ _id }).exec((err, docs) => { this.envDb.find({ _id }).exec((err, docs) => {
resolve(docs[0]); resolve(docs[0]);
}); });
}); });
@@ -155,7 +155,7 @@ export default class EnvService {
public async getBySort(sort: any): Promise<Env> { public async getBySort(sort: any): Promise<Env> {
return new Promise((resolve) => { return new Promise((resolve) => {
this.cronDb this.envDb
.find({}) .find({})
.sort({ ...sort }) .sort({ ...sort })
.limit(1) .limit(1)
@@ -167,7 +167,7 @@ export default class EnvService {
public async disabled(ids: string[]) { public async disabled(ids: string[]) {
return new Promise((resolve: any) => { return new Promise((resolve: any) => {
this.cronDb.update( this.envDb.update(
{ _id: { $in: ids } }, { _id: { $in: ids } },
{ $set: { status: EnvStatus.disabled } }, { $set: { status: EnvStatus.disabled } },
{ multi: true }, { multi: true },
@@ -181,7 +181,7 @@ export default class EnvService {
public async enabled(ids: string[]) { public async enabled(ids: string[]) {
return new Promise((resolve: any) => { return new Promise((resolve: any) => {
this.cronDb.update( this.envDb.update(
{ _id: { $in: ids } }, { _id: { $in: ids } },
{ $set: { status: EnvStatus.normal } }, { $set: { status: EnvStatus.normal } },
{ multi: true }, { multi: true },
@@ -195,7 +195,7 @@ export default class EnvService {
public async updateNames({ ids, name }: { ids: string[]; name: string }) { public async updateNames({ ids, name }: { ids: string[]; name: string }) {
return new Promise((resolve: any) => { return new Promise((resolve: any) => {
this.cronDb.update( this.envDb.update(
{ _id: { $in: ids } }, { _id: { $in: ids } },
{ $set: { name } }, { $set: { name } },
{ multi: true }, { multi: true },
@@ -218,11 +218,21 @@ export default class EnvService {
for (const key in groups) { for (const key in groups) {
if (Object.prototype.hasOwnProperty.call(groups, key)) { if (Object.prototype.hasOwnProperty.call(groups, key)) {
const group = groups[key]; const group = groups[key];
env_string += `export ${key}="${_(group)
// 忽略不符合bash要求的环境变量名称
if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) {
let value = _(group)
.filter((x) => x.status !== EnvStatus.disabled) .filter((x) => x.status !== EnvStatus.disabled)
.map('value') .map('value')
.join('&') .join('&')
.replace(/ /g, '')}"\n`; .replace(/ /g, '');
if (/"/.test(value)) {
value = `'${value}'`;
} else {
value = `"${value}"`;
}
env_string += `export ${key}=${value}\n`;
}
} }
} }
fs.writeFileSync(config.envFile, env_string); fs.writeFileSync(config.envFile, env_string);
+354
View File
@@ -0,0 +1,354 @@
import { NotificationInfo } from '../data/notify';
import { Service, Inject } from 'typedi';
import winston from 'winston';
import UserService from './user';
import got from 'got';
import nodemailer from 'nodemailer';
import crypto from 'crypto';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
@Service()
export default class NotificationService {
@Inject((type) => UserService)
private userService!: UserService;
private modeMap = new Map([
['gotify', this.gotify],
['goCqHttpBot', this.goCqHttpBot],
['serverChan', this.serverChan],
['bark', this.bark],
['telegramBot', this.telegramBot],
['dingtalkBot', this.dingtalkBot],
['weWorkBot', this.weWorkBot],
['weWorkApp', this.weWorkApp],
['iGot', this.iGot],
['pushPlus', this.pushPlus],
['email', this.email],
]);
private timeout = 10000;
private title = '';
private content = '';
private params!: Omit<NotificationInfo, 'type'>;
constructor(@Inject('logger') private logger: winston.Logger) {}
public async notify(
title: string,
content: string,
): Promise<boolean | undefined> {
const { type, ...rest } = await this.userService.getNotificationMode();
if (type) {
this.title = title;
this.content = content;
this.params = rest;
const notificationModeAction = this.modeMap.get(type);
try {
return await notificationModeAction?.call(this);
} catch (error: any) {
return false;
}
}
return false;
}
public async testNotify(
info: NotificationInfo,
title: string,
content: string,
) {
const { type, ...rest } = info;
if (type) {
this.title = title;
this.content = content;
this.params = rest;
const notificationModeAction = this.modeMap.get(type);
return await notificationModeAction?.call(this);
}
return true;
}
private async gotify() {
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
const res: any = await got
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
timeout: this.timeout,
retry: 0,
body: `title=${encodeURIComponent(
this.title,
)}&message=${encodeURIComponent(
this.content,
)}&priority=${gotifyPriority}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
})
.json();
return typeof res.id === 'number';
}
private async goCqHttpBot() {
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
const res: any = await got
.post(
`${goCqHttpBotUrl}?access_token=${goCqHttpBotToken}&${goCqHttpBotQq}`,
{
timeout: this.timeout,
retry: 0,
json: { message: `${this.title}\n${this.content}` },
},
)
.json();
return res.retcode === 0;
}
private async serverChan() {
const { serverChanKey } = this.params;
const url = serverChanKey.startsWith('SCT')
? `https://sctapi.ftqq.com/${serverChanKey}.send`
: `https://sc.ftqq.com/${serverChanKey}.send`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
body: `title=${this.title}&desp=${this.content}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return res.errno === 0 || res.data.errno === 0;
}
private async bark() {
let { barkPush, barkSound, barkGroup } = this.params;
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
barkPush = `https://api.day.app/${barkPush}`;
}
const url = `${barkPush}/${encodeURIComponent(
this.title,
)}/${encodeURIComponent(
this.content,
)}?sound=${barkSound}&group=${barkGroup}`;
const res: any = await got
.get(url, {
timeout: this.timeout,
retry: 0,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return res.code === 200;
}
private async telegramBot() {
const {
telegramBotApiHost,
telegramBotProxyAuth,
telegramBotProxyHost,
telegramBotProxyPort,
telegramBotToken,
telegramBotUserId,
} = this.params;
const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : '';
const url = `https://${
telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org'
}/bot${telegramBotToken}/sendMessage`;
let agent;
if (telegramBotProxyHost && telegramBotProxyPort) {
const options: any = {
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: 256,
maxFreeSockets: 256,
proxy: `http://${authStr}${telegramBotProxyHost}:${telegramBotProxyPort}`,
};
const httpAgent = new HttpProxyAgent(options);
const httpsAgent = new HttpsProxyAgent(options);
agent = {
http: httpAgent,
https: httpsAgent,
};
}
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
agent,
})
.json();
return !!res.ok;
}
private async dingtalkBot() {
const { dingtalkBotSecret, dingtalkBotToken } = this.params;
let secretParam = '';
if (dingtalkBotSecret) {
const dateNow = Date.now();
const hmac = crypto.createHmac('sha256', dingtalkBotSecret);
hmac.update(`${dateNow}\n${dingtalkBotSecret}`);
const result = encodeURIComponent(hmac.digest('base64'));
secretParam = `&timestamp=${dateNow}&sign=${result}`;
}
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
json: {
msgtype: 'text',
text: {
content: ` ${this.title}\n\n${this.content}`,
},
},
})
.json();
return res.errcode === 0;
}
private async weWorkBot() {
const { weWorkBotKey } = this.params;
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
json: {
msgtype: 'text',
text: {
content: ` ${this.title}\n\n${this.content}`,
},
},
})
.json();
return res.errcode === 0;
}
private async weWorkApp() {
const { weWorkAppKey } = this.params;
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
weWorkAppKey.split(',');
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
const { access_token } = await got
.post(url, {
timeout: this.timeout,
retry: 0,
json: {
corpid,
corpsecret,
},
})
.json();
let options: any = {
msgtype: 'mpnews',
mpnews: {
articles: [
{
title: `${this.title}`,
thumb_media_id,
author: `智能助手`,
content_source_url: ``,
content: `${this.content.replace(/\n/g, '<br/>')}`,
digest: `${this.content}`,
},
],
},
};
switch (thumb_media_id) {
case '0':
options = {
msgtype: 'textcard',
textcard: {
title: `${this.title}`,
description: `${this.content}`,
url: 'https://github.com/whyour/qinglong',
btntxt: '更多',
},
};
break;
case '1':
options = {
msgtype: 'text',
text: {
content: `${this.title}\n\n${this.content}`,
},
};
break;
}
const res: any = await got
.post(
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${access_token}`,
{
timeout: this.timeout,
retry: 0,
json: {
touser,
agentid,
safe: '0',
...options,
},
},
)
.json();
return res.errcode === 0;
}
private async iGot() {
const { iGotPushKey } = this.params;
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
body: `title=${this.title}&content=${this.content}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return res.ret === 0;
}
private async pushPlus() {
const { pushPlusToken, pushPlusUser } = this.params;
const url = `https://www.pushplus.plus/send`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
json: {
token: `${pushPlusToken}`,
title: `${this.title}`,
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
topic: `${pushPlusUser || ''}`,
},
})
.json();
return res.code === 200;
}
private async email() {
const { emailPass, emailService, emailUser } = this.params;
const transporter = nodemailer.createTransport({
service: emailService,
auth: {
user: emailUser,
pass: emailPass,
},
});
const info = await transporter.sendMail({
from: `"青龙快讯" <${emailUser}>`,
to: `${emailUser}`,
subject: `${this.title}`,
html: `${this.content.replace(/\n/g, '<br/>')}`,
});
transporter.close();
return !!info.messageId;
}
}
+173
View File
@@ -0,0 +1,173 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { createRandomString } from '../config/util';
import config from '../config';
import DataStore from 'nedb';
import { App } from '../data/open';
import { v4 as uuidV4 } from 'uuid';
import { dbs } from '../loaders/db';
@Service()
export default class OpenService {
private appDb = dbs.appDb;
constructor(@Inject('logger') private logger: winston.Logger) {}
public async findTokenByValue(token: string): Promise<App> {
return new Promise((resolve) => {
this.appDb.find(
{ tokens: { $elemMatch: { value: token } } },
(err, docs) => {
if (err) {
this.logger.error(err);
} else {
resolve(docs[0]);
}
},
);
});
}
public async create(payload: App): Promise<App> {
const tab = new App({ ...payload });
tab.client_id = createRandomString(12, 12);
tab.client_secret = createRandomString(24, 24);
const docs = await this.insert([tab]);
return { ...docs[0], tokens: [] };
}
public async insert(payloads: App[]): Promise<App[]> {
return new Promise((resolve) => {
this.appDb.insert(payloads, (err, docs) => {
if (err) {
this.logger.error(err);
} else {
resolve(docs);
}
});
});
}
public async update(payload: App): Promise<App> {
const { _id, client_id, client_secret, tokens, ...other } = payload;
const doc = await this.get(_id);
const tab = new App({ ...doc, ...other });
const newDoc = await this.updateDb(tab);
return { ...newDoc, tokens: [] };
}
private async updateDb(payload: App): Promise<App> {
return new Promise((resolve) => {
this.appDb.update(
{ _id: payload._id },
payload,
{ returnUpdatedDocs: true },
(err, num, doc, up) => {
if (err) {
this.logger.error(err);
} else {
resolve(doc);
}
},
);
});
}
public async remove(ids: string[]) {
return new Promise((resolve: any) => {
this.appDb.remove({ _id: { $in: ids } }, { multi: true }, async (err) => {
resolve();
});
});
}
public async resetSecret(_id: string): Promise<App> {
const doc = await this.get(_id);
const tab = new App({ ...doc });
tab.client_secret = createRandomString(24, 24);
tab.tokens = [];
const newDoc = await this.updateDb(tab);
return newDoc;
}
public async list(
searchText: string = '',
sort: any = {},
query: any = {},
): Promise<App[]> {
let condition = { ...query };
if (searchText) {
const encodeText = encodeURIComponent(searchText);
const reg = new RegExp(`${searchText}|${encodeText}`, 'i');
condition = {
$or: [
{
value: reg,
},
{
name: reg,
},
{
remarks: reg,
},
],
};
}
const newDocs = await this.find(condition, sort);
return newDocs.map((x) => ({ ...x, tokens: [] }));
}
private async find(query: any, sort: any): Promise<App[]> {
return new Promise((resolve) => {
this.appDb
.find(query)
.sort({ ...sort })
.exec((err, docs) => {
resolve(docs);
});
});
}
public async get(_id: string): Promise<App> {
return new Promise((resolve) => {
this.appDb.find({ _id }).exec((err, docs) => {
resolve(docs[0]);
});
});
}
public async authToken({
client_id,
client_secret,
}: {
client_id: string;
client_secret: string;
}): Promise<any> {
const token = uuidV4();
const expiration = Math.round(Date.now() / 1000) + 2592000; // 2592000 30天
return new Promise((resolve) => {
this.appDb.find({ client_id, client_secret }).exec((err, docs) => {
if (docs && docs[0]) {
this.appDb.update(
{ client_id, client_secret },
{ $push: { tokens: { value: token, expiration } } },
{},
(err, num, doc) => {
resolve({
code: 200,
data: {
token,
token_type: 'Bearer',
expiration,
},
});
},
);
} else {
resolve({ code: 400, message: 'client_id或client_seret有误' });
}
});
});
}
}
+62
View File
@@ -0,0 +1,62 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import nodeSchedule from 'node-schedule';
import { Crontab } from '../data/cron';
import { exec } from 'child_process';
@Service()
export default class ScheduleService {
private scheduleStacks = new Map<string, nodeSchedule.Job>();
constructor(@Inject('logger') private logger: winston.Logger) {}
async generateSchedule({ _id = '', command, name, schedule }: Crontab) {
this.logger.info(
'[创建定时任务],任务ID: %scron: %s,任务名: %s,执行命令: %s',
_id,
schedule,
name,
command,
);
this.scheduleStacks.set(
_id,
nodeSchedule.scheduleJob(_id, schedule, async () => {
try {
exec(command, async (error, stdout, stderr) => {
if (error) {
await this.logger.info(
'执行任务%s失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
error,
);
}
if (stderr) {
await this.logger.info(
'执行任务%s失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
stderr,
);
}
});
} catch (error) {
await this.logger.info(
'执行任务%s失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
error,
);
} finally {
}
}),
);
}
async cancelSchedule({ _id = '', name }: Crontab) {
this.logger.info('[取消定时任务],任务名:%s', name);
this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel();
}
}
+61
View File
@@ -0,0 +1,61 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { spawn } from 'child_process';
import SockService from './sock';
import CronService from './cron';
@Service()
export default class ScriptService {
constructor(
@Inject('logger') private logger: winston.Logger,
private sockService: SockService,
private cronService: CronService,
) {}
public async runScript(path: string) {
const cp = spawn(`task -l ${path} now`, { shell: '/bin/bash' });
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: data.toString(),
});
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: data.toString(),
});
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: JSON.stringify(err),
});
});
cp.on('close', (err) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: `执行结束`,
});
});
return { code: 200 };
}
public async stopScript(path: string) {
const err = await this.cronService.killTask(`task -l ${path} now`);
const str = err ? `\n${err}` : '';
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: `${str}\n## 执行结束... ${new Date()
.toLocaleString('zh', { hour12: false })
.replace(' 24:', ' 00:')} `,
});
return { code: 200 };
}
}
+34
View File
@@ -0,0 +1,34 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { Connection } from 'sockjs';
import { SockMessage } from '../data/sock';
@Service()
export default class SockService {
private clients: Connection[] = [];
constructor(@Inject('logger') private logger: winston.Logger) {}
public getClients() {
return this.clients;
}
public addClient(conn: Connection) {
if (this.clients.indexOf(conn) === -1) {
this.clients.push(conn);
}
}
public removeClient(conn: Connection) {
const index = this.clients.indexOf(conn);
if (index !== -1) {
this.clients.splice(index, 1);
}
}
public sendMessage(msg: SockMessage) {
this.clients.forEach((x) => {
x.write(JSON.stringify(msg));
});
}
}
+184
View File
@@ -0,0 +1,184 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import config from '../config';
import * as fs from 'fs';
import _ from 'lodash';
import { AuthDataType, AuthInfo, LoginStatus } from '../data/auth';
import { NotificationInfo } from '../data/notify';
import NotificationService from './notify';
import ScheduleService from './schedule';
import { spawn } from 'child_process';
import SockService from './sock';
import got from 'got';
import { dbs } from '../loaders/db';
@Service()
export default class SystemService {
@Inject((type) => NotificationService)
private notificationService!: NotificationService;
private authDb = dbs.authDb;
constructor(
@Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService,
private sockService: SockService,
) {}
public async getLogRemoveFrequency() {
return new Promise((resolve) => {
this.authDb
.find({ type: AuthDataType.removeLogFrequency })
.exec((err, docs) => {
if (err || docs.length === 0) {
resolve({});
} else {
resolve(docs[0].info);
}
});
});
}
private async updateAuthDb(payload: AuthInfo): Promise<any> {
return new Promise((resolve) => {
this.authDb.update(
{ type: payload.type },
{ ...payload },
{ upsert: true, returnUpdatedDocs: true },
(err, num, doc: any) => {
if (err) {
resolve({} as NotificationInfo);
} else {
resolve({ ...doc.info, _id: doc._id });
}
},
);
});
}
public async updateNotificationMode(notificationInfo: NotificationInfo) {
const code = Math.random().toString().slice(-6);
const isSuccess = await this.notificationService.testNotify(
notificationInfo,
'青龙',
`【蛟龙】测试通知 https://t.me/jiao_long`,
);
if (isSuccess) {
const result = await this.updateAuthDb({
type: AuthDataType.notification,
info: { ...notificationInfo },
});
return { code: 200, data: { ...result, code } };
} else {
return { code: 400, data: '通知发送失败,请检查参数' };
}
}
public async updateLogRemoveFrequency(frequency: number) {
const result = await this.updateAuthDb({
type: AuthDataType.removeLogFrequency,
info: { frequency },
});
const cron = {
_id: result._id,
name: '删除日志',
command: `ql rmlog ${frequency}`,
schedule: `5 23 */${frequency} * *`,
};
await this.scheduleService.cancelSchedule(cron);
if (frequency > 0) {
await this.scheduleService.generateSchedule(cron);
}
return { code: 200, data: { ...cron } };
}
public async checkUpdate() {
try {
const versionRegx = /.*export const version = \'(.*)\'\;/;
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
let lastVersion = '';
let lastLog = '';
try {
const result = await Promise.race([
got.get(config.lastVersionFile, { timeout: 1000, retry: 0 }),
got.get(`https://ghproxy.com/${config.lastVersionFile}`, {
timeout: 5000,
retry: 0,
}),
]);
const lastVersionFileContent = result.body;
lastVersion = lastVersionFileContent.match(versionRegx)![1];
lastLog = lastVersionFileContent.match(logRegx)
? lastVersionFileContent.match(logRegx)![1]
: '';
} catch (error) {}
return {
code: 200,
data: {
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion),
lastVersion,
lastLog,
},
};
} catch (error: any) {
return {
code: 400,
data: error.message,
};
}
}
private checkHasNewVersion(curVersion: string, lastVersion: string) {
const curArr = curVersion.split('.').map((x) => parseInt(x, 10));
const lastArr = lastVersion.split('.').map((x) => parseInt(x, 10));
if (curArr[0] < lastArr[0]) {
return true;
}
if (curArr[0] === lastArr[0] && curArr[1] < lastArr[1]) {
return true;
}
if (
curArr[0] === lastArr[0] &&
curArr[1] === lastArr[1] &&
curArr[2] < lastArr[2]
) {
return true;
}
return false;
}
public async updateSystem() {
const cp = spawn('ql -l update', { shell: '/bin/bash' });
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: `开始更新系统`,
});
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: data.toString(),
});
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: data.toString(),
});
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: JSON.stringify(err),
});
});
return { code: 200 };
}
}
+488
View File
@@ -0,0 +1,488 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { createRandomString, getNetIp, getPlatform } from '../config/util';
import config from '../config';
import * as fs from 'fs';
import _ from 'lodash';
import jwt from 'jsonwebtoken';
import { authenticator } from '@otplib/preset-default';
import DataStore from 'nedb';
import { AuthDataType, AuthInfo, LoginStatus } from '../data/auth';
import { NotificationInfo } from '../data/notify';
import NotificationService from './notify';
import { Request } from 'express';
import ScheduleService from './schedule';
import { spawn } from 'child_process';
import SockService from './sock';
import got from 'got';
import { dbs } from '../loaders/db';
@Service()
export default class UserService {
@Inject((type) => NotificationService)
private notificationService!: NotificationService;
private authDb = dbs.authDb;
constructor(
@Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService,
private sockService: SockService,
) {}
public async login(
payloads: {
username: string;
password: string;
},
req: Request,
needTwoFactor = true,
): Promise<any> {
if (!fs.existsSync(config.authConfigFile)) {
return this.initAuthInfo();
}
let { username, password } = payloads;
const content = this.getAuthInfo();
const timestamp = Date.now();
if (content) {
let {
username: cUsername,
password: cPassword,
retries = 0,
lastlogon,
lastip,
lastaddr,
twoFactorActivated,
twoFactorActived,
tokens = {},
platform,
} = content;
// patch old field
twoFactorActivated = twoFactorActivated || twoFactorActived;
if (
(cUsername === 'admin' && cPassword === 'admin') ||
!cUsername ||
!cPassword
) {
return this.initAuthInfo();
}
if (retries > 2 && Date.now() - lastlogon < Math.pow(3, retries) * 1000) {
return {
code: 410,
message: `失败次数过多,请${Math.round(
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
)}秒后重试`,
data: Math.round(
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
),
};
}
const { ip, address } = await getNetIp(req);
if (username === cUsername && password === cPassword) {
if (twoFactorActivated && needTwoFactor) {
this.updateAuthInfo(content, {
isTwoFactorChecking: true,
});
return {
code: 420,
message: '请输入两步验证token',
};
}
const data = createRandomString(50, 100);
const expiration = twoFactorActivated ? 30 : 3;
let token = jwt.sign({ data }, config.secret as any, {
expiresIn: 60 * 60 * 24 * expiration,
algorithm: 'HS384',
});
this.updateAuthInfo(content, {
token,
tokens: {
...tokens,
[req.platform]: token,
},
lastlogon: timestamp,
retries: 0,
lastip: ip,
lastaddr: address,
platform: req.platform,
isTwoFactorChecking: false,
});
await this.notificationService.notify(
'登录通知',
`你于${new Date(timestamp).toLocaleString()}${address} ${
req.platform
}端 登录成功,ip地址 ${ip}`,
);
await this.getLoginLog();
await this.insertDb({
type: AuthDataType.loginLog,
info: {
timestamp,
address,
ip,
platform: req.platform,
status: LoginStatus.success,
},
});
return {
code: 200,
data: { token, lastip, lastaddr, lastlogon, retries, platform },
};
} else {
this.updateAuthInfo(content, {
retries: retries + 1,
lastlogon: timestamp,
lastip: ip,
lastaddr: address,
platform: req.platform,
});
await this.notificationService.notify(
'登录通知',
`你于${new Date(timestamp).toLocaleString()}${address} ${
req.platform
}端 登录失败,ip地址 ${ip}`,
);
await this.getLoginLog();
await this.insertDb({
type: AuthDataType.loginLog,
info: {
timestamp,
address,
ip,
platform: req.platform,
status: LoginStatus.fail,
},
});
return { code: 400, message: config.authError };
}
} else {
return this.initAuthInfo();
}
}
public async logout(platform: string): Promise<any> {
const authInfo = this.getAuthInfo();
this.updateAuthInfo(authInfo, {
token: '',
tokens: { ...authInfo.tokens, [platform]: '' },
});
}
public async getLoginLog(): Promise<AuthInfo[]> {
return new Promise((resolve) => {
this.authDb.find({ type: AuthDataType.loginLog }).exec((err, docs) => {
if (err || docs.length === 0) {
resolve([]);
} else {
const result = docs.sort(
(a, b) => b.info.timestamp - a.info.timestamp,
);
if (result.length > 100) {
this.authDb.remove({ _id: result[result.length - 1]._id });
}
resolve(result.map((x) => x.info));
}
});
});
}
private async insertDb(payload: AuthInfo): Promise<AuthInfo> {
return new Promise((resolve) => {
this.authDb.insert(payload, (err, doc) => {
if (err) {
this.logger.error(err);
} else {
resolve(doc);
}
});
});
}
private initAuthInfo() {
const newPassword = createRandomString(16, 22);
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({
username: 'admin',
password: newPassword,
}),
);
return {
code: 100,
message: '已初始化密码,请前往auth.json查看并重新登录',
};
}
public async updateUsernameAndPassword({
username,
password,
}: {
username: string;
password: string;
}) {
if (password === 'admin') {
return { code: 400, message: '密码不能设置为admin' };
}
const authInfo = this.getAuthInfo();
this.updateAuthInfo(authInfo, { username, password });
return { code: 200, message: '更新成功' };
}
public getUserInfo(): Promise<any> {
return new Promise((resolve) => {
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
if (err) console.log(err);
resolve(JSON.parse(data));
});
});
}
public initTwoFactor() {
const secret = authenticator.generateSecret();
const authInfo = this.getAuthInfo();
const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);
this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
return { secret, url: otpauth };
}
public activeTwoFactor(code: string) {
const authInfo = this.getAuthInfo();
const isValid = authenticator.verify({
token: code,
secret: authInfo.twoFactorSecret,
});
if (isValid) {
this.updateAuthInfo(authInfo, { twoFactorActivated: true });
}
return isValid;
}
public async twoFactorLogin(
{
username,
password,
code,
}: { username: string; password: string; code: string },
req: any,
) {
const authInfo = this.getAuthInfo();
const { isTwoFactorChecking, twoFactorSecret } = authInfo;
if (!isTwoFactorChecking) {
return { code: 450, message: '未知错误' };
}
const isValid = authenticator.verify({
token: code,
secret: twoFactorSecret,
});
if (isValid) {
return this.login({ username, password }, req, false);
} else {
const { ip, address } = await getNetIp(req);
this.updateAuthInfo(authInfo, {
lastip: ip,
lastaddr: address,
platform: req.platform,
});
return { code: 430, message: '验证失败' };
}
}
public deactiveTwoFactor() {
const authInfo = this.getAuthInfo();
this.updateAuthInfo(authInfo, {
twoFactorActivated: false,
twoFactorActived: false,
twoFactorSecret: '',
});
return true;
}
private getAuthInfo() {
const content = fs.readFileSync(config.authConfigFile, 'utf8');
return JSON.parse(content || '{}');
}
private updateAuthInfo(authInfo: any, info: any) {
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({ ...authInfo, ...info }),
);
}
public async getNotificationMode(): Promise<NotificationInfo> {
return new Promise((resolve) => {
this.authDb
.find({ type: AuthDataType.notification })
.exec((err, docs) => {
if (err || docs.length === 0) {
resolve({} as NotificationInfo);
} else {
resolve(docs[0].info);
}
});
});
}
public async getLogRemoveFrequency() {
return new Promise((resolve) => {
this.authDb
.find({ type: AuthDataType.removeLogFrequency })
.exec((err, docs) => {
if (err || docs.length === 0) {
resolve({});
} else {
resolve(docs[0].info);
}
});
});
}
private async updateAuthDb(payload: AuthInfo): Promise<any> {
return new Promise((resolve) => {
this.authDb.update(
{ type: payload.type },
{ ...payload },
{ upsert: true, returnUpdatedDocs: true },
(err, num, doc: any) => {
if (err) {
resolve({} as NotificationInfo);
} else {
resolve({ ...doc.info, _id: doc._id });
}
},
);
});
}
public async updateNotificationMode(notificationInfo: NotificationInfo) {
const code = Math.random().toString().slice(-6);
const isSuccess = await this.notificationService.testNotify(
notificationInfo,
'青龙',
`【蛟龙】测试通知 https://t.me/jiao_long`,
);
if (isSuccess) {
const result = await this.updateAuthDb({
type: AuthDataType.notification,
info: { ...notificationInfo },
});
return { code: 200, data: { ...result, code } };
} else {
return { code: 400, data: '通知发送失败,请检查参数' };
}
}
public async updateLogRemoveFrequency(frequency: number) {
const result = await this.updateAuthDb({
type: AuthDataType.removeLogFrequency,
info: { frequency },
});
const cron = {
_id: result._id,
name: '删除日志',
command: `ql rmlog ${frequency}`,
schedule: `5 23 */${frequency} * *`,
};
await this.scheduleService.cancelSchedule(cron);
if (frequency > 0) {
await this.scheduleService.generateSchedule(cron);
}
return { code: 200, data: { ...cron } };
}
public async checkUpdate() {
try {
const versionRegx = /.*export const version = \'(.*)\'\;/;
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
let lastVersion = '';
let lastLog = '';
try {
const result = await Promise.race([
got.get(config.lastVersionFile, { timeout: 1000, retry: 0 }),
got.get(`https://ghproxy.com/${config.lastVersionFile}`, {
timeout: 5000,
retry: 0,
}),
]);
const lastVersionFileContent = result.body;
lastVersion = lastVersionFileContent.match(versionRegx)![1];
lastLog = lastVersionFileContent.match(logRegx)
? lastVersionFileContent.match(logRegx)![1]
: '';
} catch (error) {}
return {
code: 200,
data: {
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion),
lastVersion,
lastLog,
},
};
} catch (error: any) {
return {
code: 400,
data: error.message,
};
}
}
private checkHasNewVersion(curVersion: string, lastVersion: string) {
const curArr = curVersion.split('.').map((x) => parseInt(x, 10));
const lastArr = lastVersion.split('.').map((x) => parseInt(x, 10));
if (curArr[0] < lastArr[0]) {
return true;
}
if (curArr[0] === lastArr[0] && curArr[1] < lastArr[1]) {
return true;
}
if (
curArr[0] === lastArr[0] &&
curArr[1] === lastArr[1] &&
curArr[2] < lastArr[2]
) {
return true;
}
return false;
}
public async updateSystem() {
const cp = spawn('ql -l update', { shell: '/bin/bash' });
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: `开始更新系统`,
});
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: data.toString(),
});
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: data.toString(),
});
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: JSON.stringify(err),
});
});
return { code: 200 };
}
}
+13
View File
@@ -0,0 +1,13 @@
import { CommandModule } from 'yargs';
export const updateCommand: CommandModule = {
command: 'update',
describe: 'Update and restart qinglong',
builder: (yargs) => {
return yargs.option('repositority', {
type: 'string',
alias: 'r',
describe: `Specify the release warehouse address of the package`,
});
},
handler: async (argv) => {},
};
+13
View File
@@ -0,0 +1,13 @@
import * as yargs from 'yargs';
import { green, red } from 'chalk';
import { updateCommand } from './commands/update';
yargs
.usage('Usage: ql [command] <options>')
.command(updateCommand)
.fail((err) => {
console.error(`${red(err)}`);
})
.alias('h', 'help')
.showHelp()
.recommendCommands().argv;
+14
View File
@@ -0,0 +1,14 @@
version: '2'
services:
web:
image: whyour/qinglong:latest
volumes:
- ./data/config:/ql/config
- ./data/log:/ql/log
- ./data/db:/ql/db
- ./data/scripts:/ql/scripts
- ./data/repo:/ql/repo
- ./data/raw:/ql/raw
ports:
- "0.0.0.0:5700:5700"
restart: unless-stopped
+10 -8
View File
@@ -1,14 +1,17 @@
FROM node:lts-alpine FROM node:lts-alpine3.12
LABEL maintainer="whyour" ARG QL_MAINTAINER="whyour"
ARG QL_URL=https://github.com/whyour/qinglong.git LABEL maintainer="${QL_MAINTAINER}"
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
ARG QL_BRANCH=master ARG QL_BRANCH=master
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
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}
WORKDIR ${QL_DIR} WORKDIR ${QL_DIR}
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ RUN set -x \
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \
&& apk update -f \ && apk update -f \
&& apk upgrade \ && apk upgrade \
&& apk --no-cache add -f bash \ && apk --no-cache add -f bash \
@@ -24,15 +27,14 @@ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
python3 \ python3 \
jq \ jq \
openssh \ openssh \
py3-pip \
&& rm -rf /var/cache/apk/* \ && rm -rf /var/cache/apk/* \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \ && echo "Asia/Shanghai" > /etc/timezone \
&& touch ~/.bashrc \ && touch ~/.bashrc \
&& mkdir /run/nginx \
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \ && git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
&& git config --global user.email "qinglong@@users.noreply.github.com" \ && git config --global user.email "qinglong@@users.noreply.github.com" \
&& git config --global user.name "qinglong" \ && git config --global user.name "qinglong" \
&& git config --global pull.rebase true \
&& 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 \
@@ -43,7 +45,7 @@ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
&& rm -rf /root/.npm \ && rm -rf /root/.npm \
&& pnpm install --prod \ && pnpm install --prod \
&& rm -rf /root/.pnpm-store \ && rm -rf /root/.pnpm-store \
&& git clone -b ${QL_BRANCH} https://github.com/whyour/qinglong-static.git /static \ && git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
&& cp -rf /static/* ${QL_DIR} \ && cp -rf /static/* ${QL_DIR} \
&& rm -rf /static && rm -rf /static
ENTRYPOINT ["./docker/docker-entrypoint.sh"] ENTRYPOINT ["./docker/docker-entrypoint.sh"]
+10 -14
View File
@@ -1,13 +1,15 @@
#!/bin/bash #!/bin/bash
set -e
dir_shell=/ql/shell dir_shell=/ql/shell
. $dir_shell/share.sh . $dir_shell/share.sh
link_shell link_shell
echo -e "======================1. 检测配置文件========================\n" echo -e "======================1. 检测配置文件========================\n"
fix_config make_dir /etc/nginx/conf.d
cp -fv $dir_root/docker/front.conf /etc/nginx/conf.d/front.conf make_dir /run/nginx
pm2 l >/dev/null 2>&1 cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
pm2 l &>/dev/null
echo echo
echo -e "======================2. 安装依赖========================\n" echo -e "======================2. 安装依赖========================\n"
@@ -19,19 +21,13 @@ nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "nginx启动成功...\n" echo -e "nginx启动成功...\n"
echo -e "======================4. 启动控制面板========================\n" echo -e "======================4. 启动控制面板========================\n"
if [[ $(pm2 info panel 2>/dev/null) ]]; then pm2 delete panel &>/dev/null
pm2 reload panel --source-map-support --time pm2 start $dir_root/build/app.js -n panel --source-map-support --time
else
pm2 start $dir_root/build/app.js -n panel --source-map-support --time
fi
echo -e "控制面板启动成功...\n" echo -e "控制面板启动成功...\n"
echo -e "======================5. 启动定时任务========================\n" echo -e "======================5. 启动定时任务========================\n"
if [[ $(pm2 info schedule 2>/dev/null) ]]; then pm2 delete schedule &>/dev/null
pm2 reload schedule --source-map-support --time pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time
else
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time
fi
echo -e "定时任务启动成功...\n" echo -e "定时任务启动成功...\n"
if [[ $AutoStartBot == true ]]; then if [[ $AutoStartBot == true ]]; then
+18
View File
@@ -2,12 +2,30 @@ upstream api {
server 0.0.0.0:5600; server 0.0.0.0:5600;
} }
map $http_upgrade $connection_upgrade {
default keep-alive;
'websocket' upgrade;
}
server { server {
listen 5700; listen 5700;
root /ql/dist; root /ql/dist;
ssl_session_timeout 5m; ssl_session_timeout 5m;
location /api { location /api {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://api;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
location /open {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://api; proxy_pass http://api;
} }
+45
View File
@@ -0,0 +1,45 @@
user root;
worker_processes auto;
pcre_jit on;
error_log /var/log/nginx/error.log warn;
include /etc/nginx/modules/*.conf;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server_tokens off;
client_max_body_size 20m;
client_body_buffer_size 20m;
keepalive_timeout 65;
sendfile on;
tcp_nodelay on;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:2m;
gzip on;
gzip_static on;
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
gzip_proxied any;
gzip_vary on;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.0;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
include /etc/nginx/conf.d/*.conf;
}
+28 -6
View File
@@ -24,27 +24,42 @@
] ]
}, },
"dependencies": { "dependencies": {
"@otplib/preset-default": "^12.0.1",
"@sentry/node": "^6.14.0",
"@sentry/tracing": "^6.14.0",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"celebrate": "^13.0.3", "celebrate": "^13.0.3",
"chokidar": "^3.5.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron-parser": "^3.5.0", "cron-parser": "^3.5.0",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"express": "^4.17.1", "express": "^4.17.1",
"express-jwt": "^6.0.0", "express-jwt": "^6.0.0",
"express-urlrewrite": "^1.4.0",
"got": "^11.8.2",
"hpagent": "^0.1.2",
"iconv-lite": "^0.6.3",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"nedb": "^1.8.0", "nedb": "^1.8.0",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"node-schedule": "^2.0.0", "node-schedule": "^2.0.0",
"nodemailer": "^6.7.0",
"p-queue": "6.6.2", "p-queue": "6.6.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"serve-handler": "^6.1.3",
"sockjs": "^0.3.21",
"typedi": "^0.8.0", "typedi": "^0.8.0",
"winston": "^3.3.3" "uuid": "^8.3.2",
"winston": "^3.3.3",
"yargs": "^17.2.1"
}, },
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^4.6.2", "@ant-design/icons": "^4.6.2",
"@ant-design/pro-layout": "^6.5.0", "@ant-design/pro-layout": "^6.26.0",
"@monaco-editor/react": "^4.2.1", "@monaco-editor/react": "^4.3.1",
"@sentry/react": "^6.14.0",
"@sentry/webpack-plugin": "^1.18.3",
"@types/cors": "^2.8.10", "@types/cors": "^2.8.10",
"@types/express": "^4.17.8", "@types/express": "^4.17.8",
"@types/express-jwt": "^6.0.1", "@types/express-jwt": "^6.0.1",
@@ -53,11 +68,17 @@
"@types/nedb": "^1.8.11", "@types/nedb": "^1.8.11",
"@types/node": "^14.11.2", "@types/node": "^14.11.2",
"@types/node-fetch": "^2.5.8", "@types/node-fetch": "^2.5.8",
"@types/node-schedule": "^1.3.2",
"@types/nodemailer": "^6.4.4",
"@types/qrcode.react": "^1.0.1", "@types/qrcode.react": "^1.0.1",
"@types/react": "^17.0.0", "@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0", "@types/react-dom": "^17.0.0",
"@umijs/plugin-antd": "^0.9.1", "@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1",
"@umijs/plugin-antd": "^0.11.0",
"@umijs/test": "^3.3.9", "@umijs/test": "^3.3.9",
"antd": "^4.17.0-alpha.6",
"codemirror": "^5.62.2", "codemirror": "^5.62.2",
"compression-webpack-plugin": "6.1.1", "compression-webpack-plugin": "6.1.1",
"darkreader": "^4.9.27", "darkreader": "^4.9.27",
@@ -68,13 +89,14 @@
"react": "17.x", "react": "17.x",
"react-codemirror2": "^7.2.1", "react-codemirror2": "^7.2.1",
"react-diff-viewer": "^3.1.1", "react-diff-viewer": "^3.1.1",
"react-split-pane": "^0.1.92",
"react-dnd": "^14.0.2", "react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0", "react-dnd-html5-backend": "^14.0.0",
"react-dom": "17.x", "react-dom": "17.x",
"react-split-pane": "^0.1.92",
"sockjs-client": "^1.5.2",
"ts-node": "^9.0.0", "ts-node": "^9.0.0",
"typescript": "^4.1.2", "typescript": "^4.1.2",
"umi": "^3.3.9", "umi": "^3.5.0",
"umi-request": "^1.3.5", "umi-request": "^1.3.5",
"vh-check": "^2.0.5", "vh-check": "^2.0.5",
"webpack": "^5.28.0", "webpack": "^5.28.0",
-9765
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

+1 -1
View File
@@ -1 +1 @@
{ "username": "admin", "password": "adminadmin" } { "username": "admin", "password": "admin" }
+12 -1
View File
@@ -10,6 +10,9 @@ AutoDelCron="true"
## 在运行 ql repo 命令时,是否自动增加新的本地定时任务 ## 在运行 ql repo 命令时,是否自动增加新的本地定时任务
AutoAddCron="true" AutoAddCron="true"
## 拉取脚本时默认的定时规则,当匹配不到定时规则时使用,例如: 0 9 * * *
DefaultCronRule=""
## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可 ## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可
RepoFileExtensions="js py" RepoFileExtensions="js py"
@@ -100,7 +103,7 @@ export PUSH_PLUS_TOKEN=""
## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送 ## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送
export PUSH_PLUS_USER="" export PUSH_PLUS_USER=""
## 8. go-cqhttp ## 9. go-cqhttp
## gobot_url 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg ## gobot_url 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
## gobot_token 填写在go-cqhttp文件设置的访问密钥 ## gobot_token 填写在go-cqhttp文件设置的访问密钥
## gobot_qq 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群 ## gobot_qq 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
@@ -109,4 +112,12 @@ export GOBOT_URL=""
export GOBOT_TOKEN="" export GOBOT_TOKEN=""
export GOBOT_QQ="" export GOBOT_QQ=""
## 10. gotify
## gotify_url 填写gotify地址,如https://push.example.de:8080
## gotify_token 填写gotify的消息应用token
## gotify_priority 填写推送消息优先级,默认为0
export GOTIFY_URL="";
export GOTIFY_TOKEN="";
export GOTIFY_PRIORITY=0;
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可 ## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+53
View File
@@ -13,6 +13,13 @@
const querystring = require('querystring'); const querystring = require('querystring');
const $ = new Env(); const $ = new Env();
const timeout = 15000; //超时时间(单位毫秒) const timeout = 15000; //超时时间(单位毫秒)
// =======================================gotify通知设置区域==============================================
//gotify_url 填写gotify地址,如https://push.example.de:8080
//gotify_token 填写gotify的消息应用token
//gotify_priority 填写推送消息优先级,默认为0
let GOTIFY_URL = '';
let GOTIFY_TOKEN = '';
let GOTIFY_PRIORITY = 0;
// =======================================go-cqhttp通知设置区域=========================================== // =======================================go-cqhttp通知设置区域===========================================
//gobot_url 填写请求地址http://127.0.0.1/send_private_msg //gobot_url 填写请求地址http://127.0.0.1/send_private_msg
//gobot_token 填写在go-cqhttp文件设置的访问密钥 //gobot_token 填写在go-cqhttp文件设置的访问密钥
@@ -84,6 +91,16 @@ let PUSH_PLUS_TOKEN = '';
let PUSH_PLUS_USER = ''; let PUSH_PLUS_USER = '';
//==========================云端环境变量的判断与接收========================= //==========================云端环境变量的判断与接收=========================
if (process.env.GOTIFY_URL) {
GOTIFY_URL = process.env.GOTIFY_URL;
}
if (process.env.GOTIFY_TOKEN) {
GOTIFY_TOKEN = process.env.GOTIFY_TOKEN;
}
if (process.env.GOTIFY_PRIORITY) {
GOTIFY_PRIORITY = process.env.GOTIFY_PRIORITY;
}
if (process.env.GOBOT_URL) { if (process.env.GOBOT_URL) {
GOBOT_URL = process.env.GOBOT_URL; GOBOT_URL = process.env.GOBOT_URL;
} }
@@ -200,9 +217,45 @@ async function sendNotify(
qywxamNotify(text, desp), //企业微信应用消息推送 qywxamNotify(text, desp), //企业微信应用消息推送
iGotNotify(text, desp, params), //iGot iGotNotify(text, desp, params), //iGot
gobotNotify(text, desp),//go-cqhttp gobotNotify(text, desp),//go-cqhttp
gotifyNotify(text, desp),//gotify
]); ]);
} }
function gotifyNotify(text, desp) {
return new Promise((resolve) => {
if (GOTIFY_URL && GOTIFY_TOKEN) {
const options = {
url: `${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}`,
body: `title=${encodeURIComponent(text)}&message=${encodeURIComponent(desp)}&priority=${GOTIFY_PRIORITY}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
}
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('gotify发送通知调用API失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
if (data.id) {
console.log('gotify发送通知消息成功🎉\n');
} else {
console.log(`${data.message}\n`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve();
}
});
} else {
resolve();
}
});
}
function gobotNotify(text, desp, time = 2100) { function gobotNotify(text, desp, time = 2100) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (GOBOT_URL) { if (GOBOT_URL) {
+503 -239
View File
@@ -1,277 +1,541 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# _*_ coding:utf-8 _*_ # _*_ coding:utf-8 _*_
import sys
import os
cur_path = os.path.abspath(os.path.dirname(__file__))
root_path = os.path.split(cur_path)[0]
sys.path.append(root_path)
import requests
import json
import traceback
import time
import hmac
import hashlib
import base64 import base64
import urllib.parse import hashlib
from requests.adapters import HTTPAdapter import hmac
from urllib3.util import Retry import json
import os
import re import re
import threading
import time
import urllib.parse
import requests
# 原先的 print 函数和主线程的锁
_print = print
mutex = threading.Lock()
# 定义新的 print 函数
def print(text, *args, **kw):
"""
使输出有序进行,不出现多线程同一时间输出导致错乱的问题。
"""
with mutex:
_print(text, *args, **kw)
# 通知服务 # 通知服务
BARK = '' # bark服务,此参数如果以http或者https开头则判定为自建bark服务; secrets可填; # fmt: off
SCKEY = '' # Server酱的SCKEY; secrets可填 push_config = {
TG_BOT_TOKEN = '' # tg机器人的TG_BOT_TOKEN; secrets可填 'HITOKOTO': False, # 启用一言(随机句子)
TG_USER_ID = '' # tg机器人的TG_USER_ID; secrets可填
TG_PROXY_IP = '' # tg机器人的TG_PROXY_IP; secrets可填
TG_PROXY_PORT = '' # tg机器人的TG_PROXY_PORT; secrets可填
DD_BOT_ACCESS_TOKEN = '' # 钉钉机器人的DD_BOT_ACCESS_TOKEN; secrets可填
DD_BOT_SECRET = '' # 钉钉机器人的DD_BOT_SECRET; secrets可填
QYWX_APP = '' # 企业微信应用的QYWX_APP; secrets可填 参考http://note.youdao.com/s/HMiudGkb
notify_mode = [] 'BARK_PUSH': '', # bark IP 或设备码,例:https://api.day.app/DxHcxxxxxRxxxxxxcm/
'BARK_ARCHIVE': '', # bark 推送是否存档
'BARK_GROUP': '', # bark 推送分组
'BARK_SOUND': '', # bark 推送声音
# GitHub action运行需要填写对应的secrets 'CONSOLE': True, # 控制台输出
if "BARK" in os.environ and os.environ["BARK"]:
BARK = os.environ["BARK"]
if "SCKEY" in os.environ and os.environ["SCKEY"]:
SCKEY = os.environ["SCKEY"]
if "TG_BOT_TOKEN" in os.environ and os.environ["TG_BOT_TOKEN"] and "TG_USER_ID" in os.environ and os.environ["TG_USER_ID"]:
TG_BOT_TOKEN = os.environ["TG_BOT_TOKEN"]
TG_USER_ID = os.environ["TG_USER_ID"]
if "DD_BOT_ACCESS_TOKEN" in os.environ and os.environ["DD_BOT_ACCESS_TOKEN"] and "DD_BOT_SECRET" in os.environ and os.environ["DD_BOT_SECRET"]:
DD_BOT_ACCESS_TOKEN = os.environ["DD_BOT_ACCESS_TOKEN"]
DD_BOT_SECRET = os.environ["DD_BOT_SECRET"]
if "QYWX_APP" in os.environ and os.environ["QYWX_APP"]:
QYWX_APP = os.environ["QYWX_APP"]
if BARK: 'DD_BOT_SECRET': '', # 钉钉机器人的 DD_BOT_SECRET
notify_mode.append('bark') 'DD_BOT_TOKEN': '', # 钉钉机器人的 DD_BOT_TOKEN
print("BARK 推送打开")
if SCKEY:
notify_mode.append('sc_key')
print("Server酱 推送打开")
if TG_BOT_TOKEN and TG_USER_ID:
notify_mode.append('telegram_bot')
print("Telegram 推送打开")
if DD_BOT_ACCESS_TOKEN and DD_BOT_SECRET:
notify_mode.append('dingding_bot')
print("钉钉机器人 推送打开")
if QYWX_APP:
notify_mode.append('qywxapp_bot')
print("企业微信应用 推送打开")
def bark(title, content): 'FSKEY': '', # 飞书机器人的 FSKEY
print("\n")
if not BARK: 'GOBOT_URL': '', # go-cqhttp
print("bark服务的bark_token未设置!!\n取消推送") # 推送到个人QQhttp://127.0.0.1/send_private_msg
# 群:http://127.0.0.1/send_group_msg
'GOBOT_QQ': '', # go-cqhttp 的推送群或用户
# GOBOT_URL 设置 /send_private_msg 时填入 user_id=个人QQ
# /send_group_msg 时填入 group_id=QQ群
'GOBOT_TOKEN': '', # go-cqhttp 的 access_token
'GOTIFY_URL': '', # gotify地址,如https://push.example.de:8080
'GOTIFY_TOKEN': '', # gotify的消息应用token
'GOTIFY_PRIORITY': 0, # 推送消息优先级,默认为0
'IGOT_PUSH_KEY': '', # iGot 聚合推送的 IGOT_PUSH_KEY
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
'QMSG_KEY': '', # qmsg 酱的 QMSG_KEY
'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE
'QYWX_AM': '', # 企业微信应用
'QYWX_KEY': '', # 企业微信机器人
'TG_BOT_TOKEN': '', # tg 机器人的 TG_BOT_TOKEN,例:1407203283:AAG9rt-6RDaaX0HBLZQq0laNOh898iFYaRQ
'TG_USER_ID': '', # tg 机器人的 TG_USER_ID,例:1434078534
'TG_API_HOST': '', # tg 代理 api
'TG_PROXY_AUTH': '', # tg 代理认证参数
'TG_PROXY_HOST': '', # tg 机器人的 TG_PROXY_HOST
'TG_PROXY_PORT': '', # tg 机器人的 TG_PROXY_PORT
}
notify_function = []
# fmt: on
# 首先读取 面板变量 或者 github action 运行变量
for k in push_config:
if os.getenv(k):
v = os.getenv(k)
push_config[k] = v
def bark(title: str, content: str) -> None:
"""
使用 bark 推送消息。
"""
if not push_config.get("BARK_PUSH"):
print("bark 服务的 BARK_PUSH 未设置!!\n取消推送")
return return
print("bark服务启动") print("bark 服务启动")
url = None
if BARK.startswith('http'): if push_config.get("BARK_PUSH").startswith("http"):
url = f"""{BARK}/{title}/{content}""" url = f'{push_config.get("BARK_PUSH")}/{urllib.parse.quote_plus(title)}/{urllib.parse.quote_plus(content)}'
else: else:
url = f"""https://api.day.app/{BARK}/{title}/{content}""" url = f'https://api.day.app/{push_config.get("BARK_PUSH")}/{urllib.parse.quote_plus(title)}/{urllib.parse.quote_plus(content)}'
bark_params = {
"BARK_ARCHIVE": "isArchive",
"BARK_GROUP": "group",
"BARK_SOUND": "sound",
}
params = ""
for pair in filter(
lambda pairs: pairs[0].startswith("BARK_")
and pairs[0] != "BARK_PUSH"
and pairs[1]
and bark_params.get(pairs[0]),
push_config.items(),
):
params += f"{bark_params.get(pair[0])}={pair[1]}&"
if params:
url = url + "?" + params.rstrip("&")
response = requests.get(url).json() response = requests.get(url).json()
if response['code'] == 200:
print('推送成功!')
else:
print('推送失败!')
def serverJ(title, content): if response["code"] == 200:
print("\n") print("bark 推送成功!")
if not SCKEY: else:
print("server酱服务的SCKEY未设置!!\n取消推送") print("bark 推送失败!")
def console(title: str, content: str) -> None:
"""
使用 控制台 推送消息。
"""
print(f"{title}\n\n{content}")
def dingding_bot(title: str, content: str) -> None:
"""
使用 钉钉机器人 推送消息。
"""
if not push_config.get("DD_BOT_SECRET") or not push_config.get("DD_BOT_TOKEN"):
print("钉钉机器人 服务的 DD_BOT_SECRET 或者 DD_BOT_TOKEN 未设置!!\n取消推送")
return return
print("serverJ服务启动") print("钉钉机器人 服务启动")
timestamp = str(round(time.time() * 1000))
secret_enc = push_config.get("DD_BOT_SECRET").encode("utf-8")
string_to_sign = "{}\n{}".format(timestamp, push_config.get("DD_BOT_SECRET"))
string_to_sign_enc = string_to_sign.encode("utf-8")
hmac_code = hmac.new(
secret_enc, string_to_sign_enc, digestmod=hashlib.sha256
).digest()
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
url = f'https://oapi.dingtalk.com/robot/send?access_token={push_config.get("DD_BOT_TOKEN")}&timestamp={timestamp}&sign={sign}'
headers = {"Content-Type": "application/json;charset=utf-8"}
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
response = requests.post(
url=url, data=json.dumps(data), headers=headers, timeout=15
).json()
if not response["errcode"]:
print("钉钉机器人 推送成功!")
else:
print("钉钉机器人 推送失败!")
def feishu_bot(title: str, content: str) -> None:
"""
使用 飞书机器人 推送消息。
"""
if not push_config.get("FSKEY"):
print("飞书 服务的 FSKEY 未设置!!\n取消推送")
return
print("飞书 服务启动")
url = f'https://open.feishu.cn/open-apis/bot/v2/hook/{push_config.get("FSKEY")}'
data = {"msg_type": "text", "content": {"text": f"{title}\n\n{content}"}}
response = requests.post(url, data=json.dumps(data)).json()
if response.get("StatusCode") == 0:
print("飞书 推送成功!")
else:
print("飞书 推送失败!错误信息如下:\n", response)
def go_cqhttp(title: str, content: str) -> None:
"""
使用 go_cqhttp 推送消息。
"""
if not push_config.get("GOBOT_URL") or not push_config.get("GOBOT_QQ"):
print("go-cqhttp 服务的 GOBOT_URL 或 GOBOT_QQ 未设置!!\n取消推送")
return
print("go-cqhttp 服务启动")
url = f'{push_config.get("GOBOT_URL")}?access_token={push_config.get("GOBOT_TOKEN")}&{push_config.get("GOBOT_QQ")}&message=标题:{title}\n内容:{content}'
response = requests.get(url).json()
if response["status"] == "ok":
print("go-cqhttp 推送成功!")
else:
print("go-cqhttp 推送失败!")
def gotify(title:str,content:str) -> None:
"""
使用 gotify 推送消息。
"""
if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"):
print("gotify 服务的 GOTIFY_URL 或 GOTIFY_TOKEN 未设置!!\n取消推送")
return
print("gotify 服务启动")
url = f'{push_config.get("GOTIFY_URL")}/message?token={push_config.get("GOTIFY_TOKEN")}'
data = {"title": title,"message": content,"priority": push_config.get("GOTIFY_PRIORITY")}
response = requests.post(url,data=data).json()
if response.get("id"):
print("gotify 推送成功!")
else:
print("gotify 推送失败!")
def iGot(title: str, content: str) -> None:
"""
使用 iGot 推送消息。
"""
if not push_config.get("IGOT_PUSH_KEY"):
print("iGot 服务的 IGOT_PUSH_KEY 未设置!!\n取消推送")
return
print("iGot 服务启动")
url = f'https://push.hellyw.com/{push_config.get("IGOT_PUSH_KEY")}'
data = {"title": title, "content": content}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
response = requests.post(url, data=data, headers=headers).json()
if response["ret"] == 0:
print("iGot 推送成功!")
else:
print(f'iGot 推送失败!{response["errMsg"]}')
def serverJ(title: str, content: str) -> None:
"""
通过 serverJ 推送消息。
"""
if not push_config.get("PUSH_KEY"):
print("serverJ 服务的 PUSH_KEY 未设置!!\n取消推送")
return
print("serverJ 服务启动")
data = {"text": title, "desp": content.replace("\n", "\n\n")}
if push_config.get("PUSH_KEY").index("SCT") != -1:
url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send'
else:
url = f'https://sc.ftqq.com/${push_config.get("PUSH_KEY")}.send'
response = requests.post(url, data=data).json()
if response.get("errno") == 0 or response.get("code") == 0:
print("serverJ 推送成功!")
else:
print(f'serverJ 推送失败!错误码:{response["message"]}')
def pushplus_bot(title: str, content: str) -> None:
"""
通过 push+ 推送消息。
"""
if not push_config.get("PUSH_PLUS_TOKEN"):
print("PUSHPLUS 服务的 PUSH_PLUS_TOKEN 未设置!!\n取消推送")
return
print("PUSHPLUS 服务启动")
url = "http://www.pushplus.plus/send"
data = { data = {
"text": title, "token": push_config.get("PUSH_PLUS_TOKEN"),
"desp": content.replace("\n", "\n\n") "title": title,
"content": content,
"topic": push_config.get("PUSH_PLUS_USER"),
} }
response = requests.post(f"https://sc.ftqq.com/{SCKEY}.send", data=data).json() body = json.dumps(data).encode(encoding="utf-8")
if response['errno'] == 0: headers = {"Content-Type": "application/json"}
print('推送成功!') response = requests.post(url=url, data=body, headers=headers).json()
else:
print('推送失败!')
def telegram_bot(title, content): if response["code"] == 200:
print("\n") print("PUSHPLUS 推送成功!")
bot_token = TG_BOT_TOKEN
user_id = TG_USER_ID else:
if not bot_token or not user_id:
print("tg服务的bot_token或者user_id未设置!!\n取消推送") url_old = "http://pushplus.hxtrip.com/send"
headers["Accept"] = "application/json"
response = requests.post(url=url_old, data=body, headers=headers).json()
if response["code"] == 200:
print("PUSHPLUS(hxtrip) 推送成功!")
else:
print("PUSHPLUS 推送失败!")
def qmsg_bot(title: str, content: str) -> None:
"""
使用 qmsg 推送消息。
"""
if not push_config.get("QMSG_KEY") or not push_config.get("QMSG_TYPE"):
print("qmsg 的 QMSG_KEY 或者 QMSG_TYPE 未设置!!\n取消推送")
return return
print("tg服务启动") print("qmsg 服务启动")
url=f"https://api.telegram.org/bot{TG_BOT_TOKEN}/sendMessage"
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
payload = {'chat_id': str(TG_USER_ID), 'text': f'{title}\n\n{content}', 'disable_web_page_preview': 'true'}
proxies = None
if TG_PROXY_IP and TG_PROXY_PORT:
proxyStr = "http://{}:{}".format(TG_PROXY_IP, TG_PROXY_PORT)
proxies = {"http": proxyStr, "https": proxyStr}
response = requests.post(url=url, headers=headers, params=payload, proxies=proxies).json()
if response['ok']:
print('推送成功!')
else:
print('推送失败!')
def dingding_bot(title, content): url = f'https://qmsg.zendee.cn/{push_config.get("QMSG_TYPE")}/{push_config.get("QMSG_KEY")}'
timestamp = str(round(time.time() * 1000)) # 时间戳 payload = {"msg": f'{title}\n\n{content.replace("----", "-")}'.encode("utf-8")}
secret_enc = DD_BOT_SECRET.encode('utf-8') response = requests.post(url=url, params=payload).json()
string_to_sign = '{}\n{}'.format(timestamp, DD_BOT_SECRET)
string_to_sign_enc = string_to_sign.encode('utf-8')
hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code)) # 签名
print('开始使用 钉钉机器人 推送消息...', end='')
url = f'https://oapi.dingtalk.com/robot/send?access_token={DD_BOT_ACCESS_TOKEN}&timestamp={timestamp}&sign={sign}'
headers = {'Content-Type': 'application/json;charset=utf-8'}
data = {
'msgtype': 'text',
'text': {'content': f'{title}\n\n{content}'}
}
response = requests.post(url=url, data=json.dumps(data), headers=headers, timeout=15).json()
if not response['errcode']:
print('推送成功!')
else:
print('推送失败!')
def qywxapp_bot(title, content): if response["code"] == 0:
print("\n") print("qmsg 推送成功!")
if not QYWX_APP: else:
print("企业微信应用的QYWX_APP未设置!!\n取消推送") print(f'qmsg 推送失败!{response["reason"]}')
def wecom_app(title: str, content: str) -> None:
"""
通过 企业微信 APP 推送消息。
"""
if not push_config.get("QYWX_AM"):
print("QYWX_AM 未设置!!\n取消推送")
return return
print("企业微信应用启动") QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM"))
qywx_app_params = QYWX_APP.split(',') if 4 < len(QYWX_AM_AY) > 5:
url='https://qyapi.weixin.qq.com/cgi-bin/gettoken' print("QYWX_AM 设置错误!!\n取消推送")
headers= { return
'Content-Type': 'application/json', print("企业微信 APP 服务启动")
}
payload = {
'corpid': qywx_app_params[0],
'corpsecret': qywx_app_params[1],
}
response = requests.post(url=url, headers=headers, data=json.dumps(payload), timeout=15).json()
accesstoken = response["access_token"]
html = content.replace("\n", "<br/>")
options = None corpid = QYWX_AM_AY[0]
if not qywx_app_params[4]: corpsecret = QYWX_AM_AY[1]
options = { touser = QYWX_AM_AY[2]
'msgtype': 'text', agentid = QYWX_AM_AY[3]
'text': { try:
content: f'{title}\n\n${content}' media_id = QYWX_AM_AY[4]
} except IndexError:
} media_id = ""
elif qywx_app_params[4] == '0': wx = WeCom(corpid, corpsecret, agentid)
options = { # 如果没有配置 media_id 默认就以 text 方式发送
'msgtype': 'textcard', if not media_id:
'textcard': { message = title + "\n\n" + content
title: f'{title}', response = wx.send_text(message, touser)
description: f'{content}',
btntxt: '更多'
}
}
elif qywx_app_params[4] == '1':
options = {
'msgtype': 'text',
'text': {
content: f'{title}\n\n${content}'
}
}
else: else:
options = { response = wx.send_mpnews(title, content, media_id, touser)
'msgtype': 'mpnews',
'mpnews': { if response == "ok":
'articles': [ print("企业微信推送成功!")
else:
print("企业微信推送失败!错误信息如下:\n", response)
class WeCom:
def __init__(self, corpid, corpsecret, agentid):
self.CORPID = corpid
self.CORPSECRET = corpsecret
self.AGENTID = agentid
def get_access_token(self):
url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
values = {
"corpid": self.CORPID,
"corpsecret": self.CORPSECRET,
}
req = requests.post(url, params=values)
data = json.loads(req.text)
return data["access_token"]
def send_text(self, message, touser="@all"):
send_url = (
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
+ self.get_access_token()
)
send_values = {
"touser": touser,
"msgtype": "text",
"agentid": self.AGENTID,
"text": {"content": message},
"safe": "0",
}
send_msges = bytes(json.dumps(send_values), "utf-8")
respone = requests.post(send_url, send_msges)
respone = respone.json()
return respone["errmsg"]
def send_mpnews(self, title, message, media_id, touser="@all"):
send_url = (
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
+ self.get_access_token()
)
send_values = {
"touser": touser,
"msgtype": "mpnews",
"agentid": self.AGENTID,
"mpnews": {
"articles": [
{ {
'title': f'{title}', "title": title,
'thumb_media_id': f'{qywx_app_params[4]}', "thumb_media_id": media_id,
'author': '智能助手', "author": "Author",
'content_source_url': '', "content_source_url": "",
'content': f'{html}', "content": message.replace("\n", "<br/>"),
'digest': f'{content}' "digest": message,
} }
] ]
},
} }
} send_msges = bytes(json.dumps(send_values), "utf-8")
respone = requests.post(send_url, send_msges)
respone = respone.json()
return respone["errmsg"]
url=f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={accesstoken}"
data = {
'touser': f'{change_user_id(content)}',
'agentid': f'{qywx_app_params[3]}',
'safe': '0'
}
data.update(options)
headers = {
'Content-Type': 'application/json',
}
response = requests.post(url=url, headers=headers, data=json.dumps(data)).json()
if response['errcode'] == 0: def wecom_bot(title: str, content: str) -> None:
print('推送成功!')
else:
print('推送失败!')
def change_user_id(desp):
qywx_app_params = QYWX_APP.split(',')
if qywx_app_params[2]:
userIdTmp = qywx_app_params[2].split("|")
userId = ""
for i in range(len(userIdTmp)):
count1 = f"账号{i + 1}"
count2 = f"签到号{i + 1}"
if re.search(count1, desp) or re.search(count2, desp):
userId = userIdTmp[i]
if not userId:
userId = qywx_app_params[2]
return userId
else:
return "@all"
def send(title, content):
""" """
使用 bark, telegram bot, dingding bot, serverJ 发送手机推送 通过 企业微信机器人 推送消息。
:param title: """
:param content: if not push_config.get("QYWX_KEY"):
print("企业微信机器人 服务的 QYWX_KEY 未设置!!\n取消推送")
return
print("企业微信机器人服务启动")
url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={push_config.get('QYWX_KEY')}"
headers = {"Content-Type": "application/json;charset=utf-8"}
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
response = requests.post(
url=url, data=json.dumps(data), headers=headers, timeout=15
).json()
if response["errcode"] == 0:
print("企业微信机器人推送成功!")
else:
print("企业微信机器人推送失败!")
def telegram_bot(title: str, content: str) -> None:
"""
使用 telegram 机器人 推送消息。
"""
if not push_config.get("TG_BOT_TOKEN") or not push_config.get("TG_USER_ID"):
print("tg 服务的 bot_token 或者 user_id 未设置!!\n取消推送")
return
print("tg 服务启动")
if push_config.get("TG_API_HOST"):
url = f"https://{push_config.get('TG_API_HOST')}/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
else:
url = (
f"https://api.telegram.org/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
)
headers = {"Content-Type": "application/x-www-form-urlencoded"}
payload = {
"chat_id": str(push_config.get("TG_USER_ID")),
"text": f"{title}\n\n{content}",
"disable_web_page_preview": "true",
}
proxies = None
if push_config.get("TG_PROXY_HOST") and push_config.get("TG_PROXY_PORT"):
if push_config.get("TG_PROXY_AUTH") is not None and "@" not in push_config.get(
"TG_PROXY_HOST"
):
push_config["TG_PROXY_HOST"] = (
push_config.get("TG_PROXY_AUTH")
+ "@"
+ push_config.get("TG_PROXY_HOST")
)
proxyStr = "http://{}:{}".format(
push_config.get("TG_PROXY_HOST"), push_config.get("TG_PROXY_PORT")
)
proxies = {"http": proxyStr, "https": proxyStr}
response = requests.post(
url=url, headers=headers, params=payload, proxies=proxies
).json()
if response["ok"]:
print("tg 推送成功!")
else:
print("tg 推送失败!")
def one() -> str:
"""
获取一条一言。
:return: :return:
""" """
for i in notify_mode: url = "https://v1.hitokoto.cn/"
if i == 'bark': res = requests.get(url).json()
if BARK: return res["hitokoto"] + " ----" + res["from"]
bark(title=title, content=content)
else:
print('未启用 bark') if push_config.get("BARK_PUSH"):
continue notify_function.append(bark)
if i == 'sc_key': if push_config.get("CONSOLE"):
if SCKEY: notify_function.append(console)
serverJ(title=title, content=content) if push_config.get("DD_BOT_TOKEN") and push_config.get("DD_BOT_SECRET"):
else: notify_function.append(dingding_bot)
print('未启用 Server酱') if push_config.get("FSKEY"):
continue notify_function.append(feishu_bot)
elif i == 'dingding_bot': if push_config.get("GOBOT_URL") and push_config.get("GOBOT_QQ"):
if DD_BOT_ACCESS_TOKEN and DD_BOT_SECRET: notify_function.append(go_cqhttp)
dingding_bot(title=title, content=content) if push_config.get("GOTIFY_URL") and push_config.get("GOTIFY_TOKEN"):
else: notify_function.append(gotify)
print('未启用 钉钉机器人') if push_config.get("IGOT_PUSH_KEY"):
continue notify_function.append(iGot)
elif i == 'telegram_bot': if push_config.get("PUSH_KEY"):
if TG_BOT_TOKEN and TG_USER_ID: notify_function.append(serverJ)
telegram_bot(title=title, content=content) if push_config.get("PUSH_PLUS_TOKEN"):
else: notify_function.append(pushplus_bot)
print('未启用 telegram机器人') if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
continue notify_function.append(qmsg_bot)
elif i == 'qywxapp_bot': if push_config.get("QYWX_AM"):
if QYWX_APP: notify_function.append(wecom_app)
qywxapp_bot(title=title, content=content) if push_config.get("QYWX_KEY"):
else: notify_function.append(wecom_bot)
print('未启用 企业微信应用推送') if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
continue notify_function.append(telegram_bot)
else:
print('此类推送方式不存在')
def send(title: str, content: str) -> None:
if not content:
print(f"{title} 推送内容为空!")
return
hitokoto = push_config.get("HITOKOTO")
text = one() if hitokoto else ""
content += "\n\n" + text
ts = [
threading.Thread(target=mode, args=(title, content), name=mode.__name__)
for mode in notify_function
]
[t.start() for t in ts]
[t.join() for t in ts]
def main(): def main():
send('title', 'content') send("title", "content")
if __name__ == '__main__': if __name__ == "__main__":
main() main()
-1
View File
@@ -1 +0,0 @@
requests
+6 -4
View File
@@ -6,7 +6,7 @@ get_token() {
add_cron_api() { add_cron_api() {
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
if [ $# -eq 1 ]; then if [[ $# -eq 1 ]]; then
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}')
@@ -39,7 +39,7 @@ add_cron_api() {
update_cron_api() { update_cron_api() {
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
if [ $# -eq 1 ]; then if [[ $# -eq 1 ]]; then
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}')
@@ -75,7 +75,7 @@ update_cron_api() {
update_cron_command_api() { update_cron_command_api() {
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
if [ $# -eq 1 ]; then if [[ $# -eq 1 ]]; then
local command=$(echo "$1" | awk -F ":" '{print $1}') local command=$(echo "$1" | awk -F ":" '{print $1}')
local id=$(echo "$1" | awk -F ":" '{print $2}') local id=$(echo "$1" | awk -F ":" '{print $2}')
else else
@@ -153,6 +153,8 @@ update_cron() {
local status="$2" local status="$2"
local pid="${3:-''}" local pid="${3:-''}"
local logPath="$4" local logPath="$4"
local lastExecutingTime="${5:-0}"
local runningTime="${6:-0}"
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons/status?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons/status?t=$currentTimeStamp" \
@@ -164,7 +166,7 @@ update_cron() {
-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 "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\"}" \ --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)
+9 -5
View File
@@ -11,10 +11,7 @@ apk --no-cache add -f zlib-dev gcc jpeg-dev python3-dev musl-dev freetype-dev
echo -e "\nbot依赖安装成功...\n" echo -e "\nbot依赖安装成功...\n"
echo -e "2、下载bot所需文件...\n" echo -e "2、下载bot所需文件...\n"
if [ -d ${repo_path}/.git ]; then if [[ ! -d ${repo_path}/.git ]]; then
reset_romote_url ${repo_path} "${url}"
git_pull_scripts ${repo_path}
else
rm -rf ${repo_path} rm -rf ${repo_path}
git_clone_scripts ${url} ${repo_path} "main" git_clone_scripts ${url} ${repo_path} "main"
fi fi
@@ -30,8 +27,15 @@ if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror pip3 config set global.index-url $PipMirror
fi fi
cp -f "$repo_path/jbot/requirements.txt" "$dir_root" cp -f "$repo_path/jbot/requirements.txt" "$dir_root"
cd $dir_root cd $dir_root
pip3 --default-timeout=100 install -r requirements.txt --no-cache-dir cat requirements.txt | while read LREAD
do
if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
pip3 --default-timeout=100 install ${LREAD}
fi
done
echo -e "\npython3依赖安装成功...\n" echo -e "\npython3依赖安装成功...\n"
echo -e "4、启动bot程序...\n" echo -e "4、启动bot程序...\n"
+17 -25
View File
@@ -3,7 +3,6 @@
dir_shell=/ql/shell dir_shell=/ql/shell
. $dir_shell/share.sh . $dir_shell/share.sh
. $dir_shell/api.sh . $dir_shell/api.sh
get_token
reset_env() { reset_env() {
echo -e "---> 1. 开始检测配置文件\n" echo -e "---> 1. 开始检测配置文件\n"
@@ -11,37 +10,40 @@ reset_env() {
echo -e "---> 配置文件检测完成\n" echo -e "---> 配置文件检测完成\n"
echo -e "---> 2. 开始安装青龙依赖\n" echo -e "---> 2. 开始安装青龙依赖\n"
rm -rf $dir_root/node_modules
npm_install_2 $dir_root npm_install_2 $dir_root
echo -e "---> 青龙依赖安装完成\n" echo -e "---> 青龙依赖安装完成\n"
echo -e "---> 3. 开始安装脚本依赖\n" echo -e "---> 3. 开始安装脚本依赖\n"
cp -f $dir_sample/package.json $dir_scripts/package.json cp -f $dir_sample/package.json $dir_scripts/package.json
rm -rf $dir_scripts/node_modules
npm_install_2 $dir_scripts npm_install_2 $dir_scripts
echo -e "---> 脚本依赖安装完成\n" echo -e "---> 脚本依赖安装完成\n"
}
echo -e "---> 4. 复制通知文件\n" copy_dep() {
echo -e "---> 1. 复制通知文件\n"
echo -e "---> 复制一份 $file_notify_py_sample$file_notify_py\n" echo -e "---> 复制一份 $file_notify_py_sample$file_notify_py\n"
cp -fv $file_notify_py_sample $file_notify_py cp -fv $file_notify_py_sample $file_notify_py
echo echo
echo -e "---> 复制一份 $file_notify_js_sample$file_notify_js\n" echo -e "---> 复制一份 $file_notify_js_sample$file_notify_js\n"
cp -fv $file_notify_js_sample $file_notify_js cp -fv $file_notify_js_sample $file_notify_js
echo -e "---> 通知文件复制完成\n" echo -e "---> 通知文件复制完成\n"
echo -e "---> 2. 复制nginx配置文件\n"
cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
echo -e "---> 配置文件复制完成\n"
} }
reload_pm2() { reload_pm2() {
pm2 l >/dev/null 2>&1 pm2 l &>/dev/null
if [[ $(pm2 info panel 2>/dev/null) ]]; then pm2 delete panel --source-map-support --time &>/dev/null
pm2 reload panel --source-map-support --time >/dev/null 2>&1 pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null
else
pm2 start $dir_root/build/app.js -n panel --source-map-support --time >/dev/null 2>&1
fi
if [[ $(pm2 info schedule 2>/dev/null) ]]; then pm2 delete schedule --source-map-support --time &>/dev/null
pm2 reload schedule --source-map-support --time >/dev/null 2>&1 pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null
else
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time >/dev/null 2>&1
fi
} }
pm2_log() { pm2_log() {
@@ -57,6 +59,7 @@ check_nginx() {
echo -e "=====> 检测nginx服务\n$nginxPid" echo -e "=====> 检测nginx服务\n$nginxPid"
if [[ $nginxPid ]]; then if [[ $nginxPid ]]; then
echo -e "\n=====> nginx服务正常\n" echo -e "\n=====> nginx服务正常\n"
nginx -s reload
else else
echo -e "\n=====> nginx服务异常,重新启动nginx\n" echo -e "\n=====> nginx服务异常,重新启动nginx\n"
nginx -c /etc/nginx/nginx.conf nginx -c /etc/nginx/nginx.conf
@@ -95,20 +98,9 @@ check_pm2() {
fi fi
} }
init_git() {
local dir_current=$(pwd)
cd $dir_root
git config --global user.email "qinglong@@users.noreply.github.com"
git config --global user.name "qinglong"
git config --global pull.rebase true
cd $dir_current
}
main() { main() {
echo -e "=====> 开始检测" echo -e "=====> 开始检测"
init_git copy_dep
check_ql check_ql
check_nginx check_nginx
check_pm2 check_pm2
+1 -1
View File
@@ -27,7 +27,7 @@ remove_js_log() {
remove_empty_dir() { remove_empty_dir() {
cd $dir_log cd $dir_log
for dir in $(ls); do for dir in $(ls); do
if [ -d $dir ] && [[ -z $(ls $dir) ]]; then if [[ -d $dir ]] && [[ -z $(ls $dir) ]]; then
rm -rf $dir rm -rf $dir
fi fi
done done
+64 -38
View File
@@ -10,6 +10,7 @@ dir_repo=$dir_root/repo
dir_raw=$dir_root/raw dir_raw=$dir_root/raw
dir_log=$dir_root/log dir_log=$dir_root/log
dir_db=$dir_root/db dir_db=$dir_root/db
dir_dep=$dir_root/deps
dir_list_tmp=$dir_log/.tmp dir_list_tmp=$dir_log/.tmp
dir_code=$dir_log/code dir_code=$dir_log/code
dir_update_log=$dir_log/update dir_update_log=$dir_log/update
@@ -32,6 +33,10 @@ file_notify_py_sample=$dir_sample/notify.py
file_notify_py=$dir_scripts/notify.py file_notify_py=$dir_scripts/notify.py
file_notify_js=$dir_scripts/sendNotify.js file_notify_js=$dir_scripts/sendNotify.js
task_error_log_path=$dir_log/task_error.log task_error_log_path=$dir_log/task_error.log
nginx_app_conf=$dir_root/docker/front.conf
nginx_conf=$dir_root/docker/nginx.conf
dep_notify_py=$dir_dep/notify.py
dep_notify_js=$dir_dep/sendNotify.js
## 清单文件 ## 清单文件
list_crontab_user=$dir_config/crontab.list list_crontab_user=$dir_config/crontab.list
@@ -58,12 +63,18 @@ init_env() {
} }
import_config() { import_config() {
[ -f $file_config_user ] && . $file_config_user [[ -f $file_config_user ]] && . $file_config_user
[ -f $file_env ] && . $file_env [[ -f $file_env ]] && . $file_env
command_timeout_time=${CommandTimeoutTime:-"1h"} command_timeout_time=${CommandTimeoutTime:-"1h"}
github_proxy_url=${GithubProxyUrl:-""} github_proxy_url=${GithubProxyUrl:-""}
file_extensions=${RepoFileExtensions:-"js py"} file_extensions=${RepoFileExtensions:-"js py"}
if [[ -n "${DefaultCronRule}" ]]; then
default_cron="${DefaultCronRule}"
else
default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
fi
} }
make_dir() { make_dir() {
@@ -93,7 +104,7 @@ gen_random_num() {
link_shell_sub() { link_shell_sub() {
local link_path="$1" local link_path="$1"
local original_path="$2" local original_path="$2"
if [ ! -L $link_path ] || [[ $(readlink -f $link_path) != $original_path ]]; then if [[ ! -L $link_path ]] || [[ $(readlink -f $link_path) != $original_path ]]; then
rm -f $link_path 2>/dev/null rm -f $link_path 2>/dev/null
ln -sf $original_path $link_path ln -sf $original_path $link_path
fi fi
@@ -102,7 +113,7 @@ link_shell_sub() {
link_shell() { link_shell() {
if [[ $is_termux -eq 1 ]]; then if [[ $is_termux -eq 1 ]]; then
local path="/data/data/com.termux/files/usr/bin/" local path="/data/data/com.termux/files/usr/bin/"
elif [[ $PATH == */usr/local/bin* ]] && [ -d /usr/local/bin ]; then elif [[ $PATH == */usr/local/bin* ]] && [[ -d /usr/local/bin ]]; then
local path="/usr/local/bin/" local path="/usr/local/bin/"
else else
local path="" local path=""
@@ -117,16 +128,16 @@ link_shell() {
define_cmd() { define_cmd() {
local cmd_prefix cmd_suffix local cmd_prefix cmd_suffix
if type task >/dev/null 2>&1; then if type task &>/dev/null; then
cmd_suffix="" cmd_suffix=""
if [ -x "$dir_shell/task.sh" ]; then if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix="" cmd_prefix=""
else else
cmd_prefix="bash " cmd_prefix="bash "
fi fi
else else
cmd_suffix=".sh" cmd_suffix=".sh"
if [ -x "$dir_shell/task.sh" ]; then if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix="$dir_shell/" cmd_prefix="$dir_shell/"
else else
cmd_prefix="bash $dir_shell/" cmd_prefix="bash $dir_shell/"
@@ -146,69 +157,83 @@ fix_config() {
make_dir $dir_repo make_dir $dir_repo
make_dir $dir_raw make_dir $dir_raw
make_dir $dir_update_log make_dir $dir_update_log
make_dir $dir_dep
if [ ! -s $file_config_user ]; then if [[ ! -s $file_config_user ]]; then
echo -e "复制一份 $file_config_sample$file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n" echo -e "复制一份 $file_config_sample$file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
cp -fv $file_config_sample $file_config_user cp -fv $file_config_sample $file_config_user
echo echo
fi fi
if [ ! -f $file_env ]; then if [[ ! -f $file_env ]]; then
echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n" echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n"
touch $file_env touch $file_env
echo echo
fi fi
if [ ! -f $file_task_before ]; then if [[ ! -f $file_task_before ]]; then
echo -e "复制一份 $file_task_sample$file_task_before\n" echo -e "复制一份 $file_task_sample$file_task_before\n"
cp -fv $file_task_sample $file_task_before cp -fv $file_task_sample $file_task_before
echo echo
fi fi
if [ ! -f $file_task_after ]; then if [[ ! -f $file_task_after ]]; then
echo -e "复制一份 $file_task_sample$file_task_after\n" echo -e "复制一份 $file_task_sample$file_task_after\n"
cp -fv $file_task_sample $file_task_after cp -fv $file_task_sample $file_task_after
echo echo
fi fi
if [ ! -f $file_extra_shell ]; then if [[ ! -f $file_extra_shell ]]; then
echo -e "复制一份 $file_extra_sample$file_extra_shell\n" echo -e "复制一份 $file_extra_sample$file_extra_shell\n"
cp -fv $file_extra_sample $file_extra_shell cp -fv $file_extra_sample $file_extra_shell
echo echo
fi fi
if [ ! -s $file_auth_user ]; then if [[ ! -s $file_auth_user ]]; then
echo -e "复制一份 $file_auth_sample$file_auth_user\n" echo -e "复制一份 $file_auth_sample$file_auth_user\n"
cp -fv $file_auth_sample $file_auth_user cp -fv $file_auth_sample $file_auth_user
echo echo
fi fi
if [ ! -s $file_notify_py ]; then if [[ ! -s $file_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$file_notify_py\n" echo -e "复制一份 $file_notify_py_sample$file_notify_py\n"
cp -fv $file_notify_py_sample $file_notify_py cp -fv $file_notify_py_sample $file_notify_py
echo echo
fi fi
if [ ! -s $file_notify_js ]; then if [[ ! -s $file_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$file_notify_js\n" echo -e "复制一份 $file_notify_js_sample$file_notify_js\n"
cp -fv $file_notify_js_sample $file_notify_js cp -fv $file_notify_js_sample $file_notify_js
echo echo
fi fi
if [ -s /etc/nginx/conf.d/default.conf ]; then if [[ -s /etc/nginx/conf.d/default.conf ]]; then
echo -e "检测到默认nginx配置文件,清空...\n" echo -e "检测到默认nginx配置文件,清空...\n"
echo '' > /etc/nginx/conf.d/default.conf cat /dev/null > /etc/nginx/conf.d/default.conf
echo echo
fi fi
if [[ ! -s $dep_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$dep_notify_js\n"
cp -fv $file_notify_js_sample $dep_notify_js
echo
fi
if [[ ! -s $dep_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$dep_notify_py\n"
cp -fv $file_notify_py_sample $dep_notify_py
echo
fi
} }
npm_install_sub() { npm_install_sub() {
if [ $is_termux -eq 1 ]; then if [ $is_termux -eq 1 ]; then
npm install --production --no-save --no-bin-links --registry=https://registry.npm.taobao.org || npm install --production --no-bin-links --no-save npm install --production --no-bin-links --registry=https://registry.npm.taobao.org || npm install --production --no-bin-links
elif ! type pnpm >/dev/null 2>&1; then elif ! type pnpm &>/dev/null; then
npm install --production --no-save --registry=https://registry.npm.taobao.org || npm install --production --no-save npm install --production --registry=https://registry.npm.taobao.org || npm install --production
else else
pnpm install --prod pnpm install --production --registry=https://registry.npm.taobao.org || pnpm install --production
fi fi
} }
@@ -240,7 +265,7 @@ npm_install_2() {
diff_and_copy() { diff_and_copy() {
local copy_source=$1 local copy_source=$1
local copy_to=$2 local copy_to=$2
if [ ! -s $copy_to ] || [[ $(diff $copy_source $copy_to) ]]; then if [[ ! -s $copy_to ]] || [[ $(diff $copy_source $copy_to) ]]; then
cp -f $copy_source $copy_to cp -f $copy_source $copy_to
fi fi
} }
@@ -248,17 +273,11 @@ diff_and_copy() {
update_depend() { update_depend() {
local dir_current=$(pwd) local dir_current=$(pwd)
if [ ! -s $dir_scripts/package.json ] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
cp -f $dir_sample/package.json $dir_scripts/package.json cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts npm_install_2 $dir_scripts
fi fi
if [ ! -s $dir_scripts/requirements.txt ] || [[ $(diff $dir_sample/requirements.txt $dir_scripts/requirements.txt) ]]; then
cp -f $dir_sample/requirements.txt $dir_scripts/requirements.txt
cd $dir_scripts
pip3 install -r $dir_scripts/requirements.txt
fi
cd $dir_current cd $dir_current
} }
@@ -266,9 +285,9 @@ git_clone_scripts() {
local url=$1 local url=$1
local dir=$2 local dir=$2
local branch=$3 local branch=$3
[[ $branch ]] && local cmd="-b $branch " [[ $branch ]] && local part_cmd="-b $branch "
echo -e "开始克隆仓库 $url$dir\n" echo -e "开始克隆仓库 $url$dir\n"
git clone $cmd $url $dir git clone $part_cmd $url $dir
exit_status=$? exit_status=$?
} }
@@ -276,12 +295,12 @@ 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"
[[ $branch ]] && local cmd="origin/${branch}" [[ $branch ]] && local part_cmd="origin/${branch}"
cd $dir_work cd $dir_work
echo -e "开始更新仓库:$dir_work\n" echo -e "开始更新仓库:$dir_work\n"
git fetch --all git fetch --all
exit_status=$? exit_status=$?
git reset --hard $cmd git reset --hard $part_cmd
git pull git pull
cd $dir_current cd $dir_current
} }
@@ -292,20 +311,27 @@ reset_romote_url() {
local url=$2 local url=$2
local branch="$3" local branch="$3"
[[ $branch ]] && local cmd="origin/${branch}" [[ $branch ]] && local part_cmd="origin/${branch}"
if [ -d "$dir_work/.git" ]; then if [[ -d "$dir_work/.git" ]]; then
cd $dir_work cd $dir_work
[[ -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
git reset --hard $cmd >/dev/null git reset --hard $part_cmd >/dev/null
cd $dir_current cd $dir_current
fi fi
} }
random_range() {
local beg=$1
local end=$2
echo $((RANDOM % ($end - $beg) + $beg))
}
init_env init_env
detect_termux detect_termux
detect_macos detect_macos
define_cmd define_cmd
fix_config
[ -f $task_error_log_path ] && rm $task_error_log_path import_config $1 > $task_error_log_path 2>&1
import_config $1 >> $task_error_log_path 2>&1
+202 -64
View File
@@ -7,14 +7,14 @@ dir_shell=/ql/shell
## 选择python3还是node ## 选择python3还是node
define_program() { define_program() {
local p1=$1 local first_param=$1
if [[ $p1 == *.js ]]; then if [[ $first_param == *.js ]]; then
which_program="node" which_program="node"
elif [[ $p1 == *.py ]]; then elif [[ $first_param == *.py ]]; then
which_program="python3" which_program="python3"
elif [[ $p1 == *.sh ]]; then elif [[ $first_param == *.sh ]]; then
which_program="bash" which_program="bash"
elif [[ $p1 == *.ts ]]; then elif [[ $first_param == *.ts ]]; then
which_program="ts-node-transpile-only" which_program="ts-node-transpile-only"
else else
which_program="" which_program=""
@@ -39,7 +39,7 @@ gen_array_scripts() {
local i="-1" local i="-1"
cd $dir_scripts cd $dir_scripts
for file in $(ls); do for file in $(ls); do
if [ -f $file ] && [[ $file == *.js && $file != sendNotify.js ]]; then if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
let i++ let i++
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g") array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1) array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1)
@@ -56,7 +56,8 @@ usage() {
echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:" echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:"
echo -e "1.$cmd_task <file_name> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数" echo -e "1.$cmd_task <file_name> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数"
echo -e "2.$cmd_task <file_name> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中" echo -e "2.$cmd_task <file_name> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中"
echo -e "3.$cmd_task <file_name> conc <环境变量名> # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中" echo -e "3.$cmd_task <file_name> conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行"
echo -e "4.$cmd_task <file_name> desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行"
if [[ ${#array_scripts[*]} -gt 0 ]]; then if [[ ${#array_scripts[*]} -gt 0 ]]; then
echo -e "\n当前有以下脚本可以运行:" echo -e "\n当前有以下脚本可以运行:"
for ((i = 0; i < ${#array_scripts[*]}; i++)); do for ((i = 0; i < ${#array_scripts[*]}; i++)); do
@@ -75,134 +76,271 @@ run_nohup() {
## 正常运行单个脚本,$1:传入参数 ## 正常运行单个脚本,$1:传入参数
run_normal() { run_normal() {
local p1=$1 local first_param=$1
cd $dir_scripts define_program "$first_param"
define_program "$p1" if [[ $first_param == *.js ]]; then
if [[ $p1 == *.js ]]; then
if [[ $# -eq 1 ]]; then if [[ $# -eq 1 ]]; then
random_delay random_delay
fi fi
fi fi
log_time=$(date "+%Y-%m-%d-%H-%M-%S") log_time=$(date "+%Y-%m-%d-%H-%M-%S")
log_dir_tmp="${p1##*/}" log_dir_tmp="${first_param##*/}"
log_dir_tmp_path="${first_param%%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="$dir_log/${log_dir_tmp%%.*}" log_dir="$dir_log/${log_dir_tmp%%.*}"
log_path="$log_dir/$log_time.log" log_path="$log_dir/$log_time.log"
cmd="&>> $log_path"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$log_dir" make_dir "$log_dir"
local id=$(cat $list_crontab_user | grep -E "$cmd_task $p1" | perl -pe "s|.*ID=(.*) $cmd_task $p1\.*|\1|" | head -1 | awk -F " " '{print $1}')
local begin_time=$(date '+%Y-%m-%d %H:%M:%S') local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
echo -e "## 开始执行... $begin_time\n" >> $log_path local begin_timestamp=$(date "+%s" -d "$begin_time")
cat $task_error_log_path >> $log_path eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" local id=$(cat $list_crontab_user | grep -E "$cmd_task $first_param" | perl -pe "s|.*ID=(.*) $cmd_task $first_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
. $file_task_before >> $log_path 2>&1 [[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
timeout -k 10s $command_timeout_time $which_program $p1 >> $log_path 2>&1 cd $dir_scripts
local relative_path="${first_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${first_param} =~ "/" ]]; then
cd ${relative_path}
first_param=${first_param/$relative_path\//}
fi
eval timeout -k 10s $command_timeout_time $which_program $first_param $cmd
. $file_task_after >> $log_path 2>&1 eval . $file_task_after "$@" $cmd
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path"
local end_time=$(date '+%Y-%m-%d %H:%M:%S') local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time"))) local end_timestamp=$(date "+%s" -d "$end_time")
echo -e "\n## 执行结束... $end_time 耗时 $diff_time" >> $log_path local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
} }
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行 ## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
run_concurrent() { run_concurrent() {
local p1=$1 local first_param="$1"
local p3=$3 local env_param="$2"
if [[ ! $p3 ]]; then local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]]; then
echo -e "\n 缺少并发运行的环境变量参数" echo -e "\n 缺少并发运行的环境变量参数"
exit 1 exit 1
fi fi
cd $dir_scripts local envs=$(eval echo "\$${env_param}")
define_program "$p1" local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<< ${runArr[@]}))
local n=0
for i in ${runArr[@]}; do
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
define_program "$first_param"
log_time=$(date "+%Y-%m-%d-%H-%M-%S") log_time=$(date "+%Y-%m-%d-%H-%M-%S")
log_dir_tmp="${p1##*/}" log_dir_tmp="${first_param##*/}"
log_dir_tmp_path="${first_param%%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="$dir_log/${log_dir_tmp%%.*}" log_dir="$dir_log/${log_dir_tmp%%.*}"
log_path="$log_dir/$log_time.log" log_path="$log_dir/$log_time.log"
cmd="&>> $log_path"
[[ "$show_log" == "true" ]] && cmd=""
make_dir $log_dir make_dir $log_dir
local id=$(cat $list_crontab_user | grep -E "$cmd_task $p1" | perl -pe "s|.*ID=(.*) $cmd_task $p1\.*|\1|" | head -1 | awk -F " " '{print $1}')
local begin_time=$(date '+%Y-%m-%d %H:%M:%S') local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
echo -e "## 开始执行... $begin_time\n" >> $log_path local begin_timestamp=$(date "+%s" -d "$begin_time")
cat $task_error_log_path >> $log_path
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path"
. $file_task_before >> $log_path 2>&1
local envs=$(eval echo "\$${p3}") eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $first_param" | perl -pe "s|.*ID=(.*) $cmd_task $first_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
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.%N")
cd $dir_scripts
local relative_path="${first_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${first_param} =~ "/" ]]; then
cd ${relative_path}
first_param=${first_param/$relative_path\//}
fi
for i in "${!array[@]}"; do for i in "${!array[@]}"; do
export ${p3}=${array[i]} export ${env_param}=${array[i]}
single_log_path="$log_dir/${single_log_time}_$((i + 1)).log" single_log_path="$log_dir/${single_log_time}_$((i + 1)).log"
timeout -k 10s $command_timeout_time $which_program $p1 &>$single_log_path 2>&1 & timeout -k 10s $command_timeout_time $which_program $first_param &>$single_log_path &
done done
wait wait
for i in "${!array[@]}"; do for i in "${!array[@]}"; do
single_log_path="$log_dir/${single_log_time}_$((i + 1)).log" single_log_path="$log_dir/${single_log_time}_$((i + 1)).log"
cat $single_log_path >> $log_path eval cat $single_log_path $cmd
[ -f $single_log_path ] && rm -f $single_log_path [[ -f $single_log_path ]] && rm -f $single_log_path
done done
. $file_task_after >> $log_path 2>&1 eval . $file_task_after "$@" $cmd
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path"
local end_time=$(date '+%Y-%m-%d %H:%M:%S') local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time"))) local end_timestamp=$(date "+%s" -d "$end_time")
echo -e "\n## 执行结束... $end_time 耗时 $diff_time" >> $log_path local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
}
run_designated() {
local file_param="$1"
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]] || [[ ! $num_param ]]; then
echo -e "\n 缺少单独运行的参数 task xxx.js desi Test 1 3"
exit 1
fi
define_program "$file_param"
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
log_dir_tmp="${file_param##*/}"
log_dir_tmp_path="${file_param%%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="$dir_log/${log_dir_tmp%%.*}"
log_path="$log_dir/$log_time.log"
cmd="&>> $log_path"
[[ "$show_log" == "true" ]] && cmd=""
make_dir $log_dir
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
local begin_timestamp=$(date "+%s" -d "$begin_time")
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<< ${runArr[@]}))
local n=0
for i in ${runArr[@]}; do
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
eval timeout -k 10s $command_timeout_time $which_program $file_param $cmd
eval . $file_task_after "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s" -d "$end_time")
local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
} }
## 运行其他命令 ## 运行其他命令
run_else() { run_else() {
local log_time=$(date "+%Y-%m-%d-%H-%M-%S") local file_param="$1"
local log_dir_tmp="${1##*/}" define_program "$file_param"
local log_dir="$dir_log/${log_dir_tmp%%.*}" log_time=$(date "+%Y-%m-%d-%H-%M-%S")
log_dir_tmp="${file_param##*/}"
log_dir_tmp_path="${file_param%%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="$dir_log/${log_dir_tmp%%.*}"
log_path="$log_dir/$log_time.log" log_path="$log_dir/$log_time.log"
make_dir "$log_dir" cmd="&>> $log_path"
[[ "$show_log" == "true" ]] && cmd=""
make_dir $log_dir
local id=$(cat $list_crontab_user | grep -E "$cmd_task $p1" | perl -pe "s|.*ID=(.*) $cmd_task $p1\.*|\1|" | head -1 | awk -F " " '{print $1}')
local begin_time=$(date '+%Y-%m-%d %H:%M:%S') local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
echo -e "## 开始执行... $begin_time\n" >> $log_path local begin_timestamp=$(date "+%s" -d "$begin_time")
cat $task_error_log_path >> $log_path
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
. $file_task_before >> $log_path 2>&1 [[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
timeout -k 10s $command_timeout_time "$@" >> $log_path 2>&1 local id=$(cat $list_crontab_user | grep -E "$cmd_task $@" | perl -pe "s|.*ID=(.*) $cmd_task $@\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
. $file_task_after >> $log_path 2>&1 cd $dir_scripts
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
shift
eval timeout -k 10s $command_timeout_time $which_program "$file_param" "$@" $cmd
eval . $file_task_after "$file_param" "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S') local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time"))) local end_timestamp=$(date "+%s" -d "$end_time")
echo -e "\n## 执行结束... $end_time 耗时 $diff_time" >> $log_path local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
} }
## 命令检测 ## 命令检测
main() { main() {
show_log="false"
while getopts ":l" opt
do
case $opt in
l)
show_log="true"
;;
esac
done
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
case $# in case $# in
1) 1)
run_normal $1 run_normal "$1"
;; ;;
2 | 3) *)
case $2 in case $2 in
now) now)
run_normal $1 $2 run_normal "$1" "$2"
;; ;;
conc) conc)
run_concurrent $1 $2 $3 run_concurrent "$1" "$3" "$*"
;;
desi)
run_designated "$1" "$3" "$*"
;; ;;
*) *)
run_else "$@" run_else "$@"
;; ;;
esac esac
;; ;;
*)
run_else "$@"
;;
esac esac
cat $log_path [[ -f $log_path ]] && cat $log_path
elif [[ $# -eq 0 ]]; then elif [[ $# -eq 0 ]]; then
echo echo
usage usage
+107 -71
View File
@@ -12,12 +12,12 @@ diff_cron() {
local list_task="$2" local list_task="$2"
local list_add="$3" local list_add="$3"
local list_drop="$4" local list_drop="$4"
if [ -s $list_task ]; then if [[ -s $list_task ]]; then
grep -vwf $list_task $list_scripts >$list_add grep -vwf $list_task $list_scripts >$list_add
elif [ ! -s $list_task ] && [ -s $list_scripts ]; then elif [[ ! -s $list_task ]] && [[ -s $list_scripts ]]; then
cp -f $list_scripts $list_add cp -f $list_scripts $list_add
fi fi
if [ -s $list_scripts ]; then if [[ -s $list_scripts ]]; then
grep -vwf $list_scripts $list_task >$list_drop grep -vwf $list_scripts $list_task >$list_drop
else else
cp -f $list_task $list_drop cp -f $list_task $list_drop
@@ -28,18 +28,18 @@ diff_cron() {
detect_config_version() { detect_config_version() {
## 识别出两个文件的版本号 ## 识别出两个文件的版本号
ver_config_sample=$(grep " Version: " $file_config_sample | perl -pe "s|.+v((\d+\.?){3})|\1|") 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 $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 [[ -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_date=$(grep " Date: " $file_config_sample | awk -F ": " '{print $2}')
update_content=$(grep " Update Content: " $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 $file_config_user ]] && [[ $ver_config_user != $ver_config_sample ]] && [[ $update_date == $(date "+%Y-%m-%d") ]]; then
if [ ! -f $send_mark ]; then if [[ ! -f $send_mark ]]; then
local notify_title="配置文件更新通知" 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" 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 echo -e $notify_content
@@ -47,7 +47,7 @@ detect_config_version() {
[[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark [[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark
fi fi
else else
[ -f $send_mark ] && rm -f $send_mark [[ -f $send_mark ]] && rm -f $send_mark
fi fi
} }
@@ -55,7 +55,7 @@ detect_config_version() {
output_list_add_drop() { output_list_add_drop() {
local list=$1 local list=$1
local type=$2 local type=$2
if [ -s $list ]; then if [[ -s $list ]]; then
echo -e "检测到有$type的定时任务\n" echo -e "检测到有$type的定时任务\n"
cat $list cat $list
echo echo
@@ -104,8 +104,9 @@ add_cron() {
local detail="" local detail=""
cd $dir_scripts cd $dir_scripts
for file in $(cat $list_add); do for file in $(cat $list_add); do
local file_name=${file/${path}\_/} local file_name=${file/${path}\//}
if [ -f $file ]; then file_name=${file_name/${path}\_/}
if [[ -f $file ]]; then
cron_line=$( cron_line=$(
perl -ne "{ perl -ne "{
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/ print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/
@@ -120,7 +121,7 @@ add_cron() {
[[ -z $cron_name ]] && cron_name="$file_name" [[ -z $cron_name ]] && cron_name="$file_name"
[[ -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="0 6 * * *" [[ -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")
echo -e "$result" echo -e "$result"
if [[ $detail ]]; then if [[ $detail ]]; then
@@ -147,13 +148,15 @@ update_repo() {
local repo_path="${dir_repo}/${uniq_path}" local repo_path="${dir_repo}/${uniq_path}"
make_dir "${dir_scripts}/${uniq_path}"
local formatUrl="$url" local formatUrl="$url"
if [[ $github_proxy_url ]]; then if [[ $github_proxy_url ]]; then
if [[ $url =~ "github.com" ]] || [[ $url =~ "githubusercontent.com" ]]; then if [[ $url =~ "github.com" ]] || [[ $url =~ "githubusercontent.com" ]]; then
[[ ! $url =~ "https://ghproxy.com" ]] && formatUrl="${github_proxy_url}${url}" [[ ! $url =~ "https://ghproxy.com" ]] && formatUrl="${github_proxy_url}${url}"
fi fi
fi fi
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}"
else else
@@ -210,7 +213,7 @@ update_raw() {
[[ -z $cron_name ]] && cron_name="$raw_file_name" [[ -z $cron_name ]] && cron_name="$raw_file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | 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=$(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 "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="0 6 * * *" [[ -z $cron_line ]] && cron_line="$default_cron"
if [[ -z $cron_id ]]; then if [[ -z $cron_id ]]; then
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name") result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name")
echo -e "$result\n" echo -e "$result\n"
@@ -219,7 +222,7 @@ update_raw() {
fi fi
else else
echo -e "下载 ${raw_file_name} 失败,保留之前正常下载的版本...\n" echo -e "下载 ${raw_file_name} 失败,保留之前正常下载的版本...\n"
[ -f "$dir_raw/${raw_file_name}.new" ] && rm -f "$dir_raw/${raw_file_name}.new" [[ -f "$dir_raw/${raw_file_name}.new" ]] && rm -f "$dir_raw/${raw_file_name}.new"
fi fi
} }
@@ -227,7 +230,7 @@ update_raw() {
## 调用用户自定义的extra.sh ## 调用用户自定义的extra.sh
run_extra_shell() { run_extra_shell() {
if [[ ${EnableExtraShell} == true ]]; then if [[ ${EnableExtraShell} == true ]]; then
if [ -f $file_extra_shell ]; then if [[ -f $file_extra_shell ]]; then
echo -e "--------------------------------------------------------------\n" echo -e "--------------------------------------------------------------\n"
. $file_extra_shell . $file_extra_shell
else else
@@ -240,12 +243,14 @@ run_extra_shell() {
usage() { usage() {
echo -e "本脚本用法:" echo -e "本脚本用法:"
echo -e "1. $cmd_update update # 更新并重启青龙" echo -e "1. $cmd_update update # 更新并重启青龙"
echo -e "1. $cmd_update extra # 运行自定义脚本" echo -e "2. $cmd_update extra # 运行自定义脚本"
echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件" echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> # 更新单个仓库的脚本" echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> # 更新单个仓库的脚本"
echo -e "5. $cmd_update rmlog <days> # 删除旧日志" echo -e "5. $cmd_update rmlog <days> # 删除旧日志"
echo -e "6. $cmd_update bot # 启动tg-bot" echo -e "6. $cmd_update bot # 启动tg-bot"
echo -e "7. $cmd_update check # 检测青龙环境并修复" echo -e "7. $cmd_update check # 检测青龙环境并修复"
echo -e "8. $cmd_update resetlet # 重置登录错误次数"
echo -e "9. $cmd_update resettfa # 禁用两步登录"
} }
## 更新qinglong ## 更新qinglong
@@ -253,7 +258,7 @@ update_qinglong() {
patch_version patch_version
local no_restart="$1" local no_restart="$1"
[ -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} "${github_proxy_url}https://github.com/whyour/qinglong.git" "master" reset_romote_url ${dir_root} "${github_proxy_url}https://github.com/whyour/qinglong.git" "master"
git_pull_scripts $dir_root "master" git_pull_scripts $dir_root "master"
@@ -263,14 +268,14 @@ update_qinglong() {
detect_config_version 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)
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root [[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
else else
echo -e "\n更新$dir_root失败,请检查原因...\n" echo -e "\n更新$dir_root失败,请检查原因...\n"
fi fi
local url="${github_proxy_url}https://github.com/whyour/qinglong-static.git" local url="${github_proxy_url}https://github.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} "master" reset_romote_url ${ql_static_repo} ${url} "master"
git_pull_scripts ${ql_static_repo} "master" git_pull_scripts ${ql_static_repo} "master"
else else
@@ -303,25 +308,21 @@ patch_version() {
echo echo
fi fi
if ! type ts-node >/dev/null 2>&1; then if ! type ts-node &>/dev/null; then
pnpm i -g ts-node typescript tslib pnpm i -g ts-node typescript tslib
fi fi
git config --global pull.rebase false
} }
reload_pm2() { reload_pm2() {
pm2 l >/dev/null 2>&1 pm2 l &>/dev/null
if [[ $(pm2 info panel 2>/dev/null) ]]; then pm2 delete panel --source-map-support --time &>/dev/null
pm2 reload panel --source-map-support --time >/dev/null 2>&1 pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null
else
pm2 start $dir_root/build/app.js -n panel --source-map-support --time >/dev/null 2>&1
fi
if [[ $(pm2 info schedule 2>/dev/null) ]]; then pm2 delete schedule --source-map-support --time &>/dev/null
pm2 reload schedule --source-map-support --time >/dev/null 2>&1 pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null
else
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time >/dev/null 2>&1
fi
} }
## 对比脚本 ## 对比脚本
@@ -335,21 +336,20 @@ diff_scripts() {
gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence" gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence"
local repo="${repo_path##*/}" local list_add="$dir_list_tmp/${uniq_path}_add.list"
local list_add="$dir_list_tmp/${repo}_add.list" local list_drop="$dir_list_tmp/${uniq_path}_drop.list"
local list_drop="$dir_list_tmp/${repo}_drop.list" diff_cron "$dir_list_tmp/${uniq_path}_scripts.list" "$dir_list_tmp/${uniq_path}_user.list" $list_add $list_drop
diff_cron "$dir_list_tmp/${repo}_scripts.list" "$dir_list_tmp/${repo}_user.list" $list_add $list_drop
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 $repo 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 $repo add_cron $list_add $uniq_path
fi fi
fi fi
cd $dir_current cd $dir_current
@@ -364,8 +364,7 @@ gen_list_repo() {
local blackword="$4" local blackword="$4"
local dependence="$5" local dependence="$5"
local repo="${repo_path##*/}" rm -f $dir_list_tmp/${uniq_path}*.list &>/dev/null
rm -f $dir_list_tmp/${repo}*.list >/dev/null 2>&1
cd ${repo_path} cd ${repo_path}
@@ -386,24 +385,34 @@ gen_list_repo() {
if [[ $blackword ]]; then if [[ $blackword ]]; then
files=$(echo "$files" | egrep -v $blackword) files=$(echo "$files" | egrep -v $blackword)
fi fi
cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
cp -f $file_notify_py "${dir_scripts}/${uniq_path}"
if [[ $dependence ]]; then if [[ $dependence ]]; then
cd ${repo_path}
results=$(eval $cmd | sed 's/^..//' | egrep $dependence) results=$(eval $cmd | sed 's/^..//' | egrep $dependence)
for _file in ${results}; do for _file in ${results}; do
file_path=$(dirname $_file) file_path=$(dirname $_file)
make_dir "${dir_scripts}/${file_path}" make_dir "${dir_scripts}/${uniq_path}/${file_path}"
cp -f $_file "${dir_scripts}/${file_path}" cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}"
done done
fi fi
if [[ -d $dir_dep ]]; then
cp -rf $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null
fi
for file in ${files}; do for file in ${files}; do
filename=$(basename $file) filename=$(basename $file)
cp -f $file $dir_scripts/${repo}_${filename} cp -f $file "$dir_scripts/${uniq_path}/${filename}"
echo ${repo}_${filename} >>"$dir_list_tmp/${repo}_scripts.list" echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
cron_id=$(cat $list_crontab_user | grep -E "$cmd_task ${author}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task ${author}_${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 ${repo}_${filename}:$cron_id") result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
fi fi
done done
grep -E "$cmd_task $repo" $list_crontab_user | perl -pe "s|.*ID=(.*) $cmd_task ($repo_.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${repo}_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
} }
@@ -423,6 +432,18 @@ get_uniq_path() {
} }
main() { main() {
## for ql update
show_log="false"
while getopts ":l" opt
do
case $opt in
l)
show_log="true"
;;
esac
done
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
local p1=$1 local p1=$1
local p2=$2 local p2=$2
local p3=$3 local p3=$3
@@ -432,25 +453,28 @@ main() {
local log_time=$(date "+%Y-%m-%d-%H-%M-%S") local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
local log_path="$dir_log/update/${log_time}_$p1.log" local log_path="$dir_log/update/${log_time}_$p1.log"
local begin_time=$(date '+%Y-%m-%d %H:%M:%S') local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
case $p1 in case $p1 in
update) update)
echo -e "## 开始执行... $begin_time\n" >> $log_path cmd=">> $log_path 2>&1"
cat $task_error_log_path >> $log_path [[ "$show_log" == "true" ]] && cmd=""
update_qinglong "$2" >> $log_path eval echo -e "## 开始执行... $begin_time\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
eval update_qinglong "$2" $cmd
;; ;;
extra) extra)
echo -e "## 开始执行... $begin_time\n" >> $log_path echo -e "## 开始执行... $begin_time\n" >>$log_path
cat $task_error_log_path >> $log_path [[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
run_extra_shell >> $log_path run_extra_shell >>$log_path
;; ;;
repo) repo)
get_user_info get_user_info
get_uniq_path "$p2" "$p6" get_uniq_path "$p2" "$p6"
log_path="$dir_log/update/${log_time}_${uniq_path}.log" log_path="$dir_log/update/${log_time}_${uniq_path}.log"
echo -e "## 开始执行... $begin_time\n" >> $log_path echo -e "## 开始执行... $begin_time\n" >>$log_path
cat $task_error_log_path >> $log_path [[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
if [[ -n $p2 ]]; then if [[ -n $p2 ]]; then
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" >> $log_path update_repo "$p2" "$p3" "$p4" "$p5" "$p6" >>$log_path
else else
echo -e "命令输入错误...\n" echo -e "命令输入错误...\n"
usage usage
@@ -460,29 +484,41 @@ main() {
get_user_info get_user_info
get_uniq_path "$p2" get_uniq_path "$p2"
log_path="$dir_log/update/${log_time}_${uniq_path}.log" log_path="$dir_log/update/${log_time}_${uniq_path}.log"
echo -e "## 开始执行... $begin_time\n" >> $log_path echo -e "## 开始执行... $begin_time\n" >>$log_path
cat $task_error_log_path >> $log_path [[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
if [[ -n $p2 ]]; then if [[ -n $p2 ]]; then
update_raw "$p2" >> $log_path update_raw "$p2" >>$log_path
else else
echo -e "命令输入错误...\n" echo -e "命令输入错误...\n"
usage usage
fi fi
;; ;;
rmlog) rmlog)
echo -e "## 开始执行... $begin_time\n" >> $log_path echo -e "## 开始执行... $begin_time\n" >>$log_path
cat $task_error_log_path >> $log_path [[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
. $dir_shell/rmlog.sh "$p2" >> $log_path . $dir_shell/rmlog.sh "$p2" >>$log_path
;; ;;
bot) bot)
echo -e "## 开始执行... $begin_time\n" >> $log_path echo -e "## 开始执行... $begin_time\n" >>$log_path
cat $task_error_log_path >> $log_path [[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
. $dir_shell/bot.sh >> $log_path . $dir_shell/bot.sh >>$log_path
;; ;;
check) check)
echo -e "## 开始执行... $begin_time\n" >> $log_path echo -e "## 开始执行... $begin_time\n" >>$log_path
cat $task_error_log_path >> $log_path [[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
. $dir_shell/check.sh >> $log_path . $dir_shell/check.sh >>$log_path
;;
resetlet)
echo -e "## 开始执行... $begin_time\n" >>$log_path
auth_value=$(cat $file_auth_user | jq '.retries =0' -c)
echo -e "重置登录错误次数成功 \n $auth_value" >>$log_path
echo "$auth_value" >$file_auth_user
;;
resettfa)
echo -e "## 开始执行... $begin_time\n" >>$log_path
auth_value=$(cat $file_auth_user | jq '.twoFactorActivated =false' | jq '.twoFactorActived =false' -c)
echo -e "禁用两步验证成功 \n $auth_value" >>$log_path
echo "$auth_value" >$file_auth_user
;; ;;
*) *)
echo -e "命令输入错误...\n" echo -e "命令输入错误...\n"
@@ -491,7 +527,7 @@ main() {
esac esac
local end_time=$(date '+%Y-%m-%d %H:%M:%S') local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time"))) local diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
echo -e "\n## 执行结束... $end_time 耗时 $diff_time" >> $log_path echo -e "\n## 执行结束... $end_time 耗时 $diff_time" >>$log_path
cat $log_path cat $log_path
} }
-42
View File
@@ -1,42 +0,0 @@
import { history } from 'umi';
import { request } from '@/utils/http';
import config from '@/utils/config';
const titleMap: any = {
'/': '控制面板',
'/login': '登录',
'/crontab': '定时任务',
'/env': '环境变量',
'/config': '配置文件',
'/script': '脚本管理',
'/diff': '对比工具',
'/log': '任务日志',
'/setting': '系统设置',
};
export function render(oldRender: any) {
request
.get(`${config.apiPrefix}user`)
.then((data) => {
if (data.data && data.data.username) {
return oldRender();
}
localStorage.removeItem(config.authKey);
history.push('/login');
oldRender();
})
.catch((e) => {
if (e.response && e.response.status === 401) {
localStorage.removeItem(config.authKey);
history.push('/login');
oldRender();
}
});
}
export function onRouteChange({ matchedRoutes }: any) {
if (matchedRoutes.length) {
const path: string = matchedRoutes[matchedRoutes.length - 1].route.path;
document.title = `${titleMap[path]} - 控制面板`;
}
}
+7
View File
@@ -0,0 +1,7 @@
import { PageLoading } from '@ant-design/pro-layout';
const NewPageLoading = () => {
return <PageLoading delay={1}></PageLoading>;
};
export default NewPageLoading;
+14 -4
View File
@@ -7,6 +7,7 @@ import {
FolderOutlined, FolderOutlined,
RadiusSettingOutlined, RadiusSettingOutlined,
ControlOutlined, ControlOutlined,
ContainerOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
export default { export default {
@@ -18,6 +19,12 @@ export default {
hideInMenu: true, hideInMenu: true,
component: '@/pages/login/index', component: '@/pages/login/index',
}, },
{
name: '初始化',
path: '/initialization',
hideInMenu: true,
component: '@/pages/initialization/index',
},
{ {
path: '/crontab', path: '/crontab',
name: '定时任务', name: '定时任务',
@@ -42,6 +49,12 @@ export default {
icon: <FormOutlined />, icon: <FormOutlined />,
component: '@/pages/script/index', component: '@/pages/script/index',
}, },
{
path: '/dependence',
name: '依赖管理',
icon: <ContainerOutlined />,
component: '@/pages/dependence/index',
},
{ {
path: '/diff', path: '/diff',
name: '对比工具', name: '对比工具',
@@ -62,12 +75,9 @@ export default {
}, },
], ],
}, },
location: {
pathname: '/',
},
navTheme: 'light', navTheme: 'light',
fixSiderbar: true, fixSiderbar: true,
contentWidth: 'Fixed', contentWidth: 'Fixed',
splitMenus: false, splitMenus: false,
logo: 'https://qinglong.whyour.cn/qinglong.png', siderWidth: 180,
} as any; } as any;
+109 -51
View File
@@ -1,3 +1,4 @@
@import '~antd/lib/style/themes/default.less';
@import '~@/styles/variable.less'; @import '~@/styles/variable.less';
@import '~codemirror/lib/codemirror.css'; @import '~codemirror/lib/codemirror.css';
@@ -8,13 +9,6 @@
url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype'); url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype');
} }
body {
height: 100%;
overflow-y: hidden;
background-color: rgb(248, 248, 248);
width: 100% !important;
}
.log-modal { .log-modal {
.ant-modal { .ant-modal {
padding-bottom: 0 !important; padding-bottom: 0 !important;
@@ -51,8 +45,7 @@ body {
overflow: auto; overflow: auto;
.ant-pro-page-container-children-content { .ant-pro-page-container-children-content {
overflow: auto; overflow: auto;
height: calc(100vh - 96px); height: 100%;
height: calc(100vh - var(--vh-offset, 0px) - 96px);
background-color: #fff; background-color: #fff;
padding: 16px; padding: 16px;
} }
@@ -77,7 +70,7 @@ body {
.log-wrapper { .log-wrapper {
.log-select { .log-select {
width: 300px; width: 250px;
} }
.ant-page-header-heading-left { .ant-page-header-heading-left {
min-width: 100px; min-width: 100px;
@@ -121,16 +114,6 @@ body {
} }
} }
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
transition-delay: 99999s;
transition: color 99999s ease-out, background-color 99999s ease-out;
-webkit-transition-delay: 99999s;
-webkit-transition: color 99999s ease-out, background-color 99999s ease-out;
}
.ant-pro-page-container.ql-container-wrapper { .ant-pro-page-container.ql-container-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -139,8 +122,7 @@ input:-webkit-autofill:active {
.ant-pro-grid-content.wide { .ant-pro-grid-content.wide {
flex: 1; flex: 1;
.ant-pro-grid-content-children { .ant-pro-grid-content-children {
height: calc(100% - 48px); height: calc(100% - 36px);
height: calc(100% - var(--vh-offset, 0px) - 48px);
> div, > div,
.log-container, .log-container,
.react-codemirror2, .react-codemirror2,
@@ -152,22 +134,11 @@ input:-webkit-autofill:active {
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.ant-pro-grid-content.wide {
.ant-pro-page-container-children-content {
height: calc(100vh - 144px);
height: calc(100vh - var(--vh-offset, 0px) - 144px);
}
}
.ql-container-wrapper { .ql-container-wrapper {
&.crontab-wrapper, &.crontab-wrapper,
&.log-wrapper, &.log-wrapper,
&.env-wrapper, &.env-wrapper,
&.config-wrapper { &.config-wrapper {
.ant-pro-grid-content.wide .ant-pro-page-container-children-content {
height: calc(100vh - 184px);
height: calc(100vh - var(--vh-offset, 0px) - 184px);
}
.CodeMirror { .CodeMirror {
width: calc(100vw - 80px); width: calc(100vw - 80px);
} }
@@ -181,17 +152,12 @@ input:-webkit-autofill:active {
.ant-layout-content.ant-pro-basicLayout-content.ant-pro-basicLayout-has-header { .ant-layout-content.ant-pro-basicLayout-content.ant-pro-basicLayout-has-header {
margin-bottom: 0 !important; margin-bottom: 0 !important;
}
@media (min-width: 768px) {
.ant-layout-content.ant-pro-basicLayout-content.ant-pro-basicLayout-has-header {
min-height: calc(100vh - 72px); min-height: calc(100vh - 72px);
min-height: calc(100vh - var(--vh-offset, 0px) - 72px); min-height: calc(100vh - var(--vh-offset, 0px) - 72px);
}
} }
.Resizer { .Resizer {
background: #000; background: #fff;
opacity: 0.2; opacity: 0.2;
z-index: 1; z-index: 1;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
@@ -200,6 +166,8 @@ input:-webkit-autofill:active {
-moz-background-clip: padding; -moz-background-clip: padding;
-webkit-background-clip: padding; -webkit-background-clip: padding;
background-clip: padding-box; background-clip: padding-box;
border-left: 5px solid rgba(42, 161, 255, 0);
border-right: 5px solid rgba(42, 161, 255, 0);
} }
.Resizer:hover { .Resizer:hover {
@@ -210,29 +178,22 @@ input:-webkit-autofill:active {
.Resizer.horizontal { .Resizer.horizontal {
height: 11px; height: 11px;
margin: -5px 0; margin: -5px 0;
border-top: 5px solid rgba(255, 255, 255, 0);
border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize; cursor: row-resize;
width: 100%; width: 100%;
} }
.Resizer.horizontal:hover {
border-top: 5px solid rgba(0, 0, 0, 0.5);
border-bottom: 5px solid rgba(0, 0, 0, 0.5);
}
.Resizer.vertical { .Resizer.vertical {
width: 11px; width: 11px;
margin: 0 -5px; margin: 0 -5px;
border-left: 5px solid rgba(255, 255, 255, 0);
border-right: 5px solid rgba(255, 255, 255, 0);
cursor: col-resize; cursor: col-resize;
} }
.Resizer.horizontal:hover,
.Resizer.vertical:hover { .Resizer.vertical:hover {
border-left: 5px solid rgba(0, 0, 0, 0.5); border-left-color: rgba(42, 161, 255, 0.5);
border-right: 5px solid rgba(0, 0, 0, 0.5); border-right-color: rgba(42, 161, 255, 0.5);
} }
.Resizer.disabled { .Resizer.disabled {
cursor: not-allowed; cursor: not-allowed;
} }
@@ -246,10 +207,107 @@ input:-webkit-autofill:active {
} }
} }
.ant-message .ant-statistic { .inline-countdown.ant-statistic {
display: inline-block; display: inline-block;
.ant-statistic-content { .ant-statistic-content {
font-size: 14px; font-size: 14px;
padding: 0 3px; padding: 0 3px;
} }
} }
.ant-form-item-extra {
word-break: break-all;
font-size: 13px;
}
[data-dark='true'] {
/* Change autocomplete styles in WebKit */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus,
select:-webkit-autofill,
select:-webkit-autofill:hover,
select:-webkit-autofill:focus {
border: 1px solid @border-color-base;
box-shadow: none;
transition: background-color 5000s ease-in-out 0s;
-webkit-text-fill-color: @text-color;
caret-color: #e8e6f3 !important;
}
::placeholder {
opacity: 0.5 !important;
}
.ant-select-selection-placeholder {
opacity: 0.5 !important;
}
}
.ant-pro-page-container-children-content {
margin: 18px 18px 18px;
}
.ant-pro-basicLayout-content {
margin: 18px;
.ant-pro-page-container {
margin: -18px -18px -18px;
}
}
.ant-menu-item.ant-pro-sider-collapsed-button {
margin: 0;
}
.side-menu-container {
display: flex;
justify-content: space-between;
align-items: center;
color: rgba(0, 0, 0, 0.85);
.side-menu-collapse-button:hover {
color: #1890ff;
}
.side-menu-user-wrapper {
display: flex;
align-items: center;
}
}
.side-menu-user-drop-menu {
position: relative;
margin: 0;
padding: 4px 0;
text-align: left;
background-clip: padding-box;
outline: none;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 6px 16px -8px rgb(0 0 0 / 8%), 0 9px 28px 0 rgb(0 0 0 / 5%),
0 12px 48px 16px rgb(0 0 0 / 3%);
.ant-dropdown-menu-item:hover {
color: #1890ff;
}
}
.ant-pro-sider-logo {
padding: 16px 8px !important;
h1 {
margin-left: 5px !important;
}
img {
width: 32px !important;
border-radius: 52% !important;
}
}
.ant-pro-global-header-logo {
a img {
// 移动端logo被拉伸
width: auto !important;
}
}
+256 -45
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import ProLayout from '@ant-design/pro-layout'; import ProLayout, { PageLoading } from '@ant-design/pro-layout';
import { import {
enable as enableDarkMode, enable as enableDarkMode,
disable as disableDarkMode, disable as disableDarkMode,
@@ -8,60 +8,208 @@ import {
} from 'darkreader'; } from 'darkreader';
import defaultProps from './defaultProps'; import defaultProps from './defaultProps';
import { Link, history } from 'umi'; import { Link, history } from 'umi';
import { LogoutOutlined } from '@ant-design/icons'; import {
LogoutOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
UserOutlined,
} from '@ant-design/icons';
import config from '@/utils/config'; 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, changeLog } from '../version'; import { version, changeLogLink, changeLog } from '../version';
import { useCtx, useTheme } from '@/utils/hooks';
import { message, Badge, Modal, Avatar, Dropdown, Menu, Image } from 'antd';
// @ts-ignore
import SockJS from 'sockjs-client';
import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing';
Sentry.init({
dsn: 'https://3406424fb1dc4813a62d39e844a9d0ac@o1098464.ingest.sentry.io/6122818',
integrations: [new Integrations.BrowserTracing()],
release: version,
tracesSampleRate: 1.0,
});
export default function (props: any) { export default function (props: any) {
const ctx = useCtx();
const theme = useTheme();
const [user, setUser] = useState<any>();
const [loading, setLoading] = useState<boolean>(true);
const [systemInfo, setSystemInfo] = useState<{ isInitialized: boolean }>();
const ws = useRef<any>(null);
const [socketMessage, setSocketMessage] = useState<any>();
const [collapsed, setCollapsed] = useState(false);
const logout = () => { const logout = () => {
request.post(`${config.apiPrefix}logout`).then(() => { request.post(`${config.apiPrefix}user/logout`).then(() => {
localStorage.removeItem(config.authKey); localStorage.removeItem(config.authKey);
history.push('/login'); history.push('/login');
}); });
}; };
useEffect(() => { const getSystemInfo = () => {
const isAuth = localStorage.getItem(config.authKey); request
if (!isAuth) { .get(`${config.apiPrefix}system`)
history.push('/login'); .then(({ code, data }) => {
if (code === 200) {
setSystemInfo(data);
if (!data.isInitialized) {
history.push('/initialization');
setLoading(false);
} else {
getUser();
} }
vhCheck(); } else {
message.error(data);
}
})
.catch((error) => {
console.log(error);
});
};
// patch custome layout title as react node [object, object] const getUser = (needLoading = true) => {
document.title = '控制面板'; needLoading && setLoading(true);
request
.get(`${config.apiPrefix}user`)
.then(({ code, data }) => {
if (code === 200 && data.username) {
setUser(data);
if (props.location.pathname === '/') {
history.push('/crontab');
}
} else {
message.error(data);
}
needLoading && setLoading(false);
})
.catch((error) => {
console.log(error);
});
};
const reloadUser = () => {
getUser(false);
};
const setTheme = () => {
const media = window.matchMedia('(prefers-color-scheme: dark)');
const storageTheme = localStorage.getItem('qinglong_dark_theme');
const isDark =
(media.matches && storageTheme !== 'light') || storageTheme === 'dark';
if (isDark) {
document.body.setAttribute('data-dark', 'true');
} else {
document.body.setAttribute('data-dark', 'false');
}
};
useEffect(() => {
vhCheck();
}, []); }, []);
useEffect(() => { useEffect(() => {
if (props.location.pathname === '/') { if (systemInfo && systemInfo.isInitialized && !user) {
history.push('/crontab'); getUser();
} }
}, [props.location.pathname]); }, [props.location.pathname]);
useEffect(() => { useEffect(() => {
const theme = localStorage.getItem('qinglong_dark_theme') || 'auto'; if (!systemInfo) {
getSystemInfo();
}
}, [systemInfo]);
useEffect(() => {
setTheme();
}, [theme.theme]);
useEffect(() => {
const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
setFetchMethod(window.fetch); setFetchMethod(window.fetch);
if (theme === 'dark') { if (_theme === 'dark') {
enableDarkMode({ enableDarkMode({});
brightness: 100, } else if (_theme === 'light') {
contrast: 90,
sepia: 10,
});
} else if (theme === 'light') {
disableDarkMode(); disableDarkMode();
} else { } else {
followSystemColorScheme({ followSystemColorScheme({});
brightness: 100,
contrast: 90,
sepia: 10,
});
} }
}, []); }, []);
if (props.location.pathname === '/login') { useEffect(() => {
return props.children; if (!user) return;
ws.current = new SockJS(
`${location.origin}/api/ws?token=${localStorage.getItem(config.authKey)}`,
);
ws.current.onmessage = (e: any) => {
try {
const data = JSON.parse(e.data);
if (data.type === 'ping') {
if (data && data.message === 'hanhh') {
console.log('websocket连接成功', e);
} else {
console.log('websocket连接失败', e);
}
}
setSocketMessage(data);
} catch (error) {
console.log('websocket连接失败', e);
}
};
const wsCurrent = ws.current;
return () => {
wsCurrent.close();
};
}, [user]);
useEffect(() => {
window.onload = () => {
const timing = performance.timing;
console.log(`白屏时间: ${timing.responseStart - timing.navigationStart}`);
console.log(
`请求完毕至DOM加载: ${timing.domInteractive - timing.responseEnd}`,
);
console.log(
`解释dom树耗时: ${timing.domComplete - timing.domInteractive}`,
);
console.log(
`从开始至load总耗时: ${timing.loadEventEnd - timing.navigationStart}`,
);
Sentry.captureMessage(
`白屏时间 ${timing.responseStart - timing.navigationStart}`,
);
};
}, []);
if (['/login', '/initialization'].includes(props.location.pathname)) {
document.title = `${
(config.documentTitleMap as any)[props.location.pathname]
} - 控制面板`;
if (
systemInfo?.isInitialized &&
props.location.pathname === '/initialization'
) {
history.push('/crontab');
}
if (systemInfo) {
return React.Children.map(props.children, (child) => {
return React.cloneElement(child, {
...ctx,
...theme,
user,
reloadUser,
reloadTheme: setTheme,
ws: ws.current,
});
});
}
} }
const isFirefox = navigator.userAgent.includes('Firefox'); const isFirefox = navigator.userAgent.includes('Firefox');
@@ -70,13 +218,37 @@ export default function (props: any) {
!navigator.userAgent.includes('Chrome'); !navigator.userAgent.includes('Chrome');
const isQQBrowser = navigator.userAgent.includes('QQBrowser'); const isQQBrowser = navigator.userAgent.includes('QQBrowser');
return ( const menu = (
<Menu className="side-menu-user-drop-menu">
<Menu.Item key="1" icon={<LogoutOutlined />} onClick={logout}>
退
</Menu.Item>
</Menu>
);
return loading ? (
<PageLoading />
) : (
<ProLayout <ProLayout
selectedKeys={[props.location.pathname]} selectedKeys={[props.location.pathname]}
loading={loading}
ErrorBoundary={Sentry.ErrorBoundary}
logo={
<Image
preview={false}
src="https://pic.imgdb.cn/item/61acd0dd2ab3f51d912b1986.png"
/>
}
title={ title={
<> <>
<span style={{ fontSize: 16 }}></span>
<a href={changeLog} target="_blank" rel="noopener noreferrer"> <a
href={changeLogLink}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => {
e.stopPropagation();
}}
>
<span <span
style={{ style={{
fontSize: isFirefox ? 9 : 12, fontSize: isFirefox ? 9 : 12,
@@ -86,7 +258,7 @@ export default function (props: any) {
letterSpacing: isQQBrowser ? -2 : 0, letterSpacing: isQQBrowser ? -2 : 0,
}} }}
> >
{version} v{version}
</span> </span>
</a> </a>
</> </>
@@ -101,21 +273,60 @@ export default function (props: any) {
} }
return <Link to={menuItemProps.path}>{defaultDom}</Link>; return <Link to={menuItemProps.path}>{defaultDom}</Link>;
}} }}
postMenuData={(menuData) => { pageTitleRender={(props, pageName, info) => {
return [ if (info && typeof info.pageName === 'string') {
...(menuData || []), return `${info.pageName} - 控制面板`;
{ }
icon: <LogoutOutlined />, return '控制面板';
name: '退出登录',
path: 'logout',
onTitleClick: () => logout(),
},
];
}} }}
pageTitleRender={() => '控制面板'} onCollapse={setCollapsed}
collapsed={collapsed}
rightContentRender={() =>
ctx.isPhone && (
<Dropdown overlay={menu} trigger={['click']}>
<span className="side-menu-user-wrapper">
<Avatar shape="square" size="small" icon={<UserOutlined />} />
<span style={{ marginLeft: 5 }}>admin</span>
</span>
</Dropdown>
)
}
collapsedButtonRender={(collapsed) => (
<span
className="side-menu-container"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
{!collapsed && !ctx.isPhone && (
<Dropdown overlay={menu} trigger={['hover']}>
<span className="side-menu-user-wrapper">
<Avatar shape="square" size="small" icon={<UserOutlined />} />
<span style={{ marginLeft: 5 }}>admin</span>
</span>
</Dropdown>
)}
<span
className="side-menu-collapse-button"
onClick={() => setCollapsed(!collapsed)}
>
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</span>
</span>
)}
{...defaultProps} {...defaultProps}
> >
{props.children} {React.Children.map(props.children, (child) => {
return React.cloneElement(child, {
...ctx,
...theme,
user,
reloadUser,
reloadTheme: setTheme,
socketMessage,
});
})}
</ProLayout> </ProLayout>
); );
} }
+10 -5
View File
@@ -11,17 +11,15 @@ import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import Editor from '@monaco-editor/react'; import Editor from '@monaco-editor/react';
import { Controlled as CodeMirror } from 'react-codemirror2'; import { Controlled as CodeMirror } from 'react-codemirror2';
import { useCtx, useTheme } from '@/utils/hooks';
const Config = () => { const Config = ({ headerStyle, isPhone, theme }: any) => {
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [title, setTitle] = useState('config.sh'); const [title, setTitle] = useState('config.sh');
const [select, setSelect] = useState('config.sh'); const [select, setSelect] = useState('config.sh');
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<any[]>([]);
const { headerStyle, isPhone } = useCtx();
const { theme } = useTheme();
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const [confirmLoading, setConfirmLoading] = useState(false);
const getConfig = (name: string) => { const getConfig = (name: string) => {
request.get(`${config.apiPrefix}configs/${name}`).then((data: any) => { request.get(`${config.apiPrefix}configs/${name}`).then((data: any) => {
@@ -40,6 +38,7 @@ const Config = () => {
}; };
const updateConfig = () => { const updateConfig = () => {
setConfirmLoading(true);
const content = editorRef.current const content = editorRef.current
? editorRef.current.getValue().replace(/\r\n/g, '\n') ? editorRef.current.getValue().replace(/\r\n/g, '\n')
: value; : value;
@@ -50,6 +49,7 @@ const Config = () => {
}) })
.then((data: any) => { .then((data: any) => {
message.success(data.message); message.success(data.message);
setConfirmLoading(false);
}); });
}; };
@@ -79,7 +79,12 @@ const Config = () => {
defaultValue="config.sh" defaultValue="config.sh"
onSelect={onSelect} onSelect={onSelect}
/>, />,
<Button key="1" type="primary" onClick={updateConfig}> <Button
key="1"
loading={confirmLoading}
type="primary"
onClick={updateConfig}
>
</Button>, </Button>,
]} ]}
+3
View File
@@ -0,0 +1,3 @@
.ant-table-pagination.ant-pagination {
margin-bottom: 0 !important;
}
+289 -20
View File
@@ -25,13 +25,18 @@ import {
DeleteOutlined, DeleteOutlined,
PauseCircleOutlined, PauseCircleOutlined,
FieldTimeOutlined, FieldTimeOutlined,
PushpinOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
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 CronModal from './modal'; import CronModal from './modal';
import CronLogModal from './logModal'; import CronLogModal from './logModal';
import { useCtx, useTheme } from '@/utils/hooks'; import cron_parser from 'cron-parser';
import { diffTime } from '@/utils/date';
import { getTableScroll } from '@/utils/index';
import { history } from 'umi';
import './index.less';
const { Text } = Typography; const { Text } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -50,6 +55,8 @@ enum OperationName {
'禁用', '禁用',
'运行', '运行',
'停止', '停止',
'置顶',
'取消置顶',
} }
enum OperationPath { enum OperationPath {
@@ -57,24 +64,44 @@ enum OperationPath {
'disable', 'disable',
'run', 'run',
'stop', 'stop',
'pin',
'unpin',
} }
const Crontab = () => { const Crontab = ({ headerStyle, isPhone }: any) => {
const columns = [ const columns: any = [
{ {
title: '任务名', title: '任务名',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
width: 150,
align: 'center' as const, align: 'center' as const,
render: (text: string, record: any) => ( render: (text: string, record: any) => (
<span>{record.name || record._id}</span> <a
onClick={() => {
goToScriptManager(record);
}}
>
{record.name || record._id}{' '}
{record.isPinned ? (
<span>
<PushpinOutlined />
</span>
) : (
''
)}
</a>
), ),
sorter: {
compare: (a: any, b: any) => a.name.localeCompare(b.name),
multiple: 2,
},
}, },
{ {
title: '任务', title: '任务',
dataIndex: 'command', dataIndex: 'command',
key: 'command', key: 'command',
width: '40%', width: 250,
align: 'center' as const, align: 'center' as const,
render: (text: string, record: any) => { render: (text: string, record: any) => {
return ( return (
@@ -90,19 +117,132 @@ const Crontab = () => {
</span> </span>
); );
}, },
sorter: {
compare: (a: any, b: any) => a.command.localeCompare(b.command),
multiple: 3,
},
}, },
{ {
title: '任务定时', title: '任务定时',
dataIndex: 'schedule', dataIndex: 'schedule',
key: 'schedule', key: 'schedule',
width: 110,
align: 'center' as const, align: 'center' as const,
sorter: {
compare: (a: any, b: any) => a.schedule.localeCompare(b.schedule),
multiple: 1,
}, },
},
{
title: '最后运行时间',
align: 'center' as const,
width: 150,
sorter: {
compare: (a: any, b: any) => {
return a.last_execution_time - b.last_execution_time;
},
},
render: (text: string, record: any) => {
const language = navigator.language || navigator.languages[0];
return (
<span
style={{
display: 'block',
}}
>
{record.last_execution_time
? new Date(record.last_execution_time * 1000)
.toLocaleString(language, {
hour12: false,
})
.replace(' 24:', ' 00:')
: '-'}
</span>
);
},
},
{
title: '最后运行时长',
align: 'center' as const,
width: 120,
sorter: {
compare: (a: any, b: any) => {
return a.last_running_time - b.last_running_time;
},
},
render: (text: string, record: any) => {
const language = navigator.language || navigator.languages[0];
return (
<span
style={{
display: 'block',
}}
>
{record.last_running_time
? diffTime(record.last_running_time)
: '-'}
</span>
);
},
},
{
title: '下次运行时间',
align: 'center' as const,
width: 150,
sorter: {
compare: (a: any, b: any) => {
return a.nextRunTime - b.nextRunTime;
},
},
render: (text: string, record: any) => {
const language = navigator.language || navigator.languages[0];
return (
<span
style={{
display: 'block',
}}
>
{record.nextRunTime
.toLocaleString(language, {
hour12: false,
})
.replace(' 24:', ' 00:')}
</span>
);
},
},
{ {
title: '状态', title: '状态',
key: 'status', key: 'status',
dataIndex: 'status', dataIndex: 'status',
align: 'center' as const, align: 'center' as const,
width: 60, width: 85,
filters: [
{
text: '运行中',
value: 0,
},
{
text: '空闲中',
value: 1,
},
{
text: '已禁用',
value: 2,
},
{
text: '队列中',
value: 3,
},
],
onFilter: (value: number, record: any) => {
if (record.isDisabled && record.status !== 0) {
return value === 2;
} else {
return record.status === value;
}
},
render: (text: string, record: any) => ( render: (text: string, record: any) => (
<> <>
{(!record.isDisabled || record.status !== CrontabStatus.idle) && ( {(!record.isDisabled || record.status !== CrontabStatus.idle) && (
@@ -139,6 +279,7 @@ const Crontab = () => {
title: '操作', title: '操作',
key: 'action', key: 'action',
align: 'center' as const, align: 'center' as const,
width: 100,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
const isPc = !isPhone; const isPc = !isPhone;
return ( return (
@@ -191,7 +332,25 @@ const Crontab = () => {
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]); const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(20); const [pageSize, setPageSize] = useState(20);
const { headerStyle, isPhone } = useCtx(); const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const goToScriptManager = (record: any) => {
const cmd = record.command.split(' ') as string[];
if (cmd[0] === 'task') {
if (cmd[1].startsWith('/ql/scripts')) {
cmd[1] = cmd[1].replace('/ql/scripts/', '');
}
let [p, s] = cmd[1].split('/');
if (!s) {
s = p;
p = '';
}
history.push(`/script?p=${p}&s=${s}`);
} else if (cmd[1] === 'repo') {
location.href = cmd[2];
}
};
const getCrons = () => { const getCrons = () => {
setLoading(true); setLoading(true);
@@ -199,12 +358,28 @@ const Crontab = () => {
.get(`${config.apiPrefix}crons?searchValue=${searchText}`) .get(`${config.apiPrefix}crons?searchValue=${searchText}`)
.then((data: any) => { .then((data: any) => {
setValue( setValue(
data.data.sort((a: any, b: any) => { data.data
.sort((a: any, b: any) => {
const sortA = a.isDisabled ? 4 : a.status; const sortA = a.isDisabled ? 4 : a.status;
const sortB = b.isDisabled ? 4 : b.status; const sortB = b.isDisabled ? 4 : b.status;
a.isPinned = a.isPinned ? a.isPinned : 0;
b.isPinned = b.isPinned ? b.isPinned : 0;
if (a.isPinned === b.isPinned) {
return CrontabSort[sortA] - CrontabSort[sortB]; return CrontabSort[sortA] - CrontabSort[sortB];
}
return b.isPinned - a.isPinned;
})
.map((x) => {
return {
...x,
nextRunTime: cron_parser
.parseExpression(x.schedule)
.next()
.toDate(),
};
}), }),
); );
setCurrentPage(1);
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -238,7 +413,8 @@ const Crontab = () => {
if (data.code === 200) { if (data.code === 200) {
message.success('删除成功'); message.success('删除成功');
const result = [...value]; const result = [...value];
result.splice(index + pageSize * (currentPage - 1), 1); const i = result.findIndex((x) => x._id === record._id);
result.splice(i, 1);
setValue(result); setValue(result);
} else { } else {
message.error(data); message.error(data);
@@ -269,7 +445,8 @@ const Crontab = () => {
.then((data: any) => { .then((data: any) => {
if (data.code === 200) { if (data.code === 200) {
const result = [...value]; const result = [...value];
result.splice(index + pageSize * (currentPage - 1), 1, { const i = result.findIndex((x) => x._id === record._id);
result.splice(i, 1, {
...record, ...record,
status: CrontabStatus.running, status: CrontabStatus.running,
}); });
@@ -303,7 +480,8 @@ const Crontab = () => {
.then((data: any) => { .then((data: any) => {
if (data.code === 200) { if (data.code === 200) {
const result = [...value]; const result = [...value];
result.splice(index + pageSize * (currentPage - 1), 1, { const i = result.findIndex((x) => x._id === record._id);
result.splice(i, 1, {
...record, ...record,
pid: null, pid: null,
status: CrontabStatus.idle, status: CrontabStatus.idle,
@@ -347,7 +525,8 @@ const Crontab = () => {
if (data.code === 200) { if (data.code === 200) {
const newStatus = record.isDisabled === 1 ? 0 : 1; const newStatus = record.isDisabled === 1 ? 0 : 1;
const result = [...value]; const result = [...value];
result.splice(index + pageSize * (currentPage - 1), 1, { const i = result.findIndex((x) => x._id === record._id);
result.splice(i, 1, {
...record, ...record,
isDisabled: newStatus, isDisabled: newStatus,
}); });
@@ -363,6 +542,50 @@ const Crontab = () => {
}); });
}; };
const pinOrUnPinCron = (record: any, index: number) => {
Modal.confirm({
title: `确认${record.isPinned === 1 ? '取消置顶' : '置顶'}`,
content: (
<>
{record.isPinned === 1 ? '取消置顶' : '置顶'}
{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
</>
),
onOk() {
request
.put(
`${config.apiPrefix}crons/${
record.isPinned === 1 ? 'unpin' : 'pin'
}`,
{
data: [record._id],
},
)
.then((data: any) => {
if (data.code === 200) {
const newStatus = record.isPinned === 1 ? 0 : 1;
const result = [...value];
const i = result.findIndex((x) => x._id === record._id);
result.splice(i, 1, {
...record,
isPinned: newStatus,
});
setValue(result);
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const MoreBtn: React.FC<{ const MoreBtn: React.FC<{
record: any; record: any;
index: number; index: number;
@@ -376,7 +599,7 @@ const Crontab = () => {
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
key="enableordisable" key="enableOrDisable"
icon={ icon={
record.isDisabled === 1 ? ( record.isDisabled === 1 ? (
<CheckCircleOutlined /> <CheckCircleOutlined />
@@ -387,11 +610,17 @@ const Crontab = () => {
> >
{record.isDisabled === 1 ? '启用' : '禁用'} {record.isDisabled === 1 ? '启用' : '禁用'}
</Menu.Item> </Menu.Item>
{record.isSystem !== 1 && (
<Menu.Item key="delete" icon={<DeleteOutlined />}> <Menu.Item key="delete" icon={<DeleteOutlined />}>
</Menu.Item> </Menu.Item>
)} <Menu.Item
key="pinOrUnPin"
icon={
record.isPinned === 1 ? <StopOutlined /> : <PushpinOutlined />
}
>
{record.isPinned === 1 ? '取消置顶' : '置顶'}
</Menu.Item>
</Menu> </Menu>
} }
> >
@@ -406,12 +635,15 @@ const Crontab = () => {
case 'edit': case 'edit':
editCron(record, index); editCron(record, index);
break; break;
case 'enableordisable': case 'enableOrDisable':
enabledOrDisabledCron(record, index); enabledOrDisabledCron(record, index);
break; break;
case 'delete': case 'delete':
delCron(record, index); delCron(record, index);
break; break;
case 'pinOrUnPin':
pinOrUnPinCron(record, index);
break;
default: default:
break; break;
} }
@@ -425,12 +657,16 @@ const Crontab = () => {
}; };
const onSearch = (value: string) => { const onSearch = (value: string) => {
setSearchText(value); setSearchText(value.trim());
}; };
const handleCrons = (cron: any) => { const handleCrons = (cron: any) => {
const index = value.findIndex((x) => x._id === cron._id); const index = value.findIndex((x) => x._id === cron._id);
const result = [...value]; const result = [...value];
cron.nextRunTime = cron_parser
.parseExpression(cron.schedule)
.next()
.toDate();
if (index === -1) { if (index === -1) {
result.unshift(cron); result.unshift(cron);
} else { } else {
@@ -447,6 +683,10 @@ const Crontab = () => {
.then((data: any) => { .then((data: any) => {
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.data.nextRunTime = cron_parser
.parseExpression(data.data.schedule)
.next()
.toDate();
result.splice(index, 1, { result.splice(index, 1, {
...cron, ...cron,
...data.data, ...data.data,
@@ -458,6 +698,12 @@ const Crontab = () => {
const onSelectChange = (selectedIds: any[]) => { const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds); setSelectedRowIds(selectedIds);
setTimeout(() => {
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
setTableScrollHeight(getTableScroll());
}
});
}; };
const rowSelection = { const rowSelection = {
@@ -522,6 +768,10 @@ const Crontab = () => {
localStorage.setItem('pageSize', pageSize + ''); localStorage.setItem('pageSize', pageSize + '');
}; };
const getRowClassName = (record: any, index: number) => {
return record.isPinned ? 'pinned-cron' : '';
};
useEffect(() => { useEffect(() => {
if (logCron) { if (logCron) {
localStorage.setItem('logCron', logCron._id); localStorage.setItem('logCron', logCron._id);
@@ -535,6 +785,9 @@ const Crontab = () => {
useEffect(() => { useEffect(() => {
setPageSize(parseInt(localStorage.getItem('pageSize') || '20')); setPageSize(parseInt(localStorage.getItem('pageSize') || '20'));
setTimeout(() => {
setTableScrollHeight(getTableScroll());
});
}, []); }, []);
return ( return (
@@ -550,7 +803,7 @@ const Crontab = () => {
onSearch={onSearch} onSearch={onSearch}
/>, />,
<Button key="2" type="primary" onClick={() => addCron()}> <Button key="2" type="primary" onClick={() => addCron()}>
</Button>, </Button>,
]} ]}
header={{ header={{
@@ -586,6 +839,20 @@ const Crontab = () => {
<Button type="primary" onClick={() => operateCrons(3)}> <Button type="primary" onClick={() => operateCrons(3)}>
</Button> </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>
<span style={{ marginLeft: 8 }}> <span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a> <a>{selectedRowIds?.length}</a>
@@ -595,21 +862,23 @@ const Crontab = () => {
<Table <Table
columns={columns} columns={columns}
pagination={{ pagination={{
hideOnSinglePage: true,
current: currentPage, current: currentPage,
onChange: onPageChange, onChange: onPageChange,
pageSize: pageSize, pageSize: pageSize,
showSizeChanger: true, showSizeChanger: true,
simple: isPhone,
defaultPageSize: 20, defaultPageSize: 20,
showTotal: (total: number, range: number[]) => showTotal: (total: number, range: number[]) =>
`${range[0]}-${range[1]} 条/总共 ${total}`, `${range[0]}-${range[1]} 条/总共 ${total}`,
pageSizeOptions: [10, 20, 50, 100, 200, 500, 1000],
}} }}
dataSource={value} dataSource={value}
rowKey="_id" rowKey="_id"
size="middle" size="middle"
scroll={{ x: 768 }} scroll={{ x: 1000, y: tableScrollHeight }}
loading={loading} loading={loading}
rowSelection={rowSelection} rowSelection={rowSelection}
rowClassName={getRowClassName}
/> />
<CronLogModal <CronLogModal
visible={isLogModalVisible} visible={isLogModalVisible}
+20 -7
View File
@@ -1,11 +1,12 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form, Statistic } 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';
import { import {
Loading3QuartersOutlined, Loading3QuartersOutlined,
CheckCircleOutlined, CheckCircleOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-layout';
enum CrontabStatus { enum CrontabStatus {
'running', 'running',
@@ -26,7 +27,7 @@ const CronLogModal = ({
}) => { }) => {
const [value, setValue] = useState<string>('启动中...'); const [value, setValue] = useState<string>('启动中...');
const [loading, setLoading] = useState<any>(true); const [loading, setLoading] = useState<any>(true);
const [excuting, setExcuting] = 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 [theme, setTheme] = useState<string>('');
@@ -40,7 +41,7 @@ const CronLogModal = ({
if (localStorage.getItem('logCron') === cron._id) { if (localStorage.getItem('logCron') === cron._id) {
const log = data.data as string; const log = data.data as string;
setValue(log || '暂无日志'); setValue(log || '暂无日志');
setExcuting( setExecuting(
log && !log.includes('执行结束') && !log.includes('重启面板'), log && !log.includes('执行结束') && !log.includes('重启面板'),
); );
if (log && !log.includes('执行结束') && !log.includes('重启面板')) { if (log && !log.includes('执行结束') && !log.includes('重启面板')) {
@@ -57,7 +58,11 @@ const CronLogModal = ({
content: ( content: (
<span> <span>
<Countdown format="ss" value={Date.now() + 1000 * 10} /> <Countdown
className="inline-countdown"
format="ss"
value={Date.now() + 1000 * 10}
/>
</span> </span>
), ),
@@ -84,8 +89,8 @@ const CronLogModal = ({
const titleElement = () => { const titleElement = () => {
return ( return (
<> <>
{excuting && <Loading3QuartersOutlined spin />} {(executing || loading) && <Loading3QuartersOutlined spin />}
{!excuting && <CheckCircleOutlined />} {!executing && <CheckCircleOutlined />}
<span style={{ marginLeft: 5 }}>-{cron && cron.name}</span>{' '} <span style={{ marginLeft: 5 }}>-{cron && cron.name}</span>{' '}
</> </>
); );
@@ -110,12 +115,20 @@ const CronLogModal = ({
bodyStyle={{ bodyStyle={{
overflowY: 'auto', overflowY: 'auto',
maxHeight: 'calc(80vh - var(--vh-offset, 0px))', maxHeight: 'calc(80vh - var(--vh-offset, 0px))',
minHeight: '300px',
}} }}
forceRender forceRender
onOk={() => cancel()} onOk={() => cancel()}
onCancel={() => cancel()} onCancel={() => cancel()}
footer={[
<Button type="primary" onClick={() => cancel()}>
</Button>,
]}
> >
{!loading && value && ( {loading ? (
<PageLoading />
) : (
<pre <pre
style={ style={
isPhone isPhone
+11 -3
View File
@@ -27,7 +27,7 @@ const CronModal = ({
data: payload, data: payload,
}); });
if (code === 200) { if (code === 200) {
message.success(cron ? '更新Cron成功' : '添加Cron成功'); message.success(cron ? '更新Cron成功' : '新建Cron成功');
} else { } else {
message.error(data); message.error(data);
} }
@@ -66,8 +66,16 @@ const CronModal = ({
<Form.Item name="name" label="名称"> <Form.Item name="name" label="名称">
<Input placeholder="请输入任务名称" /> <Input placeholder="请输入任务名称" />
</Form.Item> </Form.Item>
<Form.Item name="command" label="命令" rules={[{ required: true }]}> <Form.Item
<Input placeholder="请输入要执行的命令" /> name="command"
label="命令"
rules={[{ required: true, whitespace: true }]}
>
<Input.TextArea
rows={4}
autoSize={true}
placeholder="请输入要执行的命令"
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="schedule" name="schedule"
View File
+450
View File
@@ -0,0 +1,450 @@
import React, { useCallback, useRef, useState, useEffect } from 'react';
import {
Button,
message,
Modal,
Table,
Tag,
Space,
Typography,
Tooltip,
Input,
Tabs,
} from 'antd';
import {
EditOutlined,
DeleteOutlined,
SyncOutlined,
CheckCircleOutlined,
StopOutlined,
BugOutlined,
FileTextOutlined,
} from '@ant-design/icons';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http';
import DependenceModal from './modal';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less';
import { getTableScroll } from '@/utils/index';
import DependenceLogModal from './logModal';
const { Text } = Typography;
const { Search } = Input;
enum Status {
'安装中',
'已安装',
'安装失败',
'删除中',
'已删除',
'删除失败',
}
enum StatusColor {
'processing',
'success',
'error',
}
const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
const columns: any = [
{
title: '序号',
align: 'center' as const,
width: 50,
render: (text: string, record: any, index: number) => {
return <span style={{ cursor: 'text' }}>{index + 1} </span>;
},
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
align: 'center' as const,
},
{
title: '状态',
key: 'status',
dataIndex: 'status',
align: 'center' as const,
render: (text: string, record: any, index: number) => {
return (
<Space size="middle" style={{ cursor: 'text' }}>
<Tag
color={StatusColor[record.status % 3]}
style={{ marginRight: 0 }}
>
{Status[record.status]}
</Tag>
</Space>
);
},
},
{
title: '创建时间',
key: 'created',
dataIndex: 'created',
align: 'center' as const,
render: (text: string, record: any) => {
return <span>{new Date(record.created).toLocaleString()}</span>;
},
},
{
title: '操作',
key: 'action',
align: 'center' as const,
render: (text: string, record: any, index: number) => {
const isPc = !isPhone;
return (
<Space size="middle">
{record.status !== Status. &&
record.status !== Status. && (
<>
<Tooltip title={isPc ? '重新安装' : ''}>
<a onClick={() => reInstallDependence(record, index)}>
<BugOutlined />
</a>
</Tooltip>
<Tooltip title={isPc ? '删除' : ''}>
<a onClick={() => deleteDependence(record, index)}>
<DeleteOutlined />
</a>
</Tooltip>
</>
)}
<Tooltip title={isPc ? '日志' : ''}>
<a
onClick={() => {
setLogDependence({ ...record, timestamp: Date.now() });
}}
>
<FileTextOutlined />
</a>
</Tooltip>
</Space>
);
},
},
];
const [value, setValue] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [isModalVisible, setIsModalVisible] = useState(false);
const [editedDependence, setEditedDependence] = useState();
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [searchText, setSearchText] = useState('');
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [logDependence, setLogDependence] = useState<any>();
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [type, setType] = useState('nodejs');
const getDependencies = () => {
setLoading(true);
request
.get(
`${config.apiPrefix}dependencies?searchValue=${searchText}&type=${type}`,
)
.then((data: any) => {
setValue(data.data);
})
.finally(() => setLoading(false));
};
const addDependence = () => {
setEditedDependence(null as any);
setIsModalVisible(true);
};
const editDependence = (record: any, index: number) => {
setEditedDependence(record);
setIsModalVisible(true);
};
const deleteDependence = (record: any, index: number) => {
Modal.confirm({
title: '确认删除',
content: (
<>
{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
</>
),
onOk() {
request
.delete(`${config.apiPrefix}dependencies`, { data: [record._id] })
.then((data: any) => {
if (data.code === 200) {
handleDependence(data.data[0]);
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const reInstallDependence = (record: any, index: number) => {
Modal.confirm({
title: '确认重新安装',
content: (
<>
{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
</>
),
onOk() {
request
.put(`${config.apiPrefix}dependencies/reinstall`, {
data: [record._id],
})
.then((data: any) => {
if (data.code === 200) {
handleDependence(data.data[0]);
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const handleCancel = (dependence?: any[]) => {
setIsModalVisible(false);
dependence && handleDependence(dependence);
};
const handleDependence = (dependence: any) => {
const result = [...value];
if (Array.isArray(dependence)) {
result.push(...dependence);
} else {
const index = value.findIndex((x) => x._id === dependence._id);
result.splice(index, 1, {
...dependence,
});
}
setValue(result);
};
const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds);
setTimeout(() => {
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
}
});
};
const rowSelection = {
selectedRowIds,
onChange: onSelectChange,
};
const delDependencies = () => {
Modal.confirm({
title: '确认删除',
content: <></>,
onOk() {
request
.delete(`${config.apiPrefix}dependencies`, { data: selectedRowIds })
.then((data: any) => {
if (data.code === 200) {
setSelectedRowIds([]);
getDependencies();
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const getDependenceDetail = (dependence: any) => {
request
.get(`${config.apiPrefix}dependencies/${dependence._id}`)
.then((data: any) => {
const index = value.findIndex((x) => x._id === dependence._id);
const result = [...value];
result.splice(index, 1, {
...dependence,
...data.data,
});
setValue(result);
})
.finally(() => setLoading(false));
};
const onSearch = (value: string) => {
setSearchText(value.trim());
};
useEffect(() => {
getDependencies();
}, [searchText, type]);
useEffect(() => {
setTimeout(() => {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
});
}, []);
useEffect(() => {
if (logDependence) {
localStorage.setItem('logDependence', logDependence._id);
setIsLogModalVisible(true);
}
}, [logDependence]);
useEffect(() => {
if (!socketMessage) return;
const { type, message, references } = socketMessage;
if (
type === 'installDependence' &&
message.includes('结束时间') &&
references.length > 0
) {
let status;
if (message.includes('安装')) {
status = message.includes('成功') ? Status.已安装 : Status.安装失败;
} else {
status = message.includes('成功') ? Status.已删除 : Status.删除失败;
}
const result = [...value];
for (let i = 0; i < references.length; i++) {
const index = value.findIndex((x) => x._id === references[i]);
result.splice(index, 1, {
...result[index],
status,
});
}
setValue(result);
if (status === Status.) {
setTimeout(() => {
const _result = [...value];
for (let i = 0; i < references.length; i++) {
const index = value.findIndex((x) => x._id === references[i]);
_result.splice(index, 1);
}
setValue(_result);
}, 5000);
}
}
}, [socketMessage]);
const panelContent = () => (
<>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={delDependencies}
>
</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>
</>
);
const onTabChange = (activeKey: string) => {
setType(activeKey);
};
return (
<PageContainer
className="ql-container-wrapper dependence-wrapper"
title="依赖管理"
extra={[
<Search
placeholder="请输入名称"
style={{ width: 'auto' }}
enterButton
loading={loading}
onSearch={onSearch}
/>,
<Button key="2" type="primary" onClick={() => addDependence()}>
</Button>,
]}
header={{
style: headerStyle,
}}
>
<Tabs
defaultActiveKey="nodejs"
size="small"
tabPosition="top"
onChange={onTabChange}
>
<Tabs.TabPane tab="NodeJs" key="nodejs">
{panelContent()}
</Tabs.TabPane>
<Tabs.TabPane tab="Python3" key="python3">
{panelContent()}
</Tabs.TabPane>
<Tabs.TabPane tab="Linux" key="linux">
{panelContent()}
</Tabs.TabPane>
</Tabs>
<DependenceModal
visible={isModalVisible}
handleCancel={handleCancel}
dependence={editedDependence}
defaultType={type}
/>
<DependenceLogModal
visible={isLogModalVisible}
handleCancel={(needRemove?: boolean) => {
setIsLogModalVisible(false);
if (needRemove) {
const index = value.findIndex((x) => x._id === logDependence._id);
const result = [...value];
result.splice(index, 1);
setValue(result);
} else if ([...value].map((x) => x._id).includes(logDependence._id)) {
getDependenceDetail(logDependence);
}
}}
socketMessage={socketMessage}
dependence={logDependence}
/>
</PageContainer>
);
};
export default Dependence;
+150
View File
@@ -0,0 +1,150 @@
import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form, Statistic, Button } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import {
Loading3QuartersOutlined,
CheckCircleOutlined,
} from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-layout';
const DependenceLogModal = ({
dependence,
handleCancel,
visible,
socketMessage,
}: {
dependence?: any;
visible: boolean;
handleCancel: (needRemove?: boolean) => void;
socketMessage: any;
}) => {
const [value, setValue] = useState<string>('');
const [executing, setExecuting] = useState<any>(true);
const [isPhone, setIsPhone] = useState(false);
const [loading, setLoading] = useState<boolean>(true);
const [isRemoveFailed, setIsRemoveFailed] = useState(false);
const [removeLoading, setRemoveLoading] = useState<boolean>(false);
const cancel = (needRemove: boolean = false) => {
localStorage.removeItem('logDependence');
handleCancel(needRemove);
};
const titleElement = () => {
return (
<>
{executing && <Loading3QuartersOutlined spin />}
{!executing && <CheckCircleOutlined />}
<span style={{ marginLeft: 5 }}>
- {dependence && dependence.name}
</span>{' '}
</>
);
};
const getDependenceLog = () => {
setLoading(true);
request
.get(`${config.apiPrefix}dependencies/${dependence._id}`)
.then((data: any) => {
if (localStorage.getItem('logDependence') === dependence._id) {
const log = (data.data.log || []).join('\n') as string;
setValue(log);
setExecuting(!log.includes('结束时间'));
setIsRemoveFailed(log.includes('删除失败'));
}
})
.finally(() => {
setLoading(false);
});
};
const forceRemoveDependence = () => {
setRemoveLoading(true);
request
.delete(`${config.apiPrefix}dependencies/force`, {
data: [dependence._id],
})
.then((data: any) => {
cancel(true);
})
.finally(() => {
setRemoveLoading(false);
});
};
const footerClick = () => {
if (isRemoveFailed) {
forceRemoveDependence();
} else {
cancel();
}
};
useEffect(() => {
if (dependence) {
getDependenceLog();
}
}, [dependence]);
useEffect(() => {
if (!socketMessage) return;
const { type, message, references } = socketMessage;
if (
type === 'installDependence' &&
message.includes('结束时间') &&
references.length > 0
) {
setExecuting(false);
setIsRemoveFailed(message.includes('删除失败'));
}
setValue(`${value} \n ${message}`);
}, [socketMessage]);
useEffect(() => {
setIsPhone(document.body.clientWidth < 768);
}, []);
return (
<Modal
title={titleElement()}
visible={visible}
centered
className="log-modal"
bodyStyle={{
overflowY: 'auto',
maxHeight: 'calc(70vh - var(--vh-offset, 0px))',
minHeight: '300px',
}}
forceRender
onOk={() => cancel()}
onCancel={() => cancel()}
footer={[
<Button type="primary" onClick={footerClick} loading={removeLoading}>
{isRemoveFailed ? '强制删除' : '知道了'}
</Button>,
]}
>
{loading ? (
<PageLoading />
) : (
<pre
style={
isPhone
? {
fontFamily: 'Source Code Pro',
width: 375,
zoom: 0.83,
}
: {}
}
>
{value}
</pre>
)}
</Modal>
);
};
export default DependenceLogModal;
+137
View File
@@ -0,0 +1,137 @@
import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form, Radio, Select } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
const { Option } = Select;
enum DependenceTypes {
'nodejs',
'python3',
'linux',
}
const DependenceModal = ({
dependence,
handleCancel,
visible,
defaultType,
}: {
dependence?: any;
visible: boolean;
handleCancel: (cks?: any[]) => void;
defaultType: string;
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const handleOk = async (values: any) => {
setLoading(true);
const { name, split, type } = values;
const method = dependence ? 'put' : 'post';
let payload;
if (!dependence) {
if (split === '1') {
const symbol = name.includes('&') ? '&' : '\n';
payload = name.split(symbol).map((x: any) => {
return {
name: x,
type,
};
});
} else {
payload = [{ name, type }];
}
} else {
payload = { ...values, _id: dependence._id };
}
try {
const { code, data } = await request[method](
`${config.apiPrefix}dependencies`,
{
data: payload,
},
);
if (code !== 200) {
message.error(data);
}
setLoading(false);
handleCancel(data);
} catch (error) {
setLoading(false);
}
};
useEffect(() => {
form.resetFields();
}, [dependence, visible]);
return (
<Modal
title={dependence ? '编辑依赖' : '新建依赖'}
visible={visible}
forceRender
onOk={() => {
form
.validateFields()
.then((values) => {
handleOk(values);
})
.catch((info) => {
console.log('Validate Failed:', info);
});
}}
onCancel={() => handleCancel()}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
name="dependence_modal"
initialValues={dependence}
>
<Form.Item
name="type"
label="依赖类型"
initialValue={DependenceTypes[defaultType as any]}
>
<Select>
{config.dependenceTypes.map((x, i) => (
<Option value={i}>{x}</Option>
))}
</Select>
</Form.Item>
{!dependence && (
<Form.Item
name="split"
label="自动拆分"
initialValue="0"
tooltip="多个依赖是否换行分割"
>
<Radio.Group>
<Radio value="1"></Radio>
<Radio value="0"></Radio>
</Radio.Group>
</Form.Item>
)}
<Form.Item
name="name"
label="名称"
rules={[
{ required: true, message: '请输入依赖名称', whitespace: true },
]}
>
<Input.TextArea
rows={4}
autoSize={true}
placeholder="请输入依赖名称"
/>
</Form.Item>
<Form.Item name="remark" label="备注">
<Input placeholder="请输入备注" />
</Form.Item>
</Form>
</Modal>
);
};
export default DependenceModal;
+7
View File
@@ -11,3 +11,10 @@
.d2h-code-side-line { .d2h-code-side-line {
padding: 0 0.5em; padding: 0 0.5em;
} }
.diff-switch-file {
min-width: 768px;
.ant-form-item {
margin-bottom: 8px;
}
}
+68 -25
View File
@@ -1,55 +1,78 @@
import React, { PureComponent, useRef, useState, useEffect } from 'react'; import React, { PureComponent, useRef, useState, useEffect } from 'react';
import { Button, message, Modal } from 'antd'; import { Button, message, Select, Form, Row, Col } 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';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import './index.less'; import './index.less';
import { DiffEditor } from '@monaco-editor/react'; import { DiffEditor } from '@monaco-editor/react';
import ReactDiffViewer from 'react-diff-viewer'; import ReactDiffViewer from 'react-diff-viewer';
import { useCtx, useTheme } from '@/utils/hooks';
const Crontab = () => { const { Option } = Select;
const [value, setValue] = useState('');
const [sample, setSample] = useState(''); const Diff = ({ headerStyle, isPhone, theme }: any) => {
const [origin, setOrigin] = useState('config.sample.sh');
const [current, setCurrent] = useState('config.sh');
const [originValue, setOriginValue] = useState('');
const [currentValue, setCurrentValue] = useState('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const { headerStyle, isPhone } = useCtx(); const [files, setFiles] = useState<any[]>([]);
const { theme } = useTheme();
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const getConfig = () => { const getConfig = () => {
request.get(`${config.apiPrefix}configs/config.sh`).then((data) => { request.get(`${config.apiPrefix}configs/${current}`).then((data) => {
setValue(data.data); setCurrentValue(data.data);
}); });
}; };
const getSample = () => { const getSample = () => {
setLoading(true); request.get(`${config.apiPrefix}configs/${origin}`).then((data) => {
request setOriginValue(data.data);
.get(`${config.apiPrefix}configs/config.sample.sh`) });
.then((data) => {
setSample(data.data);
})
.finally(() => setLoading(false));
}; };
const updateConfig = () => { const updateConfig = () => {
const content = editorRef.current const content = editorRef.current
? editorRef.current.getModel().modified.getValue().replace(/\r\n/g, '\n') ? editorRef.current.getModel().modified.getValue().replace(/\r\n/g, '\n')
: value; : currentValue;
request request
.post(`${config.apiPrefix}configs/save`, { .post(`${config.apiPrefix}configs/save`, {
data: { content, name: 'config.sh' }, data: { content, name: current },
}) })
.then((data: any) => { .then((data: any) => {
message.success(data.message); message.success(data.message);
}); });
}; };
const getFiles = () => {
setLoading(true);
request
.get(`${config.apiPrefix}configs/files`)
.then((data: any) => {
setFiles(data.data);
})
.finally(() => setLoading(false));
};
const originFileChange = (value: string) => {
setOrigin(value);
};
const currentFileChange = (value: string) => {
setCurrent(value);
};
useEffect(() => {
getFiles();
}, []);
useEffect(() => {
getSample();
}, [origin]);
useEffect(() => { useEffect(() => {
getConfig(); getConfig();
getSample(); }, [current]);
}, []);
return ( return (
<PageContainer <PageContainer
@@ -67,6 +90,26 @@ const Crontab = () => {
] ]
} }
> >
<Row gutter={24} className="diff-switch-file">
<Col span={12}>
<Form.Item label="源文件">
<Select value={origin} onChange={originFileChange}>
{files.map((x) => (
<Option value={x.value}>{x.title}</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="当前文件">
<Select value={current} onChange={currentFileChange}>
{files.map((x) => (
<Option value={x.value}>{x.title}</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
{isPhone ? ( {isPhone ? (
<ReactDiffViewer <ReactDiffViewer
styles={{ styles={{
@@ -86,8 +129,8 @@ const Crontab = () => {
wordBreak: 'break-word', wordBreak: 'break-word',
}, },
}} }}
oldValue={value} oldValue={originValue}
newValue={sample} newValue={currentValue}
splitView={true} splitView={true}
leftTitle="config.sh" leftTitle="config.sh"
rightTitle="config.sample.sh" rightTitle="config.sample.sh"
@@ -96,8 +139,8 @@ const Crontab = () => {
) : ( ) : (
<DiffEditor <DiffEditor
language={'shell'} language={'shell'}
original={sample} original={originValue}
modified={value} modified={currentValue}
options={{ options={{
fontSize: 12, fontSize: 12,
lineNumbersMinChars: 3, lineNumbersMinChars: 3,
@@ -115,4 +158,4 @@ const Crontab = () => {
); );
}; };
export default Crontab; export default Diff;
+82 -32
View File
@@ -25,7 +25,7 @@ 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 { useCtx } from '@/utils/hooks'; import { getTableScroll } from '@/utils/index';
const { Text } = Typography; const { Text } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -60,11 +60,10 @@ const DragableBodyRow = ({
...restProps ...restProps
}: any) => { }: any) => {
const ref = useRef(); const ref = useRef();
const [{ isOver, dropClassName }, drop] = useDrop( const [{ isOver, dropClassName }, drop] = useDrop({
() => ({
accept: type, accept: type,
collect: (monitor) => { collect: (monitor) => {
const { index: dragIndex } = monitor.getItem() || ({} as any); const { index: dragIndex } = (monitor.getItem() as any) || {};
if (dragIndex === index) { if (dragIndex === index) {
return {}; return {};
} }
@@ -77,19 +76,14 @@ const DragableBodyRow = ({
drop: (item: any) => { drop: (item: any) => {
moveRow(item.index, index); moveRow(item.index, index);
}, },
}), });
[index], const [, drag] = useDrag({
);
const [, drag, preview] = useDrag(
() => ({
type, type,
item: { index }, item: { index },
collect: (monitor) => ({ collect: (monitor) => ({
isDragging: monitor.isDragging(), isDragging: monitor.isDragging(),
}), }),
}), });
[index],
);
drop(drag(ref)); drop(drag(ref));
return ( return (
@@ -98,14 +92,12 @@ const DragableBodyRow = ({
className={`${className}${isOver ? dropClassName : ''}`} className={`${className}${isOver ? dropClassName : ''}`}
style={{ cursor: 'move', ...style }} style={{ cursor: 'move', ...style }}
{...restProps} {...restProps}
> />
{restProps.children}
</tr>
); );
}; };
const Env = () => { const Env = ({ headerStyle, isPhone, theme }: any) => {
const columns = [ const columns: any = [
{ {
title: '序号', title: '序号',
align: 'center' as const, align: 'center' as const,
@@ -119,19 +111,24 @@ const Env = () => {
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
align: 'center' as const, align: 'center' as const,
sorter: (a: any, b: any) => a.name.localeCompare(b.name),
}, },
{ {
title: '值', title: '值',
dataIndex: 'value', dataIndex: 'value',
key: 'value', key: 'value',
align: 'center' as const, align: 'center' as const,
width: '44%', width: '35%',
ellipsis: { ellipsis: {
showTitle: false, showTitle: false,
}, },
render: (text: string, record: any) => { render: (text: string, record: any) => {
return ( return (
<Tooltip placement="topLeft" title={text}> <Tooltip
placement="topLeft"
title={text}
trigger={['hover', 'click']}
>
<span>{text}</span> <span>{text}</span>
</Tooltip> </Tooltip>
); );
@@ -143,12 +140,50 @@ const Env = () => {
key: 'remarks', key: 'remarks',
align: 'center' as const, align: 'center' as const,
}, },
{
title: '更新时间',
dataIndex: 'timestamp',
key: 'timestamp',
align: 'center' as const,
width: 165,
ellipsis: {
showTitle: false,
},
render: (text: string, record: any) => {
const language = navigator.language || navigator.languages[0];
const date = new Date(text)
.toLocaleString(language, {
hour12: false,
})
.replace(' 24:', ' 00:');
return (
<Tooltip
placement="topLeft"
title={date}
trigger={['hover', 'click']}
>
<span>{date}</span>
</Tooltip>
);
},
},
{ {
title: '状态', title: '状态',
key: 'status', key: 'status',
dataIndex: 'status', dataIndex: 'status',
align: 'center' as const, align: 'center' as const,
width: 60, width: 70,
filters: [
{
text: '已启用',
value: 0,
},
{
text: '已禁用',
value: 1,
},
],
onFilter: (value: number, record: any) => record.status === value,
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' }}>
@@ -162,11 +197,12 @@ const Env = () => {
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
width: 100,
align: 'center' as const, 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 (
<Space size="middle" style={{ paddingLeft: 8 }}> <Space size="middle">
<Tooltip title={isPc ? '编辑' : ''}> <Tooltip title={isPc ? '编辑' : ''}>
<a onClick={() => editEnv(record, index)}> <a onClick={() => editEnv(record, index)}>
<EditOutlined /> <EditOutlined />
@@ -202,7 +238,7 @@ 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 { headerStyle, isPhone } = useCtx(); const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const getEnvs = () => { const getEnvs = () => {
setLoading(true); setLoading(true);
@@ -278,7 +314,7 @@ const Env = () => {
<> <>
{' '} {' '}
<Text style={{ wordBreak: 'break-all' }} type="warning"> <Text style={{ wordBreak: 'break-all' }} type="warning">
{record.value} {record.name}: {record.value}
</Text>{' '} </Text>{' '}
</> </>
@@ -317,7 +353,8 @@ const Env = () => {
const result = [...value]; const result = [...value];
const index = value.findIndex((x) => x._id === env._id); const index = value.findIndex((x) => x._id === env._id);
if (index === -1) { if (index === -1) {
result.push(env); env = Array.isArray(env) ? env : [env];
result.push(...env);
} else { } else {
result.splice(index, 1, { result.splice(index, 1, {
...env, ...env,
@@ -338,16 +375,17 @@ const Env = () => {
return; return;
} }
const dragRow = value[dragIndex]; const dragRow = value[dragIndex];
const newData = [...value];
newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, dragRow);
setValue([...newData]);
request request
.put(`${config.apiPrefix}envs/${dragRow._id}/move`, { .put(`${config.apiPrefix}envs/${dragRow._id}/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex }, data: { fromIndex: dragIndex, toIndex: hoverIndex },
}) })
.then((data: any) => { .then((data: any) => {
if (data.code !== 200) { if (data.code === 200) {
const newData = [...value];
newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, { ...dragRow, ...data.data });
setValue([...newData]);
} else {
message.error(data); message.error(data);
} }
}); });
@@ -357,6 +395,12 @@ 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: 87 }));
}
});
}; };
const rowSelection = { const rowSelection = {
@@ -415,13 +459,19 @@ const Env = () => {
}; };
const onSearch = (value: string) => { const onSearch = (value: string) => {
setSearchText(value); setSearchText(value.trim());
}; };
useEffect(() => { useEffect(() => {
getEnvs(); getEnvs();
}, [searchText]); }, [searchText]);
useEffect(() => {
setTimeout(() => {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
});
}, []);
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper env-wrapper" className="ql-container-wrapper env-wrapper"
@@ -435,7 +485,7 @@ const Env = () => {
onSearch={onSearch} onSearch={onSearch}
/>, />,
<Button key="2" type="primary" onClick={() => addEnv()}> <Button key="2" type="primary" onClick={() => addEnv()}>
</Button>, </Button>,
]} ]}
header={{ header={{
@@ -486,7 +536,7 @@ const Env = () => {
dataSource={value} dataSource={value}
rowKey="_id" rowKey="_id"
size="middle" size="middle"
scroll={{ x: 768 }} scroll={{ x: 1000, y: tableScrollHeight }}
components={components} components={components}
loading={loading} loading={loading}
onRow={(record: any, index: number) => { onRow={(record: any, index: number) => {
+44 -6
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form } from 'antd'; import { Modal, message, Input, Form, Radio } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
@@ -17,13 +17,30 @@ const EnvModal = ({
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
setLoading(true); setLoading(true);
const { value, split, name, remarks } = values;
const method = env ? 'put' : 'post'; const method = env ? 'put' : 'post';
const payload = env ? { ...values, _id: env._id } : values; let payload;
if (!env) {
if (split === '1') {
const symbol = value.includes('&') ? '&' : '\n';
payload = value.split(symbol).map((x: any) => {
return {
name: name,
value: x,
remarks: remarks,
};
});
} else {
payload = [{ value, name, remarks }];
}
} else {
payload = { ...values, _id: env._id };
}
const { code, data } = await request[method](`${config.apiPrefix}envs`, { const { code, data } = await request[method](`${config.apiPrefix}envs`, {
data: payload, data: payload,
}); });
if (code === 200) { if (code === 200) {
message.success(env ? '更新变量成功' : '添加变量成功'); message.success(env ? '更新变量成功' : '新建变量成功');
} else { } else {
message.error(data); message.error(data);
} }
@@ -57,14 +74,35 @@ const EnvModal = ({
<Form.Item <Form.Item
name="name" name="name"
label="名称" label="名称"
rules={[{ required: true, message: '请输入环境变量名称' }]} rules={[
{ required: true, message: '请输入环境变量名称', whitespace: true },
{
pattern: /^[a-zA-Z_][0-9a-zA-Z_]*$/,
message: '只能输入字母数字下划线,且不能以数字开头',
},
]}
> >
<Input placeholder="请输入环境变量名称" /> <Input placeholder="请输入环境变量名称" />
</Form.Item> </Form.Item>
{!env && (
<Form.Item
name="split"
label="自动拆分"
initialValue="0"
tooltip="多个依赖是否换行分割"
>
<Radio.Group>
<Radio value="1"></Radio>
<Radio value="0"></Radio>
</Radio.Group>
</Form.Item>
)}
<Form.Item <Form.Item
name="value" name="value"
label="值" label="值"
rules={[{ required: true, message: '请输入环境变量值' }]} rules={[
{ required: true, message: '请输入环境变量值', whitespace: true },
]}
> >
<Input.TextArea <Input.TextArea
rows={4} rows={4}
@@ -73,7 +111,7 @@ const EnvModal = ({
/> />
</Form.Item> </Form.Item>
<Form.Item name="remarks" label="备注"> <Form.Item name="remarks" label="备注">
<Input /> <Input placeholder="请输入备注" />
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>
+77
View File
@@ -0,0 +1,77 @@
@import '~antd/es/style/themes/default.less';
.container {
display: flex;
flex-direction: column;
align-items: center;
height: 100vh;
height: calc(100vh - var(--vh-offset, 0px));
overflow: auto;
background: @layout-body-background;
padding-top: 70px;
}
@media (min-width: @screen-md-min) {
.container {
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
}
}
.top {
text-align: center;
}
.header {
display: flex;
align-items: center;
flex-direction: column;
}
.logo {
width: 48px;
display: block;
margin-bottom: 24px;
margin-top: 20px;
}
.title {
font-size: 20px;
margin-bottom: 16px;
}
.desc {
margin-top: 12px;
margin-bottom: 40px;
color: @text-color-secondary;
font-size: @font-size-base;
}
.main {
padding: 20px;
border-radius: 6px;
background-color: #f6f8fa;
border: 1px solid #ebedef;
display: flex;
max-width: 500px;
width: 90%;
height: 350px;
.ant-steps {
width: 35%;
display: flex;
align-items: center;
position: relative;
top: 6%;
}
.steps-container {
flex: 1;
overflow-y: auto;
}
}
.extra {
margin-top: 20px;
}
+260
View File
@@ -0,0 +1,260 @@
import React, { Fragment, useEffect, useState } from 'react';
import {
Button,
Row,
Input,
Form,
message,
Typography,
Steps,
Select,
} from 'antd';
import config from '@/utils/config';
import { history } from 'umi';
import styles from './index.less';
import { request } from '@/utils/http';
const FormItem = Form.Item;
const { Step } = Steps;
const { Option } = Select;
const { Link } = Typography;
const Initialization = () => {
const [loading, setLoading] = useState(false);
const [current, setCurrent] = React.useState(0);
const [fields, setFields] = useState<any[]>([]);
const next = () => {
setCurrent(current + 1);
};
const prev = () => {
setCurrent(current - 1);
};
const submitAccountSetting = (values: any) => {
setLoading(true);
request
.put(`${config.apiPrefix}user/init`, {
data: {
username: values.username,
password: values.password,
},
})
.then((data) => {
if (data.code === 200) {
next();
} else {
message.error(data.message);
}
})
.finally(() => setLoading(false));
};
const submitNotification = (values: any) => {
setLoading(true);
request
.put(`${config.apiPrefix}user/notification/init`, {
data: {
...values,
},
})
.then((_data: any) => {
if (_data && _data.code === 200) {
next();
} else {
message.error(_data.message);
}
})
.finally(() => setLoading(false));
};
const notificationModeChange = (value: string) => {
const _fields = (config.notificationModeMap as any)[value];
setFields(_fields || []);
};
useEffect(() => {
localStorage.removeItem(config.authKey);
}, []);
const steps = [
{
title: '欢迎使用',
content: (
<div className={styles.top} style={{ marginTop: 100 }}>
<div className={styles.header}>
<span className={styles.title}>使</span>
</div>
<div className={styles.action}>
<Button
type="primary"
onClick={() => {
next();
}}
>
</Button>
</div>
</div>
),
},
{
title: '通知设置',
content: (
<Form onFinish={submitNotification} layout="vertical">
<Form.Item
label="通知方式"
name="type"
rules={[{ required: true, message: '请选择通知方式' }]}
style={{ maxWidth: 350 }}
>
<Select
onChange={notificationModeChange}
placeholder="请选择通知方式"
>
{config.notificationModes
.filter((x) => x.value !== 'closed')
.map((x) => (
<Option value={x.value}>{x.label}</Option>
))}
</Select>
</Form.Item>
{fields.map((x) => (
<Form.Item
label={x.label}
name={x.label}
extra={x.tip}
rules={[{ required: x.required }]}
style={{ maxWidth: 400 }}
>
<Input.TextArea
autoSize={true}
placeholder={`请输入${x.label}`}
/>
</Form.Item>
))}
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
<Button type="link" htmlType="button" onClick={() => next()}>
</Button>
</Form>
),
},
{
title: '账户设置',
content: (
<Form onFinish={submitAccountSetting} layout="vertical">
<Form.Item
label="用户名"
name="username"
rules={[{ required: true }]}
style={{ maxWidth: 350 }}
>
<Input placeholder="用户名" />
</Form.Item>
<Form.Item
label="密码"
name="password"
rules={[
{ required: true },
{
pattern: /^(?!admin$).*$/,
message: '密码不能为admin',
},
]}
hasFeedback
style={{ maxWidth: 350 }}
>
<Input type="password" placeholder="密码" />
</Form.Item>
<Form.Item
name="confirm"
label="确认密码"
dependencies={['password']}
hasFeedback
style={{ maxWidth: 350 }}
rules={[
{
required: true,
},
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('password') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('您输入的两个密码不匹配!'));
},
}),
]}
>
<Input.Password placeholder="确认密码" />
</Form.Item>
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
</Form>
),
},
{
title: '完成安装',
content: (
<div className={styles.top} style={{ marginTop: 80 }}>
<div className={styles.header}>
<span className={styles.title}></span>
<Link href="https://github.com/whyour/qinglong" target="_blank">
Github
</Link>
<Link href="https://t.me/jiao_long" target="_blank">
Telegram频道
</Link>
</div>
<div style={{ marginTop: 16 }}>
<Button
type="primary"
onClick={() => {
history.push('/login');
}}
>
</Button>
</div>
</div>
),
},
];
return (
<div className={styles.container}>
<div className={styles.top}>
<div className={styles.header}>
<img
alt="logo"
className={styles.logo}
src="https://pic.imgdb.cn/item/61acd0dd2ab3f51d912b1986.png"
/>
<span className={styles.title}></span>
</div>
</div>
<div className={styles.main}>
<Steps
current={current}
direction="vertical"
size="small"
className={styles['ant-steps']}
>
{steps.map((item) => (
<Step key={item.title} title={item.title} />
))}
</Steps>
<div className={styles['steps-container']}>
{steps[current].content}
</div>
</div>
</div>
);
};
export default Initialization;
+2 -1
View File
@@ -7,7 +7,6 @@
background-color: #fff; background-color: #fff;
height: calc(100vh - 128px); height: calc(100vh - 128px);
height: calc(100vh - var(--vh-offset, 0px) - 128px); height: calc(100vh - var(--vh-offset, 0px) - 128px);
width: @tree-width;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@@ -15,9 +14,11 @@
flex: 1; flex: 1;
overflow: auto; overflow: auto;
border-right: 1px dashed #f0f0f0; border-right: 1px dashed #f0f0f0;
padding-top: 6px;
} }
} }
.log-container { .log-container {
display: flex; display: flex;
position: relative;
} }
+35 -25
View File
@@ -7,6 +7,7 @@ import { request } from '@/utils/http';
import styles from './index.module.less'; import styles from './index.module.less';
import { Controlled as CodeMirror } from 'react-codemirror2'; import { Controlled as CodeMirror } from 'react-codemirror2';
import { useCtx, useTheme } from '@/utils/hooks'; import { useCtx, useTheme } from '@/utils/hooks';
import SplitPane from 'react-split-pane';
function getFilterData(keyword: string, data: any) { function getFilterData(keyword: string, data: any) {
const expandedKeys: string[] = []; const expandedKeys: string[] = [];
@@ -15,7 +16,6 @@ function getFilterData(keyword: string, data: any) {
data.forEach((item: any) => { data.forEach((item: any) => {
if (item.title.toLocaleLowerCase().includes(keyword)) { if (item.title.toLocaleLowerCase().includes(keyword)) {
tree.push(item); tree.push(item);
expandedKeys.push(...item.children.map((x: any) => x.key));
} else { } else {
const children: any[] = []; const children: any[] = [];
(item.children || []).forEach((subItem: any) => { (item.children || []).forEach((subItem: any) => {
@@ -28,7 +28,7 @@ function getFilterData(keyword: string, data: any) {
...item, ...item,
children, children,
}); });
expandedKeys.push(...children.map((x) => x.key)); expandedKeys.push(item.key);
} }
} }
}); });
@@ -37,17 +37,16 @@ function getFilterData(keyword: string, data: any) {
return { tree: data, expandedKeys }; return { tree: data, expandedKeys };
} }
const Log = () => { const Log = ({ headerStyle, isPhone, theme }: any) => {
const [title, setTitle] = useState('请选择日志文件'); const [title, setTitle] = useState('请选择日志文件');
const [value, setValue] = useState('请选择日志文件'); const [value, setValue] = useState('请选择日志文件');
const [select, setSelect] = useState(); const [select, setSelect] = useState<any>();
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<any[]>([]);
const [filterData, setFilterData] = useState<any[]>([]); const [filterData, setFilterData] = useState<any[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [height, setHeight] = useState<number>(); const [height, setHeight] = useState<number>();
const treeDom = useRef<any>(); const treeDom = useRef<any>();
const { headerStyle, isPhone } = useCtx(); const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const { theme } = useTheme();
const getLogs = () => { const getLogs = () => {
setLoading(true); setLoading(true);
@@ -85,6 +84,10 @@ const Log = () => {
}; };
const onSelect = (value: any, node: any) => { const onSelect = (value: any, node: any) => {
if (node.key === select || !value) {
return;
}
setValue('加载中...');
setSelect(value); setSelect(value);
setTitle(node.parent || node.value); setTitle(node.parent || node.value);
getLog(node); getLog(node);
@@ -97,15 +100,21 @@ const Log = () => {
const onSearch = useCallback( const onSearch = useCallback(
(e) => { (e) => {
const keyword = e.target.value; const keyword = e.target.value;
const { tree } = getFilterData(keyword.toLocaleLowerCase(), data); const { tree, expandedKeys } = getFilterData(
keyword.toLocaleLowerCase(),
data,
);
setFilterData(tree); setFilterData(tree);
setExpandedKeys(expandedKeys);
}, },
[data, setFilterData], [data, setFilterData],
); );
useEffect(() => { useEffect(() => {
getLogs(); getLogs();
if (treeDom && treeDom.current) {
setHeight(treeDom.current.clientHeight); setHeight(treeDom.current.clientHeight);
}
}, []); }, []);
return ( return (
@@ -122,7 +131,6 @@ const Log = () => {
treeData={data} treeData={data}
placeholder="请选择日志文件" placeholder="请选择日志文件"
showSearch showSearch
key="value"
onSelect={onSelect} onSelect={onSelect}
/>, />,
] ]
@@ -133,6 +141,7 @@ const Log = () => {
> >
<div className={`${styles['log-container']} log-container`}> <div className={`${styles['log-container']} log-container`}>
{!isPhone && ( {!isPhone && (
<SplitPane split="vertical" size={200} maxSize={-100}>
<div className={styles['left-tree-container']}> <div className={styles['left-tree-container']}>
<Input.Search <Input.Search
className={styles['left-tree-search']} className={styles['left-tree-search']}
@@ -144,28 +153,12 @@ const Log = () => {
treeData={filterData} treeData={filterData}
showIcon={true} showIcon={true}
height={height} height={height}
selectedKeys={[select]}
showLine={{ showLeafIcon: true }} showLine={{ showLeafIcon: true }}
onSelect={onTreeSelect} onSelect={onTreeSelect}
></Tree> ></Tree>
</div> </div>
</div> </div>
)}
{isPhone ? (
<CodeMirror
value={value}
options={{
lineNumbers: true,
lineWrapping: true,
styleActiveLine: true,
matchBrackets: true,
readOnly: true,
}}
onBeforeChange={(editor, data, value) => {
setValue(value);
}}
onChange={(editor, data, value) => {}}
/>
) : (
<Editor <Editor
language="shell" language="shell"
theme={theme} theme={theme}
@@ -180,6 +173,23 @@ const Log = () => {
wordWrap: 'on', wordWrap: 'on',
}} }}
/> />
</SplitPane>
)}
{isPhone && (
<CodeMirror
value={value}
options={{
lineNumbers: true,
lineWrapping: true,
styleActiveLine: true,
matchBrackets: true,
readOnly: true,
}}
onBeforeChange={(editor, data, value) => {
setValue(value);
}}
onChange={(editor, data, value) => {}}
/>
)} )}
</div> </div>
</PageContainer> </PageContainer>
+23 -27
View File
@@ -3,31 +3,12 @@
.container { .container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center;
height: 100vh; height: 100vh;
height: calc(100vh - var(--vh-offset, 0px)); height: calc(100vh - var(--vh-offset, 0px));
overflow: auto; overflow: auto;
background: @layout-body-background; background: @layout-body-background;
} padding-top: 70px;
.lang {
width: 100%;
height: 40px;
line-height: 44px;
text-align: right;
:global(.ant-dropdown-trigger) {
margin-right: 24px;
}
}
.content {
position: absolute;
top: 45%;
left: 50%;
margin: -160px 0 0 -160px;
width: 320px;
height: 320px;
padding: 36px;
box-shadow: 0 0 100px rgba(0, 0, 0, 0.08);
} }
@media (min-width: @screen-md-min) { @media (min-width: @screen-md-min) {
@@ -44,16 +25,21 @@
} }
.header { .header {
height: 44px;
line-height: 44px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; flex-direction: column;
} }
.logo { .logo {
width: 30px; width: 48px;
margin-right: 8px; height: 48px;
display: block;
margin-bottom: 24px;
}
.title {
font-size: 20px;
margin-bottom: 16px;
} }
.desc { .desc {
@@ -64,7 +50,12 @@
} }
.main { .main {
margin: 35px auto 0; padding: 20px;
border-radius: 6px;
background-color: #f6f8fa;
border: 1px solid #ebedef;
width: 340px;
@media screen and (max-width: @screen-sm) { @media screen and (max-width: @screen-sm) {
width: 95%; width: 95%;
max-width: 320px; max-width: 320px;
@@ -104,3 +95,8 @@
font-size: @font-size-base; font-size: @font-size-base;
} }
} }
.extra {
margin-top: 20px;
width: 340px;
}
+167 -25
View File
@@ -1,36 +1,126 @@
import React, { Fragment, useEffect } from 'react'; import React, { Fragment, useEffect, useState } from 'react';
import { Button, Row, Input, Form, message } from 'antd'; import {
Button,
Row,
Input,
Form,
message,
notification,
Statistic,
} from 'antd';
import config from '@/utils/config'; import config from '@/utils/config';
import { history, Link } from 'umi'; import { history, Link } from 'umi';
import styles from './index.less'; import styles from './index.less';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import { useTheme } from '@/utils/hooks';
import { MobileOutlined } from '@ant-design/icons';
const FormItem = Form.Item; const FormItem = Form.Item;
const { Countdown } = Statistic;
const Login = () => { const Login = () => {
const [loading, setLoading] = useState(false);
const [waitTime, setWaitTime] = useState<any>();
const { theme } = useTheme();
const [twoFactor, setTwoFactor] = useState(false);
const [verifying, setVerifying] = useState(false);
const [loginInfo, setLoginInfo] = useState<any>();
const handleOk = (values: any) => { const handleOk = (values: any) => {
setLoading(true);
setTwoFactor(false);
setWaitTime(null);
request request
.post(`${config.apiPrefix}login`, { .post(`${config.apiPrefix}user/login`, {
data: { data: {
username: values.username, username: values.username,
password: values.password, password: values.password,
}, },
}) })
.then((data) => { .then((data) => {
if (data.code === 200) { if (data.code === 420) {
localStorage.setItem(config.authKey, data.token); setLoginInfo({
history.push('/crontab'); username: values.username,
} else if (data.code === 100) { password: values.password,
message.warn(data.message); });
setTwoFactor(true);
} else { } else {
message.error(data.message); checkResponse(data);
} }
setLoading(false);
}) })
.catch(function (error) { .catch(function (error) {
console.log(error); console.log(error);
setLoading(false);
}); });
}; };
const completeTowFactor = (values: any) => {
setVerifying(true);
request
.put(`${config.apiPrefix}user/two-factor/login`, {
data: { ...loginInfo, code: values.code },
})
.then((data: any) => {
if (data.code === 430) {
message.error(data.message);
} else {
checkResponse(data);
}
setVerifying(false);
})
.catch((error: any) => {
console.log(error);
setVerifying(false);
});
};
const checkResponse = (data: any) => {
if (data.code === 200) {
const {
token,
lastip,
lastaddr,
lastlogon,
retries = 0,
platform,
} = data.data;
localStorage.setItem(config.authKey, token);
notification.success({
message: '登录成功!',
description: (
<>
<div>
{lastlogon ? new Date(lastlogon).toLocaleString() : '-'}
</div>
<div>{lastaddr || '-'}</div>
<div>IP{lastip || '-'}</div>
<div>{platform || '-'}</div>
<div>{retries > 0 ? `失败${retries}` : '成功'}</div>
</>
),
duration: 5,
});
history.push('/crontab');
} else if (data.code === 100) {
message.warn(data.message);
} else if (data.code === 410) {
message.error(data.message);
setWaitTime(data.data);
} else {
message.error(data.message);
}
};
const codeInputChange = (e: React.ChangeEvent) => {
const { value } = e.target as any;
const regx = /^[0-9]{6}$/;
if (regx.test(value)) {
completeTowFactor({ code: value });
}
};
useEffect(() => { useEffect(() => {
const isAuth = localStorage.getItem(config.authKey); const isAuth = localStorage.getItem(config.authKey);
if (isAuth) { if (isAuth) {
@@ -40,44 +130,96 @@ const Login = () => {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.content}>
<div className={styles.top}> <div className={styles.top}>
<div className={styles.header}> <div className={styles.header}>
<img <img
alt="logo" alt="logo"
className={styles.logo} className={styles.logo}
src="https://qinglong.whyour.cn/qinglong.png" src="https://pic.imgdb.cn/item/61acd0dd2ab3f51d912b1986.png"
/> />
<span className={styles.title}>{config.siteName}</span> <span className={styles.title}>
{twoFactor ? '两步验证' : config.siteName}
</span>
</div> </div>
</div> </div>
<div className={styles.main}> <div className={styles.main}>
<Form onFinish={handleOk}> {twoFactor ? (
<Form layout="vertical" onFinish={completeTowFactor}>
<FormItem <FormItem
name="username" name="code"
rules={[{ required: true, message: '请输入用户名' }]} label="验证码"
hasFeedback rules={[
{
pattern: /^[0-9]{6}$/,
message: '验证码为6位数字',
},
]}
> >
<Input placeholder="用户名" autoFocus /> <Input
placeholder="6位数字"
onChange={codeInputChange}
autoFocus
autoComplete="off"
/>
</FormItem> </FormItem>
<FormItem
name="password"
rules={[{ required: true, message: '请输入密码' }]}
hasFeedback
>
<Input type="password" placeholder="密码" />
</FormItem>
<Row>
<Button <Button
type="primary" type="primary"
htmlType="submit" htmlType="submit"
style={{ width: '100%' }} style={{ width: '100%' }}
loading={verifying}
>
</Button>
</Form>
) : (
<Form layout="vertical" onFinish={handleOk}>
<FormItem name="username" label="用户名" hasFeedback>
<Input placeholder="用户名" autoFocus />
</FormItem>
<FormItem name="password" label="密码" hasFeedback>
<Input type="password" placeholder="密码" />
</FormItem>
<Row>
{waitTime ? (
<Button type="primary" style={{ width: '100%' }} disabled>
<Countdown
valueStyle={{
color:
theme === 'vs'
? 'rgba(0,0,0,.25)'
: 'rgba(232, 230, 227, 0.25)',
}}
className="inline-countdown"
onFinish={() => setWaitTime(null)}
format="ss"
value={Date.now() + 1000 * waitTime}
/>
</Button>
) : (
<Button
type="primary"
htmlType="submit"
style={{ width: '100%' }}
loading={loading}
> >
</Button> </Button>
)}
</Row> </Row>
</Form> </Form>
)}
</div> </div>
<div className={styles.extra}>
{twoFactor ? (
<div style={{ paddingLeft: 20, position: 'relative' }}>
<MobileOutlined style={{ position: 'absolute', left: 0, top: 4 }} />
</div>
) : (
''
)}
</div> </div>
</div> </div>
); );
+101 -39
View File
@@ -24,83 +24,131 @@ const prefixMap: any = {
const EditModal = ({ const EditModal = ({
treeData, treeData,
currentFile, currentNode,
content, content,
handleCancel, handleCancel,
visible, visible,
socketMessage,
}: { }: {
treeData?: any; treeData?: any;
currentFile?: string;
content?: string; content?: string;
visible: boolean; visible: boolean;
socketMessage: any;
currentNode: any;
handleCancel: () => void; handleCancel: () => void;
}) => { }) => {
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const [language, setLanguage] = useState<string>('javascript'); const [language, setLanguage] = useState<string>('javascript');
const [fileName, setFileName] = useState<string>(''); const [cNode, setCNode] = useState<any>();
const [selectedKey, setSelectedKey] = useState<string>('');
const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false); const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false);
const [settingModalVisible, setSettingModalVisible] = const [settingModalVisible, setSettingModalVisible] =
useState<boolean>(false); useState<boolean>(false);
const [isNewFile, setIsNewFile] = useState<boolean>(false);
const [log, setLog] = useState<string>(''); const [log, setLog] = useState<string>('');
const { theme } = useTheme(); const { theme } = useTheme();
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const [isRunning, setIsRunning] = useState(false);
const cancel = () => { const cancel = () => {
handleCancel(); handleCancel();
}; };
const onSelect = (value: any, node: any) => { const onSelect = (value: any, node: any) => {
if (node.key === selectedKey || !value) {
return;
}
const newMode = LangMap[value.slice(-3)] || ''; const newMode = LangMap[value.slice(-3)] || '';
setFileName(value); setCNode(node);
setLanguage(newMode); setLanguage(newMode);
setIsNewFile(false);
getDetail(node); getDetail(node);
setSelectedKey(node.key);
}; };
const getDetail = (node: any) => { const getDetail = (node: any) => {
request.get(`${config.apiPrefix}scripts/${node.value}`).then((data) => { request
.get(`${config.apiPrefix}scripts/${node.value}?path=${node.parent || ''}`)
.then((data) => {
setValue(data.data); setValue(data.data);
}); });
}; };
const createFile = () => { const run = () => {
setFileName(`未命名${prefixMap[language]}`); setLog('');
setIsNewFile(true); request
setValue(''); .put(`${config.apiPrefix}scripts/run`, {
data: {
filename: cNode.value,
path: cNode.parent || '',
},
})
.then((data) => {
setIsRunning(true);
});
}; };
const run = () => {}; const stop = () => {
request
.put(`${config.apiPrefix}scripts/stop`, {
data: {
filename: cNode.value,
path: cNode.parent || '',
},
})
.then((data) => {
setIsRunning(false);
});
};
useEffect(() => { useEffect(() => {
if (!currentFile) { if (!socketMessage) {
createFile(); return;
} else {
setFileName(currentFile);
setValue(content as string);
} }
setIsNewFile(!currentFile);
}, []); let { type, message: _message, references } = socketMessage;
if (type !== 'manuallyRunScript') {
return;
}
if (_message === '执行结束') {
_message = '';
setTimeout(() => {
setIsRunning(false);
}, 300);
}
if (log) {
_message = `\n${_message}`;
}
setLog(`${log}${_message}`);
}, [socketMessage]);
useEffect(() => {
if (currentNode) {
setCNode(currentNode);
setValue(content as string);
setSelectedKey(currentNode.key);
}
}, [content, currentNode]);
return ( return (
<Drawer <Drawer
className="edit-modal" className="edit-modal"
closable={false}
title={ title={
<> <>
<span style={{ marginRight: 8 }}>{fileName}</span>
<TreeSelect <TreeSelect
style={{ marginRight: 8, width: 120 }} style={{ marginRight: 8, width: 150 }}
value={currentFile} value={selectedKey}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={treeData} treeData={treeData}
placeholder="请选择脚本文件" placeholder="请选择脚本文件"
showSearch showSearch
key="value"
onSelect={onSelect} onSelect={onSelect}
/> />
<Select <Select
value={language} value={language}
style={{ width: 120, marginRight: 8 }} style={{ width: 110, marginRight: 8 }}
onChange={(e) => { onChange={(e) => {
setLanguage(e); setLanguage(e);
}} }}
@@ -110,8 +158,12 @@ const EditModal = ({
<Option value="shell">shell</Option> <Option value="shell">shell</Option>
<Option value="python">python</Option> <Option value="python">python</Option>
</Select> </Select>
<Button type="primary" style={{ marginRight: 8 }} onClick={run}> <Button
type="primary"
style={{ marginRight: 8 }}
onClick={isRunning ? stop : run}
>
{isRunning ? '停止' : '运行'}
</Button> </Button>
<Button <Button
type="primary" type="primary"
@@ -131,13 +183,6 @@ const EditModal = ({
> >
</Button> </Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={createFile}
>
</Button>
<Button <Button
type="primary" type="primary"
style={{ marginRight: 8 }} style={{ marginRight: 8 }}
@@ -147,6 +192,16 @@ const EditModal = ({
> >
</Button> </Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={() => {
stop();
handleCancel();
}}
>
退
</Button>
</> </>
} }
width={'100%'} width={'100%'}
@@ -154,7 +209,12 @@ const EditModal = ({
onClose={cancel} onClose={cancel}
visible={visible} visible={visible}
> >
<SplitPane split="vertical" minSize={200} defaultSize="50%"> <SplitPane
split="vertical"
minSize={200}
defaultSize="50%"
style={{ height: 'calc(100vh - 55px)' }}
>
<Editor <Editor
language={language} language={language}
value={value} value={value}
@@ -169,17 +229,19 @@ const EditModal = ({
editorRef.current = editor; editorRef.current = editor;
}} }}
/> />
<div> <pre style={{ height: '100%', whiteSpace: 'break-spaces' }}>{log}</pre>
<pre>{log}</pre>
</div>
</SplitPane> </SplitPane>
<SaveModal <SaveModal
visible={saveModalVisible} visible={saveModalVisible}
handleCancel={() => { handleCancel={() => {
setSaveModalVisible(false); setSaveModalVisible(false);
}} }}
isNewFile={isNewFile} file={{
file={{ content: editorRef.current && editorRef.current.getValue().replace(/\r\n/g, '\n'), filename: fileName }} content:
editorRef.current &&
editorRef.current.getValue().replace(/\r\n/g, '\n'),
filename: cNode?.value,
}}
/> />
<SettingModal <SettingModal
visible={settingModalVisible} visible={settingModalVisible}
+96
View File
@@ -0,0 +1,96 @@
import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form, Select } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
const { Option } = Select;
const EditScriptNameModal = ({
handleCancel,
treeData,
visible,
}: {
visible: boolean;
treeData: any[];
handleCancel: (file?: {
filename: string;
path: string;
key: string;
}) => void;
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [dirs, setDirs] = useState<any[]>([]);
const handleOk = async (values: any) => {
setLoading(true);
values.path = values.path || '';
request
.post(`${config.apiPrefix}scripts`, {
data: { filename: values.filename, path: values.path, content: '' },
})
.then(({ code, data }) => {
if (code === 200) {
message.success('保存文件成功');
handleCancel({
filename: values.filename,
path: values.path,
key: `${values.path}-${values.filename}`,
});
} else {
message.error(data);
}
setLoading(false);
})
.finally(() => setLoading(false));
};
useEffect(() => {
form.resetFields();
const originDirs = treeData.filter((x) => x.disabled);
setDirs([{ key: '' }, ...originDirs]);
}, [visible]);
return (
<Modal
title="新建文件"
visible={visible}
forceRender
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="filename"
label="文件名"
rules={[{ required: true, message: '请输入文件名' }]}
>
<Input placeholder="请输入文件名" />
</Form.Item>
<Form.Item
label="父目录"
name="path"
initialValue={dirs && dirs.length > 0 ? dirs[0].key : ''}
>
<Select placeholder="请选择父目录">
{dirs.map((x) => (
<Option value={x.key}>{x.key || '根'}</Option>
))}
</Select>
</Form.Item>
</Form>
</Modal>
);
};
export default EditScriptNameModal;
+2 -1
View File
@@ -7,7 +7,6 @@
background-color: #fff; background-color: #fff;
height: calc(100vh - 128px); height: calc(100vh - 128px);
height: calc(100vh - var(--vh-offset, 0px) - 128px); height: calc(100vh - var(--vh-offset, 0px) - 128px);
width: @tree-width;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@@ -15,9 +14,11 @@
flex: 1; flex: 1;
overflow: auto; overflow: auto;
border-right: 1px dashed #f0f0f0; border-right: 1px dashed #f0f0f0;
padding-top: 6px;
} }
} }
.log-container { .log-container {
display: flex; display: flex;
position: relative;
} }
+391 -32
View File
@@ -1,5 +1,16 @@
import { useState, useEffect, useCallback, Key, useRef } from 'react'; import { useState, useEffect, useCallback, Key, useRef } from 'react';
import { TreeSelect, Tree, Input, Button } from 'antd'; import {
TreeSelect,
Tree,
Input,
Button,
Modal,
message,
Typography,
Tooltip,
Dropdown,
Menu,
} 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';
import Editor from '@monaco-editor/react'; import Editor from '@monaco-editor/react';
@@ -7,19 +18,50 @@ import { request } from '@/utils/http';
import styles from './index.module.less'; import styles from './index.module.less';
import EditModal from './editModal'; import EditModal from './editModal';
import { Controlled as CodeMirror } from 'react-codemirror2'; import { Controlled as CodeMirror } from 'react-codemirror2';
import { useCtx, useTheme } from '@/utils/hooks'; import SplitPane from 'react-split-pane';
import {
DeleteOutlined,
DownloadOutlined,
EditOutlined,
EllipsisOutlined,
FormOutlined,
PlusOutlined,
PlusSquareOutlined,
SearchOutlined,
UserOutlined,
} from '@ant-design/icons';
import EditScriptNameModal from './editNameModal';
import debounce from 'lodash/debounce';
import { history } from 'umi';
const { Text } = Typography;
function getFilterData(keyword: string, data: any) { function getFilterData(keyword: string, data: any) {
const expandedKeys: string[] = [];
if (keyword) { if (keyword) {
const tree: any = []; const tree: any = [];
data.forEach((item: any) => { data.forEach((item: any) => {
if (item.title.toLocaleLowerCase().includes(keyword)) { if (item.title.toLocaleLowerCase().includes(keyword)) {
tree.push(item); tree.push(item);
} else {
const children: any[] = [];
(item.children || []).forEach((subItem: any) => {
if (subItem.title.toLocaleLowerCase().includes(keyword)) {
children.push(subItem);
} }
}); });
return { tree }; if (children.length > 0) {
tree.push({
...item,
children,
});
expandedKeys.push(item.key);
} }
return { tree: data }; }
});
return { tree, expandedKeys };
}
return { tree: data, expandedKeys };
} }
const LangMap: any = { const LangMap: any = {
@@ -29,10 +71,10 @@ const LangMap: any = {
'.ts': 'typescript', '.ts': 'typescript',
}; };
const Script = () => { const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
const [title, setTitle] = useState('请选择脚本文件'); const [title, setTitle] = useState('请选择脚本文件');
const [value, setValue] = useState('请选择脚本文件'); const [value, setValue] = useState('请选择脚本文件');
const [select, setSelect] = useState(); const [select, setSelect] = useState<any>();
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<any[]>([]);
const [filterData, setFilterData] = useState<any[]>([]); const [filterData, setFilterData] = useState<any[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -40,8 +82,12 @@ const Script = () => {
const [height, setHeight] = useState<number>(); const [height, setHeight] = useState<number>();
const treeDom = useRef<any>(); const treeDom = useRef<any>();
const [isLogModalVisible, setIsLogModalVisible] = useState(false); const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const { headerStyle, isPhone } = useCtx(); const [searchValue, setSearchValue] = useState('');
const { theme } = useTheme(); const [isEditing, setIsEditing] = useState(false);
const editorRef = useRef<any>(null);
const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false);
const [currentNode, setCurrentNode] = useState<any>();
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const getScripts = () => { const getScripts = () => {
setLoading(true); setLoading(true);
@@ -50,42 +96,308 @@ const Script = () => {
.then((data) => { .then((data) => {
setData(data.data); setData(data.data);
setFilterData(data.data); setFilterData(data.data);
initGetScript();
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
const getDetail = (node: any) => { const getDetail = (node: any) => {
request.get(`${config.apiPrefix}scripts/${node.value}`).then((data) => { request
.get(`${config.apiPrefix}scripts/${node.value}?path=${node.parent || ''}`)
.then((data) => {
setValue(data.data); setValue(data.data);
}); });
}; };
const initGetScript = () => {
const { p, s } = history.location.query as any;
if (s) {
const obj = {
node: {
title: s,
value: s,
key: p ? `${p}/${s}` : s,
parent: p,
},
};
setExpandedKeys([p]);
onTreeSelect([`${p}/${s}`], obj);
}
};
const onSelect = (value: any, node: any) => { const onSelect = (value: any, node: any) => {
const newMode = LangMap[value.slice(-3)] || ''; if (node.key === select || !value) {
return;
}
setValue('加载中...');
const newMode = value ? LangMap[value.slice(-3)] : '';
setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode); setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode);
setSelect(value); setSelect(node.key);
setTitle(node.parent || node.value); setTitle(node.parent || node.value);
setCurrentNode(node);
getDetail(node); getDetail(node);
}; };
const onTreeSelect = useCallback((keys: Key[], e: any) => { const onExpand = (expKeys: any) => {
setExpandedKeys(expKeys);
};
const onTreeSelect = useCallback(
(keys: Key[], e: any) => {
const content = editorRef.current
? editorRef.current.getValue().replace(/\r\n/g, '\n')
: value;
if (content !== value) {
Modal.confirm({
title: `确认离开`,
content: <></>,
onOk() {
onSelect(keys[0], e.node); onSelect(keys[0], e.node);
}, []); setIsEditing(false);
},
onCancel() {
console.log('Cancel');
},
});
} else {
setIsEditing(false);
onSelect(keys[0], e.node);
}
},
[value],
);
const onSearch = useCallback( const onSearch = useCallback(
(e) => { (e) => {
const keyword = e.target.value; const keyword = e.target.value;
const { tree } = getFilterData(keyword.toLocaleLowerCase(), data); debounceSearch(keyword);
setFilterData(tree);
}, },
[data, setFilterData], [data, setFilterData],
); );
const debounceSearch = useCallback(
debounce((keyword) => {
setSearchValue(keyword);
const { tree, expandedKeys } = getFilterData(
keyword.toLocaleLowerCase(),
data,
);
setExpandedKeys(expandedKeys);
setFilterData(tree);
}, 300),
[data, setFilterData],
);
const editFile = () => {
setTimeout(() => {
setIsEditing(true);
}, 300);
};
const cancelEdit = () => {
setIsEditing(false);
setValue('加载中...');
getDetail(currentNode);
};
const saveFile = () => {
Modal.confirm({
title: `确认保存`,
content: (
<>
<Text style={{ wordBreak: 'break-all' }} type="warning">
{currentNode.value}
</Text>{' '}
</>
),
onOk() {
const content = editorRef.current
? editorRef.current.getValue().replace(/\r\n/g, '\n')
: value;
return new Promise((resolve, reject) => {
request
.put(`${config.apiPrefix}scripts`, {
data: {
filename: currentNode.value,
path: currentNode.parent || '',
content,
},
})
.then((_data: any) => {
if (_data.code === 200) {
message.success(`保存成功`);
setValue(content);
setIsEditing(false);
} else {
message.error(_data);
}
resolve(null);
})
.catch((e) => reject(e));
});
},
onCancel() {
console.log('Cancel');
},
});
};
const deleteFile = () => {
Modal.confirm({
title: `确认删除`,
content: (
<>
<Text style={{ wordBreak: 'break-all' }} type="warning">
{select}
</Text>{' '}
</>
),
onOk() {
request
.delete(`${config.apiPrefix}scripts`, {
data: {
filename: currentNode.value,
path: currentNode.parent || '',
},
})
.then((_data: any) => {
if (_data.code === 200) {
message.success(`删除成功`);
let newData = [...data];
if (currentNode.parent) {
const parentNodeIndex = newData.findIndex(
(x) => x.key === currentNode.parent,
);
const parentNode = newData[parentNodeIndex];
const index = parentNode.children.findIndex(
(y) => y.key === currentNode.key,
);
parentNode.children.splice(index, 1);
newData.splice(parentNodeIndex, 1, { ...parentNode });
} else {
const index = newData.findIndex(
(x) => x.key === currentNode.key,
);
newData.splice(index, 1);
}
setData(newData);
} else {
message.error(_data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const addFile = () => {
setIsAddFileModalVisible(true);
};
const addFileModalClose = (
{ filename, path, key }: { filename: string; path: string; key: string } = {
filename: '',
path: '',
key: '',
},
) => {
if (filename) {
const newData = [...data];
const _file = { title: filename, key, value: filename, parent: path };
if (path) {
const parentNodeIndex = newData.findIndex((x) => x.key === path);
const parentNode = newData[parentNodeIndex];
if (parentNode.children && parentNode.children.length > 0) {
parentNode.children.unshift(_file);
} else {
parentNode.children = [_file];
}
newData.splice(parentNodeIndex, 1, { ...parentNode });
} else {
newData.unshift(_file);
}
setData(newData);
onSelect(_file.value, _file);
setIsEditing(true);
}
setIsAddFileModalVisible(false);
};
const downloadFile = () => {
request
.post(`${config.apiPrefix}scripts/download`, {
data: {
filename: currentNode.value,
},
})
.then((_data: any) => {
const blob = new Blob([_data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = currentNode.value;
document.documentElement.appendChild(a);
a.click();
document.documentElement.removeChild(a);
});
};
useEffect(() => {
const word = searchValue || '';
const { tree } = getFilterData(word.toLocaleLowerCase(), data);
setFilterData(tree);
setSelect('');
setCurrentNode(null);
setTitle('请选择脚本文件');
setValue('请选择脚本文件');
}, [data]);
useEffect(() => { useEffect(() => {
getScripts(); getScripts();
if (treeDom && treeDom.current) {
setHeight(treeDom.current.clientHeight); setHeight(treeDom.current.clientHeight);
}
}, []); }, []);
const menu = isEditing ? (
<Menu>
<Menu.Item key="save" icon={<PlusOutlined />} onClick={saveFile}>
</Menu.Item>
<Menu.Item key="exit" icon={<EditOutlined />} onClick={cancelEdit}>
退
</Menu.Item>
</Menu>
) : (
<Menu>
<Menu.Item key="add" icon={<PlusOutlined />} onClick={addFile}>
</Menu.Item>
<Menu.Item
key="edit"
icon={<EditOutlined />}
onClick={editFile}
disabled={!select}
>
</Menu.Item>
<Menu.Item
key="delete"
icon={<DeleteOutlined />}
onClick={deleteFile}
disabled={!select}
>
</Menu.Item>
</Menu>
);
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper log-wrapper" className="ql-container-wrapper log-wrapper"
@@ -101,11 +413,45 @@ const Script = () => {
treeData={data} treeData={data}
placeholder="请选择脚本文件" placeholder="请选择脚本文件"
showSearch showSearch
key="value"
onSelect={onSelect} onSelect={onSelect}
/>, />,
<Dropdown overlay={menu} trigger={['click']}>
<Button type="primary" icon={<EllipsisOutlined />} />
</Dropdown>,
]
: isEditing
? [
<Button type="primary" onClick={saveFile}>
</Button>,
<Button type="primary" onClick={cancelEdit}>
退
</Button>,
] ]
: [ : [
<Tooltip title="新建">
<Button
type="primary"
onClick={addFile}
icon={<PlusOutlined />}
/>
</Tooltip>,
<Tooltip title="编辑">
<Button
disabled={!select}
type="primary"
onClick={editFile}
icon={<EditOutlined />}
/>
</Tooltip>,
<Tooltip title="删除">
<Button
type="primary"
disabled={!select}
onClick={deleteFile}
icon={<DeleteOutlined />}
/>
</Tooltip>,
<Button <Button
type="primary" type="primary"
onClick={() => { onClick={() => {
@@ -122,6 +468,7 @@ const Script = () => {
> >
<div className={`${styles['log-container']} log-container`}> <div className={`${styles['log-container']} log-container`}>
{!isPhone && ( {!isPhone && (
<SplitPane split="vertical" size={200} maxSize={-100}>
<div className={styles['left-tree-container']}> <div className={styles['left-tree-container']}>
<Input.Search <Input.Search
className={styles['left-tree-search']} className={styles['left-tree-search']}
@@ -133,13 +480,32 @@ const Script = () => {
treeData={filterData} treeData={filterData}
showIcon={true} showIcon={true}
height={height} height={height}
selectedKeys={[select]}
expandedKeys={expandedKeys}
onExpand={onExpand}
showLine={{ showLeafIcon: true }} showLine={{ showLeafIcon: true }}
onSelect={onTreeSelect} onSelect={onTreeSelect}
></Tree> ></Tree>
</div> </div>
</div> </div>
<Editor
language={mode}
value={value}
theme={theme}
options={{
readOnly: !isEditing,
fontSize: 12,
lineNumbersMinChars: 3,
folding: false,
glyphMargin: false,
}}
onMount={(editor) => {
editorRef.current = editor;
}}
/>
</SplitPane>
)} )}
{isPhone ? ( {isPhone && (
<CodeMirror <CodeMirror
value={value} value={value}
options={{ options={{
@@ -148,36 +514,29 @@ const Script = () => {
styleActiveLine: true, styleActiveLine: true,
matchBrackets: true, matchBrackets: true,
mode, mode,
readOnly: true, readOnly: !isEditing,
}} }}
onBeforeChange={(editor, data, value) => { onBeforeChange={(editor, data, value) => {
setValue(value); setValue(value);
}} }}
onChange={(editor, data, value) => {}} onChange={(editor, data, value) => {}}
/> />
) : (
<Editor
language={mode}
value={value}
theme={theme}
options={{
readOnly: true,
fontSize: 12,
lineNumbersMinChars: 3,
folding: false,
glyphMargin: false,
}}
/>
)} )}
<EditModal <EditModal
visible={isLogModalVisible} visible={isLogModalVisible}
treeData={data} treeData={data}
currentFile={select} currentNode={currentNode}
content={value} content={value}
socketMessage={socketMessage}
handleCancel={() => { handleCancel={() => {
setIsLogModalVisible(false); setIsLogModalVisible(false);
}} }}
/> />
<EditScriptNameModal
visible={isAddFileModalVisible}
treeData={data}
handleCancel={addFileModalClose}
/>
</div> </div>
</PageContainer> </PageContainer>
); );
+3 -10
View File
@@ -7,20 +7,17 @@ const SaveModal = ({
file, file,
handleCancel, handleCancel,
visible, visible,
isNewFile,
}: { }: {
file?: any; file?: any;
visible: boolean; visible: boolean;
handleCancel: (cks?: any[]) => void; handleCancel: (cks?: any[]) => void;
isNewFile: boolean;
}) => { }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
console.log(file.filename);
setLoading(true); setLoading(true);
const payload = { ...file, ...values }; const payload = { ...file, ...values, originFilename: file.filename };
request request
.post(`${config.apiPrefix}scripts`, { .post(`${config.apiPrefix}scripts`, {
data: payload, data: payload,
@@ -72,12 +69,8 @@ const SaveModal = ({
> >
<Input placeholder="请输入文件名" /> <Input placeholder="请输入文件名" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item name="path" label="保存目录">
name="path" <Input placeholder="请输入保存目录,默认scripts目录" />
label="保存目录"
rules={[{ required: true, message: '请输入保存目录' }]}
>
<Input placeholder="请输入保存目录" />
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>
-1
View File
@@ -16,7 +16,6 @@ const SettingModal = ({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
console.log(file.filename);
setLoading(true); setLoading(true);
const payload = { ...file, ...values }; const payload = { ...file, ...values };
request request

Some files were not shown because too many files have changed in this diff Show More