Compare commits

..
607 Commits
Author SHA1 Message Date
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
hanhh 87231fac25 localhost替换为0.0.0.0 2021-08-16 16:59:49 +08:00
hanhh 2a5f0eee92 修复删除错误日志文件 2021-08-16 16:36:39 +08:00
hanhh df2a659050 修复check.sh 2021-08-16 15:10:27 +08:00
hanhh adc8f35e83 修复bot.sh 2021-08-16 15:06:19 +08:00
hanhh dfb0d73ef3 ql命令行执行最后打印日志 2021-08-16 14:48:21 +08:00
hanhh 3a8101aad1 更新版本号 2021-08-16 14:38:55 +08:00
hanhh 65d6432dad 增加重启刷新延迟 2021-08-16 14:31:14 +08:00
hanhh 4929fa0440 移除tee,添加config错误日志捕获,合并并发任务日志 2021-08-16 12:16:40 +08:00
hanhh 7a02fe0d8e 修复内容区样式 2021-08-15 15:45:18 +08:00
hanhh d06fccd0d1 修复添加定时任务插入顺序
ql repo拉取失败自动删除目录重新拉取一次
2021-08-12 00:00:39 +08:00
hanhh 8be71b129a 更新版本号 2021-08-11 16:52:21 +08:00
hanhh 1b02668014 修改kill日志 2021-08-11 16:51:38 +08:00
hanhh 3f536b5062 修复停止任务时进程无法删除 2021-08-11 16:50:59 +08:00
hanhh c3517ccc41 停止任务添加调试日志 2021-08-11 16:36:50 +08:00
hanhh bdc3763a6f 修复body样式 2021-08-10 16:20:05 +08:00
hanhh fcfdee4988 修复conc任务 2021-08-10 16:03:21 +08:00
eb15e096e3 修改 cqhttp 的发送的数据类型为 JSON ,避免使用表单可能出现的转码问题 (#543)
Co-authored-by: shaochuan.wu <shaochuan.wu@fit2cloud.com>
2021-08-09 14:04:49 +08:00
hanhh 6e36e06dda 更新版本号 2021-08-07 20:12:38 +08:00
hanhh cbd1ddc5e7 修复github_proxy_url参数影响拉取gitee仓库或者链接 2021-08-07 20:11:06 +08:00
hanhh ebc2e0e4b6 PC端支持对比工具修改配置文件 2021-08-06 19:10:02 +08:00
hanhh 95e104dc9c 更新readme 2021-08-06 18:42:45 +08:00
hanhh 99f02e7c54 更新readme 2021-08-06 18:41:06 +08:00
hanhh bc110ff949 修复定时匹配 2021-08-06 16:05:33 +08:00
hanhh 69a5ba8996 更新readme 2021-08-06 15:09:17 +08:00
hanhh 25236958ff 修复日志弹框样式 2021-08-06 14:46:42 +08:00
hanhh f2dbb64984 移除编辑器rename input 2021-08-05 21:18:50 +08:00
hanhh 069e236674 修改文件判断语法 2021-08-05 18:02:00 +08:00
hanhh e06175c3da 修复raw参数 2021-08-05 16:29:42 +08:00
hanhh dfab2cb7f1 更新版本号 2021-08-05 15:06:00 +08:00
hanhh f1e0823a10 修复单个文件唯一路径 2021-08-05 15:05:40 +08:00
hanhh 5fbad9956f 修复repo命令日志文件唯一值 2021-08-05 14:42:53 +08:00
hanhh d6f5198893 修复首次拉区repo cdn无效 2021-08-05 01:07:47 +08:00
hanhh 603dc07581 修复手机端配置文件编辑 2021-08-03 21:37:28 +08:00
hanhh ace820efd9 修复windows换行符 2021-08-03 21:12:31 +08:00
hanhh 6ed4aa6bf4 修复脚本管理editorRef错误 2021-08-03 19:05:01 +08:00
hanhh 9e4b41ce48 修复文件为空无法保存 2021-08-03 19:03:41 +08:00
hanhh d8587b64a5 修改set envs逻辑 2021-08-03 18:45:24 +08:00
hanhh eba78ca84f 测试内容获取 2021-08-03 18:06:40 +08:00
hanhh 5fd79173d6 测试kill任务进程 2021-08-03 12:06:38 +08:00
hanhh b86ee5674d 修复批量停止任务 2021-08-03 11:23:06 +08:00
hanhh 42fbed157f 修复页面标题显示 2021-08-03 11:14:33 +08:00
hanhh c51306ef27 修复ql update中reset操作 2021-08-03 11:07:05 +08:00
hanhh e260dcb985 更新版本号 2021-08-02 21:46:29 +08:00
hanhh 2ddf69a79a 修复matchMedia兼容性 2021-08-02 21:44:42 +08:00
hanhh 2f4fb657e6 修复matchMedia兼容性 2021-08-02 21:21:28 +08:00
hanhh 2a9751e051 清空默认nginx配置文件 2021-08-02 19:07:45 +08:00
hanhh 43e02905c8 更新版本号 2021-08-02 19:04:01 +08:00
hanhh 635306e2d3 更新任务状态日志 2021-08-02 19:02:21 +08:00
hanhh 63f21b1698 去掉有可能中断的tee 2021-08-02 18:57:48 +08:00
hanhh 59c9b17c40 修复移动端判断 2021-08-02 16:00:45 +08:00
hanhh dc5b4de5fa 重构平台判断代码 2021-08-02 15:19:20 +08:00
hanhh 5d846beabd 更新版本号 2021-07-31 23:13:41 +08:00
hanhh 9728119101 手机端使用codemirror 2021-07-31 23:09:53 +08:00
hanhh 38609feee9 monaco-editor测试版本 2021-07-28 21:39:43 +08:00
hanhh 0b99882193 修复表格样式 2021-07-27 00:09:06 +08:00
hanhh e03a9d2c1d 删除冗余代码 2021-07-26 23:07:22 +08:00
hanhh 105c0f802f 修复windows换行符 2021-07-26 22:57:59 +08:00
hanhh da70b0799a 修复task.sh 2021-07-26 08:37:35 +08:00
hanhh 9d89ecc51e 修复task参数提示 2021-07-26 01:06:27 +08:00
hanhh c77e302874 修复弹框日志样式 2021-07-26 00:56:53 +08:00
hanhh fd7153403d 测试monaco代替codemirror 2021-07-26 00:26:14 +08:00
hanhh eff109bf81 修复bot.sh 2021-07-24 23:17:33 +08:00
hanhh 1614f3c8ad 更新版本号 2021-07-22 22:00:53 +08:00
hanhh 7ced425e01 修改task_before逻辑 2021-07-22 19:13:54 +08:00
hanhh b183e9cd29 增加codemirror搜索功能 2021-07-20 00:10:18 +08:00
hanhh 9320074a02 添加配置文件编辑黑名单 2021-07-17 22:24:29 +08:00
hanhh ecdfdff50d 优化面板样式 2021-07-17 22:20:08 +08:00
hanhh 20ddb5a3cf 修复task_before, task_after 2021-07-17 01:33:10 +08:00
hanhh 9d8c592e69 修复初始化任务状态 2021-07-17 00:01:16 +08:00
hanhh 66266fd545 修复ql repo依赖文件目录嵌套 2021-07-16 01:00:40 +08:00
hanhh cc238dedbc 修改tee参数 2021-07-14 15:40:59 +08:00
hanhh dac694776d 修改spawn参数 2021-07-14 15:13:17 +08:00
hanhh 1f42d0245c 修复task_before影响任务执行 2021-07-13 17:54:10 +08:00
hanhh a6319698e2 更新版本号 2021-07-13 17:44:01 +08:00
hanhh 9ab702e344 支持任务读取全局模块 2021-07-13 17:43:41 +08:00
hanhh 49045c39c7 忽略ts脚本语法检查 2021-07-13 10:53:17 +08:00
hanhh 52490bf320 回退task修改 2021-07-12 23:32:07 +08:00
hanhh faf982b156 更新版本号 2021-07-12 23:18:56 +08:00
hanhh cad1d86caf 修复task_before出错任务中断 2021-07-12 23:18:40 +08:00
LicardoandGitHub dd424237e2 添加bark推送消息分组 (#479) 2021-07-12 20:17:11 +08:00
hanhh 0214c6f81b 撤回task日志修改 2021-07-09 22:39:54 +08:00
hanhh f7e1c2ae5e 暂时移除docker action 条件 2021-07-09 18:52:29 +08:00
hanhh 5e52398a43 测试docker action 2021-07-09 18:48:22 +08:00
hanhh 7bc24a4b62 测试版本号action 2021-07-09 18:42:40 +08:00
hanhh 105073ec23 测试版本号action 2021-07-09 18:42:21 +08:00
hanhh bbb1fc4e1b 测试docker打包 2021-07-09 18:35:33 +08:00
hanhh 381427fd60 更新版本号 2.8.0-068 2021-07-09 18:30:35 +08:00
hanhh 6c63fb83ef 修复执行任务时task_before出错造成任务日志中断 2021-07-09 18:28:06 +08:00
hanhh 602ffdcd5a 修改docker build条件 2021-07-09 15:22:10 +08:00
hanhh 72240a7834 更新版本号2.8.0-067 2021-07-08 23:17:52 +08:00
hanhh 8336434d3f 容器启动时bot和自定义脚本改成后台运行 2021-07-08 23:11:00 +08:00
hanhh dd2d3127a0 修复文件名 2021-07-08 19:13:00 +08:00
hanhh c960717d92 修改workflow 2021-07-08 18:52:48 +08:00
hanhh 23db56df57 移除临时文件 2021-07-08 18:15:25 +08:00
hanhh ed95d66441 测试 2021-07-08 13:05:39 +08:00
hanhh 6cd0feaace 撤销spawn修改 2021-07-08 10:24:40 +08:00
hanhh 22a23fe05e 更新版本号 2021-07-07 18:27:19 +08:00
hanhh 517fdcc8ec 修复spawn可能中断,修复token过期任务状态修改 2021-07-07 18:26:56 +08:00
hanhh 71d7d3818f 修复更新任务状态日志 2021-07-06 13:42:25 +08:00
hanhh 7b7d62760e 初始化更新任务为空闲状态 2021-07-04 19:35:35 +08:00
hanhh f554a5b1a0 更新版本号 2021-07-04 19:26:47 +08:00
hanhh f26d6f1449 去除console.log 2021-07-04 19:25:40 +08:00
hanhh 0728f1f231 修复手动停止任务日志目录 2021-07-04 19:15:09 +08:00
hanhh e687511f32 修复shell任务id获取 2021-07-04 19:12:45 +08:00
hanhh a8a2a54bea 重构任务日志,修复手动停止任务 2021-07-04 19:00:02 +08:00
hanhh c1a3ac6e4d 修复环境变量tooltip 2021-07-03 18:38:02 +08:00
hanhh b1d55422ac 修复环境变量tooltip样式 2021-07-03 17:17:08 +08:00
hanhh f6630d84ab 更新版本号 2021-07-03 16:49:29 +08:00
hanhh 5c2bbc941f 修复获取sample文件 2021-07-03 16:49:02 +08:00
hanhh 53123af832 环境变量值超出显示... 2021-07-03 16:27:13 +08:00
孙善明andGitHub 41c3f21863 增加强制提取 cron 时间 (#373) 2021-07-01 21:51:58 +08:00
hanhh 356f8db728 修复curl proxy 2021-07-01 21:32:54 +08:00
hanhh a14a8cc067 修复entrypoint步骤 2021-07-01 18:52:50 +08:00
hanhh 7ecd38ad1f 修改dockerfile 2021-07-01 15:27:13 +08:00
hanhh 4bc1deda42 修复req参数 2021-07-01 11:49:44 +08:00
hanhh f9c1dfc8cb 修复dockerfile 2021-07-01 00:40:20 +08:00
hanhh 7d417a11c9 修复dockerfile 2021-07-01 00:38:33 +08:00
hanhh a35c0cb64e 修复dockerfile 2021-07-01 00:12:32 +08:00
hanhh 1aaa0f0fa6 修复docker action 2021-06-30 23:52:39 +08:00
hanhh 2fe8c38293 修改镜像打包流程 2021-06-30 23:38:46 +08:00
hanhh 2a1adb2e0e 更新action yml 2021-06-30 23:02:22 +08:00
hanhh 5b76213354 修复check命令 2021-06-30 22:38:19 +08:00
hanhh ebcf8f96c8 修复token验证 2021-06-30 12:26:54 +08:00
hanhh 0c3b3c206e 修复token验证 2021-06-30 12:20:42 +08:00
hanhh ef16622a7e 更新版本号 2021-06-30 12:16:40 +08:00
hanhh e7aff6d1d6 修复token唯一性验证 2021-06-30 12:16:05 +08:00
hanhh b1bee910b2 修复ql check逻辑 2021-06-29 12:06:15 +08:00
hanhh 2049cf9aa9 增加ql check日志显示 2021-06-29 10:51:27 +08:00
Xinglin QiangandGitHub 6c3f848b2c 兼容使用force update的repo (#341) 2021-06-29 10:48:01 +08:00
hanhh 2f8d31658b 更新版本号 2021-06-29 00:05:52 +08:00
hanhh c77eb3d28d 添加ql check运行环境监测 2021-06-29 00:05:37 +08:00
hanhh 1a801c2d31 修复查看脚本高亮 2021-06-27 17:23:13 +08:00
hanhh 0a6c7e3719 修复弹出数据不重置 2021-06-27 00:19:48 +08:00
hanhh 9f5c8fa70a 修改环境变量文本 2021-06-26 23:37:54 +08:00
hanhh ecb035d0b4 修复删除日志log 2021-06-26 23:08:38 +08:00
hanhh b78b382ba1 修复replaceAll方法 2021-06-26 20:32:29 +08:00
hanhh 49088f0d3a 过滤env中的空格 2021-06-26 20:27:25 +08:00
hanhh 0abf657a9c 更新Readme图片 2021-06-26 16:00:19 +08:00
hanhh eb2390135f 更新Readme 2021-06-26 15:57:44 +08:00
hanhh 0753ef9ce9 更新Readme 2021-06-26 15:56:15 +08:00
hanhh f3aca5575c 修复日志结束标识 2021-06-25 16:06:12 +08:00
hanhh 54dd82dc77 修复日志参数获取 2021-06-25 15:58:14 +08:00
hanhh 86e5f208fb 修复ql命令日志 2021-06-25 15:50:04 +08:00
hanhh ce3543779e 修复手动执行任务日志 2021-06-24 23:46:02 +08:00
hanhh e11d0c25f3 更新版本号 2021-06-24 18:56:22 +08:00
hanhh e03c64dceb 任务面板支持查看最新日志 2021-06-24 18:55:26 +08:00
hanhh 4d4d31431d 修复执行shell脚本任务 2021-06-23 18:58:32 +08:00
hanhh c9fecae67a ql bot添加重置remote地址 2021-06-21 22:41:19 +08:00
hanhh 173b87a7b1 修复env启用禁用 2021-06-21 17:22:23 +08:00
hanhh 49ab1ad93a 环境变量支持搜索 2021-06-21 14:16:51 +08:00
hanhh 6c943ac97a 修复环境变量position 2021-06-20 23:28:46 +08:00
hanhh 325b423c4d 修复查看脚本页搜索 2021-06-20 22:59:03 +08:00
hanhh 164126ddc7 修复diff apipath 2021-06-20 22:28:42 +08:00
hanhh e728faf519 修复任务运行依赖 2021-06-20 22:13:04 +08:00
hanhh b00acc7ef5 修复cookie.db迁移 2021-06-20 22:06:29 +08:00
hanhh e44a7aca1a 过滤scripts目录下的子目录 2021-06-20 22:05:43 +08:00
hanhh aef57d94d6 修复package.json依赖 2021-06-20 21:38:26 +08:00
hanhh 9124f1acf3 ql update 添加patch操作 2021-06-20 20:49:14 +08:00
hanhh 01e0a89ba9 更新版本号 2021-06-20 20:37:04 +08:00
hanhh 3052b48f51 删除无用代码 2021-06-20 20:33:18 +08:00
hanhh d630f34505 修复任务并发参数 2021-06-20 20:15:29 +08:00
hanhh 2692351e0a 导入env.sh 2021-06-20 18:06:32 +08:00
hanhh 0fade7a5a9 重构环境变量管理,添加脚本查看 2021-06-20 17:47:12 +08:00
hanhh 7ed1abde36 更新版本号 2021-06-17 21:04:30 +08:00
hanhh 4f5540f756 修复版本号样式和查看cron日志列表异常 2021-06-17 20:56:50 +08:00
hanhh 4289a23895 更新版本号 2021-06-16 23:58:19 +08:00
hanhh f345f362e4 修复entrypoint判断 2021-06-16 23:54:15 +08:00
hanhh bc0044d119 修复 pm2 判断逻辑 2021-06-16 23:25:43 +08:00
hanhh 849883f8d0 删除冗余代码 2021-06-16 23:20:04 +08:00
hanhh b3fc5bd366 修复容器启动逻辑 2021-06-16 23:19:05 +08:00
hanhh 5b9c38385a 修改进程判断命令 2021-06-16 23:02:07 +08:00
hanhh 3232f1efa4 修改容器启动逻辑 2021-06-16 21:27:21 +08:00
hanhh e217ca6d8f 修改docker分支 2021-06-16 21:09:43 +08:00
hanhh 896a09d701 修复版本号样式,fix #349 2021-06-16 21:06:47 +08:00
hanhh 63cc3e59b4 测试entrypoint 2021-06-15 23:33:41 +08:00
hanhh f6d7c3c35e 更新版本号 2021-06-15 23:17:24 +08:00
hanhh 4336f6f175 修复TempCk问题,修复ql raw拉取任务重复 2021-06-15 23:15:58 +08:00
hanhh 8c7f1bc3d3 修复ql repo依赖参数 2021-06-13 22:41:38 +08:00
hanhh 3e481900e2 修复ql repo命令 2021-06-13 20:27:06 +08:00
hanhh 031f1bc1ca 暂时回退ql repo拉取 2021-06-13 20:04:56 +08:00
hanhh 51133e24ef 修复拉取文件后缀异常 2021-06-13 19:33:35 +08:00
hanhh 736da7dfa0 更新版本号 2021-06-13 18:50:29 +08:00
hanhh 95221e2b1d 修复更新状态参数类型 2021-06-13 18:45:05 +08:00
hanhh a53f788886 更新shell token获取 2021-06-13 18:37:25 +08:00
hanhh 0360ba0b1e 修复更新定时状态参数 2021-06-13 18:34:12 +08:00
hanhh 1923ceb5c2 增加自动运行任务时任务状态展示
1.  添加bot远程地址
2. 增加文件后缀配置
3. 增加定时任务页数缓存
2021-06-13 18:18:01 +08:00
hanhh 424a1e6491 移除测试shell 2021-06-11 22:38:33 +08:00
hanhh 61963d5f5d 修复初始化任务执行 2021-06-10 10:28:48 +08:00
hanhh f068cd6a46 更新版本号 2021-06-09 22:54:31 +08:00
hanhh 5df860befe 修复屏蔽指定脚本功能 2021-06-09 22:46:37 +08:00
hanhh 049a87780e ql repo命令增加分支参数 2021-06-09 22:31:13 +08:00
hanhh 3355963168 更新安卓移动端日志展示 2021-06-09 22:19:50 +08:00
hanhh 1824fd35fe 修复task命令错误 2021-06-08 22:44:04 +08:00
hanhh 69068ff899 更新版本号 2021-06-08 22:10:12 +08:00
hanhh 8df18342c0 日志搜索忽略大小写 2021-06-08 21:50:48 +08:00
hanhh b6042c7e46 添加monospace字体 2021-06-08 21:48:31 +08:00
hanhh 5989be7f25 修改移动端二维码尺寸 2021-06-08 21:35:12 +08:00
hanhh c3c3dc3e06 修复删除日志命令 2021-06-08 15:29:00 +08:00
hanhh 1cbc534edc 修复删除日志命令 2021-06-08 15:28:35 +08:00
hanhh 5b3c37d07c 修复config示例 2021-06-08 12:50:19 +08:00
hanhh a5b0924036 支持某个ck临时屏蔽某个脚本 2021-06-07 22:31:03 +08:00
hanhh 26fdb626ea 修改版本号,更新日志 2021-06-07 22:14:04 +08:00
hanhh 9e666f4348 添加重置ql reset命令,支持批量操作ck 2021-06-07 21:54:49 +08:00
FcatmeandGitHub 64aadb7eb8 增加go-cqhttp推送 (#257) 2021-06-07 21:42:28 +08:00
hanhh 7f1c55c40a 修复页面标题 fix 256, 修改 ql update 依赖安装逻辑 2021-06-07 00:57:57 +08:00
hanhh a8707e5422 更新版本号 2021-06-06 15:54:54 +08:00
hanhh a975166656 修复日志面板样式 2021-06-06 15:37:00 +08:00
hanhh a4b2d84443 增加版本号展示 2021-06-06 15:30:14 +08:00
6549eeaf10 增加左侧日志筛选 (#241)
* 增加左侧日志筛选

Co-authored-by: 周腾 <zhout@olymtech.com>
2021-06-06 15:28:03 +08:00
hanhh ce323d0703 修复ql repo命令参数 2021-06-05 17:08:08 +08:00
hanhh e61f2e3ace 修复日志二维码展示 2021-06-05 12:33:29 +08:00
hanhh 42c71f4b8b 修复任务日志二维码展示,pushplus使用https 2021-06-05 11:27:51 +08:00
hanhh 73642ba11d 修复cron正则匹配 2021-06-04 20:49:10 +08:00
hanhh 8ebe049d82 修复cron正则匹配 2021-06-04 19:14:56 +08:00
hanhh 94c2c407d8 修复cron name匹配 2021-06-04 00:02:12 +08:00
hanhh e4f8c58eab github仓库更新添加默认 2021-06-03 23:41:40 +08:00
hanhh 9c1addae69 修复env名称匹配 2021-06-03 23:28:36 +08:00
hanhh b601a3fdbf 增加ghproxy配置,镜像添加wget 2021-06-03 23:10:22 +08:00
arhoandGitHub ee4528a8ef 脚本名称参数传入 config.sh (#213) 2021-05-31 20:59:00 +08:00
hanhh ea2c08be12 修复config sample 2021-05-30 20:49:08 +08:00
hanhh 307207dddd 修复bot启动下载文件失败 2021-05-30 20:44:40 +08:00
hanhh ea56e44669 修复entrypoint 2021-05-30 15:10:44 +08:00
arhoandGitHub 9eb614b17e 添加 openssh 支持使用密钥克隆私库 (#207) 2021-05-29 17:53:29 +08:00
阿浩仔andGitHub 2170628fa3 修复仓库更新失败 重复添加定时问题。
仓库更新失败时,没找到对应仓库目录,会把其他仓库的脚本添加前缀重复添加到定时。
2021-05-29 17:44:13 +08:00
hanhh 06595021cd 移除console.log 2021-05-29 12:05:36 +08:00
hanhh e5ec982891 修改reload pm2逻辑 2021-05-29 11:04:25 +08:00
hanhh 2f89494185 修复log样式 2021-05-28 23:59:14 +08:00
hanhh bcdca5084c 修改log日志 2021-05-28 23:40:25 +08:00
hanhh 68884b37a2 删除bot多余日志 2021-05-28 18:02:30 +08:00
hanhh 54f03a044e 修复entrypoint,修改新增任务通知 2021-05-28 17:14:19 +08:00
hanhh cf52721639 修复entrypoint 2021-05-28 17:02:25 +08:00
hanhh dc0225f100 修改通知默认提示信息 2021-05-27 19:45:11 +08:00
hanhh 6d32c86535 修复执行脚本为全路径时,日志目录截取错误 2021-05-25 21:12:56 +08:00
hanhh 99085e5c95 修复任务实时日志样式 2021-05-25 18:16:53 +08:00
hanhh fefe7782a9 修复日志页面样式 2021-05-25 17:56:30 +08:00
hanhh e12f199ca7 修改手机端日志样式,忽略cron搜索大小写 2021-05-25 11:27:00 +08:00
hanhh 630ced4451 修复文件编辑页样式 2021-05-25 08:07:40 +08:00
hanhh 6256acbfca 跟新sample config版本 2021-05-24 23:39:31 +08:00
hanhh e28d84dc6b antd notification改为message 2021-05-24 23:29:35 +08:00
hanhh 09162d503a 修复文件路径读取 2021-05-24 23:04:26 +08:00
hanhh a4c79dfe3b 修复静态资源拉取 2021-05-24 22:35:41 +08:00
hanhh 92056d66b5 修复cron id获取 2021-05-24 20:52:53 +08:00
hanhh 91e4905a3f 定时任务默认倒序,显示任务总条数,#22 #23 #173 #175 2021-05-24 20:27:25 +08:00
hanhh 4958d700de 删除额外日志 2021-05-21 18:54:15 +08:00
hanhh 12216abcff 修复ql update任务日志 2021-05-21 18:43:52 +08:00
hanhh e6869fefa4 添加静态资源版本展示 2021-05-21 17:14:44 +08:00
hanhh 703707c480 增加重启时静态资源清理 2021-05-21 17:09:59 +08:00
hanhh 989bc51f0d 修复git fetch退出码 2021-05-20 22:39:23 +08:00
hanhh f70c16be52 修复ck启用禁用 2021-05-20 22:17:27 +08:00
hanhh 189184832e session api改为批量操作 2021-05-20 22:04:12 +08:00
hanhh bb469d4847 修复ck启用 2021-05-20 21:37:12 +08:00
hanhh a10d33adc7 修复启用ck缓存未刷新 2021-05-20 20:56:05 +08:00
hanhh 1cf40624da 修复ql update 逻辑 2021-05-20 18:57:45 +08:00
hanhh 65bc4c40e9 修改static仓储到github 2021-05-20 17:49:54 +08:00
hanhh 95f8e9953a 测试build action 2021-05-20 15:17:32 +08:00
hanhh 177150cb75 修复build action 2021-05-20 15:12:54 +08:00
hanhh a6c2a99c14 移除测试文件夹 2021-05-20 14:42:15 +08:00
hanhh 87be858220 更新build action 2021-05-20 00:50:01 +08:00
hanhh 78726ab832 测试build action 2021-05-20 00:32:18 +08:00
hanhh ab35901ca6 修复build action 2021-05-20 00:02:33 +08:00
hanhh 05358ed1a4 修复build action 2021-05-19 23:48:38 +08:00
hanhh 421b9b8fb9 修复build action 2021-05-19 23:43:06 +08:00
hanhh e1c04bb7fb 修复定时任务排序 2021-05-19 23:33:13 +08:00
hanhh c3c585bd6e 修复排序顺序 2021-05-17 21:00:30 +08:00
whyour 12896659a8 修复cron列表状态显示顺序 2021-05-17 10:44:08 +08:00
whyour e7355842d9 使用 pnpm 代替 yarn 2021-05-16 23:09:20 +08:00
whyour aa61fa7a45 修复ql依赖无法安装 2021-05-16 21:03:51 +08:00
whyour 57aa8440a9 修复ql依赖安装 2021-05-16 21:02:07 +08:00
whyour f63464858d 更新队列中图标 2021-05-16 20:30:49 +08:00
whyour 3d090d221a 更新config.sample 2021-05-16 20:03:52 +08:00
whyour c931fbeaa9 更新config.sample 2021-05-16 18:28:21 +08:00
whyour 364a582389 自动刷新cron列表状态 2021-05-16 18:19:40 +08:00
whyour da74481ec4 修复删除日志错误 2021-05-16 17:34:11 +08:00
whyour c183201e6f 批量运行任务添加并行数限制 2021-05-16 17:24:53 +08:00
whyour fefdcb88fd 修复build action 2021-05-15 01:53:01 +08:00
whyour a49d471204 修改 ql update 逻辑 2021-05-15 01:36:51 +08:00
whyour 89d9357a8a 测试build action 2021-05-15 01:31:22 +08:00
whyour 3574f69723 测试git push 2021-05-15 01:28:28 +08:00
whyour 09ab572d79 测试ql update 2021-05-15 01:18:28 +08:00
whyour 427bf44e01 修复 ql update顺序 2021-05-15 00:59:45 +08:00
whyour 52b4633d46 修复日志结束判断 2021-05-14 23:37:18 +08:00
whyour 7d5e215b18 修复日志弹框结束判断 2021-05-14 23:36:37 +08:00
whyour 7fb46f0079 更新log modal style 2021-05-14 22:46:44 +08:00
whyour 9c5e6efff7 修复日志状态样式 2021-05-14 22:03:39 +08:00
whyour c3a53ddda9 修复重启面板提示 2021-05-14 21:59:51 +08:00
whyour d25ae98c68 重启系统添加倒计时提示 2021-05-14 21:07:04 +08:00
whyour 0afd8fcbd6 去掉pm2 reload面板日志 2021-05-14 20:16:02 +08:00
whyour c4a4d2550c 修复diff列表唯一值 2021-05-14 20:14:08 +08:00
whyour dba341bdec 修改比对脚本列表唯一值 2021-05-14 19:24:15 +08:00
whyour e32a56df93 添加日志弹窗日志状态显示 2021-05-14 14:57:30 +08:00
whyour 2ba99d9c47 初始化保存一次session和cron数据 2021-05-14 10:20:41 +08:00
whyour 6d44b0ef0c 修复docker yml 2021-05-13 16:14:12 +08:00
whyour fc612859f1 更新镜像架构支持 2021-05-13 15:14:17 +08:00
whyour d2c1cb5779 修复删除临时脚本列表文件 2021-05-12 23:01:16 +08:00
whyour 4abefccc5b 修复同时执行多个ql repo命令脚本比对错误,独立ql extra命令 2021-05-12 22:50:07 +08:00
whyour 394eccab5e 修复日志弹框逻辑 2021-05-12 21:05:15 +08:00
whyour 5ddc9ac22e 系统重启时会执行一次所有ql repo 和 ql raw的定时任务 2021-05-12 20:36:47 +08:00
whyour ed9db7c8d5 修复ql update状态异常 2021-05-12 20:09:19 +08:00
whyour 7aefcaf75e 暂时移除ql update中的重启后台服务 2021-05-12 19:17:03 +08:00
whyour dfe51c6b73 测试ql update 2021-05-12 19:04:11 +08:00
whyour 9534bbe79e 测试ql update 2021-05-12 18:55:02 +08:00
whyour 560195a54c 测试ql update 2021-05-12 18:02:09 +08:00
whyour 7f66e0624d 更新任务运行日志 2021-05-12 17:57:13 +08:00
whyour f59f1bb0c4 测试 ql update 2021-05-12 17:46:38 +08:00
whyour a8da02b74d 测试ql update 2021-05-12 17:43:22 +08:00
whyour d5964a857a 测试 ql update 2021-05-12 17:32:54 +08:00
whyour 8817342332 修改ql update 2021-05-12 17:24:43 +08:00
whyour 4367d8ecca 修复路由环境shell语法 2021-05-12 16:49:49 +08:00
whyour e64f7f5426 重启面板改为后台运行 2021-05-12 15:22:27 +08:00
whyour b614d7ec7a 修复无法停止任务 2021-05-12 14:57:49 +08:00
whyour ef26dd847e 修复青龙重启 2021-05-12 14:14:43 +08:00
whyour a264af0504 修复 ql update 2021-05-12 12:30:27 +08:00
whyour e3c1685203 修复ql update 2021-05-12 12:25:54 +08:00
whyour 006e0e2626 修复ql update 2021-05-12 12:24:48 +08:00
whyour 391be1987e 修复ql update 2021-05-12 12:23:44 +08:00
whyour cac0f21a99 添加自动启动bot配置 2021-05-12 12:07:25 +08:00
whyour 57c1bbcae4 修复轮询日志和任务状态 2021-05-12 12:02:08 +08:00
whyour a50e3ad401 更新entrypoint 2021-05-12 11:12:38 +08:00
whyour 5acdd977bf 移除ql restart说明 2021-05-12 10:41:09 +08:00
whyour 1437c975ef 更新dockerfile 2021-05-12 10:19:59 +08:00
whyour 48999a9358 修改entrypoint 2021-05-12 01:04:37 +08:00
whyour 1644fb97b4 移除ql restart,读取gitee资源 2021-05-12 00:59:07 +08:00
whyour 3373911b7b 添加自动编译 action 2021-05-11 23:33:57 +08:00
whyour 633fe7707c 修复强制停止任务 2021-05-11 21:06:01 +08:00
whyour 092222b4cb 修复日志页移动端样式 2021-05-11 20:55:14 +08:00
whyour 49a3662788 修复移动端100vh兼容性 2021-05-11 19:37:37 +08:00
whyour d9db5442d3 ck管理重命名 2021-05-11 15:25:59 +08:00
whyour ca2bda67b8 添加自定时脚本示例 2021-05-11 11:01:41 +08:00
whyour 665ed19cff 添加extra文件校验 2021-05-11 10:09:03 +08:00
whyour 982643d6ef 修复bot启动命令 2021-05-11 10:01:13 +08:00
whyour 2bcd0112c4 修改重启命令执行顺序 2021-05-10 23:35:43 +08:00
whyour e9f77829c8 修复禁用ck失效 2021-05-10 23:16:03 +08:00
whyour 25369e1aa8 更新bot启动脚本 2021-05-10 22:21:22 +08:00
whyour af8d52744e 修复bot启动覆盖bot.json 2021-05-10 22:05:16 +08:00
whyourandGitHub 5b3687f7b6 全新青龙2.0 (#65)
* 重构shell (#17)

* 更新正则

* 更新update命令

* 移除测试代码

* 重构删除日志命令

* 更新entrypoint

* 更新dockerfile

* 完善shell调用

* 修复share shell引用

* 修复entrypoint

* 修复share shell

* 修复share.sh

* 修改依赖重装逻辑

* 更新docker entrypoint

* curl 使用静默模式

* 更新ql raw

* 修复添加单个任务

* 修复shell语法

* 添加定时任务进程

* 更新默认定时任务

* 更新定时任务重启schedule

* 更新青龙重启逻辑

* 修复定时任务列表创建

* 修复schedule进程

* 修复entrypoint

* 更新task命令

* pm2 restart替换成reload

* 修复task命令参数引入

* 完善ql repo命令

* 修复update.sh

* 更新ql repo命令

* ql repo添加登录验证,修复package.json示例

* 修复定时任务命令补全

* 修改默认cron端口

* 修复cron日志弹框异常

* 修改cron新建label

* 修复ql repo命令

* 修复cron格式验证

* 修改日志目录格式

* 修改青龙remote url

* 修复添加定时cron匹配

* 添加定时任务超时时间设置

* 暂时移除timeout命令

* 恢复定时任务timeout

* 修复cookie.sh引用

* 修复shell变量自加

* 修复ck更新状态同步

* 增加tg bot测试,修改增删任务通知

* 修复shell函数返回值

* 修改添加任务日志打印

* 修改entrypoint日志

* 修复api日志打印

* 修改api日志打印

* 定时任务支持批量启用禁用删除运行

* 修改cron管理操作按钮响应样式

* 更新bot启动脚本

* 更新bot启动脚本

* 增加timeout默认值,修改session管理逻辑

* 更新config示例和通知日志

* 更新bot.sh

* 更新启动bot命令

* 更新启动bot命令

* 修复task运行参数合并

* 增加停止定时任务功能

* 修复停止定时任务api

* 更新停止定时任务日志

* 更新停止任务日志

* 修复删除cron api

* 更新删除cron通知文本

* 更新命令提示

* 更新bot启动脚本
2021-05-10 20:47:23 +08:00
whyour 489846d2e6 修复删除失效任务失败 fix #50 2021-04-21 20:24:43 +08:00
135 changed files with 24291 additions and 3829 deletions
+1
View File
@@ -1,6 +1,7 @@
MONGODB_URI='mongodb://' MONGODB_URI='mongodb://'
YIYAN_MONGODB_URI='' YIYAN_MONGODB_URI=''
CRON_PORT=5500
PORT=5600 PORT=5600
LOG_LEVEL='debug' LOG_LEVEL='debug'
+113
View File
@@ -0,0 +1,113 @@
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 }}
+3 -26
View File
@@ -1,24 +1,19 @@
name: Publish Docker Image name: PublishDockerImage
on: on:
push: workflow_dispatch:
tags:
- v*
jobs: jobs:
build: build:
name: Build name: Build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout base - name: Checkout base
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
# https://github.com/docker/setup-qemu-action
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1
# https://github.com/docker/setup-buildx-action
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v1
@@ -32,23 +27,5 @@ jobs:
- name: Docker buildx image and push on master branch - name: Docker buildx image and push on master branch
env: env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
if: github.ref == 'refs/heads/master'
run: | run: |
docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v7,linux/arm64 --tag whyour/qinglong:latest docker/ docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x --tag whyour/qinglong:latest docker/
- name: Replace tag without `v`
if: startsWith(github.ref, 'refs/tags/')
uses: actions/github-script@v1
id: version
with:
script: |
return context.payload.ref.replace(/\/?refs\/tags\/v/, '')
result-encoding: string
- name: Docker buildx image and push on release
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
if: startsWith(github.ref, 'refs/tags/')
run: |
docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v7,linux/arm64 --tag whyour/qinglong:${{steps.version.outputs.result}} docker/
docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v7,linux/arm64 --tag whyour/qinglong:latest docker/
+4 -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,8 +21,11 @@
/.env.local /.env.local
.env .env
.history .history
.version.ts
/config /config
/log /log
/db /db
/manual_log /manual_log
/scripts
/bak
+14 -6
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) => {
@@ -28,14 +32,18 @@ export default defineConfig({
externals: { externals: {
react: 'window.React', react: 'window.React',
'react-dom': 'window.ReactDOM', 'react-dom': 'window.ReactDOM',
codemirror: 'window.CodeMirror',
darkreader: 'window.DarkReader', darkreader: 'window.DarkReader',
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/codemirror@5.60.0/lib/codemirror.min.js', 'https://cdn.jsdelivr.net/npm/darkreader@4/darkreader.min.js',
'https://cdn.jsdelivr.net/npm/darkreader@4.9.27/darkreader.min.js', 'https://cdn.jsdelivr.net/npm/codemirror@5/lib/codemirror.min.js',
'https://cdn.jsdelivr.net/npm/codemirror@5.60.0/mode/shell/shell.js', 'https://cdn.jsdelivr.net/npm/codemirror@5/mode/shell/shell.js',
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/python/python.js',
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/javascript/javascript.js',
'https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js',
], ],
}); });
+92
View File
@@ -0,0 +1,92 @@
# 安装说明
## 前置要求
```bash
1.已安装docker-ce
2.选装docker-compose
```
## 安装方式1
```bash
1. 新建一个文件夹,用于存放相关数据
2. 下载本仓库中的`docker-compose.yml`至本地,或是复制文件内容后在本地自行建立并粘贴内容
3. 使用docker-compose启动
4. 浏览器输入ip:5700即可进入面板
# 新建数据文件夹
mkdir qinglong
cd qinglong
# 下载docker-compose.yml文件
wget https://raw.githubusercontent.com/whyour/qinglong/develop/docker-compose.yml
# 启动
docker-compose up -d
```
## 安装方式2
```bash
# 复制下列命令在ssh执行(先安装docker)
# 注:$PWD请修改为实际你想安装的路径
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 \
-v $PWD/ql/jbot:/ql/jbot \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
whyour/qinglong:latest
```
## 登录
```bash
打开浏览器访问宿主机ip的5700端口即可
例如http://192.168.100.123:5700即ip:5700
首次登录
账号:admin 密码:admin
会生成`auth.json`
在ssh输入
1.docker exec -it qinglong bash
2.cat /ql/config/auth.json
cat查看之后返回的结果类似如下字段
{"username":"admin","password":"Xb-ZYP526wmg4_h6q1WqIO"}
# admin即为登录名;Xb-ZYP526wmg4_h6q1WqIO为登录密码
```
输入此处记录的`username``password`,即可成功登录qinglong面板,登录后即可正常使用
## 拉取脚本
```bash
示例
ql repo https://github.com/xxx.git #拉取仓库
ql raw https://raw.githubusercontent.com/xxx #拉取单个脚本
```
## 备份
所有数据都将保存在`docker-compose.yml`所在的同级目录的`data`文件夹中,如需要备份,请直接备份`docker-compose.yml``data`文件夹即可
```bash
root@debian:/opt/qinglong# ls -lah
总用量 8.0K
drwxr-xr-x 3 root root 4.0K 8月 30 01:29 .
drwxr-xr-x 4 root root 4.0K 8月 30 00:51 ..
drwxr-xr-x 8 root root 4.0K 8月 30 01:30 data
-rw-r--r-- 1 root root 386 8月 30 01:29 docker-compose.yml
```
## 更新
在面板执行"更新面板"任务即可
或者
```bash
cd qinglong
docker-compose down
docker pull whyour/qinglong:latest
docker-compose up -d
```
+20 -30
View File
@@ -1,43 +1,49 @@
<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="/public/images/qinglong.png">
</a> </a>
</p> </p>
<h1 align="center">青龙(WIP)</h1> <h1 align="center">青龙</h1>
<div align="center"> <div align="center">
python和javaScript的定时任务管理面板 A timed task management panel that supports typescript, javaScript, python3, and shell.(支持python3、javaScript、shell、typescript 的定时任务管理面板
[![donate][donate-image]][donate-url] [![build status][build-status-image]][build-status-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker version][docker-version-image]][docker-version-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url] [![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]
[donate-image]: https://img.shields.io/badge/donate-wechat-green?style=for-the-badge [donate-image]: https://img.shields.io/badge/donate-wechat-green?style=flat
[donate-url]: https://qinglong.whyour.cn/nice.png [donate-url]: https://qinglong.whyour.cn/nice.png
[build-status-image]: https://img.shields.io/docker/cloud/build/whyour/qinglong?style=for-the-badge [docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
[build-status-url]: https://img.shields.io/docker/cloud/build/whyour/qinglong
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=for-the-badge
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong [docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=for-the-badge [docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
[docker-version-url]: https://hub.docker.com/r/whyour/qinglong/tags?page=1&ordering=last_updated [docker-version-url]: https://hub.docker.com/r/whyour/qinglong/tags?page=1&ordering=last_updated
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=for-the-badge [docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong [docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=for-the-badge [docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
[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">
<img width="49%" src="/public/images/login.png">
<img width="49%" src="/public/images/home.png">
</p>
青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](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)中,青龙是东方七宿(角、亢、氐、房、心、尾、箕)的总称。 在早期星宿信仰中,祂是最尊贵的天神。 但被道教信仰吸纳入其神系后,神格大跌,道教将其称为“孟章”,在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼,与白虎监兵神君一起,是道教的护卫天神。
## 如何安装
请查看[INSTALL.md](INSTALL.md)
## 多谢 ## 多谢
* [https://github.com/nevinee/jd_shell](https://github.com/nevinee/jd_shell) * [nevinee](https://gitee.com/evine)
* [https://github.com/alseambusher/crontab-ui](https://github.com/alseambusher/crontab-ui) * [crontab-ui](https://github.com/alseambusher/crontab-ui)
* [Ant Design](https://ant.design) * [Ant Design](https://ant.design)
@@ -46,19 +52,3 @@ python和javaScript的定时任务管理面板
* [Umijs3.0](https://umijs.org) * [Umijs3.0](https://umijs.org)
* [darkreader](https://github.com/darkreader/darkreader) * [darkreader](https://github.com/darkreader/darkreader)
## 免责声明
1. 此仓储脚本仅用于学习研究,不保证其合法性、准确性、有效性,请根据情况自行判断,本人对此不承担任何保证责任。
2. 由于此仓储脚本仅用于学习研究,您必须在下载后 24 小时内将所有内容从您的计算机或手机或任何存储设备中完全删除,若违反规定引起任何事件本人对此均不负责。
3. 请勿将此仓储脚本用于任何商业或非法目的,若违反规定请自行对此负责。
4. 此仓储脚本涉及应用与本人无关,本人对因此引起的任何隐私泄漏或其他后果不承担任何责任。
5. 本人对任何脚本引发的问题概不负责,包括但不限于由脚本错误引起的任何损失和损害。
6. 如果任何单位或个人认为此仓储脚本可能涉嫌侵犯其权利,应及时通知并提供身份证明,所有权证明,我们将在收到认证文件确认后删除此仓储脚本。
7. 所有直接或间接使用、查看此仓储脚本的人均应该仔细阅读此声明。本人保留随时更改或补充此声明的权利。一旦您使用或复制了此仓储脚本,即视为您已接受此免责声明。
-128
View File
@@ -1,128 +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 { createPassword } from '../config/util';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.post(
'/login',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let username = req.body.username;
let password = req.body.password;
fs.readFile(config.authConfigFile, 'utf8', function (err, data) {
if (err) console.log(err);
const authInfo = JSON.parse(data);
if (username && password) {
if (
authInfo.username === 'admin' &&
authInfo.password === 'adminadmin'
) {
const newPassword = createPassword(16, 22);
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({
username: authInfo.username,
password: newPassword,
}),
);
return res.send({
code: 100,
msg: '已初始化密码,请前往auth.json查看并重新登录',
});
}
if (
username == authInfo.username &&
password == authInfo.password
) {
let token = jwt.sign(
{ username, password },
config.secret as any,
{ expiresIn: 60 * 60 * 24 * 7, algorithm: 'HS384' },
);
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({
username: authInfo.username,
password: authInfo.password,
token,
}),
);
res.send({ code: 200, token });
} else {
res.send({ code: 400, msg: config.authError });
}
} else {
res.send({ err: 400, msg: '请输入用户名密码!' });
}
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/logout',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
fs.readFile(config.authConfigFile, 'utf8', function (err, data) {
if (err) console.log(err);
const authInfo = JSON.parse(data);
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({
username: authInfo.username,
password: authInfo.password,
}),
);
res.send({ code: 200 });
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/user',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
fs.writeFile(config.authConfigFile, JSON.stringify(req.body), (err) => {
if (err) console.log(err);
res.send({ code: 200, msg: '更新成功' });
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/user',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
if (err) console.log(err);
const authInfo = JSON.parse(data);
res.send({ code: 200, data: { username: authInfo.username } });
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+42 -29
View File
@@ -5,36 +5,49 @@ 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 { execSync } from 'child_process';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/', route);
route.get( route.get(
'/config/:key', '/configs/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 {
let content = '未找到文件'; const fileList = fs.readdirSync(config.configPath, 'utf-8');
switch (req.params.key) { res.send({
case 'config': code: 200,
content = getFileContentByName(config.confFile); data: fileList
break; .filter((x) => !config.blackFileList.includes(x))
case 'sample': .map((x) => {
content = getFileContentByName(config.sampleFile); return { title: x, value: x };
break; }),
case 'crontab': });
content = getFileContentByName(config.crontabFile); } catch (e) {
break; logger.error('🔥 error: %o', e);
case 'shareCode': return next(e);
let shareCodeFile = getLastModifyFilePath(config.shareCodeDir); }
content = getFileContentByName(shareCodeFile); },
break; );
case 'extra':
content = getFileContentByName(config.extraFile); route.get(
break; '/configs/:file',
default: async (req: Request, res: Response, next: NextFunction) => {
break; const logger: Logger = Container.get('logger');
try {
let content = '';
if (config.blackFileList.includes(req.params.file)) {
res.send({ code: 403, message: '文件无法访问' });
}
if (req.params.file.includes('sample')) {
content = getFileContentByName(
`${config.samplePath}${req.params.file}`,
);
} else {
content = getFileContentByName(
`${config.configPath}${req.params.file}`,
);
} }
res.send({ code: 200, data: content }); res.send({ code: 200, data: content });
} catch (e) { } catch (e) {
@@ -45,23 +58,23 @@ export default (app: Router) => {
); );
route.post( route.post(
'/save', '/configs/save',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
name: Joi.string().required(), name: Joi.string().required(),
content: Joi.string().required(), content: Joi.string().allow('').optional(),
}), }),
}), }),
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 { name, content } = req.body; const { name, content } = req.body;
const path = (config.fileMap as any)[name]; if (config.blackFileList.includes(name)) {
fs.writeFileSync(path, content); res.send({ code: 403, message: '文件无法访问' });
if (name === 'crontab.list') {
execSync(`crontab ${path}`);
} }
res.send({ code: 200, msg: '保存成功' }); const path = `${config.configPath}${name}`;
fs.writeFileSync(path, content);
res.send({ code: 200, message: '保存成功' });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
return next(e); return next(e);
+118 -33
View File
@@ -1,10 +1,9 @@
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 * as fs from 'fs';
import config from '../config';
import CronService from '../services/cron'; import CronService from '../services/cron';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import cron_parser from 'cron-parser';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
@@ -32,14 +31,36 @@ export default (app: Router) => {
body: Joi.object({ body: Joi.object({
command: Joi.string().required(), command: Joi.string().required(),
schedule: Joi.string().required(), schedule: Joi.string().required(),
name: Joi.string(), name: Joi.string().optional(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
if (cron_parser.parseExpression(req.body.schedule).hasNext()) {
const cronService = Container.get(CronService);
const data = await cronService.create(req.body);
return res.send({ code: 200, data });
} else {
return res.send({ code: 400, message: 'param schedule error' });
}
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/crons/run',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
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.create(req.body); const data = await cronService.run(req.body);
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);
@@ -48,18 +69,16 @@ export default (app: Router) => {
}, },
); );
route.get( route.put(
'/crons/:id/run', '/crons/stop',
celebrate({ celebrate({
params: Joi.object({ body: Joi.array().items(Joi.string().required()),
id: Joi.string().required(),
}),
}), }),
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.run(req.params.id); const data = await cronService.stop(req.body);
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);
@@ -68,18 +87,16 @@ export default (app: Router) => {
}, },
); );
route.get( route.put(
'/crons/:id/disable', '/crons/disable',
celebrate({ celebrate({
params: Joi.object({ body: Joi.array().items(Joi.string().required()),
id: Joi.string().required(),
}),
}), }),
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.disabled(req.params.id); const data = await cronService.disabled(req.body);
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);
@@ -88,18 +105,16 @@ export default (app: Router) => {
}, },
); );
route.get( route.put(
'/crons/:id/enable', '/crons/enable',
celebrate({ celebrate({
params: Joi.object({ body: Joi.array().items(Joi.string().required()),
id: Joi.string().required(),
}),
}), }),
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.enabled(req.params.id); const data = await cronService.enabled(req.body);
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);
@@ -132,18 +147,25 @@ export default (app: Router) => {
'/crons', '/crons',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
command: Joi.string().required(), command: Joi.string().optional(),
schedule: Joi.string().required(), schedule: Joi.string().optional(),
name: Joi.string(), name: Joi.string().optional(),
_id: Joi.string().required(), _id: Joi.string().required(),
}), }),
}), }),
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); if (
const data = await cronService.update(req.body); !req.body.schedule ||
return res.send({ code: 200, data }); cron_parser.parseExpression(req.body.schedule).hasNext()
) {
const cronService = Container.get(CronService);
const data = await cronService.update(req.body);
return res.send({ code: 200, data });
} else {
return res.send({ code: 400, message: 'param schedule error' });
}
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
return next(e); return next(e);
@@ -152,17 +174,51 @@ export default (app: Router) => {
); );
route.delete( route.delete(
'/crons/:id', '/crons',
celebrate({ celebrate({
params: Joi.object({ body: Joi.array().items(Joi.string().required()),
id: Joi.string().required(),
}),
}), }),
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.remove(req.params.id); const data = await cronService.remove(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/crons/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(
'/crons/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 }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
@@ -205,4 +261,33 @@ export default (app: Router) => {
} }
}, },
); );
route.put(
'/crons/status',
celebrate({
body: Joi.object({
ids: Joi.array().items(Joi.string().required()),
status: Joi.string().required(),
pid: 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) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.status({
...req.body,
status: parseInt(req.body.status),
pid: parseInt(req.body.pid) || '',
});
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
}; };
+145
View File
@@ -0,0 +1,145 @@
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('/', route);
route.get(
'/dependencies',
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(
'/dependencies',
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(
'/dependencies',
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(
'/dependencies',
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(
'/dependencies/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(
'/dependencies/: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(
'/dependencies/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);
}
},
);
};
+86 -83
View File
@@ -1,6 +1,6 @@
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
import CookieService from '../services/cookie'; import EnvService from '../services/env';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
const route = Router(); const route = Router();
@@ -8,12 +8,12 @@ const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/', route);
route.get( route.get(
'/cookies', '/envs',
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 cookieService = Container.get(CookieService); const envService = Container.get(EnvService);
const data = await cookieService.cookies(); const data = await envService.envs(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);
@@ -23,15 +23,21 @@ export default (app: Router) => {
); );
route.post( route.post(
'/cookies', '/envs',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()).min(1), body: Joi.array().items(
Joi.object({
value: Joi.string().required(),
name: Joi.string().required(),
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');
try { try {
const cookieService = Container.get(CookieService); const envService = Container.get(EnvService);
const data = await cookieService.create(req.body); const data = await envService.create(req.body);
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);
@@ -41,18 +47,20 @@ export default (app: Router) => {
); );
route.put( route.put(
'/cookies', '/envs',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
value: Joi.string().required(), value: Joi.string().required(),
name: Joi.string().required(),
remarks: Joi.string().optional().allow(''),
_id: Joi.string().required(), _id: Joi.string().required(),
}), }),
}), }),
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 cookieService = Container.get(CookieService); const envService = Container.get(EnvService);
const data = await cookieService.update(req.body); const data = await envService.update(req.body);
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);
@@ -62,17 +70,15 @@ export default (app: Router) => {
); );
route.delete( route.delete(
'/cookies/:id', '/envs',
celebrate({ celebrate({
params: Joi.object({ body: Joi.array().items(Joi.string().required()),
id: Joi.string().required(),
}),
}), }),
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 cookieService = Container.get(CookieService); const envService = Container.get(EnvService);
const data = await cookieService.remove(req.params.id); const data = await envService.remove(req.body);
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);
@@ -82,7 +88,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/cookies/:id/move', '/envs/:id/move',
celebrate({ celebrate({
params: Joi.object({ params: Joi.object({
id: Joi.string().required(), id: Joi.string().required(),
@@ -95,8 +101,65 @@ export default (app: Router) => {
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 cookieService = Container.get(CookieService); const envService = Container.get(EnvService);
const data = await cookieService.move(req.params.id, req.body); const data = await envService.move(req.params.id, req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/envs/disable',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const envService = Container.get(EnvService);
const data = await envService.disabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/envs/enable',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const envService = Container.get(EnvService);
const data = await envService.enabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/envs/name',
celebrate({
body: Joi.object({
ids: Joi.array().items(Joi.string().required()),
name: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const envService = Container.get(EnvService);
const data = await envService.updateNames(req.body);
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);
@@ -106,7 +169,7 @@ export default (app: Router) => {
); );
route.get( route.get(
'/cookies/:id/refresh', '/envs/:id',
celebrate({ celebrate({
params: Joi.object({ params: Joi.object({
id: Joi.string().required(), id: Joi.string().required(),
@@ -115,68 +178,8 @@ export default (app: Router) => {
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 cookieService = Container.get(CookieService); const envService = Container.get(EnvService);
const data = await cookieService.refreshCookie(req.params.id); const data = await envService.get(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/cookies/:id/disable',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.disabled(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/cookies/:id/enable',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.enabled(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/cookies/:id',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.get(req.params.id);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
+10 -4
View File
@@ -1,17 +1,23 @@
import { Router } from 'express'; import { Router } from 'express';
import auth from './auth'; import user from './user';
import cookie from './cookie'; 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 open from './open';
import dependence from './dependence';
export default () => { export default () => {
const app = Router(); const app = Router();
auth(app); user(app);
cookie(app); env(app);
config(app); config(app);
log(app); log(app);
cron(app); cron(app);
script(app);
open(app);
dependence(app);
return app; return app;
}; };
+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);
}
},
);
};
+240
View File
@@ -0,0 +1,240 @@
import {
fileExist,
getFileContentByName,
getLastModifyFilePath,
} from '../config/util';
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import config from '../config';
import * as fs from 'fs';
import { celebrate, Joi } from 'celebrate';
import path from 'path';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.get(
'/scripts/files',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
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({
code: 200,
data: result,
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/scripts/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const path = req.query.path ? `${req.query.path}/` : '';
const content = getFileContentByName(
`${config.scriptPath}${path}${req.params.file}`,
);
res.send({ code: 200, data: content });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/scripts',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
content: Joi.string().allow(''),
originFilename: Joi.string().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, path, content, originFilename } = req.body as {
filename: string;
path: string;
content: string;
originFilename: string;
};
if (!path) {
path = config.scriptPath;
}
if (!path.endsWith('/')) {
path += '/';
}
if (!path.startsWith('/')) {
path = `${config.scriptPath}${path}`;
}
if (config.writePathList.every((x) => !path.startsWith(x))) {
return res.send({
code: 430,
data: '文件路径禁止访问',
});
}
if (!originFilename) {
originFilename = filename;
}
const originFilePath = `${path}${originFilename.replace(/\//g, '')}`;
const filePath = `${path}${filename.replace(/\//g, '')}`;
if (fs.existsSync(originFilePath)) {
if (!fs.existsSync(config.bakPath)) {
fs.mkdirSync(config.bakPath);
}
fs.copyFileSync(
originFilePath,
`${config.bakPath}${originFilename.replace(/\//g, '')}`,
);
if (filename !== originFilename) {
fs.unlinkSync(originFilePath);
}
}
fs.writeFileSync(filePath, content);
return res.send({ code: 200 });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/scripts',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().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 = `${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(
'/scripts',
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 = `${config.scriptPath}${path}/${filename}`;
fs.unlinkSync(filePath);
res.send({ code: 200 });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/scripts/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);
}
},
);
};
+342
View File
@@ -0,0 +1,342 @@
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('/', 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(
'/user',
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(
'/user',
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(
'/user/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(
'/user/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(
'/user/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(
'/user/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(
'/user/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(
'/user/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(
'/user/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.get(
'/system',
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);
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,
},
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
// 初始化api
route.put(
'/init/user',
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(
'/init/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.get(
'/system/log/remove',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const data = await userService.getLogRemoveFrequency();
res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/system/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 userService = Container.get(UserService);
const result = await userService.updateLogRemoveFrequency(
req.body.frequency,
);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/system/update-check',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const result = await userService.checkUpdate();
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/system/update',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const result = await userService.updateSystem();
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+8 -2
View File
@@ -9,9 +9,13 @@ 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/sentry').default({ expressApp: app });
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 +27,8 @@ async function startServer() {
Logger.error(err); Logger.error(err);
process.exit(1); process.exit(1);
}); });
await require('./loaders/server').default({ server });
} }
startServer(); startServer();
+52 -16
View File
@@ -1,59 +1,95 @@
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import path from 'path'; import path from 'path';
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://ghproxy.com/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 cookieFile = path.join(rootPath, 'config/cookie.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.sh.sample'); const sampleFile = path.join(rootPath, 'sample/config.sample.sh');
const crontabFile = path.join(rootPath, 'config/crontab.list'); const crontabFile = path.join(rootPath, 'config/crontab.list');
const confBakDir = path.join(rootPath, 'config/bak/'); const confBakDir = path.join(rootPath, 'config/bak/');
const authConfigFile = path.join(rootPath, 'config/auth.json'); const authConfigFile = path.join(rootPath, 'config/auth.json');
const shareCodeDir = path.join(rootPath, 'log/export_sharecodes/');
const extraFile = path.join(rootPath, 'config/extra.sh'); const extraFile = path.join(rootPath, 'config/extra.sh');
const configPath = path.join(rootPath, 'config/');
const scriptPath = path.join(rootPath, 'scripts/');
const bakPath = path.join(rootPath, 'bak/');
const samplePath = path.join(rootPath, 'sample/');
const logPath = path.join(rootPath, 'log/'); const logPath = path.join(rootPath, 'log/');
const authError = '错误的用户名密码,请重试'; const authError = '错误的用户名密码,请重试';
const loginFaild = '请先登录!'; const loginFaild = '请先登录!';
const configString = 'config sample crontab shareCode diy'; const configString = 'config sample crontab shareCode diy';
const dbPath = path.join(rootPath, 'db/'); const dbPath = path.join(rootPath, 'db/');
const manualLogPath = path.join(rootPath, 'manual_log/');
const cronDbFile = path.join(rootPath, 'db/crontab.db'); const cronDbFile = path.join(rootPath, 'db/crontab.db');
const cookieDbFile = path.join(rootPath, 'db/cookie.db'); const envDbFile = path.join(rootPath, 'db/env.db');
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');
const configFound = dotenv.config({ path: confFile });
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),
secret: process.env.SECRET, cronPort: parseInt(process.env.CRON_PORT as string, 10),
secret: process.env.SECRET || createRandomString(16, 32),
logs: { logs: {
level: process.env.LOG_LEVEL || 'silly', level: process.env.LOG_LEVEL || 'silly',
}, },
api: { api: {
prefix: '/api', prefix: '/api',
}, },
rootPath,
configString, configString,
loginFaild, loginFaild,
authError, authError,
logPath, logPath,
extraFile, extraFile,
shareCodeDir,
authConfigFile, authConfigFile,
confBakDir, confBakDir,
crontabFile, crontabFile,
sampleFile, sampleFile,
confFile, confFile,
cookieFile, envFile,
fileMap: {
'config.sh': confFile,
'crontab.list': crontabFile,
'extra.sh': extraFile,
},
dbPath, dbPath,
cronDbFile, cronDbFile,
cookieDbFile, envDbFile,
manualLogPath, appDbFile,
authDbFile,
dependenceDbFile,
configPath,
scriptPath,
samplePath,
blackFileList: [
'auth.json',
'config.sh.sample',
'cookie.sh',
'crontab.list',
'env.sh',
],
writePathList: [configPath, scriptPath],
bakPath,
apiWhiteList: [
'/api/login',
'/open/auth/token',
'/api/user/two-factor/login',
'/api/system',
'/api/init/user',
'/api/init/notification',
],
versionFile,
lastVersionFile,
}; };
+122 -2
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)) {
@@ -27,7 +29,7 @@ export function getLastModifyFilePath(dir: string) {
return filePath; return filePath;
} }
export function createPassword(min: number, max: number): string { export function createRandomString(min: number, max: number): string {
const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const english = [ const english = [
'a', 'a',
@@ -85,7 +87,7 @@ export function createPassword(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 = [];
@@ -111,3 +113,121 @@ export function createPassword(min: number, max: number): string {
return newArr.join(''); return newArr.join('');
} }
export function getToken(req: any) {
const { authorization = '' } = req.headers;
if (authorization && authorization.split(' ')[0] === 'Bearer') {
return (authorization as string)
.replace('Bearer ', '')
.replace('mobile-', '')
.replace('desktop-', '');
}
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);
}
});
}
+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',
}
+12 -1
View File
@@ -8,6 +8,10 @@ export class Crontab {
_id?: string; _id?: string;
status?: CrontabStatus; status?: CrontabStatus;
isSystem?: 1 | 0; isSystem?: 1 | 0;
pid?: number;
isDisabled?: 1 | 0;
log_path?: string;
isPinned?: 1 | 0;
constructor(options: Crontab) { constructor(options: Crontab) {
this.name = options.name; this.name = options.name;
@@ -16,9 +20,15 @@ export class Crontab {
this.saved = options.saved; this.saved = options.saved;
this._id = options._id; this._id = options._id;
this.created = options.created; this.created = options.created;
this.status = options.status || CrontabStatus.idle; this.status = CrontabStatus[options.status]
? options.status
: CrontabStatus.idle;
this.timestamp = new Date().toString(); this.timestamp = new Date().toString();
this.isSystem = options.isSystem || 0; this.isSystem = options.isSystem || 0;
this.pid = options.pid;
this.isDisabled = options.isDisabled || 0;
this.log_path = options.log_path || '';
this.isPinned = options.isPinned || 0;
} }
} }
@@ -26,4 +36,5 @@ export enum CrontabStatus {
'running', 'running',
'idle', 'idle',
'disabled', 'disabled',
'queued',
} }
+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',
}
+10 -9
View File
@@ -1,27 +1,28 @@
export class Cookie { export class Env {
value?: string; value?: string;
timestamp?: string; timestamp?: string;
created?: number; created?: number;
_id?: string; _id?: string;
status?: CookieStatus; status?: EnvStatus;
position?: number; position?: number;
name?: number;
remarks?: number;
constructor(options: Cookie) { constructor(options: Env) {
this.value = options.value; this.value = options.value;
this._id = options._id; this._id = options._id;
this.created = options.created || new Date().valueOf(); this.created = options.created || new Date().valueOf();
this.status = options.status || CookieStatus.noacquired; this.status = options.status || EnvStatus.normal;
this.timestamp = new Date().toString(); this.timestamp = new Date().toString();
this.position = options.position; this.position = options.position;
this.name = options.name;
this.remarks = options.remarks;
} }
} }
export enum CookieStatus { export enum EnvStatus {
'noacquired',
'normal', 'normal',
'disabled', 'disabled',
'invalid',
'abnormal',
} }
export const initCookiePosition = 9999999999; export const initEnvPosition = 9999999999;
+81
View File
@@ -0,0 +1,81 @@
export enum NotificationMode {
'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 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,
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',
}
+16
View File
@@ -0,0 +1,16 @@
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';
+5
View File
@@ -0,0 +1,5 @@
declare namespace Express {
interface Request {
platform: 'desktop' | 'mobile';
}
}
@@ -2,10 +2,9 @@ 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';
export default async ({ expressApp }: { expressApp: any }) => { export default async ({ expressApp }: { expressApp: Application }) => {
Logger.info('✌️ DB loaded and connected!');
await dependencyInjectorLoader({ await dependencyInjectorLoader({
models: [], models: [],
}); });
+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;
+107 -9
View File
@@ -5,34 +5,124 @@ 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 { 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'], secret: config.secret as string,
algorithms: ['HS384'],
}).unless({
path: [...config.apiWhiteList, /^\/open\//],
}), }),
); );
app.use((req, res, next) => {
if (req.url && req.url.includes('/api/login')) { 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);
if (req.path.startsWith('/open/')) {
const openService = Container.get(OpenService);
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();
}
}
}
if (
!headerToken &&
req.path &&
config.apiWhiteList.includes(req.path) &&
req.path !== '/api/crons/status'
) {
return next(); return next();
} }
const remoteAddress = req.socket.remoteAddress;
if (
remoteAddress === '::ffff:127.0.0.1' &&
req.path === '/api/crons/status'
) {
return next();
}
const data = fs.readFileSync(config.authConfigFile, 'utf8'); const data = fs.readFileSync(config.authConfigFile, 'utf8');
const authHeader = req.headers.authorization;
if (data) { if (data) {
const { token } = JSON.parse(data); const { token = '', tokens = {} } = JSON.parse(data);
if (token && authHeader.includes(token)) { if (headerToken === token || tokens[req.platform] === headerToken) {
return next(); 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/init/user', '/api/init/notification'].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) => {
@@ -41,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 },
@@ -49,7 +141,10 @@ export default ({ app }: { app: Application }) => {
next: NextFunction, next: NextFunction,
) => { ) => {
if (err.name === 'UnauthorizedError') { if (err.name === 'UnauthorizedError') {
return res.status(err.status).send({ message: err.message }).end(); return res
.status(err.status)
.send({ code: 401, message: err.message })
.end();
} }
return next(err); return next(err);
}, },
@@ -62,8 +157,11 @@ 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,
message: err.message, message: err.message,
}); });
}, },
+65 -73
View File
@@ -1,89 +1,81 @@
import DependenceService from '../services/dependence';
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';
const initData = [ import _ from 'lodash';
{ import { dbs } from '../loaders/db';
name: '更新面板',
command: `sleep ${randomSchedule(
60,
1,
)} && git_pull >> $QL_DIR/log/git_pull.log 2>&1`,
schedule: `${randomSchedule(60, 1)} ${randomSchedule(
24,
7,
).toString()} * * *`,
status: CrontabStatus.idle,
},
{
name: 'build面板',
command: 'rebuild >> ${QL_DIR}/log/rebuild.log 2>&1',
schedule: '30 7 */7 * *',
status: CrontabStatus.disabled,
},
{
name: '自定义仓库',
command: `sleep ${randomSchedule(
60,
1,
)} && diy https://ghproxy.com/https://github.com/whyour/hundun.git "quanx/jx|quanx/jd" tokens >> $QL_DIR/log/diy_pull.log 2>&1`,
schedule: `${randomSchedule(60, 1)} ${randomSchedule(
24,
6,
).toString()} * * *`,
status: CrontabStatus.idle,
},
{
name: '互助码导出',
command: 'export_sharecodes',
schedule: '48 5 * * *',
status: CrontabStatus.idle,
},
{
name: '删除日志',
command: 'rm_log >/dev/null 2>&1',
schedule: '30 7 */7 * *',
status: CrontabStatus.disabled,
},
{
name: '重置密码',
command: 'js resetpwd',
schedule: '33 6 */7 * *',
status: CrontabStatus.disabled,
},
{
name: '运行所有脚本(慎用)',
command: 'js runall',
schedule: '33 6 */7 * *',
status: CrontabStatus.disabled,
},
];
export default async () => { export default async () => {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const cronDb = cronService.getDb(); const envService = Container.get(EnvService);
const dependenceService = Container.get(DependenceService);
const cronDb = dbs.cronDb;
const dependenceDb = dbs.dependenceDb;
cronDb.count({}, async (err, count) => { // 初始化更新所有任务状态为空闲
const data = initData.map((x) => { cronDb.update(
const tab = new Crontab(x); { status: { $in: [CrontabStatus.running, CrontabStatus.queued] } },
tab.created = new Date().valueOf(); { $set: { status: CrontabStatus.idle } },
tab.saved = false; { multi: true },
if (tab.name === '更新面板') { );
tab.isSystem = 1;
} else { // 初始化时安装所有处于安装中,安装成功,安装失败的依赖
tab.isSystem = 0; dependenceDb.find({ status: { $in: [0, 1, 2] } }).exec((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) {
dependenceService.reInstall([dep]);
}
} }
return tab;
});
if (count === 0) {
cronDb.insert(data);
await cronService.autosave_crontab();
} }
}); });
// 初始化时执行一次所有的ql repo 任务
cronDb
.find({
command: /ql (repo|raw)/,
isDisabled: { $ne: 1 },
})
.exec((err, docs) => {
for (let i = 0; i < docs.length; i++) {
const doc = docs[i];
if (doc) {
exec(doc.command);
}
}
});
// patch 禁用状态字段改变
cronDb
.find({
status: CrontabStatus.disabled,
})
.exec((err, docs) => {
if (docs.length > 0) {
const ids = docs.map((x) => x._id);
cronDb.update(
{ _id: { $in: ids } },
{ $set: { status: CrontabStatus.idle, isDisabled: 1 } },
{ multi: true },
(err) => {
cronService.autosave_crontab();
},
);
}
});
// 初始化保存一次ck和定时任务数据
await cronService.autosave_crontab();
await envService.set_envs();
}; };
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++) {
+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://e14681bce55f4849b11024a7d424b711@o1051273.ingest.sentry.io/6047906',
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);
};
+61
View File
@@ -0,0 +1,61 @@
import schedule from 'node-schedule';
import express from 'express';
import { exec } from 'child_process';
import Logger from './loaders/logger';
import { CrontabStatus } from './data/cron';
import config from './config';
import { dbs } from './loaders/db';
const app = express();
const run = async () => {
const cronDb = dbs.cronDb;
cronDb
.find({})
.sort({ created: 1 })
.exec((err, docs) => {
if (err) {
Logger.error(err);
process.exit(1);
}
if (docs && docs.length > 0) {
for (let i = 0; i < docs.length; i++) {
const task = docs[i];
const _schedule = task.schedule && task.schedule.split(/ +/);
if (
_schedule &&
_schedule.length > 5 &&
task.status !== CrontabStatus.disabled &&
!task.isDisabled
) {
schedule.scheduleJob(task.schedule, function () {
let command = task.command as string;
if (!command.includes('task ') && !command.includes('ql ')) {
command = `task ${command}`;
}
exec(command);
});
}
}
}
});
};
app
.listen(config.cronPort, async () => {
await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default();
await run();
Logger.info(`
################################################
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
################################################
`);
})
.on('error', (err) => {
Logger.error(err);
process.exit(1);
});
-281
View File
@@ -1,281 +0,0 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import fetch from 'node-fetch';
import { getFileContentByName } from '../config/util';
import config from '../config';
import * as fs from 'fs';
import got from 'got';
import DataStore from 'nedb';
import { Cookie, CookieStatus, initCookiePosition } from '../data/cookie';
@Service()
export default class CookieService {
private cronDb = new DataStore({ filename: config.cookieDbFile });
constructor(@Inject('logger') private logger: winston.Logger) {
this.cronDb.loadDatabase((err) => {
if (err) throw err;
});
}
public async getCookies() {
const content = getFileContentByName(config.cookieFile);
return this.formatCookie(content.split('\n').filter((x) => !!x));
}
public async addCookie(cookies: string[]) {
let content = getFileContentByName(config.cookieFile);
const originCookies = content.split('\n').filter((x) => !!x);
const result = originCookies.concat(cookies);
fs.writeFileSync(config.cookieFile, result.join('\n'));
return '';
}
public async updateCookie({ cookie, oldCookie }) {
let content = getFileContentByName(config.cookieFile);
const cookies = content.split('\n');
const index = cookies.findIndex((x) => x === oldCookie);
if (index !== -1) {
cookies[index] = cookie;
fs.writeFileSync(config.cookieFile, cookies.join('\n'));
return '';
} else {
return '未找到要原有Cookie';
}
}
public async deleteCookie(cookie: string) {
let content = getFileContentByName(config.cookieFile);
const cookies = content.split('\n');
const index = cookies.findIndex((x) => x === cookie);
if (index !== -1) {
cookies.splice(index, 1);
fs.writeFileSync(config.cookieFile, cookies.join('\n'));
return '';
} else {
return '未找到要删除的Cookie';
}
}
private async formatCookie(data: any[]) {
const result = [];
for (const x of data) {
const { nickname, status } = await this.getJdInfo(x);
if (/pt_pin=(.+?);/.test(x)) {
result.push({
pin: x.match(/pt_pin=(.+?);/)[1],
cookie: x,
status,
nickname: nickname,
});
} else {
result.push({
pin: 'pin未匹配到',
cookie: x,
status,
nickname: nickname,
});
}
}
return result;
}
public async refreshCookie(_id: string) {
const current = await this.get(_id);
const { status, nickname } = await this.getJdInfo(current.value);
return {
...current,
status,
nickname,
};
}
private getJdInfo(cookie: string) {
return fetch(
`https://me-api.jd.com/user_new/info/GetJDUserInfoUnion?orgFlag=JD_PinGou_New&callSource=mainorder&channel=4&isHomewhite=0&sceneval=2&_=${Date.now()}&sceneval=2&g_login_type=1&g_ty=ls`,
{
method: 'get',
headers: {
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-cn',
Connection: 'keep-alive',
Cookie: cookie,
Referer: 'https://home.m.jd.com/myJd/newhome.action',
'User-Agent':
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1',
Host: 'me-api.jd.com',
},
},
)
.then((x) => x.json())
.then((x) => {
if (x.retcode === '0' && x.data && x.data.userInfo) {
return {
nickname: x.data.userInfo.baseInfo.nickname,
status: CookieStatus.normal,
};
} else if (x.retcode === 13) {
return { status: CookieStatus.invalid, nickname: '-' };
}
return { status: CookieStatus.abnormal, nickname: '-' };
});
}
public async create(payload: string[]): Promise<Cookie[]> {
const cookies = await this.cookies('');
let position = initCookiePosition;
if (cookies && cookies.length > 0) {
position = cookies[cookies.length - 1].position;
}
const tabs = payload.map((x) => {
const cookie = new Cookie({ value: x, position });
position = position / 2;
cookie.position = position;
return cookie;
});
const docs = await this.insert(tabs);
await this.set_cookies();
return docs;
}
public async insert(payload: Cookie[]): Promise<Cookie[]> {
return new Promise((resolve) => {
this.cronDb.insert(payload, (err, docs) => {
if (err) {
this.logger.error(err);
} else {
resolve(docs);
}
});
});
}
public async update(payload: Cookie): Promise<Cookie> {
const { _id, ...other } = payload;
const doc = await this.get(_id);
const tab = new Cookie({ ...doc, ...other });
const newDoc = await this.updateDb(tab);
await this.set_cookies();
return newDoc;
}
public async updateDb(payload: Cookie): Promise<Cookie> {
return new Promise((resolve) => {
this.cronDb.update(
{ _id: payload._id },
payload,
{ returnUpdatedDocs: true },
(err, docs) => {
if (err) {
this.logger.error(err);
} else {
resolve(docs as Cookie);
}
},
);
});
}
public async remove(_id: string) {
this.cronDb.remove({ _id }, {});
await this.set_cookies();
}
public async move(
_id: string,
{
fromIndex,
toIndex,
}: {
fromIndex: number;
toIndex: number;
},
) {
let targetPosition: number;
const isUpward = fromIndex > toIndex;
const cookies = await this.cookies();
if (toIndex === 0 || toIndex === cookies.length - 1) {
targetPosition = isUpward
? cookies[0].position * 2
: cookies[toIndex].position / 2;
} else {
targetPosition = isUpward
? (cookies[toIndex].position + cookies[toIndex - 1].position) / 2
: (cookies[toIndex].position + cookies[toIndex + 1].position) / 2;
}
this.update({
_id,
position: targetPosition,
});
await this.set_cookies();
}
public async cookies(
searchText?: string,
sort: any = { position: -1 },
): Promise<Cookie[]> {
let query = {};
if (searchText) {
const reg = new RegExp(searchText);
query = {
$or: [
{
name: reg,
},
{
command: reg,
},
],
};
}
return new Promise((resolve) => {
this.cronDb
.find(query)
.sort({ ...sort })
.exec((err, docs) => {
resolve(docs);
});
});
}
public async get(_id: string): Promise<Cookie> {
return new Promise((resolve) => {
this.cronDb.find({ _id }).exec((err, docs) => {
resolve(docs[0]);
});
});
}
public async getBySort(sort: any): Promise<Cookie> {
return new Promise((resolve) => {
this.cronDb
.find({})
.sort({ ...sort })
.limit(1)
.exec((err, docs) => {
resolve(docs[0]);
});
});
}
public async disabled(_id: string) {
this.cronDb.update({ _id }, { $set: { status: CookieStatus.disabled } });
await this.set_cookies();
}
public async enabled(_id: string) {
this.cronDb.update({ _id }, { $set: { status: CookieStatus.noacquired } });
}
private async set_cookies() {
const cookies = await this.cookies();
let cookie_string = '';
cookies.forEach((tab) => {
if (tab.status !== CookieStatus.disabled) {
cookie_string += tab.value;
cookie_string += '\n';
}
});
fs.writeFileSync(config.cookieFile, cookie_string);
}
}
+288 -78
View File
@@ -7,19 +7,27 @@ 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 } from '../config/util';
import PQueue from 'p-queue';
import { promises, existsSync } from 'fs';
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;
constructor(@Inject('logger') private logger: winston.Logger) { private queue = new PQueue({
this.cronDb.loadDatabase((err) => { concurrency: parseInt(process.env.MaxConcurrentNum as string) || 5,
if (err) throw err; });
});
}
public getDb(): DataStore { constructor(@Inject('logger') private logger: winston.Logger) {}
return this.cronDb;
private isSixCron(cron: Crontab) {
const { schedule } = cron;
if (schedule.split(/ +/).length === 6) {
return true;
}
return false;
} }
public async create(payload: Crontab): Promise<Crontab> { public async create(payload: Crontab): Promise<Crontab> {
@@ -27,7 +35,7 @@ export default class CronService {
tab.created = new Date().valueOf(); tab.created = new Date().valueOf();
tab.saved = false; tab.saved = false;
const doc = await this.insert(tab); const doc = await this.insert(tab);
await this.set_crontab(); await this.set_crontab(this.isSixCron(doc));
return doc; return doc;
} }
@@ -49,7 +57,7 @@ export default class CronService {
const tab = new Crontab({ ...doc, ...other }); const tab = new Crontab({ ...doc, ...other });
tab.saved = false; tab.saved = false;
const newDoc = await this.updateDb(tab); const newDoc = await this.updateDb(tab);
await this.set_crontab(); await this.set_crontab(this.isSixCron(newDoc));
return newDoc; return newDoc;
} }
@@ -70,19 +78,84 @@ export default class CronService {
}); });
} }
public async status(_id: string, stopped: boolean) { public async status({
this.cronDb.update({ _id }, { $set: { stopped, saved: false } }); ids,
status,
pid,
log_path,
last_running_time = 0,
last_execution_time = 0,
}: {
ids: string[];
status: CrontabStatus;
pid: number;
log_path: string;
last_running_time: number;
last_execution_time: number;
}) {
return new Promise((resolve) => {
this.cronDb.update(
{ _id: { $in: ids } },
{
$set: {
status,
pid,
log_path,
last_running_time,
last_execution_time,
},
},
{ multi: true, returnUpdatedDocs: true },
(err) => {
resolve(null);
},
);
});
} }
public async remove(_id: string) { public async remove(ids: string[]) {
this.cronDb.remove({ _id }, {}); return new Promise((resolve: any) => {
await this.set_crontab(); this.cronDb.remove(
{ _id: { $in: ids } },
{ multi: true },
async (err) => {
await this.set_crontab(true);
resolve();
},
);
});
}
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); const reg = new RegExp(searchText, 'i');
query = { query = {
$or: [ $or: [
{ {
@@ -91,13 +164,16 @@ export default class CronService {
{ {
command: reg, command: reg,
}, },
{
schedule: reg,
},
], ],
}; };
} }
return new Promise((resolve) => { return new Promise((resolve) => {
this.cronDb this.cronDb
.find(query) .find(query)
.sort({ created: 1 }) .sort({ created: -1 })
.exec((err, docs) => { .exec((err, docs) => {
resolve(docs); resolve(docs);
}); });
@@ -112,73 +188,206 @@ export default class CronService {
}); });
} }
public async run(_id: string) { public async run(ids: string[]) {
this.cronDb.find({ _id }).exec((err, docs: Crontab[]) => { this.cronDb.update(
let res = docs[0]; { _id: { $in: ids } },
{ $set: { status: CrontabStatus.queued } },
{ multi: true },
);
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
this.queue.add(() => this.runSingle(id));
}
}
public async stop(ids: string[]) {
return new Promise((resolve: any) => {
this.cronDb
.find({ _id: { $in: ids } })
.exec(async (err, docs: Crontab[]) => {
for (const doc of docs) {
if (doc.pid) {
try {
process.kill(-doc.pid);
} catch (error) {
this.logger.silly(error);
}
}
const err = await this.killTask(doc.command);
if (doc.log_path) {
const str = err ? `\n${err}` : '';
fs.appendFileSync(
`${doc.log_path}`,
`${str}\n## 执行结束... ${new Date()
.toLocaleString('zh', { hour12: false })
.replace(' 24:', ' 00:')} `,
);
}
}
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
{ multi: true },
);
this.queue.clear();
resolve();
});
});
}
private async killTask(name: string) {
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
const execAsync = promisify(exec);
try {
let pid = (await execAsync(taskCommand)).stdout;
if (pid) {
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
} else {
return;
}
const pids = pid.match(/\(\d+/g);
const killLogs = [];
for (const id of pids) {
const c = `kill -9 ${id.slice(1)}`;
const { stdout, stderr } = await execAsync(c);
if (stderr) {
killLogs.push(stderr);
}
if (stdout) {
killLogs.push(stdout);
}
}
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
} catch (e) {
return JSON.stringify(e);
}
}
private async runSingle(id: string): Promise<number> {
return new Promise(async (resolve: any) => {
const cron = await this.get(id);
if (cron.status !== CrontabStatus.queued) {
resolve();
return;
}
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);
this.logger.silly('Original command: ' + res.command); this.logger.silly('Original command: ' + command);
let logFile = `${config.manualLogPath}${res._id}.log`; let cmdStr = command;
fs.writeFileSync(logFile, `开始执行...\n\n${new Date().toString()}\n`); if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
cmdStr = `task ${cmdStr}`;
let cmdStr = res.command; }
if (res.command.startsWith('js') && !res.command.endsWith('now')) { if (cmdStr.endsWith('.js')) {
cmdStr = `${res.command} now`; cmdStr = `${cmdStr} now`;
} else if (/&& (.*) >>/.test(res.command)) {
cmdStr = res.command.match(/&& (.*) >>/)[1];
} }
const cmd = spawn(cmdStr, { shell: true });
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.running } }); const cp = spawn(cmdStr, { shell: '/bin/bash' });
this.cronDb.update(
cmd.stdout.on('data', (data) => { { _id },
this.logger.silly(`stdout: ${data}`); { $set: { status: CrontabStatus.running, pid: cp.pid } },
fs.appendFileSync(logFile, data); );
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)}`);
}
}); });
cmd.stderr.on('data', (data) => { cp.on('exit', (code, signal) => {
this.logger.error(`stderr: ${data}`); this.logger.info(
fs.appendFileSync(logFile, data); `${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
);
this.cronDb.update(
{ _id },
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
);
resolve();
}); });
cp.on('close', (code) => {
cmd.on('close', (code) => { this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
this.logger.silly(`child process exited with code ${code}`); this.cronDb.update(
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } }); { _id },
}); { $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
);
cmd.on('error', (err) => { resolve();
this.logger.silly(err);
fs.appendFileSync(logFile, err.stack);
});
cmd.on('exit', (code: number, signal: any) => {
this.logger.silly(`cmd exit ${code}`);
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
fs.appendFileSync(logFile, `\n\n执行结束...`);
});
cmd.on('disconnect', () => {
this.logger.silly(`cmd disconnect`);
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
fs.appendFileSync(logFile, `\n\n连接断开...`);
}); });
}); });
} }
public async disabled(_id: string) { public async disabled(ids: string[]) {
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.disabled } }); return new Promise((resolve: any) => {
await this.set_crontab(); this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { isDisabled: 1 } },
{ multi: true },
async (err) => {
await this.set_crontab(true);
resolve();
},
);
});
} }
public async enabled(_id: string) { public async enabled(ids: string[]) {
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } }); return new Promise((resolve: any) => {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { isDisabled: 0 } },
{ multi: true },
async (err) => {
await this.set_crontab(true);
resolve();
},
);
});
} }
public async log(_id: string) { public async log(_id: string) {
let logFile = `${config.manualLogPath}${_id}.log`; const doc = await this.get(_id);
return getFileContentByName(logFile); if (!doc) {
return '';
}
if (doc.log_path) {
return getFileContentByName(`${doc.log_path}`);
}
const [, commandStr, url] = doc.command.split(/ +/);
let logPath = this.getKey(commandStr);
const isQlCommand = doc.command.startsWith('ql ');
const key =
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
logPath;
if (isQlCommand) {
logPath = 'update';
}
let logDir = `${config.logPath}${logPath}`;
if (existsSync(logDir)) {
let files = await promises.readdir(logDir);
if (isQlCommand) {
files = files.filter((x) => x.includes(key));
}
return getFileContentByName(`${logDir}/${files[files.length - 1]}`);
} else {
return '';
}
}
private getKey(command: string) {
const start =
command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0;
const end =
command.lastIndexOf('.') !== -1
? command.lastIndexOf('.')
: command.length;
return command.substring(start, end);
} }
private make_command(tab: Crontab) { private make_command(tab: Crontab) {
@@ -186,11 +395,12 @@ export default class CronService {
return crontab_job_string; return crontab_job_string;
} }
private async set_crontab() { private async set_crontab(needReloadSchedule: boolean = false) {
const tabs = await this.crontabs(); const tabs = await this.crontabs();
var crontab_string = ''; var crontab_string = '';
tabs.forEach((tab) => { tabs.forEach((tab) => {
if (tab.status === CrontabStatus.disabled) { const _schedule = tab.schedule && tab.schedule.split(/ +/);
if (tab.isDisabled === 1 || _schedule.length !== 5) {
crontab_string += '# '; crontab_string += '# ';
crontab_string += tab.schedule; crontab_string += tab.schedule;
crontab_string += ' '; crontab_string += ' ';
@@ -208,13 +418,12 @@ export default class CronService {
fs.writeFileSync(config.crontabFile, crontab_string); fs.writeFileSync(config.crontabFile, crontab_string);
execSync(`crontab ${config.crontabFile}`); execSync(`crontab ${config.crontabFile}`);
if (needReloadSchedule) {
exec(`pm2 reload schedule`);
}
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');
@@ -222,15 +431,16 @@ export default class CronService {
lines.reverse().forEach((line, index) => { lines.reverse().forEach((line, index) => {
line = line.replace(/\t+/g, ' '); line = line.replace(/\t+/g, ' ');
var regex = /^((\@[a-zA-Z]+\s+)|(([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+))/; var regex =
/^((\@[a-zA-Z]+\s+)|(([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+))/;
var command = line.replace(regex, '').trim(); var command = line.replace(regex, '').trim();
var schedule = line.replace(command, '').trim(); var schedule = line.replace(command, '').trim();
var is_valid = false; if (
try { command &&
is_valid = cron_parser.parseString(line).expressions.length > 0; schedule &&
} catch (e) {} cron_parser.parseExpression(schedule).hasNext()
if (command && schedule && is_valid) { ) {
var name = namePrefix + '_' + index; var name = namePrefix + '_' + index;
this.cronDb.findOne({ command, schedule }, (err, doc) => { this.cronDb.findOne({ command, schedule }, (err, doc) => {
+280
View File
@@ -0,0 +1,280 @@
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[]) => {
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,
) {
if (dependencies.length === 0) {
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));
});
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);
}
});
}
}
+232
View File
@@ -0,0 +1,232 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { getFileContentByName } from '../config/util';
import config from '../config';
import * as fs from 'fs';
import DataStore from 'nedb';
import { Env, EnvStatus, initEnvPosition } from '../data/env';
import _ from 'lodash';
import { dbs } from '../loaders/db';
@Service()
export default class EnvService {
private envDb = dbs.envDb;
constructor(@Inject('logger') private logger: winston.Logger) {}
public async create(payloads: Env[]): Promise<Env[]> {
const envs = await this.envs();
let position = initEnvPosition;
if (envs && envs.length > 0 && envs[envs.length - 1].position) {
position = envs[envs.length - 1].position as number;
}
const tabs = payloads.map((x) => {
position = position / 2;
const tab = new Env({ ...x, position });
return tab;
});
const docs = await this.insert(tabs);
await this.set_envs();
return docs;
}
public async insert(payloads: Env[]): Promise<Env[]> {
return new Promise((resolve) => {
this.envDb.insert(payloads, (err, docs) => {
if (err) {
this.logger.error(err);
} else {
resolve(docs);
}
});
});
}
public async update(payload: Env): Promise<Env> {
const { _id, ...other } = payload;
const doc = await this.get(_id);
const tab = new Env({ ...doc, ...other });
const newDoc = await this.updateDb(tab);
await this.set_envs();
return newDoc;
}
private async updateDb(payload: Env): Promise<Env> {
return new Promise((resolve) => {
this.envDb.update(
{ _id: payload._id },
payload,
{ returnUpdatedDocs: true },
(err, num, doc) => {
if (err) {
this.logger.error(err);
} else {
resolve(doc as Env);
}
},
);
});
}
public async remove(ids: string[]) {
return new Promise((resolve: any) => {
this.envDb.remove({ _id: { $in: ids } }, { multi: true }, async (err) => {
await this.set_envs();
resolve();
});
});
}
public async move(
_id: string,
{
fromIndex,
toIndex,
}: {
fromIndex: number;
toIndex: number;
},
): Promise<Env> {
let targetPosition: number;
const isUpward = fromIndex > toIndex;
const envs = await this.envs();
if (toIndex === 0 || toIndex === envs.length - 1) {
targetPosition = isUpward
? envs[0].position * 2
: envs[toIndex].position / 2;
} else {
targetPosition = isUpward
? (envs[toIndex].position + envs[toIndex - 1].position) / 2
: (envs[toIndex].position + envs[toIndex + 1].position) / 2;
}
const newDoc = await this.update({
_id,
position: targetPosition,
});
return newDoc;
}
public async envs(
searchText: string = '',
sort: any = { position: -1 },
query: any = {},
): Promise<Env[]> {
let condition = { ...query };
if (searchText) {
const reg = new RegExp(searchText);
condition = {
$or: [
{
value: reg,
},
{
name: reg,
},
{
remarks: reg,
},
],
};
}
const newDocs = await this.find(condition, sort);
return newDocs;
}
private async find(query: any, sort: any): Promise<Env[]> {
return new Promise((resolve) => {
this.envDb
.find(query)
.sort({ ...sort })
.exec((err, docs) => {
resolve(docs);
});
});
}
public async get(_id: string): Promise<Env> {
return new Promise((resolve) => {
this.envDb.find({ _id }).exec((err, docs) => {
resolve(docs[0]);
});
});
}
public async getBySort(sort: any): Promise<Env> {
return new Promise((resolve) => {
this.envDb
.find({})
.sort({ ...sort })
.limit(1)
.exec((err, docs) => {
resolve(docs[0]);
});
});
}
public async disabled(ids: string[]) {
return new Promise((resolve: any) => {
this.envDb.update(
{ _id: { $in: ids } },
{ $set: { status: EnvStatus.disabled } },
{ multi: true },
async (err) => {
await this.set_envs();
resolve();
},
);
});
}
public async enabled(ids: string[]) {
return new Promise((resolve: any) => {
this.envDb.update(
{ _id: { $in: ids } },
{ $set: { status: EnvStatus.normal } },
{ multi: true },
async (err, num) => {
await this.set_envs();
resolve();
},
);
});
}
public async updateNames({ ids, name }: { ids: string[]; name: string }) {
return new Promise((resolve: any) => {
this.envDb.update(
{ _id: { $in: ids } },
{ $set: { name } },
{ multi: true },
async (err, num) => {
await this.set_envs();
resolve();
},
);
});
}
public async set_envs() {
const envs = await this.envs(
'',
{ position: -1 },
{ name: { $exists: true } },
);
const groups = _.groupBy(envs, 'name');
let env_string = '';
for (const key in groups) {
if (Object.prototype.hasOwnProperty.call(groups, key)) {
const group = groups[key];
// 忽略不符合bash要求的环境变量名称
if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) {
env_string += `export ${key}="${_(group)
.filter((x) => x.status !== EnvStatus.disabled)
.map('value')
.join('&')
.replace(/ /g, '')}"\n`;
}
}
}
fs.writeFileSync(config.envFile, env_string);
}
}
+334
View File
@@ -0,0 +1,334 @@
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([
['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 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;
}
}
+171
View File
@@ -0,0 +1,171 @@
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 reg = new RegExp(searchText);
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();
}
}
+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));
});
}
}
+479
View File
@@ -0,0 +1,479 @@
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];
const lastVersionFileContent = await (
await got.get(config.lastVersionFile)
).body;
const lastVersion = lastVersionFileContent.match(versionRegx)![1];
const lastLog = lastVersionFileContent.match(logRegx)
? lastVersionFileContent.match(logRegx)![1]
: '';
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 };
}
}
+16
View File
@@ -0,0 +1,16 @@
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
ports:
- "0.0.0.0:5700:5700"
environment:
- ENABLE_HANGUP=true
- ENABLE_WEB_PANEL=true
restart: always
+25 -22
View File
@@ -1,14 +1,19 @@
FROM node:lts-alpine FROM node:lts-alpine
LABEL maintainer="whyour" ARG QL_MAINTAINER="whyour"
ARG QL_BASE_URL=https://github.com.cnpmjs.org/whyour/qinglong LABEL maintainer="${QL_MAINTAINER}"
ARG QL_BASE_BRANCH=master ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
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_MAINTAINER=${QL_MAINTAINER} \
QL_URL=${QL_URL} \
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 \
@@ -16,35 +21,33 @@ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
moreutils \ moreutils \
git \ git \
curl \ curl \
wget \
tzdata \ tzdata \
perl \ perl \
openssl \ openssl \
nginx \ nginx \
python3 \ python3 \
jq \ jq \
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_BASE_BRANCH} ${QL_BASE_URL} ${QL_DIR} \ && git config --global user.email "qinglong@@users.noreply.github.com" \
&& git config --global user.name "qinglong" \
&& cd ${QL_DIR} \ && cd ${QL_DIR} \
&& cp -f .env.example .env \ && cp -f .env.example .env \
&& ln -sf ${QL_DIR}/shell/js.sh /usr/local/bin/js \
&& ln -sf ${QL_DIR}/shell/git_pull.sh /usr/local/bin/git_pull \
&& ln -sf ${QL_DIR}/shell/rm_log.sh /usr/local/bin/rm_log \
&& ln -sf ${QL_DIR}/shell/export_sharecodes.sh /usr/local/bin/export_sharecodes \
&& ln -sf ${QL_DIR}/shell/git_diy.sh /usr/local/bin/diy \
&& ln -sf ${QL_DIR}/shell/notify.sh /usr/local/bin/notify \
&& ln -sf ${QL_DIR}/shell/rebuild.sh /usr/local/bin/rebuild \
&& ln -sf ${QL_DIR}/docker/docker-entrypoint.sh /usr/local/bin/entrypoint \
&& chmod 777 ${QL_DIR}/shell/*.sh \ && chmod 777 ${QL_DIR}/shell/*.sh \
&& chmod 777 ${QL_DIR}/docker/*.sh \ && chmod 777 ${QL_DIR}/docker/*.sh \
&& npm install -g pm2 \ && npm install -g pnpm \
&& pnpm install -g pm2 \
&& pnpm install -g ts-node typescript tslib \
&& rm -rf /root/.npm \ && rm -rf /root/.npm \
&& yarn install --network-timeout 100000 \ && pnpm install --prod \
&& yarn build \ && rm -rf /root/.pnpm-store \
&& yarn build-back \ && git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
&& yarn install --production --network-timeout 100000 \ && cp -rf /static/* ${QL_DIR} \
&& yarn cache clean && rm -rf /static
ENTRYPOINT ["entrypoint"] ENTRYPOINT ["./docker/docker-entrypoint.sh"]
+40 -53
View File
@@ -1,64 +1,51 @@
#!/bin/bash #!/bin/bash
set -e
dir_shell=/ql/shell
. $dir_shell/share.sh
link_shell
echo -e "======================1. 检测配置文件========================\n" echo -e "======================1. 检测配置文件========================\n"
[ ! -d ${QL_DIR}/config ] && mkdir -p ${QL_DIR}/config make_dir /etc/nginx/conf.d
[ ! -d ${QL_DIR}/log ] && mkdir -p ${QL_DIR}/log cp -fv $nginx_conf /etc/nginx/nginx.conf
[ ! -d ${QL_DIR}/db ] && mkdir -p ${QL_DIR}/db cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
[ ! -d ${QL_DIR}/manual_log ] && mkdir -p ${QL_DIR}/manual_log pm2 l &>/dev/null
if [ ! -s ${QL_DIR}/config/crontab.list ]
then
echo -e "检测到config配置目录下不存在crontab.list或存在但文件为空,从示例文件复制一份用于初始化...\n"
cp -fv ${QL_DIR}/sample/crontab.list.sample ${QL_DIR}/config/crontab.list
sed -i "s,MY_PATH,${QL_DIR},g" ${QL_DIR}/config/crontab.list
sed -i "s,ENV_PATH=,PATH=$PATH,g" ${QL_DIR}/config/crontab.list
fi
crond
crontab ${QL_DIR}/config/crontab.list
echo -e "成功添加定时任务...\n"
if [ ! -s ${QL_DIR}/config/cookie.sh ]; then
echo -e "检测到config配置目录下不存在cookie.sh,从示例文件复制一份用于初始化...\n"
touch ${QL_DIR}/config/cookie.sh
echo
fi
if [ ! -s ${QL_DIR}/config/config.sh ]; then
echo -e "检测到config配置目录下不存在config.sh,从示例文件复制一份用于初始化...\n"
cp -fv ${QL_DIR}/sample/config.sh.sample ${QL_DIR}/config/config.sh
echo
fi
if [ ! -s ${QL_DIR}/config/auth.json ]; then
echo -e "检测到config配置目录下不存在auth.json,从示例文件复制一份用于初始化...\n"
cp -fv ${QL_DIR}/sample/auth.json ${QL_DIR}/config/auth.json
echo
fi
if [ -s /etc/nginx/conf.d/default.conf ]; then
echo -e "检测到默认nginx配置文件,删除...\n"
rm -f /etc/nginx/conf.d/default.conf
echo
fi
cp -fv ${QL_DIR}/docker/front.conf /etc/nginx/conf.d/front.conf
echo -e "======================2. 启动nginx========================\n"
nginx -c /etc/nginx/nginx.conf
echo echo
echo -e "======================3. 启动控制面板========================\n" echo -e "======================2. 安装依赖========================\n"
pm2 start ${QL_DIR}/build/app.js -n panel update_depend
echo
echo -e "======================3. 启动nginx========================\n"
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "nginx启动成功...\n"
echo -e "======================4. 启动控制面板========================\n"
pm2 delete panel &>/dev/null
pm2 start $dir_root/build/app.js -n panel --source-map-support --time
echo -e "控制面板启动成功...\n" echo -e "控制面板启动成功...\n"
echo -e "\n容器启动成功...\n" echo -e "======================5. 启动定时任务========================\n"
echo -e "\n请先访问5700端口,登录面板成功之后先手动执行一次git_pull命令...\n" pm2 delete schedule &>/dev/null
echo -e "\n如果需要启动挂机程序手动执行docker exec -it qinglong js hangup...\n" pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time
echo -e "\n或者去cron管理搜索hangup手动执行挂机任务...\n" echo -e "定时任务启动成功...\n"
if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ]; then if [[ $AutoStartBot == true ]]; then
set -- node "$@" echo -e "======================6. 启动bot========================\n"
nohup ql bot >>$dir_log/start.log 2>&1 &
echo -e "bot后台启动中...\n"
fi fi
if [[ $EnableExtraShell == true ]]; then
echo -e "======================7. 执行自定义脚本========================\n"
nohup ql extra >>$dir_log/start.log 2>&1 &
echo -e "自定义脚本后台执行中...\n"
fi
echo -e "############################################################\n"
echo -e "容器启动成功..."
echo -e "\n请先访问5700端口,登录成功面板之后再执行添加定时任务..."
echo -e "############################################################\n"
crond -f >/dev/null
exec "$@" exec "$@"
+19 -1
View File
@@ -1,5 +1,10 @@
upstream api { upstream api {
server localhost:5600; server 0.0.0.0:5600;
}
map $http_upgrade $connection_upgrade {
default keep-alive;
'websocket' upgrade;
} }
server { server {
@@ -8,6 +13,19 @@ server {
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;
}
+41 -13
View File
@@ -5,7 +5,8 @@
"build": "umi build", "build": "umi build",
"build-back": "tsc -p tsconfig.back.json", "build-back": "tsc -p tsconfig.back.json",
"start-back": "nodemon", "start-back": "nodemon",
"pm2": "npm run build-back && node build/app.js", "panel": "npm run build-back && node build/app.js",
"schedule": "npm run build-back && node build/schedule.js",
"prepare": "umi generate tmp", "prepare": "umi generate tmp",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"test": "umi-test", "test": "umi-test",
@@ -23,52 +24,79 @@
] ]
}, },
"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",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron-parser": "^3.3.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", "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",
"nedb": "^1.8.0", "nedb": "^1.8.0",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"node-schedule": "^2.0.0",
"nodemailer": "^6.7.0",
"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",
"uuid": "^8.3.2",
"winston": "^3.3.3" "winston": "^3.3.3"
}, },
"devDependencies": { "devDependencies": {
"umi": "^3.3.9", "@ant-design/icons": "^4.6.2",
"umi-request": "^1.3.5", "@ant-design/pro-layout": "^6.26.0",
"react-codemirror2": "^7.2.1", "@monaco-editor/react": "^4.3.1",
"react-diff-viewer": "^3.1.1", "@sentry/react": "^6.14.0",
"react-dnd": "^14.0.2", "@sentry/webpack-plugin": "^1.18.3",
"react-dnd-html5-backend": "^14.0.0",
"qrcode.react": "^1.0.1",
"darkreader": "^4.9.27",
"codemirror": "^5.59.4",
"@ant-design/pro-layout": "^6.5.0",
"@umijs/plugin-antd": "^0.9.1",
"@types/cors": "^2.8.10", "@types/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",
"@types/jsonwebtoken": "^8.5.0", "@types/jsonwebtoken": "^8.5.0",
"@types/lodash": "^4.14.170",
"@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",
"@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",
"compression-webpack-plugin": "6.1.1", "compression-webpack-plugin": "6.1.1",
"darkreader": "^4.9.27",
"lint-staged": "^10.0.7", "lint-staged": "^10.0.7",
"nodemon": "^2.0.4", "nodemon": "^2.0.4",
"prettier": "^2.2.0", "prettier": "^2.2.0",
"qrcode.react": "^1.0.1",
"react": "17.x", "react": "17.x",
"react-codemirror2": "^7.2.1",
"react-diff-viewer": "^3.1.1",
"react-dnd": "^14.0.2",
"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.5.0",
"umi-request": "^1.3.5",
"vh-check": "^2.0.5",
"webpack": "^5.28.0", "webpack": "^5.28.0",
"yorkie": "^2.0.0" "yorkie": "^2.0.0"
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

-1
View File
@@ -1 +0,0 @@
{ "username": "admin", "password": "adminadmin" }
+1
View File
@@ -0,0 +1 @@
{ "username": "admin", "password": "admin" }
+115
View File
@@ -0,0 +1,115 @@
## Version: v2.8.0
## Date: 2021-06-20
## Update Content: 可持续发展纲要\n1. session管理破坏性修改\n2. 配置管理可编辑config下文件\n3. 自定义脚本改为查看脚本\n4. 移除互助相关
## 上面版本号中,如果第2位数字有变化,那么代表增加了新的参数,如果只有第3位数字有变化,仅代表更新了注释,没有增加新的参数,可更新可不更新
## 在运行 ql repo 命令时,是否自动删除失效的脚本与定时任务
AutoDelCron="true"
## 在运行 ql repo 命令时,是否自动增加新的本地定时任务
AutoAddCron="true"
## 拉取脚本时默认的定时规则,当匹配不到定时规则时使用,例如: 0 9 * * *
DefaultCronRule=""
## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可
RepoFileExtensions="js py"
## 由于github仓库拉取较慢,所以会默认添加代理前缀,如不需要请移除
GithubProxyUrl="https://ghproxy.com/"
## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
CommandTimeoutTime="1h"
## 设置批量执行任务时的并发数,默认同时执行5个任务
MaxConcurrentNum="5"
## 在运行 task 命令时,随机延迟启动任务的最大延迟时间
## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
RandomDelay="300"
## 如果你自己会写shell脚本,并且希望在每次运行 ql update 命令时,额外运行你的 shell 脚本,请赋值为 "true",默认为true
EnableExtraShell="true"
## 是否自动启动bot,默认不启动,设置为true时自动启动,目前需要自行克隆bot仓库所需代码,存到ql/repo目录下,文件夹命名为dockerbot
AutoStartBot=""
## 安装bot依赖时指定pip源,默认使用清华源,如不需要源,设置此参数为空
PipMirror="https://pypi.tuna.tsinghua.edu.cn/simple"
## 通知环境变量
## 1. Server酱
## https://sct.ftqq.com
## 下方填写 SCHKEY 值或 SendKey 值
export PUSH_KEY=""
## 2. BARK
## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123
export BARK_PUSH=""
## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声
export BARK_SOUND=""
## 下方填写推送消息分组,默认为"QingLong"
export BARK_GROUP="QingLong"
## 3. Telegram
## 下方填写自己申请@BotFather的Token,如10xxx4:AAFcqxxxxgER5uw
export TG_BOT_TOKEN=""
## 下方填写 @getuseridbot 中获取到的纯数字ID
export TG_USER_ID=""
## Telegram 代理IP(选填)
## 下方填写代理IP地址,代理类型为 http,比如您代理是 http://127.0.0.1:1080,则填写 "127.0.0.1"
## 如需使用,请自行解除下一行的注释
export TG_PROXY_HOST=""
## Telegram 代理端口(选填)
## 下方填写代理端口号,代理类型为 http,比如您代理是 http://127.0.0.1:1080,则填写 "1080"
## 如需使用,请自行解除下一行的注释
export TG_PROXY_PORT=""
## Telegram 代理的认证参数(选填)
export TG_PROXY_AUTH=""
## Telegram api自建反向代理地址(选填)
## 教程:https://www.hostloc.com/thread-805441-1-1.html
## 如反向代理地址 http://aaa.bbb.ccc 则填写 aaa.bbb.ccc
## 如需使用,请赋值代理地址链接,并自行解除下一行的注释
export TG_API_HOST=""
## 4. 钉钉
## 官方文档:https://developers.dingtalk.com/document/app/custom-robot-access
## 下方填写token后面的内容,只需 https://oapi.dingtalk.com/robot/send?access_token=XXX 等于=符号后面的XXX即可
export DD_BOT_TOKEN=""
export DD_BOT_SECRET=""
## 5. 企业微信机器人
## 官方说明文档:https://work.weixin.qq.com/api/doc/90000/90136/91770
## 下方填写密钥,企业微信推送 webhook 后面的 key
export QYWX_KEY=""
## 6. 企业微信应用
## 参考文档:http://note.youdao.com/s/HMiudGkb
## 下方填写素材库图片idcorpid,corpsecret,touser,agentid),素材库图片填0为图文消息, 填1为纯文本消息
export QYWX_AM=""
## 7. iGot聚合
## 参考文档:https://wahao.github.io/Bark-MP-helper
## 下方填写iGot的推送key,支持多方式推送,确保消息可达
export IGOT_PUSH_KEY=""
## 8. Push Plus
## 官方网站:http://www.pushplus.plus
## 下方填写您的Token,微信扫码登录后一对一推送或一对多推送下面的token,只填 PUSH_PLUS_TOKEN 默认为一对一推送
export PUSH_PLUS_TOKEN=""
## 一对一多推送(选填)
## 下方填写您的一对多推送的 "群组编码" ,(一对多推送下面->您的群组(如无则新建)->群组编码)
## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送
export PUSH_PLUS_USER=""
## 9. go-cqhttp
## gobot_url 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
## gobot_token 填写在go-cqhttp文件设置的访问密钥
## gobot_qq 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
## go-cqhttp相关API https://docs.go-cqhttp.org/api
export GOBOT_URL=""
export GOBOT_TOKEN=""
export GOBOT_QQ=""
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
-599
View File
@@ -1,599 +0,0 @@
## Version: v3.30.6
## Date: 2021-3-8
## Update Content: 京小兑控制变量,宠汪汪自己内部账户互助变量,环球挑战赛互助变量。
## 上面版本号中,如果第2位数字有变化,那么代表增加了新的参数,如果只有第3位数字有变化,仅代表更新了注释,没有增加新的参数,可更新可不更新
################################## 说明 ##################################
## 以下配置中,带有 export 申明的,均由LXK9301大佬定义,详见:https://gitee.com/lxk0301/jd_scripts/blob/master/githubAction.md
## 其他互助码,考虑到直接按LXK9301大佬定义的变量去填的话,一是不方便记忆,二是容易搞混,所以最终的变量将由脚本去组合,你只要按注释去填即可
## 除此之外,还额外增加了是否自动删除失效任务AutoDelCron、是否自动增加新任务AutoAddCron、删除旧日志时间RmLogDaysAgo、随机延迟启动任务RandomDelay、是否添加自定义脚本EnableExtraShell五个人性化的设置供选择
## 所有赋值等号两边不能有空格,所有的值请一律在两侧添加半角的双引号,如果变量值中含有双引号,则外侧改为一对半角的单引号。
## 所有的赋值都可以参考 “定义东东萌宠是否静默运行” 部分,在不同时间设置不同的值,以达到你想要的效果,具体判断条件如下:
## $(date "+%d") 当前的日期,如:13
## $(date "+%w") 当前是星期几,如:3
## $(date "+%H") 当前的小时数,如:23
## $(date "+%M") 当前的分钟数,如:49
## 其他date命令的更多用法,可以在命令行中输入 date --help 查看
## 判断条件 -eq -ne -gt -ge -lt -le ,具体含义可百度一下
################################## 临时屏蔽某个Cookie(选填) ##################################
## 如果某些Cookie已经失效了,但暂时还没法更新,可以使用此功能在不删除该Cookie和重新修改Cookie编号的前提下,临时屏蔽掉某些编号的Cookie
## 多个Cookie编号以半角的空格分隔,两侧一对半角双引号,使用此功能后,在运行js脚本时账户编号将发生变化
## 举例1TempBlockCookie="2" 临时屏蔽掉Cookie2
## 举例2TempBlockCookie="2 4" 临时屏蔽掉Cookie2和Cookie4
## 如果只是想要屏蔽某个账号不玩某些小游戏,可以参考下面 case 这个命令的例子来控制,脚本名称请去掉后缀 “.js”
## case $1 in
## jd_fruit)
## TempBlockCookie="5" # 账号5不玩东东农场
## ;;
## jd_dreamFactory | jd_jdfactory)
## TempBlockCookie="2" # 账号2不玩京喜工厂和东东工厂
## ;;
## jd_jdzz | jd_joy)
## TempBlockCookie="3 6" # 账号3、账号6不玩京东赚赚和宠汪汪
## ;;
## esac
TempBlockCookie=""
################################## 定义是否自动删除失效的脚本与定时任务(选填) ##################################
## 有的时候,某些JS脚本只在特定的时间有效,过了时间就失效了,需要自动删除失效的本地定时任务,则设置为 "true" ,否则请设置为 "false"
## 检测文件:LXK9301/jd_scripts 仓库中的 docker/crontab_list.sh,和 shylocks/Loon 仓库中的 docker/crontab_list.sh
## 当设置为 "true" 时,会自动从检测文件中读取比对删除的任务(识别以“jd_”、“jr_”、“jx_”开头的任务)
## 当设置为 "true" 时,脚本只会删除一整行失效的定时任务,不会修改其他现有任务,所以任何时候,你都可以自己调整你的crontab.list
## 当设置为 "true" 时,如果你有添加额外脚本是以“jd_”“jr_”“jx_”开头的,如检测文件中,会被删除,不是以“jd_”“jr_”“jx_”开头的任务则不受影响
AutoDelCron="true"
################################## 定义是否自动增加新的本地定时任务(选填) ##################################
## LXK9301 大佬会在有需要的时候,增加定时任务,如需要本地自动增加新的定时任务,则设置为 "true" ,否则请设置为 "false"
## 检测文件:LXK9301/jd_scripts 仓库中的 docker/crontab_list.sh,和 shylocks/Loon 仓库中的 docker/crontab_list.sh
## 当设置为 "true" 时,如果检测到检测文件中有增加新的定时任务,那么在本地也增加(识别以“jd_”、“jr_”、“jx_”开头的任务)
## 当设置为 "true" 时,会自动从检测文件新增加的任务中读取时间,该时间为北京时间
## 当设置为 "true" 时,脚本只会增加新的定时任务,不会修改其他现有任务,所以任何时候,你都可以自己调整你的crontab.list
AutoAddCron="true"
################################## 定义删除日志的时间(选填) ##################################
## 定义在运行删除旧的日志任务时,要删除多少天以前的日志,请输入正整数,不填则禁用删除日志的功能
RmLogDaysAgo="7"
################################## 定义随机延迟启动任务(选填) ##################################
## 如果任务不是必须准点运行的任务,那么给它增加一个随机延迟,由你定义最大延迟时间,单位为秒,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行
## 在crontab.list中,在每小时第0-2分、第30-31分、第59分这几个时间内启动的任务,均算作必须准点运行的任务,在启动这些任务时,即使你定义了RandomDelay,也将准点运行,不启用随机延迟
## 在crontab.list中,除掉每小时上述时间启动的任务外,其他任务在你定义了 RandomDelay 的情况下,一律启用随机延迟,但如果你按照Wiki教程给某些任务添加了 "now",那么这些任务也将无视随机延迟直接启动
RandomDelay="300"
################################## 定义User-Agent(选填) ##################################
## 自定义LXK9301大佬仓库里京东系列js脚本的User-Agent,不懂不知不会User-Agent的请不要随意填写内容,随意填写了出错概不负责
## 如需使用,请自行解除下一行注释
# export JD_USER_AGENT=""
################################## 定义通知TOKEN(选填) ##################################
## 想通过什么渠道收取通知,就填入对应渠道的值
## 1. ServerChan,教程:http://sc.ftqq.com/3.version
export PUSH_KEY=""
## 2. BARK,教程(看BARK_PUSH和BARK_SOUND的说明):https://gitee.com/lxk0301/jd_scripts/blob/master/githubAction.md
export BARK_PUSH=""
export BARK_SOUND=""
## 3. Telegram,如需使用,TG_BOT_TOKEN和TG_USER_ID必须同时赋值,教程:https://gitee.com/lxk0301/jd_scripts/blob/master/backUp/TG_PUSH.md
export TG_BOT_TOKEN=""
export TG_USER_ID=""
## 4. 钉钉,教程(看DD_BOT_TOKEN和DD_BOT_SECRET部分):https://gitee.com/lxk0301/jd_scripts/blob/master/githubAction.md
export DD_BOT_TOKEN=""
export DD_BOT_SECRET=""
## 5. iGot聚合推送,支持多方式推送,填写iGot的推送key。教程:https://gitee.com/lxk0301/jd_scripts/blob/master/githubAction.md
export IGOT_PUSH_KEY=""
## 6. Push Plus,微信扫码登录后一对一推送或一对多推送,参考文档:http://pushplus.hxtrip.com/
## 其中PUSH_PLUS_USER是一对多推送的“群组编码”(一对多推送下面->您的群组(如无则新建)->群组编码)注:(1、需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送),只填PUSH_PLUS_TOKEN默认为一对一推送
export PUSH_PLUS_TOKEN=""
export PUSH_PLUS_USER=""
## 7. 企业微信机器人消息推送 webhook 后面的 key,文档:https://work.weixin.qq.com/api/doc/90000/90136/91770
export QYWX_KEY=""
## 8. 企业微信应用消息推送的值,教程:https://note.youdao.com/ynoteshare1/index.html?id=351e08a72378206f9dd64d2281e9b83b&type=note
## 依次填上corpid的值,corpsecret的值,touser的值,agentid,media_id的值,注意用,号隔开,例如:"wwcff56746d9adwers,B-791548lnzXBE6_BWfxdf3kSTMJr9vFEPKAbh6WERQ,mingcheng,1000001,2COXgjH2UIfERF2zxrtUOKgQ9XklUqMdGSWLBoW_lSDAdafat"
export QYWX_AM=""
################################## 定义每日签到的通知形式(选填) ##################################
## js脚本每日签到提供3种通知方式,分别为:
## 关闭通知,那么请在下方填入0
## 简洁通知,那么请在下方填入1,其效果见:https://github.com/LXK9301/jd_scripts/blob/master/icon/bean_sign_simple.jpg
## 原始通知,那么请在下方填入2,如果不填也默认为2,内容比较长,这也是默认通知方式
NotifyBeanSign=""
################################## 定义每日签到每个接口间的延迟时间(选填) ##################################
## 默认每个签到接口并发无延迟,如需要依次进行每个接口,请自定义延迟时间,单位为毫秒,延迟作用于每个签到接口, 如填入延迟则切换顺序签到(耗时较长)
export JD_BEAN_STOP=""
################################## 定义东东农场互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MyFruit1=""
MyFruit2=""
MyFruit3=""
MyFruit4=""
MyFruit5=""
MyFruit6=""
MyFruitA=""
MyFruitB=""
ForOtherFruit1=""
ForOtherFruit2=""
ForOtherFruit3=""
ForOtherFruit4=""
ForOtherFruit5=""
ForOtherFruit6=""
################################## 定义东东萌宠互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MyPet1=""
MyPet2=""
MyPet3=""
MyPet4=""
MyPet5=""
MyPet6=""
MyPetA=""
MyPetB=""
ForOtherPet1=""
ForOtherPet2=""
ForOtherPet3=""
ForOtherPet4=""
ForOtherPet5=""
ForOtherPet6=""
################################## 定义种豆得豆互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MyBean1=""
MyBean2=""
MyBean3=""
MyBean4=""
MyBean5=""
MyBean6=""
MyBeanA=""
MyBeanB=""
ForOtherBean1=""
ForOtherBean2=""
ForOtherBean3=""
ForOtherBean4=""
ForOtherBean5=""
ForOtherBean6=""
################################## 定义京喜工厂互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MyDreamFactory1=""
MyDreamFactory2=""
MyDreamFactory3=""
MyDreamFactory4=""
MyDreamFactory5=""
MyDreamFactory6=""
MyDreamFactoryA=""
MyDreamFactoryB=""
ForOtherDreamFactory1=""
ForOtherDreamFactory2=""
ForOtherDreamFactory3=""
ForOtherDreamFactory4=""
ForOtherDreamFactory5=""
ForOtherDreamFactory6=""
################################## 定义东东工厂互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MyJdFactory1=""
MyJdFactory2=""
MyJdFactory3=""
MyJdFactory4=""
MyJdFactory5=""
MyJdFactory6=""
MyJdFactoryA=""
MyJdFactoryB=""
ForOtherJdFactory1=""
ForOtherJdFactory2=""
ForOtherJdFactory3=""
ForOtherJdFactory4=""
ForOtherJdFactory5=""
ForOtherJdFactory6=""
################################## 定义京东赚赚互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MyJdzz1=""
MyJdzz2=""
MyJdzz3=""
MyJdzz4=""
MyJdzz5=""
MyJdzz6=""
MyJdzzA=""
MyJdzzB=""
ForOtherJdzz1=""
ForOtherJdzz2=""
ForOtherJdzz3=""
ForOtherJdzz4=""
ForOtherJdzz5=""
ForOtherJdzz6=""
################################## 定义疯狂的JOY互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MyJoy1=""
MyJoy2=""
MyJoy3=""
MyJoy4=""
MyJoy5=""
MyJoy6=""
MyJoyA=""
MyJoyB=""
ForOtherJoy1=""
ForOtherJoy2=""
ForOtherJoy3=""
ForOtherJoy4=""
ForOtherJoy5=""
ForOtherJoy6=""
################################## 定义京喜农场互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
## 京喜农场助力码为 JSON 格式因此使用单引号,json 格式如下
## {"smp":"22bdadsfaadsfadse8a","active":"jdnc_1_btorange210113_2","joinnum":"1"}
## 助力码获取可以通过 js jd_get_share_code now 命令获取
## 注意:京喜农场 种植种子发生变化的时候,互助码也会变!!
MyJxnc1=''
MyJxnc2=''
MyJxnc3=''
MyJxnc4=''
MyJxnc5=''
MyJxnc6=''
MyJxncA=''
MyJxncB=''
ForOtherJxnc1=""
ForOtherJxnc2=""
ForOtherJxnc3=""
ForOtherJxnc4=""
ForOtherJxnc5=""
ForOtherJxnc6=""
################################## 定义京喜农场TOKEN(选填) ##################################
## 如果某个Cookie的账号种植的是app种子,则必须填入有效的TOKEN;而种植非app种子则不需要TOKEN
## TOKEN的形式:{"farm_jstoken":"749a90f871adsfads8ffda7bf3b1576760","timestamp":"1610165423873","phoneid":"42c7e3dadfadsfdsaac-18f0e4f4a0cf"}
## 因TOKEN中带有双引号,因此,变量值两侧必须由一对单引号引起来
## TOKEN如何获取请阅读以下文件的注释:https://github.com/LXK9301/jd_scripts/blob/master/jd_jxnc.js
TokenJxnc1=''
TokenJxnc2=''
TokenJxnc3=''
TokenJxnc4=''
TokenJxnc5=''
TokenJxnc6=''
################################## 定义口袋书店互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MyBookShop1=""
MyBookShop2=""
MyBookShop3=""
MyBookShop4=""
MyBookShop5=""
MyBookShop6=""
MyBookShopA=""
MyBookShopB=""
ForOtherBookShop1=""
ForOtherBookShop2=""
ForOtherBookShop3=""
ForOtherBookShop4=""
ForOtherBookShop5=""
ForOtherBookShop6=""
################################## 定义签到领现金互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MyCash1=""
MyCash2=""
MyCash3=""
MyCash4=""
MyCash5=""
MyCash6=""
MyCashA=""
MyCashB=""
ForOtherCash1=""
ForOtherCash2=""
ForOtherCash3=""
ForOtherCash4=""
ForOtherCash5=""
ForOtherCash6=""
################################## 定义闪购盲盒互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MySgmh1=""
MySgmh2=""
MySgmh3=""
MySgmh4=""
MySgmh5=""
MySgmh6=""
MySgmhA=""
MySgmhB=""
ForOtherSgmh1=""
ForOtherSgmh2=""
ForOtherSgmh3=""
ForOtherSgmh4=""
ForOtherSgmh5=""
ForOtherSgmh6=""
################################## 定义京东环球挑战赛活动互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
## 仅2021年2月22日(含)前有效
MyGLOBAL1=""
MyGLOBAL2=""
MyGLOBAL3=""
MyGLOBAL4=""
MyGLOBAL5=""
MyGLOBAL6=""
MyGLOBALA=""
MyGLOBALB=""
ForOtherGLOBAL1=""
ForOtherGLOBAL2=""
ForOtherGLOBAL3=""
ForOtherGLOBAL4=""
ForOtherGLOBAL5=""
ForOtherGLOBAL6=""
################################## 定义惊喜财富岛互助(选填) ##################################
## 具体填法及要求详见本文件最下方“互助码填法示例”
MyJdcfd1=""
MyJdcfd2=""
MyJdcfd3=""
MyJdcfd4=""
MyJdcfd5=""
MyJdcfd6=""
MyJdcfdA=""
MyJdcfdB=""
ForOtherJdcfd1=""
ForOtherJdcfd2=""
ForOtherJdcfd3=""
ForOtherJdcfd4=""
ForOtherJdcfd5=""
ForOtherJdcfd6=""
################################## 定义东东超市蓝币兑换数量(选填) ##################################
## 东东超市蓝币兑换,可用值包括:
## 一、0:表示不兑换京豆,这也是js脚本的默认值
## 二、20:表示兑换20个京豆
## 三、1000:表示兑换1000个京豆
## 四、可兑换清单的商品名称,输入能跟唯一识别出来的关键词即可,比如:MARKET_COIN_TO_BEANS="抽纸"
## 注意:有些比较贵的实物商品京东只是展示出来忽悠人的,即使你零点用脚本去抢,也会提示没有或提示已下架
export MARKET_COIN_TO_BEANS="0"
################################## 定义东东超市蓝币成功兑换奖品是否静默运行(选填) ##################################
## 默认 "false" 关闭(即:奖品兑换成功后会发出通知提示),如需要静默运行不发出通知,请改为 "true"
export MARKET_REWARD_NOTIFY=""
################################## 定义东东超市是否自动升级商品和货架(选填) ##################################
## 升级顺序:解锁升级商品、升级货架,默认 "true" 自动升级,如需关闭自动升级,请改为 "false"
export SUPERMARKET_UPGRADE=""
################################## 定义东东超市是否自动更换商圈(选填) ##################################
## 小于对方300热力值自动更换商圈队伍,默认 "true" 自动更换,如不想更换商圈,请改为 "false"
## 目前已无此功能,申明了也无效
# export BUSINESS_CIRCLE_JUMP=""
################################## 定义东东超市是否自动使用金币去抽奖(选填) ##################################
## 是否用金币去抽奖,默认 "false" 关闭,如需开启,请修改为 "true"
export SUPERMARKET_LOTTERY=""
################################## 定义东东超市是否自动参加PK队伍(选填) ##################################
## 是否每次PK活动参加脚本作者创建的PK队伍,"true" 表示参加,"false" 表示不参加,默认为 "true"
export JOIN_PK_TEAM=""
################################## 定义东东农场是否静默运行(选填) ##################################
## 默认为 "false",不静默,发送推送通知消息,如不想收到通知,请修改为 "true"
## 如果你不想完全关闭或者完全开启通知,只想在特定的时间发送通知,可以参考下面的“定义东东萌宠是否静默运行”部分,设定几个if判断条件
export FRUIT_NOTIFY_CONTROL=""
################################## 定义东东农场是否使用水滴换豆卡(选填) ##################################
## 如果出现限时活动时100g水换20豆,此时比浇水划算,"true" 表示换豆(不浇水),"false" 表示不换豆(继续浇水),默认是"false"
## 如需切换为换豆(不浇水),请修改为 "true"
export FRUIT_BEAN_CARD=""
################################## 定义宠汪汪喂食克数(选填) ##################################
## 你期望的宠汪汪每次喂食克数,只能填入10、20、40、80,默认为10
## 如实际持有食物量小于所设置的克数,脚本会自动降一档,直到降无可降
## 具体情况请自行在宠汪汪游戏中去查阅攻略
export JOY_FEED_COUNT=""
################################## 定义宠汪汪兑换京豆数量(选填) ##################################
## 目前的可用值包括:0、20、500、1000,其中0表示为不自动兑换京豆,如不设置,将默认为"20"
## 不同等级可兑换不同数量的京豆,详情请见宠汪汪游戏中兑换京豆选项
## 500、1000的京豆每天有总量限制,设置了并且你也有足够积分时,也并不代表就一定能抢到
export JD_JOY_REWARD_NAME=""
################################## 定义宠汪汪兑换京豆是否静默运行(选填) ##################################
## 默认为 "false",在成功兑换京豆时将发送推送通知消息(失败不发送),如想要静默不发送通知,请修改为 "true"
export JD_JOY_REWARD_NOTIFY=""
################################## 定义宠汪汪是否自动给好友的汪汪喂食(选填) ##################################
## 默认 "false" 不会自动给好友的汪汪喂食,如想自动喂食,请改成 "true"
export JOY_HELP_FEED=""
################################## 定义宠汪汪是否自动报名宠物赛跑(选填) ##################################
## 默认 "true" 参加宠物赛跑,如需关闭,请改成 "false"
export JOY_RUN_FLAG=""
################################## 定义宠汪汪参加比赛类型(选填) ##################################
## 当JOY_RUN_FLAG不设置或设置为 "true" 时生效
## 可选值:2,10,50,其他值不可以。其中2代表参加双人PK赛,10代表参加10人突围赛,50代表参加50人挑战赛,不填时默认为2
## 各个账号间请使用 & 分隔,比如:JOY_TEAM_LEVEL="2&2&50&10"
## 如果你有5个账号但只写了四个数字,那么第5个账号将默认参加2人赛,账号如果更多,与此类似
export JOY_TEAM_LEVEL=""
################################## 定义宠汪汪赛跑获胜后是否推送通知(选填) ##################################
## 控制jd_joy.js脚本宠汪汪赛跑获胜后是否推送通知,"false" 为否(不推送通知消息)"true" 为是(即:发送推送通知消息),默认为 "true"
export JOY_RUN_NOTIFY=""
################################## 定义摇钱树是否自动将金果卖出变成金币(选填) ##################################
## 金币有时效,默认为 "false",不卖出金果为金币,如想希望自动卖出,请修改为 "true"
export MONEY_TREE_SELL_FRUIT=""
################################## 定义东东萌宠是否静默运行(选填) ##################################
## 默认 "false"(不静默,发送推送通知消息),如想静默请修改为 true
## 每次执行脚本通知太频繁了,改成只在周三和周六中午那一次运行时发送通知提醒
## 除掉上述提及时间之外,均设置为 true,静默不发通知
## 特别说明:针对北京时间有效。
if [ $(date "+%w") -eq 6 ] && [ $(date "+%H") -ge 9 ] && [ $(date "+%H") -lt 14 ]; then
export PET_NOTIFY_CONTROL="false"
elif [ $(date "+%w") -eq 3 ] && [ $(date "+%H") -ge 9 ] && [ $(date "+%H") -lt 14 ]; then
export PET_NOTIFY_CONTROL="false"
else
export PET_NOTIFY_CONTROL="true"
fi
################################## 定义京喜工厂控制哪个京东账号不运行此脚本(选填) ##################################
## 输入"1"代表第一个京东账号不运行,多个使用 & 连接,例:"1&3" 代表账号1和账号3不运行京喜工厂脚本,注:输入"0",代表全部账号不运行京喜工厂脚本
## 如果使用了 “临时屏蔽某个Cookie” TempBlockCookie 功能,编号会发生变化
export DREAMFACTORY_FORBID_ACCOUNT=""
################################## 定义东东工厂控制哪个京东账号不运行此脚本(选填) ##################################
## 输入"1"代表第一个京东账号不运行,多个使用 & 连接,例:"1&3" 代表账号1和账号3不运行东东工厂脚本,注:输入"0",代表全部账号不运行东东工厂脚本
## 如果使用了 “临时屏蔽某个Cookie” TempBlockCookie 功能,编号会发生变化
export JDFACTORY_FORBID_ACCOUNT=""
################################## 定义东东工厂心仪的商品(选填) ##################################
## 只有在满足以下条件时,才自动投入电力:一是存储的电力满足生产商品所需的电力,二是心仪的商品有库存,如果没有输入心仪的商品,那么当前你正在生产的商品视作心仪的商品
## 如果你看不懂上面的话,请去东东工厂游戏中查阅攻略
## 心仪的商品请输入商品的全称或能唯一识别出该商品的关键字
export FACTORAY_WANTPRODUCT_NAME=""
################################## 定义京喜农场通知级别(选填) ##################################
## 可用值: 0(不通知); 1(本次获得水滴>0); 2(任务执行); 3(任务执行+未种植种子),默认为"3"
export JXNC_NOTIFY_LEVEL="3"
################################## 定义取关参数(选填) ##################################
## jd_unsubscribe这个任务是用来取关每天做任务关注的商品和店铺,默认在每次运行时取关20个商品和20个店铺
## 如果取关数量不够,可以根据情况增加,还可以设置 jdUnsubscribeStopGoods 和 jdUnsubscribeStopShop
## 商品取关数量
goodPageSize=""
## 店铺取关数量
shopPageSize=""
## 遇到此商品不再取关此商品以及它后面的商品,需去商品详情页长按拷贝商品信息
jdUnsubscribeStopGoods=""
## 遇到此店铺不再取关此店铺以及它后面的店铺,请从头开始输入店铺名称
jdUnsubscribeStopShop=""
################################## 疯狂的JOY(选填) ##################################
## 疯狂的JOY循环助力,"true" 表示循环助力,"false" 表示不循环助力,默认 "false"
export JDJOY_HELPSELF=""
## 疯狂的JOY京豆兑换,目前最小值为500/1000京豆,默认为 "0" 不开启京豆兑换
export JDJOY_APPLYJDBEAN=""
## 疯狂的JOY自动购买什么等级的JOY,如需要使用请自行解除注释
# export BUY_JOY_LEVEL=""
################################## 定义是否自动加购物车(选填) ##################################
## 口袋书店和东东小窝有些任务需要将商品加进购物车才能完成,默认 "false" 不做这些任务,如想做,请设置为 "true"
export PURCHASE_SHOPS=""
################################## 定义京喜财富岛通知(选填) ##################################
## 输入true为通知,默认 "false",不填则为不通知
export CFD_NOTIFY_CONTROL=""
################################## 定义京小兑是否自动把抽奖卷兑换为兑币,(选填) ##################################
## 输入true为自动兑换,不填则为不兑换
export JD_JXD_EXCHANGE=""
################################## 定义宠汪汪是否开启内部互助,(选填) ##################################
## 输入true为开启内部互助,不填默认关闭
export JOY_RUN_HELP_MYSELF=""
################################## Telegram 代理(选填) ##################################
## Telegram 代理的 IP,代理类型为 http,比如你代理是 http://127.0.0.1:1080,则填写 "127.0.0.1"
## 如需使用,请自行解除下一行的注释
# export TG_PROXY_HOST=""
## Telegram 代理的端口,代理类型为 http,比如你代理是 http://127.0.0.1:1080,则填写 "1080"
## 如需使用,请自行解除下一行的注释
# export TG_PROXY_PORT=""
################################## 是否添加DIY脚本(选填) ##################################
## 如果你自己会写shell脚本,并且希望在每次git_pull.sh这个脚本运行时,额外运行你的DIY脚本,请赋值为 "true"
## 同时,请务必将你的脚本命名为 extra.sh (只能叫这个文件名),放在 config 目录下
## 仓库下已经上传extra.sh模板,如果你想使用;可以参考本仓库下 extra.sh 文件;地址:https://raw.githubusercontent.com/dockere/jd-base/master/sample/diy.sh
EnableExtraShell=""
################################## 互助码填法示例 ##################################
## **互助码是填在My系列变量中的,ForOther系统变量中只要填入My系列的变量名即可,按注释中的例子拼接,以东东农场为例,如下所示。**
## **实际上东东农场一个账号只能给别人助力3次,我多写的话,只有前几个会被助力。但如果前面的账号获得的助力次数已经达到上限了,那么还是会尝试继续给余下的账号助力,所以多填也是有意义的。**
## **ForOther系列变量必须从1开始编号,依次编下去。**
# MyFruit1="e6e04602d5e343258873af1651b603ec" # 这是Cookie1这个账号的互助码
# MyFruit2="52801b06ce2a462f95e1d59d7e856ef4" # 这是Cookie2这个账号的互助码
# MyFruit3="e2fd1311229146cc9507528d0b054da8" # 这是Cookie3这个账号的互助码
# MyFruit4="6dc9461f662d490991a31b798f624128" # 这是Cookie4这个账号的互助码
# MyFruit5="30f29addd75d44e88fb452bbfe9f2110" # 这是Cookie5这个账号的互助码
# MyFruit6="1d02fc9e0e574b4fa928e84cb1c5e70b" # 这是Cookie6这个账号的互助码
# MyFruitA="5bc73a365ff74a559bdee785ea97fcc5" # 这是我和别人交换互助,另外一个用户A的互助码
# MyFruitB="6d402dcfae1043fba7b519e0d6579a6f" # 这是我和别人交换互助,另外一个用户B的互助码
# MyFruitC="5efc7fdbb8e0436f8694c4c393359576" # 这是我和别人交换互助,另外一个用户C的互助码
# ForOtherFruit1="${MyFruit2}@${MyFruitB}@${MyFruit4}" # Cookie1这个账号助力Cookie2的账号的账号、Cookie4的账号以及用户B
# ForOtherFruit2="${MyFruit1}@${MyFruitA}@${MyFruit4}" # Cookie2这个账号助力Cookie1的账号的账号、Cookie4的账号以及用户A
# ForOtherFruit3="${MyFruit1}@${MyFruit2}@${MyFruitC}@${MyFruit4}@${MyFruitA}@${MyFruit6}" # 解释同上,东东农场实际上只能助力3次
# ForOtherFruit4="${MyFruit1}@${MyFruit2}@${MyFruit3}@${MyFruitC}@${MyFruit6}@${MyFruitA}" # 解释同上,东东农场实际上只能助力3次
# ForOtherFruit5="${MyFruit1}@${MyFruit2}@${MyFruit3}@${MyFruitB}@${MyFruit4}@${MyFruit6}@${MyFruitC}@${MyFruitA}"
# ForOtherFruit6="${MyFruit1}@${MyFruit2}@${MyFruit3}@${MyFruitA}@${MyFruit4}@${MyFruit5}@${MyFruitC}"
-27
View File
@@ -1,27 +0,0 @@
# 更新shell及面板代码
55 2,13 * * * git_pull >> ${QL_DIR}/log/git_pull.log 2>&1
# 更新你需要的仓库的代码
# diy test作者 test仓库 test路径1|test路径2 黑名单1文件名|黑名单2文件名
33 * * * * diy whyour hundun "quanx/jx|quanx/jd" tokens >> ${QL_DIR}/log/diy_pull.log 2>&1
31 * * * * diy monk-coder dust i-chenzhe >> ${QL_DIR}/log/diy_pull.log 2>&1
# 重新build面板
# 30 7 */7 * * rebuild >> ${QL_DIR}/log/rebuild.log 2>&1
# 删除 RmLogDaysAgo 指定天数以前的旧日志,本行为不记录日志
57 13 * * * rm_log >/dev/null 2>&1
# 导出所有互助码清单,日志在log/export_sharecodes下
48 5 * * * export_sharecodes
# 重启挂机脚本
# 33 3 * * * js hangup
# 重置密码
# 33 6 */7 * * js resetpwd
# 运行所有脚本(慎用)
# 33 9 */7 * * js runall
# 其他定时任务
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
## 添加你需要重启自动执行的任意命令,比如 ql repo
## 安装node依赖使用 pnpm install -g xxx xxx
## 安装python依赖使用 pip3 install xxx
+862
View File
File diff suppressed because one or more lines are too long
+540
View File
@@ -0,0 +1,540 @@
#!/usr/bin/env python3
# _*_ coding:utf-8 _*_
import base64
import hashlib
import hmac
import json
import os
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)
# 通知服务
# fmt: off
push_config = {
'HITOKOTO': False, # 启用一言(随机句子)
'BARK_PUSH': '', # bark IP 或设备码,例:https://api.day.app/DxHcxxxxxRxxxxxxcm/
'BARK_ARCHIVE': '', # bark 推送是否存档
'BARK_GROUP': '', # bark 推送分组
'BARK_SOUND': '', # bark 推送声音
'CONSOLE': True, # 控制台输出
'DD_BOT_SECRET': '', # 钉钉机器人的 DD_BOT_SECRET
'DD_BOT_TOKEN': '', # 钉钉机器人的 DD_BOT_TOKEN
'FSKEY': '', # 飞书机器人的 FSKEY
'GOBOT_URL': '', # go-cqhttp
# 推送到个人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
print("bark 服务启动")
if push_config.get("BARK_PUSH").startswith("http"):
url = f'{push_config.get("BARK_PUSH")}/{urllib.parse.quote_plus(title)}/{urllib.parse.quote_plus(content)}'
else:
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()
if response["code"] == 200:
print("bark 推送成功!")
else:
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
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 = {
"token": push_config.get("PUSH_PLUS_TOKEN"),
"title": title,
"content": content,
"topic": push_config.get("PUSH_PLUS_USER"),
}
body = json.dumps(data).encode(encoding="utf-8")
headers = {"Content-Type": "application/json"}
response = requests.post(url=url, data=body, headers=headers).json()
if response["code"] == 200:
print("PUSHPLUS 推送成功!")
else:
url_old = "http://pushplus.hxtrip.com/send"
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
print("qmsg 服务启动")
url = f'https://qmsg.zendee.cn/{push_config.get("QMSG_TYPE")}/{push_config.get("QMSG_KEY")}'
payload = {"msg": f'{title}\n\n{content.replace("----", "-")}'.encode("utf-8")}
response = requests.post(url=url, params=payload).json()
if response["code"] == 0:
print("qmsg 推送成功!")
else:
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
QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM"))
if 4 < len(QYWX_AM_AY) > 5:
print("QYWX_AM 设置错误!!\n取消推送")
return
print("企业微信 APP 服务启动")
corpid = QYWX_AM_AY[0]
corpsecret = QYWX_AM_AY[1]
touser = QYWX_AM_AY[2]
agentid = QYWX_AM_AY[3]
try:
media_id = QYWX_AM_AY[4]
except IndexError:
media_id = ""
wx = WeCom(corpid, corpsecret, agentid)
# 如果没有配置 media_id 默认就以 text 方式发送
if not media_id:
message = title + "\n\n" + content
response = wx.send_text(message, touser)
else:
response = wx.send_mpnews(title, content, media_id, touser)
if response == "ok":
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": title,
"thumb_media_id": media_id,
"author": "Author",
"content_source_url": "",
"content": message.replace("\n", "<br/>"),
"digest": message,
}
]
},
}
send_msges = bytes(json.dumps(send_values), "utf-8")
respone = requests.post(send_url, send_msges)
respone = respone.json()
return respone["errmsg"]
def wecom_bot(title: str, content: str) -> None:
"""
通过 企业微信机器人 推送消息。
"""
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:
"""
url = "https://v1.hitokoto.cn/"
res = requests.get(url).json()
return res["hitokoto"] + " ----" + res["from"]
if push_config.get("BARK_PUSH"):
notify_function.append(bark)
if push_config.get("CONSOLE"):
notify_function.append(console)
if push_config.get("DD_BOT_TOKEN") and push_config.get("DD_BOT_SECRET"):
notify_function.append(dingding_bot)
if push_config.get("FSKEY"):
notify_function.append(feishu_bot)
if push_config.get("GOBOT_URL") and push_config.get("GOBOT_QQ"):
notify_function.append(go_cqhttp)
if push_config.get("GOTIFY_URL") and push_config.get("GOTIFY_TOKEN"):
notify_function.append(gotify)
if push_config.get("IGOT_PUSH_KEY"):
notify_function.append(iGot)
if push_config.get("PUSH_KEY"):
notify_function.append(serverJ)
if push_config.get("PUSH_PLUS_TOKEN"):
notify_function.append(pushplus_bot)
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
notify_function.append(qmsg_bot)
if push_config.get("QYWX_AM"):
notify_function.append(wecom_app)
if push_config.get("QYWX_KEY"):
notify_function.append(wecom_bot)
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
notify_function.append(telegram_bot)
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():
send("title", "content")
if __name__ == "__main__":
main()
+16
View File
@@ -0,0 +1,16 @@
{
"name": "dependence",
"author": "",
"license": "ISC",
"dependencies": {
"crypto-js": "^4.0.0",
"download": "^8.0.0",
"http-server": "^0.12.3",
"got": "^11.5.1",
"qrcode-terminal": "^0.12.0",
"request": "^2.88.2",
"tough-cookie": "^4.0.0",
"tunnel": "0.0.6",
"ws": "^7.4.3"
}
}
+1
View File
@@ -0,0 +1 @@
#!/usr/bin/env bash
+168 -59
View File
@@ -1,70 +1,179 @@
#!/usr/bin/env bash #!/usr/bin/env bash
get_token() { get_token() {
token=$(cat $AuthConf | jq -r .token) token=$(cat $file_auth_user | jq -r .token)
}
get_json_value() {
local json=$1
local key=$2
if [[ -z "$3" ]]; then
local num=1
else
local num=$3
fi
local value=$(echo "${json}" | awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/'${key}'\042/){print $(i+1)}}}' | tr -d '"' | sed -n ${num}p)
echo ${value}
} }
add_cron_api() { 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}')
else else
local schedule=$1 local schedule=$1
local command=$2 local command=$2
local name=$3 local name=$3
fi fi
local api=$(curl "http://localhost:5600/api/crons?t=$currentTimeStamp" \ local api=$(
-H "Accept: application/json" \ curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?t=$currentTimeStamp" \
-H "Authorization: Bearer $token" \ -H "Accept: application/json" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ -H "Authorization: Bearer $token" \
-H "Content-Type: application/json;charset=UTF-8" \ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Origin: http://localhost:5700" \ -H "Content-Type: application/json;charset=UTF-8" \
-H "Referer: http://localhost:5700/crontab" \ -H "Origin: http://0.0.0.0:5700" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Referer: http://0.0.0.0:5700/crontab" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--compressed) --data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \
code=$(echo $api | jq -r .code) --compressed
if [[ $code == 200 ]]; then )
echo -e "$name 添加成功" code=$(echo $api | jq -r .code)
else message=$(echo $api | jq -r .message)
echo -e "$name 添加失败" if [[ $code == 200 ]]; then
fi echo -e "$name -> 添加成功"
else
echo -e "$name -> 添加失败(${message})"
fi
}
update_cron_api() {
local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}')
local id=$(echo "$1" | awk -F ":" '{print $4}')
else
local schedule=$1
local command=$2
local name=$3
local id=$4
fi
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"_id\":\"$id\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$name -> 更新成功"
else
echo -e "$name -> 更新失败(${message})"
fi
}
update_cron_command_api() {
local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then
local command=$(echo "$1" | awk -F ":" '{print $1}')
local id=$(echo "$1" | awk -F ":" '{print $2}')
else
local command=$1
local id=$2
fi
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"command\":\"$command\",\"_id\":\"$id\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$command -> 更新成功"
else
echo -e "$command -> 更新失败(${message})"
fi
} }
del_cron_api() { del_cron_api() {
local id=$1 local ids=$1
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$(curl "http://localhost:5600/api/crons/$id?t=$currentTimeStamp" \ local api=$(
-X 'DELETE' \ curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?t=$currentTimeStamp" \
-H "Accept: application/json" \ -X 'DELETE' \
-H "Authorization: Bearer $token" \ -H "Accept: application/json" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ -H "Authorization: Bearer $token" \
-H "Content-Type: application/json;charset=UTF-8" \ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Origin: http://localhost:5700" \ -H "Content-Type: application/json;charset=UTF-8" \
-H "Referer: http://localhost:5700/crontab" \ -H "Origin: http://0.0.0.0:5700" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7") -H "Referer: http://0.0.0.0:5700/crontab" \
code=$(echo $api | jq -r .code) -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
if [[ $code == 200 ]]; then --data-raw "[$ids]" \
echo -e "$name 删除成功" --compressed
else )
echo -e "$name 删除失败" code=$(echo $api | jq -r .code)
fi message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "成功"
else
echo -e "失败(${message})"
fi
} }
get_user_info() {
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/api/user?t=$currentTimeStamp" \
-H 'Accept: */*' \
-H "Authorization: Bearer $token" \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \
-H 'Referer: http://0.0.0.0:5700/crontab' \
-H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \
--compressed
)
code=$(echo $api | jq -r .code)
if [[ $code != 200 ]]; then
echo -e "请先登录!"
exit 0
fi
}
update_cron() {
local ids="$1"
local status="$2"
local pid="${3:-''}"
local logPath="$4"
local lastExecutingTime="${5:-0}"
local runningTime="${6:-0}"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons/status?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code != 200 ]]; then
echo -e "\n## 更新任务状态失败(${message})\n" >> $log_path
fi
}
get_token
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=/ql/shell
. $dir_shell/share.sh
url="${github_proxy_url}https://github.com/SuMaiKaDe/bot.git"
repo_path="${dir_repo}/dockerbot"
echo -e "\n1、安装bot依赖...\n"
apk --no-cache add -f zlib-dev gcc jpeg-dev python3-dev musl-dev freetype-dev
echo -e "\nbot依赖安装成功...\n"
echo -e "2、下载bot所需文件...\n"
if [[ ! -d ${repo_path}/.git ]]; then
rm -rf ${repo_path}
git_clone_scripts ${url} ${repo_path} "main"
fi
cp -rf "$repo_path/jbot" $dir_root
if [[ ! -f "$dir_root/config/bot.json" ]]; then
cp -f "$repo_path/config/bot.json" "$dir_root/config"
fi
echo -e "\nbot文件下载成功...\n"
echo -e "3、安装python3依赖...\n"
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
cp -f "$repo_path/jbot/requirements.txt" "$dir_root"
cd $dir_root
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 "4、启动bot程序...\n"
make_dir $dir_log/bot
cd $dir_root
ps -ef | grep "python3 -m jbot" | grep -v grep | awk '{print $1}' | xargs kill -9 2>/dev/null
nohup python3 -m jbot >$dir_log/bot/nohup.log 2>&1 &
echo -e "bot启动成功...\n"
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env bash
dir_shell=/ql/shell
. $dir_shell/share.sh
. $dir_shell/api.sh
reset_env() {
echo -e "---> 1. 开始检测配置文件\n"
fix_config
echo -e "---> 配置文件检测完成\n"
echo -e "---> 2. 开始安装青龙依赖\n"
rm -rf $dir_root/node_modules
npm_install_2 $dir_root
echo -e "---> 青龙依赖安装完成\n"
echo -e "---> 3. 开始安装脚本依赖\n"
cp -f $dir_sample/package.json $dir_scripts/package.json
rm -rf $dir_scripts/node_modules
npm_install_2 $dir_scripts
echo -e "---> 脚本依赖安装完成\n"
}
copy_dep() {
echo -e "---> 1. 复制通知文件\n"
echo -e "---> 复制一份 $file_notify_py_sample$file_notify_py\n"
cp -fv $file_notify_py_sample $file_notify_py
echo
echo -e "---> 复制一份 $file_notify_js_sample$file_notify_js\n"
cp -fv $file_notify_js_sample $file_notify_js
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() {
pm2 l &>/dev/null
pm2 delete panel --source-map-support --time &>/dev/null
pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null
pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null
}
pm2_log() {
echo -e "---> pm2日志"
local panelOut="/root/.pm2/logs/panel-out.log"
local panelError="/root/.pm2/logs/panel-error.log"
tail -n 20 "$panelOut"
tail -n 20 "$panelError"
}
check_nginx() {
local nginxPid=$(ps -ef | grep nginx | grep -v grep)
echo -e "=====> 检测nginx服务\n$nginxPid"
if [[ $nginxPid ]]; then
echo -e "\n=====> nginx服务正常\n"
nginx -s reload
else
echo -e "\n=====> nginx服务异常,重新启动nginx\n"
nginx -c /etc/nginx/nginx.conf
fi
}
check_ql() {
local api=$(curl -s --noproxy "*" "http://0.0.0.0:5700")
echo -e "\n=====> 检测面板\n\n$api\n"
if [[ $api =~ "<div id=\"root\"></div>" ]]; then
echo -e "=====> 面板服务启动正常\n"
else
echo -e "=====> 面板服务异常,重置基础环境\n"
reset_env
fi
}
check_pm2() {
pm2_log
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/api/user?t=$currentTimeStamp" \
-H 'Accept: */*' \
-H "Authorization: Bearer $token" \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \
-H 'Referer: http://0.0.0.0:5700/crontab' \
-H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \
--compressed
)
echo -e "\n=====> 检测后台\n\n$api\n"
if [[ $api =~ "{\"code\"" ]]; then
echo -e "=====> 后台服务启动正常\n"
else
echo -e "=====> 后台服务异常,重置基础环境并重启后台\n"
reset_env
fi
}
main() {
echo -e "=====> 开始检测"
copy_dep
check_ql
check_nginx
check_pm2
reload_pm2
echo -e "\n=====> 检测结束\n"
}
main
-141
View File
@@ -1,141 +0,0 @@
#!/usr/bin/env bash
## 路径、环境判断
ShellDir=${QL_DIR:-$(cd $(dirname $0); pwd)}
LogDir=${ShellDir}/log
ConfigDir=${ShellDir}/config
FileConf=${ConfigDir}/config.sh
CookieConf=${ConfigDir}/cookie.sh
[[ ${ANDROID_RUNTIME_ROOT}${ANDROID_ROOT} ]] && Opt="P" || Opt="E"
Tips="从日志中未找到任何互助码"
## 所有有互助码的活动,只需要把脚本名称去掉前缀 jd_ 后列在 Name1 中,将其中文名称列在 Name2 中,对应 config.sh 中互助码后缀列在 Name3 中即可。
## Name1、Name2 和 Name3 中的三个名称必须一一对应。
Name1=(fruit pet plantBean dreamFactory jdfactory crazy_joy jdzz jxnc bookshop cash sgmh cfd global)
Name2=(东东农场 东东萌宠 京东种豆得豆 京喜工厂 东东工厂 crazyJoy任务 京东赚赚 京喜农场 口袋书店 签到领现金 闪购盲盒 京喜财富岛 环球挑战赛)
Name3=(Fruit Pet Bean DreamFactory JdFactory Joy Jdzz Jxnc BookShop Cash Sgmh Cfd Global)
## 导入 config.sh
function Import_Conf {
if [ -f ${FileConf} ]
then
. ${CookieConf}
. ${FileConf}
if [ ! -s ${CookieConf} ]; then
echo -e "请先在Cookie管理中添加一条Cookie...\n"
exit 1
fi
else
echo -e "配置文件 ${FileConf} 不存在,请先按教程配置好该文件...\n"
exit 1
fi
}
## 用户数量 UserSum
function Count_UserSum {
UserSum=$(awk '{print NR}' ${CookieConf} | tail -n1)
}
## 导出互助码的通用程序
function Cat_Scodes {
if [ -d ${LogDir}/jd_$1 ] && [[ $(ls ${LogDir}/jd_$1) != "" ]]; then
cd ${LogDir}/jd_$1
## 导出助力码变量(My
for log in $(ls -r); do
case $# in
2)
codes=$(cat ${log} | grep -${Opt} "开始【京东账号|您的(好友)?助力码为" | uniq | perl -0777 -pe "{s|\*||g; s|开始||g; s|\n您的(好友)?助力码为()?:?||g; s|.+||g}" | sed -r "s/【京东账号/My$2/;s/】.*?/='/;s/】.*?/='/;s/$/'/;s/\(每次运行都变化,不影响\)//")
;;
3)
codes=$(grep -${Opt} $3 ${log} | uniq | sed -r "s/【京东账号/My$2/;s/.*?】/='/;s/$/'/")
;;
esac
if [[ ${codes} ]]; then
## 添加判断,若未找到该用户互助码,则设置为空值
for ((user_num=1;user_num<=${UserSum};user_num++)); do
echo -e "${codes}" | grep -${Opt}q "My$2${user_num}="
if [ $? -eq 1 ]; then
if [ $user_num == 1 ]; then
codes=$(echo "${codes}" | sed -r "1i My${2}1=''")
else
codes=$(echo "${codes}" | sed -r "/My$2$(expr ${user_num} - 1)=/a\My$2${user_num}=''")
fi
fi
done
break
fi
done
## 导出为他人助力变量(ForOther)
if [[ ${codes} ]]; then
help_code=""
for ((user_num=1;user_num<=${UserSum};user_num++)); do
echo -e "${codes}" | grep -${Opt}q "My$2${user_num}=''"
if [ $? -eq 1 ]; then
help_code=${help_code}"\${My"$2${user_num}"}@"
fi
done
## 生成互助规则模板
for_other_codes=""
case $HelpType in
0) ### 统一优先级助力模板
new_code=$(echo ${help_code} | sed "s/@$//")
for ((user_num=1;user_num<=${UserSum};user_num++)); do
if [ $user_num == 1 ]; then
for_other_codes=${for_other_codes}"ForOther"$2${user_num}"=\""${new_code}"\"\n"
else
for_other_codes=${for_other_codes}"ForOther"$2${user_num}"=\"\${ForOther"${2}1"}\"\n"
fi
done
;;
1) ### 均匀助力模板
for ((user_num=1;user_num<=${UserSum};user_num++)); do
echo ${help_code} | grep "\${My"$2${user_num}"}@" > /dev/null
if [ $? -eq 0 ]; then
left_str=$(echo ${help_code} | sed "s/\${My$2${user_num}}@/ /g" | awk '{print $1}')
right_str=$(echo ${help_code} | sed "s/\${My$2${user_num}}@/ /g" | awk '{print $2}')
mark="\${My$2${user_num}}@"
else
left_str=$(echo ${help_code} | sed "s/${mark}/ /g" | awk '{print $1}')${mark}
right_str=$(echo ${help_code} | sed "s/${mark}/ /g" | awk '{print $2}')
fi
new_code=$(echo ${right_str}${left_str} | sed "s/@$//")
for_other_codes=${for_other_codes}"ForOther"$2${user_num}"=\""${new_code}"\"\n"
done
;;
*) ### 普通优先级助力模板
for ((user_num=1;user_num<=${UserSum};user_num++)); do
new_code=$(echo ${help_code} | sed "s/\${My"$2${user_num}"}@//;s/@$//")
for_other_codes=${for_other_codes}"ForOther"$2${user_num}"=\""${new_code}"\"\n"
done
;;
esac
echo -e "${codes}\n\n${for_other_codes}" | sed s/[[:space:]]//g
else
echo ${Tips}
fi
else
echo "未运行过 jd_$1 脚本,未产生日志"
fi
}
## 汇总
function Cat_All {
echo -e "\n从最后一个日志提取互助码,受日志内容影响,仅供参考。"
for ((i=0; i<${#Name1[*]}; i++)); do
echo -e "\n${Name2[i]}"
[[ $(Cat_Scodes "${Name1[i]}" "${Name3[i]}" "${Name2[i]}好友互助码") == ${Tips} ]] && Cat_Scodes "${Name1[i]}" "${Name3[i]}" || Cat_Scodes "${Name1[i]}" "${Name3[i]}" "${Name2[i]}好友互助码"
done
}
## 执行并写入日志
LogTime=$(date "+%Y-%m-%d-%H-%M-%S")
LogFile="${LogDir}/export_sharecodes/${LogTime}.log"
[ ! -d "${LogDir}/export_sharecodes" ] && mkdir -p ${LogDir}/export_sharecodes
Import_Conf && Count_UserSum && Cat_All | perl -pe "{s|京东种豆|种豆|; s|crazyJoy任务|疯狂的JOY|}" | tee ${LogFile}
-122
View File
@@ -1,122 +0,0 @@
#!/usr/bin/env bash
#author:spark thanks to: https://github.com/sparkssssssss/scripts
ShellDir=${QL_DIR:-$(
cd $(dirname $0)
pwd
)}
[[ $QL_DIR ]] && ShellJs=js
ConfigDir=$ShellDir/config
ListCronCurrent=$ConfigDir/crontab.list
AuthConf=$ConfigDir/auth.json
declare -A BlackListDict
url=$1
path=$2
blackword=$3
if [[ $# -lt 2 ]] || [[ $# -gt 4 ]]; then
echo 'Desc: 用户拉取指定用户的指定仓储'
echo 'Usage: diy <repourl> <path> <blacklist>'
echo 'repourl 仓储地址'
echo 'path 需要下载脚本的指定目录,多个目录 | 分割 path1 | path2'
echo 'blacklist 需要排除的脚本名,多个名称 | 分割 blacklist1 | blacklist2'
exit 0
fi
diyscriptsdir=/ql/diyscripts
mkdir -p ${diyscriptsdir}
urlTmp=${url%*/}
repoTmp=${urlTmp##*/}
repo=${repoTmp%.*}
tmp=${url%/*}
authorTmp1=${tmp##*/}
authorTmp2=${authorTmp1##*:}
author=${authorTmp2##*.}
if [ ! -d "$diyscriptsdir/${author}_${repo}" ]; then
echo -e "${author}本地仓库不存在,从远程拉取ing..."
cd ${diyscriptsdir} && git clone $url ${author}_${repo}
gitpullstatus=$?
[ $gitpullstatus -eq 0 ] && echo -e "${author}本地仓库拉取完毕"
[ $gitpullstatus -ne 0 ] && echo -e "${author}本地仓库拉取失败,请检查!" && exit 0
else
cd ${diyscriptsdir}/${author}_${repo}
branch=$(git symbolic-ref --short -q HEAD)
git fetch --all
git reset --hard origin/$branch
git pull
gitpullstatus=$?
fi
rand() {
min=$1
max=$(($2 - $min + 1))
num=$(cat /proc/sys/kernel/random/uuid | cksum | awk -F ' ' '{print $1}')
echo $(($num % $max + $min))
}
addnewcron() {
addname=""
cd ${diyscriptsdir}/${author}_${repo}
express=$(find . -name "*.js")
if [ $path ]; then
express=$(find . -name "*.js" | egrep $path)
fi
if [ $blackword ]; then
express=$(find . -name "*.js" | egrep -v $blackword | egrep $path)
fi
for js in $express; do
base=$(basename $js)
croname=$(echo "${author}_$base" | awk -F\. '{print $1}')
script_date=$(cat $js | grep ^[0-9] | awk '{print $1,$2,$3,$4,$5}' | egrep -v "[a-zA-Z]|:|\." | sort | uniq | head -n 1)
[ -z "${script_date}" ] && script_date=$(cat $js | grep -Eo "([0-9]+|\*|[0-9]+[,-].*) ([0-9]+|\*|[0-9]+[,-].*) ([0-9]+|\*|[0-9]+[,-].*) ([0-9]+|\*|[0-9]+[,-].*) ([0-9]+|\*|[0-9][,-].*)" | sort | uniq | head -n 1)
[ -z "${script_date}" ] && cron_min=$(rand 1 59) && cron_hour=$(rand 7 9) && script_date="${cron_min} ${cron_hour} * * *"
local oldCron=$(grep -c -w "$croname" "$ListCronCurrent")
if [[ oldCron -eq 0 ]]; then
local name=$(cat "$js" | grep -E "new Env\(" | perl -pe "s|(^.+)new Env\(\'*\"*(.+?)'*\"*\).+|\2|")
add_cron_api "$script_date" "js $croname" "$name"
addname="${addname}\n${croname}"
echo -e "添加了新的脚本${croname}."
fi
if [ ! -f "/ql/scripts/${author}_$base" ]; then
\cp $js /ql/scripts/${author}_$base
else
change=$(diff $js /ql/scripts/${author}_$base)
[ -n "${change}" ] && \cp $js /ql/scripts/${author}_$base && echo -e "${author}_$base 脚本更新了."
fi
done
[ "$addname" != "" ] && notify "新增 ${author} 自定义脚本" "${addname}"
}
delcron() {
delname=""
cronfiles=$(grep "$author" /ql/config/crontab.list | grep -v "^#" | perl -pe "s|.*ID=(.*) js (${author}_.*)\.*|\1:\2|")
for filename in $cronfiles; do
local id=$(echo "$1" | awk -F ":" '{print $1}')
local name=$(echo "$1" | awk -F ":" '{print $2}')
hasFile=$(cd ${diyscriptsdir}/${author}_${repo} && find . -name "$filename.js" | wc -l)
if [[ $hasFile != 0 ]]; then
del_cron_api "$id"
echo -e "删除失效脚本${name}."
delname="${delname}\n${author}_${filename}"
fi
done
[ "$delname" != "" ] && notify "删除 ${author} 失效脚本" "${delname}"
}
. $ShellDir/shell/api.sh
get_token
if [[ ${gitpullstatus} -eq 0 ]]; then
addnewcron
delcron
else
echo -e "$author 仓库更新失败了."
notify "自定义仓库更新失败" "$author"
fi
exit 0
-297
View File
@@ -1,297 +0,0 @@
#!/usr/bin/env bash
ShellDir=${QL_DIR:-$(
cd $(dirname $0)
pwd
)}
[[ $QL_DIR ]] && ShellJs=js
LogDir=$ShellDir/log
DbDir=$ShellDir/db
ManualLogDir=$ShellDir/manual_log
ScriptsDir=$ShellDir/scripts
ConfigDir=$ShellDir/config
FileConf=$ConfigDir/config.sh
CookieConf=$ConfigDir/cookie.sh
AuthConf=$ConfigDir/auth.json
ExtraShell=$ConfigDir/extra.sh
FileConfSample=$ShellDir/sample/config.sh.sample
ListCronSample=$ShellDir/sample/crontab.list.sample
ListCronCurrent=$ConfigDir/crontab.list
ListCronRemote=$ScriptsDir/docker/crontab_list.sh
ListCurrentTask=$LogDir/task.list
ListRemoteTask=$LogDir/js.list
ListJsAdd=$LogDir/js-add.list
ListJsDrop=$LogDir/js-drop.list
ContentVersion=$ShellDir/version
ContentNewTask=$ShellDir/new_task
ContentDropTask=$ShellDir/drop_task
SendVersion=$ShellDir/send_version
isTermux=$ANDROID_RUNTIME_ROOT$ANDROID_ROOT
ShellURL=https://ghproxy.com/https://github.com/whyour/qinglong
ScriptsURL=https://ghproxy.com/https://github.com/gossh520/jd_scripts
Import_Conf() {
if [ ! -s $FileConf ]; then
echo -e "复制一份 $FileConfSample 示例配置文件\n\n"
cp -fv $FileConfSample $FileConf
fi
if [ ! -s $ListCronCurrent ]; then
echo -e "复制一份 $ListCronSample 基础定时任务\n\n"
cp -fv $ListCronSample $ListCronCurrent
fi
[ -f $CookieConf ] && . $CookieConf
[ -f $FileConf ] && . $FileConf
}
# 更新shell
Git_Pull_Shell() {
echo -e "更新shell...\n"
cd $ShellDir
git remote set-url origin $ShellURL
git fetch --all
ExitStatusShell=$?
git reset --hard origin/master
git pull
}
Git_Pull_Shell_Next() {
if [[ $ExitStatusShell -eq 0 ]]; then
echo -e "更新shell成功...\n"
[ ! -d $ShellDir/node_modules ] && Npm_Install panel
[ -f $ShellDir/package.json ] && PanelDependNew=$(cat $ShellDir/package.json)
[[ "$PanelDependOld" != "$PanelDependNew" ]] && cd $ShellDir && Npm_Install panel
cp -f $FileConfSample $ConfigDir/config.sh.sample
Notify_Version
else
echo -e "更新shell失败,请检查原因...\n"
fi
}
## npm install
Npm_Install() {
echo -e "检测到 $1 的依赖包有变化,运行 npm install...\n"
Npm_InstallSub
if [ $? -ne 0 ]; then
echo -e "\nnpm install 运行不成功,自动删除 $1/node_modules 后再次尝试一遍..."
rm -rf node_modules
fi
echo
if [ ! -d node_modules ]; then
echo -e "运行 npm install...\n"
Npm_InstallSub
if [ $? -ne 0 ]; then
echo -e "\nnpm install 运行不成功,自动删除 $1/node_modules...\n"
echo -e "请进入 $1 目录后手动运行 npm install...\n"
echo -e "3...\n"
sleep 1
echo -e "2...\n"
sleep 1
echo -e "1...\n"
sleep 1
rm -rf node_modules
fi
fi
}
## npm install 子程序,判断是否为安卓,判断是否安装有yarn
function Npm_InstallSub() {
if [ -n "$isTermux" ]; then
npm install --production --no-save --no-bin-links || npm install --production --no-bin-links --no-save --registry=https://registry.npm.taobao.org
elif ! type yarn >/dev/null 2>&1; then
npm install --production --no-save || npm install --production --no-save --registry=https://registry.npm.taobao.org
else
echo -e "检测到本机安装了 yarn,使用 yarn 替代 npm...\n"
yarn install --production --network-timeout 1000000000 || yarn install --production --registry=https://registry.npm.taobao.org --network-timeout 1000000000
fi
}
## 检测配置文件版本
Notify_Version() {
## 识别出两个文件的版本号
VerConfSample=$(grep " Version: " $FileConfSample | perl -pe "s|.+v((\d+\.?){3})|\1|")
[ -f $FileConf ] && VerConf=$(grep " Version: " $FileConf | perl -pe "s|.+v((\d+\.?){3})|\1|")
## 删除旧的发送记录文件
[ -f "$SendVersion" ] && [[ $(cat $SendVersion) != $VerConfSample ]] && rm -f $SendVersion
## 识别出更新日期和更新内容
UpdateDate=$(grep " Date: " $FileConfSample | awk -F ": " '{print $2}')
UpdateContent=$(grep " Update Content: " $FileConfSample | awk -F ": " '{print $2}')
## 如果是今天,并且版本号不一致,则发送通知
if [ -f $FileConf ] && [[ "$VerConf" != "$VerConfSample" ]] && [[ $UpdateDate == $(date "+%Y-%m-%d") ]]; then
if [ ! -f $SendVersion ]; then
notify "检测到配置文件config.sh.sample有更新" "更新日期: $UpdateDate\n当前版本: $VerConf\n新的版本: $VerConfSample\n更新内容: $UpdateContent\n更新说明: 如需使用新功能请对照config.sh.sample,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。"
fi
else
[ -f $ContentVersion ] && rm -f $ContentVersion
[ -f $SendVersion ] && rm -f $SendVersion
fi
}
## 每天次数随机,更新时间随机,更新秒数随机,至少6次,至多12次,大部分为8-10次,符合正态分布。
Random_Pull_Cron() {
if [[ $(date "+%-H") -le 2 ]]; then
RanMin=$(($RANDOM % 60))
RanSleep=$(($RANDOM % 56))
RanHourArray[0]=$(($RANDOM % 3))
for ((i = 1; i < 14; i++)); do
j=$(($i - 1))
tmp=$(($RANDOM % 3 + ${RanHourArray[j]} + 2))
[[ $tmp -lt 24 ]] && RanHourArray[i]=$tmp || break
done
RanHour=${RanHourArray[0]}
for ((i = 1; i < ${#RanHourArray[*]}; i++)); do
RanHour="$RanHour,${RanHourArray[i]}"
done
perl -i -pe "s|.+(git_pull? .+git_pull\.log.*)|$RanMin $RanHour \* \* \* sleep $RanSleep && \1|" $ListCronCurrent
crontab $ListCronCurrent
fi
}
## 克隆scripts
Git_Clone_Scripts() {
git clone -b master $ScriptsURL $ScriptsDir
ExitStatusScripts=$?
}
## 更新scripts
Git_Pull_Scripts() {
if [ -d $ScriptsDir/.git ]; then
echo -e "更新scripts...\n"
cd $ScriptsDir
git remote set-url origin $ScriptsURL
git fetch --all
ExitStatusScripts=$?
git reset --hard origin/master
git pull
else
Git_Clone_Scripts
fi
}
Git_Pull_Scripts_Next() {
if [[ $ExitStatusScripts -eq 0 ]]; then
echo -e "更新scripts成功...\n"
[ ! -d $ScriptsDir/node_modules ] && Npm_Install scripts
[ -f $ScriptsDir/package.json ] && ScriptsDependNew=$(cat $ScriptsDir/package.json)
[[ "$ScriptsDependOld" != "$ScriptsDependNew" ]] && cd $ScriptsDir && Npm_Install scripts
Diff_Cron
if [ -s $ListJsDrop ]; then
Output_ListJs $ListJsDrop "失效"
Del_Cron
fi
if [ -s $ListJsAdd ]; then
Output_ListJs $ListJsAdd "新"
Add_Cron
fi
else
echo -e "更新scripts失败,请检查原因...\n"
fi
}
Diff_Cron() {
cat $ListCronRemote | grep -E "node.+j[drx]_\w+\.js" | perl -pe "s|.+(j[drx]_\w+)\.js.+|\1|" | sort -u >$ListRemoteTask
cat $ListCronCurrent | grep -E "$ShellJs j[drx]_\w+" | perl -pe "s|.*ID=(.*) $ShellJs (j[drx]_\w+)\.*|\2|" | sort -u >$ListCurrentTask
if [ -s $ListCurrentTask ]; then
grep -vwf $ListCurrentTask $ListRemoteTask >$ListJsAdd
else
cp -f $ListRemoteTask $ListJsAdd
fi
if [ -s $ListRemoteTask ]; then
grep -vwf $ListRemoteTask $ListCurrentTask >$ListJsDrop
else
cp -f $ListCurrentTask $ListJsDrop
fi
}
Del_Cron() {
if [ $AutoDelCron == true ] && [ -s $ListJsDrop ]; then
echo -e "开始尝试自动删除定时任务如下:\n"
cat $ListJsDrop
echo
JsDrop=$(cat $ListJsDrop)
for Cron in $JsDrop; do
local id=$(cat $ListCronCurrent | grep -E "js $Cron" | perl -pe "s|.*ID=(.*) js $Cron|\1|")
del_cron_api "$id"
done
crontab $ListCronCurrent
echo -e "成功删除失效的脚本与定时任务\n"
notify "删除 lxk0301 失效脚本" "$JsDrop"
fi
}
Add_Cron() {
if [ $AutoAddCron == true ] && [ -s $ListJsAdd ]; then
echo -e "开始尝试自动添加定时任务\n"
JsAdd=$(cat $ListJsAdd)
for Cron in $JsAdd; do
if [[ $Cron == jd_bean_sign ]]; then
local name=$(cat "$ScriptsDir/$Cron.js" | grep -E "new Env\(" | perl -pe "s|(^.+)new Env\(\'*\"*(.+?)'*\"*\).+|\2|")
add_cron_api "4 0,9 * * *" "$ShellJs $Cron" "$name"
else
local name=$(cat "$ScriptsDir/$Cron.js" | grep -E "new Env\(" | perl -pe "s|(^.+)new Env\(\'*\"*(.+?)'*\"*\).+|\2|")
local param=$(cat $ListCronRemote | grep -E "\/$Cron\." | perl -pe "s|(^.+) node */scripts/(j[drx]_\w+)\.js.+|\1\:$ShellJs \2|")
add_cron_api "$param:$name"
fi
done
if [ $? -eq 0 ]; then
crontab $ListCronCurrent
echo -e "成功添加新的定时任务...\n"
notify "新增lxk0301脚本" "$JsAdd"
else
echo -e "添加新的定时任务出错,请手动添加...\n"
notify "尝试自动添加lxk0301以下新的定时任务出错,请手动添加:" "$JsAdd"
fi
fi
}
## 输出定时任务变化
Output_ListJs() {
local list=$1
local type=$2
if [ -s $list ]; then
echo -e "检测到有$type的定时任务\n"
cat $list
echo
fi
}
#################################################################################################################################
echo -e "\n--------------------------------------------------------------\n"
echo -n "系统时间:"
echo $(date "+%Y-%m-%d %H:%M:%S")
if [ "${TZ}" = "UTC" ]; then
echo -n "北京时间:"
echo $(date -d "8 hour" "+%Y-%m-%d %H:%M:%S")
fi
echo -e "\nJS脚本目录:$ScriptsDir\n"
echo -e "--------------------------------------------------------------\n"
. $ShellDir/shell/api.sh
get_token
Import_Conf
# 更新shell
[ -f $ShellDir/package.json ] && PanelDependOld=$(cat $ShellDir/package.json)
Git_Pull_Shell
Git_Pull_Shell_Next
## 更新scripts
[ -f $ScriptsDir/package.json ] && ScriptsDependOld=$(cat $ScriptsDir/package.json)
Git_Pull_Scripts
Git_Pull_Scripts_Next
## 调用用户自定义的extra.sh
if [[ $EnableExtraShell == true ]]; then
if [ -f $ExtraShell ]; then
. $ExtraShell
else
echo -e "$ExtraShell 文件不存在,跳过执行DIY脚本...\n"
fi
fi
-283
View File
@@ -1,283 +0,0 @@
#!/usr/bin/env bash
## 路径
ShellDir=${QL_DIR:-$(cd $(dirname $0); pwd)}
[[ ${QL_DIR} ]] && ShellJS=js
ScriptsDir=${ShellDir}/scripts
ConfigDir=${ShellDir}/config
FileConf=${ConfigDir}/config.sh
CookieConf=${ConfigDir}/cookie.sh
FileConfSample=${ShellDir}/sample/config.sh.sample
LogDir=${ShellDir}/log
ListScripts=($(cd ${ScriptsDir}; ls *.js | grep -E "j[drx]_"))
ListCron=${ConfigDir}/crontab.list
ListCronLxk=${ScriptsDir}/docker/crontab_list.sh
ListJs=${LogDir}/js.list
## 导入config.sh
function Import_Conf {
if [ -f ${FileConf} ]
then
. ${CookieConf}
. ${FileConf}
if [[ ! -s ${CookieConf} ]]; then
echo -e "请先在Cookie管理中添加一条Cookie...\n"
exit 1
fi
else
echo -e "配置文件 ${FileConf} 不存在,请先按教程配置好该文件...\n"
exit 1
fi
}
## 更新crontab
function Detect_Cron {
if [[ $(cat ${ListCron}) != $(crontab -l) ]]; then
crontab ${ListCron}
fi
}
## 用户数量UserSum
function Count_UserSum {
UserSum=0
for line in `cat $CookieConf`
do
((UserSum++))
eval Cookie${UserSum}="\"${line}\""
done
}
## 组合Cookie和互助码子程序
function Combin_Sub {
CombinAll=""
if [[ ${AutoHelpOther} == true ]] && [[ $1 == ForOther* ]]; then
ForOtherAll=""
MyName=$(echo $1 | perl -pe "s|ForOther|My|")
for ((m=1; m<=${UserSum}; m++)); do
TmpA=${MyName}$m
TmpB=${!TmpA}
ForOtherAll="${ForOtherAll}@${TmpB}"
done
for ((n=1; n<=${UserSum}; n++)); do
for num in ${TempBlockCookie}; do
[[ $n -eq $num ]] && continue 2
done
CombinAll="${CombinAll}&${ForOtherAll}"
done
else
for ((i=1; i<=${UserSum}; i++)); do
for num in ${TempBlockCookie}; do
[[ $i -eq $num ]] && continue 2
done
Tmp1=$1$i
Tmp2=${!Tmp1}
CombinAll="${CombinAll}&${Tmp2}"
done
fi
echo ${CombinAll} | perl -pe "{s|^&||; s|^@+||; s|&@|&|g; s|@+&|&|g; s|@+|@|g; s|@+$||}"
}
## 组合Cookie、Token与互助码
function Combin_All {
export JD_COOKIE=$(Combin_Sub Cookie)
export FRUITSHARECODES=$(Combin_Sub ForOtherFruit)
export PETSHARECODES=$(Combin_Sub ForOtherPet)
export PLANT_BEAN_SHARECODES=$(Combin_Sub ForOtherBean)
export DREAM_FACTORY_SHARE_CODES=$(Combin_Sub ForOtherDreamFactory)
export DDFACTORY_SHARECODES=$(Combin_Sub ForOtherJdFactory)
export JDZZ_SHARECODES=$(Combin_Sub ForOtherJdzz)
export JDJOY_SHARECODES=$(Combin_Sub ForOtherJoy)
export JXNC_SHARECODES=$(Combin_Sub ForOtherJxnc)
export JXNCTOKENS=$(Combin_Sub TokenJxnc)
export BOOKSHOP_SHARECODES=$(Combin_Sub ForOtherBookShop)
export JD_CASH_SHARECODES=$(Combin_Sub ForOtherCash)
export JDSGMH_SHARECODES=$(Combin_Sub ForOtherSgmh)
export JDCFD_SHARECODES=$(Combin_Sub ForOtherCfd)
export JDGLOBAL_SHARECODES=$(Combin_Sub ForOtherGlobal)
}
## 转换JD_BEAN_SIGN_STOP_NOTIFY或JD_BEAN_SIGN_NOTIFY_SIMPLE
function Trans_JD_BEAN_SIGN_NOTIFY {
case ${NotifyBeanSign} in
0)
export JD_BEAN_SIGN_STOP_NOTIFY="true"
;;
1)
export JD_BEAN_SIGN_NOTIFY_SIMPLE="true"
;;
esac
}
## 转换UN_SUBSCRIBES
function Trans_UN_SUBSCRIBES {
export UN_SUBSCRIBES="${goodPageSize}\n${shopPageSize}\n${jdUnsubscribeStopGoods}\n${jdUnsubscribeStopShop}"
}
## 申明全部变量
function Set_Env {
Count_UserSum
Combin_All
Trans_JD_BEAN_SIGN_NOTIFY
Trans_UN_SUBSCRIBES
}
## 随机延迟
function Random_Delay {
if [[ -n ${RandomDelay} ]] && [[ ${RandomDelay} -gt 0 ]]; then
CurMin=$(date "+%-M")
if [[ ${CurMin} -gt 2 && ${CurMin} -lt 30 ]] || [[ ${CurMin} -gt 31 && ${CurMin} -lt 59 ]]; then
CurDelay=$((${RANDOM} % ${RandomDelay} + 1))
echo -e "\n命令未添加 \"now\",随机延迟 ${CurDelay} 秒后再执行任务,如需立即终止,请按 CTRL+C...\n"
sleep ${CurDelay}
fi
fi
}
## 使用说明
function Help {
echo -e "本脚本的用法为:"
echo -e "1. ${ShellJS} xxx # 如果设置了随机延迟并且当时时间不在0-2、30-31、59分内,将随机延迟一定秒数"
echo -e "2. ${ShellJS} xxx now # 无论是否设置了随机延迟,均立即运行"
echo -e "3. ${ShellJS} runall # 运行所有非挂机脚本,非常耗时"
echo -e "4. ${ShellJS} hangup # 重启挂机程序"
echo -e "5. ${ShellJS} resetpwd # 重置控制面板用户名和密码"
echo -e "\n针对用法1、用法2中的\"xxx\",可以不输入后缀\".js\",另外,如果前缀是\"jd_\"的话前缀也可以省略。"
echo -e "当前有以下脚本可以运行(仅列出以jd_、jr_、jx_开头的脚本):"
cd ${ScriptsDir}
for ((i=0; i<${#ListScripts[*]}; i++)); do
Name=$(grep "new Env" ${ListScripts[i]} | awk -F "'|\"" '{print $2}')
echo -e "$(($i + 1)).${Name}${ListScripts[i]}"
done
}
## nohup
function Run_Nohup {
if [[ $(ps -ef | grep "${js}" | grep -v "grep") != "" ]]; then
ps -ef | grep "${js}" | grep -v "grep" | awk '{print $2}' | xargs kill -9
fi
[ ! -d ${LogDir}/${js} ] && mkdir -p ${LogDir}/${js}
LogTime=$(date "+%Y-%m-%d-%H-%M-%S")
LogFile="${LogDir}/${js}/${LogTime}.log"
nohup node ${js}.js > ${LogFile} &
}
## 运行挂机脚本
function Run_HangUp {
HangUpJs="jd_crazy_joy_coin"
cd ${ScriptsDir}
for js in ${HangUpJs}; do
Import_Conf ${js} && Set_Env
if type pm2 >/dev/null 2>&1; then
pm2 stop ${js}.js 2>/dev/null
pm2 flush
pm2 start -a ${js}.js --watch "${ScriptsDir}/${js}.js" --name="${js}"
else
Run_Nohup >/dev/null 2>&1
fi
done
}
## 重置密码
function Reset_Pwd {
cp -f ${ShellDir}/sample/auth.json ${ConfigDir}/auth.json
echo -e "控制面板重置成功,用户名:admin,密码:adminadmin\n"
}
## 一次性运行所有脚本
function Run_All {
if [ ! -f ${ListJs} ]; then
cat ${ListCronLxk} | grep -E "j[drx]_\w+\.js" | perl -pe "s|.+(j[drx]_\w+)\.js.+|\1|" | sort -u > ${ListJs}
fi
echo -e "\n==================== 开始运行所有非挂机脚本 ====================\n"
echo -e "请注意:本过程将非常非常耗时,一个账号可能长达几小时,账号越多耗时越长,如果是手动运行,退出终端也将终止运行。\n"
echo -e "倒计时5秒...\n"
for ((sec=5; sec>0; sec--)); do
echo -e "$sec...\n"
sleep 1
done
for file in $(cat ${ListJs}); do
echo -e "==================== 运行 $file.js 脚本 ====================\n"
${ShellJS} $file now
done
}
## 正常运行单个脚本
function Run_Normal {
Import_Conf $1 && Detect_Cron && Set_Env
FileNameTmp1=$(echo $1 | perl -pe "s|\.js||")
FileNameTmp2=$(echo $1 | perl -pe "{s|jd_||; s|\.js||; s|^|jd_|}")
SeekDir="${ScriptsDir} ${ScriptsDir}/backUp ${ConfigDir}"
FileName=""
WhichDir=""
for dir in ${SeekDir}
do
if [ -f ${dir}/${FileNameTmp1}.js ]; then
FileName=${FileNameTmp1}
WhichDir=${dir}
break
elif [ -f ${dir}/${FileNameTmp2}.js ]; then
FileName=${FileNameTmp2}
WhichDir=${dir}
break
fi
done
if [ -n "${FileName}" ] && [ -n "${WhichDir}" ]
then
[ $# -eq 1 ] && Random_Delay
LogTime=$(date "+%Y-%m-%d-%H-%M-%S")
LogFile="${LogDir}/${FileName}/${LogTime}.log"
[ ! -d ${LogDir}/${FileName} ] && mkdir -p ${LogDir}/${FileName}
cd ${WhichDir}
node ${FileName}.js 2>&1 | tee ${LogFile}
else
echo -e "\n在${ScriptsDir}${ScriptsDir}/backUp、${ConfigDir}三个目录下均未检测到 $1 脚本的存在,请确认...\n"
Help
fi
}
## 命令检测
case $# in
0)
echo
Help
;;
1)
case $1 in
hangup)
Run_HangUp
;;
resetpwd)
Reset_Pwd
;;
runall)
Run_All
;;
*)
Run_Normal $1
;;
esac
;;
2)
case $2 in
now)
Run_Normal $1 $2
;;
*)
echo -e "\n命令输入错误...\n"
Help
;;
esac
;;
*)
echo -e "\n命令过多...\n"
Help
;;
esac
+1 -1
View File
@@ -1,4 +1,4 @@
const notify = require('/ql/scripts/sendNotify'); const notify = require('/ql/scripts/sendNotify.js');
const title = process.argv[2]; const title = process.argv[2];
const content = process.argv[3]; const content = process.argv[3];
-1
View File
@@ -1,7 +1,6 @@
#!/bin/bash #!/bin/bash
#author:spark thanks to: https://github.com/sparkssssssss/scripts #author:spark thanks to: https://github.com/sparkssssssss/scripts
. /ql/config/cookie.sh
. /ql/config/config.sh . /ql/config/config.sh
title=$(echo $1|sed 's/-/_/g') title=$(echo $1|sed 's/-/_/g')
msg=$(echo -e $2) msg=$(echo -e $2)
-29
View File
@@ -1,29 +0,0 @@
#!/usr/bin/env bash
ShellDir=${QL_DIR:-$(cd $(dirname $0); pwd)}
echo -e "更新shell...\n"
cd ${ShellDir}
git fetch --all
git pull
echo -e "更新shell完成...\n"
echo -e "重新build...\n"
yarn install --network-timeout 1000000000 || yarn install --registry=https://registry.npm.taobao.org --network-timeout 1000000000
yarn build
yarn build-back
yarn cache clean
echo -e "重新build完成...\n"
echo -e "重启服务...\n"
PIDS=`ps -ef|grep "app.js"|grep -v grep`
if [ "$PIDS" != "" ]; then
pm2 restart panel
else
pm2 start ${QL_DIR}/build/app.js -n panel
fi
nginx -s reload
echo -e "重启服务完成...\n"
-58
View File
@@ -1,58 +0,0 @@
#!/usr/bin/env bash
## 判断环境
ShellDir=${QL_DIR:-$(cd $(dirname $0); pwd)}
LogDir=${ShellDir}/log
## 导入配置文件
. ${ShellDir}/config/cookie.sh
. ${ShellDir}/config/config.sh
## 删除运行js脚本的旧日志
function Rm_JsLog {
LogFileList=$(ls -l ${LogDir}/*/*.log | awk '{print $9}')
for log in ${LogFileList}
do
LogDate=$(echo ${log} | awk -F "/" '{print $NF}' | cut -c1-10) #文件名比文件属性获得的日期要可靠
if [[ $(uname -s) == Darwin ]]
then
DiffTime=$(($(date +%s) - $(date -j -f "%Y-%m-%d" "${LogDate}" +%s)))
else
DiffTime=$(($(date +%s) - $(date +%s -d "${LogDate}")))
fi
[ ${DiffTime} -gt $((${RmLogDaysAgo} * 86400)) ] && rm -vf ${log}
done
}
## 删除git_pull.sh的运行日志
function Rm_GitPullLog {
if [[ $(uname -s) == Darwin ]]
then
DateDelLog=$(date -v-${RmLogDaysAgo}d "+%Y-%m-%d")
else
Stmp=$(($(date "+%s") - 86400 * ${RmLogDaysAgo}))
DateDelLog=$(date -d "@${Stmp}" "+%Y-%m-%d")
fi
LineEndGitPull=$[$(cat ${LogDir}/git_pull.log | grep -n "${DateDelLog} " | head -1 | awk -F ":" '{print $1}') - 3]
[ ${LineEndGitPull} -gt 0 ] && perl -i -ne "{print unless 1 .. ${LineEndGitPull} }" ${LogDir}/git_pull.log
}
## 删除空文件夹
function Rm_EmptyDir {
cd ${LogDir}
for dir in $(ls)
do
if [ -d ${dir} ] && [[ $(ls ${dir}) == "" ]]; then
rm -rf ${dir}
fi
done
}
## 运行
if [ -n "${RmLogDaysAgo}" ]; then
echo -e "查找旧日志文件中...\n"
Rm_JsLog
Rm_GitPullLog
Rm_EmptyDir
echo -e "删除旧日志执行完毕\n"
fi
Executable
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=/ql/shell
. $dir_shell/share.sh
days=$1
## 删除运行js脚本的旧日志
remove_js_log() {
local log_full_path_list=$(find $dir_log/ -name "*.log")
local diff_time
for log in $log_full_path_list; do
local log_date=$(echo $log | awk -F "/" '{print $NF}' | cut -c1-10) #文件名比文件属性获得的日期要可靠
if [[ $(date +%s -d $log_date 2>/dev/null) ]]; then
if [[ $is_macos -eq 1 ]]; then
diff_time=$(($(date +%s) - $(date -j -f "%Y-%m-%d" "$log_date" +%s)))
else
diff_time=$(($(date +%s) - $(date +%s -d "$log_date")))
fi
[[ $diff_time -gt $((${days} * 86400)) ]] && rm -vf $log
fi
done
}
## 删除空文件夹
remove_empty_dir() {
cd $dir_log
for dir in $(ls); do
if [[ -d $dir ]] && [[ -z $(ls $dir) ]]; then
rm -rf $dir
fi
done
}
## 运行
if [[ ${days} ]]; then
echo -e "查找旧日志文件中...\n"
remove_js_log
remove_empty_dir
echo -e "删除旧日志执行完毕\n"
fi
Executable
+322
View File
@@ -0,0 +1,322 @@
#!/usr/bin/env bash
## 目录
dir_root=/ql
dir_shell=$dir_root/shell
dir_sample=$dir_root/sample
dir_config=$dir_root/config
dir_scripts=$dir_root/scripts
dir_repo=$dir_root/repo
dir_raw=$dir_root/raw
dir_log=$dir_root/log
dir_db=$dir_root/db
dir_dep=$dir_root/deps
dir_list_tmp=$dir_log/.tmp
dir_code=$dir_log/code
dir_update_log=$dir_log/update
ql_static_repo=$dir_repo/static
## 文件
file_config_sample=$dir_sample/config.sample.sh
file_env=$dir_config/env.sh
file_sharecode=$dir_config/sharecode.sh
file_config_user=$dir_config/config.sh
file_auth_sample=$dir_sample/auth.sample.json
file_auth_user=$dir_config/auth.json
file_extra_shell=$dir_config/extra.sh
file_task_before=$dir_config/task_before.sh
file_task_after=$dir_config/task_after.sh
file_task_sample=$dir_sample/task.sample.sh
file_extra_sample=$dir_sample/extra.sample.sh
file_notify_js_sample=$dir_sample/notify.js
file_notify_py_sample=$dir_sample/notify.py
file_notify_py=$dir_scripts/notify.py
file_notify_js=$dir_scripts/sendNotify.js
task_error_log_path=$dir_log/task_error.log
nginx_app_conf=$dir_root/docker/front.conf
nginx_conf=$dir_root/docker/nginx.conf
## 清单文件
list_crontab_user=$dir_config/crontab.list
list_crontab_sample=$dir_sample/crontab.sample.list
list_own_scripts=$dir_list_tmp/own_scripts.list
list_own_user=$dir_list_tmp/own_user.list
list_own_add=$dir_list_tmp/own_add.list
list_own_drop=$dir_list_tmp/own_drop.list
## 软连接及其原始文件对应关系
link_name=(
task
ql
notify
)
original_name=(
task.sh
update.sh
notify.sh
)
init_env() {
export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules
}
import_config() {
[[ -f $file_config_user ]] && . $file_config_user
[[ -f $file_env ]] && . $file_env
command_timeout_time=${CommandTimeoutTime:-"1h"}
github_proxy_url=${GithubProxyUrl:-""}
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() {
local dir=$1
if [[ ! -d $dir ]]; then
mkdir -p $dir
fi
}
detect_termux() {
if [[ ${ANDROID_RUNTIME_ROOT}${ANDROID_ROOT} ]] || [[ $PATH == *com.termux* ]]; then
is_termux=1
else
is_termux=0
fi
}
detect_macos() {
[[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0
}
gen_random_num() {
local divi=$1
echo $((${RANDOM} % $divi))
}
link_shell_sub() {
local link_path="$1"
local original_path="$2"
if [[ ! -L $link_path ]] || [[ $(readlink -f $link_path) != $original_path ]]; then
rm -f $link_path 2>/dev/null
ln -sf $original_path $link_path
fi
}
link_shell() {
if [[ $is_termux -eq 1 ]]; then
local path="/data/data/com.termux/files/usr/bin/"
elif [[ $PATH == */usr/local/bin* ]] && [[ -d /usr/local/bin ]]; then
local path="/usr/local/bin/"
else
local path=""
echo -e "脚本功能受限,请自行添加命令的软连接...\n"
fi
if [[ $path ]]; then
for ((i = 0; i < ${#link_name[*]}; i++)); do
link_shell_sub "$path${link_name[i]}" "$dir_shell/${original_name[i]}"
done
fi
}
define_cmd() {
local cmd_prefix cmd_suffix
if type task &>/dev/null; then
cmd_suffix=""
if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix=""
else
cmd_prefix="bash "
fi
else
cmd_suffix=".sh"
if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix="$dir_shell/"
else
cmd_prefix="bash $dir_shell/"
fi
fi
for ((i = 0; i < ${#link_name[*]}; i++)); do
export cmd_${link_name[i]}="${cmd_prefix}${link_name[i]}${cmd_suffix}"
done
}
fix_config() {
make_dir $dir_config
make_dir $dir_log
make_dir $dir_db
make_dir $dir_scripts
make_dir $dir_list_tmp
make_dir $dir_repo
make_dir $dir_raw
make_dir $dir_update_log
make_dir $dir_dep
if [[ ! -s $file_config_user ]]; then
echo -e "复制一份 $file_config_sample$file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
cp -fv $file_config_sample $file_config_user
echo
fi
if [[ ! -f $file_env ]]; then
echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n"
touch $file_env
echo
fi
if [[ ! -f $file_task_before ]]; then
echo -e "复制一份 $file_task_sample$file_task_before\n"
cp -fv $file_task_sample $file_task_before
echo
fi
if [[ ! -f $file_task_after ]]; then
echo -e "复制一份 $file_task_sample$file_task_after\n"
cp -fv $file_task_sample $file_task_after
echo
fi
if [[ ! -f $file_extra_shell ]]; then
echo -e "复制一份 $file_extra_sample$file_extra_shell\n"
cp -fv $file_extra_sample $file_extra_shell
echo
fi
if [[ ! -s $file_auth_user ]]; then
echo -e "复制一份 $file_auth_sample$file_auth_user\n"
cp -fv $file_auth_sample $file_auth_user
echo
fi
if [[ ! -s $file_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$file_notify_py\n"
cp -fv $file_notify_py_sample $file_notify_py
echo
fi
if [[ ! -s $file_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$file_notify_js\n"
cp -fv $file_notify_js_sample $file_notify_js
echo
fi
if [[ -s /etc/nginx/conf.d/default.conf ]]; then
echo -e "检测到默认nginx配置文件,清空...\n"
cat /dev/null > /etc/nginx/conf.d/default.conf
echo
fi
}
npm_install_sub() {
if [ $is_termux -eq 1 ]; then
npm install --production --no-bin-links --registry=https://registry.npm.taobao.org || npm install --production --no-bin-links
elif ! type pnpm &>/dev/null; then
npm install --production --registry=https://registry.npm.taobao.org || npm install --production
else
pnpm install --production --registry=https://registry.npm.taobao.org || pnpm install --production
fi
}
npm_install_1() {
local dir_current=$(pwd)
local dir_work=$1
cd $dir_work
echo -e "运行 npm install...\n"
npm_install_sub
[[ $? -ne 0 ]] && echo -e "\nnpm install 运行不成功,请进入 $dir_work 目录后手动运行 npm install...\n"
cd $dir_current
}
npm_install_2() {
local dir_current=$(pwd)
local dir_work=$1
cd $dir_work
echo -e "检测到 $dir_work 的依赖包有变化,运行 npm install...\n"
npm_install_sub
if [[ $? -ne 0 ]]; then
echo -e "\n安装 $dir_work 的依赖包运行不成功,再次尝试一遍...\n"
npm_install_1 $dir_work
fi
cd $dir_current
}
diff_and_copy() {
local copy_source=$1
local copy_to=$2
if [[ ! -s $copy_to ]] || [[ $(diff $copy_source $copy_to) ]]; then
cp -f $copy_source $copy_to
fi
}
update_depend() {
local dir_current=$(pwd)
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
npm_install_2 $dir_scripts
fi
cd $dir_current
}
git_clone_scripts() {
local url=$1
local dir=$2
local branch=$3
[[ $branch ]] && local part_cmd="-b $branch "
echo -e "开始克隆仓库 $url$dir\n"
git clone $part_cmd $url $dir
exit_status=$?
}
git_pull_scripts() {
local dir_current=$(pwd)
local dir_work="$1"
local branch="$2"
[[ $branch ]] && local part_cmd="origin/${branch}"
cd $dir_work
echo -e "开始更新仓库:$dir_work\n"
git fetch --all
exit_status=$?
git reset --hard $part_cmd
git pull
cd $dir_current
}
reset_romote_url() {
local dir_current=$(pwd)
local dir_work=$1
local url=$2
local branch="$3"
[[ $branch ]] && local part_cmd="origin/${branch}"
if [[ -d "$dir_work/.git" ]]; then
cd $dir_work
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
git remote set-url origin $url >/dev/null
git reset --hard $part_cmd >/dev/null
cd $dir_current
fi
}
random_range() {
local beg=$1
local end=$2
echo $((RANDOM % ($end - $beg) + $beg))
}
init_env
detect_termux
detect_macos
define_cmd
fix_config
import_config $1 > $task_error_log_path 2>&1
Executable
+341
View File
@@ -0,0 +1,341 @@
#!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=/ql/shell
. $dir_shell/share.sh
. $dir_shell/api.sh
## 选择python3还是node
define_program() {
local first_param=$1
if [[ $first_param == *.js ]]; then
which_program="node"
elif [[ $first_param == *.py ]]; then
which_program="python3"
elif [[ $first_param == *.sh ]]; then
which_program="bash"
elif [[ $first_param == *.ts ]]; then
which_program="ts-node-transpile-only"
else
which_program=""
fi
}
random_delay() {
local random_delay_max=$RandomDelay
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
local current_min=$(date "+%-M")
if [[ $current_min -ne 0 ]] && [[ $current_min -ne 30 ]]; then
delay_second=$(($(gen_random_num $random_delay_max) + 1))
echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后再执行任务,如需立即终止,请按 CTRL+C...\n"
sleep $delay_second
fi
fi
}
## scripts目录下所有可运行脚本数组
gen_array_scripts() {
local dir_current=$(pwd)
local i="-1"
cd $dir_scripts
for file in $(ls); do
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
let i++
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)
[[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
fi
done
cd $dir_current
}
## 使用说明
usage() {
define_cmd
gen_array_scripts
echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:"
echo -e "1.$cmd_task <file_name> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数"
echo -e "2.$cmd_task <file_name> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中"
echo -e "3.$cmd_task <file_name> conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行"
echo -e "4.$cmd_task <file_name> desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行"
if [[ ${#array_scripts[*]} -gt 0 ]]; then
echo -e "\n当前有以下脚本可以运行:"
for ((i = 0; i < ${#array_scripts[*]}; i++)); do
echo -e "$(($i + 1)). ${array_scripts_name[i]}${array_scripts[i]}"
done
else
echo -e "\n暂无脚本可以执行"
fi
}
## run nohup,$1:文件名,不含路径,带后缀
run_nohup() {
local file_name=$1
nohup node $file_name &>$log_path &
}
## 正常运行单个脚本,$1:传入参数
run_normal() {
local first_param=$1
define_program "$first_param"
if [[ $first_param == *.js ]]; then
if [[ $# -eq 1 ]]; then
random_delay
fi
fi
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
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_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")
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
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
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
}
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
run_concurrent() {
local first_param="$1"
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]]; then
echo -e "\n 缺少并发运行的环境变量参数"
exit 1
fi
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')
define_program "$first_param"
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
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_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")
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'))
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
export ${env_param}=${array[i]}
single_log_path="$log_dir/${single_log_time}_$((i + 1)).log"
timeout -k 10s $command_timeout_time $which_program $first_param &>$single_log_path &
done
wait
for i in "${!array[@]}"; do
single_log_path="$log_dir/${single_log_time}_$((i + 1)).log"
eval cat $single_log_path $cmd
[[ -f $single_log_path ]] && rm -f $single_log_path
done
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_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() {
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
local log_dir_tmp="${1##*/}"
local 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")
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
eval timeout -k 10s $command_timeout_time "$@" $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
}
## 命令检测
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
case $# in
1)
run_normal "$1"
;;
*)
case $2 in
now)
run_normal "$1" "$2"
;;
conc)
run_concurrent "$1" "$3" "$*"
;;
desi)
run_designated "$1" "$3" "$*"
;;
*)
run_else "$@"
;;
esac
;;
esac
[[ -f $log_path ]] && cat $log_path
elif [[ $# -eq 0 ]]; then
echo
usage
else
run_else "$@"
fi
}
main "$@"
exit 0
+536
View File
@@ -0,0 +1,536 @@
#!/usr/bin/env bash
dir_shell=/ql/shell
. $dir_shell/share.sh
. $dir_shell/api.sh
send_mark=$dir_shell/send_mark
## 检测cron的差异,$1:脚本清单文件路径,$2:cron任务清单文件路径,$3:增加任务清单文件路径,$4:删除任务清单文件路径
diff_cron() {
local list_scripts="$1"
local list_task="$2"
local list_add="$3"
local list_drop="$4"
if [[ -s $list_task ]]; then
grep -vwf $list_task $list_scripts >$list_add
elif [[ ! -s $list_task ]] && [[ -s $list_scripts ]]; then
cp -f $list_scripts $list_add
fi
if [[ -s $list_scripts ]]; then
grep -vwf $list_scripts $list_task >$list_drop
else
cp -f $list_task $list_drop
fi
}
## 检测配置文件版本
detect_config_version() {
## 识别出两个文件的版本号
ver_config_sample=$(grep " Version: " $file_config_sample | perl -pe "s|.+v((\d+\.?){3})|\1|")
[[ -f $file_config_user ]] && ver_config_user=$(grep " Version: " $file_config_user | perl -pe "s|.+v((\d+\.?){3})|\1|")
## 删除旧的发送记录文件
[[ -f $send_mark ]] && [[ $(cat $send_mark) != $ver_config_sample ]] && rm -f $send_mark
## 识别出更新日期和更新内容
update_date=$(grep " Date: " $file_config_sample | awk -F ": " '{print $2}')
update_content=$(grep " Update Content: " $file_config_sample | awk -F ": " '{print $2}')
## 如果是今天,并且版本号不一致,则发送通知
if [[ -f $file_config_user ]] && [[ $ver_config_user != $ver_config_sample ]] && [[ $update_date == $(date "+%Y-%m-%d") ]]; then
if [[ ! -f $send_mark ]]; then
local notify_title="配置文件更新通知"
local notify_content="更新日期: $update_date\n用户版本: $ver_config_user\n新的版本: $ver_config_sample\n更新内容: $update_content\n更新说明: 如需使用新功能请对照config.sample.sh,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。\n"
echo -e $notify_content
notify "$notify_title" "$notify_content"
[[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark
fi
else
[[ -f $send_mark ]] && rm -f $send_mark
fi
}
## 输出是否有新的或失效的定时任务,$1:新的或失效的任务清单文件路径,$2:新/失效
output_list_add_drop() {
local list=$1
local type=$2
if [[ -s $list ]]; then
echo -e "检测到有$type的定时任务\n"
cat $list
echo
fi
}
## 自动删除失效的脚本与定时任务,需要:1.AutoDelCron 设置为 true2.正常更新js脚本,没有报错;3.存在失效任务
## $1:失效任务清单文件路径
del_cron() {
local list_drop=$1
local path=$2
local detail=""
local ids=""
echo -e "开始尝试自动删除失效的定时任务...\n"
for cron in $(cat $list_drop); do
local id=$(cat $list_crontab_user | grep -E "$cmd_task $cron" | perl -pe "s|.*ID=(.*) $cmd_task $cron\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
if [[ $ids ]]; then
ids="$ids,\"$id\""
else
ids="\"$id\""
fi
cron_file="$dir_scripts/${cron}"
if [[ -f $cron_file ]]; then
cron_name=$(grep "new Env" $cron_file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
rm -f $cron_file
fi
[[ -z $cron_name ]] && cron_name="$cron"
if [[ $detail ]]; then
detail="${detail}\n${cron_name}"
else
detail="${cron_name}"
fi
done
if [[ $ids ]]; then
result=$(del_cron_api "$ids")
notify "$path 删除任务${result}" "$detail"
fi
}
## 自动增加定时任务,需要:1.AutoAddCron 设置为 true2.正常更新js脚本,没有报错;3.存在新任务;4.crontab.list存在并且不为空
## $1:新任务清单文件路径
add_cron() {
local list_add=$1
local path=$2
echo -e "开始尝试自动添加定时任务...\n"
local detail=""
cd $dir_scripts
for file in $(cat $list_add); do
local file_name=${file/${path}\//}
file_name=${file_name/${path}\_/}
if [[ -f $file ]]; then
cron_line=$(
perl -ne "{
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/
}" $file |
perl -pe "{
s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$file_name.*|\1|g;
s|\*([\d\*])(.*)|\1\2|g;
s| | |g;
}" | sort -u | head -1
)
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
[[ -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 "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron"
result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name")
echo -e "$result"
if [[ $detail ]]; then
detail="${detail}${result}\n"
else
detail="${result}\n"
fi
fi
done
notify "$path 新增任务" "$detail"
}
## 更新仓库
update_repo() {
local url="$1"
local path="$2"
local blackword="$3"
local dependence="$4"
local branch="$5"
local tmp="${url%/*}"
local authorTmp1="${tmp##*/}"
local authorTmp2="${authorTmp1##*:}"
local author="${authorTmp2##*.}"
local repo_path="${dir_repo}/${uniq_path}"
make_dir "${dir_scripts}/${uniq_path}"
local formatUrl="$url"
if [[ $github_proxy_url ]]; then
if [[ $url =~ "github.com" ]] || [[ $url =~ "githubusercontent.com" ]]; then
[[ ! $url =~ "https://ghproxy.com" ]] && formatUrl="${github_proxy_url}${url}"
fi
fi
if [[ -d ${repo_path}/.git ]]; then
reset_romote_url ${repo_path} "${formatUrl}" "${branch}"
git_pull_scripts ${repo_path} "${branch}"
else
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}"
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新${repo_path}成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence"
else
echo -e "\n更新${repo_path}失败,重新下载全新仓库...\n"
rm -rf ${repo_path}
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}"
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新${repo_path}成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence"
else
echo -e "\n更新${repo_path}失败,请检查网络...\n"
fi
fi
}
## 更新所有 raw 文件
update_raw() {
echo -e "--------------------------------------------------------------\n"
local url="$1"
local raw_url="$url"
if [[ $github_proxy_url ]]; then
if [[ $url =~ "github.com" ]] || [[ $url =~ "githubusercontent.com" ]]; then
[[ ! $url =~ "https://ghproxy.com" ]] && raw_url="${github_proxy_url}${url}"
fi
fi
local suffix="${raw_url##*.}"
local raw_file_name="${uniq_path}.${suffix}"
echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
if [[ $? -eq 0 ]]; then
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
echo -e "下载 ${raw_file_name} 成功...\n"
cd $dir_raw
local filename="raw_${raw_file_name}"
local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task $filename" | perl -pe "s|.*ID=(.*) $cmd_task $filename\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
cp -f $raw_file_name $dir_scripts/${filename}
cron_line=$(
perl -ne "{
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$raw_file_name/
}" $raw_file_name |
perl -pe "{
s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$raw_file_name.*|\1|g;
s|\*([\d\*])(.*)|\1\2|g;
s| | |g;
}" | sort -u | head -1
)
cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
[[ -z $cron_name ]] && cron_name="$raw_file_name"
[[ -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="$default_cron"
if [[ -z $cron_id ]]; then
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name")
echo -e "$result\n"
notify "新增任务通知" "\n$result"
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
fi
else
echo -e "下载 ${raw_file_name} 失败,保留之前正常下载的版本...\n"
[[ -f "$dir_raw/${raw_file_name}.new" ]] && rm -f "$dir_raw/${raw_file_name}.new"
fi
}
## 调用用户自定义的extra.sh
run_extra_shell() {
if [[ ${EnableExtraShell} == true ]]; then
if [[ -f $file_extra_shell ]]; then
echo -e "--------------------------------------------------------------\n"
. $file_extra_shell
else
echo -e "$file_extra_shell文件不存在,跳过执行...\n"
fi
fi
}
## 脚本用法
usage() {
echo -e "本脚本用法:"
echo -e "1. $cmd_update update # 更新并重启青龙"
echo -e "2. $cmd_update extra # 运行自定义脚本"
echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> # 更新单个仓库的脚本"
echo -e "5. $cmd_update rmlog <days> # 删除旧日志"
echo -e "6. $cmd_update bot # 启动tg-bot"
echo -e "7. $cmd_update check # 检测青龙环境并修复"
echo -e "8. $cmd_update resetlet # 重置登录错误次数"
echo -e "9. $cmd_update resettfa # 禁用两步登录"
}
## 更新qinglong
update_qinglong() {
patch_version
local no_restart="$1"
[[ -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"
git_pull_scripts $dir_root "master"
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新$dir_root成功...\n"
cp -f $file_config_sample $dir_config/config.sample.sh
detect_config_version
update_depend
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
else
echo -e "\n更新$dir_root失败,请检查原因...\n"
fi
local url="${github_proxy_url}https://github.com/whyour/qinglong-static.git"
if [[ -d ${ql_static_repo}/.git ]]; then
reset_romote_url ${ql_static_repo} ${url} "master"
git_pull_scripts ${ql_static_repo} "master"
else
git_clone_scripts ${url} ${ql_static_repo}
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新$ql_static_repo成功...\n"
local static_version=$(cat /ql/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1)
echo -e "\n当前版本 $static_version...\n"
cd $dir_root
rm -rf $dir_root/build && rm -rf $dir_root/dist
cp -rf $ql_static_repo/* $dir_root
if [[ $no_restart != "no-restart" ]]; then
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "重启面板中..."
sleep 3
reload_pm2
fi
else
echo -e "\n更新$dir_root失败,请检查原因...\n"
fi
}
patch_version() {
if [[ -f "/ql/db/cookie.db" ]]; then
echo -e "检测到旧的db文件,拷贝为新db...\n"
mv /ql/db/cookie.db /ql/db/env.db
rm -rf /ql/db/cookie.db
echo
fi
if ! type ts-node &>/dev/null; then
pnpm i -g ts-node typescript tslib
fi
git config --global pull.rebase false
}
reload_pm2() {
pm2 l &>/dev/null
pm2 delete panel --source-map-support --time &>/dev/null
pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null
pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null
}
## 对比脚本
diff_scripts() {
local dir_current=$(pwd)
local repo_path="$1"
local author="$2"
local path="$3"
local blackword="$4"
local dependence="$5"
gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence"
local list_add="$dir_list_tmp/${uniq_path}_add.list"
local list_drop="$dir_list_tmp/${uniq_path}_drop.list"
diff_cron "$dir_list_tmp/${uniq_path}_scripts.list" "$dir_list_tmp/${uniq_path}_user.list" $list_add $list_drop
if [[ -s $list_drop ]]; then
output_list_add_drop $list_drop "失效"
if [[ ${AutoDelCron} == true ]]; then
del_cron $list_drop $uniq_path
fi
fi
if [[ -s $list_add ]]; then
output_list_add_drop $list_add "新"
if [[ ${AutoAddCron} == true ]]; then
add_cron $list_add $uniq_path
fi
fi
cd $dir_current
}
## 生成脚本的路径清单文件
gen_list_repo() {
local dir_current=$(pwd)
local repo_path="$1"
local author="$2"
local path="$3"
local blackword="$4"
local dependence="$5"
rm -f $dir_list_tmp/${uniq_path}*.list &>/dev/null
cd ${repo_path}
local cmd="find ."
local index=0
for extension in $file_extensions; do
if [[ $index -eq 0 ]]; then
cmd="${cmd} -name \"*.${extension}\""
else
cmd="${cmd} -o -name \"*.${extension}\""
fi
let index+=1
done
files=$(eval $cmd | sed 's/^..//')
if [[ $path ]]; then
files=$(echo "$files" | egrep $path)
fi
if [[ $blackword ]]; then
files=$(echo "$files" | egrep -v $blackword)
fi
cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
cp -f $file_notify_py "${dir_scripts}/${uniq_path}"
if [[ $dependence ]]; then
cd ${repo_path}
results=$(eval $cmd | sed 's/^..//' | egrep $dependence)
for _file in ${results}; do
file_path=$(dirname $_file)
make_dir "${dir_scripts}/${uniq_path}/${file_path}"
cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}"
done
fi
if [[ -d $dir_dep ]]; then
cp $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null
fi
for file in ${files}; do
filename=$(basename $file)
cp -f $file "$dir_scripts/${uniq_path}/${filename}"
echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
cron_id=$(cat $list_crontab_user | grep -E "$cmd_task ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
if [[ $cron_id ]]; then
result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
fi
done
grep -E "${cmd_task} ${uniq_path}" ${list_crontab_user} | perl -pe "s|.*ID=(.*) ${cmd_task} (${uniq_path}.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${uniq_path}_user.list"
cd $dir_current
}
get_uniq_path() {
local url="$1"
local branch="$2"
local urlTmp="${url%*/}"
local repoTmp="${urlTmp##*/}"
local repo="${repoTmp%.*}"
local tmp="${url%/*}"
local authorTmp1="${tmp##*/}"
local authorTmp2="${authorTmp1##*:}"
local author="${authorTmp2##*.}"
uniq_path="${author}_${repo}"
[[ $branch ]] && uniq_path="${uniq_path}_${branch}"
}
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 p2=$2
local p3=$3
local p4=$4
local p5=$5
local p6=$6
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
local log_path="$dir_log/update/${log_time}_$p1.log"
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
case $p1 in
update)
cmd=">> $log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
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)
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
run_extra_shell >>$log_path
;;
repo)
get_user_info
get_uniq_path "$p2" "$p6"
log_path="$dir_log/update/${log_time}_${uniq_path}.log"
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
if [[ -n $p2 ]]; then
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" >>$log_path
else
echo -e "命令输入错误...\n"
usage
fi
;;
raw)
get_user_info
get_uniq_path "$p2"
log_path="$dir_log/update/${log_time}_${uniq_path}.log"
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
if [[ -n $p2 ]]; then
update_raw "$p2" >>$log_path
else
echo -e "命令输入错误...\n"
usage
fi
;;
rmlog)
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
. $dir_shell/rmlog.sh "$p2" >>$log_path
;;
bot)
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
. $dir_shell/bot.sh >>$log_path
;;
check)
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$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"
usage
;;
esac
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
echo -e "\n## 执行结束... $end_time 耗时 $diff_time" >>$log_path
cat $log_path
}
main "$@"
exit 0
-24
View File
@@ -1,24 +0,0 @@
import { history } from 'umi';
import { request } from '@/utils/http';
import config from '@/utils/config';
export function render(oldRender: any) {
request
.get(`${config.apiPrefix}user`)
.then((data) => {
if (data.data && data.data.username) {
return oldRender();
}
localStorage.removeItem(config.authKey);
history.push('/login');
oldRender();
})
.catch((e) => {
console.log(e);
if (e.response && e.response.status === 401) {
localStorage.removeItem(config.authKey);
history.push('/login');
oldRender();
}
});
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

+7
View File
@@ -0,0 +1,7 @@
import { PageLoading } from '@ant-design/pro-layout';
const NewPageLoading = () => {
return <PageLoading delay={1}></PageLoading>;
};
export default NewPageLoading;
+23 -12
View File
@@ -7,17 +7,24 @@ import {
FolderOutlined, FolderOutlined,
RadiusSettingOutlined, RadiusSettingOutlined,
ControlOutlined, ControlOutlined,
ContainerOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
export default { export default {
route: { route: {
routes: [ routes: [
{ {
name: 'login', name: '登录',
path: '/login', path: '/login',
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: '定时任务',
@@ -25,10 +32,10 @@ export default {
component: '@/pages/crontab/index', component: '@/pages/crontab/index',
}, },
{ {
path: '/cookie', path: '/env',
name: 'Cookie管理', name: '环境变量',
icon: <RadiusSettingOutlined />, icon: <RadiusSettingOutlined />,
component: '@/pages/cookie/index', component: '@/pages/env/index',
}, },
{ {
path: '/config', path: '/config',
@@ -37,10 +44,16 @@ export default {
component: '@/pages/config/index', component: '@/pages/config/index',
}, },
{ {
path: '/diy', path: '/script',
name: '自定义脚本', name: '脚本管理',
icon: <FormOutlined />, icon: <FormOutlined />,
component: '@/pages/diy/index', component: '@/pages/script/index',
},
{
path: '/dependence',
name: '依赖管理',
icon: <ContainerOutlined />,
component: '@/pages/dependence/index',
}, },
{ {
path: '/diff', path: '/diff',
@@ -50,7 +63,7 @@ export default {
}, },
{ {
path: '/log', path: '/log',
name: '日志', name: '任务日志',
icon: <FolderOutlined />, icon: <FolderOutlined />,
component: '@/pages/log/index', component: '@/pages/log/index',
}, },
@@ -62,12 +75,10 @@ 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', logo: '/images/qinglong.png',
siderWidth: 180,
} as any; } as any;
+272 -19
View File
@@ -1,15 +1,43 @@
body { @import '~antd/lib/style/themes/default.less';
height: 100%; @import '~@/styles/variable.less';
overflow-y: hidden;
background-color: rgb(248, 248, 248);
}
@import '~codemirror/lib/codemirror.css'; @import '~codemirror/lib/codemirror.css';
.code-mirror-wrapper .CodeMirror { @font-face {
position: absolute; font-family: 'Source Code Pro';
height: calc(100vh - 128px); src: url('../assets/fonts/SourceCodePro-Regular.ttf.woff2') format('woff2'),
width: calc(100% - 32px); url('../assets/fonts/SourceCodePro-Regular.otf.woff') format('woff'),
url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype');
}
.log-modal {
.ant-modal {
padding-bottom: 0 !important;
width: 580px !important;
}
pre {
white-space: break-spaces;
line-height: 17px;
margin-bottom: 0;
overflow: hidden;
}
}
.monaco-editor:not(.rename-box) {
height: calc(100vh - 128px) !important;
height: calc(100vh - var(--vh-offset, 0px) - 128px) !important;
.view-overlays .current-line {
border-width: 0;
}
}
.rename-box {
height: 0;
.rename-input {
height: 0;
padding: 0 !important;
display: none !important;
}
} }
.ant-pro-grid-content.wide { .ant-pro-grid-content.wide {
@@ -17,25 +45,250 @@ 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%;
background-color: #fff; background-color: #fff;
padding: 16px; padding: 16px;
} }
} }
.cookie-wrapper { .ant-table-cell-ellipsis {
text-align: left !important;
}
.ant-tooltip {
max-width: 500px !important;
.ant-tooltip-inner {
word-break: break-all !important;
}
}
.env-wrapper {
th { th {
white-space: nowrap; white-space: nowrap;
} }
} }
@media (max-width: 768px) { .log-wrapper {
.ant-pro-grid-content.wide { .log-select {
.ant-pro-page-container-children-content { width: 250px;
height: calc(100vh - 144px);
}
} }
.code-mirror-wrapper .CodeMirror { .ant-page-header-heading-left {
height: calc(100vh - 176px); min-width: 100px;
}
}
.config-wrapper {
.config-select {
width: 250px;
}
.ant-page-header-heading-left {
min-width: 100px;
}
}
.ant-tree {
.ant-tree-treenode {
width: 100%;
padding-right: 8px !important;
}
.ant-tree-node-content-wrapper {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
}
.ant-select-tree {
.ant-select-tree-treenode {
width: 100%;
padding-right: 8px !important;
}
.ant-select-tree-node-content-wrapper {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
}
.ant-pro-page-container.ql-container-wrapper {
display: flex;
flex-direction: column;
height: calc(100vh - 48px);
height: calc(100vh - var(--vh-offset, 0px) - 48px);
.ant-pro-grid-content.wide {
flex: 1;
.ant-pro-grid-content-children {
height: calc(100% - 36px);
> div,
.log-container,
.react-codemirror2,
.CodeMirror {
height: 100%;
}
}
}
}
@media (max-width: 768px) {
.ql-container-wrapper {
&.crontab-wrapper,
&.log-wrapper,
&.env-wrapper,
&.config-wrapper {
.CodeMirror {
width: calc(100vw - 80px);
}
}
}
.ant-tooltip {
max-width: 250px !important;
}
}
.ant-layout-content.ant-pro-basicLayout-content.ant-pro-basicLayout-has-header {
margin-bottom: 0 !important;
min-height: calc(100vh - 66px);
min-height: calc(100vh - var(--vh-offset, 0px) - 66px);
}
.Resizer {
background: #fff;
opacity: 0.2;
z-index: 1;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
-moz-background-clip: padding;
-webkit-background-clip: padding;
background-clip: padding-box;
border-left: 5px solid rgba(42, 161, 255, 0);
border-right: 5px solid rgba(42, 161, 255, 0);
}
.Resizer:hover {
-webkit-transition: all 2s ease;
transition: all 2s ease;
}
.Resizer.horizontal {
height: 11px;
margin: -5px 0;
cursor: row-resize;
width: 100%;
}
.Resizer.vertical {
width: 11px;
margin: 0 -5px;
cursor: col-resize;
}
.Resizer.horizontal:hover,
.Resizer.vertical:hover {
border-left-color: rgba(42, 161, 255, 0.5);
border-right-color: rgba(42, 161, 255, 0.5);
}
.Resizer.disabled {
cursor: not-allowed;
}
.Resizer.disabled:hover {
border-color: transparent;
}
.edit-modal {
.ant-drawer-body {
padding: 0;
}
}
.inline-countdown.ant-statistic {
display: inline-block;
.ant-statistic-content {
font-size: 14px;
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;
}
::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;
} }
} }
+271 -38
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,13 +8,41 @@ 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 'codemirror/mode/shell/shell.js';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import './index.less'; import './index.less';
import vhCheck from 'vh-check';
import { version, changeLogLink, changeLog } from '../version';
import { useCtx, useTheme } from '@/utils/hooks';
import { message, Badge, Modal, Avatar, Dropdown, Menu, Popover } from 'antd';
// @ts-ignore
import SockJS from 'sockjs-client';
import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing';
Sentry.init({
dsn: 'https://ea2fede373244db99c536210b910d9da@o1051273.ingest.sentry.io/6047851',
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}logout`).then(() => {
localStorage.removeItem(config.authKey); localStorage.removeItem(config.authKey);
@@ -22,46 +50,213 @@ export default function (props: any) {
}); });
}; };
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();
}
} else {
message.error(data);
}
})
.catch((error) => {
console.log(error);
});
};
const getUser = (needLoading = true) => {
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,
});
});
}
} }
return (
const isFirefox = navigator.userAgent.includes('Firefox');
const isSafari =
navigator.userAgent.includes('Safari') &&
!navigator.userAgent.includes('Chrome');
const isQQBrowser = navigator.userAgent.includes('QQBrowser');
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]}
title="控制面板" loading={loading}
ErrorBoundary={Sentry.ErrorBoundary}
title={
<>
<span style={{ fontSize: 16 }}></span>
<a
href={changeLogLink}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => {
e.stopPropagation();
}}
>
<span
style={{
fontSize: isFirefox ? 9 : 12,
color: '#666',
marginLeft: 2,
zoom: isSafari ? 0.66 : 0.8,
letterSpacing: isQQBrowser ? -2 : 0,
}}
>
v{version}
</span>
</a>
</>
}
menuItemRender={(menuItemProps: any, defaultDom: any) => { menuItemRender={(menuItemProps: any, defaultDom: any) => {
if ( if (
menuItemProps.isUrl || menuItemProps.isUrl ||
@@ -72,20 +267,58 @@ 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(),
},
];
}} }}
onCollapse={setCollapsed}
collapsed={collapsed}
rightContentRender={() => (
<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>
); );
} }
+93 -57
View File
@@ -1,91 +1,127 @@
import React, { PureComponent, Fragment, useState, useEffect } from 'react'; import React, {
import { Button, notification, Modal } from 'antd'; PureComponent,
Fragment,
useState,
useEffect,
useRef,
} from 'react';
import { Button, message, Modal, TreeSelect } 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 { Controlled as CodeMirror } from 'react-codemirror2';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import Editor from '@monaco-editor/react';
import { Controlled as CodeMirror } from 'react-codemirror2';
const Config = () => { const Config = ({ headerStyle, isPhone, theme }: any) => {
const [width, setWdith] = useState('100%');
const [marginLeft, setMarginLeft] = useState(0);
const [marginTop, setMarginTop] = useState(-72);
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 [select, setSelect] = useState('config.sh');
const [data, setData] = useState<any[]>([]);
const editorRef = useRef<any>(null);
const [confirmLoading, setConfirmLoading] = useState(false);
const getConfig = () => { const getConfig = (name: string) => {
request.get(`${config.apiPrefix}configs/${name}`).then((data: any) => {
setValue(data.data);
});
};
const getFiles = () => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}config/config`) .get(`${config.apiPrefix}configs/files`)
.then((data) => { .then((data: any) => {
setValue(data.data); setData(data.data);
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
const updateConfig = () => { const updateConfig = () => {
setConfirmLoading(true);
const content = editorRef.current
? editorRef.current.getValue().replace(/\r\n/g, '\n')
: value;
request request
.post(`${config.apiPrefix}save`, { .post(`${config.apiPrefix}configs/save`, {
data: { content: value, name: 'config.sh' }, data: { content, name: select },
}) })
.then((data) => { .then((data: any) => {
notification.success({ message.success(data.message);
message: data.msg, setConfirmLoading(false);
});
}); });
}; };
const onSelect = (value: any, node: any) => {
setSelect(value);
setTitle(node.value);
getConfig(node.value);
};
useEffect(() => { useEffect(() => {
if (document.body.clientWidth < 768) { getFiles();
setWdith('auto'); getConfig('config.sh');
setMarginLeft(0);
setMarginTop(0);
} else {
setWdith('100%');
setMarginLeft(0);
setMarginTop(-72);
}
getConfig();
}, []); }, []);
return ( return (
<PageContainer <PageContainer
className="code-mirror-wrapper" className="ql-container-wrapper config-wrapper"
title="config.sh" title={title}
loading={loading}
extra={[ extra={[
<Button key="1" type="primary" onClick={updateConfig}> <TreeSelect
className="config-select"
value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={data}
key="value"
defaultValue="config.sh"
onSelect={onSelect}
/>,
<Button
key="1"
loading={confirmLoading}
type="primary"
onClick={updateConfig}
>
</Button>, </Button>,
]} ]}
header={{ header={{
style: { style: headerStyle,
padding: '4px 16px 4px 15px',
position: 'sticky',
top: 0,
left: 0,
zIndex: 20,
marginTop,
width,
marginLeft,
},
}}
style={{
height: '100vh',
}} }}
> >
<CodeMirror {isPhone ? (
value={value} <CodeMirror
options={{ value={value}
lineNumbers: true, options={{
lineWrapping: true, lineNumbers: true,
styleActiveLine: true, styleActiveLine: true,
matchBrackets: true, matchBrackets: true,
mode: 'shell', mode: 'shell',
}} }}
onBeforeChange={(editor, data, value) => { onBeforeChange={(editor, data, value) => {
setValue(value); setValue(value);
}} }}
onChange={(editor, data, value) => {}} onChange={(editor, data, value) => {}}
/> />
) : (
<Editor
defaultLanguage="shell"
value={value}
theme={theme}
options={{
fontSize: 12,
lineNumbersMinChars: 3,
folding: false,
glyphMargin: false,
}}
onMount={(editor) => {
editorRef.current = editor;
}}
/>
)}
</PageContainer> </PageContainer>
); );
}; };
-486
View File
@@ -1,486 +0,0 @@
import React, { useCallback, useRef, useState, useEffect } from 'react';
import {
Button,
notification,
Modal,
Table,
Tag,
Space,
Typography,
Tooltip,
} from 'antd';
import {
EditOutlined,
DeleteOutlined,
SyncOutlined,
CheckCircleOutlined,
StopOutlined,
} from '@ant-design/icons';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http';
import QRCode from 'qrcode.react';
import CookieModal from './modal';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less';
const { Text } = Typography;
enum Status {
'未获取',
'正常',
'已禁用',
'已失效',
'状态异常',
}
enum StatusColor {
'default',
'success',
'warning',
'error',
}
const type = 'DragableBodyRow';
const DragableBodyRow = ({
index,
moveRow,
className,
style,
...restProps
}: any) => {
const ref = useRef();
const [{ isOver, dropClassName }, drop] = useDrop(
() => ({
accept: type,
collect: (monitor) => {
const { index: dragIndex } = monitor.getItem() || ({} as any);
if (dragIndex === index) {
return {};
}
return {
isOver: monitor.isOver(),
dropClassName:
dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
};
},
drop: (item: any) => {
moveRow(item.index, index);
},
}),
[index],
);
const [, drag, preview] = useDrag(
() => ({
type,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
}),
[index],
);
drop(drag(ref));
return (
<tr
ref={ref}
className={`${className}${isOver ? dropClassName : ''}`}
style={{ cursor: 'move', ...style }}
{...restProps}
>
{restProps.children}
</tr>
);
};
const Config = () => {
const columns = [
{
title: '序号',
align: 'center' as const,
render: (text: string, record: any, index: number) => {
return <span style={{ cursor: 'text' }}>{index + 1} </span>;
},
},
{
title: '用户名',
dataIndex: 'pin',
key: 'pin',
align: 'center' as const,
render: (text: string, record: any) => {
const match = record.value.match(/pt_pin=([^; ]+)(?=;?)/);
const val = (match && match[1]) || '未匹配用户名';
return <span style={{ cursor: 'text' }}>{decodeUrl(val)}</span>;
},
},
{
title: '昵称',
dataIndex: 'nickname',
key: 'nickname',
align: 'center' as const,
width: '15%',
render: (text: string, record: any, index: number) => {
return (
<span style={{ cursor: 'text' }}>{record.nickname || '-'} </span>
);
},
},
{
title: '值',
dataIndex: 'value',
key: 'value',
align: 'center' as const,
width: '50%',
render: (text: string, record: any) => {
return (
<span
style={{
textAlign: 'left',
display: 'inline-block',
wordBreak: 'break-all',
cursor: 'text',
}}
>
{text}
</span>
);
},
},
{
title: '状态',
key: 'status',
dataIndex: 'status',
align: 'center' as const,
width: '15%',
render: (text: string, record: any, index: number) => {
return (
<Space size="middle" style={{ cursor: 'text' }}>
<Tag
color={StatusColor[record.status] || StatusColor[3]}
style={{ marginRight: 0 }}
>
{Status[record.status]}
</Tag>
{record.status !== Status. && (
<Tooltip title="刷新">
<a onClick={() => refreshStatus(record, index)}>
<SyncOutlined />
</a>
</Tooltip>
)}
</Space>
);
},
},
{
title: '操作',
key: 'action',
align: 'center' as const,
render: (text: string, record: any, index: number) => (
<Space size="middle">
<Tooltip title="编辑">
<a onClick={() => editCookie(record, index)}>
<EditOutlined />
</a>
</Tooltip>
<Tooltip title={record.status === Status. ? '启用' : '禁用'}>
<a onClick={() => enabledOrDisabledCron(record, index)}>
{record.status === Status. ? (
<CheckCircleOutlined />
) : (
<StopOutlined />
)}
</a>
</Tooltip>
<Tooltip title="删除">
<a onClick={() => deleteCookie(record, index)}>
<DeleteOutlined />
</a>
</Tooltip>
</Space>
),
},
];
const [width, setWdith] = useState('100%');
const [marginLeft, setMarginLeft] = useState(0);
const [marginTop, setMarginTop] = useState(-72);
const [value, setValue] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [isModalVisible, setIsModalVisible] = useState(false);
const [editedCookie, setEditedCookie] = useState();
const getCookies = () => {
setLoading(true);
request
.get(`${config.apiPrefix}cookies`)
.then((data: any) => {
setValue(data.data);
})
.finally(() => setLoading(false));
};
const decodeUrl = (val: string) => {
try {
const newUrl = decodeURIComponent(val);
return newUrl;
} catch (error) {
return val;
}
};
useEffect(() => {
if (value && loading) {
asyncUpdateStatus();
}
}, [value]);
const asyncUpdateStatus = async () => {
for (let i = 0; i < value.length; i++) {
const cookie = value[i];
if (cookie.status === Status.) {
continue;
}
await sleep(1000);
location.pathname === '/cookie' && refreshStatus(cookie, i);
}
};
const sleep = (time: number) => {
return new Promise((resolve) => setTimeout(resolve, time));
};
const refreshStatus = (record: any, index: number) => {
request
.get(`${config.apiPrefix}cookies/${record._id}/refresh`)
.then(async (data: any) => {
if (data.data && data.data.value) {
(value as any).splice(index, 1, data.data);
setValue([...(value as any)] as any);
} else {
notification.error({ message: '更新状态失败' });
}
});
};
const enabledOrDisabledCron = (record: any, index: number) => {
Modal.confirm({
title: `确认${record.status === Status. ? '启用' : '禁用'}`,
content: (
<>
{record.status === Status. ? '启用' : '禁用'}
Cookie{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.value}
</Text>{' '}
</>
),
onOk() {
request
.get(
`${config.apiPrefix}cookies/${record._id}/${
record.status === Status. ? 'enable' : 'disable'
}`,
{
data: { _id: record._id },
},
)
.then((data: any) => {
if (data.code === 200) {
notification.success({
message: `${
record.status === Status. ? '启用' : '禁用'
}成功`,
});
const newStatus =
record.status === Status. ? Status.未获取 : Status.已禁用;
const result = [...value];
result.splice(index, 1, {
...record,
status: newStatus,
});
setValue(result);
} else {
notification.error({
message: data,
});
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const addCookie = () => {
setEditedCookie(null as any);
setIsModalVisible(true);
};
const editCookie = (record: any, index: number) => {
setEditedCookie(record);
setIsModalVisible(true);
};
const deleteCookie = (record: any, index: number) => {
Modal.confirm({
title: '确认删除',
content: (
<>
Cookie{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.value}
</Text>{' '}
</>
),
onOk() {
request
.delete(`${config.apiPrefix}cookies/${record._id}`)
.then((data: any) => {
if (data.code === 200) {
notification.success({
message: '删除成功',
});
const result = [...value];
result.splice(index, 1);
setValue(result);
} else {
notification.error({
message: data,
});
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const handleCancel = (cookies: any[]) => {
setIsModalVisible(false);
if (cookies && cookies.length > 0) {
handleCookies(cookies);
}
};
const handleCookies = (cookies: any[]) => {
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i];
const index = value.findIndex((x) => x._id === cookie._id);
const result = [...value];
if (index === -1) {
result.push(cookie);
} else {
result.splice(index, 1, {
...cookie,
});
}
setValue(result);
refreshStatus(cookie, index);
}
};
const components = {
body: {
row: DragableBodyRow,
},
};
const moveRow = useCallback(
(dragIndex, hoverIndex) => {
if (dragIndex === hoverIndex) {
return;
}
const dragRow = value[dragIndex];
const newData = [...value];
newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, dragRow);
setValue([...newData]);
request
.put(`${config.apiPrefix}cookies/${dragRow._id}/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex },
})
.then((data: any) => {
if (data.code !== 200) {
notification.error({
message: data,
});
}
});
},
[value],
);
useEffect(() => {
if (document.body.clientWidth < 768) {
setWdith('auto');
setMarginLeft(0);
setMarginTop(0);
} else {
setWdith('100%');
setMarginLeft(0);
setMarginTop(-72);
}
getCookies();
}, []);
return (
<PageContainer
className="cookie-wrapper"
title="Cookie管理"
loading={loading}
extra={[
<Button key="2" type="primary" onClick={() => addCookie()}>
Cookie
</Button>,
]}
header={{
style: {
padding: '4px 16px 4px 15px',
position: 'sticky',
top: 0,
left: 0,
zIndex: 20,
marginTop,
width,
marginLeft,
},
}}
style={{
height: '100vh',
}}
>
<DndProvider backend={HTML5Backend}>
<Table
columns={columns}
pagination={false}
dataSource={value}
rowKey="value"
size="middle"
bordered
scroll={{ x: 768 }}
components={components}
onRow={(record, index) => {
return {
index,
moveRow,
} as any;
}}
/>
</DndProvider>
<CookieModal
visible={isModalVisible}
handleCancel={handleCancel}
cookie={editedCookie}
/>
</PageContainer>
);
};
export default Config;
-103
View File
@@ -1,103 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Modal, notification, Input, Form } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
const CookieModal = ({
cookie,
handleCancel,
visible,
}: {
cookie?: any;
visible: boolean;
handleCancel: (needUpdate?: boolean) => void;
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const handleOk = async (values: any) => {
const cookies = values.value
.split('\n')
.map((x: any) => x.trim().replace(/\s/g, ''));
let flag = false;
for (const coo of cookies) {
if (!/pt_key=\S*;\s*pt_pin=\S*;\s*/.test(coo)) {
notification.error({ message: `${coo}格式有误` });
flag = true;
break;
}
}
if (flag) {
return;
}
setLoading(true);
const method = cookie ? 'put' : 'post';
const payload = cookie ? { value: cookies[0], _id: cookie._id } : cookies;
const { code, data } = await request[method](`${config.apiPrefix}cookies`, {
data: payload,
});
if (code === 200) {
notification.success({
message: cookie ? '更新Cookie成功' : '添加Cookie成功',
});
} else {
notification.error({
message: data,
});
}
setLoading(false);
handleCancel(data);
};
useEffect(() => {
form.resetFields();
}, [cookie]);
return (
<Modal
title={cookie ? '编辑Cookie' : '新建Cookie'}
visible={visible}
forceRender
onOk={() => {
form
.validateFields()
.then((values) => {
handleOk(values);
})
.catch((info) => {
console.log('Validate Failed:', info);
});
}}
onCancel={() => handleCancel()}
confirmLoading={loading}
destroyOnClose
>
<Form
form={form}
layout="vertical"
name="form_in_modal"
preserve={false}
initialValues={cookie}
>
<Form.Item
name="value"
rules={[
{ required: true, message: '请输入Cookie' },
{
pattern: /pt_key=\S*;\s*pt_pin=\S*;\s*/,
message: 'Cookie格式错误,注意分号(pt_key=***;pt_pin=***;)',
},
]}
>
<Input.TextArea
rows={4}
autoSize={true}
placeholder="请输入cookie,多个cookie换行输入"
/>
</Form.Item>
</Form>
</Modal>
);
};
export default CookieModal;
+3
View File
@@ -0,0 +1,3 @@
.ant-table-pagination.ant-pagination {
margin-bottom: 0 !important;
}
+530 -124
View File
@@ -1,7 +1,7 @@
import React, { PureComponent, Fragment, useState, useEffect } from 'react'; import React, { PureComponent, Fragment, useState, useEffect } from 'react';
import { import {
Button, Button,
notification, message,
Modal, Modal,
Table, Table,
Tag, Tag,
@@ -14,7 +14,7 @@ import {
} from 'antd'; } from 'antd';
import { import {
ClockCircleOutlined, ClockCircleOutlined,
SyncOutlined, Loading3QuartersOutlined,
CloseCircleOutlined, CloseCircleOutlined,
FileTextOutlined, FileTextOutlined,
EllipsisOutlined, EllipsisOutlined,
@@ -23,12 +23,19 @@ import {
EditOutlined, EditOutlined,
StopOutlined, StopOutlined,
DeleteOutlined, DeleteOutlined,
PauseCircleOutlined,
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 cron_parser from 'cron-parser';
import { diffTime } from '@/utils/date';
import { getTableScroll } from '@/utils/index';
import './index.less';
const { Text } = Typography; const { Text } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -37,24 +44,59 @@ enum CrontabStatus {
'running', 'running',
'idle', 'idle',
'disabled', 'disabled',
'queued',
} }
const Crontab = () => { const CrontabSort: any = { 0: 0, 3: 1, 1: 2, 4: 3 };
const columns = [
enum OperationName {
'启用',
'禁用',
'运行',
'停止',
'置顶',
'取消置顶',
}
enum OperationPath {
'enable',
'disable',
'run',
'stop',
'pin',
'unpin',
}
const Crontab = ({ headerStyle, isPhone }: any) => {
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> <span>
{record.name || record._id}{' '}
{record.isPinned ? (
<span>
<PushpinOutlined />
</span>
) : (
''
)}
</span>
), ),
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 (
@@ -70,31 +112,157 @@ 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: 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.status === CrontabStatus.idle && ( {(!record.isDisabled || record.status !== CrontabStatus.idle) && (
<Tag icon={<ClockCircleOutlined />} color="default"> <>
{record.status === CrontabStatus.idle && (
</Tag> <Tag icon={<ClockCircleOutlined />} color="default">
</Tag>
)}
{record.status === CrontabStatus.running && (
<Tag
icon={<Loading3QuartersOutlined spin />}
color="processing"
>
</Tag>
)}
{record.status === CrontabStatus.queued && (
<Tag icon={<FieldTimeOutlined />} color="default">
</Tag>
)}
</>
)} )}
{record.status === CrontabStatus.running && ( {record.isDisabled === 1 && record.status === CrontabStatus.idle && (
<Tag icon={<SyncOutlined spin />} color="processing">
</Tag>
)}
{record.status === CrontabStatus.disabled && (
<Tag icon={<CloseCircleOutlined />} color="error"> <Tag icon={<CloseCircleOutlined />} color="error">
</Tag> </Tag>
@@ -106,35 +274,49 @@ const Crontab = () => {
title: '操作', title: '操作',
key: 'action', key: 'action',
align: 'center' as const, align: 'center' as const,
render: (text: string, record: any, index: number) => ( width: 100,
<Space size="middle"> render: (text: string, record: any, index: number) => {
<Tooltip title="运行"> const isPc = !isPhone;
<a return (
onClick={() => { <Space size="middle">
runCron(record, index); {record.status === CrontabStatus.idle && (
}} <Tooltip title={isPc ? '运行' : ''}>
> <a
<PlayCircleOutlined /> onClick={() => {
</a> runCron(record, index);
</Tooltip> }}
<Tooltip title="日志"> >
<a <PlayCircleOutlined />
onClick={() => { </a>
setLogCron({ ...record, timestamp: Date.now() }); </Tooltip>
}} )}
> {record.status !== CrontabStatus.idle && (
<FileTextOutlined /> <Tooltip title={isPc ? '停止' : ''}>
</a> <a
</Tooltip> onClick={() => {
<MoreBtn key="more" record={record} index={index} /> stopCron(record, index);
</Space> }}
), >
<PauseCircleOutlined />
</a>
</Tooltip>
)}
<Tooltip title={isPc ? '日志' : ''}>
<a
onClick={() => {
setLogCron({ ...record, timestamp: Date.now() });
}}
>
<FileTextOutlined />
</a>
</Tooltip>
<MoreBtn key="more" record={record} index={index} />
</Space>
);
},
}, },
]; ];
const [width, setWdith] = useState('100%');
const [marginLeft, setMarginLeft] = useState(0);
const [marginTop, setMarginTop] = useState(-72);
const [value, setValue] = useState<any[]>([]); const [value, setValue] = useState<any[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
@@ -142,13 +324,39 @@ const Crontab = () => {
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [isLogModalVisible, setIsLogModalVisible] = useState(false); const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [logCron, setLogCron] = useState<any>(); const [logCron, setLogCron] = useState<any>();
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const getCrons = () => { const getCrons = () => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}crons?searchValue=${searchText}`) .get(`${config.apiPrefix}crons?searchValue=${searchText}`)
.then((data: any) => { .then((data: any) => {
setValue(data.data.sort((a: any, b: any) => a.status - b.status)); setValue(
data.data
.sort((a: any, b: any) => {
const sortA = a.isDisabled ? 4 : a.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 b.isPinned - a.isPinned;
})
.map((x) => {
return {
...x,
nextRunTime: cron_parser
.parseExpression(x.schedule)
.next()
.toDate(),
};
}),
);
setCurrentPage(1);
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -177,19 +385,16 @@ const Crontab = () => {
), ),
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}crons/${record._id}`) .delete(`${config.apiPrefix}crons`, { data: [record._id] })
.then((data: any) => { .then((data: any) => {
if (data.code === 200) { if (data.code === 200) {
notification.success({ message.success('删除成功');
message: '删除成功',
});
const result = [...value]; const result = [...value];
result.splice(index, 1); const i = result.findIndex((x) => x._id === record._id);
result.splice(i, 1);
setValue(result); setValue(result);
} else { } else {
notification.error({ message.error(data);
message: data,
});
} }
}); });
}, },
@@ -213,19 +418,54 @@ const Crontab = () => {
), ),
onOk() { onOk() {
request request
.get(`${config.apiPrefix}crons/${record._id}/run`) .put(`${config.apiPrefix}crons/run`, { data: [record._id] })
.then((data: any) => { .then((data: any) => {
if (data.code === 200) { if (data.code === 200) {
const result = [...value]; const result = [...value];
result.splice(index, 1, { const i = result.findIndex((x) => x._id === record._id);
result.splice(i, 1, {
...record, ...record,
status: CrontabStatus.running, status: CrontabStatus.running,
}); });
setValue(result); setValue(result);
} else { } else {
notification.error({ message.error(data);
message: data, }
});
},
onCancel() {
console.log('Cancel');
},
});
};
const stopCron = (record: any, index: number) => {
Modal.confirm({
title: '确认停止',
content: (
<>
{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
</>
),
onOk() {
request
.put(`${config.apiPrefix}crons/stop`, { data: [record._id] })
.then((data: any) => {
if (data.code === 200) {
const result = [...value];
const i = result.findIndex((x) => x._id === record._id);
result.splice(i, 1, {
...record,
pid: null,
status: CrontabStatus.idle,
}); });
setValue(result);
} else {
message.error(data);
} }
}); });
}, },
@@ -237,12 +477,10 @@ const Crontab = () => {
const enabledOrDisabledCron = (record: any, index: number) => { const enabledOrDisabledCron = (record: any, index: number) => {
Modal.confirm({ Modal.confirm({
title: `确认${ title: `确认${record.isDisabled === 1 ? '启用' : '禁用'}`,
record.status === CrontabStatus.disabled ? '启用' : '禁用'
}`,
content: ( content: (
<> <>
{record.status === CrontabStatus.disabled ? '启用' : '禁用'} {record.isDisabled === 1 ? '启用' : '禁用'}
{' '} {' '}
<Text style={{ wordBreak: 'break-all' }} type="warning"> <Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name} {record.name}
@@ -252,35 +490,70 @@ const Crontab = () => {
), ),
onOk() { onOk() {
request request
.get( .put(
`${config.apiPrefix}crons/${record._id}/${ `${config.apiPrefix}crons/${
record.status === CrontabStatus.disabled ? 'enable' : 'disable' record.isDisabled === 1 ? 'enable' : 'disable'
}`, }`,
{ {
data: { _id: record._id }, data: [record._id],
}, },
) )
.then((data: any) => { .then((data: any) => {
if (data.code === 200) { if (data.code === 200) {
notification.success({ const newStatus = record.isDisabled === 1 ? 0 : 1;
message: `${
record.status === CrontabStatus.disabled ? '启用' : '禁用'
}成功`,
});
const newStatus =
record.status === CrontabStatus.disabled
? CrontabStatus.idle
: CrontabStatus.disabled;
const result = [...value]; const result = [...value];
result.splice(index, 1, { const i = result.findIndex((x) => x._id === record._id);
result.splice(i, 1, {
...record, ...record,
status: newStatus, isDisabled: newStatus,
}); });
setValue(result); setValue(result);
} else { } else {
notification.error({ message.error(data);
message: data, }
});
},
onCancel() {
console.log('Cancel');
},
});
};
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);
} }
}); });
}, },
@@ -296,31 +569,35 @@ const Crontab = () => {
}> = ({ record, index }) => ( }> = ({ record, index }) => (
<Dropdown <Dropdown
arrow arrow
trigger={['click', 'hover']} trigger={['click']}
overlay={ overlay={
<Menu onClick={({ key }) => action(key, record, index)}> <Menu onClick={({ key }) => action(key, record, index)}>
<Menu.Item key="edit" icon={<EditOutlined />}> <Menu.Item key="edit" icon={<EditOutlined />}>
</Menu.Item> </Menu.Item>
{record.isSystem !== 1 && ( <Menu.Item
<> key="enableOrDisable"
<Menu.Item icon={
key="enableordisable" record.isDisabled === 1 ? (
icon={ <CheckCircleOutlined />
record.status === CrontabStatus.disabled ? ( ) : (
<CheckCircleOutlined /> <StopOutlined />
) : ( )
<StopOutlined /> }
) >
} {record.isDisabled === 1 ? '启用' : '禁用'}
> </Menu.Item>
{record.status === CrontabStatus.disabled ? '启用' : '禁用'} <Menu.Item key="delete" icon={<DeleteOutlined />}>
</Menu.Item>
<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>
} }
> >
@@ -335,12 +612,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;
} }
@@ -354,14 +634,18 @@ 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.push(cron); result.unshift(cron);
} else { } else {
result.splice(index, 1, { result.splice(index, 1, {
...cron, ...cron,
@@ -376,6 +660,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,
@@ -385,8 +673,85 @@ const Crontab = () => {
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds);
setTimeout(() => {
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
setTableScrollHeight(getTableScroll());
}
});
};
const rowSelection = {
selectedRowIds,
onChange: onSelectChange,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE,
],
};
const delCrons = () => {
Modal.confirm({
title: '确认删除',
content: <></>,
onOk() {
request
.delete(`${config.apiPrefix}crons`, { data: selectedRowIds })
.then((data: any) => {
if (data.code === 200) {
message.success('批量删除成功');
setSelectedRowIds([]);
getCrons();
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const operateCrons = (operationStatus: number) => {
Modal.confirm({
title: `确认${OperationName[operationStatus]}`,
content: <>{OperationName[operationStatus]}</>,
onOk() {
request
.put(`${config.apiPrefix}crons/${OperationPath[operationStatus]}`, {
data: selectedRowIds,
})
.then((data: any) => {
if (data.code === 200) {
getCrons();
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const onPageChange = (page: number, pageSize: number | undefined) => {
setCurrentPage(page);
setPageSize(pageSize as number);
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);
setIsLogModalVisible(true); setIsLogModalVisible(true);
} }
}, [logCron]); }, [logCron]);
@@ -396,22 +761,16 @@ const Crontab = () => {
}, [searchText]); }, [searchText]);
useEffect(() => { useEffect(() => {
if (document.body.clientWidth < 768) { setPageSize(parseInt(localStorage.getItem('pageSize') || '20'));
setWdith('auto'); setTimeout(() => {
setMarginLeft(0); setTableScrollHeight(getTableScroll());
setMarginTop(0); });
} else {
setWdith('100%');
setMarginLeft(0);
setMarginTop(-72);
}
}, []); }, []);
return ( return (
<PageContainer <PageContainer
className="code-mirror-wrapper" className="ql-container-wrapper crontab-wrapper"
title="定时任务" title="定时任务"
loading={loading}
extra={[ extra={[
<Search <Search
placeholder="请输入名称或者关键词" placeholder="请输入名称或者关键词"
@@ -421,34 +780,81 @@ const Crontab = () => {
onSearch={onSearch} onSearch={onSearch}
/>, />,
<Button key="2" type="primary" onClick={() => addCron()}> <Button key="2" type="primary" onClick={() => addCron()}>
</Button>, </Button>,
]} ]}
header={{ header={{
style: { style: headerStyle,
padding: '4px 16px 4px 15px',
position: 'sticky',
top: 0,
left: 0,
zIndex: 20,
marginTop,
width,
marginLeft,
},
}} }}
> >
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button type="primary" style={{ marginBottom: 5 }} onClick={delCrons}>
</Button>
<Button
type="primary"
onClick={() => operateCrons(0)}
style={{ marginLeft: 8, marginBottom: 5 }}
>
</Button>
<Button
type="primary"
onClick={() => operateCrons(1)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={() => operateCrons(2)}
>
</Button>
<Button type="primary" onClick={() => operateCrons(3)}>
</Button>
<Button
type="primary"
onClick={() => operateCrons(4)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => operateCrons(5)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span>
</div>
)}
<Table <Table
columns={columns} columns={columns}
pagination={{ pagination={{
hideOnSinglePage: true, current: currentPage,
onChange: onPageChange,
pageSize: pageSize,
showSizeChanger: true, showSizeChanger: true,
simple: isPhone,
defaultPageSize: 20, defaultPageSize: 20,
showTotal: (total: number, range: number[]) =>
`${range[0]}-${range[1]} 条/总共 ${total}`,
}} }}
dataSource={value} dataSource={value}
rowKey="_id" rowKey="_id"
size="middle" size="middle"
bordered scroll={{ x: 1000, y: tableScrollHeight }}
scroll={{ x: 768 }} loading={loading}
rowSelection={rowSelection}
rowClassName={getRowClassName}
/> />
<CronLogModal <CronLogModal
visible={isLogModalVisible} visible={isLogModalVisible}
+90 -11
View File
@@ -1,13 +1,20 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Modal, notification, Input, Form } 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 {
Loading3QuartersOutlined,
CheckCircleOutlined,
} from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-layout';
enum CrontabStatus { enum CrontabStatus {
'running', 'running',
'idle', 'idle',
'disabled', 'disabled',
'queued',
} }
const { Countdown } = Statistic;
const CronLogModal = ({ const CronLogModal = ({
cron, cron,
@@ -20,6 +27,9 @@ 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 [executing, setExecuting] = useState<any>(true);
const [isPhone, setIsPhone] = useState(false);
const [theme, setTheme] = useState<string>('');
const getCronLog = (isFirst?: boolean) => { const getCronLog = (isFirst?: boolean) => {
if (isFirst) { if (isFirst) {
@@ -28,12 +38,40 @@ const CronLogModal = ({
request request
.get(`${config.apiPrefix}crons/${cron._id}/log`) .get(`${config.apiPrefix}crons/${cron._id}/log`)
.then((data: any) => { .then((data: any) => {
const log = data.data as string; if (localStorage.getItem('logCron') === cron._id) {
setValue(log || '暂无日志'); const log = data.data as string;
if (log && !log.includes('执行结束')) { setValue(log || '暂无日志');
setTimeout(() => { setExecuting(
getCronLog(); log && !log.includes('执行结束') && !log.includes('重启面板'),
}, 2000); );
if (log && !log.includes('执行结束') && !log.includes('重启面板')) {
setTimeout(() => {
getCronLog();
}, 2000);
}
if (
log &&
log.includes('重启面板') &&
cron.status === CrontabStatus.running
) {
message.warning({
content: (
<span>
<Countdown
className="inline-countdown"
format="ss"
value={Date.now() + 1000 * 10}
/>
</span>
),
duration: 10,
});
setTimeout(() => {
window.location.reload();
}, 10000);
}
} }
}) })
.finally(() => { .finally(() => {
@@ -44,26 +82,67 @@ const CronLogModal = ({
}; };
const cancel = () => { const cancel = () => {
localStorage.removeItem('logCron');
handleCancel(); handleCancel();
}; };
const titleElement = () => {
return (
<>
{(executing || loading) && <Loading3QuartersOutlined spin />}
{!executing && <CheckCircleOutlined />}
<span style={{ marginLeft: 5 }}>-{cron && cron.name}</span>{' '}
</>
);
};
useEffect(() => { useEffect(() => {
if (cron) { if (cron) {
getCronLog(true); getCronLog(true);
} }
}, [cron]); }, [cron]);
useEffect(() => {
setIsPhone(document.body.clientWidth < 768);
}, []);
return ( return (
<Modal <Modal
title={`日志-${cron && cron.name}`} title={titleElement()}
visible={visible} visible={visible}
centered
className="log-modal"
bodyStyle={{
overflowY: 'auto',
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>,
]}
> >
<pre style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word' }}> {loading ? (
{!loading && value} <PageLoading />
</pre> ) : (
<pre
style={
isPhone
? {
fontFamily: 'Source Code Pro',
width: 375,
zoom: 0.83,
}
: {}
}
>
{value}
</pre>
)}
</Modal> </Modal>
); );
}; };
+25 -21
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Modal, notification, Input, Form } from 'antd'; import { Modal, message, Input, Form } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
import cronParse from 'cron-parser'; import cronParse from 'cron-parser';
@@ -27,25 +27,17 @@ const CronModal = ({
data: payload, data: payload,
}); });
if (code === 200) { if (code === 200) {
notification.success({ message.success(cron ? '更新Cron成功' : '添加Cron成功');
message: cron ? '更新Cron成功' : '添加Cron成功',
});
} else { } else {
notification.error({ message.error(data);
message: data,
});
} }
setLoading(false); setLoading(false);
handleCancel(data); handleCancel(data);
}; };
useEffect(() => { useEffect(() => {
if (cron) { form.resetFields();
form.setFieldsValue(cron); }, [cron, visible]);
} else {
form.resetFields();
}
}, [cron]);
return ( return (
<Modal <Modal
@@ -64,23 +56,35 @@ const CronModal = ({
}} }}
onCancel={() => handleCancel()} onCancel={() => handleCancel()}
confirmLoading={loading} confirmLoading={loading}
destroyOnClose
> >
<Form form={form} layout="vertical" name="form_in_modal" preserve={false}> <Form
form={form}
layout="vertical"
name="form_in_modal"
initialValues={cron}
>
<Form.Item name="name" label="名称"> <Form.Item name="name" label="名称">
<Input /> <Input placeholder="请输入任务名称" />
</Form.Item> </Form.Item>
<Form.Item name="command" label="任务" rules={[{ required: true }]}> <Form.Item
<Input /> 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"
label="时间" label="定时规则"
rules={[ rules={[
{ required: true }, { required: true },
{ {
validator: (rule, value) => { validator: (rule, value) => {
if (cronParse.parseString(value).expressions.length > 0) { if (cronParse.parseExpression(value).hasNext()) {
return Promise.resolve(); return Promise.resolve();
} else { } else {
return Promise.reject('Cron表达式格式有误'); return Promise.reject('Cron表达式格式有误');
@@ -89,7 +93,7 @@ const CronModal = ({
}, },
]} ]}
> >
<Input /> <Input placeholder="秒(可选) 分 时 天 月 周" />
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>
+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;
+11 -4
View File
@@ -1,12 +1,19 @@
.d2h-files-diff { .d2h-files-diff {
height: calc(100vh - 130px); height: calc(100vh - 130px);
overflow: auto; height: calc(100vh - var(--vh-offset, 0px) - 130px);
overflow: auto;
} }
.d2h-code-side-linenumber { .d2h-code-side-linenumber {
position: relative; position: relative;
} }
.d2h-code-side-line { .d2h-code-side-line {
padding: 0 0.5em; padding: 0 0.5em;
}
.diff-switch-file {
.ant-form-item {
margin-bottom: 8px;
}
} }

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