Compare commits

..
449 Commits
Author SHA1 Message Date
whyour 5dd8d9df91 更新版本 v2.10.9 2021-11-22 22:27:15 +08:00
whyour 2dbee94e85 默认拷贝deps目录下所有文件 2021-11-22 22:20:13 +08:00
风之凌殇andGitHub 9001797037 表格选择pageSize增加200/500/1000 (#934) 2021-11-22 19:26:02 +08:00
kilo5hzandGitHub 5e7104d5a3 系统通知增加gotify (#936) 2021-11-22 19:23:40 +08:00
whyour 7c33b22d63 修改docker基础镜像为alpine3.12,解决arm32无法启动 2021-11-22 19:22:39 +08:00
whyour b7cb1e379c 更新readme 2021-11-21 22:26:22 +08:00
whyour 274a6ce52e 修复调试功能 2021-11-20 01:06:25 +08:00
kilo5hzandwhyour 8079e4e7b8 点击任务名跳转脚本管理编辑脚本 (#929)
* 增加捷径:点击任务名跳转脚本管理编辑脚本

* 修正树节点选中,父节点自动展开
2021-11-20 01:06:25 +08:00
whyour 920efb8fab 更新readme图片链接 2021-11-20 01:06:25 +08:00
whyour 92aeb2b9da 更新readme 2021-11-20 01:06:25 +08:00
whyour ced76cf343 更新readme 2021-11-20 01:06:25 +08:00
whyour 3750d6ecd5 增加初始化依赖目录及文件 2021-11-20 01:06:25 +08:00
风之凌殇andwhyour 7444f30384 移除dockerfile中不必要的环境变量 (#925)
* 移除上次pr新增的不必要的环境变量
2021-11-20 01:06:16 +08:00
whyour b0fd5ba47f 更新readme 2021-11-17 19:22:40 +08:00
whyour cc098bc2e5 修复拷贝deps已存在依赖 2021-11-17 10:23:45 +08:00
whyour 1cfcf8d194 更新版本 v2.10.8 2021-11-16 14:36:13 +08:00
whyour d1e9f741d0 修复脚本管理删除、新建增加选择父目录 2021-11-16 14:18:02 +08:00
kilo5hzandGitHub 0cde9d07ee 增加gotify推送 (#905)
* 增加gotify推送
2021-11-15 23:04:55 +08:00
whyour a266425348 修复拷贝deps 2021-11-13 22:05:54 +08:00
whyour 6dbe02e321 修复dep目录 2021-11-13 21:54:45 +08:00
whyour 39979879c9 更新版本 v2.10.7 2021-11-13 21:21:35 +08:00
whyour ba3f604a5c 修复bot安装依赖判断 2021-11-13 20:58:27 +08:00
whyour 9572582401 修改拷贝依赖 2021-11-13 19:05:34 +08:00
whyour 2bcef0ba75 修复脚本管理搜索 2021-11-13 17:42:06 +08:00
whyour 31cd7c4007 外部依赖改为系统直接分发通知文件js和py版 2021-11-13 17:21:27 +08:00
whyour 1c4c1799b0 修复环境变量表格样式 2021-11-13 16:44:21 +08:00
whyour 2d558df15e 修复schedule服务 2021-11-13 12:34:49 +08:00
whyour 7b769da4e2 修复脚本管理树结构 2021-11-13 11:14:43 +08:00
whyour 4e95d62933 更新版本 v2.10.6 2021-11-13 00:03:12 +08:00
whyour 83255280b9 修复检查更新 2021-11-12 23:55:57 +08:00
whyour 2546cebdea 修复定时匹配 2021-11-12 23:42:11 +08:00
whyour f14a3fe0f1 修复脚本管理数据 2021-11-12 23:18:24 +08:00
whyour c3eb845be4 修改workflow变量 2021-11-12 22:09:02 +08:00
MiniersandGitHub 1eef77dca5 增加环境变量更新时间 (#887) 2021-11-12 21:55:03 +08:00
风之凌殇andGitHub d7d9f47fe5 增加在添加语义化版本tag时自动生成static并构建多个不同tag的容器的workflow,简化版本发布流程 (#888) 2021-11-12 21:54:42 +08:00
whyour fce6a996d9 更新版本号 v2.10.5 2021-11-12 14:23:16 +08:00
whyour 48a484d7de 修复文件判断引起服务异常 2021-11-12 14:22:21 +08:00
whyour 4b81d3c612 更新版本号 2021-11-12 13:18:32 +08:00
whyour 21b6edd7eb 修复entrypoint启动失败 2021-11-12 13:16:53 +08:00
whyour 9a43e022e9 修改版本说明序号 2021-11-11 23:04:29 +08:00
whyour 62bef3f13d 更新版本号 v2.10.3 2021-11-11 23:03:28 +08:00
whyour c87f5fa004 服务重启时依赖改为单独安装,避免同时失败 2021-11-11 22:56:47 +08:00
whyour 8d99f3ffbb 修复获取日志判断 2021-11-11 22:50:28 +08:00
whyour 100ca7b54b 单个数据库文件大于500兆时,裁剪文件,避免服务无法启动 2021-11-11 22:41:12 +08:00
whyour 4a2624f12a 脚本管理改成树结构 2021-11-11 00:10:05 +08:00
whyour 21b01a85a8 修复pushplus通知设置 2021-11-10 23:33:19 +08:00
whyour e039ae436e 修复依赖文件获取 2021-11-10 23:26:21 +08:00
whyour b13a1a45f8 修复repo依赖路径 2021-11-10 10:35:53 +08:00
whyour 667fe2bbba 修复shell语法 2021-11-09 23:55:50 +08:00
whyour f083871ca0 修复repo文件比对 2021-11-09 23:23:07 +08:00
whyour 782e1ba235 修改ql repo逻辑 2021-11-09 23:07:43 +08:00
whyour b92c6edc4e 添加动态引入 2021-11-09 22:34:24 +08:00
whyour bdf4657c73 修改退出下拉样式 2021-11-08 22:56:53 +08:00
whyour 8559438611 修改数据库加载 2021-11-08 22:47:46 +08:00
whyour 04045fd410 修复centos -x兼容性 2021-11-07 21:54:31 +08:00
whyour 1c71a9d8a8 回退shell [ 2021-11-07 19:18:36 +08:00
whyour 9a9ae9782c 修改pnpm安装源 2021-11-07 12:51:10 +08:00
whyour da2569850b 修复entrypoint 2021-11-07 11:25:18 +08:00
whyour b475035415 修复 arm 没有nginx conf.d 目录 2021-11-07 11:08:21 +08:00
OreoandGitHub 7baee27585 更新 Python 通知文件 (#832)
已在 Python3.6.x(云函数平台),3.8.x,3.10.0 测试通过
特性如下
1. 支持 11 种推送方式,未来还会继续添加
2. 多线程通知
3. 支持一言(一句话)和控制台输出开关
4. 对于现有推送方式,补充了配置变量
2021-11-06 20:45:53 +08:00
whyour 2cb2f1ce0a 更新dockerfile 2021-11-06 20:19:07 +08:00
whyour 8c1407e2a9 修改dockerfile 2021-11-06 19:04:01 +08:00
whyour 7bcb113085 更新docker yml 2021-11-06 18:58:18 +08:00
whyour 42dc27addf 修复任务日志结束时间展示 2021-11-06 00:50:44 +08:00
whyour aac30398c4 修复toLocaleString 0点显示24点 2021-11-05 23:56:16 +08:00
whyour ceae888372 修改shell语法 2021-11-05 23:39:22 +08:00
whyour 61df7050db 修复移动端展开导航 2021-11-04 19:24:55 +08:00
whyour af5561cbbf 更新版本号 v2.10.2 2021-11-04 18:10:43 +08:00
whyour bce30f922d 修复基础镜像pip3依赖 2021-11-04 18:10:14 +08:00
whyour d6ed394557 修复openapi路由 2021-11-04 18:07:34 +08:00
whyour 7b512a1681 更新版本号 v2.10.1 2021-11-04 15:09:07 +08:00
whyour 7af0597cd5 添加白屏时间及错误上报 2021-11-04 15:05:34 +08:00
whyour 2f5cf6d93c 修复check命令 2021-11-03 16:35:18 +08:00
whyour d672d2d104 修改失败任务日志 2021-11-01 23:06:23 +08:00
whyour c0d1fb1ef4 shell 中 [ 改为 [[ 2021-11-01 23:04:10 +08:00
whyour 3c882ad0ce 移动退出登录位置 2021-11-01 15:23:06 +08:00
whyour 58acd2eb68 修复表格滚动高度 2021-10-31 19:45:51 +08:00
whyour 590473e7d2 修复无法覆盖安装依赖 2021-10-31 17:34:19 +08:00
21139157c8 修改错误文字 (#809)
Co-authored-by: 14K <9112790+the14k@user.noreply.gitee.com>
2021-10-29 23:26:44 +08:00
whyour 8f8296ab64 支持服务端端口直接访问前端 2021-10-28 23:35:21 +08:00
whyour 40bf07d2a4 修复置顶后编辑置顶消失 2021-10-28 22:38:34 +08:00
whyour fd2b4fb86d 更新版本号 v2.10.0 2021-10-24 17:51:20 +08:00
whyour 783e867ef6 修复python移除依赖命令 2021-10-24 17:19:14 +08:00
whyour 3873d9f978 添加依赖管理 2021-10-24 17:07:16 +08:00
whyour 7dece938c8 依赖安装日志增加安装时间 2021-10-24 11:11:23 +08:00
whyour 0842d66c79 修复表格初次获取滚动高度 2021-10-23 23:44:11 +08:00
whyour d071e50d62 由于浏览器兼容性,移除dynamicImport 2021-10-23 23:31:46 +08:00
whyour 337f01aff1 修复python3任务无法停止 2021-10-23 23:22:08 +08:00
whyour 5614e41eb0 修复service引入路径 2021-10-23 18:46:24 +08:00
whyour 4f0fdbe453 初始化安装所有依赖 2021-10-23 18:44:25 +08:00
whyour 49c068db78 修改依赖安装结束标识 2021-10-23 18:27:20 +08:00
whyour fbba44d482 添加依赖管理 2021-10-23 18:23:32 +08:00
whyour 10c07b0a09 两步验证输入框自动聚焦 2021-10-21 22:25:15 +08:00
whyour 20893698ba 更新版本号 v2.9.10 2021-10-21 21:59:24 +08:00
longjuanandGitHub 31de659ed3 更新install.md (#789) 2021-10-21 21:55:26 +08:00
whyour 6c08a16c66 修改websocket日志 2021-10-21 21:52:06 +08:00
whyour 21e7178f48 修复table滚动高度 2021-10-21 19:13:38 +08:00
whyour aefd267eb2 更新monaco 2021-10-19 22:41:59 +08:00
whyour fb1a3980d4 检查密码默认值 2021-10-19 21:46:24 +08:00
whyour cfc467f8a5 修复tg系统通知认证参数 2021-10-18 19:18:46 +08:00
whyour 48120f012d 修复检查更新日志展示 2021-10-18 17:05:38 +08:00
whyour c907a78437 修复shell变量重复 2021-10-16 19:32:23 +08:00
whyour 915c0d09a9 升级umi 2021-10-16 17:21:19 +08:00
whyour 80638862cc 添加 yarn.lock 2021-10-16 17:07:05 +08:00
whyour a7350948cd 更新版本号 v2.9.9 2021-10-16 15:19:53 +08:00
whyour 1e1be013b5 脚本管理移动端支持添加脚本、编辑脚本 2021-10-16 15:11:23 +08:00
hanhh 4062584eae 移除无用代码 2021-10-16 13:39:18 +08:00
hanhh abedddffa6 修复表格样式 2021-10-16 10:53:14 +08:00
hanhh 38858ccdb2 修改pm2任务日志 2021-10-15 22:32:32 +08:00
hanhh d56e6359d0 修复更新日志匹配正则 2021-10-15 22:14:42 +08:00
hanhh 5886991464 放开ql update删除操作 2021-10-15 22:01:34 +08:00
hanhh 838326616b 更新版本号 v2.9.8-001 2021-10-15 11:47:32 +08:00
hanhh fcf80fe946 修复设置删除日志 2021-10-15 10:53:27 +08:00
phoenixandGitHub 2e63a7056c 修复nginx.conf错误 (#776)
Fix nginx: [emerg] unknown directive.
Log
`nginx: [emerg] unknown directive "client_body_buffer_size:" in /etc/nginx/nginx.conf:18
`
2021-10-15 10:40:58 +08:00
hanhh 7c807e3ec8 更新版本 2.9.8 2021-10-14 22:07:21 +08:00
hanhh b558aa0934 修复初始化流程 2021-10-14 22:00:36 +08:00
hanhh 38757758cb 增加日志删除频率设置 2021-10-14 10:58:54 +08:00
hanhh 84dd3d8fd1 修改版本更新执行命令 2021-10-13 23:50:25 +08:00
hanhh 19423c2d30 添加ql -l update 2021-10-13 23:45:46 +08:00
hanhh 98bba7d7fa 更新nginx配置文件 2021-10-13 23:31:38 +08:00
hanhh dcaa9ce000 修复websocket连接域名 2021-10-13 23:09:48 +08:00
hanhh 3cae44a136 修复版本号匹配正则 2021-10-13 22:53:10 +08:00
hanhh df8a3d2692 修复版本号匹配 2021-10-13 22:45:24 +08:00
hanhh 56e43f1808 移除远程版本更新链接 2021-10-13 22:41:59 +08:00
hanhh 2f6b95cccf 修复获取最后版本文件 2021-10-13 22:39:59 +08:00
hanhh ad9f6b2b64 修复获取最新版本号 2021-10-13 22:05:54 +08:00
hanhh 165c3dc976 容器启动忽略config和env中的错误 2021-10-13 14:25:38 +08:00
hanhh cbe6390b41 修复版本检查 2021-10-12 23:06:28 +08:00
hanhh 026eaf6606 完善检查更新操作 2021-10-12 22:54:17 +08:00
hanhh ee184f8e62 并发命令支持指定账号 2021-10-12 21:58:41 +08:00
hanhh 5745231dc9 添加系统更新操作和设置删除日志频率 2021-10-12 00:27:42 +08:00
hanhh cbdc3a9861 添加检查更新和删除日志频率操作 2021-10-08 23:02:45 +08:00
hanhh ba5c42ad6d 修复表格滚动样式和交互 2021-10-08 22:39:17 +08:00
hanhh b056a731a1 增加新版本检测 2021-10-07 15:26:25 +08:00
hanhh b4cd94f2d9 增加环境变量名称正则校验 2021-10-06 16:38:46 +08:00
hanhh 3b8cb43f02 添加新版本提醒 2021-10-06 16:22:53 +08:00
hanhh 82cb11c3c4 更新版本号 v2.9.7 2021-10-03 22:26:16 +08:00
SowevoandGitHub eb7522a90b 增加企业微信机器人推送的python版本 (#748) 2021-10-03 22:16:07 +08:00
hanhh 3898201b1e 修复登录两步提醒样式 2021-10-03 22:05:56 +08:00
hanhh bb6ab0c17b 修改初始化判断条件 2021-10-03 22:01:44 +08:00
hanhh 652c40ab1f 增加初始化过程 2021-10-03 20:58:55 +08:00
hanhh ca0d6215a9 修复entrypoint启动nginx配置 2021-09-30 23:49:19 +08:00
hanhh 61d155257c ql update添加patch 2021-09-30 19:14:40 +08:00
hanhh 91399f1a1f 移除check中init git 2021-09-30 19:08:13 +08:00
hanhh 2e738c5977 更新版本号 v2.9.6 2021-09-29 23:16:59 +08:00
hanhh c0bd51cf03 修复类型检查 2021-09-29 23:15:09 +08:00
hanhh ca5afe04f8 修复退出登录获取token出错 2021-09-29 23:09:13 +08:00
hanhh 47db0b9f4e 对比工具支持对比config目录下的文件 2021-09-29 22:48:49 +08:00
hanhh 27f17a377a 重启服务时自动压缩db 2021-09-29 21:40:35 +08:00
hanhh ecaf731ecf 修复loading状态位置 2021-09-29 21:19:25 +08:00
hanhh 9fba2a85f6 修复清空文件操作 2021-09-27 22:04:47 +08:00
hanhh 93f75be182 task和ql命令添加基础目录检测 2021-09-27 19:38:06 +08:00
hanhh d537901f59 测试动态加载 2021-09-25 12:11:13 +08:00
hanhh 260c1ed4a4 修改定时任务列表滚动宽度 2021-09-25 11:40:55 +08:00
hanhh ac2bd52463 修复status接口参数 2021-09-25 02:00:50 +08:00
hanhh 16c6f8fee1 修复并发参数 2021-09-25 01:51:18 +08:00
hanhh 8a0db21f17 更新版本号 2021-09-25 00:43:05 +08:00
hanhh 9ab08a71c0 增加登录设备展示 2021-09-25 00:42:03 +08:00
hanhh 381c32a3b4 修复更新任务状态api 2021-09-25 00:28:18 +08:00
hanhh 350c0f10c4 修复ql resettfa命令 2021-09-25 00:15:23 +08:00
hanhh d5b982f813 ql resettfa增加兼容字段 2021-09-24 23:33:52 +08:00
hanhh 6587375ff2 移除定时任务左右列固定n 2021-09-24 23:31:50 +08:00
hanhh f5ecb81bad ql bot移除自动更新远程仓库 2021-09-24 23:22:40 +08:00
hanhh 0141cd81c4 修复服务端ts类型检查 2021-09-24 22:49:14 +08:00
hanhh ff412c7497 更新版本号 2021-09-24 22:30:09 +08:00
hanhh 623f239f91 修复定时任务操作宽度 2021-09-24 22:28:25 +08:00
hanhh c7ba304c6a 支持多端登录 2021-09-24 22:24:39 +08:00
hanhh c8393604d1 修复task运行指定账号 2021-09-24 00:24:54 +08:00
hanhh c5d47a67aa 修复定时任务列表下次运行时间 2021-09-24 00:11:14 +08:00
hanhh 3a1423cd59 task支持指定账号运行 2021-09-23 23:59:53 +08:00
hanhh d57f4cf6cd 修改定时任务表格布局 2021-09-23 22:12:19 +08:00
hanhh af420942c4 添加最后运行时间和运行时长 2021-09-23 13:42:32 +08:00
hanhh eac4e5e66f 修复主题切换全局class未变化 2021-09-21 23:29:38 +08:00
hanhh b8822a98f7 定时任务添加下次运行时间 2021-09-21 20:11:39 +08:00
hanhh 95322b42c0 修改登录通知消息 2021-09-20 23:56:20 +08:00
hanhh 33f5a06d32 修复pushplus通知参数 2021-09-20 22:23:56 +08:00
hanhh a7e5126c92 修复新建文件 2021-09-20 00:59:38 +08:00
hanhh c62f3e5b8e 添加bak目录ignore 2021-09-20 00:47:58 +08:00
hanhh 5d3f413a05 修改测试通知内容 2021-09-19 23:10:58 +08:00
hanhh 644154ca38 更新版本号 2021-09-19 23:00:53 +08:00
hanhh cccce5ff74 修复通知 2021-09-19 23:00:21 +08:00
hanhh aa1fe98188 定时任务增加置顶功能 2021-09-19 22:25:57 +08:00
hanhh b6a78550d6 添加全局主题className 2021-09-19 21:27:20 +08:00
hanhh 70b7f2c493 脚本管理支持新增文件 2021-09-19 21:23:03 +08:00
hanhh ea44e5f209 修复pageTitle 2021-09-18 23:27:50 +08:00
hanhh 41ef87d9f8 任务详情日志添加loading 2021-09-18 16:33:12 +08:00
hanhh c0b4a70aa0 添加全局loading 2021-09-18 15:08:45 +08:00
hanhh 955875cb7a 修复关闭通知 2021-09-18 02:09:06 +08:00
hanhh 827e8571aa 添加通知设置页面 2021-09-18 01:44:55 +08:00
whyour aed08b0f59 添加通知api 2021-09-17 18:06:14 +08:00
hanhh 3ddaf41e27 更新通知服务 2021-09-17 00:35:27 +08:00
whyour 38a9cce6bf 更新通知服务 2021-09-16 21:01:29 +08:00
whyour ae255ef381 添加通知服务 2021-09-16 18:01:25 +08:00
prcxuandGitHub 4316bef01e 更新安装说明 (#705) 2021-09-15 11:18:58 +08:00
JH2628andGitHub 8491c5706d 修复config sample序号 (#694) 2021-09-14 19:25:00 +08:00
hanhh 7b7fab1f32 修改系统异常提醒 2021-09-13 00:12:44 +08:00
hanhh 5a6fccdc2f 更新tg链接 2021-09-12 20:28:43 +08:00
hanhh 23668be46d 更新版本号 2021-09-11 13:53:58 +08:00
hanhh f92f2c1666 修复写入任务错误日志逻辑 2021-09-11 13:53:24 +08:00
hanhh 895740a3cb 修改获取用户数据逻辑 2021-09-11 02:03:31 +08:00
hanhh 4c76c3231f 移除自动压缩db数据 2021-09-09 13:02:49 +08:00
hanhh ca9d3f9559 更新版本号 2021-09-08 12:26:21 +08:00
hanhh 3ca3c5e75c 修复开启两步失败提醒 2021-09-08 12:26:05 +08:00
hanhh c2c0b1dfdc 添加应用名称改为选填 2021-09-08 12:15:30 +08:00
hanhh c633774487 修复两步验证逻辑 2021-09-08 12:13:19 +08:00
hanhh c2d86fc517 更新版本号 2021-09-07 23:53:01 +08:00
hanhh d7c8d150ca 修改登陆日志title 2021-09-07 23:52:40 +08:00
L1n3andGitHub 6b8a4e65f2 添加安装说明 (#667) 2021-09-07 17:46:53 +08:00
LeonandGitHub 482e43dd1f 修复 Python 和 JavaScript Server酱和企业微信推送的环境变量不一致 (#673) 2021-09-07 17:46:14 +08:00
hanhh 1437babc6c 增加登录日志 2021-09-07 01:40:41 +08:00
Zy143LandGitHub 5ed882943a 修复python和JavaScript钉钉推送的环境变量不一致 (#659) 2021-09-05 23:53:18 +08:00
hanhh e678b3fb3a config文件读取添加黑名单 2021-09-05 23:48:58 +08:00
hanhh a5770bc4e4 添加环境变量命令input改为textarea 2021-09-05 23:38:36 +08:00
hanhh 59821bb939 修改ql check依赖安装逻辑 2021-09-03 22:30:24 +08:00
hanhh b58040be44 修改两步验证逻辑 2021-09-03 19:09:09 +08:00
hanhh be3b8ad52e 修复环境变量拖拽 2021-09-02 19:16:28 +08:00
hanhh 0fdaf97461 更新两步验证逻辑 2021-09-01 23:03:27 +08:00
L1n3andGitHub f5f036372f 添加 docker-compose.yml (#629) 2021-08-31 15:52:24 +08:00
hanhh 3fceb983e6 增加上次登录状态 2021-08-31 15:29:23 +08:00
hanhh ae6a39196d 增加ql resettfa禁用两步验证 2021-08-31 11:06:20 +08:00
hanhh 2810ff2753 更新版本号 2021-08-31 10:56:37 +08:00
hanhh 205c0cf8d3 两步验证失败增加ip记录 2021-08-31 10:54:46 +08:00
hanhh d9c801a091 添加两步验证 2021-08-30 23:37:26 +08:00
hanhh 0833aa3b30 增加nedb数据自定比对,测试对db文件不断增大的影响 2021-08-29 00:52:21 +08:00
hanhh 9c90bbc146 修复移动端无法打开脚本管理、任务日志 2021-08-29 00:34:47 +08:00
hanhh 231b387076 更换ip地址查询接口 2021-08-27 14:00:11 +08:00
hanhh 3de432652f 更换ip查询接口 2021-08-27 13:27:32 +08:00
hanhh cad714493e 更新版本号 2021-08-27 11:08:08 +08:00
hanhh 6a690a6108 更新nginx配置 2021-08-27 11:04:31 +08:00
hanhh 1b85c52557 修改新建应用placeholder 2021-08-27 10:52:49 +08:00
hanhh b37be92ea9 搜索时去掉前后空格 2021-08-27 10:47:30 +08:00
HeroWongandGitHub f95979f445 fix: 只有一页时不隐藏分页栏,确保用户可以更改 pagesize (#613) 2021-08-26 21:27:22 +08:00
hanhh 09e07218ae 添加openapi模块 2021-08-26 19:01:39 +08:00
hanhh 085f5a9451 更新版本号 2021-08-25 23:55:16 +08:00
hanhh eb4e935554 修复cron分隔符,修复登录获取ip逻辑 2021-08-25 23:54:42 +08:00
hanhh 57986cf425 修复登录倒计时颜色 2021-08-25 18:58:45 +08:00
hanhh 7bf14b9acc 更新版本号 2021-08-25 18:20:29 +08:00
hanhh 3ba0be34d9 修复6位cron判断 2021-08-25 18:19:22 +08:00
hanhh 0f828a3c75 修复群辉update之后403 2021-08-25 17:58:21 +08:00
hanhh 16a768a142 更新版本号 2021-08-24 18:21:19 +08:00
hanhh f380d20279 添加重置登录错误次数命令 2021-08-24 18:20:36 +08:00
hanhh be908ab36c 添加默认定时规则配置 2021-08-23 19:10:57 +08:00
hanhh b545454537 task传递参数给task_before和task_after 2021-08-23 15:58:25 +08:00
hanhh a5ff8dd1d6 更新readme图片 2021-08-23 15:31:13 +08:00
hanhh 59f23e518b 修复readme图片 2021-08-23 15:26:30 +08:00
hanhh 858b3f0176 更新版本号 2021-08-23 13:50:44 +08:00
hanhh 89b8494ad0 修复获取ip api失败时,无法登陆
修复禁用或者运行定时任务出现重复
2021-08-23 13:44:54 +08:00
hanhh 64ecd75bd5 修复定时任务搜索后页码 2021-08-23 12:29:29 +08:00
hanhh 138eb3393d 修复error_log文件读取 2021-08-23 11:41:34 +08:00
hanhh 1ad51d2925 更新版本号 2021-08-23 10:47:11 +08:00
hanhh 317e175bca 回退update.sh 2021-08-23 10:43:26 +08:00
hanhh 0e287aa7d1 修复task_before eval 2021-08-22 22:33:07 +08:00
hanhh 28018b705b 修复未产生日志文件cat报错 2021-08-22 22:02:31 +08:00
hanhh 46ec72d6be ql命令增加-l参数 2021-08-22 21:57:10 +08:00
hanhh 82494a87af 修改task.sh任务顺序 2021-08-22 20:54:40 +08:00
hanhh 95c19b447a 修复task语法 2021-08-22 18:57:52 +08:00
hanhh 9c6eae936a 修复task.sh执行语法 2021-08-22 18:21:31 +08:00
hanhh 77605a9360 修改task.sh中参数名 2021-08-22 16:05:58 +08:00
hanhh 82880c3f06 修复最后登录ip读取 2021-08-22 15:25:16 +08:00
hanhh 11143c771b 更新版本号 2021-08-22 15:23:58 +08:00
hanhh 0968eaa5a8 修复任务执行参数出错 2021-08-22 15:23:12 +08:00
hanhh 0687f9d472 修改登陆成功提示文本 2021-08-22 14:47:07 +08:00
hanhh 25f0f5951a 更新版本号 2021-08-22 14:07:43 +08:00
hanhh eec0ce65ad 修复获取访问ip逻辑 2021-08-22 14:07:00 +08:00
hanhh be4e100168 更新版本号 2021-08-22 12:59:54 +08:00
hanhh 59e4f1063c 修复登录重试判断 2021-08-22 12:59:34 +08:00
hanhh 359e21f02b 登录增加错误延迟提醒 2021-08-22 12:28:45 +08:00
hanhh 6894bd9c0e 登录增加最后登录时间地点,登录失败增加重试间隔 2021-08-22 01:22:23 +08:00
hanhh 7040f02823 默认定时改为随机时间 2021-08-21 19:26:37 +08:00
hanhh 41408de337 修复更新环境变量备注无法删除 2021-08-21 18:34:53 +08:00
hanhh 72e243dce8 增加命令行运行日志实时打印 2021-08-21 17:26:41 +08:00
hanhh 77d4840e2a 定时任务,环境变量添加状态筛选 2021-08-20 16:31:24 +08:00
hanhh 0d5961821c 更新版本号 2021-08-20 16:17:04 +08:00
hanhh f34980b173 移除console 2021-08-20 14:26:12 +08:00
hanhh 5b21a6e529 手动运行任务添加错误捕获 2021-08-20 10:59:59 +08:00
hanhh 6dbb737d4b 修复脚本管理编辑交互,修复ico无法加载 2021-08-19 23:46:26 +08:00
hanhh 76ecfbaaa2 更新版本号 2021-08-19 14:47:07 +08:00
hanhh d9aacbcb92 定时任务支持任务名,定时,命令排序
环境变量支持名称排序
2021-08-19 14:45:42 +08:00
hanhh e088325d95 脚本管理支持编辑删除文件 2021-08-19 14:18:25 +08:00
hanhh b29dd2c580 修复未勾选拆分环境变量参数验证失败 2021-08-17 22:31:39 +08:00
hanhh 89e82fd5b0 修复未勾选拆分环境变量参数验证失败 2021-08-17 22:26:33 +08:00
hanhh 9f0b671d42 修复手动运行任务进程 2021-08-17 22:06:04 +08:00
hanhh 7de7c724dc 更新版本号 2021-08-17 18:35:53 +08:00
hanhh 3525b1d92a 支持批量创建同名称环境变量,日志和脚本管理PC页支持宽度拖拽 2021-08-17 18:34:55 +08:00
hanhh 30cea84886 修改暗黑模式默认值 2021-08-16 18:05:52 +08:00
DerekandGitHub 0c2cc7f575 更正ql"本脚本用法"中的序号 (#552) 2021-08-16 17:17:45 +08:00
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
119 changed files with 21586 additions and 12490 deletions
-34
View File
@@ -1,34 +0,0 @@
name: build static
on:
push:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '14'
- name: build front and back
run: |
yarn install
yarn build
yarn build-back
- name: copy to static repo
env:
# GITHUB_REPO: gitee.com/whyour/qinglong-static
GITHUB_REPO: github.com/whyour/qinglong-static
run: |
mkdir -p static
cd ./static
cp -rf ../dist ./ && cp -rf ../build ./
git init && git add .
git config user.name "whyour"
git config user.email "imwhyour@gmail.com"
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
git push --force --quiet "https://whyour:${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" master:master
+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 }}
+2 -24
View File
@@ -1,8 +1,6 @@
name: Publish Docker Image name: PublishDockerImage
on: on:
push: workflow_dispatch:
tags:
- v*
jobs: jobs:
build: build:
@@ -14,10 +12,8 @@ jobs:
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
@@ -31,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/v6,linux/arm/v7,linux/arm64,linux/s390x --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/v6,linux/arm/v7,linux/arm64,linux/s390x --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/v6,linux/arm/v7,linux/arm64,linux/s390x --tag whyour/qinglong:latest docker/
+5 -2
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',
], ],
}); });
+151 -43
View File
@@ -1,64 +1,172 @@
<p align="center"> <p align="center">
<a href="https://github.com/whyour/qinglong"> <a href="https://github.com/whyour/qinglong">
<img width="150" src="https://qinglong.whyour.cn/qinglong.png"> <img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
</a> </a>
</p> </p>
<h1 align="center">青龙(WIP)</h1> <h1 align="center">青龙</h1>
<div align="center"> <div align="center">
pythonjavaScript的定时任务管理面板 支持python3、javaScript、shell、typescript 的定时任务管理面板(A timed task management panel that supports typescript, javaScript, python3, and shell.
[![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-image]: https://img.shields.io/badge/donate-wechat-green?style=for-the-badge [docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
[donate-url]: https://qinglong.whyour.cn/nice.png
[build-status-image]: https://img.shields.io/docker/cloud/build/whyour/qinglong?style=for-the-badge
[build-status-url]: https://img.shields.io/docker/cloud/build/whyour/qinglong
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=for-the-badge
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong [docker-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>
[![](https://z3.ax1x.com/2021/11/18/I7KrTg.jpg)](https://whyour.cn)
简体中文 | [English](./README-en.md)
## 功能
- 支持多种脚本语言(python3、javaScript、shell、typescript
- 支持在线管理脚本、环境变量、配置文件
- 支持在线查看任务日志
- 支持秒级任务设置
- 支持系统级通知
- 支持暗黑模式
- 支持手机端操作
## 部署
### 本机部署
```bash
# 待完善
```
### docker 部署
1. docker 安装
```bash
sudo curl -sSL get.docker.com | sh
```
2. 启动容器
```bash
docker run -dit \
-v $PWD/ql/config:/ql/config \
-v $PWD/ql/log:/ql/log \
-v $PWD/ql/db:/ql/db \
-v $PWD/ql/repo:/ql/repo \
-v $PWD/ql/raw:/ql/raw \
-v $PWD/ql/scripts:/ql/scripts \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
whyour/qinglong:latest
```
### docker-compose 部署
1. docker-compose 安装
```bash
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
```
2. 启动容器
```bash
mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker-compose.yml
# 启动
docker-compose up -d
# 停止
docker-compose down
```
## 使用
1. 内置命令
```bash
# 更新并重启青龙
ql update
# 运行自定义脚本extra.sh
ql extra
# 添加单个脚本文件
ql raw <file_url>
# 添加单个仓库的指定脚本
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch>
# 删除旧日志
ql rmlog <days>
# 启动tg-bot
ql bot
# 检测青龙环境并修复
ql check
# 重置登录错误次数
ql resetlet
# 禁用两步登录
ql resettfa
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
task <file_path>
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
task <file_path> now
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
task <file_path> conc <env_name> <account_number>(可选的)
# 指定账号执行,无论是否设置了随机延迟,均立即运行
task <file_path> desi <env_name> <account_number>
```
2. 参数说明
* file_url: 脚本地址
* repo_url: 仓库地址
* whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串
* blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串
* dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响
* branch: 拉取仓库的分支
* days: 需要保留的日志的天数
* file_path: 任务执行时的文件路径
* env_name: 任务执行时需要并发或者指定时的环境变量名称
* account_number: 任务执行时指定某个环境变量需要执行的账号序号
## 链接
- [nevinee](https://gitee.com/evine)
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/)
- [Umijs3.0](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server)
## 开发
```bash
$ git clone git@github.com:whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
$ yarn install
$ yarn start-back
$ yarn start
```
打开你的浏览器,访问 http://127.0.0.1:5601
## 交流
[telegram频道](https://t.me/jiao_long)
## 名称来源
青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。 青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
《后汉书·律历志下》记载:日周于天,一寒一暑,四时备成,万物毕改,摄提迁次,青龙移辰,谓之岁。 《后汉书·律历志下》记载:日周于天,一寒一暑,四时备成,万物毕改,摄提迁次,青龙移辰,谓之岁。
在中国[二十八宿](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF)中,青龙是东方七宿(角、亢、氐、房、心、尾、箕)的总称。 在早期星宿信仰中,祂是最尊贵的天神。 但被道教信仰吸纳入其神系后,神格大跌,道教将其称为“孟章”,在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼,与白虎监兵神君一起,是道教的护卫天神。 在中国[二十八宿](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF)中,青龙是东方七宿(角、亢、氐、房、心、尾、箕)的总称。 在早期星宿信仰中,祂是最尊贵的天神。 但被道教信仰吸纳入其神系后,神格大跌,道教将其称为“孟章”,在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼,与白虎监兵神君一起,是道教的护卫天神。
## 多谢
* [nevinee](https://gitee.com/evine)
* [crontab-ui](https://github.com/alseambusher/crontab-ui)
* [Ant Design](https://ant.design)
* [Ant Design Pro](https://pro.ant.design/)
* [Umijs3.0](https://umijs.org)
* [darkreader](https://github.com/darkreader/darkreader)
## 免责声明
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 -25
View File
@@ -5,32 +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 'extra': return next(e);
content = getFileContentByName(config.extraFile); }
break; },
default: );
break;
route.get(
'/configs/:file',
async (req: Request, res: Response, next: NextFunction) => {
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) {
@@ -41,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);
+41
View File
@@ -191,6 +191,42 @@ export default (app: Router) => {
}, },
); );
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 });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get( route.get(
'/crons/import', '/crons/import',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -232,6 +268,10 @@ export default (app: Router) => {
body: Joi.object({ body: Joi.object({
ids: Joi.array().items(Joi.string().required()), ids: Joi.array().items(Joi.string().required()),
status: Joi.string().required(), status: Joi.string().required(),
pid: Joi.string().optional(),
log_path: Joi.string().optional(),
last_running_time: Joi.number().optional(),
last_execution_time: Joi.number().optional(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -241,6 +281,7 @@ export default (app: Router) => {
const data = await cronService.status({ const data = await cronService.status({
...req.body, ...req.body,
status: parseInt(req.body.status), status: parseInt(req.body.status),
pid: parseInt(req.body.pid) || '',
}); });
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (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);
}
},
);
};
+55 -46
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('', { position: -1 }, true); 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,15 +70,15 @@ export default (app: Router) => {
); );
route.delete( route.delete(
'/cookies', '/envs',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), 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 cookieService = Container.get(CookieService); const envService = Container.get(EnvService);
const data = await cookieService.remove(req.body); 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);
@@ -80,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(),
@@ -93,28 +101,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.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.get(
'/cookies/:id/refresh',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.refreshCookie(req.params.id);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
@@ -124,15 +112,15 @@ export default (app: Router) => {
); );
route.put( route.put(
'/cookies/disable', '/envs/disable',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), 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 cookieService = Container.get(CookieService); const envService = Container.get(EnvService);
const data = await cookieService.disabled(req.body); const data = await envService.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);
@@ -142,15 +130,36 @@ export default (app: Router) => {
); );
route.put( route.put(
'/cookies/enable', '/envs/enable',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), 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 cookieService = Container.get(CookieService); const envService = Container.get(EnvService);
const data = await cookieService.enabled(req.body); 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);
@@ -160,7 +169,7 @@ export default (app: Router) => {
); );
route.get( route.get(
'/cookies/:id', '/envs/:id',
celebrate({ celebrate({
params: Joi.object({ params: Joi.object({
id: Joi.string().required(), id: Joi.string().required(),
@@ -169,8 +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.get(req.params.id); const data = await envService.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);
}
},
);
};
+267
View File
@@ -0,0 +1,267 @@
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';
import ScriptService from '../services/script';
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);
}
},
);
route.put(
'/scripts/run',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().optional().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, path } = req.body as {
filename: string;
path: string;
};
const filePath = `${path}/${filename}`;
const scriptService = Container.get(ScriptService);
const result = await scriptService.runScript(filePath);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+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);
}
},
);
};
+10 -2
View File
@@ -9,9 +9,15 @@ import Logger from './loaders/logger';
async function startServer() { async function startServer() {
const app = express(); const app = express();
await require('./loaders').default({ expressApp: app }); await require('./loaders/initFile').default();
app await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default();
await require('./loaders/app').default({ expressApp: app });
const server = app
.listen(config.port, () => { .listen(config.port, () => {
Logger.info(` Logger.info(`
################################################ ################################################
@@ -23,6 +29,8 @@ async function startServer() {
Logger.error(err); Logger.error(err);
process.exit(1); process.exit(1);
}); });
await require('./loaders/server').default({ server });
} }
startServer(); startServer();
+44 -18
View File
@@ -1,45 +1,52 @@
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.sample.sh'); 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 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 configFound = dotenv.config({ path: confFile }); const appDbFile = path.join(rootPath, 'db/app.db');
const authDbFile = path.join(rootPath, 'db/auth.db');
const dependenceDbFile = path.join(rootPath, 'db/dependence.db');
const versionFile = path.join(rootPath, 'src/version.ts');
if (envFound.error) { if (envFound.error) {
throw new Error("⚠️ Couldn't find .env file ⚠️"); throw new Error("⚠️ Couldn't find .env file ⚠️");
} }
if (configFound.error) {
throw new Error("⚠️ Couldn't find config.sh file ⚠️");
}
export default { export default {
port: parseInt(process.env.PORT as string, 10), port: parseInt(process.env.PORT as string, 10),
cronPort: parseInt(process.env.CRON_PORT as string, 10), cronPort: parseInt(process.env.CRON_PORT as string, 10),
secret: process.env.SECRET, 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,
@@ -50,14 +57,33 @@ export default {
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',
}
+7 -1
View File
@@ -10,6 +10,8 @@ export class Crontab {
isSystem?: 1 | 0; isSystem?: 1 | 0;
pid?: number; pid?: number;
isDisabled?: 1 | 0; isDisabled?: 1 | 0;
log_path?: string;
isPinned?: 1 | 0;
constructor(options: Crontab) { constructor(options: Crontab) {
this.name = options.name; this.name = options.name;
@@ -18,11 +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.pid = options.pid;
this.isDisabled = options.isDisabled || 0; this.isDisabled = options.isDisabled || 0;
this.log_path = options.log_path || '';
this.isPinned = options.isPinned || 0;
} }
} }
+48
View File
@@ -0,0 +1,48 @@
export class Dependence {
timestamp?: string;
created?: number;
_id?: string;
status?: DependenceStatus;
type?: DependenceTypes;
name?: number;
log?: string[];
remark?: string;
constructor(options: Dependence) {
this._id = options._id;
this.created = options.created || new Date().valueOf();
this.status = options.status || DependenceStatus.installing;
this.type = options.type || DependenceTypes.nodejs;
this.timestamp = new Date().toString();
this.name = options.name;
this.log = options.log || [];
this.remark = options.remark || '';
}
}
export enum DependenceStatus {
'installing',
'installed',
'installFailed',
'removing',
'removed',
'removeFailed',
}
export enum DependenceTypes {
'nodejs',
'python3',
'linux',
}
export enum InstallDependenceCommandTypes {
'npm i -g --force',
'pip3 install',
'apk add --no-cache -f',
}
export enum unInstallDependenceCommandTypes {
'npm uninstall -g --force',
'pip3 uninstall -y',
'apk del -f',
}
+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;
+89
View File
@@ -0,0 +1,89 @@
export enum NotificationMode {
'gotify' = 'gotify',
'goCqHttpBot' = 'goCqHttpBot',
'serverChan' = 'serverChan',
'bark' = 'bark',
'telegramBot' = 'telegramBot',
'dingtalkBot' = 'dingtalkBot',
'weWorkBot' = 'weWorkBot',
'weWorkApp' = 'weWorkApp',
'iGot' = 'iGot',
'pushPlus' = 'pushPlus',
'email' = 'email',
}
abstract class NotificationBaseInfo {
public type!: NotificationMode;
}
export class GotifyNotification extends NotificationBaseInfo {
public gotifyUrl = '';
public gotifyToken = '';
public gotifyPriority = 0;
}
export class GoCqHttpBotNotification extends NotificationBaseInfo {
public goCqHttpBotUrl = '';
public goCqHttpBotToken = '';
public goCqHttpBotQq = '';
}
export class ServerChanNotification extends NotificationBaseInfo {
public serverChanKey = '';
}
export class BarkNotification extends NotificationBaseInfo {
public barkPush = '';
public barkSound = '';
public barkGroup = 'qinglong';
}
export class TelegramBotNotification extends NotificationBaseInfo {
public telegramBotToken = '';
public telegramBotUserId = '';
public telegramBotProxyHost = '';
public telegramBotProxyPort = '';
public telegramBotProxyAuth = '';
public telegramBotApiHost = 'api.telegram.org';
}
export class DingtalkBotNotification extends NotificationBaseInfo {
public dingtalkBotToken = '';
public dingtalkBotSecret = '';
}
export class WeWorkBotNotification extends NotificationBaseInfo {
public weWorkBotKey = '';
}
export class WeWorkAppNotification extends NotificationBaseInfo {
public weWorkAppKey = '';
}
export class IGotNotification extends NotificationBaseInfo {
public iGotPushKey = '';
}
export class PushPlusNotification extends NotificationBaseInfo {
public pushPlusToken = '';
public pushPlusUser = '';
}
export class EmailNotification extends NotificationBaseInfo {
public emailService: string = '';
public emailUser: string = '';
public emailPass: string = '';
}
export interface NotificationInfo
extends GoCqHttpBotNotification,
GotifyNotification,
ServerChanNotification,
BarkNotification,
TelegramBotNotification,
DingtalkBotNotification,
WeWorkBotNotification,
WeWorkAppNotification,
IGotNotification,
PushPlusNotification,
EmailNotification {}
+31
View File
@@ -0,0 +1,31 @@
export class App {
name: string;
scopes: AppScope[];
client_id: string;
client_secret: string;
tokens?: AppToken[];
_id?: string;
constructor(options: App) {
this.name = options.name;
this.scopes = options.scopes;
this.client_id = options.client_id;
this.client_secret = options.client_secret;
this._id = options._id;
}
}
export interface AppToken {
value: string;
type: 'Bearer';
expiration: number;
}
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
export enum CrontabStatus {
'running',
'idle',
'disabled',
'queued',
}
+17
View File
@@ -0,0 +1,17 @@
export class SockMessage {
message?: string;
type?: SockMessageType;
references?: string[];
constructor(options: SockMessage) {
this.type = options.type;
this.message = options.message;
this.references = options.references;
}
}
export type SockMessageType =
| 'ping'
| 'installDependence'
| 'updateSystemVersion'
| 'manuallyRunScript';
+5
View File
@@ -0,0 +1,5 @@
declare namespace Express {
interface Request {
platform: 'desktop' | 'mobile';
}
}
@@ -2,10 +2,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;
+102 -8
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 },
@@ -65,6 +157,8 @@ export default ({ app }: { app: Application }) => {
res: Response, res: Response,
next: NextFunction, next: NextFunction,
) => { ) => {
Sentry.captureException(err);
res.status(err.status || 500); res.status(err.status || 500);
res.json({ res.json({
code: err.status || 500, code: err.status || 500,
+29 -55
View File
@@ -1,64 +1,37 @@
import DependenceService from '../services/dependence';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Crontab, CrontabStatus } from '../data/cron'; import { Crontab, CrontabStatus } from '../data/cron';
import CronService from '../services/cron'; import CronService from '../services/cron';
import CookieService from '../services/cookie'; import EnvService from '../services/env';
import _ from 'lodash';
const initData = [ import { dbs } from '../loaders/db';
{
name: '更新面板',
command: `ql update`,
schedule: `${randomSchedule(60, 1)} ${randomSchedule(
6,
1,
).toString()} * * *`,
status: CrontabStatus.disabled,
},
{
name: '删除日志',
command: 'ql rmlog 7',
schedule: '30 7 */7 * *',
status: CrontabStatus.idle,
},
{
name: '互助码',
command: 'ql code',
schedule: '30 7 * * *',
status: CrontabStatus.idle,
},
];
export default async () => { export default async () => {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const cookieService = Container.get(CookieService); const envService = Container.get(EnvService);
const cronDb = cronService.getDb(); const dependenceService = Container.get(DependenceService);
const cronDb = dbs.cronDb;
const dependenceDb = dbs.dependenceDb;
cronDb.count({}, async (err, count) => { // 初始化更新所有任务状态为空闲
if (count === 0) { cronDb.update(
const data = initData.map((x) => { { status: { $in: [CrontabStatus.running, CrontabStatus.queued] } },
const tab = new Crontab(x); { $set: { status: CrontabStatus.idle } },
tab.created = new Date().valueOf(); { multi: true },
tab.saved = false; );
if (tab.name === '更新面板') {
tab.isSystem = 1; // 初始化时安装所有处于安装中,安装成功,安装失败的依赖
} else { dependenceDb.find({ status: { $in: [0, 1, 2] } }).exec((err, docs) => {
tab.isSystem = 0; 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; }
});
cronDb.insert(data);
await cronService.autosave_crontab();
}
});
// patch更新面板任务状态
cronDb.find({ name: '更新面板' }).exec((err, docs) => {
const doc = docs[0];
if (doc && doc.status === CrontabStatus.running) {
cronDb.update(
{ name: '更新面板' },
{ $set: { status: CrontabStatus.idle } },
);
} }
}); });
@@ -66,11 +39,12 @@ export default async () => {
cronDb cronDb
.find({ .find({
command: /ql (repo|raw)/, command: /ql (repo|raw)/,
isDisabled: { $ne: 1 },
}) })
.exec((err, docs) => { .exec((err, docs) => {
for (let i = 0; i < docs.length; i++) { for (let i = 0; i < docs.length; i++) {
const doc = docs[i]; const doc = docs[i];
if (doc && doc.isDisabled !== 1) { if (doc) {
exec(doc.command); exec(doc.command);
} }
} }
@@ -97,11 +71,11 @@ export default async () => {
// 初始化保存一次ck和定时任务数据 // 初始化保存一次ck和定时任务数据
await cronService.autosave_crontab(); await cronService.autosave_crontab();
await cookieService.set_cookies(); 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++) {
+46
View File
@@ -0,0 +1,46 @@
import fs from 'fs';
import path from 'path';
import dotenv from 'dotenv';
import Logger from './logger';
import { fileExist } from '../config/util';
const rootPath = process.cwd();
const confFile = path.join(rootPath, 'config/config.sh');
const sampleConfigFile = path.join(rootPath, 'sample/config.sample.sh');
const sampleAuthFile = path.join(rootPath, 'sample/auth.sample.json');
const authConfigFile = path.join(rootPath, 'config/auth.json');
const configPath = path.join(rootPath, 'config/');
const scriptPath = path.join(rootPath, 'scripts/');
const logPath = path.join(rootPath, 'log/');
export default async () => {
const authFileExist = await fileExist(authConfigFile);
const confFileExist = await fileExist(confFile);
const scriptDirExist = await fileExist(scriptPath);
const logDirExist = await fileExist(logPath);
const configDirExist = await fileExist(configPath);
if (!configDirExist) {
fs.mkdirSync(configPath);
}
if (!authFileExist) {
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
}
if (!confFileExist) {
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
}
if (!scriptDirExist) {
fs.mkdirSync(scriptPath);
}
if (!logDirExist) {
fs.mkdirSync(logPath);
}
dotenv.config({ path: confFile });
Logger.info('✌️ Init file down');
};
+21
View File
@@ -0,0 +1,21 @@
import { Application } from 'express';
import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing';
import Logger from './logger';
export default ({ expressApp }: { expressApp: Application }) => {
Sentry.init({
dsn: 'https://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);
};
+8 -7
View File
@@ -2,16 +2,14 @@ import schedule from 'node-schedule';
import express from 'express'; import express from 'express';
import { exec } from 'child_process'; import { exec } from 'child_process';
import Logger from './loaders/logger'; import Logger from './loaders/logger';
import { Container } from 'typedi';
import CronService from './services/cron';
import { CrontabStatus } from './data/cron'; import { CrontabStatus } from './data/cron';
import config from './config'; import config from './config';
import { dbs } from './loaders/db';
const app = express(); const app = express();
const run = async () => { const run = async () => {
const cronService = Container.get(CronService); const cronDb = dbs.cronDb;
const cronDb = cronService.getDb();
cronDb cronDb
.find({}) .find({})
@@ -25,7 +23,7 @@ const run = async () => {
if (docs && docs.length > 0) { if (docs && docs.length > 0) {
for (let i = 0; i < docs.length; i++) { for (let i = 0; i < docs.length; i++) {
const task = docs[i]; const task = docs[i];
const _schedule = task.schedule && task.schedule.split(' '); const _schedule = task.schedule && task.schedule.split(/ +/);
if ( if (
_schedule && _schedule &&
_schedule.length > 5 && _schedule.length > 5 &&
@@ -46,8 +44,11 @@ const run = async () => {
}; };
app app
.listen(config.cronPort, () => { .listen(config.cronPort, async () => {
run(); await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default();
await run();
Logger.info(` Logger.info(`
################################################ ################################################
🛡️ Schedule listening on port: ${config.cronPort} 🛡️ 🛡️ Schedule listening on port: ${config.cronPort} 🛡️
-333
View File
@@ -1,333 +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: '-' };
});
}
private async formatCookies(cookies: Cookie[]) {
const result = [];
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i];
if (cookie.status !== CookieStatus.disabled) {
const { status, nickname } = await this.getJdInfo(cookie.value);
result.push({ ...cookie, status, nickname });
} else {
result.push({ ...cookie, nickname: '-' });
}
}
return result;
}
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 await this.formatCookies(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();
const [newCookie] = await this.formatCookies([newDoc]);
return newCookie;
}
private async updateDb(payload: Cookie): Promise<Cookie> {
return new Promise((resolve) => {
this.cronDb.update(
{ _id: payload._id },
payload,
{ returnUpdatedDocs: true },
(err, num, doc) => {
if (err) {
this.logger.error(err);
} else {
resolve(doc as Cookie);
}
},
);
});
}
public async remove(ids: string[]) {
return new Promise((resolve: any) => {
this.cronDb.remove(
{ _id: { $in: ids } },
{ multi: true },
async (err) => {
await this.set_cookies();
resolve();
},
);
});
}
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 },
needDetail: boolean = false,
): Promise<Cookie[]> {
let query = {};
if (searchText) {
const reg = new RegExp(searchText);
query = {
$or: [
{
name: reg,
},
{
command: reg,
},
],
};
}
const newDocs = await this.find(query, sort);
if (needDetail) {
return await this.formatCookies(newDocs);
} else {
return newDocs;
}
}
private async find(query: any, sort: any): Promise<Cookie[]> {
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(ids: string[]) {
return new Promise((resolve: any) => {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { status: CookieStatus.disabled } },
{ multi: true },
async (err) => {
await this.set_cookies();
resolve();
},
);
});
}
public async enabled(ids: string[]) {
return new Promise((resolve: any) => {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { status: CookieStatus.noacquired } },
{ multi: true },
async (err, num) => {
await this.set_cookies();
resolve();
},
);
});
}
public 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);
}
}
+175 -68
View File
@@ -8,28 +8,23 @@ 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 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;
private queue = new PQueue({ private queue = new PQueue({
concurrency: parseInt(process.env.MaxConcurrentNum) || 5, concurrency: parseInt(process.env.MaxConcurrentNum as string) || 5,
}); });
constructor(@Inject('logger') private logger: winston.Logger) { constructor(@Inject('logger') private logger: winston.Logger) {}
this.cronDb.loadDatabase((err) => {
if (err) throw err;
});
}
public getDb(): DataStore {
return this.cronDb;
}
private isSixCron(cron: Crontab) { private isSixCron(cron: Crontab) {
const { schedule } = cron; const { schedule } = cron;
if (schedule.split(' ').length === 6) { if (schedule.split(/ +/).length === 6) {
return true; return true;
} }
return false; return false;
@@ -86,11 +81,36 @@ export default class CronService {
public async status({ public async status({
ids, ids,
status, status,
pid,
log_path,
last_running_time = 0,
last_execution_time = 0,
}: { }: {
ids: string[]; ids: string[];
status: CrontabStatus; status: CrontabStatus;
pid: number;
log_path: string;
last_running_time: number;
last_execution_time: number;
}) { }) {
this.cronDb.update({ _id: { $in: ids } }, { $set: { status } }); 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(ids: string[]) { public async remove(ids: string[]) {
@@ -106,6 +126,32 @@ export default class CronService {
}); });
} }
public async pin(ids: string[]) {
return new Promise((resolve: any) => {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { isPinned: 1 } },
{ multi: true },
async (err) => {
resolve();
},
);
});
}
public async unPin(ids: string[]) {
return new Promise((resolve: any) => {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { isPinned: 0 } },
{ multi: true },
async (err) => {
resolve();
},
);
});
}
public async crontabs(searchText?: string): Promise<Crontab[]> { public async crontabs(searchText?: string): Promise<Crontab[]> {
let query = {}; let query = {};
if (searchText) { if (searchText) {
@@ -156,22 +202,67 @@ export default class CronService {
public async stop(ids: string[]) { public async stop(ids: string[]) {
return new Promise((resolve: any) => { return new Promise((resolve: any) => {
this.cronDb.find({ _id: { $in: ids } }).exec((err, docs: Crontab[]) => { this.cronDb
this.cronDb.update( .find({ _id: { $in: ids } })
{ _id: { $in: ids } }, .exec(async (err, docs: Crontab[]) => {
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } }, for (const doc of docs) {
); if (doc.pid) {
const pids = docs try {
.map((x) => x.pid) process.kill(-doc.pid);
.filter((x) => !!x) } catch (error) {
.join('\n'); this.logger.silly(error);
exec(`echo - e "${pids}" | xargs kill - 9`, (err) => { }
}
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(); 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> { private async runSingle(id: string): Promise<number> {
return new Promise(async (resolve: any) => { return new Promise(async (resolve: any) => {
const cron = await this.get(id); const cron = await this.get(id);
@@ -180,18 +271,12 @@ export default class CronService {
return; return;
} }
let { _id, command } = cron; let { _id, command, log_path } = cron;
this.logger.silly('Running job'); this.logger.silly('Running job');
this.logger.silly('ID: ' + _id); this.logger.silly('ID: ' + _id);
this.logger.silly('Original command: ' + command); this.logger.silly('Original command: ' + command);
let logFile = `${config.manualLogPath}${_id}.log`;
fs.writeFileSync(
logFile,
`开始执行... ${new Date().toLocaleString()}\n\n`,
);
let cmdStr = command; let cmdStr = command;
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) { if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
cmdStr = `task ${cmdStr}`; cmdStr = `task ${cmdStr}`;
@@ -199,48 +284,39 @@ export default class CronService {
if (cmdStr.endsWith('.js')) { if (cmdStr.endsWith('.js')) {
cmdStr = `${cmdStr} now`; cmdStr = `${cmdStr} now`;
} }
const cmd = spawn(cmdStr, { shell: true });
const cp = spawn(cmdStr, { shell: '/bin/bash' });
this.cronDb.update( this.cronDb.update(
{ _id }, { _id },
{ $set: { status: CrontabStatus.running, pid: cmd.pid } }, { $set: { status: CrontabStatus.running, pid: cp.pid } },
); );
cp.stderr.on('data', (data) => {
cmd.stdout.on('data', (data) => { if (log_path) {
this.logger.silly(`stdout: ${data}`); fs.appendFileSync(`${log_path}`, `${data}`);
fs.appendFileSync(logFile, 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.silly(`stderr: ${data}`); this.logger.info(
fs.appendFileSync(logFile, data); `${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
}); );
cmd.on('close', (code) => {
this.logger.silly(`child process exited with code ${code}`);
this.cronDb.update( this.cronDb.update(
{ _id }, { _id },
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } }, { $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
); );
});
cmd.on('error', (err) => {
this.logger.info(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 }, $unset: { pid: true } },
);
fs.appendFileSync(logFile, `\n执行结束...`);
resolve(); resolve();
}); });
cp.on('close', (code) => {
process.on('SIGINT', function () { this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
fs.appendFileSync(logFile, `\n执行结束...`); this.cronDb.update(
{ _id },
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
);
resolve(); resolve();
}); });
}); });
@@ -275,8 +351,43 @@ export default class CronService {
} }
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) {
@@ -288,7 +399,7 @@ export default class CronService {
const tabs = await this.crontabs(); const tabs = await this.crontabs();
var crontab_string = ''; var crontab_string = '';
tabs.forEach((tab) => { tabs.forEach((tab) => {
const _schedule = tab.schedule && tab.schedule.split(' '); const _schedule = tab.schedule && tab.schedule.split(/ +/);
if (tab.isDisabled === 1 || _schedule.length !== 5) { if (tab.isDisabled === 1 || _schedule.length !== 5) {
crontab_string += '# '; crontab_string += '# ';
crontab_string += tab.schedule; crontab_string += tab.schedule;
@@ -313,10 +424,6 @@ export default class CronService {
this.cronDb.update({}, { $set: { saved: true } }, { multi: true }); this.cronDb.update({}, { $set: { saved: true } }, { multi: true });
} }
private reload_db() {
this.cronDb.loadDatabase();
}
public import_crontab() { public import_crontab() {
exec('crontab -l', (error, stdout, stderr) => { exec('crontab -l', (error, stdout, stderr) => {
var lines = stdout.split('\n'); var lines = stdout.split('\n');
+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);
}
}
+354
View File
@@ -0,0 +1,354 @@
import { NotificationInfo } from '../data/notify';
import { Service, Inject } from 'typedi';
import winston from 'winston';
import UserService from './user';
import got from 'got';
import nodemailer from 'nodemailer';
import crypto from 'crypto';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
@Service()
export default class NotificationService {
@Inject((type) => UserService)
private userService!: UserService;
private modeMap = new Map([
['gotify', this.gotify],
['goCqHttpBot', this.goCqHttpBot],
['serverChan', this.serverChan],
['bark', this.bark],
['telegramBot', this.telegramBot],
['dingtalkBot', this.dingtalkBot],
['weWorkBot', this.weWorkBot],
['weWorkApp', this.weWorkApp],
['iGot', this.iGot],
['pushPlus', this.pushPlus],
['email', this.email],
]);
private timeout = 10000;
private title = '';
private content = '';
private params!: Omit<NotificationInfo, 'type'>;
constructor(@Inject('logger') private logger: winston.Logger) {}
public async notify(
title: string,
content: string,
): Promise<boolean | undefined> {
const { type, ...rest } = await this.userService.getNotificationMode();
if (type) {
this.title = title;
this.content = content;
this.params = rest;
const notificationModeAction = this.modeMap.get(type);
try {
return await notificationModeAction?.call(this);
} catch (error: any) {
return false;
}
}
return false;
}
public async testNotify(
info: NotificationInfo,
title: string,
content: string,
) {
const { type, ...rest } = info;
if (type) {
this.title = title;
this.content = content;
this.params = rest;
const notificationModeAction = this.modeMap.get(type);
return await notificationModeAction?.call(this);
}
return true;
}
private async gotify() {
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
const res: any = await got
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
timeout: this.timeout,
retry: 0,
body: `title=${encodeURIComponent(
this.title,
)}&message=${encodeURIComponent(
this.content,
)}&priority=${gotifyPriority}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
})
.json();
return typeof res.id === 'number';
}
private async goCqHttpBot() {
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
const res: any = await got
.post(
`${goCqHttpBotUrl}?access_token=${goCqHttpBotToken}&${goCqHttpBotQq}`,
{
timeout: this.timeout,
retry: 0,
json: { message: `${this.title}\n${this.content}` },
},
)
.json();
return res.retcode === 0;
}
private async serverChan() {
const { serverChanKey } = this.params;
const url = serverChanKey.startsWith('SCT')
? `https://sctapi.ftqq.com/${serverChanKey}.send`
: `https://sc.ftqq.com/${serverChanKey}.send`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
body: `title=${this.title}&desp=${this.content}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return res.errno === 0 || res.data.errno === 0;
}
private async bark() {
let { barkPush, barkSound, barkGroup } = this.params;
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
barkPush = `https://api.day.app/${barkPush}`;
}
const url = `${barkPush}/${encodeURIComponent(
this.title,
)}/${encodeURIComponent(
this.content,
)}?sound=${barkSound}&group=${barkGroup}`;
const res: any = await got
.get(url, {
timeout: this.timeout,
retry: 0,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return res.code === 200;
}
private async telegramBot() {
const {
telegramBotApiHost,
telegramBotProxyAuth,
telegramBotProxyHost,
telegramBotProxyPort,
telegramBotToken,
telegramBotUserId,
} = this.params;
const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : '';
const url = `https://${
telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org'
}/bot${telegramBotToken}/sendMessage`;
let agent;
if (telegramBotProxyHost && telegramBotProxyPort) {
const options: any = {
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: 256,
maxFreeSockets: 256,
proxy: `http://${authStr}${telegramBotProxyHost}:${telegramBotProxyPort}`,
};
const httpAgent = new HttpProxyAgent(options);
const httpsAgent = new HttpsProxyAgent(options);
agent = {
http: httpAgent,
https: httpsAgent,
};
}
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
agent,
})
.json();
return !!res.ok;
}
private async dingtalkBot() {
const { dingtalkBotSecret, dingtalkBotToken } = this.params;
let secretParam = '';
if (dingtalkBotSecret) {
const dateNow = Date.now();
const hmac = crypto.createHmac('sha256', dingtalkBotSecret);
hmac.update(`${dateNow}\n${dingtalkBotSecret}`);
const result = encodeURIComponent(hmac.digest('base64'));
secretParam = `&timestamp=${dateNow}&sign=${result}`;
}
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
json: {
msgtype: 'text',
text: {
content: ` ${this.title}\n\n${this.content}`,
},
},
})
.json();
return res.errcode === 0;
}
private async weWorkBot() {
const { weWorkBotKey } = this.params;
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
json: {
msgtype: 'text',
text: {
content: ` ${this.title}\n\n${this.content}`,
},
},
})
.json();
return res.errcode === 0;
}
private async weWorkApp() {
const { weWorkAppKey } = this.params;
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
weWorkAppKey.split(',');
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
const { access_token } = await got
.post(url, {
timeout: this.timeout,
retry: 0,
json: {
corpid,
corpsecret,
},
})
.json();
let options: any = {
msgtype: 'mpnews',
mpnews: {
articles: [
{
title: `${this.title}`,
thumb_media_id,
author: `智能助手`,
content_source_url: ``,
content: `${this.content.replace(/\n/g, '<br/>')}`,
digest: `${this.content}`,
},
],
},
};
switch (thumb_media_id) {
case '0':
options = {
msgtype: 'textcard',
textcard: {
title: `${this.title}`,
description: `${this.content}`,
url: 'https://github.com/whyour/qinglong',
btntxt: '更多',
},
};
break;
case '1':
options = {
msgtype: 'text',
text: {
content: `${this.title}\n\n${this.content}`,
},
};
break;
}
const res: any = await got
.post(
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${access_token}`,
{
timeout: this.timeout,
retry: 0,
json: {
touser,
agentid,
safe: '0',
...options,
},
},
)
.json();
return res.errcode === 0;
}
private async iGot() {
const { iGotPushKey } = this.params;
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
body: `title=${this.title}&content=${this.content}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return res.ret === 0;
}
private async pushPlus() {
const { pushPlusToken, pushPlusUser } = this.params;
const url = `https://www.pushplus.plus/send`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
json: {
token: `${pushPlusToken}`,
title: `${this.title}`,
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
topic: `${pushPlusUser || ''}`,
},
})
.json();
return res.code === 200;
}
private async email() {
const { emailPass, emailService, emailUser } = this.params;
const transporter = nodemailer.createTransport({
service: emailService,
auth: {
user: emailUser,
pass: emailPass,
},
});
const info = await transporter.sendMail({
from: `"青龙快讯" <${emailUser}>`,
to: `${emailUser}`,
subject: `${this.title}`,
html: `${this.content.replace(/\n/g, '<br/>')}`,
});
transporter.close();
return !!info.messageId;
}
}
+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();
}
}
+43
View File
@@ -0,0 +1,43 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { spawn } from 'child_process';
import SockService from './sock';
@Service()
export default class ScriptService {
constructor(
@Inject('logger') private logger: winston.Logger,
private sockService: SockService,
) {}
public async runScript(path: string) {
const cp = spawn(`task -l ${path} now`, { shell: '/bin/bash' });
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: `开始执行脚本`,
});
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: data.toString(),
});
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: data.toString(),
});
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: JSON.stringify(err),
});
});
return { code: 200 };
}
}
+34
View File
@@ -0,0 +1,34 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { Connection } from 'sockjs';
import { SockMessage } from '../data/sock';
@Service()
export default class SockService {
private clients: Connection[] = [];
constructor(@Inject('logger') private logger: winston.Logger) {}
public getClients() {
return this.clients;
}
public addClient(conn: Connection) {
if (this.clients.indexOf(conn) === -1) {
this.clients.push(conn);
}
}
public removeClient(conn: Connection) {
const index = this.clients.indexOf(conn);
if (index !== -1) {
this.clients.splice(index, 1);
}
}
public sendMessage(msg: SockMessage) {
this.clients.forEach((x) => {
x.write(JSON.stringify(msg));
});
}
}
+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 };
}
}
+14
View File
@@ -0,0 +1,14 @@
version: '2'
services:
web:
image: whyour/qinglong:latest
volumes:
- ./data/config:/ql/config
- ./data/log:/ql/log
- ./data/db:/ql/db
- ./data/scripts:/ql/scripts
- ./data/repo:/ql/repo
- ./data/raw:/ql/raw
ports:
- "0.0.0.0:5700:5700"
restart: unless-stopped
+17 -8
View File
@@ -1,14 +1,17 @@
FROM node:lts-alpine FROM node:lts-alpine3.12
LABEL maintainer="whyour" ARG QL_MAINTAINER="whyour"
ARG QL_URL=https://github.com/whyour/qinglong.git LABEL maintainer="${QL_MAINTAINER}"
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
ARG QL_BRANCH=master ARG QL_BRANCH=master
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
LANG=zh_CN.UTF-8 \ LANG=zh_CN.UTF-8 \
SHELL=/bin/bash \ SHELL=/bin/bash \
PS1="\u@\h:\w \$ " \ PS1="\u@\h:\w \$ " \
QL_DIR=/ql QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH}
WORKDIR ${QL_DIR} WORKDIR ${QL_DIR}
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ RUN set -x \
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \
&& apk update -f \ && apk update -f \
&& apk upgrade \ && apk upgrade \
&& apk --no-cache add -f bash \ && apk --no-cache add -f bash \
@@ -24,19 +27,25 @@ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
python3 \ python3 \
jq \ jq \
openssh \ openssh \
py3-pip \
&& rm -rf /var/cache/apk/* \ && rm -rf /var/cache/apk/* \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \ && echo "Asia/Shanghai" > /etc/timezone \
&& touch ~/.bashrc \ && touch ~/.bashrc \
&& mkdir /run/nginx \
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \ && git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
&& git config --global user.email "qinglong@@users.noreply.github.com" \
&& git config --global user.name "qinglong" \
&& cd ${QL_DIR} \ && cd ${QL_DIR} \
&& cp -f .env.example .env \ && cp -f .env.example .env \
&& chmod 777 ${QL_DIR}/shell/*.sh \ && chmod 777 ${QL_DIR}/shell/*.sh \
&& chmod 777 ${QL_DIR}/docker/*.sh \ && chmod 777 ${QL_DIR}/docker/*.sh \
&& npm install -g pm2 \
&& npm install -g pnpm \ && npm install -g pnpm \
&& pnpm install -g pm2 \
&& pnpm install -g ts-node typescript tslib \
&& rm -rf /root/.npm \ && rm -rf /root/.npm \
&& pnpm install --prod \ && pnpm install --prod \
&& rm -rf /root/.pnpm-store && rm -rf /root/.pnpm-store \
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
&& cp -rf /static/* ${QL_DIR} \
&& rm -rf /static
ENTRYPOINT ["./docker/docker-entrypoint.sh"] ENTRYPOINT ["./docker/docker-entrypoint.sh"]
+19 -17
View File
@@ -1,17 +1,18 @@
#!/bin/bash #!/bin/bash
set -e
dir_shell=/ql/shell dir_shell=/ql/shell
. $dir_shell/share.sh . $dir_shell/share.sh
link_shell link_shell
echo -e "======================1. 检测配置文件========================\n" echo -e "======================1. 检测配置文件========================\n"
fix_config make_dir /etc/nginx/conf.d
cp -fv $dir_root/docker/front.conf /etc/nginx/conf.d/front.conf cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
pm2 l &>/dev/null
echo echo
echo -e "======================2. 更新源代码========================\n" echo -e "======================2. 安装依赖========================\n"
ql update "no-restart" update_depend
pm2 l >/dev/null 2>&1
echo echo
echo -e "======================3. 启动nginx========================\n" echo -e "======================3. 启动nginx========================\n"
@@ -19,24 +20,25 @@ nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "nginx启动成功...\n" echo -e "nginx启动成功...\n"
echo -e "======================4. 启动控制面板========================\n" echo -e "======================4. 启动控制面板========================\n"
if [[ $(pm2 info panel 2>/dev/null) ]]; then pm2 delete panel &>/dev/null
pm2 reload panel pm2 start $dir_root/build/app.js -n panel --source-map-support --time
else
pm2 start $dir_root/build/app.js -n panel
fi
echo -e "控制面板启动成功...\n" echo -e "控制面板启动成功...\n"
echo -e "======================5. 启动定时任务========================\n" echo -e "======================5. 启动定时任务========================\n"
if [[ $(pm2 info schedule 2>/dev/null) ]]; then pm2 delete schedule &>/dev/null
pm2 reload schedule pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time
else
pm2 start $dir_root/build/schedule.js -n schedule
fi
echo -e "定时任务启动成功...\n" echo -e "定时任务启动成功...\n"
if [[ $AutoStartBot == true ]]; then if [[ $AutoStartBot == true ]]; then
echo -e "======================6. 启动bot========================\n" echo -e "======================6. 启动bot========================\n"
ql bot nohup ql bot >>$dir_log/start.log 2>&1 &
echo -e "bot后台启动中...\n"
fi
if [[ $EnableExtraShell == true ]]; then
echo -e "======================7. 执行自定义脚本========================\n"
nohup ql extra >>$dir_log/start.log 2>&1 &
echo -e "自定义脚本后台执行中...\n"
fi fi
echo -e "############################################################\n" echo -e "############################################################\n"
+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;
}
+27 -4
View File
@@ -24,6 +24,9 @@
] ]
}, },
"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",
@@ -31,32 +34,50 @@
"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", "node-schedule": "^2.0.0",
"nodemailer": "^6.7.0",
"p-queue": "6.6.2", "p-queue": "6.6.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"serve-handler": "^6.1.3",
"sockjs": "^0.3.21",
"typedi": "^0.8.0", "typedi": "^0.8.0",
"uuid": "^8.3.2",
"winston": "^3.3.3" "winston": "^3.3.3"
}, },
"devDependencies": { "devDependencies": {
"@ant-design/pro-layout": "^6.5.0",
"@ant-design/icons": "^4.6.2", "@ant-design/icons": "^4.6.2",
"@ant-design/pro-layout": "^6.26.0",
"@monaco-editor/react": "^4.3.1",
"@sentry/react": "^6.14.0",
"@sentry/webpack-plugin": "^1.18.3",
"@types/cors": "^2.8.10", "@types/cors": "^2.8.10",
"@types/express": "^4.17.8", "@types/express": "^4.17.8",
"@types/express-jwt": "^6.0.1", "@types/express-jwt": "^6.0.1",
"@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",
"@umijs/plugin-antd": "^0.9.1", "@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1",
"@umijs/plugin-antd": "^0.11.0",
"@umijs/test": "^3.3.9", "@umijs/test": "^3.3.9",
"codemirror": "^5.59.4", "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", "darkreader": "^4.9.27",
"lint-staged": "^10.0.7", "lint-staged": "^10.0.7",
@@ -69,9 +90,11 @@
"react-dnd": "^14.0.2", "react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0", "react-dnd-html5-backend": "^14.0.0",
"react-dom": "17.x", "react-dom": "17.x",
"react-split-pane": "^0.1.92",
"sockjs-client": "^1.5.2",
"ts-node": "^9.0.0", "ts-node": "^9.0.0",
"typescript": "^4.1.2", "typescript": "^4.1.2",
"umi": "^3.3.9", "umi": "^3.5.0",
"umi-request": "^1.3.5", "umi-request": "^1.3.5",
"vh-check": "^2.0.5", "vh-check": "^2.0.5",
"webpack": "^5.28.0", "webpack": "^5.28.0",
-9826
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

+1 -1
View File
@@ -1 +1 @@
{ "username": "admin", "password": "adminadmin" } { "username": "admin", "password": "admin" }
+16 -103
View File
@@ -1,6 +1,6 @@
## Version: v2.2.0-066 ## Version: v2.8.0
## Date: 2021-06-17 ## Date: 2021-06-20
## Update Content: \n1. 修复版本号样式\n2. 修复查看日志或者编辑任务后列表异常 ## Update Content: 可持续发展纲要\n1. session管理破坏性修改\n2. 配置管理可编辑config下文件\n3. 自定义脚本改为查看脚本\n4. 移除互助相关
## 上面版本号中,如果第2位数字有变化,那么代表增加了新的参数,如果只有第3位数字有变化,仅代表更新了注释,没有增加新的参数,可更新可不更新 ## 上面版本号中,如果第2位数字有变化,那么代表增加了新的参数,如果只有第3位数字有变化,仅代表更新了注释,没有增加新的参数,可更新可不更新
@@ -10,6 +10,9 @@ AutoDelCron="true"
## 在运行 ql repo 命令时,是否自动增加新的本地定时任务 ## 在运行 ql repo 命令时,是否自动增加新的本地定时任务
AutoAddCron="true" AutoAddCron="true"
## 拉取脚本时默认的定时规则,当匹配不到定时规则时使用,例如: 0 9 * * *
DefaultCronRule=""
## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可 ## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可
RepoFileExtensions="js py" RepoFileExtensions="js py"
@@ -29,16 +32,6 @@ RandomDelay="300"
## 如果你自己会写shell脚本,并且希望在每次运行 ql update 命令时,额外运行你的 shell 脚本,请赋值为 "true",默认为true ## 如果你自己会写shell脚本,并且希望在每次运行 ql update 命令时,额外运行你的 shell 脚本,请赋值为 "true",默认为true
EnableExtraShell="true" EnableExtraShell="true"
## 自动按顺序进行账号间互助(选填) 设置为 true 时,将直接导入code最新日志来进行互助
AutoHelpOther=""
## 定义 jcode 脚本导出的互助码模板样式(选填)
## 不填 使用“按编号顺序助力模板”,Cookie编号在前的优先助力
## 填 0 使用“全部一致助力模板”,所有账户要助力的码全部一致
## 填 1 使用“均等机会助力模板”,所有账户获得助力次数一致
## 填 2 使用“随机顺序助力模板”,本套脚本内账号间随机顺序助力,每次生成的顺序都不一致。
HelpType=""
## 是否自动启动bot,默认不启动,设置为true时自动启动,目前需要自行克隆bot仓库所需代码,存到ql/repo目录下,文件夹命名为dockerbot ## 是否自动启动bot,默认不启动,设置为true时自动启动,目前需要自行克隆bot仓库所需代码,存到ql/repo目录下,文件夹命名为dockerbot
AutoStartBot="" AutoStartBot=""
@@ -56,6 +49,8 @@ export PUSH_KEY=""
export BARK_PUSH="" export BARK_PUSH=""
## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声 ## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声
export BARK_SOUND="" export BARK_SOUND=""
## 下方填写推送消息分组,默认为"QingLong"
export BARK_GROUP="QingLong"
## 3. Telegram ## 3. Telegram
## 下方填写自己申请@BotFather的Token,如10xxx4:AAFcqxxxxgER5uw ## 下方填写自己申请@BotFather的Token,如10xxx4:AAFcqxxxxgER5uw
@@ -108,7 +103,7 @@ export PUSH_PLUS_TOKEN=""
## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送 ## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送
export PUSH_PLUS_USER="" export PUSH_PLUS_USER=""
## 8. go-cqhttp ## 9. go-cqhttp
## gobot_url 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg ## gobot_url 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
## gobot_token 填写在go-cqhttp文件设置的访问密钥 ## gobot_token 填写在go-cqhttp文件设置的访问密钥
## gobot_qq 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群 ## gobot_qq 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
@@ -117,94 +112,12 @@ export GOBOT_URL=""
export GOBOT_TOKEN="" export GOBOT_TOKEN=""
export GOBOT_QQ="" export GOBOT_QQ=""
## 如果只是想要屏蔽某个ck不执行某个脚本,可以参考下面 case 这个命令的例子来控制,脚本名称包含后缀 ## 10. gotify
## case $1 in ## gotify_url 填写gotify地址,如https://push.example.de:8080
## test.js) ## gotify_token 填写gotify的消息应用token
## TempBlockCookie="5" ## gotify_priority 填写推送消息优先级,默认为0
## ;; export GOTIFY_URL="";
## esac export GOTIFY_TOKEN="";
export GOTIFY_PRIORITY=0;
## 需组合的环境变量列表,env_name需要和var_name一一对应,如何有新活动按照格式添加(不懂勿动)
env_name=(
JD_COOKIE
FRUITSHARECODES
PETSHARECODES
PLANT_BEAN_SHARECODES
DREAM_FACTORY_SHARE_CODES
DDFACTORY_SHARECODES
JDZZ_SHARECODES
JDJOY_SHARECODES
JXNC_SHARECODES
BOOKSHOP_SHARECODES
JD_CASH_SHARECODES
JDSGMH_SHARECODES
JDCFD_SHARECODES
JDHEALTH_SHARECODES
)
var_name=(
Cookie
ForOtherFruit
ForOtherPet
ForOtherBean
ForOtherDreamFactory
ForOtherJdFactory
ForOtherJdzz
ForOtherJoy
ForOtherJxnc
ForOtherBookShop
ForOtherCash
ForOtherSgmh
ForOtherCfd
ForOtherHealth
)
## name_js为脚本文件名,如果使用ql repo命令拉取,文件名含有作者名
## 所有有互助码的活动,把脚本名称列在 name_js 中,对应 config.sh 中互助码后缀列在 name_config 中,中文名称列在 name_chinese 中。
## name_js、name_config 和 name_chinese 中的三个名称必须一一对应。
name_js=(
jd_fruit
jd_pet
jd_plantBean
jd_dreamFactory
jd_jdfactory
jd_jdzz
jd_crazy_joy
jd_jxnc
jd_bookshop
jd_cash
jd_sgmh
jd_cfd
jd_health
)
name_config=(
Fruit
Pet
Bean
DreamFactory
JdFactory
Jdzz
Joy
Jxnc
BookShop
Cash
Sgmh
Cfd
Health
)
name_chinese=(
东东农场
东东萌宠
京东种豆得豆
京喜工厂
东东工厂
京东赚赚
crazyJoy任务
京喜农场
口袋书店
签到领现金
闪购盲盒
京喜财富岛
东东健康社区
)
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可 ## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+2
View File
@@ -1,3 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 添加你需要重启自动执行的任意命令,比如 ql repo ## 添加你需要重启自动执行的任意命令,比如 ql repo
## 安装node依赖使用 pnpm install -g xxx xxx
## 安装python依赖使用 pip3 install xxx
+65 -7
View File
@@ -13,6 +13,13 @@
const querystring = require('querystring'); const querystring = require('querystring');
const $ = new Env(); const $ = new Env();
const timeout = 15000; //超时时间(单位毫秒) const timeout = 15000; //超时时间(单位毫秒)
// =======================================gotify通知设置区域==============================================
//gotify_url 填写gotify地址,如https://push.example.de:8080
//gotify_token 填写gotify的消息应用token
//gotify_priority 填写推送消息优先级,默认为0
let GOTIFY_URL = '';
let GOTIFY_TOKEN = '';
let GOTIFY_PRIORITY = 0;
// =======================================go-cqhttp通知设置区域=========================================== // =======================================go-cqhttp通知设置区域===========================================
//gobot_url 填写请求地址http://127.0.0.1/send_private_msg //gobot_url 填写请求地址http://127.0.0.1/send_private_msg
//gobot_token 填写在go-cqhttp文件设置的访问密钥 //gobot_token 填写在go-cqhttp文件设置的访问密钥
@@ -32,6 +39,8 @@ let SCKEY = '';
let BARK_PUSH = ''; let BARK_PUSH = '';
//BARK app推送铃声,铃声列表去APP查看复制填写 //BARK app推送铃声,铃声列表去APP查看复制填写
let BARK_SOUND = ''; let BARK_SOUND = '';
//BARK app推送消息的分组, 默认为"QingLong"
let BARK_GROUP = 'QingLong';
// =======================================telegram机器人通知设置区域=========================================== // =======================================telegram机器人通知设置区域===========================================
//此处填你telegram bot 的Tokentelegram机器人通知推送必填项.例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw //此处填你telegram bot 的Tokentelegram机器人通知推送必填项.例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw
@@ -82,6 +91,16 @@ let PUSH_PLUS_TOKEN = '';
let PUSH_PLUS_USER = ''; let PUSH_PLUS_USER = '';
//==========================云端环境变量的判断与接收========================= //==========================云端环境变量的判断与接收=========================
if (process.env.GOTIFY_URL) {
GOTIFY_URL = process.env.GOTIFY_URL;
}
if (process.env.GOTIFY_TOKEN) {
GOTIFY_TOKEN = process.env.GOTIFY_TOKEN;
}
if (process.env.GOTIFY_PRIORITY) {
GOTIFY_PRIORITY = process.env.GOTIFY_PRIORITY;
}
if (process.env.GOBOT_URL) { if (process.env.GOBOT_URL) {
GOBOT_URL = process.env.GOBOT_URL; GOBOT_URL = process.env.GOBOT_URL;
} }
@@ -117,6 +136,9 @@ if (process.env.BARK_PUSH) {
if (process.env.BARK_SOUND) { if (process.env.BARK_SOUND) {
BARK_SOUND = process.env.BARK_SOUND; BARK_SOUND = process.env.BARK_SOUND;
} }
if (process.env.BARK_GROUP) {
BARK_GROUP = process.env.BARK_GROUP;
}
} else { } else {
if ( if (
BARK_PUSH && BARK_PUSH &&
@@ -195,17 +217,53 @@ async function sendNotify(
qywxamNotify(text, desp), //企业微信应用消息推送 qywxamNotify(text, desp), //企业微信应用消息推送
iGotNotify(text, desp, params), //iGot iGotNotify(text, desp, params), //iGot
gobotNotify(text, desp),//go-cqhttp gobotNotify(text, desp),//go-cqhttp
gotifyNotify(text, desp),//gotify
]); ]);
} }
function gotifyNotify(text, desp) {
return new Promise((resolve) => {
if (GOTIFY_URL && GOTIFY_TOKEN) {
const options = {
url: `${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}`,
body: `title=${encodeURIComponent(text)}&message=${encodeURIComponent(desp)}&priority=${GOTIFY_PRIORITY}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
}
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('gotify发送通知调用API失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
if (data.id) {
console.log('gotify发送通知消息成功🎉\n');
} else {
console.log(`${data.message}\n`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve();
}
});
} else {
resolve();
}
});
}
function gobotNotify(text, desp, time = 2100) { function gobotNotify(text, desp, time = 2100) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (GOBOT_URL) { if (GOBOT_URL) {
const options = { const options = {
url: `${GOBOT_URL}?access_token=${GOBOT_TOKEN}&${GOBOT_QQ}`, url: `${GOBOT_URL}?access_token=${GOBOT_TOKEN}&${GOBOT_QQ}`,
body: `message=${text}\n${desp}`, json: {message:`${text}\n${desp}`},
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/json',
}, },
timeout, timeout,
}; };
@@ -371,7 +429,7 @@ function BarkNotify(text, desp, params = {}) {
const options = { const options = {
url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent( url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent(
desp, desp,
)}?sound=${BARK_SOUND}&${querystring.stringify(params)}`, )}?sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(params)}`,
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}, },
@@ -668,8 +726,8 @@ function qywxamNotify(text, desp) {
if (err) { if (err) {
console.log( console.log(
'成员ID:' + '成员ID:' +
ChangeUserId(desp) + ChangeUserId(desp) +
'企业微信应用消息发送通知消息失败!!\n', '企业微信应用消息发送通知消息失败!!\n',
); );
console.log(err); console.log(err);
} else { } else {
@@ -677,8 +735,8 @@ function qywxamNotify(text, desp) {
if (data.errcode === 0) { if (data.errcode === 0) {
console.log( console.log(
'成员ID:' + '成员ID:' +
ChangeUserId(desp) + ChangeUserId(desp) +
'企业微信应用消息发送通知消息成功🎉。\n', '企业微信应用消息发送通知消息成功🎉。\n',
); );
} else { } else {
console.log(`${data.errmsg}\n`); console.log(`${data.errmsg}\n`);
+503 -240
View File
@@ -1,277 +1,540 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# _*_ coding:utf-8 _*_ # _*_ coding:utf-8 _*_
import sys
import os
cur_path = os.path.abspath(os.path.dirname(__file__))
root_path = os.path.split(cur_path)[0]
sys.path.append(root_path)
import requests
import json
import traceback
import time
import hmac
import hashlib
import base64 import base64
import urllib.parse import hashlib
from requests.adapters import HTTPAdapter import hmac
from urllib3.util import Retry import json
import os
import re import re
import threading
import time
import urllib.parse
import requests
# 原先的 print 函数和主线程的锁
_print = print
mutex = threading.Lock()
# 定义新的 print 函数
def print(text, *args, **kw):
"""
使输出有序进行,不出现多线程同一时间输出导致错乱的问题。
"""
with mutex:
_print(text, *args, **kw)
# 通知服务 # 通知服务
BARK = '' # bark服务,此参数如果以http或者https开头则判定为自建bark服务; secrets可填; # fmt: off
SCKEY = '' # Server酱的SCKEY; secrets可填 push_config = {
TG_BOT_TOKEN = '' # tg机器人的TG_BOT_TOKEN; secrets可填 'HITOKOTO': False, # 启用一言(随机句子)
TG_USER_ID = '' # tg机器人的TG_USER_ID; secrets可填
TG_PROXY_IP = '' # tg机器人的TG_PROXY_IP; secrets可填
TG_PROXY_PORT = '' # tg机器人的TG_PROXY_PORT; secrets可填
DD_BOT_ACCESS_TOKEN = '' # 钉钉机器人的DD_BOT_ACCESS_TOKEN; secrets可填
DD_BOT_SECRET = '' # 钉钉机器人的DD_BOT_SECRET; secrets可填
QYWX_APP = '' # 企业微信应用的QYWX_APP; secrets可填 参考http://note.youdao.com/s/HMiudGkb
notify_mode = [] 'BARK_PUSH': '', # bark IP 或设备码,例:https://api.day.app/DxHcxxxxxRxxxxxxcm/
'BARK_ARCHIVE': '', # bark 推送是否存档
'BARK_GROUP': '', # bark 推送分组
'BARK_SOUND': '', # bark 推送声音
# GitHub action运行需要填写对应的secrets 'CONSOLE': True, # 控制台输出
if "BARK" in os.environ and os.environ["BARK"]:
BARK = os.environ["BARK"]
if "SCKEY" in os.environ and os.environ["SCKEY"]:
SCKEY = os.environ["SCKEY"]
if "TG_BOT_TOKEN" in os.environ and os.environ["TG_BOT_TOKEN"] and "TG_USER_ID" in os.environ and os.environ["TG_USER_ID"]:
TG_BOT_TOKEN = os.environ["TG_BOT_TOKEN"]
TG_USER_ID = os.environ["TG_USER_ID"]
if "DD_BOT_ACCESS_TOKEN" in os.environ and os.environ["DD_BOT_ACCESS_TOKEN"] and "DD_BOT_SECRET" in os.environ and os.environ["DD_BOT_SECRET"]:
DD_BOT_ACCESS_TOKEN = os.environ["DD_BOT_ACCESS_TOKEN"]
DD_BOT_SECRET = os.environ["DD_BOT_SECRET"]
if "QYWX_APP" in os.environ and os.environ["QYWX_APP"]:
QYWX_APP = os.environ["QYWX_APP"]
if BARK: 'DD_BOT_SECRET': '', # 钉钉机器人的 DD_BOT_SECRET
notify_mode.append('bark') 'DD_BOT_TOKEN': '', # 钉钉机器人的 DD_BOT_TOKEN
print("BARK 推送打开")
if SCKEY:
notify_mode.append('sc_key')
print("Server酱 推送打开")
if TG_BOT_TOKEN and TG_USER_ID:
notify_mode.append('telegram_bot')
print("Telegram 推送打开")
if DD_BOT_ACCESS_TOKEN and DD_BOT_SECRET:
notify_mode.append('dingding_bot')
print("钉钉机器人 推送打开")
if QYWX_APP:
notify_mode.append('qywxapp_bot')
print("企业微信应用 推送打开")
def bark(title, content): 'FSKEY': '', # 飞书机器人的 FSKEY
print("\n")
if not BARK: 'GOBOT_URL': '', # go-cqhttp
print("bark服务的bark_token未设置!!\n取消推送") # 推送到个人QQhttp://127.0.0.1/send_private_msg
# 群:http://127.0.0.1/send_group_msg
'GOBOT_QQ': '', # go-cqhttp 的推送群或用户
# GOBOT_URL 设置 /send_private_msg 时填入 user_id=个人QQ
# /send_group_msg 时填入 group_id=QQ群
'GOBOT_TOKEN': '', # go-cqhttp 的 access_token
'GOTIFY_URL': '', # gotify地址,如https://push.example.de:8080
'GOTIFY_TOKEN': '', # gotify的消息应用token
'GOTIFY_PRIORITY': 0, # 推送消息优先级,默认为0
'IGOT_PUSH_KEY': '', # iGot 聚合推送的 IGOT_PUSH_KEY
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
'QMSG_KEY': '', # qmsg 酱的 QMSG_KEY
'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE
'QYWX_AM': '', # 企业微信应用
'QYWX_KEY': '', # 企业微信机器人
'TG_BOT_TOKEN': '', # tg 机器人的 TG_BOT_TOKEN,例:1407203283:AAG9rt-6RDaaX0HBLZQq0laNOh898iFYaRQ
'TG_USER_ID': '', # tg 机器人的 TG_USER_ID,例:1434078534
'TG_API_HOST': '', # tg 代理 api
'TG_PROXY_AUTH': '', # tg 代理认证参数
'TG_PROXY_HOST': '', # tg 机器人的 TG_PROXY_HOST
'TG_PROXY_PORT': '', # tg 机器人的 TG_PROXY_PORT
}
notify_function = []
# fmt: on
# 首先读取 面板变量 或者 github action 运行变量
for k in push_config:
if os.getenv(k):
v = os.getenv(k)
push_config[k] = v
def bark(title: str, content: str) -> None:
"""
使用 bark 推送消息。
"""
if not push_config.get("BARK_PUSH"):
print("bark 服务的 BARK_PUSH 未设置!!\n取消推送")
return return
print("bark服务启动") print("bark 服务启动")
url = None
if BARK.startswith('http'): if push_config.get("BARK_PUSH").startswith("http"):
url = f"""{BARK}/{title}/{content}""" url = f'{push_config.get("BARK_PUSH")}/{urllib.parse.quote_plus(title)}/{urllib.parse.quote_plus(content)}'
else: else:
url = f"""https://api.day.app/{BARK}/{title}/{content}""" url = f'https://api.day.app/{push_config.get("BARK_PUSH")}/{urllib.parse.quote_plus(title)}/{urllib.parse.quote_plus(content)}'
bark_params = {
"BARK_ARCHIVE": "isArchive",
"BARK_GROUP": "group",
"BARK_SOUND": "sound",
}
params = ""
for pair in filter(
lambda pairs: pairs[0].startswith("BARK_")
and pairs[0] != "BARK_PUSH"
and pairs[1]
and bark_params.get(pairs[0]),
push_config.items(),
):
params += f"{bark_params.get(pair[0])}={pair[1]}&"
if params:
url = url + "?" + params.rstrip("&")
response = requests.get(url).json() response = requests.get(url).json()
if response['code'] == 200:
print('推送成功!')
else:
print('推送失败!')
def serverJ(title, content): if response["code"] == 200:
print("\n") print("bark 推送成功!")
if not SCKEY: else:
print("server酱服务的SCKEY未设置!!\n取消推送") print("bark 推送失败!")
def console(title: str, content: str) -> None:
"""
使用 控制台 推送消息。
"""
print(f"{title}\n\n{content}")
def dingding_bot(title: str, content: str) -> None:
"""
使用 钉钉机器人 推送消息。
"""
if not push_config.get("DD_BOT_SECRET") or not push_config.get("DD_BOT_TOKEN"):
print("钉钉机器人 服务的 DD_BOT_SECRET 或者 DD_BOT_TOKEN 未设置!!\n取消推送")
return return
print("serverJ服务启动") print("钉钉机器人 服务启动")
timestamp = str(round(time.time() * 1000))
secret_enc = push_config.get("DD_BOT_SECRET").encode("utf-8")
string_to_sign = "{}\n{}".format(timestamp, push_config.get("DD_BOT_SECRET"))
string_to_sign_enc = string_to_sign.encode("utf-8")
hmac_code = hmac.new(
secret_enc, string_to_sign_enc, digestmod=hashlib.sha256
).digest()
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
url = f'https://oapi.dingtalk.com/robot/send?access_token={push_config.get("DD_BOT_TOKEN")}&timestamp={timestamp}&sign={sign}'
headers = {"Content-Type": "application/json;charset=utf-8"}
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
response = requests.post(
url=url, data=json.dumps(data), headers=headers, timeout=15
).json()
if not response["errcode"]:
print("钉钉机器人 推送成功!")
else:
print("钉钉机器人 推送失败!")
def feishu_bot(title: str, content: str) -> None:
"""
使用 飞书机器人 推送消息。
"""
if not push_config.get("FSKEY"):
print("飞书 服务的 FSKEY 未设置!!\n取消推送")
return
print("飞书 服务启动")
url = f'https://open.feishu.cn/open-apis/bot/v2/hook/{push_config.get("FSKEY")}'
data = {"msg_type": "text", "content": {"text": f"{title}\n\n{content}"}}
response = requests.post(url, data=json.dumps(data)).json()
if response.get("StatusCode") == 0:
print("飞书 推送成功!")
else:
print("飞书 推送失败!错误信息如下:\n", response)
def go_cqhttp(title: str, content: str) -> None:
"""
使用 go_cqhttp 推送消息。
"""
if not push_config.get("GOBOT_URL") or not push_config.get("GOBOT_QQ"):
print("go-cqhttp 服务的 GOBOT_URL 或 GOBOT_QQ 未设置!!\n取消推送")
return
print("go-cqhttp 服务启动")
url = f'{push_config.get("GOBOT_URL")}?access_token={push_config.get("GOBOT_TOKEN")}&{push_config.get("GOBOT_QQ")}&message=标题:{title}\n内容:{content}'
response = requests.get(url).json()
if response["status"] == "ok":
print("go-cqhttp 推送成功!")
else:
print("go-cqhttp 推送失败!")
def gotify(title:str,content:str) -> None:
"""
使用 gotify 推送消息。
"""
if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"):
print("gotify 服务的 GOTIFY_URL 或 GOTIFY_TOKEN 未设置!!\n取消推送")
return
print("gotify 服务启动")
url = f'{push_config.get("GOTIFY_URL")}/message?token={push_config.get("GOTIFY_TOKEN")}'
data = {"title": title,"message": content,"priority": push_config.get("GOTIFY_PRIORITY")}
response = requests.post(url,data=data).json()
if response.get("id"):
print("gotify 推送成功!")
else:
print("gotify 推送失败!")
def iGot(title: str, content: str) -> None:
"""
使用 iGot 推送消息。
"""
if not push_config.get("IGOT_PUSH_KEY"):
print("iGot 服务的 IGOT_PUSH_KEY 未设置!!\n取消推送")
return
print("iGot 服务启动")
url = f'https://push.hellyw.com/{push_config.get("IGOT_PUSH_KEY")}'
data = {"title": title, "content": content}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
response = requests.post(url, data=data, headers=headers).json()
if response["ret"] == 0:
print("iGot 推送成功!")
else:
print(f'iGot 推送失败!{response["errMsg"]}')
def serverJ(title: str, content: str) -> None:
"""
通过 serverJ 推送消息。
"""
if not push_config.get("PUSH_KEY"):
print("serverJ 服务的 PUSH_KEY 未设置!!\n取消推送")
return
print("serverJ 服务启动")
data = {"text": title, "desp": content.replace("\n", "\n\n")}
if push_config.get("PUSH_KEY").index("SCT") != -1:
url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send'
else:
url = f'https://sc.ftqq.com/${push_config.get("PUSH_KEY")}.send'
response = requests.post(url, data=data).json()
if response.get("errno") == 0 or response.get("code") == 0:
print("serverJ 推送成功!")
else:
print(f'serverJ 推送失败!错误码:{response["message"]}')
def pushplus_bot(title: str, content: str) -> None:
"""
通过 push+ 推送消息。
"""
if not push_config.get("PUSH_PLUS_TOKEN"):
print("PUSHPLUS 服务的 PUSH_PLUS_TOKEN 未设置!!\n取消推送")
return
print("PUSHPLUS 服务启动")
url = "http://www.pushplus.plus/send"
data = { data = {
"text": title, "token": push_config.get("PUSH_PLUS_TOKEN"),
"desp": content.replace("\n", "\n\n") "title": title,
"content": content,
"topic": push_config.get("PUSH_PLUS_USER"),
} }
response = requests.post(f"https://sc.ftqq.com/{SCKEY}.send", data=data).json() body = json.dumps(data).encode(encoding="utf-8")
if response['errno'] == 0: headers = {"Content-Type": "application/json"}
print('推送成功!') response = requests.post(url=url, data=body, headers=headers).json()
else:
print('推送失败!')
def telegram_bot(title, content): if response["code"] == 200:
print("\n") print("PUSHPLUS 推送成功!")
bot_token = TG_BOT_TOKEN
user_id = TG_USER_ID else:
if not bot_token or not user_id:
print("tg服务的bot_token或者user_id未设置!!\n取消推送") url_old = "http://pushplus.hxtrip.com/send"
response = requests.post(url=url_old, data=body, headers=headers).json()
if response["code"] == 200:
print("PUSHPLUS(hxtrip) 推送成功!")
else:
print("PUSHPLUS 推送失败!")
def qmsg_bot(title: str, content: str) -> None:
"""
使用 qmsg 推送消息。
"""
if not push_config.get("QMSG_KEY") or not push_config.get("QMSG_TYPE"):
print("qmsg 的 QMSG_KEY 或者 QMSG_TYPE 未设置!!\n取消推送")
return return
print("tg服务启动") print("qmsg 服务启动")
url=f"https://api.telegram.org/bot{TG_BOT_TOKEN}/sendMessage"
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
payload = {'chat_id': str(TG_USER_ID), 'text': f'{title}\n\n{content}', 'disable_web_page_preview': 'true'}
proxies = None
if TG_PROXY_IP and TG_PROXY_PORT:
proxyStr = "http://{}:{}".format(TG_PROXY_IP, TG_PROXY_PORT)
proxies = {"http": proxyStr, "https": proxyStr}
response = requests.post(url=url, headers=headers, params=payload, proxies=proxies).json()
if response['ok']:
print('推送成功!')
else:
print('推送失败!')
def dingding_bot(title, content): url = f'https://qmsg.zendee.cn/{push_config.get("QMSG_TYPE")}/{push_config.get("QMSG_KEY")}'
timestamp = str(round(time.time() * 1000)) # 时间戳 payload = {"msg": f'{title}\n\n{content.replace("----", "-")}'.encode("utf-8")}
secret_enc = DD_BOT_SECRET.encode('utf-8') response = requests.post(url=url, params=payload).json()
string_to_sign = '{}\n{}'.format(timestamp, DD_BOT_SECRET)
string_to_sign_enc = string_to_sign.encode('utf-8')
hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code)) # 签名
print('开始使用 钉钉机器人 推送消息...', end='')
url = f'https://oapi.dingtalk.com/robot/send?access_token={DD_BOT_ACCESS_TOKEN}&timestamp={timestamp}&sign={sign}'
headers = {'Content-Type': 'application/json;charset=utf-8'}
data = {
'msgtype': 'text',
'text': {'content': f'{title}\n\n{content}'}
}
response = requests.post(url=url, data=json.dumps(data), headers=headers, timeout=15).json()
if not response['errcode']:
print('推送成功!')
else:
print('推送失败!')
def qywxapp_bot(title, content): if response["code"] == 0:
print("\n") print("qmsg 推送成功!")
if not QYWX_APP: else:
print("企业微信应用的QYWX_APP未设置!!\n取消推送") print(f'qmsg 推送失败!{response["reason"]}')
def wecom_app(title: str, content: str) -> None:
"""
通过 企业微信 APP 推送消息。
"""
if not push_config.get("QYWX_AM"):
print("QYWX_AM 未设置!!\n取消推送")
return return
print("企业微信应用启动") QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM"))
qywx_app_params = QYWX_APP.split(',') if 4 < len(QYWX_AM_AY) > 5:
url='https://qyapi.weixin.qq.com/cgi-bin/gettoken' print("QYWX_AM 设置错误!!\n取消推送")
headers= { return
'Content-Type': 'application/json', print("企业微信 APP 服务启动")
}
payload = {
'corpid': qywx_app_params[0],
'corpsecret': qywx_app_params[1],
}
response = requests.post(url=url, headers=headers, data=json.dumps(payload), timeout=15).json()
accesstoken = response["access_token"]
html = content.replace("\n", "<br/>")
options = None corpid = QYWX_AM_AY[0]
if not qywx_app_params[4]: corpsecret = QYWX_AM_AY[1]
options = { touser = QYWX_AM_AY[2]
'msgtype': 'text', agentid = QYWX_AM_AY[3]
'text': { try:
content: f'{title}\n\n${content}' media_id = QYWX_AM_AY[4]
} except IndexError:
} media_id = ""
elif qywx_app_params[4] == '0': wx = WeCom(corpid, corpsecret, agentid)
options = { # 如果没有配置 media_id 默认就以 text 方式发送
'msgtype': 'textcard', if not media_id:
'textcard': { message = title + "\n\n" + content
title: f'{title}', response = wx.send_text(message, touser)
description: f'{content}',
btntxt: '更多'
}
}
elif qywx_app_params[4] == '1':
options = {
'msgtype': 'text',
'text': {
content: f'{title}\n\n${content}'
}
}
else: else:
options = { response = wx.send_mpnews(title, content, media_id, touser)
'msgtype': 'mpnews',
'mpnews': { if response == "ok":
'articles': [ print("企业微信推送成功!")
else:
print("企业微信推送失败!错误信息如下:\n", response)
class WeCom:
def __init__(self, corpid, corpsecret, agentid):
self.CORPID = corpid
self.CORPSECRET = corpsecret
self.AGENTID = agentid
def get_access_token(self):
url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
values = {
"corpid": self.CORPID,
"corpsecret": self.CORPSECRET,
}
req = requests.post(url, params=values)
data = json.loads(req.text)
return data["access_token"]
def send_text(self, message, touser="@all"):
send_url = (
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
+ self.get_access_token()
)
send_values = {
"touser": touser,
"msgtype": "text",
"agentid": self.AGENTID,
"text": {"content": message},
"safe": "0",
}
send_msges = bytes(json.dumps(send_values), "utf-8")
respone = requests.post(send_url, send_msges)
respone = respone.json()
return respone["errmsg"]
def send_mpnews(self, title, message, media_id, touser="@all"):
send_url = (
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
+ self.get_access_token()
)
send_values = {
"touser": touser,
"msgtype": "mpnews",
"agentid": self.AGENTID,
"mpnews": {
"articles": [
{ {
'title': f'{title}', "title": title,
'thumb_media_id': f'{qywx_app_params[4]}', "thumb_media_id": media_id,
'author': '智能助手', "author": "Author",
'content_source_url': '', "content_source_url": "",
'content': f'{html}', "content": message.replace("\n", "<br/>"),
'digest': f'{content}' "digest": message,
} }
] ]
} },
} }
send_msges = bytes(json.dumps(send_values), "utf-8")
respone = requests.post(send_url, send_msges)
respone = respone.json()
return respone["errmsg"]
url=f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={accesstoken}"
data = {
'touser': f'{change_user_id(content)}',
'agentid': f'{qywx_app_params[3]}',
'safe': '0'
}
data.update(options)
headers = {
'Content-Type': 'application/json',
}
response = requests.post(url=url, headers=headers, data=json.dumps(data)).json()
if response['errcode'] == 0: def wecom_bot(title: str, content: str) -> None:
print('推送成功!')
else:
print('推送失败!')
def change_user_id(desp):
qywx_app_params = QYWX_APP.split(',')
if qywx_app_params[2]:
userIdTmp = qywx_app_params[2].split("|")
userId = ""
for i in range(len(userIdTmp)):
count1 = f"账号{i + 1}"
count2 = f"签到号{i + 1}"
if re.search(count1, desp) or re.search(count2, desp):
userId = userIdTmp[i]
if not userId:
userId = qywx_app_params[2]
return userId
else:
return "@all"
def send(title, content):
""" """
使用 bark, telegram bot, dingding bot, serverJ 发送手机推送 通过 企业微信机器人 推送消息。
:param title: """
:param content: if not push_config.get("QYWX_KEY"):
print("企业微信机器人 服务的 QYWX_KEY 未设置!!\n取消推送")
return
print("企业微信机器人服务启动")
url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={push_config.get('QYWX_KEY')}"
headers = {"Content-Type": "application/json;charset=utf-8"}
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
response = requests.post(
url=url, data=json.dumps(data), headers=headers, timeout=15
).json()
if response["errcode"] == 0:
print("企业微信机器人推送成功!")
else:
print("企业微信机器人推送失败!")
def telegram_bot(title: str, content: str) -> None:
"""
使用 telegram 机器人 推送消息。
"""
if not push_config.get("TG_BOT_TOKEN") or not push_config.get("TG_USER_ID"):
print("tg 服务的 bot_token 或者 user_id 未设置!!\n取消推送")
return
print("tg 服务启动")
if push_config.get("TG_API_HOST"):
url = f"https://{push_config.get('TG_API_HOST')}/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
else:
url = (
f"https://api.telegram.org/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
)
headers = {"Content-Type": "application/x-www-form-urlencoded"}
payload = {
"chat_id": str(push_config.get("TG_USER_ID")),
"text": f"{title}\n\n{content}",
"disable_web_page_preview": "true",
}
proxies = None
if push_config.get("TG_PROXY_HOST") and push_config.get("TG_PROXY_PORT"):
if push_config.get("TG_PROXY_AUTH") is not None and "@" not in push_config.get(
"TG_PROXY_HOST"
):
push_config["TG_PROXY_HOST"] = (
push_config.get("TG_PROXY_AUTH")
+ "@"
+ push_config.get("TG_PROXY_HOST")
)
proxyStr = "http://{}:{}".format(
push_config.get("TG_PROXY_HOST"), push_config.get("TG_PROXY_PORT")
)
proxies = {"http": proxyStr, "https": proxyStr}
response = requests.post(
url=url, headers=headers, params=payload, proxies=proxies
).json()
if response["ok"]:
print("tg 推送成功!")
else:
print("tg 推送失败!")
def one() -> str:
"""
获取一条一言。
:return: :return:
""" """
for i in notify_mode: url = "https://v1.hitokoto.cn/"
if i == 'bark': res = requests.get(url).json()
if BARK: return res["hitokoto"] + " ----" + res["from"]
bark(title=title, content=content)
else:
print('未启用 bark') if push_config.get("BARK_PUSH"):
continue notify_function.append(bark)
if i == 'sc_key': if push_config.get("CONSOLE"):
if SCKEY: notify_function.append(console)
serverJ(title=title, content=content) if push_config.get("DD_BOT_TOKEN") and push_config.get("DD_BOT_SECRET"):
else: notify_function.append(dingding_bot)
print('未启用 Server酱') if push_config.get("FSKEY"):
continue notify_function.append(feishu_bot)
elif i == 'dingding_bot': if push_config.get("GOBOT_URL") and push_config.get("GOBOT_QQ"):
if DD_BOT_ACCESS_TOKEN and DD_BOT_SECRET: notify_function.append(go_cqhttp)
dingding_bot(title=title, content=content) if push_config.get("GOTIFY_URL") and push_config.get("GOTIFY_TOKEN"):
else: notify_function.append(gotify)
print('未启用 钉钉机器人') if push_config.get("IGOT_PUSH_KEY"):
continue notify_function.append(iGot)
elif i == 'telegram_bot': if push_config.get("PUSH_KEY"):
if TG_BOT_TOKEN and TG_USER_ID: notify_function.append(serverJ)
telegram_bot(title=title, content=content) if push_config.get("PUSH_PLUS_TOKEN"):
else: notify_function.append(pushplus_bot)
print('未启用 telegram机器人') if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
continue notify_function.append(qmsg_bot)
elif i == 'qywxapp_bot': if push_config.get("QYWX_AM"):
if QYWX_APP: notify_function.append(wecom_app)
qywxapp_bot(title=title, content=content) if push_config.get("QYWX_KEY"):
else: notify_function.append(wecom_bot)
print('未启用 企业微信应用推送') if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
continue notify_function.append(telegram_bot)
else:
print('此类推送方式不存在')
def send(title: str, content: str) -> None:
if not content:
print(f"{title} 推送内容为空!")
return
hitokoto = push_config.get("HITOKOTO")
text = one() if hitokoto else ""
content += "\n\n" + text
ts = [
threading.Thread(target=mode, args=(title, content), name=mode.__name__)
for mode in notify_function
]
[t.start() for t in ts]
[t.join() for t in ts]
def main(): def main():
send('title', 'content') send("title", "content")
if __name__ == '__main__': if __name__ == "__main__":
main() main()
+1 -1
View File
@@ -5,8 +5,8 @@
"dependencies": { "dependencies": {
"crypto-js": "^4.0.0", "crypto-js": "^4.0.0",
"download": "^8.0.0", "download": "^8.0.0",
"got": "^11.5.1",
"http-server": "^0.12.3", "http-server": "^0.12.3",
"got": "^11.5.1",
"qrcode-terminal": "^0.12.0", "qrcode-terminal": "^0.12.0",
"request": "^2.88.2", "request": "^2.88.2",
"tough-cookie": "^4.0.0", "tough-cookie": "^4.0.0",
-1
View File
@@ -1 +0,0 @@
requests
+1
View File
@@ -0,0 +1 @@
#!/usr/bin/env bash
+31 -29
View File
@@ -6,7 +6,7 @@ get_token() {
add_cron_api() { add_cron_api() {
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
if [ $# -eq 1 ]; then if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}') local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}') local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}') local name=$(echo "$1" | awk -F ":" '{print $3}')
@@ -17,13 +17,13 @@ add_cron_api() {
fi fi
local api=$( local api=$(
curl -s "http://localhost:5600/api/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?t=$currentTimeStamp" \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -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 "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 "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://localhost:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://localhost:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \ --data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \
--compressed --compressed
@@ -39,7 +39,7 @@ add_cron_api() {
update_cron_api() { update_cron_api() {
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
if [ $# -eq 1 ]; then if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}') local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}') local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}') local name=$(echo "$1" | awk -F ":" '{print $3}')
@@ -52,14 +52,14 @@ update_cron_api() {
fi fi
local api=$( local api=$(
curl -s "http://localhost:5600/api/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?t=$currentTimeStamp" \
-X 'PUT' \ -X 'PUT' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -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 "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 "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://localhost:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://localhost:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"_id\":\"$id\"}" \ --data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"_id\":\"$id\"}" \
--compressed --compressed
@@ -75,7 +75,7 @@ update_cron_api() {
update_cron_command_api() { update_cron_command_api() {
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
if [ $# -eq 1 ]; then if [[ $# -eq 1 ]]; then
local command=$(echo "$1" | awk -F ":" '{print $1}') local command=$(echo "$1" | awk -F ":" '{print $1}')
local id=$(echo "$1" | awk -F ":" '{print $2}') local id=$(echo "$1" | awk -F ":" '{print $2}')
else else
@@ -84,14 +84,14 @@ update_cron_command_api() {
fi fi
local api=$( local api=$(
curl -s "http://localhost:5600/api/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?t=$currentTimeStamp" \
-X 'PUT' \ -X 'PUT' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -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 "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 "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://localhost:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://localhost:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"command\":\"$command\",\"_id\":\"$id\"}" \ --data-raw "{\"command\":\"$command\",\"_id\":\"$id\"}" \
--compressed --compressed
@@ -109,14 +109,14 @@ del_cron_api() {
local ids=$1 local ids=$1
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$( local api=$(
curl -s "http://localhost:5600/api/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?t=$currentTimeStamp" \
-X 'DELETE' \ -X 'DELETE' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -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 "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 "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://localhost:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://localhost:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "[$ids]" \ --data-raw "[$ids]" \
--compressed --compressed
@@ -133,11 +133,11 @@ del_cron_api() {
get_user_info() { get_user_info() {
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$( local api=$(
curl -s "http://localhost:5600/api/user?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/api/user?t=$currentTimeStamp" \
-H 'Accept: */*' \ -H 'Accept: */*' \
-H "Authorization: Bearer $token" \ -H "Authorization: Bearer $token" \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \ -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \
-H 'Referer: http://localhost:5700/crontab' \ -H 'Referer: http://0.0.0.0:5700/crontab' \
-H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \ -H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \
--compressed --compressed
) )
@@ -148,30 +148,32 @@ get_user_info() {
fi fi
} }
update_cron_status() { update_cron() {
local ids=$1 local ids="$1"
local status=$2 local status="$2"
local pid="${3:-''}"
local logPath="$4"
local lastExecutingTime="${5:-0}"
local runningTime="${6:-0}"
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$( local api=$(
curl -s "http://localhost:5600/api/crons/status?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons/status?t=$currentTimeStamp" \
-X 'PUT' \ -X 'PUT' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -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 "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 "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://localhost:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://localhost:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"ids\":[$ids],\"status\":\"$status\"}" \ --data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code != 200 ]]; then
echo -e "更新任务状态成功" echo -e "\n## 更新任务状态失败(${message})\n" >> $log_path
else
echo -e "更新任务状态失败(${message})"
fi fi
} }
get_token get_token
+10 -6
View File
@@ -11,9 +11,7 @@ apk --no-cache add -f zlib-dev gcc jpeg-dev python3-dev musl-dev freetype-dev
echo -e "\nbot依赖安装成功...\n" echo -e "\nbot依赖安装成功...\n"
echo -e "2、下载bot所需文件...\n" echo -e "2、下载bot所需文件...\n"
if [ -d ${repo_path}/.git ]; then if [[ ! -d ${repo_path}/.git ]]; then
git_pull_scripts ${repo_path}
else
rm -rf ${repo_path} rm -rf ${repo_path}
git_clone_scripts ${url} ${repo_path} "main" git_clone_scripts ${url} ${repo_path} "main"
fi fi
@@ -29,7 +27,15 @@ if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror pip3 config set global.index-url $PipMirror
fi fi
cp -f "$repo_path/jbot/requirements.txt" "$dir_root" cp -f "$repo_path/jbot/requirements.txt" "$dir_root"
pip3 --default-timeout=100 install -r requirements.txt --no-cache-dir
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 "\npython3依赖安装成功...\n"
echo -e "4、启动bot程序...\n" echo -e "4、启动bot程序...\n"
@@ -38,5 +44,3 @@ cd $dir_root
ps -ef | grep "python3 -m jbot" | grep -v grep | awk '{print $1}' | xargs kill -9 2>/dev/null 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 & nohup python3 -m jbot >$dir_log/bot/nohup.log 2>&1 &
echo -e "bot启动成功...\n" echo -e "bot启动成功...\n"
exit 0
+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
-151
View File
@@ -1,151 +0,0 @@
#!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=/ql/shell
. $dir_shell/share.sh
## 生成pt_pin清单
gen_pt_pin_array() {
local tmp1 tmp2 i pt_pin_temp
for ((user_num = 1; user_num <= $user_sum; user_num++)); do
tmp1=Cookie$user_num
tmp2=${!tmp1}
i=$(($user_num - 1))
pt_pin_temp=$(echo $tmp2 | perl -pe "{s|.*pt_pin=([^; ]+)(?=;?).*|\1|; s|%|\\\x|g}")
[[ $pt_pin_temp == *\\x* ]] && pt_pin[i]=$(printf $pt_pin_temp) || pt_pin[i]=$pt_pin_temp
done
}
## 导出互助码的通用程序,$1:去掉后缀的脚本名称,$2:config.sh中的后缀,$3:活动中文名称
export_codes_sub() {
local task_name=$1
local config_name=$2
local chinese_name=$3
local config_name_my=My$config_name
local config_name_for_other=ForOther$config_name
local i j k m n pt_pin_in_log code tmp_grep tmp_my_code tmp_for_other user_num random_num_list
if cd $dir_log/$task_name &>/dev/null && [[ $(ls) ]]; then
## 寻找所有互助码以及对应的pt_pin
i=0
pt_pin_in_log=()
code=()
pt_pin_and_code=$(ls -r *.log | xargs awk -v var="$chinese_name好友互助码" 'BEGIN{FS="[ )】]+"; OFS="&"} $3~var {print $2,$4}')
for line in $pt_pin_and_code; do
pt_pin_in_log[i]=$(echo $line | awk -F "&" '{print $1}')
code[i]=$(echo $line | awk -F "&" '{print $2}')
let i++
done
## 输出My系列变量
if [[ ${#code[*]} -gt 0 ]]; then
for ((m = 0; m < ${#pt_pin[*]}; m++)); do
tmp_my_code=""
j=$((m + 1))
for ((n = 0; n < ${#code[*]}; n++)); do
if [[ ${pt_pin[m]} == ${pt_pin_in_log[n]} ]]; then
tmp_my_code=${code[n]}
break
fi
done
echo "$config_name_my$j='$tmp_my_code'"
done
else
echo "## 从日志中未找到任何互助码"
fi
## 输出ForOther系列变量
if [[ ${#code[*]} -gt 0 ]]; then
echo
case $HelpType in
0) ## 全部一致
tmp_for_other=""
for ((m = 0; m < ${#pt_pin[*]}; m++)); do
j=$((m + 1))
tmp_for_other="$tmp_for_other@\${$config_name_my$j}"
done
echo "${config_name_for_other}1=\"$tmp_for_other\"" | perl -pe "s|($config_name_for_other\d+=\")@|\1|"
for ((m = 1; m < ${#pt_pin[*]}; m++)); do
j=$((m + 1))
echo "$config_name_for_other$j=\"\${${config_name_for_other}1}\""
done
;;
1) ## 均等助力
for ((m = 0; m < ${#pt_pin[*]}; m++)); do
tmp_for_other=""
j=$((m + 1))
for ((n = $m; n < $(($user_sum + $m)); n++)); do
[[ $m -eq $n ]] && continue
if [[ $((n + 1)) -le $user_sum ]]; then
k=$((n + 1))
else
k=$((n + 1 - $user_sum))
fi
tmp_for_other="$tmp_for_other@\${$config_name_my$k}"
done
echo "$config_name_for_other$j=\"$tmp_for_other\"" | perl -pe "s|($config_name_for_other\d+=\")@|\1|"
done
;;
2) ## 本套脚本内账号间随机顺序助力
for ((m = 0; m < ${#pt_pin[*]}; m++)); do
tmp_for_other=""
random_num_list=$(seq $user_sum | sort -R)
j=$((m + 1))
for n in $random_num_list; do
[[ $j -eq $n ]] && continue
tmp_for_other="$tmp_for_other@\${$config_name_my$n}"
done
echo "$config_name_for_other$j=\"$tmp_for_other\"" | perl -pe "s|($config_name_for_other\d+=\")@|\1|"
done
;;
*) ## 按编号优先
for ((m = 0; m < ${#pt_pin[*]}; m++)); do
tmp_for_other=""
j=$((m + 1))
for ((n = 0; n < ${#pt_pin[*]}; n++)); do
[[ $m -eq $n ]] && continue
k=$((n + 1))
tmp_for_other="$tmp_for_other@\${$config_name_my$k}"
done
echo "$config_name_for_other$j=\"$tmp_for_other\"" | perl -pe "s|($config_name_for_other\d+=\")@|\1|"
done
;;
esac
fi
else
echo "## 未运行过 $task_name.js 脚本,未产生日志"
fi
}
## 汇总输出
export_all_codes() {
gen_pt_pin_array
echo -e "\n# 从日志提取互助码,编号和配置文件中Cookie编号完全对应,如果为空就是所有日志中都没有。\n\n# 即使某个MyXxx变量未赋值,也可以将其变量名填在ForOtherXxx中,jtask脚本会自动过滤空值。\n"
echo -n "# 你选择的互助码模板为:"
case $HelpType in
0)
echo "所有账号助力码全部一致。"
;;
1)
echo "所有账号机会均等助力。"
;;
2)
echo "本套脚本内账号间随机顺序助力。"
;;
*)
echo "按账号编号优先。"
;;
esac
for ((i = 0; i < ${#name_js[*]}; i++)); do
echo -e "\n## ${name_chinese[i]}"
export_codes_sub "${name_js[i]}" "${name_config[i]}" "${name_chinese[i]}"
done
}
## 执行并写入日志
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
log_path="$dir_code/$log_time.log"
make_dir "$dir_code"
export_all_codes | perl -pe "{s|京东种豆|种豆|; s|crazyJoy任务|疯狂的JOY|}" | tee $log_path
-26
View File
@@ -1,26 +0,0 @@
#!/usr/bin/env bash
# 导入通用变量与函数
dir_shell=/ql/shell
. $dir_shell/share.sh
echo -e "1. 开始安装青龙依赖\n"
npm_install_2 $dir_root
echo -e "青龙依赖安装完成\n"
echo -e "2. 开始安装脚本依赖\n"
cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts
echo -e "脚本依赖安装完成\n"
echo -e "3. 复制通知文件\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"
exit 0
+1 -1
View File
@@ -27,7 +27,7 @@ remove_js_log() {
remove_empty_dir() { remove_empty_dir() {
cd $dir_log cd $dir_log
for dir in $(ls); do for dir in $(ls); do
if [ -d $dir ] && [[ -z $(ls $dir) ]]; then if [[ -d $dir ]] && [[ -z $(ls $dir) ]]; then
rm -rf $dir rm -rf $dir
fi fi
done done
+84 -63
View File
@@ -10,7 +10,7 @@ dir_repo=$dir_root/repo
dir_raw=$dir_root/raw dir_raw=$dir_root/raw
dir_log=$dir_root/log dir_log=$dir_root/log
dir_db=$dir_root/db dir_db=$dir_root/db
dir_manual_log=$dir_root/manual_log dir_dep=$dir_root/deps
dir_list_tmp=$dir_log/.tmp dir_list_tmp=$dir_log/.tmp
dir_code=$dir_log/code dir_code=$dir_log/code
dir_update_log=$dir_log/update dir_update_log=$dir_log/update
@@ -18,17 +18,23 @@ ql_static_repo=$dir_repo/static
## 文件 ## 文件
file_config_sample=$dir_sample/config.sample.sh file_config_sample=$dir_sample/config.sample.sh
file_cookie=$dir_config/cookie.sh file_env=$dir_config/env.sh
file_sharecode=$dir_config/sharecode.sh file_sharecode=$dir_config/sharecode.sh
file_config_user=$dir_config/config.sh file_config_user=$dir_config/config.sh
file_auth_sample=$dir_sample/auth.sample.json file_auth_sample=$dir_sample/auth.sample.json
file_auth_user=$dir_config/auth.json file_auth_user=$dir_config/auth.json
file_extra_shell=$dir_config/extra.sh 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_extra_sample=$dir_sample/extra.sample.sh
file_notify_js_sample=$dir_sample/notify.js file_notify_js_sample=$dir_sample/notify.js
file_notify_py_sample=$dir_sample/notify.py file_notify_py_sample=$dir_sample/notify.py
file_notify_py=$dir_scripts/notify.py file_notify_py=$dir_scripts/notify.py
file_notify_js=$dir_scripts/sendNotify.js file_notify_js=$dir_scripts/sendNotify.js
task_error_log_path=$dir_log/task_error.log
nginx_app_conf=$dir_root/docker/front.conf
nginx_conf=$dir_root/docker/nginx.conf
## 清单文件 ## 清单文件
list_crontab_user=$dir_config/crontab.list list_crontab_user=$dir_config/crontab.list
@@ -50,22 +56,25 @@ original_name=(
notify.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() { import_config() {
[ -f $file_config_user ] && . $file_config_user [[ -f $file_config_user ]] && . $file_config_user
user_sum=0 [[ -f $file_env ]] && . $file_env
for line in $(cat $file_cookie); do
let user_sum+=1
eval Cookie${user_sum}="\"${line}\""
done
command_timeout_time=${CommandTimeoutTime:-"1h"} command_timeout_time=${CommandTimeoutTime:-"1h"}
github_proxy_url=${GithubProxyUrl:-""} github_proxy_url=${GithubProxyUrl:-""}
block_cookie=${TempBlockCookie:-""}
file_extensions=${RepoFileExtensions:-"js py"} file_extensions=${RepoFileExtensions:-"js py"}
if [[ -n "${DefaultCronRule}" ]]; then
default_cron="${DefaultCronRule}"
else
default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
fi
} }
## 创建目录,$1:目录的绝对路径
make_dir() { make_dir() {
local dir=$1 local dir=$1
if [[ ! -d $dir ]]; then if [[ ! -d $dir ]]; then
@@ -73,7 +82,6 @@ make_dir() {
fi fi
} }
## 检测termux
detect_termux() { detect_termux() {
if [[ ${ANDROID_RUNTIME_ROOT}${ANDROID_ROOT} ]] || [[ $PATH == *com.termux* ]]; then if [[ ${ANDROID_RUNTIME_ROOT}${ANDROID_ROOT} ]] || [[ $PATH == *com.termux* ]]; then
is_termux=1 is_termux=1
@@ -82,32 +90,28 @@ detect_termux() {
fi fi
} }
## 检测macos
detect_macos() { detect_macos() {
[[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0 [[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0
} }
## 生成随机数,$1:用来求余的数字
gen_random_num() { gen_random_num() {
local divi=$1 local divi=$1
echo $((${RANDOM} % $divi)) echo $((${RANDOM} % $divi))
} }
## 创建软连接的子函数,$1:软连接文件路径,$2:要连接的对象
link_shell_sub() { link_shell_sub() {
local link_path="$1" local link_path="$1"
local original_path="$2" local original_path="$2"
if [ ! -L $link_path ] || [[ $(readlink -f $link_path) != $original_path ]]; then if [[ ! -L $link_path ]] || [[ $(readlink -f $link_path) != $original_path ]]; then
rm -f $link_path 2>/dev/null rm -f $link_path 2>/dev/null
ln -sf $original_path $link_path ln -sf $original_path $link_path
fi fi
} }
## 创建软连接
link_shell() { link_shell() {
if [[ $is_termux -eq 1 ]]; then if [[ $is_termux -eq 1 ]]; then
local path="/data/data/com.termux/files/usr/bin/" local path="/data/data/com.termux/files/usr/bin/"
elif [[ $PATH == */usr/local/bin* ]] && [ -d /usr/local/bin ]; then elif [[ $PATH == */usr/local/bin* ]] && [[ -d /usr/local/bin ]]; then
local path="/usr/local/bin/" local path="/usr/local/bin/"
else else
local path="" local path=""
@@ -120,19 +124,18 @@ link_shell() {
fi fi
} }
## 定义各命令
define_cmd() { define_cmd() {
local cmd_prefix cmd_suffix local cmd_prefix cmd_suffix
if type task >/dev/null 2>&1; then if type task &>/dev/null; then
cmd_suffix="" cmd_suffix=""
if [ -x "$dir_shell/task.sh" ]; then if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix="" cmd_prefix=""
else else
cmd_prefix="bash " cmd_prefix="bash "
fi fi
else else
cmd_suffix=".sh" cmd_suffix=".sh"
if [ -x "$dir_shell/task.sh" ]; then if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix="$dir_shell/" cmd_prefix="$dir_shell/"
else else
cmd_prefix="bash $dir_shell/" cmd_prefix="bash $dir_shell/"
@@ -143,74 +146,82 @@ define_cmd() {
done done
} }
## 修复配置文件
fix_config() { fix_config() {
make_dir $dir_config make_dir $dir_config
make_dir $dir_log make_dir $dir_log
make_dir $dir_db make_dir $dir_db
make_dir $dir_manual_log
make_dir $dir_scripts make_dir $dir_scripts
make_dir $dir_list_tmp make_dir $dir_list_tmp
make_dir $dir_repo make_dir $dir_repo
make_dir $dir_raw make_dir $dir_raw
make_dir $dir_update_log make_dir $dir_update_log
make_dir $dir_dep
if [ ! -s $file_config_user ]; then if [[ ! -s $file_config_user ]]; then
echo -e "复制一份 $file_config_sample$file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n" echo -e "复制一份 $file_config_sample$file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
cp -fv $file_config_sample $file_config_user cp -fv $file_config_sample $file_config_user
echo echo
fi fi
if [ ! -f $file_cookie ]; then if [[ ! -f $file_env ]]; then
echo -e "检测到config配置目录下不存在cookie.sh,创建一个空文件用于初始化...\n" echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n"
touch $file_cookie touch $file_env
echo echo
fi fi
if [ ! -f $file_extra_shell ]; then 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" echo -e "复制一份 $file_extra_sample$file_extra_shell\n"
cp -fv $file_extra_sample $file_extra_shell cp -fv $file_extra_sample $file_extra_shell
echo echo
fi fi
if [ ! -s $file_auth_user ]; then if [[ ! -s $file_auth_user ]]; then
echo -e "复制一份 $file_auth_sample$file_auth_user\n" echo -e "复制一份 $file_auth_sample$file_auth_user\n"
cp -fv $file_auth_sample $file_auth_user cp -fv $file_auth_sample $file_auth_user
echo echo
fi fi
if [ ! -s $file_notify_py ]; then if [[ ! -s $file_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$file_notify_py\n" echo -e "复制一份 $file_notify_py_sample$file_notify_py\n"
cp -fv $file_notify_py_sample $file_notify_py cp -fv $file_notify_py_sample $file_notify_py
echo echo
fi fi
if [ ! -s $file_notify_js ]; then if [[ ! -s $file_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$file_notify_js\n" echo -e "复制一份 $file_notify_js_sample$file_notify_js\n"
cp -fv $file_notify_js_sample $file_notify_js cp -fv $file_notify_js_sample $file_notify_js
echo echo
fi fi
if [ -s /etc/nginx/conf.d/default.conf ]; then if [[ -s /etc/nginx/conf.d/default.conf ]]; then
echo -e "检测到默认nginx配置文件,删除...\n" echo -e "检测到默认nginx配置文件,清空...\n"
rm -f /etc/nginx/conf.d/default.conf cat /dev/null > /etc/nginx/conf.d/default.conf
echo echo
fi fi
} }
## npm install 子程序,判断是否为安卓,判断是否安装有pnpm
npm_install_sub() { npm_install_sub() {
if [ $is_termux -eq 1 ]; then if [ $is_termux -eq 1 ]; then
npm install --production --no-save --no-bin-links --registry=https://registry.npm.taobao.org || npm install --production --no-bin-links --no-save npm install --production --no-bin-links --registry=https://registry.npm.taobao.org || npm install --production --no-bin-links
elif ! type pnpm >/dev/null 2>&1; then elif ! type pnpm &>/dev/null; then
npm install --production --no-save --registry=https://registry.npm.taobao.org || npm install --production --no-save npm install --production --registry=https://registry.npm.taobao.org || npm install --production
else else
echo -e "检测到本机安装了 pnpm,使用 pnpm 替代 ...\n" pnpm install --production --registry=https://registry.npm.taobao.org || pnpm install --production
pnpm install --prod
fi fi
} }
## npm install$1package.json文件所在路径
npm_install_1() { npm_install_1() {
local dir_current=$(pwd) local dir_current=$(pwd)
local dir_work=$1 local dir_work=$1
@@ -236,66 +247,76 @@ npm_install_2() {
cd $dir_current cd $dir_current
} }
## 比对两个文件,$1比$2新时,将$1复制为$2
diff_and_copy() { diff_and_copy() {
local copy_source=$1 local copy_source=$1
local copy_to=$2 local copy_to=$2
if [ ! -s $copy_to ] || [[ $(diff $copy_source $copy_to) ]]; then if [[ ! -s $copy_to ]] || [[ $(diff $copy_source $copy_to) ]]; then
cp -f $copy_source $copy_to cp -f $copy_source $copy_to
fi fi
} }
## 更新依赖
update_depend() { update_depend() {
local dir_current=$(pwd) local dir_current=$(pwd)
if [ ! -s $dir_scripts/package.json ] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
cp -f $dir_sample/package.json $dir_scripts/package.json cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts npm_install_2 $dir_scripts
fi fi
if [ ! -s $dir_scripts/requirements.txt ] || [[ $(diff $dir_sample/requirements.txt $dir_scripts/requirements.txt) ]]; then
cp -f $dir_sample/requirements.txt $dir_scripts/requirements.txt
cd $dir_scripts
pip3 install -r $dir_scripts/requirements.txt
fi
cd $dir_current cd $dir_current
} }
## 克隆脚本,$1:仓库地址,$2:仓库保存路径,$3:分支(可省略)
git_clone_scripts() { git_clone_scripts() {
local url=$1 local url=$1
local dir=$2 local dir=$2
local branch=$3 local branch=$3
[[ $branch ]] && local cmd="-b $branch " [[ $branch ]] && local part_cmd="-b $branch "
echo -e "开始克隆仓库 $url$dir\n" echo -e "开始克隆仓库 $url$dir\n"
git clone $cmd $url $dir git clone $part_cmd $url $dir
exit_status=$? exit_status=$?
} }
## 更新脚本,$1:仓库保存路径
git_pull_scripts() { git_pull_scripts() {
local dir_current=$(pwd) local dir_current=$(pwd)
local dir_work="$1" local dir_work="$1"
local branch="$2" local branch="$2"
[[ $branch ]] && local cmd="origin/${branch}" [[ $branch ]] && local part_cmd="origin/${branch}"
cd $dir_work cd $dir_work
echo -e "开始更新仓库:$dir_work\n" echo -e "开始更新仓库:$dir_work\n"
git fetch --all git fetch --all
exit_status=$? exit_status=$?
git reset --hard $cmd git reset --hard $part_cmd
git pull git pull
cd $dir_current cd $dir_current
} }
init_env() { reset_romote_url() {
TempBlockCookie="" 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_termux
detect_macos detect_macos
define_cmd define_cmd
init_env fix_config
import_config $1
import_config $1 > $task_error_log_path 2>&1
+241 -91
View File
@@ -5,48 +5,17 @@ dir_shell=/ql/shell
. $dir_shell/share.sh . $dir_shell/share.sh
. $dir_shell/api.sh . $dir_shell/api.sh
## 组合Cookie和互助码子程序,$1:要组合的内容
combine_sub() {
local what_combine=$1
local combined_all=""
local tmp1 tmp2
for ((i = 1; i <= $user_sum; i++)); do
for num in $block_cookie; do
[[ $i -eq $num ]] && continue 2
done
local tmp1=$what_combine$i
local tmp2=${!tmp1}
combined_all="$combined_all&$tmp2"
done
echo $combined_all | perl -pe "{s|^&||; s|^@+||; s|&@|&|g; s|@+&|&|g; s|@+|@|g; s|@+$||}"
}
## 正常依次运行时,组合所有账号的Cookie与互助码
combine_all() {
for ((i = 0; i < ${#env_name[*]}; i++)); do
result=$(combine_sub ${var_name[i]})
if [[ $result ]]; then
export ${env_name[i]}="$result"
fi
done
}
## 并发运行时,直接申明每个账号的Cookie与互助码,$1:用户Cookie编号
combine_one() {
local user_num=$1
for ((i = 0; i < ${#env_name[*]}; i++)); do
local tmp=${var_name[i]}$user_num
export ${env_name[i]}=${!tmp}
done
}
## 选择python3还是node ## 选择python3还是node
define_program() { define_program() {
local p1=$1 local first_param=$1
if [[ $p1 == *.js ]]; then if [[ $first_param == *.js ]]; then
which_program="node" which_program="node"
elif [[ $p1 == *.py ]]; then elif [[ $first_param == *.py ]]; then
which_program="python3" which_program="python3"
elif [[ $first_param == *.sh ]]; then
which_program="bash"
elif [[ $first_param == *.ts ]]; then
which_program="ts-node-transpile-only"
else else
which_program="" which_program=""
fi fi
@@ -70,7 +39,7 @@ gen_array_scripts() {
local i="-1" local i="-1"
cd $dir_scripts cd $dir_scripts
for file in $(ls); do for file in $(ls); do
if [ -f $file ] && [[ $file == *.js && $file != sendNotify.js ]]; then if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
let i++ let i++
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g") array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1) array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1)
@@ -85,9 +54,10 @@ usage() {
define_cmd define_cmd
gen_array_scripts gen_array_scripts
echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:" echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:"
echo -e "1.$cmd_task <file_name> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数" echo -e "1.$cmd_task <file_name> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数"
echo -e "2.$cmd_task <file_name> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中" echo -e "2.$cmd_task <file_name> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中"
echo -e "3.$cmd_task <file_name> conc # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中" echo -e "3.$cmd_task <file_name> conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行"
echo -e "4.$cmd_task <file_name> desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行"
if [[ ${#array_scripts[*]} -gt 0 ]]; then if [[ ${#array_scripts[*]} -gt 0 ]]; then
echo -e "\n当前有以下脚本可以运行:" echo -e "\n当前有以下脚本可以运行:"
for ((i = 0; i < ${#array_scripts[*]}; i++)); do for ((i = 0; i < ${#array_scripts[*]}; i++)); do
@@ -106,84 +76,264 @@ run_nohup() {
## 正常运行单个脚本,$1:传入参数 ## 正常运行单个脚本,$1:传入参数
run_normal() { run_normal() {
local p1=$1 local first_param=$1
cd $dir_scripts define_program "$first_param"
define_program "$p1" if [[ $first_param == *.js ]]; then
if [[ $p1 == *.js ]]; then
if [[ $AutoHelpOther == true ]] && [[ $(ls $dir_code) ]]; then
local latest_log=$(ls -r $dir_code | head -1)
. $dir_code/$latest_log
fi
if [[ $# -eq 1 ]]; then if [[ $# -eq 1 ]]; then
random_delay random_delay
fi fi
fi fi
combine_all
log_time=$(date "+%Y-%m-%d-%H-%M-%S") log_time=$(date "+%Y-%m-%d-%H-%M-%S")
log_dir_tmp="${p1##*/}" log_dir_tmp="${first_param##*/}"
log_dir_tmp_path="${first_param%%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="$dir_log/${log_dir_tmp%%.*}" log_dir="$dir_log/${log_dir_tmp%%.*}"
log_path="$log_dir/$log_time.log" log_path="$log_dir/$log_time.log"
cmd="&>> $log_path"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$log_dir" make_dir "$log_dir"
local id=$(cat $list_crontab_user | grep -E "$cmd_task $p1$" | perl -pe "s|.*ID=(.*) $cmd_task $p1$|\1|" | xargs | sed 's/ /","/g') local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
update_cron_status "\"$id\"" "0" local begin_timestamp=$(date "+%s" -d "$begin_time")
timeout $command_timeout_time $which_program $p1 2>&1 | tee $log_path eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
update_cron_status "\"$id\"" "1" [[ -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
} }
## 并发执行,因为是并发,所以日志只能直接记录在日志文件中(日志文件以Cookie编号结尾),前台执行并发跑时不会输出日志
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行 ## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
run_concurrent() { run_concurrent() {
local p1=$1 local first_param="$1"
cd $dir_scripts local env_param="$2"
define_program "$p1" local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
log_dir="$dir_log/${p1%%.*}" if [[ ! $env_param ]]; then
make_dir $log_dir echo -e "\n 缺少并发运行的环境变量参数"
log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N") exit 1
echo -e "\n各账号间已经在后台开始并发执行,前台不输入日志,日志直接写入文件中。\n" fi
for ((user_num = 1; user_num <= $user_sum; user_num++)); do
combine_one $user_num local envs=$(eval echo "\$${env_param}")
log_path="$log_dir/${log_time}_${user_num}.log" local array=($(echo $envs | sed 's/&/ /g'))
timeout $command_timeout_time $which_program $p1 &>$log_path & 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 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() { run_else() {
local log_time=$(date "+%Y-%m-%d-%H-%M-%S") local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
local log_dir="$dir_log/$1" local log_dir_tmp="${1##*/}"
local log_path="$log_dir/$log_time.log" 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" make_dir "$log_dir"
timeout $command_timeout_time "$@" 2>&1 | tee $log_path
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() { main() {
case $# in show_log="false"
0) while getopts ":l" opt
echo do
usage case $opt in
;; l)
1) show_log="true"
run_normal $1 ;;
;; esac
2) done
case $2 in [[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
now)
run_normal $1 $2 if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
;; case $# in
conc) 1)
run_concurrent $1 $2 run_normal "$1"
;; ;;
*) *)
run_else "$@" case $2 in
now)
run_normal "$1" "$2"
;;
conc)
run_concurrent "$1" "$3" "$*"
;;
desi)
run_designated "$1" "$3" "$*"
;;
*)
run_else "$@"
;;
esac
;; ;;
esac esac
;; [[ -f $log_path ]] && cat $log_path
*) elif [[ $# -eq 0 ]]; then
echo
usage
else
run_else "$@" run_else "$@"
;; fi
esac
} }
main "$@" main "$@"
+196 -116
View File
@@ -1,41 +1,23 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# 导入通用变量与函数
dir_shell=/ql/shell dir_shell=/ql/shell
. $dir_shell/share.sh . $dir_shell/share.sh
. $dir_shell/api.sh . $dir_shell/api.sh
send_mark=$dir_shell/send_mark send_mark=$dir_shell/send_mark
## 重置仓库remote urldocker专用,$1:要重置的目录,$2:要重置为的网址
reset_romote_url() {
local dir_current=$(pwd)
local dir_work=$1
local url=$2
local branch="$3"
[[ $branch ]] && local cmd="origin/${branch}"
if [ -d "$dir_work/.git" ]; then
cd $dir_work
git remote set-url origin $url >/dev/null
git reset --hard $cmd >/dev/null
cd $dir_current
fi
}
## 检测cron的差异,$1:脚本清单文件路径,$2:cron任务清单文件路径,$3:增加任务清单文件路径,$4:删除任务清单文件路径 ## 检测cron的差异,$1:脚本清单文件路径,$2:cron任务清单文件路径,$3:增加任务清单文件路径,$4:删除任务清单文件路径
diff_cron() { diff_cron() {
local list_scripts="$1" local list_scripts="$1"
local list_task="$2" local list_task="$2"
local list_add="$3" local list_add="$3"
local list_drop="$4" local list_drop="$4"
if [ -s $list_task ]; then if [[ -s $list_task ]]; then
grep -vwf $list_task $list_scripts >$list_add grep -vwf $list_task $list_scripts >$list_add
elif [ ! -s $list_task ] && [ -s $list_scripts ]; then elif [[ ! -s $list_task ]] && [[ -s $list_scripts ]]; then
cp -f $list_scripts $list_add cp -f $list_scripts $list_add
fi fi
if [ -s $list_scripts ]; then if [[ -s $list_scripts ]]; then
grep -vwf $list_scripts $list_task >$list_drop grep -vwf $list_scripts $list_task >$list_drop
else else
cp -f $list_task $list_drop cp -f $list_task $list_drop
@@ -46,18 +28,18 @@ diff_cron() {
detect_config_version() { detect_config_version() {
## 识别出两个文件的版本号 ## 识别出两个文件的版本号
ver_config_sample=$(grep " Version: " $file_config_sample | perl -pe "s|.+v((\d+\.?){3})|\1|") ver_config_sample=$(grep " Version: " $file_config_sample | perl -pe "s|.+v((\d+\.?){3})|\1|")
[ -f $file_config_user ] && ver_config_user=$(grep " Version: " $file_config_user | perl -pe "s|.+v((\d+\.?){3})|\1|") [[ -f $file_config_user ]] && ver_config_user=$(grep " Version: " $file_config_user | perl -pe "s|.+v((\d+\.?){3})|\1|")
## 删除旧的发送记录文件 ## 删除旧的发送记录文件
[ -f $send_mark ] && [[ $(cat $send_mark) != $ver_config_sample ]] && rm -f $send_mark [[ -f $send_mark ]] && [[ $(cat $send_mark) != $ver_config_sample ]] && rm -f $send_mark
## 识别出更新日期和更新内容 ## 识别出更新日期和更新内容
update_date=$(grep " Date: " $file_config_sample | awk -F ": " '{print $2}') update_date=$(grep " Date: " $file_config_sample | awk -F ": " '{print $2}')
update_content=$(grep " Update Content: " $file_config_sample | awk -F ": " '{print $2}') update_content=$(grep " Update Content: " $file_config_sample | awk -F ": " '{print $2}')
## 如果是今天,并且版本号不一致,则发送通知 ## 如果是今天,并且版本号不一致,则发送通知
if [ -f $file_config_user ] && [[ $ver_config_user != $ver_config_sample ]] && [[ $update_date == $(date "+%Y-%m-%d") ]]; then if [[ -f $file_config_user ]] && [[ $ver_config_user != $ver_config_sample ]] && [[ $update_date == $(date "+%Y-%m-%d") ]]; then
if [ ! -f $send_mark ]; then if [[ ! -f $send_mark ]]; then
local notify_title="配置文件更新通知" local notify_title="配置文件更新通知"
local notify_content="更新日期: $update_date\n用户版本: $ver_config_user\n新的版本: $ver_config_sample\n更新内容: $update_content\n更新说明: 如需使用新功能请对照config.sample.sh,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。\n" local notify_content="更新日期: $update_date\n用户版本: $ver_config_user\n新的版本: $ver_config_sample\n更新内容: $update_content\n更新说明: 如需使用新功能请对照config.sample.sh,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。\n"
echo -e $notify_content echo -e $notify_content
@@ -65,7 +47,7 @@ detect_config_version() {
[[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark [[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark
fi fi
else else
[ -f $send_mark ] && rm -f $send_mark [[ -f $send_mark ]] && rm -f $send_mark
fi fi
} }
@@ -73,7 +55,7 @@ detect_config_version() {
output_list_add_drop() { output_list_add_drop() {
local list=$1 local list=$1
local type=$2 local type=$2
if [ -s $list ]; then if [[ -s $list ]]; then
echo -e "检测到有$type的定时任务\n" echo -e "检测到有$type的定时任务\n"
cat $list cat $list
echo echo
@@ -89,7 +71,7 @@ del_cron() {
local ids="" local ids=""
echo -e "开始尝试自动删除失效的定时任务...\n" echo -e "开始尝试自动删除失效的定时任务...\n"
for cron in $(cat $list_drop); do 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|" | xargs | sed 's/ /","/g' | head -1) 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 if [[ $ids ]]; then
ids="$ids,\"$id\"" ids="$ids,\"$id\""
else else
@@ -122,8 +104,9 @@ add_cron() {
local detail="" local detail=""
cd $dir_scripts cd $dir_scripts
for file in $(cat $list_add); do for file in $(cat $list_add); do
local file_name=${file/${path}\_/} local file_name=${file/${path}\//}
if [ -f $file ]; then file_name=${file_name/${path}\_/}
if [[ -f $file ]]; then
cron_line=$( cron_line=$(
perl -ne "{ perl -ne "{
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/ print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/
@@ -136,14 +119,15 @@ add_cron() {
) )
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | 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_name ]] && cron_name="$file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="0 6 * * *" [[ -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") result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name")
echo -e "$result" echo -e "$result"
if [[ $detail ]]; then if [[ $detail ]]; then
detail="${detail}\n${result}" detail="${detail}${result}\n"
else else
detail="${result}" detail="${result}\n"
fi fi
fi fi
done done
@@ -152,42 +136,60 @@ add_cron() {
## 更新仓库 ## 更新仓库
update_repo() { update_repo() {
echo -e "--------------------------------------------------------------\n"
local url="$1" local url="$1"
local path="$2" local path="$2"
local blackword="$3" local blackword="$3"
local dependence="$4" local dependence="$4"
local branch="$5" local branch="$5"
local urlTmp="${url%*/}"
local repoTmp="${urlTmp##*/}"
local repo="${repoTmp%.*}"
local tmp="${url%/*}" local tmp="${url%/*}"
local authorTmp1="${tmp##*/}" local authorTmp1="${tmp##*/}"
local authorTmp2="${authorTmp1##*:}" local authorTmp2="${authorTmp1##*:}"
local author="${authorTmp2##*.}" local author="${authorTmp2##*.}"
local repo_path="${dir_repo}/${author}_${repo}" local repo_path="${dir_repo}/${uniq_path}"
[[ $branch ]] && repo_path="${repo_path}_${branch}"
if [ -d ${repo_path}/.git ]; then make_dir "${dir_scripts}/${uniq_path}"
reset_romote_url ${repo_path} "${github_proxy_url}${url/https:\/\/ghproxy.com\//}" "${branch}"
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}" git_pull_scripts ${repo_path} "${branch}"
else else
git_clone_scripts ${url} ${repo_path} "${branch}" git_clone_scripts "${formatUrl}" ${repo_path} "${branch}"
fi fi
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新${repo_path}成功...\n" echo -e "\n更新${repo_path}成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence"
else else
echo -e "\n更新${repo_path}失败,请检查原因...\n" 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 fi
} }
## 更新所有 raw 文件 ## 更新所有 raw 文件
update_raw() { update_raw() {
echo -e "--------------------------------------------------------------\n" echo -e "--------------------------------------------------------------\n"
local raw_url="$1" local url="$1"
raw_file_name=$(echo ${raw_url} | awk -F "/" '{print $NF}') 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" 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} wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
if [[ $? -eq 0 ]]; then if [[ $? -eq 0 ]]; then
@@ -195,7 +197,7 @@ update_raw() {
echo -e "下载 ${raw_file_name} 成功...\n" echo -e "下载 ${raw_file_name} 成功...\n"
cd $dir_raw cd $dir_raw
local filename="raw_${raw_file_name}" local filename="raw_${raw_file_name}"
local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task $filename" | perl -pe "s|.*ID=(.*) $cmd_task $filename\.*|\1|" | head -1) 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} cp -f $raw_file_name $dir_scripts/${filename}
cron_line=$( cron_line=$(
perl -ne "{ perl -ne "{
@@ -209,17 +211,18 @@ update_raw() {
) )
cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | 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_name ]] && cron_name="$raw_file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="0 6 * * *" [[ -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 if [[ -z $cron_id ]]; then
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name") result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name")
echo -e "$result" echo -e "$result\n"
notify "新增任务通知" "\n$result" notify "新增任务通知" "\n$result"
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id" # update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
fi fi
else else
echo -e "下载 ${raw_file_name} 失败,保留之前正常下载的版本...\n" echo -e "下载 ${raw_file_name} 失败,保留之前正常下载的版本...\n"
[ -f "$dir_raw/${raw_file_name}.new" ] && rm -f "$dir_raw/${raw_file_name}.new" [[ -f "$dir_raw/${raw_file_name}.new" ]] && rm -f "$dir_raw/${raw_file_name}.new"
fi fi
} }
@@ -227,7 +230,7 @@ update_raw() {
## 调用用户自定义的extra.sh ## 调用用户自定义的extra.sh
run_extra_shell() { run_extra_shell() {
if [[ ${EnableExtraShell} == true ]]; then if [[ ${EnableExtraShell} == true ]]; then
if [ -f $file_extra_shell ]; then if [[ -f $file_extra_shell ]]; then
echo -e "--------------------------------------------------------------\n" echo -e "--------------------------------------------------------------\n"
. $file_extra_shell . $file_extra_shell
else else
@@ -240,22 +243,24 @@ run_extra_shell() {
usage() { usage() {
echo -e "本脚本用法:" echo -e "本脚本用法:"
echo -e "1. $cmd_update update # 更新并重启青龙" echo -e "1. $cmd_update update # 更新并重启青龙"
echo -e "1. $cmd_update extra # 运行自定义脚本" echo -e "2. $cmd_update extra # 运行自定义脚本"
echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件" echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> # 更新单个仓库的脚本" echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> # 更新单个仓库的脚本"
echo -e "5. $cmd_update rmlog <days> # 删除旧日志" echo -e "5. $cmd_update rmlog <days> # 删除旧日志"
echo -e "6. $cmd_update code # 获取互助码"
echo -e "6. $cmd_update bot # 启动tg-bot" echo -e "6. $cmd_update bot # 启动tg-bot"
echo -e "7. $cmd_update reset # 重置青龙基础环境" echo -e "7. $cmd_update check # 检测青龙环境并修复"
echo -e "8. $cmd_update resetlet # 重置登录错误次数"
echo -e "9. $cmd_update resettfa # 禁用两步登录"
} }
## 更新qinglong ## 更新qinglong
update_qinglong() { update_qinglong() {
patch_version
local no_restart="$1" local no_restart="$1"
echo -e "--------------------------------------------------------------\n" [[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
[ -f $dir_root/package.json ] && ql_depend_old=$(cat $dir_root/package.json) reset_romote_url ${dir_root} "${github_proxy_url}https://github.com/whyour/qinglong.git" "master"
reset_romote_url ${dir_root} "${github_proxy_url}https://github.com/whyour/qinglong.git" git_pull_scripts $dir_root "master"
git_pull_scripts $dir_root
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新$dir_root成功...\n" echo -e "\n更新$dir_root成功...\n"
@@ -263,35 +268,30 @@ update_qinglong() {
detect_config_version detect_config_version
update_depend update_depend
[ -f $dir_root/package.json ] && ql_depend_new=$(cat $dir_root/package.json) [[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root [[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
else else
echo -e "\n更新$dir_root失败,请检查原因...\n" echo -e "\n更新$dir_root失败,请检查原因...\n"
fi fi
local url="${github_proxy_url}https://github.com/whyour/qinglong-static.git" local url="${github_proxy_url}https://github.com/whyour/qinglong-static.git"
if [ -d ${ql_static_repo}/.git ]; then if [[ -d ${ql_static_repo}/.git ]]; then
reset_romote_url ${ql_static_repo} ${url} reset_romote_url ${ql_static_repo} ${url} "master"
cd ${ql_static_repo} git_pull_scripts ${ql_static_repo} "master"
git fetch --all
exit_status=$?
git reset --hard origin/master
cd $dir_root
else else
git_clone_scripts ${url} ${ql_static_repo} git_clone_scripts ${url} ${ql_static_repo}
fi fi
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新$ql_static_repo成功...\n" echo -e "\n更新$ql_static_repo成功...\n"
cd $ql_static_repo local static_version=$(cat /ql/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1)
commit_id=$(git rev-parse --short HEAD) echo -e "\n当前版本 $static_version...\n"
echo -e "\n当前静态资源版本 $commit_id...\n"
cd $dir_root cd $dir_root
rm -rf $dir_root/build && rm -rf $dir_root/dist rm -rf $dir_root/build && rm -rf $dir_root/dist
cp -rf $ql_static_repo/* $dir_root cp -rf $ql_static_repo/* $dir_root
if [[ $no_restart != "no-restart" ]]; then if [[ $no_restart != "no-restart" ]]; then
echo -e "重启面板中..."
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
sleep 1 echo -e "重启面板中..."
sleep 3
reload_pm2 reload_pm2
fi fi
else else
@@ -300,20 +300,29 @@ update_qinglong() {
} }
reload_pm2() { patch_version() {
pm2 l >/dev/null 2>&1 if [[ -f "/ql/db/cookie.db" ]]; then
echo -e "检测到旧的db文件,拷贝为新db...\n"
if [[ $(pm2 info panel 2>/dev/null) ]]; then mv /ql/db/cookie.db /ql/db/env.db
pm2 reload panel >/dev/null 2>&1 rm -rf /ql/db/cookie.db
else echo
pm2 start $dir_root/build/app.js -n panel >/dev/null 2>&1
fi fi
if [[ $(pm2 info schedule 2>/dev/null) ]]; then if ! type ts-node &>/dev/null; then
pm2 reload schedule >/dev/null 2>&1 pnpm i -g ts-node typescript tslib
else
pm2 start $dir_root/build/schedule.js -n schedule >/dev/null 2>&1
fi 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
} }
## 对比脚本 ## 对比脚本
@@ -327,21 +336,20 @@ diff_scripts() {
gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence" gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence"
local repo="${repo_path##*/}" local list_add="$dir_list_tmp/${uniq_path}_add.list"
local list_add="$dir_list_tmp/${repo}_add.list" local list_drop="$dir_list_tmp/${uniq_path}_drop.list"
local list_drop="$dir_list_tmp/${repo}_drop.list" diff_cron "$dir_list_tmp/${uniq_path}_scripts.list" "$dir_list_tmp/${uniq_path}_user.list" $list_add $list_drop
diff_cron "$dir_list_tmp/${repo}_scripts.list" "$dir_list_tmp/${repo}_user.list" $list_add $list_drop
if [ -s $list_drop ]; then if [[ -s $list_drop ]]; then
output_list_add_drop $list_drop "失效" output_list_add_drop $list_drop "失效"
if [[ ${AutoDelCron} == true ]]; then if [[ ${AutoDelCron} == true ]]; then
del_cron $list_drop $repo del_cron $list_drop $uniq_path
fi fi
fi fi
if [ -s $list_add ]; then if [[ -s $list_add ]]; then
output_list_add_drop $list_add "新" output_list_add_drop $list_add "新"
if [[ ${AutoAddCron} == true ]]; then if [[ ${AutoAddCron} == true ]]; then
add_cron $list_add $repo add_cron $list_add $uniq_path
fi fi
fi fi
cd $dir_current cd $dir_current
@@ -356,8 +364,7 @@ gen_list_repo() {
local blackword="$4" local blackword="$4"
local dependence="$5" local dependence="$5"
local repo="${repo_path##*/}" rm -f $dir_list_tmp/${uniq_path}*.list &>/dev/null
rm -f $dir_list_tmp/${repo}*.list >/dev/null 2>&1
cd ${repo_path} cd ${repo_path}
@@ -378,44 +385,96 @@ gen_list_repo() {
if [[ $blackword ]]; then if [[ $blackword ]]; then
files=$(echo "$files" | egrep -v $blackword) files=$(echo "$files" | egrep -v $blackword)
fi fi
cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
cp -f $file_notify_py "${dir_scripts}/${uniq_path}"
if [[ $dependence ]]; then if [[ $dependence ]]; then
eval $cmd | sed 's/^..//' | egrep $dependence | xargs -i cp {} $dir_scripts 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 fi
if [[ -d $dir_dep ]]; then
cp -rf $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null
fi
for file in ${files}; do for file in ${files}; do
filename=$(basename $file) filename=$(basename $file)
cp -f $file $dir_scripts/${repo}_${filename} cp -f $file "$dir_scripts/${uniq_path}/${filename}"
echo ${repo}_${filename} >>"$dir_list_tmp/${repo}_scripts.list" echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
cron_id=$(cat $list_crontab_user | grep -E "$cmd_task ${author}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task ${author}_${filename}\.*|\1|" | head -1) cron_id=$(cat $list_crontab_user | grep -E "$cmd_task ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
if [[ $cron_id ]]; then if [[ $cron_id ]]; then
result=$(update_cron_command_api "$cmd_task ${repo}_${filename}:$cron_id") result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
fi fi
done done
grep -E "$cmd_task $repo" $list_crontab_user | perl -pe "s|.*ID=(.*) $cmd_task ($repo_.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${repo}_user.list" grep -E "${cmd_task} ${uniq_path}" ${list_crontab_user} | perl -pe "s|.*ID=(.*) ${cmd_task} (${uniq_path}.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${uniq_path}_user.list"
cd $dir_current cd $dir_current
} }
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() { main() {
## for ql update
show_log="false"
while getopts ":l" opt
do
case $opt in
l)
show_log="true"
;;
esac
done
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
local p1=$1 local p1=$1
local p2=$2 local p2=$2
local p3=$3 local p3=$3
local p4=$4 local p4=$4
local p5=$5 local p5=$5
local p6=$6 local p6=$6
log_time=$(date "+%Y-%m-%d-%H-%M-%S") local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
log_path="$dir_log/update/${log_time}_$p1.log" local log_path="$dir_log/update/${log_time}_$p1.log"
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
case $p1 in case $p1 in
update) update)
update_qinglong "$2" | tee $log_path 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) extra)
run_extra_shell | tee -a $log_path 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) repo)
get_user_info get_user_info
local name=$(echo "${p2##*/}" | awk -F "." '{print $1}') get_uniq_path "$p2" "$p6"
log_path="$dir_log/update/${log_time}_$name.log" 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 if [[ -n $p2 ]]; then
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" | tee $log_path update_repo "$p2" "$p3" "$p4" "$p5" "$p6" >>$log_path
else else
echo -e "命令输入错误...\n" echo -e "命令输入错误...\n"
usage usage
@@ -423,32 +482,53 @@ main() {
;; ;;
raw) raw)
get_user_info get_user_info
local name=$(echo "${p2##*/}" | awk -F "." '{print $1}') get_uniq_path "$p2"
log_path="$dir_log/update/${log_time}_$name.log" 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 if [[ -n $p2 ]]; then
update_raw "$p2" | tee $log_path update_raw "$p2" >>$log_path
else else
echo -e "命令输入错误...\n" echo -e "命令输入错误...\n"
usage usage
fi fi
;; ;;
rmlog) rmlog)
. $dir_shell/rmlog.sh "$p2" | tee $log_path echo -e "## 开始执行... $begin_time\n" >>$log_path
;; [[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
code) . $dir_shell/rmlog.sh "$p2" >>$log_path
. $dir_shell/code.sh
;; ;;
bot) bot)
. $dir_shell/bot.sh 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
;; ;;
reset) check)
. $dir_shell/reset.sh 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" echo -e "命令输入错误...\n"
usage usage
;; ;;
esac 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 "$@" main "$@"
-43
View File
@@ -1,43 +0,0 @@
import { history } from 'umi';
import { request } from '@/utils/http';
import config from '@/utils/config';
const titleMap: any = {
'/': '控制面板',
'/login': '登录',
'/crontab': '定时任务',
'/cookie': 'Session管理',
'/config': '配置文件',
'/diy': '自定义脚本',
'/diff': '对比工具',
'/log': '日志',
'/setting': '系统设置',
};
export function render(oldRender: any) {
request
.get(`${config.apiPrefix}user`)
.then((data) => {
if (data.data && data.data.username) {
return oldRender();
}
localStorage.removeItem(config.authKey);
history.push('/login');
oldRender();
})
.catch((e) => {
console.log(e);
if (e.response && e.response.status === 401) {
localStorage.removeItem(config.authKey);
history.push('/login');
oldRender();
}
});
}
export function onRouteChange({ matchedRoutes }: any) {
if (matchedRoutes.length) {
const path: string = matchedRoutes[matchedRoutes.length - 1].route.path;
document.title = `${titleMap[path]} - 控制面板`;
}
}
+7
View File
@@ -0,0 +1,7 @@
import { PageLoading } from '@ant-design/pro-layout';
const NewPageLoading = () => {
return <PageLoading delay={1}></PageLoading>;
};
export default NewPageLoading;
+22 -11
View File
@@ -7,6 +7,7 @@ import {
FolderOutlined, FolderOutlined,
RadiusSettingOutlined, RadiusSettingOutlined,
ControlOutlined, ControlOutlined,
ContainerOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
export default { export default {
@@ -18,6 +19,12 @@ export default {
hideInMenu: true, hideInMenu: true,
component: '@/pages/login/index', component: '@/pages/login/index',
}, },
{
name: '初始化',
path: '/initialization',
hideInMenu: true,
component: '@/pages/initialization/index',
},
{ {
path: '/crontab', path: '/crontab',
name: '定时任务', name: '定时任务',
@@ -25,10 +32,10 @@ export default {
component: '@/pages/crontab/index', component: '@/pages/crontab/index',
}, },
{ {
path: '/cookie', path: '/env',
name: 'Session管理', 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: 'https://z3.ax1x.com/2021/11/18/I7MpAe.png',
siderWidth: 180,
} as any; } as any;
+250 -36
View File
@@ -1,4 +1,6 @@
@import '~antd/lib/style/themes/default.less';
@import '~@/styles/variable.less'; @import '~@/styles/variable.less';
@import '~codemirror/lib/codemirror.css';
@font-face { @font-face {
font-family: 'Source Code Pro'; font-family: 'Source Code Pro';
@@ -7,20 +9,34 @@
url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype'); url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype');
} }
body { .log-modal {
height: 100%; .ant-modal {
overflow-y: hidden; padding-bottom: 0 !important;
background-color: rgb(248, 248, 248); width: 580px !important;
}
pre {
white-space: break-spaces;
line-height: 17px;
margin-bottom: 0;
overflow: hidden;
}
} }
@import '~codemirror/lib/codemirror.css'; .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;
}
}
.ql-container-wrapper { .rename-box {
.CodeMirror { height: 0;
position: absolute; .rename-input {
height: calc(100vh - 128px); height: 0;
height: calc(100vh - var(--vh-offset, 0px) - 128px); padding: 0 !important;
width: calc(100% - 32px); display: none !important;
} }
} }
@@ -29,14 +45,24 @@ body {
overflow: auto; overflow: auto;
.ant-pro-page-container-children-content { .ant-pro-page-container-children-content {
overflow: auto; overflow: auto;
height: calc(100vh - 96px); height: 100%;
height: calc(100vh - var(--vh-offset, 0px) - 96px);
background-color: #fff; background-color: #fff;
padding: 16px; padding: 16px;
} }
} }
.session-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;
} }
@@ -44,38 +70,226 @@ body {
.log-wrapper { .log-wrapper {
.log-select { .log-select {
width: 300px; width: 250px;
} }
.ant-page-header-heading-left { .ant-page-header-heading-left {
min-width: 100px; min-width: 100px;
} }
} }
@media (max-width: 768px) { .config-wrapper {
.ant-pro-grid-content.wide { .config-select {
.ant-pro-page-container-children-content { width: 250px;
height: calc(100vh - 144px);
height: calc(100vh - var(--vh-offset, 0px) - 144px);
}
} }
.ql-container-wrapper { .ant-page-header-heading-left {
&.crontab-wrapper, min-width: 100px;
&.log-wrapper { }
.ant-pro-grid-content.wide .ant-pro-page-container-children-content { }
height: calc(100vh - 184px);
height: calc(100vh - var(--vh-offset, 0px) - 184px); .ant-tree {
margin-left: 0; .ant-tree-treenode {
margin-right: 0; 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 { .CodeMirror {
height: calc(100vh - 216px); height: 100%;
height: calc(100vh - var(--vh-offset, 0px) - 216px);
width: calc(100vw - 32px);
} }
} }
.CodeMirror { }
height: calc(100vh - 176px); }
height: calc(100vh - var(--vh-offset, 0px) - 176px);
@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;
caret-color: #e8e6f3 !important;
}
::placeholder {
opacity: 0.5 !important;
}
.ant-select-selection-placeholder {
opacity: 0.5 !important;
}
}
.ant-pro-page-container-children-content {
margin: 18px 18px 18px;
}
.ant-pro-basicLayout-content {
margin: 18px;
.ant-pro-page-container {
margin: -18px -18px -18px;
}
}
.ant-menu-item.ant-pro-sider-collapsed-button {
margin: 0;
}
.side-menu-container {
display: flex;
justify-content: space-between;
align-items: center;
color: rgba(0, 0, 0, 0.85);
.side-menu-collapse-button:hover {
color: #1890ff;
}
.side-menu-user-wrapper {
display: flex;
align-items: center;
}
}
.side-menu-user-drop-menu {
position: relative;
margin: 0;
padding: 4px 0;
text-align: left;
background-clip: padding-box;
outline: none;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 6px 16px -8px rgb(0 0 0 / 8%), 0 9px 28px 0 rgb(0 0 0 / 5%),
0 12px 48px 16px rgb(0 0 0 / 3%);
.ant-dropdown-menu-item:hover {
color: #1890ff;
} }
} }
+251 -43
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,15 +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 vhCheck from 'vh-check';
import { version, changeLog } from '../version'; import { version, changeLogLink, changeLog } from '../version';
import { useCtx, useTheme } from '@/utils/hooks';
import { message, Badge, Modal, Avatar, Dropdown, Menu, 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);
@@ -24,64 +50,209 @@ 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(); vhCheck();
}, []); }, []);
useEffect(() => { useEffect(() => {
if (props.location.pathname === '/') { if (systemInfo && systemInfo.isInitialized && !user) {
history.push('/crontab'); getUser();
} }
}, [props.location.pathname]); }, [props.location.pathname]);
useEffect(() => { useEffect(() => {
const theme = localStorage.getItem('qinglong_dark_theme') || 'auto'; if (!systemInfo) {
getSystemInfo();
}
}, [systemInfo]);
useEffect(() => {
setTheme();
}, [theme.theme]);
useEffect(() => {
const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
setFetchMethod(window.fetch); setFetchMethod(window.fetch);
if (theme === 'dark') { if (_theme === 'dark') {
enableDarkMode({ enableDarkMode({});
brightness: 100, } else if (_theme === 'light') {
contrast: 90,
sepia: 10,
});
} else if (theme === 'light') {
disableDarkMode(); disableDarkMode();
} else { } else {
followSystemColorScheme({ followSystemColorScheme({});
brightness: 100,
contrast: 90,
sepia: 10,
});
} }
}, []); }, []);
if (props.location.pathname === '/login') { useEffect(() => {
return props.children; if (!user) return;
ws.current = new SockJS(
`${location.origin}/api/ws?token=${localStorage.getItem(config.authKey)}`,
);
ws.current.onmessage = (e: any) => {
try {
const data = JSON.parse(e.data);
if (data.type === 'ping') {
if (data && data.message === 'hanhh') {
console.log('websocket连接成功', e);
} else {
console.log('websocket连接失败', e);
}
}
setSocketMessage(data);
} catch (error) {
console.log('websocket连接失败', e);
}
};
const wsCurrent = ws.current;
return () => {
wsCurrent.close();
};
}, [user]);
useEffect(() => {
window.onload = () => {
const timing = performance.timing;
console.log(`白屏时间: ${timing.responseStart - timing.navigationStart}`);
console.log(
`请求完毕至DOM加载: ${timing.domInteractive - timing.responseEnd}`,
);
console.log(
`解释dom树耗时: ${timing.domComplete - timing.domInteractive}`,
);
console.log(
`从开始至load总耗时: ${timing.loadEventEnd - timing.navigationStart}`,
);
Sentry.captureMessage(
`白屏时间 ${timing.responseStart - timing.navigationStart}`,
);
};
}, []);
if (['/login', '/initialization'].includes(props.location.pathname)) {
document.title = `${
(config.documentTitleMap as any)[props.location.pathname]
} - 控制面板`;
if (
systemInfo?.isInitialized &&
props.location.pathname === '/initialization'
) {
history.push('/crontab');
}
if (systemInfo) {
return React.Children.map(props.children, (child) => {
return React.cloneElement(child, {
...ctx,
...theme,
user,
reloadUser,
reloadTheme: setTheme,
ws: ws.current,
});
});
}
} }
const isFirefox = navigator.userAgent.includes('Firefox'); const isFirefox = navigator.userAgent.includes('Firefox');
const isSafari = const isSafari =
navigator.userAgent.includes('Safari') && navigator.userAgent.includes('Safari') &&
!navigator.userAgent.includes('Chrome'); !navigator.userAgent.includes('Chrome');
return ( 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]}
loading={loading}
ErrorBoundary={Sentry.ErrorBoundary}
title={ title={
<> <>
<span style={{ fontSize: 16 }}></span>
<a href={changeLog} target="_blank" rel="noopener noreferrer"> <a
href={changeLogLink}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => {
e.stopPropagation();
}}
>
<span <span
style={{ style={{
fontSize: isFirefox ? 9 : 12, fontSize: isFirefox ? 9 : 12,
color: '#666', color: '#666',
marginLeft: 5, marginLeft: 2,
zoom: isSafari ? 0.66 : 0.8, zoom: isSafari ? 0.66 : 0.8,
letterSpacing: isQQBrowser ? -2 : 0,
}} }}
> >
{version} v{version}
</span> </span>
</a> </a>
</> </>
@@ -96,21 +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(),
},
];
}} }}
pageTitleRender={() => '控制面板'} 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>
); );
} }
+91 -49
View File
@@ -1,85 +1,127 @@
import React, { PureComponent, Fragment, useState, useEffect } from 'react'; import React, {
import { Button, message, 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, setWidth] = 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: any) => { .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: any) => { .then((data: any) => {
message.success(data.msg); message.success(data.message);
setConfirmLoading(false);
}); });
}; };
const onSelect = (value: any, node: any) => {
setSelect(value);
setTitle(node.value);
getConfig(node.value);
};
useEffect(() => { useEffect(() => {
if (document.body.clientWidth < 768) { getFiles();
setWidth('auto'); getConfig('config.sh');
setMarginLeft(0);
setMarginTop(0);
} else {
setWidth('100%');
setMarginLeft(0);
setMarginTop(-72);
}
getConfig();
}, []); }, []);
return ( return (
<PageContainer <PageContainer
className="ql-container-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,
},
}} }}
> >
<CodeMirror {isPhone ? (
value={value} <CodeMirror
options={{ value={value}
lineNumbers: true, options={{
styleActiveLine: true, lineNumbers: true,
matchBrackets: true, styleActiveLine: true,
mode: 'shell', matchBrackets: true,
}} mode: 'shell',
onBeforeChange={(editor, data, value) => { }}
setValue(value); onBeforeChange={(editor, data, 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>
); );
}; };
-99
View File
@@ -1,99 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
const CookieModal = ({
cookie,
handleCancel,
visible,
}: {
cookie?: any;
visible: boolean;
handleCancel: (cks?: any[]) => 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)) {
message.error(`${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) {
message.success(cookie ? '更新Cookie成功' : '添加Cookie成功');
} else {
message.error(data);
}
setLoading(false);
handleCancel(cookie ? [data] : 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;
+11
View File
@@ -0,0 +1,11 @@
.ant-table-pagination.ant-pagination {
margin-bottom: 0 !important;
}
tr {
td:nth-child(2) {
span {
cursor: pointer;
}
}
}
+327 -79
View File
@@ -25,12 +25,18 @@ import {
DeleteOutlined, DeleteOutlined,
PauseCircleOutlined, PauseCircleOutlined,
FieldTimeOutlined, FieldTimeOutlined,
PushpinOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import CronModal from './modal'; import CronModal from './modal';
import CronLogModal from './logModal'; import CronLogModal from './logModal';
import cron_parser from 'cron-parser';
import { diffTime } from '@/utils/date';
import { getTableScroll } from '@/utils/index';
import { history } from 'umi';
import './index.less';
const { Text } = Typography; const { Text } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -49,6 +55,8 @@ enum OperationName {
'禁用', '禁用',
'运行', '运行',
'停止', '停止',
'置顶',
'取消置顶',
} }
enum OperationPath { enum OperationPath {
@@ -56,24 +64,44 @@ enum OperationPath {
'disable', 'disable',
'run', 'run',
'stop', 'stop',
'pin',
'unpin',
} }
const Crontab = () => { const Crontab = ({ headerStyle, isPhone }: any) => {
const columns = [ const columns: any = [
{ {
title: '任务名', title: '任务名',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
width: 150,
align: 'center' as const, align: 'center' as const,
render: (text: string, record: any) => ( render: (text: string, record: any) => (
<span>{record.name || record._id}</span> <span
onClick={() => {
goToScriptManager(record);
}}
>
{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 (
@@ -89,18 +117,132 @@ const Crontab = () => {
</span> </span>
); );
}, },
sorter: {
compare: (a: any, b: any) => a.command.localeCompare(b.command),
multiple: 3,
},
}, },
{ {
title: '任务定时', title: '任务定时',
dataIndex: 'schedule', dataIndex: 'schedule',
key: 'schedule', key: 'schedule',
width: 110,
align: 'center' as const, align: 'center' as const,
sorter: {
compare: (a: any, b: any) => a.schedule.localeCompare(b.schedule),
multiple: 1,
},
}, },
{
title: '最后运行时间',
align: 'center' as const,
width: 150,
sorter: {
compare: (a: any, b: any) => {
return a.last_execution_time - b.last_execution_time;
},
},
render: (text: string, record: any) => {
const language = navigator.language || navigator.languages[0];
return (
<span
style={{
display: 'block',
}}
>
{record.last_execution_time
? new Date(record.last_execution_time * 1000)
.toLocaleString(language, {
hour12: false,
})
.replace(' 24:', ' 00:')
: '-'}
</span>
);
},
},
{
title: '最后运行时长',
align: 'center' as const,
width: 120,
sorter: {
compare: (a: any, b: any) => {
return a.last_running_time - b.last_running_time;
},
},
render: (text: string, record: any) => {
const language = navigator.language || navigator.languages[0];
return (
<span
style={{
display: 'block',
}}
>
{record.last_running_time
? diffTime(record.last_running_time)
: '-'}
</span>
);
},
},
{
title: '下次运行时间',
align: 'center' as const,
width: 150,
sorter: {
compare: (a: any, b: any) => {
return a.nextRunTime - b.nextRunTime;
},
},
render: (text: string, record: any) => {
const language = navigator.language || navigator.languages[0];
return (
<span
style={{
display: 'block',
}}
>
{record.nextRunTime
.toLocaleString(language, {
hour12: false,
})
.replace(' 24:', ' 00:')}
</span>
);
},
},
{ {
title: '状态', title: '状态',
key: 'status', key: 'status',
dataIndex: 'status', dataIndex: 'status',
align: 'center' as const, align: 'center' as const,
width: 85,
filters: [
{
text: '运行中',
value: 0,
},
{
text: '空闲中',
value: 1,
},
{
text: '已禁用',
value: 2,
},
{
text: '队列中',
value: 3,
},
],
onFilter: (value: number, record: any) => {
if (record.isDisabled && record.status !== 0) {
return value === 2;
} else {
return record.status === value;
}
},
render: (text: string, record: any) => ( render: (text: string, record: any) => (
<> <>
{(!record.isDisabled || record.status !== CrontabStatus.idle) && ( {(!record.isDisabled || record.status !== CrontabStatus.idle) && (
@@ -137,48 +279,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) => {
{record.status === CrontabStatus.idle && ( const isPc = !isPhone;
<Tooltip title="运行"> return (
<Space size="middle">
{record.status === CrontabStatus.idle && (
<Tooltip title={isPc ? '运行' : ''}>
<a
onClick={() => {
runCron(record, index);
}}
>
<PlayCircleOutlined />
</a>
</Tooltip>
)}
{record.status !== CrontabStatus.idle && (
<Tooltip title={isPc ? '停止' : ''}>
<a
onClick={() => {
stopCron(record, index);
}}
>
<PauseCircleOutlined />
</a>
</Tooltip>
)}
<Tooltip title={isPc ? '日志' : ''}>
<a <a
onClick={() => { onClick={() => {
runCron(record, index); setLogCron({ ...record, timestamp: Date.now() });
}} }}
> >
<PlayCircleOutlined /> <FileTextOutlined />
</a> </a>
</Tooltip> </Tooltip>
)} <MoreBtn key="more" record={record} index={index} />
{record.status !== CrontabStatus.idle && ( </Space>
<Tooltip title="停止"> );
<a },
onClick={() => {
stopCron(record, index);
}}
>
<PauseCircleOutlined />
</a>
</Tooltip>
)}
<Tooltip title="日志">
<a
onClick={() => {
setLogCron({ ...record, timestamp: Date.now() });
}}
>
<FileTextOutlined />
</a>
</Tooltip>
<MoreBtn key="more" record={record} index={index} />
</Space>
),
}, },
]; ];
const [width, setWidth] = 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);
@@ -189,6 +332,21 @@ const Crontab = () => {
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]); const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(20); const [pageSize, setPageSize] = useState(20);
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const goToScriptManager = (record: any) => {
const cmd = record.command.split(' ');
if (cmd[0] === 'task') {
let [p, s] = cmd[1].split('/');
if (!s) {
s = p;
p = '';
}
history.push(`/script?p=${p}&s=${s}`);
} else if (cmd[1] === 'repo') {
location.href = cmd[2];
}
};
const getCrons = () => { const getCrons = () => {
setLoading(true); setLoading(true);
@@ -196,12 +354,28 @@ const Crontab = () => {
.get(`${config.apiPrefix}crons?searchValue=${searchText}`) .get(`${config.apiPrefix}crons?searchValue=${searchText}`)
.then((data: any) => { .then((data: any) => {
setValue( setValue(
data.data.sort((a: any, b: any) => { data.data
const sortA = a.isDisabled ? 4 : a.status; .sort((a: any, b: any) => {
const sortB = b.isDisabled ? 4 : b.status; const sortA = a.isDisabled ? 4 : a.status;
return CrontabSort[sortA] - CrontabSort[sortB]; 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));
}; };
@@ -235,7 +409,8 @@ const Crontab = () => {
if (data.code === 200) { if (data.code === 200) {
message.success('删除成功'); message.success('删除成功');
const result = [...value]; const result = [...value];
result.splice(index + pageSize * (currentPage - 1), 1); const i = result.findIndex((x) => x._id === record._id);
result.splice(i, 1);
setValue(result); setValue(result);
} else { } else {
message.error(data); message.error(data);
@@ -266,7 +441,8 @@ const Crontab = () => {
.then((data: any) => { .then((data: any) => {
if (data.code === 200) { if (data.code === 200) {
const result = [...value]; const result = [...value];
result.splice(index + pageSize * (currentPage - 1), 1, { const i = result.findIndex((x) => x._id === record._id);
result.splice(i, 1, {
...record, ...record,
status: CrontabStatus.running, status: CrontabStatus.running,
}); });
@@ -300,7 +476,8 @@ const Crontab = () => {
.then((data: any) => { .then((data: any) => {
if (data.code === 200) { if (data.code === 200) {
const result = [...value]; const result = [...value];
result.splice(index + pageSize * (currentPage - 1), 1, { const i = result.findIndex((x) => x._id === record._id);
result.splice(i, 1, {
...record, ...record,
pid: null, pid: null,
status: CrontabStatus.idle, status: CrontabStatus.idle,
@@ -344,7 +521,8 @@ const Crontab = () => {
if (data.code === 200) { if (data.code === 200) {
const newStatus = record.isDisabled === 1 ? 0 : 1; const newStatus = record.isDisabled === 1 ? 0 : 1;
const result = [...value]; const result = [...value];
result.splice(index + pageSize * (currentPage - 1), 1, { const i = result.findIndex((x) => x._id === record._id);
result.splice(i, 1, {
...record, ...record,
isDisabled: newStatus, isDisabled: newStatus,
}); });
@@ -360,6 +538,50 @@ const Crontab = () => {
}); });
}; };
const pinOrUnPinCron = (record: any, index: number) => {
Modal.confirm({
title: `确认${record.isPinned === 1 ? '取消置顶' : '置顶'}`,
content: (
<>
{record.isPinned === 1 ? '取消置顶' : '置顶'}
{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
</>
),
onOk() {
request
.put(
`${config.apiPrefix}crons/${
record.isPinned === 1 ? 'unpin' : 'pin'
}`,
{
data: [record._id],
},
)
.then((data: any) => {
if (data.code === 200) {
const newStatus = record.isPinned === 1 ? 0 : 1;
const result = [...value];
const i = result.findIndex((x) => x._id === record._id);
result.splice(i, 1, {
...record,
isPinned: newStatus,
});
setValue(result);
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const MoreBtn: React.FC<{ const MoreBtn: React.FC<{
record: any; record: any;
index: number; index: number;
@@ -373,7 +595,7 @@ const Crontab = () => {
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
key="enableordisable" key="enableOrDisable"
icon={ icon={
record.isDisabled === 1 ? ( record.isDisabled === 1 ? (
<CheckCircleOutlined /> <CheckCircleOutlined />
@@ -384,11 +606,17 @@ const Crontab = () => {
> >
{record.isDisabled === 1 ? '启用' : '禁用'} {record.isDisabled === 1 ? '启用' : '禁用'}
</Menu.Item> </Menu.Item>
{record.isSystem !== 1 && ( <Menu.Item key="delete" icon={<DeleteOutlined />}>
<Menu.Item key="delete" icon={<DeleteOutlined />}>
</Menu.Item>
</Menu.Item> <Menu.Item
)} key="pinOrUnPin"
icon={
record.isPinned === 1 ? <StopOutlined /> : <PushpinOutlined />
}
>
{record.isPinned === 1 ? '取消置顶' : '置顶'}
</Menu.Item>
</Menu> </Menu>
} }
> >
@@ -403,12 +631,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;
} }
@@ -422,14 +653,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,
@@ -442,10 +677,12 @@ const Crontab = () => {
request request
.get(`${config.apiPrefix}crons/${cron._id}`) .get(`${config.apiPrefix}crons/${cron._id}`)
.then((data: any) => { .then((data: any) => {
console.log(value);
const index = value.findIndex((x) => x._id === cron._id); const index = value.findIndex((x) => x._id === cron._id);
console.log(index);
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,
@@ -457,6 +694,12 @@ const Crontab = () => {
const onSelectChange = (selectedIds: any[]) => { const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds); setSelectedRowIds(selectedIds);
setTimeout(() => {
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
setTableScrollHeight(getTableScroll());
}
});
}; };
const rowSelection = { const rowSelection = {
@@ -521,6 +764,10 @@ const Crontab = () => {
localStorage.setItem('pageSize', pageSize + ''); localStorage.setItem('pageSize', pageSize + '');
}; };
const getRowClassName = (record: any, index: number) => {
return record.isPinned ? 'pinned-cron' : '';
};
useEffect(() => { useEffect(() => {
if (logCron) { if (logCron) {
localStorage.setItem('logCron', logCron._id); localStorage.setItem('logCron', logCron._id);
@@ -533,16 +780,10 @@ const Crontab = () => {
}, [searchText]); }, [searchText]);
useEffect(() => { useEffect(() => {
if (document.body.clientWidth < 768) {
setWidth('auto');
setMarginLeft(0);
setMarginTop(0);
} else {
setWidth('100%');
setMarginLeft(0);
setMarginTop(-72);
}
setPageSize(parseInt(localStorage.getItem('pageSize') || '20')); setPageSize(parseInt(localStorage.getItem('pageSize') || '20'));
setTimeout(() => {
setTableScrollHeight(getTableScroll());
});
}, []); }, []);
return ( return (
@@ -558,20 +799,11 @@ 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 && ( {selectedRowIds.length > 0 && (
@@ -603,6 +835,20 @@ const Crontab = () => {
<Button type="primary" onClick={() => operateCrons(3)}> <Button type="primary" onClick={() => operateCrons(3)}>
</Button> </Button>
<Button
type="primary"
onClick={() => operateCrons(4)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => operateCrons(5)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<span style={{ marginLeft: 8 }}> <span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a> <a>{selectedRowIds?.length}</a>
@@ -612,21 +858,23 @@ const Crontab = () => {
<Table <Table
columns={columns} columns={columns}
pagination={{ pagination={{
hideOnSinglePage: true,
current: currentPage, current: currentPage,
onChange: onPageChange, onChange: onPageChange,
pageSize: pageSize, pageSize: pageSize,
showSizeChanger: true, showSizeChanger: true,
simple: isPhone,
defaultPageSize: 20, defaultPageSize: 20,
showTotal: (total: number, range: number[]) => showTotal: (total: number, range: number[]) =>
`${range[0]}-${range[1]} 条/总共 ${total}`, `${range[0]}-${range[1]} 条/总共 ${total}`,
pageSizeOptions: [10, 20, 50, 100, 200, 500, 1000],
}} }}
dataSource={value} dataSource={value}
rowKey="_id" rowKey="_id"
size="middle" size="middle"
scroll={{ x: 768 }} scroll={{ x: 1000, y: tableScrollHeight }}
loading={loading} loading={loading}
rowSelection={rowSelection} rowSelection={rowSelection}
rowClassName={getRowClassName}
/> />
<CronLogModal <CronLogModal
visible={isLogModalVisible} visible={isLogModalVisible}
+36 -14
View File
@@ -1,12 +1,12 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Modal, message, 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 { import {
Loading3QuartersOutlined, Loading3QuartersOutlined,
CheckCircleOutlined, CheckCircleOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Controlled as CodeMirror } from 'react-codemirror2'; import { PageLoading } from '@ant-design/pro-layout';
enum CrontabStatus { enum CrontabStatus {
'running', 'running',
@@ -14,6 +14,7 @@ enum CrontabStatus {
'disabled', 'disabled',
'queued', 'queued',
} }
const { Countdown } = Statistic;
const CronLogModal = ({ const CronLogModal = ({
cron, cron,
@@ -26,8 +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 [excuting, setExcuting] = useState<any>(true); const [executing, setExecuting] = useState<any>(true);
const [isPhone, setIsPhone] = useState(false); const [isPhone, setIsPhone] = useState(false);
const [theme, setTheme] = useState<string>('');
const getCronLog = (isFirst?: boolean) => { const getCronLog = (isFirst?: boolean) => {
if (isFirst) { if (isFirst) {
@@ -39,7 +41,7 @@ const CronLogModal = ({
if (localStorage.getItem('logCron') === cron._id) { if (localStorage.getItem('logCron') === cron._id) {
const log = data.data as string; const log = data.data as string;
setValue(log || '暂无日志'); setValue(log || '暂无日志');
setExcuting( setExecuting(
log && !log.includes('执行结束') && !log.includes('重启面板'), log && !log.includes('执行结束') && !log.includes('重启面板'),
); );
if (log && !log.includes('执行结束') && !log.includes('重启面板')) { if (log && !log.includes('执行结束') && !log.includes('重启面板')) {
@@ -52,10 +54,23 @@ const CronLogModal = ({
log.includes('重启面板') && log.includes('重启面板') &&
cron.status === CrontabStatus.running cron.status === CrontabStatus.running
) { ) {
message.warning({ content: '系统将在5秒后自动刷新', duration: 5 }); message.warning({
content: (
<span>
<Countdown
className="inline-countdown"
format="ss"
value={Date.now() + 1000 * 10}
/>
</span>
),
duration: 10,
});
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();
}, 5000); }, 10000);
} }
} }
}) })
@@ -74,8 +89,8 @@ const CronLogModal = ({
const titleElement = () => { const titleElement = () => {
return ( return (
<> <>
{excuting && <Loading3QuartersOutlined spin />} {(executing || loading) && <Loading3QuartersOutlined spin />}
{!excuting && <CheckCircleOutlined />} {!executing && <CheckCircleOutlined />}
<span style={{ marginLeft: 5 }}>-{cron && cron.name}</span>{' '} <span style={{ marginLeft: 5 }}>-{cron && cron.name}</span>{' '}
</> </>
); );
@@ -96,26 +111,33 @@ const CronLogModal = ({
title={titleElement()} title={titleElement()}
visible={visible} visible={visible}
centered centered
className="log-modal"
bodyStyle={{ bodyStyle={{
overflowY: 'auto', overflowY: 'auto',
maxHeight: 'calc(80vh - var(--vh-offset, 0px))', maxHeight: 'calc(80vh - var(--vh-offset, 0px))',
minHeight: '300px',
}} }}
forceRender forceRender
onOk={() => cancel()} onOk={() => cancel()}
onCancel={() => cancel()} onCancel={() => cancel()}
footer={[
<Button type="primary" onClick={() => cancel()}>
</Button>,
]}
> >
{!loading && value && ( {loading ? (
<PageLoading />
) : (
<pre <pre
style={ style={
!isPhone isPhone
? { whiteSpace: 'break-spaces', lineHeight: '17px' } ? {
: {
whiteSpace: 'break-spaces',
lineHeight: '17px',
fontFamily: 'Source Code Pro', fontFamily: 'Source Code Pro',
width: 375, width: 375,
zoom: 0.83, zoom: 0.83,
} }
: {}
} }
> >
{value} {value}
+18 -10
View File
@@ -36,12 +36,8 @@ const CronModal = ({
}; };
useEffect(() => { useEffect(() => {
if (cron) { form.resetFields();
form.setFieldsValue(cron); }, [cron, visible]);
} else {
form.resetFields();
}
}, [cron]);
return ( return (
<Modal <Modal
@@ -60,14 +56,26 @@ 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 placeholder="请输入任务名称" /> <Input placeholder="请输入任务名称" />
</Form.Item> </Form.Item>
<Form.Item name="command" label="命令" rules={[{ required: true }]}> <Form.Item
<Input placeholder="请输入要执行的命令" /> name="command"
label="命令"
rules={[{ required: true, whitespace: true }]}
>
<Input.TextArea
rows={4}
autoSize={true}
placeholder="请输入要执行的命令"
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="schedule" name="schedule"
+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;
+6
View File
@@ -11,3 +11,9 @@
.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;
}
}
+128 -67
View File
@@ -1,100 +1,161 @@
import React, { PureComponent, Fragment, useState, useEffect } from 'react'; import React, { PureComponent, useRef, useState, useEffect } from 'react';
import { Button, message, Modal } from 'antd'; import { Button, message, Select, Form, Row, Col } from 'antd';
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import ReactDiffViewer from 'react-diff-viewer';
import './index.less'; import './index.less';
import { DiffEditor } from '@monaco-editor/react';
import ReactDiffViewer from 'react-diff-viewer';
const Crontab = () => { const { Option } = Select;
const [width, setWidth] = useState('100%');
const [marginLeft, setMarginLeft] = useState(0); const Diff = ({ headerStyle, isPhone, theme }: any) => {
const [marginTop, setMarginTop] = useState(-72); const [origin, setOrigin] = useState('config.sample.sh');
const [value, setValue] = useState(''); const [current, setCurrent] = useState('config.sh');
const [sample, setSample] = useState(''); const [originValue, setOriginValue] = useState('');
const [currentValue, setCurrentValue] = useState('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [files, setFiles] = useState<any[]>([]);
const editorRef = useRef<any>(null);
const getConfig = () => { const getConfig = () => {
request.get(`${config.apiPrefix}config/config`).then((data) => { request.get(`${config.apiPrefix}configs/${current}`).then((data) => {
setValue(data.data); setCurrentValue(data.data);
}); });
}; };
const getSample = () => { const getSample = () => {
request.get(`${config.apiPrefix}configs/${origin}`).then((data) => {
setOriginValue(data.data);
});
};
const updateConfig = () => {
const content = editorRef.current
? editorRef.current.getModel().modified.getValue().replace(/\r\n/g, '\n')
: currentValue;
request
.post(`${config.apiPrefix}configs/save`, {
data: { content, name: current },
})
.then((data: any) => {
message.success(data.message);
});
};
const getFiles = () => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}config/sample`) .get(`${config.apiPrefix}configs/files`)
.then((data) => { .then((data: any) => {
setSample(data.data); setFiles(data.data);
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
const originFileChange = (value: string) => {
setOrigin(value);
};
const currentFileChange = (value: string) => {
setCurrent(value);
};
useEffect(() => { useEffect(() => {
if (document.body.clientWidth < 768) { getFiles();
setWidth('auto');
setMarginLeft(0);
setMarginTop(0);
} else {
setWidth('100%');
setMarginLeft(0);
setMarginTop(-72);
}
getConfig();
getSample();
}, []); }, []);
useEffect(() => {
getSample();
}, [origin]);
useEffect(() => {
getConfig();
}, [current]);
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper" className="ql-container-wrapper"
title="对比工具" title="对比工具"
loading={loading} loading={loading}
header={{ header={{
style: { style: headerStyle,
padding: '4px 16px 4px 15px',
position: 'sticky',
top: 0,
left: 0,
zIndex: 20,
marginTop,
width,
marginLeft,
},
}} }}
extra={
!isPhone && [
<Button key="1" type="primary" onClick={updateConfig}>
</Button>,
]
}
> >
<ReactDiffViewer <Row gutter={24} className="diff-switch-file">
styles={{ <Col span={12}>
diffContainer: { <Form.Item label="源文件">
overflowX: 'auto', <Select value={origin} onChange={originFileChange}>
minWidth: 768, {files.map((x) => (
}, <Option value={x.value}>{x.title}</Option>
diffRemoved: { ))}
overflowX: 'auto', </Select>
maxWidth: 300, </Form.Item>
}, </Col>
diffAdded: { <Col span={12}>
overflowX: 'auto', <Form.Item label="当前文件">
maxWidth: 300, <Select value={current} onChange={currentFileChange}>
}, {files.map((x) => (
line: { <Option value={x.value}>{x.title}</Option>
wordBreak: 'break-word', ))}
}, </Select>
}} </Form.Item>
oldValue={value} </Col>
newValue={sample} </Row>
splitView={true} {isPhone ? (
leftTitle="config.sh" <ReactDiffViewer
rightTitle="config.sample.sh" styles={{
disableWordDiff={true} diffContainer: {
/> overflowX: 'auto',
{/* <CodeDiff minWidth: 768,
style={{ height: 'calc(100vh - 72px)', overflowY: 'auto' }} },
outputFormat="side-by-side" diffRemoved: {
oldStr={value} overflowX: 'auto',
newStr={sample} maxWidth: 300,
/> */} },
diffAdded: {
overflowX: 'auto',
maxWidth: 300,
},
line: {
wordBreak: 'break-word',
},
}}
oldValue={originValue}
newValue={currentValue}
splitView={true}
leftTitle="config.sh"
rightTitle="config.sample.sh"
disableWordDiff={true}
/>
) : (
<DiffEditor
language={'shell'}
original={originValue}
modified={currentValue}
options={{
fontSize: 12,
lineNumbersMinChars: 3,
folding: false,
glyphMargin: false,
wordWrap: 'on',
}}
theme={theme}
onMount={(editor) => {
editorRef.current = editor;
}}
/>
)}
</PageContainer> </PageContainer>
); );
}; };
export default Crontab; export default Diff;
-88
View File
@@ -1,88 +0,0 @@
import React, { PureComponent, Fragment, useState, useEffect } from 'react';
import { Button, message, Modal } from 'antd';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
import { Controlled as CodeMirror } from 'react-codemirror2';
import { request } from '@/utils/http';
const Crontab = () => {
const [width, setWidth] = useState('100%');
const [marginLeft, setMarginLeft] = useState(0);
const [marginTop, setMarginTop] = useState(-72);
const [value, setValue] = useState('');
const [loading, setLoading] = useState(true);
const getConfig = () => {
setLoading(true);
request
.get(`${config.apiPrefix}config/extra`)
.then((data) => {
setValue(data.data);
})
.finally(() => setLoading(false));
};
const updateConfig = () => {
request
.post(`${config.apiPrefix}save`, {
data: { content: value, name: 'extra.sh' },
})
.then((data) => {
message.success(data.msg);
});
};
useEffect(() => {
if (document.body.clientWidth < 768) {
setWidth('auto');
setMarginLeft(0);
setMarginTop(0);
} else {
setWidth('100%');
setMarginLeft(0);
setMarginTop(-72);
}
getConfig();
}, []);
return (
<PageContainer
className="ql-container-wrapper"
title="extra.sh"
extra={[
<Button key="1" type="primary" onClick={updateConfig}>
</Button>,
]}
header={{
style: {
padding: '4px 16px 4px 15px',
position: 'sticky',
top: 0,
left: 0,
zIndex: 20,
marginTop,
width,
marginLeft,
},
}}
>
<CodeMirror
value={value}
options={{
lineNumbers: true,
lineWrapping: true,
styleActiveLine: true,
matchBrackets: true,
mode: 'shell',
}}
onBeforeChange={(editor, data, value) => {
setValue(value);
}}
onChange={(editor, data, value) => {}}
/>
</PageContainer>
);
};
export default Crontab;
+69
View File
@@ -0,0 +1,69 @@
import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
const EditNameModal = ({
ids,
handleCancel,
visible,
}: {
ids?: string[];
visible: boolean;
handleCancel: () => void;
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const handleOk = async (values: any) => {
setLoading(true);
const { code, data } = await request.put(`${config.apiPrefix}envs/name`, {
data: {
ids,
name: values.name,
},
});
if (code === 200) {
message.success('更新环境变量名称成功');
} else {
message.error(data);
}
setLoading(false);
handleCancel();
};
useEffect(() => {
form.resetFields();
}, [ids, visible]);
return (
<Modal
title="修改环境变量名称"
visible={visible}
forceRender
onOk={() => {
form
.validateFields()
.then((values) => {
handleOk(values);
})
.catch((info) => {
console.log('Validate Failed:', info);
});
}}
onCancel={() => handleCancel()}
confirmLoading={loading}
>
<Form form={form} layout="vertical" name="edit_name_modal">
<Form.Item
name="name"
rules={[{ required: true, message: '请输入新的环境变量名称' }]}
>
<Input placeholder="请输入新的环境变量名称" />
</Form.Item>
</Form>
</Modal>
);
};
export default EditNameModal;
+226 -190
View File
@@ -8,6 +8,7 @@ import {
Space, Space,
Typography, Typography,
Tooltip, Tooltip,
Input,
} from 'antd'; } from 'antd';
import { import {
EditOutlined, EditOutlined,
@@ -19,26 +20,23 @@ import {
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import QRCode from 'qrcode.react'; import EnvModal from './modal';
import CookieModal from './modal'; import EditNameModal from './editNameModal';
import { DndProvider, useDrag, useDrop } from 'react-dnd'; import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less'; import './index.less';
import { getTableScroll } from '@/utils/index';
const { Text } = Typography; const { Text } = Typography;
const { Search } = Input;
enum Status { enum Status {
'未获取', '已启用',
'正常',
'已禁用', '已禁用',
'已失效',
'状态异常',
} }
enum StatusColor { enum StatusColor {
'default',
'success', 'success',
'warning',
'error', 'error',
} }
@@ -62,36 +60,30 @@ const DragableBodyRow = ({
...restProps ...restProps
}: any) => { }: any) => {
const ref = useRef(); const ref = useRef();
const [{ isOver, dropClassName }, drop] = useDrop( const [{ isOver, dropClassName }, drop] = useDrop({
() => ({ accept: type,
accept: type, collect: (monitor) => {
collect: (monitor) => { const { index: dragIndex } = (monitor.getItem() as any) || {};
const { index: dragIndex } = monitor.getItem() || ({} as any); if (dragIndex === index) {
if (dragIndex === index) { return {};
return {}; }
} return {
return { isOver: monitor.isOver(),
isOver: monitor.isOver(), dropClassName:
dropClassName: dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
dragIndex < index ? ' drop-over-downward' : ' drop-over-upward', };
}; },
}, drop: (item: any) => {
drop: (item: any) => { moveRow(item.index, index);
moveRow(item.index, index); },
}, });
const [, drag] = useDrag({
type,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}), }),
[index], });
);
const [, drag, preview] = useDrag(
() => ({
type,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
}),
[index],
);
drop(drag(ref)); drop(drag(ref));
return ( return (
@@ -100,53 +92,78 @@ const DragableBodyRow = ({
className={`${className}${isOver ? dropClassName : ''}`} className={`${className}${isOver ? dropClassName : ''}`}
style={{ cursor: 'move', ...style }} style={{ cursor: 'move', ...style }}
{...restProps} {...restProps}
> />
{restProps.children}
</tr>
); );
}; };
const Config = () => { const Env = ({ headerStyle, isPhone, theme }: any) => {
const columns = [ const columns: any = [
{ {
title: '序号', title: '序号',
align: 'center' as const, align: 'center' as const,
width: 50,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
return <span style={{ cursor: 'text' }}>{index + 1} </span>; return <span style={{ cursor: 'text' }}>{index + 1} </span>;
}, },
}, },
{ {
title: '称', title: '称',
dataIndex: 'nickname', dataIndex: 'name',
key: 'nickname', key: 'name',
align: 'center' as const, align: 'center' as const,
width: '15%', sorter: (a: any, b: any) => a.name.localeCompare(b.name),
render: (text: string, record: any, index: number) => {
const match = record.value.match(/pt_pin=([^; ]+)(?=;?)/);
const val = (match && match[1]) || '未匹配用户名';
return (
<span style={{ cursor: 'text' }}>{record.nickname || val} </span>
);
},
}, },
{ {
title: '值', title: '值',
dataIndex: 'value', dataIndex: 'value',
key: 'value', key: 'value',
align: 'center' as const, align: 'center' as const,
width: '50%', width: '35%',
ellipsis: {
showTitle: false,
},
render: (text: string, record: any) => { render: (text: string, record: any) => {
return ( return (
<span <Tooltip
style={{ placement="topLeft"
textAlign: 'left', title={text}
display: 'inline-block', trigger={['hover', 'click']}
wordBreak: 'break-all',
cursor: 'text',
}}
> >
{text} <span>{text}</span>
</span> </Tooltip>
);
},
},
{
title: '备注',
dataIndex: 'remarks',
key: 'remarks',
align: 'center' as const,
},
{
title: '更新时间',
dataIndex: 'timestamp',
key: 'timestamp',
align: 'center' as const,
width: 165,
ellipsis: {
showTitle: false,
},
render: (text: string, record: any) => {
const language = navigator.language || navigator.languages[0];
const date = new Date(text)
.toLocaleString(language, {
hour12: false,
})
.replace(' 24:', ' 00:');
return (
<Tooltip
placement="topLeft"
title={date}
trigger={['hover', 'click']}
>
<span>{date}</span>
</Tooltip>
); );
}, },
}, },
@@ -155,23 +172,24 @@ const Config = () => {
key: 'status', key: 'status',
dataIndex: 'status', dataIndex: 'status',
align: 'center' as const, align: 'center' as const,
width: '15%', width: 70,
filters: [
{
text: '已启用',
value: 0,
},
{
text: '已禁用',
value: 1,
},
],
onFilter: (value: number, record: any) => record.status === value,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
return ( return (
<Space size="middle" style={{ cursor: 'text' }}> <Space size="middle" style={{ cursor: 'text' }}>
<Tag <Tag color={StatusColor[record.status]} style={{ marginRight: 0 }}>
color={StatusColor[record.status] || StatusColor[3]}
style={{ marginRight: 0 }}
>
{Status[record.status]} {Status[record.status]}
</Tag> </Tag>
{record.status !== Status. && (
<Tooltip title="刷新">
<a onClick={() => refreshStatus(record, index)}>
<SyncOutlined />
</a>
</Tooltip>
)}
</Space> </Space>
); );
}, },
@@ -179,71 +197,66 @@ const Config = () => {
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
width: 100,
align: 'center' as const, align: 'center' as const,
render: (text: string, record: any, index: number) => ( render: (text: string, record: any, index: number) => {
<Space size="middle"> const isPc = !isPhone;
<Tooltip title="编辑"> return (
<a onClick={() => editCookie(record, index)}> <Space size="middle">
<EditOutlined /> <Tooltip title={isPc ? '编辑' : ''}>
</a> <a onClick={() => editEnv(record, index)}>
</Tooltip> <EditOutlined />
<Tooltip title={record.status === Status. ? '启用' : '禁用'}> </a>
<a onClick={() => enabledOrDisabledCookie(record, index)}> </Tooltip>
{record.status === Status. ? ( <Tooltip
<CheckCircleOutlined /> title={
) : ( isPc ? (record.status === Status. ? '启用' : '禁用') : ''
<StopOutlined /> }
)} >
</a> <a onClick={() => enabledOrDisabledEnv(record, index)}>
</Tooltip> {record.status === Status. ? (
<Tooltip title="删除"> <CheckCircleOutlined />
<a onClick={() => deleteCookie(record, index)}> ) : (
<DeleteOutlined /> <StopOutlined />
</a> )}
</Tooltip> </a>
</Space> </Tooltip>
), <Tooltip title={isPc ? '删除' : ''}>
<a onClick={() => deleteEnv(record, index)}>
<DeleteOutlined />
</a>
</Tooltip>
</Space>
);
},
}, },
]; ];
const [width, setWidth] = 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);
const [editedCookie, setEditedCookie] = useState(); const [isEditNameModalVisible, setIsEditNameModalVisible] = useState(false);
const [editedEnv, setEditedEnv] = useState();
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]); const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [searchText, setSearchText] = useState('');
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const getCookies = () => { const getEnvs = () => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}cookies`) .get(`${config.apiPrefix}envs?searchValue=${searchText}`)
.then((data: any) => { .then((data: any) => {
setValue(data.data); setValue(data.data);
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
const refreshStatus = (record: any, index: number) => { const enabledOrDisabledEnv = (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 {
message.error('更新状态失败');
}
});
};
const enabledOrDisabledCookie = (record: any, index: number) => {
Modal.confirm({ Modal.confirm({
title: `确认${record.status === Status. ? '启用' : '禁用'}`, title: `确认${record.status === Status. ? '启用' : '禁用'}`,
content: ( content: (
<> <>
{record.status === Status. ? '启用' : '禁用'} {record.status === Status. ? '启用' : '禁用'}
Cookie{' '} Env{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning"> <Text style={{ wordBreak: 'break-all' }} type="warning">
{record.value} {record.value}
</Text>{' '} </Text>{' '}
@@ -253,7 +266,7 @@ const Config = () => {
onOk() { onOk() {
request request
.put( .put(
`${config.apiPrefix}cookies/${ `${config.apiPrefix}envs/${
record.status === Status. ? 'enable' : 'disable' record.status === Status. ? 'enable' : 'disable'
}`, }`,
{ {
@@ -266,7 +279,7 @@ const Config = () => {
`${record.status === Status. ? '启用' : '禁用'}成功`, `${record.status === Status. ? '启用' : '禁用'}成功`,
); );
const newStatus = const newStatus =
record.status === Status. ? Status.未获取 : Status.已禁用; record.status === Status. ? Status.已启用 : Status.已禁用;
const result = [...value]; const result = [...value];
result.splice(index, 1, { result.splice(index, 1, {
...record, ...record,
@@ -284,31 +297,31 @@ const Config = () => {
}); });
}; };
const addCookie = () => { const addEnv = () => {
setEditedCookie(null as any); setEditedEnv(null as any);
setIsModalVisible(true); setIsModalVisible(true);
}; };
const editCookie = (record: any, index: number) => { const editEnv = (record: any, index: number) => {
setEditedCookie(record); setEditedEnv(record);
setIsModalVisible(true); setIsModalVisible(true);
}; };
const deleteCookie = (record: any, index: number) => { const deleteEnv = (record: any, index: number) => {
Modal.confirm({ Modal.confirm({
title: '确认删除', title: '确认删除',
content: ( content: (
<> <>
Cookie{' '} {' '}
<Text style={{ wordBreak: 'break-all' }} type="warning"> <Text style={{ wordBreak: 'break-all' }} type="warning">
{record.value} {record.name}: {record.value}
</Text>{' '} </Text>{' '}
</> </>
), ),
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}cookies`, { data: [record._id] }) .delete(`${config.apiPrefix}envs`, { data: [record._id] })
.then((data: any) => { .then((data: any) => {
if (data.code === 200) { if (data.code === 200) {
message.success('删除成功'); message.success('删除成功');
@@ -326,25 +339,26 @@ const Config = () => {
}); });
}; };
const handleCancel = (cookies?: any[]) => { const handleCancel = (env?: any[]) => {
setIsModalVisible(false); setIsModalVisible(false);
if (cookies && cookies.length > 0) { env && handleEnv(env);
handleCookies(cookies);
}
}; };
const handleCookies = (cookies: any[]) => { const handleEditNameCancel = (env?: any[]) => {
setIsEditNameModalVisible(false);
getEnvs();
};
const handleEnv = (env: any) => {
const result = [...value]; const result = [...value];
for (let i = 0; i < cookies.length; i++) { const index = value.findIndex((x) => x._id === env._id);
const cookie = cookies[i]; if (index === -1) {
const index = value.findIndex((x) => x._id === cookie._id); env = Array.isArray(env) ? env : [env];
if (index === -1) { result.push(...env);
result.push(cookie); } else {
} else { result.splice(index, 1, {
result.splice(index, 1, { ...env,
...cookie, });
});
}
} }
setValue(result); setValue(result);
}; };
@@ -361,16 +375,17 @@ const Config = () => {
return; return;
} }
const dragRow = value[dragIndex]; const dragRow = value[dragIndex];
const newData = [...value];
newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, dragRow);
setValue([...newData]);
request request
.put(`${config.apiPrefix}cookies/${dragRow._id}/move`, { .put(`${config.apiPrefix}envs/${dragRow._id}/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex }, data: { fromIndex: dragIndex, toIndex: hoverIndex },
}) })
.then((data: any) => { .then((data: any) => {
if (data.code !== 200) { if (data.code === 200) {
const newData = [...value];
newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, { ...dragRow, ...data.data });
setValue([...newData]);
} else {
message.error(data); message.error(data);
} }
}); });
@@ -380,6 +395,12 @@ const Config = () => {
const onSelectChange = (selectedIds: any[]) => { const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds); setSelectedRowIds(selectedIds);
setTimeout(() => {
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
}
});
}; };
const rowSelection = { const rowSelection = {
@@ -387,18 +408,18 @@ const Config = () => {
onChange: onSelectChange, onChange: onSelectChange,
}; };
const delCookies = () => { const delEnvs = () => {
Modal.confirm({ Modal.confirm({
title: '确认删除', title: '确认删除',
content: <>Cookie吗</>, content: <></>,
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}cookies`, { data: selectedRowIds }) .delete(`${config.apiPrefix}envs`, { data: selectedRowIds })
.then((data: any) => { .then((data: any) => {
if (data.code === 200) { if (data.code === 200) {
message.success('批量删除成功'); message.success('批量删除成功');
setSelectedRowIds([]); setSelectedRowIds([]);
getCookies(); getEnvs();
} else { } else {
message.error(data); message.error(data);
} }
@@ -410,18 +431,18 @@ const Config = () => {
}); });
}; };
const operateCookies = (operationStatus: number) => { const operateEnvs = (operationStatus: number) => {
Modal.confirm({ Modal.confirm({
title: `确认${OperationName[operationStatus]}`, title: `确认${OperationName[operationStatus]}`,
content: <>{OperationName[operationStatus]}Cookie吗</>, content: <>{OperationName[operationStatus]}</>,
onOk() { onOk() {
request request
.put(`${config.apiPrefix}cookies/${OperationPath[operationStatus]}`, { .put(`${config.apiPrefix}envs/${OperationPath[operationStatus]}`, {
data: selectedRowIds, data: selectedRowIds,
}) })
.then((data: any) => { .then((data: any) => {
if (data.code === 200) { if (data.code === 200) {
getCookies(); getEnvs();
} else { } else {
message.error(data); message.error(data);
} }
@@ -433,39 +454,42 @@ const Config = () => {
}); });
}; };
const modifyName = () => {
setIsEditNameModalVisible(true);
};
const onSearch = (value: string) => {
setSearchText(value.trim());
};
useEffect(() => { useEffect(() => {
if (document.body.clientWidth < 768) { getEnvs();
setWidth('auto'); }, [searchText]);
setMarginLeft(0);
setMarginTop(0); useEffect(() => {
} else { setTimeout(() => {
setWidth('100%'); setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
setMarginLeft(0); });
setMarginTop(-72);
}
getCookies();
}, []); }, []);
return ( return (
<PageContainer <PageContainer
className="session-wrapper" className="ql-container-wrapper env-wrapper"
title="Session管理" title="环境变量"
extra={[ extra={[
<Button key="2" type="primary" onClick={() => addCookie()}> <Search
Cookie placeholder="请输入名称/值/备注"
style={{ width: 'auto' }}
enterButton
loading={loading}
onSearch={onSearch}
/>,
<Button key="2" type="primary" onClick={() => addEnv()}>
</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 && ( {selectedRowIds.length > 0 && (
@@ -473,20 +497,27 @@ const Config = () => {
<Button <Button
type="primary" type="primary"
style={{ marginBottom: 5 }} style={{ marginBottom: 5 }}
onClick={delCookies} onClick={modifyName}
>
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={delEnvs}
> >
</Button> </Button>
<Button <Button
type="primary" type="primary"
onClick={() => operateCookies(0)} onClick={() => operateEnvs(0)}
style={{ marginLeft: 8, marginBottom: 5 }} style={{ marginLeft: 8, marginBottom: 5 }}
> >
</Button> </Button>
<Button <Button
type="primary" type="primary"
onClick={() => operateCookies(1)} onClick={() => operateEnvs(1)}
style={{ marginLeft: 8, marginRight: 8 }} style={{ marginLeft: 8, marginRight: 8 }}
> >
@@ -505,10 +536,10 @@ const Config = () => {
dataSource={value} dataSource={value}
rowKey="_id" rowKey="_id"
size="middle" size="middle"
scroll={{ x: 768 }} scroll={{ x: 1000, y: tableScrollHeight }}
components={components} components={components}
loading={loading} loading={loading}
onRow={(record, index) => { onRow={(record: any, index: number) => {
return { return {
index, index,
moveRow, moveRow,
@@ -516,13 +547,18 @@ const Config = () => {
}} }}
/> />
</DndProvider> </DndProvider>
<CookieModal <EnvModal
visible={isModalVisible} visible={isModalVisible}
handleCancel={handleCancel} handleCancel={handleCancel}
cookie={editedCookie} env={editedEnv}
/>
<EditNameModal
visible={isEditNameModalVisible}
handleCancel={handleEditNameCancel}
ids={selectedRowIds}
/> />
</PageContainer> </PageContainer>
); );
}; };
export default Config; export default Env;
+121
View File
@@ -0,0 +1,121 @@
import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form, Radio } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
const EnvModal = ({
env,
handleCancel,
visible,
}: {
env?: any;
visible: boolean;
handleCancel: (cks?: any[]) => void;
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const handleOk = async (values: any) => {
setLoading(true);
const { value, split, name, remarks } = values;
const method = env ? 'put' : 'post';
let payload;
if (!env) {
if (split === '1') {
const symbol = value.includes('&') ? '&' : '\n';
payload = value.split(symbol).map((x: any) => {
return {
name: name,
value: x,
remarks: remarks,
};
});
} else {
payload = [{ value, name, remarks }];
}
} else {
payload = { ...values, _id: env._id };
}
const { code, data } = await request[method](`${config.apiPrefix}envs`, {
data: payload,
});
if (code === 200) {
message.success(env ? '更新变量成功' : '添加变量成功');
} else {
message.error(data);
}
setLoading(false);
handleCancel(data);
};
useEffect(() => {
form.resetFields();
}, [env, visible]);
return (
<Modal
title={env ? '编辑变量' : '新建变量'}
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="env_modal" initialValues={env}>
<Form.Item
name="name"
label="名称"
rules={[
{ required: true, message: '请输入环境变量名称', whitespace: true },
{
pattern: /^[a-zA-Z_][0-9a-zA-Z_]+$/,
message: '只能输入字母数字下划线,且不能以数字开头',
},
]}
>
<Input placeholder="请输入环境变量名称" />
</Form.Item>
{!env && (
<Form.Item
name="split"
label="自动拆分"
initialValue="0"
tooltip="多个依赖是否换行分割"
>
<Radio.Group>
<Radio value="1"></Radio>
<Radio value="0"></Radio>
</Radio.Group>
</Form.Item>
)}
<Form.Item
name="value"
label="值"
rules={[
{ required: true, message: '请输入环境变量值', whitespace: true },
]}
>
<Input.TextArea
rows={4}
autoSize={true}
placeholder="请输入环境变量值"
/>
</Form.Item>
<Form.Item name="remarks" label="备注">
<Input placeholder="请输入备注" />
</Form.Item>
</Form>
</Modal>
);
};
export default EnvModal;
+77
View File
@@ -0,0 +1,77 @@
@import '~antd/es/style/themes/default.less';
.container {
display: flex;
flex-direction: column;
align-items: center;
height: 100vh;
height: calc(100vh - var(--vh-offset, 0px));
overflow: auto;
background: @layout-body-background;
padding-top: 70px;
}
@media (min-width: @screen-md-min) {
.container {
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
}
}
.top {
text-align: center;
}
.header {
display: flex;
align-items: center;
flex-direction: column;
}
.logo {
width: 48px;
display: block;
margin-bottom: 24px;
margin-top: 20px;
}
.title {
font-size: 20px;
margin-bottom: 16px;
}
.desc {
margin-top: 12px;
margin-bottom: 40px;
color: @text-color-secondary;
font-size: @font-size-base;
}
.main {
padding: 20px;
border-radius: 6px;
background-color: #f6f8fa;
border: 1px solid #ebedef;
display: flex;
max-width: 500px;
width: 90%;
height: 350px;
.ant-steps {
width: 35%;
display: flex;
align-items: center;
position: relative;
top: 6%;
}
.steps-container {
flex: 1;
overflow-y: auto;
}
}
.extra {
margin-top: 20px;
}
+255
View File
@@ -0,0 +1,255 @@
import React, { Fragment, useEffect, useState } from 'react';
import {
Button,
Row,
Input,
Form,
message,
Typography,
Steps,
Select,
} from 'antd';
import config from '@/utils/config';
import { history } from 'umi';
import styles from './index.less';
import { request } from '@/utils/http';
const FormItem = Form.Item;
const { Step } = Steps;
const { Option } = Select;
const { Link } = Typography;
const Initialization = () => {
const [loading, setLoading] = useState(false);
const [current, setCurrent] = React.useState(0);
const [fields, setFields] = useState<any[]>([]);
const next = () => {
setCurrent(current + 1);
};
const prev = () => {
setCurrent(current - 1);
};
const submitAccountSetting = (values: any) => {
setLoading(true);
request
.put(`${config.apiPrefix}init/user`, {
data: {
username: values.username,
password: values.password,
},
})
.then((data) => {
if (data.code === 200) {
next();
} else {
message.error(data.message);
}
})
.finally(() => setLoading(false));
};
const submitNotification = (values: any) => {
request
.put(`${config.apiPrefix}init/notification`, {
data: {
...values,
},
})
.then((_data: any) => {
if (_data && _data.code === 200) {
next();
} else {
message.error(_data.message);
}
})
.finally(() => setLoading(false));
};
const notificationModeChange = (value: string) => {
const _fields = (config.notificationModeMap as any)[value];
setFields(_fields || []);
};
const steps = [
{
title: '欢迎使用',
content: (
<div className={styles.top} style={{ marginTop: 100 }}>
<div className={styles.header}>
<span className={styles.title}>使</span>
</div>
<div className={styles.action}>
<Button
type="primary"
onClick={() => {
next();
}}
>
</Button>
</div>
</div>
),
},
{
title: '通知设置',
content: (
<Form onFinish={submitNotification} layout="vertical">
<Form.Item
label="通知方式"
name="type"
rules={[{ required: true, message: '请选择通知方式' }]}
style={{ maxWidth: 350 }}
>
<Select
onChange={notificationModeChange}
placeholder="请选择通知方式"
>
{config.notificationModes
.filter((x) => x.value !== 'closed')
.map((x) => (
<Option value={x.value}>{x.label}</Option>
))}
</Select>
</Form.Item>
{fields.map((x) => (
<Form.Item
label={x.label}
name={x.label}
extra={x.tip}
rules={[{ required: x.required }]}
style={{ maxWidth: 400 }}
>
<Input.TextArea
autoSize={true}
placeholder={`请输入${x.label}`}
/>
</Form.Item>
))}
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
<Button type="link" htmlType="button" onClick={() => next()}>
</Button>
</Form>
),
},
{
title: '账户设置',
content: (
<Form onFinish={submitAccountSetting} layout="vertical">
<Form.Item
label="用户名"
name="username"
rules={[{ required: true }]}
style={{ maxWidth: 350 }}
>
<Input placeholder="用户名" />
</Form.Item>
<Form.Item
label="密码"
name="password"
rules={[
{ required: true },
{
pattern: /^(?!admin$).*$/,
message: '密码不能为admin',
},
]}
hasFeedback
style={{ maxWidth: 350 }}
>
<Input type="password" placeholder="密码" />
</Form.Item>
<Form.Item
name="confirm"
label="确认密码"
dependencies={['password']}
hasFeedback
style={{ maxWidth: 350 }}
rules={[
{
required: true,
},
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('password') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('您输入的两个密码不匹配!'));
},
}),
]}
>
<Input.Password placeholder="确认密码" />
</Form.Item>
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
</Form>
),
},
{
title: '完成安装',
content: (
<div className={styles.top} style={{ marginTop: 80 }}>
<div className={styles.header}>
<span className={styles.title}></span>
<Link href="https://github.com/whyour/qinglong" target="_blank">
Github
</Link>
<Link href="https://t.me/jiao_long" target="_blank">
Telegram频道
</Link>
</div>
<div style={{ marginTop: 16 }}>
<Button
type="primary"
onClick={() => {
history.push('/login');
}}
>
</Button>
</div>
</div>
),
},
];
return (
<div className={styles.container}>
<div className={styles.top}>
<div className={styles.header}>
<img
alt="logo"
className={styles.logo}
src="https://z3.ax1x.com/2021/11/18/I7MpAe.png"
/>
<span className={styles.title}></span>
</div>
</div>
<div className={styles.main}>
<Steps
current={current}
direction="vertical"
size="small"
className={styles['ant-steps']}
>
{steps.map((item) => (
<Step key={item.title} title={item.title} />
))}
</Steps>
<div className={styles['steps-container']}>
{steps[current].content}
</div>
</div>
</div>
);
};
export default Initialization;
+3 -16
View File
@@ -7,31 +7,18 @@
background-color: #fff; background-color: #fff;
height: calc(100vh - 128px); height: calc(100vh - 128px);
height: calc(100vh - var(--vh-offset, 0px) - 128px); height: calc(100vh - var(--vh-offset, 0px) - 128px);
width: @tree-width;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
&-scroller { &-scroller {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
} border-right: 1px dashed #f0f0f0;
&-search { padding-top: 6px;
margin-bottom: 16px;
} }
} }
.log-container { .log-container {
display: flex; display: flex;
} position: relative;
:global {
.log-wrapper {
.ant-pro-grid-content.wide .ant-pro-page-container-children-content {
background-color: #f8f8f8;
}
.CodeMirror {
width: calc(100% - 32px - @tree-width);
}
}
} }
+87 -74
View File
@@ -1,10 +1,13 @@
import { useState, useEffect, useCallback, Key } from 'react'; import { useState, useEffect, useCallback, Key, useRef } from 'react';
import { TreeSelect, Tree, Input } from 'antd'; import { TreeSelect, Tree, Input } 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 Editor from '@monaco-editor/react';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import styles from './index.module.less'; import styles from './index.module.less';
import { Controlled as CodeMirror } from 'react-codemirror2';
import { useCtx, useTheme } from '@/utils/hooks';
import SplitPane from 'react-split-pane';
function getFilterData(keyword: string, data: any) { function getFilterData(keyword: string, data: any) {
const expandedKeys: string[] = []; const expandedKeys: string[] = [];
@@ -13,7 +16,6 @@ function getFilterData(keyword: string, data: any) {
data.forEach((item: any) => { data.forEach((item: any) => {
if (item.title.toLocaleLowerCase().includes(keyword)) { if (item.title.toLocaleLowerCase().includes(keyword)) {
tree.push(item); tree.push(item);
expandedKeys.push(...item.children.map((x: any) => x.key));
} else { } else {
const children: any[] = []; const children: any[] = [];
(item.children || []).forEach((subItem: any) => { (item.children || []).forEach((subItem: any) => {
@@ -26,7 +28,7 @@ function getFilterData(keyword: string, data: any) {
...item, ...item,
children, children,
}); });
expandedKeys.push(...children.map((x) => x.key)); expandedKeys.push(item.key);
} }
} }
}); });
@@ -35,24 +37,27 @@ function getFilterData(keyword: string, data: any) {
return { tree: data, expandedKeys }; return { tree: data, expandedKeys };
} }
const Log = () => { const Log = ({ headerStyle, isPhone, theme }: any) => {
const [width, setWidth] = useState('100%');
const [marginLeft, setMarginLeft] = useState(0);
const [marginTop, setMarginTop] = useState(-72);
const [title, setTitle] = useState('请选择日志文件'); const [title, setTitle] = useState('请选择日志文件');
const [value, setValue] = useState('请选择日志文件'); const [value, setValue] = useState('请选择日志文件');
const [select, setSelect] = useState(); const [select, setSelect] = useState<any>();
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<any[]>([]);
const [filterData, setFilterData] = useState<any[]>([]); const [filterData, setFilterData] = useState<any[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isPhone, setIsPhone] = useState(false); const [height, setHeight] = useState<number>();
const treeDom = useRef<any>();
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const getConfig = () => { const getLogs = () => {
request.get(`${config.apiPrefix}logs`).then((data) => { setLoading(true);
const result = formatData(data.dirs) as any; request
setData(result); .get(`${config.apiPrefix}logs`)
setFilterData(result); .then((data) => {
}); const result = formatData(data.dirs) as any;
setData(result);
setFilterData(result);
})
.finally(() => setLoading(false));
}; };
const formatData = (tree: any[]) => { const formatData = (tree: any[]) => {
@@ -73,16 +78,16 @@ const Log = () => {
}; };
const getLog = (node: any) => { const getLog = (node: any) => {
setLoading(true); request.get(`${config.apiPrefix}logs/${node.value}`).then((data) => {
request setValue(data.data);
.get(`${config.apiPrefix}logs/${node.value}`) });
.then((data) => {
setValue(data.data);
})
.finally(() => setLoading(false));
}; };
const onSelect = (value: any, node: any) => { const onSelect = (value: any, node: any) => {
if (node.key === select || !value) {
return;
}
setValue('加载中...');
setSelect(value); setSelect(value);
setTitle(node.parent || node.value); setTitle(node.parent || node.value);
getLog(node); getLog(node);
@@ -95,31 +100,28 @@ const Log = () => {
const onSearch = useCallback( const onSearch = useCallback(
(e) => { (e) => {
const keyword = e.target.value; const keyword = e.target.value;
const { tree } = getFilterData(keyword.toLocaleLowerCase(), data); const { tree, expandedKeys } = getFilterData(
keyword.toLocaleLowerCase(),
data,
);
setFilterData(tree); setFilterData(tree);
setExpandedKeys(expandedKeys);
}, },
[data, setFilterData], [data, setFilterData],
); );
useEffect(() => { useEffect(() => {
if (document.body.clientWidth < 768) { getLogs();
setWidth('auto'); if (treeDom && treeDom.current) {
setMarginLeft(0); setHeight(treeDom.current.clientHeight);
setMarginTop(0);
setIsPhone(true);
} else {
setWidth('100%');
setMarginLeft(0);
setMarginTop(-72);
setIsPhone(false);
} }
getConfig();
}, []); }, []);
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper log-wrapper" className="ql-container-wrapper log-wrapper"
title={title} title={title}
loading={loading}
extra={ extra={
isPhone && [ isPhone && [
<TreeSelect <TreeSelect
@@ -129,55 +131,66 @@ const Log = () => {
treeData={data} treeData={data}
placeholder="请选择日志文件" placeholder="请选择日志文件"
showSearch showSearch
key="value"
onSelect={onSelect} onSelect={onSelect}
/>, />,
] ]
} }
header={{ header={{
style: { style: headerStyle,
padding: '4px 16px 4px 15px',
position: 'sticky',
top: 0,
left: 0,
zIndex: 20,
marginTop,
width,
marginLeft,
},
}} }}
> >
<div className={`${styles['log-container']}`}> <div className={`${styles['log-container']} log-container`}>
{!isPhone && ( {!isPhone && (
<div className={styles['left-tree-container']}> <SplitPane split="vertical" size={200} maxSize={-100}>
<Input.Search <div className={styles['left-tree-container']}>
className={styles['left-tree-search']} <Input.Search
onChange={onSearch} className={styles['left-tree-search']}
></Input.Search> onChange={onSearch}
<div className={styles['left-tree-scroller']}> ></Input.Search>
<Tree <div className={styles['left-tree-scroller']} ref={treeDom}>
className={styles['left-tree']} <Tree
treeData={filterData} className={styles['left-tree']}
onSelect={onTreeSelect} treeData={filterData}
></Tree> showIcon={true}
height={height}
selectedKeys={[select]}
showLine={{ showLeafIcon: true }}
onSelect={onTreeSelect}
></Tree>
</div>
</div> </div>
</div> <Editor
language="shell"
theme={theme}
value={value}
options={{
readOnly: true,
fontSize: 12,
lineNumbersMinChars: 3,
fontFamily: 'Source Code Pro',
folding: false,
glyphMargin: false,
wordWrap: 'on',
}}
/>
</SplitPane>
)}
{isPhone && (
<CodeMirror
value={value}
options={{
lineNumbers: true,
lineWrapping: true,
styleActiveLine: true,
matchBrackets: true,
readOnly: true,
}}
onBeforeChange={(editor, data, value) => {
setValue(value);
}}
onChange={(editor, data, value) => {}}
/>
)} )}
<CodeMirror
value={value}
options={{
lineNumbers: true,
lineWrapping: true,
styleActiveLine: true,
matchBrackets: true,
mode: 'shell',
readOnly: true,
}}
onBeforeChange={(editor, data, value) => {
setValue(value);
}}
onChange={(editor, data, value) => {}}
/>
</div> </div>
</PageContainer> </PageContainer>
); );
+22 -27
View File
@@ -3,31 +3,12 @@
.container { .container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center;
height: 100vh; height: 100vh;
height: calc(100vh - var(--vh-offset, 0px)); height: calc(100vh - var(--vh-offset, 0px));
overflow: auto; overflow: auto;
background: @layout-body-background; background: @layout-body-background;
} padding-top: 70px;
.lang {
width: 100%;
height: 40px;
line-height: 44px;
text-align: right;
:global(.ant-dropdown-trigger) {
margin-right: 24px;
}
}
.content {
position: absolute;
top: 45%;
left: 50%;
margin: -160px 0 0 -160px;
width: 320px;
height: 320px;
padding: 36px;
box-shadow: 0 0 100px rgba(0, 0, 0, 0.08);
} }
@media (min-width: @screen-md-min) { @media (min-width: @screen-md-min) {
@@ -44,16 +25,20 @@
} }
.header { .header {
height: 44px;
line-height: 44px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; flex-direction: column;
} }
.logo { .logo {
width: 30px; width: 48px;
margin-right: 8px; display: block;
margin-bottom: 24px;
}
.title {
font-size: 20px;
margin-bottom: 16px;
} }
.desc { .desc {
@@ -64,7 +49,12 @@
} }
.main { .main {
margin: 35px auto 0; padding: 20px;
border-radius: 6px;
background-color: #f6f8fa;
border: 1px solid #ebedef;
width: 340px;
@media screen and (max-width: @screen-sm) { @media screen and (max-width: @screen-sm) {
width: 95%; width: 95%;
max-width: 320px; max-width: 320px;
@@ -104,3 +94,8 @@
font-size: @font-size-base; font-size: @font-size-base;
} }
} }
.extra {
margin-top: 20px;
width: 340px;
}
+166 -35
View File
@@ -1,14 +1,35 @@
import React, { Fragment, useEffect } from 'react'; import React, { Fragment, useEffect, useState } from 'react';
import { Button, Row, Input, Form, message } from 'antd'; import {
Button,
Row,
Input,
Form,
message,
notification,
Statistic,
} from 'antd';
import config from '@/utils/config'; import config from '@/utils/config';
import { history, Link } from 'umi'; import { history, Link } from 'umi';
import styles from './index.less'; import styles from './index.less';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import { useTheme } from '@/utils/hooks';
import { MobileOutlined } from '@ant-design/icons';
const FormItem = Form.Item; const FormItem = Form.Item;
const { Countdown } = Statistic;
const Login = () => { const Login = () => {
const [loading, setLoading] = useState(false);
const [waitTime, setWaitTime] = useState<any>();
const { theme } = useTheme();
const [twoFactor, setTwoFactor] = useState(false);
const [verifying, setVerifying] = useState(false);
const [loginInfo, setLoginInfo] = useState<any>();
const handleOk = (values: any) => { const handleOk = (values: any) => {
setLoading(true);
setTwoFactor(false);
setWaitTime(null);
request request
.post(`${config.apiPrefix}login`, { .post(`${config.apiPrefix}login`, {
data: { data: {
@@ -17,20 +38,81 @@ const Login = () => {
}, },
}) })
.then((data) => { .then((data) => {
if (data.code === 200) { if (data.code === 420) {
localStorage.setItem(config.authKey, data.token); setLoginInfo({
history.push('/crontab'); username: values.username,
} else if (data.code === 100) { password: values.password,
message.warn(data.msg); });
setTwoFactor(true);
} else { } else {
message.error(data.msg); checkResponse(data);
} }
setLoading(false);
}) })
.catch(function (error) { .catch(function (error) {
console.log(error); console.log(error);
setLoading(false);
}); });
}; };
const completeTowFactor = (values: any) => {
setVerifying(true);
request
.put(`${config.apiPrefix}user/two-factor/login`, {
data: { ...loginInfo, code: values.code },
})
.then((data: any) => {
if (data.code === 430) {
message.error(data.message);
} else {
checkResponse(data);
}
setVerifying(false);
})
.catch((error: any) => {
console.log(error);
setVerifying(false);
});
};
const checkResponse = (data: any) => {
if (data.code === 200) {
const {
token,
lastip,
lastaddr,
lastlogon,
retries = 0,
platform,
} = data.data;
localStorage.setItem(config.authKey, token);
notification.success({
message: '登录成功!',
description: (
<>
<div>
{lastlogon ? new Date(lastlogon).toLocaleString() : '-'}
</div>
<div>{lastaddr || '-'}</div>
<div>IP{lastip || '-'}</div>
<div>{platform || '-'}</div>
<div>{retries > 0 ? `失败${retries}` : '成功'}</div>
</>
),
duration: 5,
});
history.push('/crontab');
} else if (data.code === 100) {
message.warn(data.message);
} else if (data.code === 410) {
message.error(data.message);
setWaitTime(data.data);
} else {
message.error(data.message);
}
};
useEffect(() => { useEffect(() => {
const isAuth = localStorage.getItem(config.authKey); const isAuth = localStorage.getItem(config.authKey);
if (isAuth) { if (isAuth) {
@@ -40,44 +122,93 @@ const Login = () => {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.content}> <div className={styles.top}>
<div className={styles.top}> <div className={styles.header}>
<div className={styles.header}> <img
<img alt="logo"
alt="logo" className={styles.logo}
className={styles.logo} src="https://z3.ax1x.com/2021/11/18/I7MpAe.png"
src="https://qinglong.whyour.cn/qinglong.png" />
/> <span className={styles.title}>
<span className={styles.title}>{config.siteName}</span> {twoFactor ? '两步验证' : config.siteName}
</div> </span>
</div> </div>
<div className={styles.main}> </div>
<Form onFinish={handleOk}> <div className={styles.main}>
{twoFactor ? (
<Form layout="vertical" onFinish={completeTowFactor}>
<FormItem <FormItem
name="username" name="code"
rules={[{ required: true, message: '请输入用户名' }]} label="验证码"
rules={[
{
pattern: /^[0-9]{6}$/,
message: '验证码为6位数字',
validateTrigger: 'onBlur',
},
]}
hasFeedback hasFeedback
> >
<Input placeholder="6位数字" autoFocus autoComplete="off" />
</FormItem>
<Button
type="primary"
htmlType="submit"
style={{ width: '100%' }}
loading={verifying}
>
</Button>
</Form>
) : (
<Form layout="vertical" onFinish={handleOk}>
<FormItem name="username" label="用户名" hasFeedback>
<Input placeholder="用户名" autoFocus /> <Input placeholder="用户名" autoFocus />
</FormItem> </FormItem>
<FormItem <FormItem name="password" label="密码" hasFeedback>
name="password"
rules={[{ required: true, message: '请输入密码' }]}
hasFeedback
>
<Input type="password" placeholder="密码" /> <Input type="password" placeholder="密码" />
</FormItem> </FormItem>
<Row> <Row>
<Button {waitTime ? (
type="primary" <Button type="primary" style={{ width: '100%' }} disabled>
htmlType="submit"
style={{ width: '100%' }} <Countdown
> valueStyle={{
color:
</Button> theme === 'vs'
? 'rgba(0,0,0,.25)'
: 'rgba(232, 230, 227, 0.25)',
}}
className="inline-countdown"
onFinish={() => setWaitTime(null)}
format="ss"
value={Date.now() + 1000 * waitTime}
/>
</Button>
) : (
<Button
type="primary"
htmlType="submit"
style={{ width: '100%' }}
loading={loading}
>
</Button>
)}
</Row> </Row>
</Form> </Form>
</div> )}
</div>
<div className={styles.extra}>
{twoFactor ? (
<div style={{ paddingLeft: 20, position: 'relative' }}>
<MobileOutlined style={{ position: 'absolute', left: 0, top: 4 }} />
</div>
) : (
''
)}
</div> </div>
</div> </div>
); );
+213
View File
@@ -0,0 +1,213 @@
import React, { useEffect, useState, useRef } from 'react';
import { Drawer, Button, Tabs, Badge, Select, TreeSelect } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import SplitPane from 'react-split-pane';
import Editor from '@monaco-editor/react';
import SaveModal from './saveModal';
import SettingModal from './setting';
import { useTheme } from '@/utils/hooks';
const { Option } = Select;
const LangMap: any = {
'.py': 'python',
'.js': 'javascript',
'.sh': 'shell',
'.ts': 'typescript',
};
const prefixMap: any = {
python: '.py',
javascript: '.js',
shell: '.sh',
typescript: '.ts',
};
const EditModal = ({
treeData,
currentFile,
content,
handleCancel,
visible,
socketMessage,
}: {
treeData?: any;
currentFile?: string;
content?: string;
visible: boolean;
socketMessage: any;
handleCancel: () => void;
}) => {
const [value, setValue] = useState('');
const [language, setLanguage] = useState<string>('javascript');
const [fileName, setFileName] = useState<string>('');
const [selectedKey, setSelectedKey] = useState<string>('');
const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false);
const [settingModalVisible, setSettingModalVisible] =
useState<boolean>(false);
const [log, setLog] = useState<string>('');
const { theme } = useTheme();
const editorRef = useRef<any>(null);
const cancel = () => {
handleCancel();
};
const onSelect = (value: any, node: any) => {
if (node.value === fileName || !value) {
return;
}
const newMode = LangMap[value.slice(-3)] || '';
setFileName(value);
setLanguage(newMode);
getDetail(node);
setSelectedKey(node.key);
};
const getDetail = (node: any) => {
request.get(`${config.apiPrefix}scripts/${node.value}`).then((data) => {
setValue(data.data);
});
};
const run = () => {
request
.put(`${config.apiPrefix}scripts/run`, {
data: {
filename: fileName,
path: '',
},
})
.then((data) => {});
};
useEffect(() => {
if (!socketMessage) {
return;
}
let { type, message: _message, references } = socketMessage;
if (type !== 'manuallyRunScript') {
return;
}
if (log) {
_message = `\n${_message}`;
}
setLog(`${log}${_message}`);
}, [socketMessage]);
useEffect(() => {
if (currentFile) {
setFileName(currentFile);
setValue(content as string);
setSelectedKey(currentFile);
}
}, [currentFile, content]);
return (
<Drawer
className="edit-modal"
title={
<>
<span style={{ marginRight: 8 }}>{fileName}</span>
<TreeSelect
style={{ marginRight: 8, width: 120 }}
value={selectedKey}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={treeData}
placeholder="请选择脚本文件"
showSearch
onSelect={onSelect}
/>
<Select
value={language}
style={{ width: 120, marginRight: 8 }}
onChange={(e) => {
setLanguage(e);
}}
>
<Option value="javascript">javascript</Option>
<Option value="typescript">typescript</Option>
<Option value="shell">shell</Option>
<Option value="python">python</Option>
</Select>
<Button type="primary" style={{ marginRight: 8 }} onClick={run}>
</Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={() => {
setLog('');
}}
>
</Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={() => {
setSettingModalVisible(true);
}}
>
</Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={() => {
setSaveModalVisible(true);
}}
>
</Button>
</>
}
width={'100%'}
headerStyle={{ padding: '11px 24px' }}
onClose={cancel}
visible={visible}
>
<SplitPane split="vertical" minSize={200} defaultSize="50%">
<Editor
language={language}
value={value}
theme={theme}
options={{
fontSize: 12,
minimap: { enabled: false },
lineNumbersMinChars: 3,
glyphMargin: false,
}}
onMount={(editor) => {
editorRef.current = editor;
}}
/>
<div>
<pre>{log}</pre>
</div>
</SplitPane>
<SaveModal
visible={saveModalVisible}
handleCancel={() => {
setSaveModalVisible(false);
}}
file={{
content:
editorRef.current &&
editorRef.current.getValue().replace(/\r\n/g, '\n'),
filename: fileName,
}}
/>
<SettingModal
visible={settingModalVisible}
handleCancel={() => {
setSettingModalVisible(false);
}}
/>
</Drawer>
);
};
export default EditModal;
+96
View File
@@ -0,0 +1,96 @@
import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form, Select } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
const { Option } = Select;
const EditScriptNameModal = ({
handleCancel,
treeData,
visible,
}: {
visible: boolean;
treeData: any[];
handleCancel: (file?: {
filename: string;
path: string;
key: string;
}) => void;
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [dirs, setDirs] = useState<any[]>([]);
const handleOk = async (values: any) => {
setLoading(true);
values.path = values.path || '';
request
.post(`${config.apiPrefix}scripts`, {
data: { filename: values.filename, path: values.path, content: '' },
})
.then(({ code, data }) => {
if (code === 200) {
message.success('保存文件成功');
handleCancel({
filename: values.filename,
path: values.path,
key: `${values.path}-${values.filename}`,
});
} else {
message.error(data);
}
setLoading(false);
})
.finally(() => setLoading(false));
};
useEffect(() => {
form.resetFields();
const originDirs = treeData.filter((x) => x.disabled);
setDirs([{ key: '' }, ...originDirs]);
}, [visible]);
return (
<Modal
title="新建文件"
visible={visible}
forceRender
onOk={() => {
form
.validateFields()
.then((values) => {
handleOk(values);
})
.catch((info) => {
console.log('Validate Failed:', info);
});
}}
onCancel={() => handleCancel()}
confirmLoading={loading}
>
<Form form={form} layout="vertical" name="edit_name_modal">
<Form.Item
name="filename"
label="文件名"
rules={[{ required: true, message: '请输入文件名' }]}
>
<Input placeholder="请输入文件名" />
</Form.Item>
<Form.Item
label="父目录"
name="path"
initialValue={dirs && dirs.length > 0 ? dirs[0].key : ''}
>
<Select placeholder="请选择父目录">
{dirs.map((x) => (
<Option value={x.key}>{x.key || '根'}</Option>
))}
</Select>
</Form.Item>
</Form>
</Modal>
);
};
export default EditScriptNameModal;

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