Compare commits

..
394 Commits
Author SHA1 Message Date
whyour 6131fe0110 更新版本 v2.15.19 2023-07-20 23:14:16 +08:00
whyour 665c344ae4 修复依赖是否已经安装判断 2023-07-20 22:54:11 +08:00
雨思andGitHub 373b8c97d7 增加PushMe消息通道 (#2018),修复系统设置保存通知 2023-07-20 13:19:39 +08:00
whyour 56eb0c5408 更新版本 v2.15.18 2023-07-19 21:17:22 +08:00
whyour fe55929959 更新 readme 2023-07-19 21:02:52 +08:00
whyour 01e2bd007d 修复重启提示持续时间 2023-07-18 21:39:03 +08:00
whyour 4e091b0c3e 修改 nginx body 限制,服务重启延迟时间 2023-07-18 15:30:43 +08:00
whyour db94cd3799 修复备份数据超时时间 2023-07-18 13:43:02 +08:00
whyour a15192b9b2 升级 @umijs/max 版本 2023-07-17 23:18:48 +08:00
whyour efd4f1d5ab umi-request 替换为 axios 2023-07-17 23:13:06 +08:00
whyour bd166ee794 修复 pm2 重启逻辑 2023-07-17 13:25:56 +08:00
whyour 93e94ea94c 系统设置增加数据恢复功能 2023-07-16 22:02:30 +08:00
whyour 88b87de391 增加数据备份功能 2023-07-16 00:23:29 +08:00
whyour 8affff96f3 修改系统内更新系统逻辑 2023-07-13 22:09:45 +08:00
whyour 936b565fb1 修改系统更新时间和版本获取 2023-07-12 00:05:46 +08:00
whyour b69ff2895e 增加依赖是否已经安装判断 2023-07-10 23:48:05 +08:00
whyour f3791cbb62 修复编译错误 2023-07-09 17:46:02 +08:00
whyour b0f3b51736 修复服务启动时定时删除日志失效 2023-07-08 20:58:36 +08:00
whyour 3aa112e373 修改 node 进程错误监控 2023-07-08 00:27:05 +08:00
whyour 683482e067 修改 ql update 逻辑 2023-07-08 00:18:09 +08:00
whyour 5a2caeb66b 更新版本 v2.15.17 2023-07-02 16:09:09 +08:00
whyour ee6e5bd8b4 修改任务状态值 2023-07-02 16:09:01 +08:00
whyour f970322f0a 修复定时任务状态筛选 2023-07-02 12:19:34 +08:00
whyour 1718120623 修改更新仓库逻辑 2023-07-01 20:48:28 +08:00
whyour 702c3160ec 修改并发逻辑,系统设置增加定时任务并发设置 2023-07-01 15:26:20 +08:00
whyour db227e56bf 更新版本 v2.15.16 2023-06-18 15:51:42 +08:00
whyour 84828865f7 修复依赖安装流程 2023-06-18 15:51:35 +08:00
whyour 87455cebc0 修改应用设置 loading 默认值 2023-06-17 20:26:40 +08:00
whyour 9f4435b237 修改初始化逻辑 2023-06-16 02:12:25 +08:00
whyour e5bc4b3b04 修复关闭全部任务视图,默认视图筛选错误 2023-06-14 22:38:15 +08:00
whyour 11e1b50000 修改订阅通知 2023-06-11 20:05:39 +08:00
whyour bec226ae13 增加初始化文件写入,修复 shell 变量 2023-06-11 19:58:30 +08:00
whyour c9ecdc6c97 企业微信通知增加代理地址配置 QYWX_ORIGIN 2023-06-11 15:03:07 +08:00
whyour 4cf64e7af0 增加 demo 环境变量 2023-06-10 22:20:19 +08:00
whyour 4ce01c5f0e 修复初始化界面样式 2023-06-10 16:16:36 +08:00
whyour 2eff99abe1 修复侧边栏样式 2023-06-10 13:27:21 +08:00
whyour 5bf57185d9 更新 readme 2023-06-10 12:52:53 +08:00
whyour c59dac19ae 修复 shell 变量无引号报错 2023-06-09 19:50:38 +08:00
whyour f1ef8e72ae 修复解析脚本名称逻辑 2023-06-09 19:45:25 +08:00
whyour 5b1accffb7 修改 api 限流策略,修复检查检查日志 2023-06-07 22:45:37 +08:00
whyour f64ff83414 修复列表查询字符转码 2023-06-04 22:11:48 +08:00
whyour 26c914d1db 修复侧边栏样式 2023-06-03 01:24:52 +08:00
whyour 8f75a54110 修改 textarea 样式 2023-06-03 00:48:04 +08:00
whyour 874b6e3a56 修复错误页样式 2023-06-01 23:02:36 +08:00
whyour f305d45225 修复依赖安装状态流转 2023-05-31 15:07:40 +08:00
whyour a7f97bd729 替换 spawn 为 cross-spawn 2023-05-31 14:25:20 +08:00
whyour 0bbb7c7b89 修复获取 cpu 核数 2023-05-30 17:45:47 +08:00
whyour f7465759c7 降级 p-limit,修复 esm 问题 2023-05-30 17:31:31 +08:00
whyour 998e82d323 增加 api 限流配置 2023-05-30 16:41:19 +08:00
whyour f8dfee8945 重构任务并发执行逻辑 2023-05-30 16:32:00 +08:00
whyour 86e3d8736b 更新版本 v2.15.15 2023-05-28 20:06:15 +08:00
whyour 85d32d00e1 修改打包 latest 镜像分支 2023-05-28 20:06:05 +08:00
whyour 6a52e2f804 修复 env.js 路径 2023-05-27 23:05:46 +08:00
whyour b4cb72c5d0 修改错误页面提示 2023-05-27 11:35:30 +08:00
whyour 79fd4ec58f 修复 nginx 变量替换 2023-05-26 17:08:40 +08:00
jzksnsjswkwandGitHub 49ed5c6632 修复 可执行文件 提示找不到文件的问题 (#1943) 2023-05-26 15:17:47 +08:00
whyour ffba7ee4a1 增加定时任务服务启动失败日志 2023-05-25 21:45:28 +08:00
whyour 91d080472f 修复环境变量接口前缀 2023-05-24 23:46:07 +08:00
whyour 826f214f0f 修复设置通知错误时提示不清晰 2023-05-24 23:37:44 +08:00
whyour ca150dc6a6 修复 docker-compose 配置 2023-05-22 20:20:46 +08:00
whyour ae5db60211 修复筛选可能报错 2023-05-21 12:29:29 +08:00
whyour 490bdc15f6 支持非根目录部署 2023-05-19 01:10:33 +08:00
whyour 1377fe31aa 更新版本 v2.15.14 2023-05-15 23:44:42 +08:00
whyour 3d1cf6bf2e 修改群晖名称 2023-05-15 23:44:32 +08:00
whyour 6ac755b478 修复后端 html 缓存 2023-05-15 22:34:06 +08:00
whyour e70fa83074 修复获取配置文件参数 2023-05-14 20:53:45 +08:00
whyour 41521f1d08 修复 token 任务脚本路径,容器启动 bot 2023-05-13 01:10:49 +08:00
whyour 8b7bef097c 修复拉取仓库逻辑 2023-05-12 18:28:01 +08:00
whyour 618756d4a9 修复设置代理时,服务启动失败 2023-05-10 23:17:05 +08:00
whyour 70d0c71414 修复错误日志样式 2023-05-10 23:01:51 +08:00
whyour 65eb9c806d 更新 sqlite 2023-05-10 21:58:11 +08:00
whyour 50718ba714 修复 public 服务转发 2023-05-10 01:07:46 +08:00
whyour 7c3bb90d12 修复删除依赖和移动环境变量接口数据结构 2023-05-08 21:29:25 +08:00
whyour f66390eabd 修改初始系统状态逻辑已经错误页面 2023-05-07 23:40:37 +08:00
whyour 481646501c 修复订阅更新仓库失败 2023-05-06 22:58:05 +08:00
whyour 5baa8e4391 增加 pm2 平滑重启 2023-05-06 00:09:03 +08:00
whyour 3d11210aeb pnpm 版本降至 8.3.1 2023-05-05 00:26:23 +08:00
whyour e1863407f2 修复 npm 源判断 2023-05-04 22:23:55 +08:00
whyour 0c28b63ab7 修改容器启动流程 2023-05-04 22:07:39 +08:00
whyour 8b8bd279c6 修复 nginx 转发stream接口失效,停止运行命令支持 pid 参数 2023-05-04 21:28:14 +08:00
whyour 01b404765e 更新版本 v2.15.13 2023-05-04 17:13:18 +08:00
whyour c231a871d7 修复 check 异常 2023-05-04 17:13:04 +08:00
whyour 598c6b4e57 修复 task 打印日志逻辑 2023-05-04 15:17:38 +08:00
whyour 2295fabcc5 修复 task 命令日志目录判断 2023-05-04 13:11:18 +08:00
whyour be94d796ac 增加 procps 依赖 2023-05-04 13:05:48 +08:00
whyour c84908d7fa 修复运行命令api任务日志路径 2023-05-04 10:25:04 +08:00
whyour d8ae039b92 修复内容区头部样式 2023-05-03 21:23:13 +08:00
whyour bce1431d03 修复错误界面重试次数判断 2023-05-03 19:59:06 +08:00
whyour 9db0095e29 修复系统通知错误提示,gotifyPriority 配置参数 2023-05-03 18:18:28 +08:00
whyour 77dc7817fb 修复 pnpm 8.4.0 重新安装依赖会默认进行询问 2023-05-03 01:58:41 +08:00
whyour d0dd97631e 修复 task 命令任务 ID 查询参数 2023-05-03 01:27:14 +08:00
whyour d11d6d0c18 增加容器健康检查 2023-05-02 22:11:50 +08:00
whyour 0af687f781 增加停止指定命令接口 2023-05-01 22:56:47 +08:00
whyour 8db997abe8 修改 check 逻辑 2023-05-01 16:16:47 +08:00
whyour aab6bbeb15 增加运行指定命令接口 2023-05-01 14:28:58 +08:00
whyour df1addc1ff 修改 task 命令超时时间判断 2023-05-01 11:05:36 +08:00
whyour 1f8f35476a 移除任务默认超时时间 2023-04-30 20:02:14 +08:00
whyour 5354fc76db 修复 task 命令任务ID查询,系统依赖安装逻辑 2023-04-30 19:35:14 +08:00
whyour b29a9e012c 修复停止任务无效 2023-04-29 22:49:50 +08:00
whyour 6e5d89c197 修改 task 命令生成日志逻辑和关联任务查询 2023-04-28 23:15:07 +08:00
whyour d6cfb18f06 修改仓库拉取状态判断逻辑 2023-04-24 15:37:58 +08:00
whyour 9a3e38051d 克隆仓库增加 depth 2023-04-24 14:47:17 +08:00
whyour b705ad6ee8 修复更新任务、环境变量、依赖、订阅状态丢失 2023-04-21 21:15:28 +08:00
whyour db564d1add 更新版本 v2.15.12 2023-04-18 15:38:06 +08:00
whyour 4f422c0658 修改 dockerfile LANG 环境变量 2023-04-18 15:37:43 +08:00
whyour f267720af4 修复初始化定时服务运行非禁用任务 2023-04-17 13:36:37 +08:00
whyour 4a0c66bcc0 修复初始化 ssh 配置文件不存在 2023-04-15 12:14:56 +08:00
whyour ef7283a9fd 修改依赖安装初始状态 2023-04-12 00:45:06 +08:00
whyour 4498449eea 修复容器启动逻辑 2023-04-11 14:15:34 +08:00
whyour ebb9676a51 修复定时服务启动路径 2023-04-08 21:58:34 +08:00
whyour 6fb39ce835 重构六位定时任务服务 2023-04-08 17:07:06 +08:00
whyour 59c26d90d3 修改手机端网页样式 2023-04-06 21:49:42 +08:00
whyour 1d2df860e8 修复 ssh 配置文件目录 2023-04-06 14:23:02 +08:00
whyour ce22cad5b2 修改列表页更多菜单弹窗 2023-04-06 14:08:08 +08:00
whyour dc44ce6b1a 修复 workflow pnpm 版本 2023-04-06 13:41:18 +08:00
whyour b27ee23cc3 重构私有仓库ssh配置逻辑 2023-04-06 13:38:55 +08:00
whyour 083c8869aa 修复更新环境变量、定时任务、订阅等操作 2023-04-06 13:00:28 +08:00
whyour 6a971a0d6e 更新 readme 2023-04-02 14:49:30 +08:00
whyour a25bfb6912 修复定时任务状态筛选 2023-04-02 14:42:18 +08:00
whyour f19dd21155 更新 readme 2023-04-01 21:05:21 +08:00
whyour 79b342d1d2 更新版本 v2.15.11 2023-03-31 22:38:32 +08:00
whyour 20b0d75b0d 修改 pnpm 版本 2023-03-30 23:20:38 +08:00
whyour c89724906e 修复定时任务表头样式 2023-03-23 22:19:48 +08:00
whyour c3a548ece3 修改定时任务列表滚动 2023-03-22 22:30:47 +08:00
whyour b431e39f8f 修复系统设置中登录日志样式 2023-03-20 23:23:03 +08:00
whyour 710746809b 修复删除视图筛选条件 2023-03-20 23:00:13 +08:00
whyour 8a225e9beb 修复视图筛选条件样式 2023-03-20 18:03:09 +08:00
whyour a5ecb204b2 修复 python Server酱推送 2023-03-19 22:54:50 +08:00
whyour f1f009da3b 禁止手机浏览器下拉刷新 2023-03-19 15:15:01 +08:00
whyour 7291098f98 修复手机页面滚动 2023-03-19 15:11:28 +08:00
whyour 666545ff03 修改初始化依赖安装逻辑 2023-03-18 22:26:29 +08:00
whyour 81e2bbb14c 更新版本 v2.15.10 2023-03-17 21:30:13 +08:00
whyour 970dff144c 修复订阅下载 http 协议文件 2023-03-15 21:44:11 +08:00
whyour 819b15b15e 容器增加 ipv6 检测,自动配置 nginx 2023-03-14 22:39:10 +08:00
whyour 3fa57c69ac 修改检查 pm2 状态 api 2023-03-13 23:57:56 +08:00
whyour 18689c8119 修复 shell 引入通用变量 2023-03-13 22:30:53 +08:00
adams549659584andGitHub 61e7049baa 修复系统 bark 推送铃声 (#1850) 2023-03-13 22:13:20 +08:00
whyour 39c40a328d 修复检查更新弹框逻辑 2023-03-12 21:48:31 +08:00
whyour 955d815d14 修改 task 获取 token 逻辑 2023-03-12 21:33:46 +08:00
adams549659584andGitHub 3a3b945de8 修复 js 下 bark 推送声音 bug (#1847) 2023-03-12 20:58:26 +08:00
whyour 73c53dbc8d 修改 typescript execute 2023-03-12 20:53:15 +08:00
whyour f76828d4ec 修改 shell 文件权限 2023-03-11 01:10:03 +08:00
whyour 7b2956eb0a 修复日志删除逻辑 2023-03-10 14:21:52 +08:00
whyour b2167ae5e4 修复自定义通知 2023-03-07 22:20:08 +08:00
whyour 98dfa4c425 更新版本 v2.15.9 2023-03-04 15:48:29 +08:00
whyour 5bf08b76de 修复系统更新源判断,回退 @monaco-editor/react 2023-03-03 23:45:38 +08:00
whyour 5c5b4f804e 更新 antd 2023-03-03 22:52:44 +08:00
whyour 6455245d11 更新 @umijs/max 2023-03-03 22:14:03 +08:00
pharaoh2012andGitHub 2ff3d9c601 根据标题跳过消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔 (#1833) 2023-03-03 21:59:44 +08:00
whyour d342fbf29f 修改登录日志表格样式 2023-03-03 11:58:06 +08:00
whyour f865018cd6 修改表格样式 2023-03-02 23:31:46 +08:00
whyour 3b9a4f0834 修复定时任务状态筛选 2023-03-02 23:16:18 +08:00
whyour 67bc305950 修复拉取仓库重置操作 2023-03-01 22:12:49 +08:00
whyour 97f128d59f 修改 ql extra 执行逻辑 2023-02-28 22:56:06 +08:00
whyour e25885c4d9 修改 nginx ipv6 配置 2023-02-27 23:30:00 +08:00
whyour c285026339 修改删除日志逻辑 2023-02-27 23:25:08 +08:00
whyour 6c34045a48 修改订阅名称必填 2023-02-26 23:36:32 +08:00
whyour e1e6261b4f 修复定时任务/视图/环境变量更新逻辑 2023-02-24 23:21:08 +08:00
whyour 697bcb5922 暂时去掉 vlist 2023-02-24 23:04:18 +08:00
whyour e05b0a2491 修复文件类型订阅重复添加任务 2023-02-18 14:38:13 +08:00
whyour e1655455d8 更新版本 v2.15.8 2023-02-17 23:11:41 +08:00
whyour 9921dab064 修复日志文件名格式化 2023-02-17 23:03:06 +08:00
whyour 403ab7f0f0 修改日志名称格式 2023-02-17 22:49:06 +08:00
whyour 9bc7ca4094 增加 NODE_PATH 环境变量 2023-02-15 22:27:40 +08:00
whyour e924aa76b0 修复环境变量位置重置逻辑 2023-02-15 11:54:51 +08:00
whyour 0c46606e14 修复添加文件类型订阅 2023-02-14 21:15:59 +08:00
whyour dbfa4049f5 修复订阅格式化参数 2023-02-14 11:10:25 +08:00
whyour 622fe2a8f8 修复订阅自动增加/删除任务默认值 2023-02-14 11:07:16 +08:00
whyour 7bce5c4f6a 任务增加关联订阅 2023-02-13 23:50:01 +08:00
whyour 1f7f2c8971 修复订阅生成 ssh 配置逻辑,自动添加/删除任务 2023-02-13 23:12:55 +08:00
whyour 437a849c2a 更新版本 v2.15.7 2023-02-12 17:09:07 +08:00
whyour 8f7261dd50 修复 workflow 2023-02-12 17:08:47 +08:00
whyour 76fa82c3a7 订阅支持自动添加和删除任务设置 2023-02-09 00:20:28 +08:00
whyour 46c9774efa 更新 workflow 2023-02-07 21:52:24 +08:00
whyour 1bc64a23e3 修复查看任务日志,列表位置重置 2023-02-07 21:29:56 +08:00
whyour cc9a17afa3 修复定时任务列表格式化 crontab 2023-02-06 23:26:59 +08:00
whyour fcbcfc3a03 更新 sqlite3 2023-02-06 16:24:31 +08:00
whyour 0144f3b131 修改任务日志自动滚动逻辑 2023-02-05 20:57:37 +08:00
whyour 518436d5ca 修复脚本管理跳转逻辑 2023-02-04 00:20:44 +08:00
whyour 1961a6b913 修复定时任务表格自动换行 2023-02-03 23:45:55 +08:00
whyour 5edebdd5f3 修复创建日志目录逻辑 2023-02-03 23:30:02 +08:00
whyour 48edf2c212 增加 ppc64le/s390x/386 镜像 2023-02-03 22:38:00 +08:00
whyour 9b16dca9da 修改定时任务状态宽度 2023-02-02 23:39:18 +08:00
whyour 9479c2498c 移除 console 2023-02-02 13:20:47 +08:00
whyour 4a18bcabe2 修复移动端新建脚本 2023-02-02 13:17:00 +08:00
whyour 34f690c53d 修改 shell 参数处理逻辑 2023-02-02 12:14:35 +08:00
whyour e70cd6579f 更新版本 v2.15.6 2023-02-01 22:46:23 +08:00
whyour 99c047e9c7 修复定时任务表格滚动 2023-02-01 22:45:47 +08:00
whyour 978ae326f8 更新 sqlite3 依赖 2023-02-01 15:30:53 +08:00
whyour b01f27f2d3 修改更新列表后刷新逻辑 2023-01-31 23:52:02 +08:00
whyour 4d34e0dd96 修复环境变量滚动卡顿 2023-01-31 23:46:42 +08:00
whyour 9b0065e533 修复定时任务过多搜索卡顿 2023-01-31 23:30:47 +08:00
whyour 34a51feee9 修改定时任务列表title 2023-01-31 17:42:30 +08:00
whyour 679091de59 修改定时任务输入提示 2023-01-31 17:41:35 +08:00
whyour c6220cb920 修改定时任务查询日志逻辑 2023-01-29 18:07:57 +08:00
whyour 633b30269c 修复定时任务脚本跳转 2023-01-29 10:57:08 +08:00
whyour 8ad26267d8 修改 task before/after 逻辑 2023-01-20 23:38:32 +08:00
OceanJiangandGitHub 7cfb125682 实现 Node.js 部分 SMTP 推送 (#1776) 2023-01-14 19:53:36 +08:00
Wankko ReeandGitHub a2dc5de5ce 添加基于 SMTP 邮件 的消息通知服务 (#1763) 2023-01-14 19:48:41 +08:00
whyour 4f41ff29ec 修改资源通知逻辑 2023-01-14 19:46:33 +08:00
whyour 13d95b8c19 修复日志滚动逻辑 2023-01-11 23:46:47 +08:00
whyour 335c13ab44 修改日志滚动逻辑 2023-01-11 23:44:04 +08:00
whyour 015cdc10a7 publicPath使用相对目录 2023-01-11 22:52:51 +08:00
whyour a5075ab28f 修改新建任务命令提示 2023-01-11 22:14:32 +08:00
whyour 2e3c3274c7 修复antd兼容性,日志详情自动滚动 2023-01-11 22:03:44 +08:00
whyour 443c581955 修改定时任务 shouldCellUpdate 2023-01-08 22:30:29 +08:00
whyour 5e41faf590 修改 monaco-editor 版本 2023-01-08 22:16:22 +08:00
whyour 0579fb83d3 修改环境变量值省略展示 2023-01-08 21:56:29 +08:00
whyour 2864845d5b 优化定时任务表格渲染 2023-01-08 18:15:18 +08:00
whyour 659dddace7 梅子留酸软齿牙,芭蕉分绿与窗纱。 2023-01-08 16:49:21 +08:00
whyour b096f30558 更新 dockerfile 2023-01-08 16:39:01 +08:00
whyour dcfb72143a 修复定时任务每页数据量大时搜索卡顿 2023-01-07 18:19:03 +08:00
whyour c63bcd7706 修改docker编译 2023-01-07 18:16:39 +08:00
whyour bac306f9b3 更新版本 v2.15.5 2023-01-04 21:09:52 +08:00
whyour a6fd96e97b 修复定时规则修改 2023-01-04 21:09:27 +08:00
whyour 7edd91f923 修改服务异常判断逻辑 2023-01-04 19:27:15 +08:00
whyour 66df445bd5 修改添加任务命令提示 2023-01-04 17:28:26 +08:00
whyour 5c03034bb4 修复定时任务不以task开头时,任务无效 2023-01-04 17:25:30 +08:00
whyour 0eab181d46 修改update逻辑 2022-12-29 12:15:25 +08:00
whyour f6f95308c7 monaco editor改为本地加载 2022-12-28 22:12:37 +08:00
whyour e93cad91b4 更新版本 v2.15.4 2022-12-28 11:15:20 +08:00
whyour 08ee61b179 修改 pub 脚本 2022-12-28 11:14:57 +08:00
whyour 0ab756665e 修改版本文件 2022-12-28 11:06:47 +08:00
whyour 3570cddce0 关于增加更新日志查看 2022-12-27 17:10:21 +08:00
whyour a2d9f1a5db 脚本管理支持重命名文件/文件夹 2022-12-27 16:02:15 +08:00
whyour 0c6a214e55 修复启动public服务命令 2022-12-27 12:24:24 +08:00
whyour 095fcdbe69 修复任务脚本快捷跳转 2022-12-17 12:46:26 +08:00
whyour 3afb1d18f6 更新版本 v2.15.3 2022-12-10 19:06:39 +08:00
whyour 4526c84330 任务视图支持标签筛选 2022-12-10 19:00:03 +08:00
whyour a9cc1cb4b9 修改token获取逻辑 2022-12-10 17:12:45 +08:00
whyour 7acf1eace3 修改添加任务视图筛选条件交互 2022-12-09 23:34:23 +08:00
whyour bb2e8cb287 修复调试保存文件默认目录和任务视图创建 2022-12-09 22:59:38 +08:00
whyour 3b389259c1 修复调试脚本日志 2022-12-06 00:22:05 +08:00
whyour 310ce55b09 修改默认基础镜像 2022-12-05 23:49:33 +08:00
CoolBoyandwhyour 4662e3fec2 修复notify.js中智能微秘书 (#1736) 2022-12-05 23:44:34 +08:00
whyour 10fab9d415 修改进程默认退出信号 2022-12-05 21:00:14 +08:00
whyour acecf01cbe 修复 killTask 返回值 2022-12-05 15:38:21 +08:00
whyour b95fb9cda4 修改退出进程逻辑 2022-12-05 15:26:22 +08:00
whyour 23fd595582 修改 timeout 命令默认信号 2022-12-05 13:18:10 +08:00
whyour 468d05cb10 更新版本 v2.15.2 2022-12-02 18:07:54 +08:00
whyour 88c2e5d160 更新 readme 2022-12-02 18:07:21 +08:00
whyour 8cf491b802 task 命令增加 -m 参数,支持设置任务超时时间 2022-12-01 18:26:24 +08:00
whyour 5886e78299 修复获取更新分支 2022-12-01 16:25:02 +08:00
whyour 591ef9781c 修改获取 cpu 占用 2022-12-01 16:16:17 +08:00
whyour 52fef96c1c 修改check逻辑 2022-11-30 21:29:58 +08:00
whyour d5fd9c57ec 修复shell语法 2022-11-29 22:50:11 +08:00
whyour 8a7d6a1c13 修改资源检查逻辑 2022-11-29 21:01:28 +08:00
whyour 1745c72981 修改 check/update 逻辑 2022-11-29 00:05:50 +08:00
whyour b98b7646cb 修复日志样式 2022-11-28 23:59:23 +08:00
whyour 1446e925ec 任务视图增加系统视图 2022-11-26 20:34:07 +08:00
whyour 799cda1e3e 移除未使用依赖 2022-11-24 22:09:33 +08:00
whyour d2e0231341 修改 workflow node_modules 缓存 2022-11-22 21:33:03 +08:00
whyour 9e0aa655a0 修改 workflow 依赖缓存 2022-11-22 21:00:53 +08:00
whyour e303c73d44 workflow 增加 digest 打印 2022-11-22 20:03:56 +08:00
whyour e5e8e66d6b 增加 docker action 缓存 2022-11-22 19:53:02 +08:00
whyour 0bb3381122 修复拉去仓库逻辑 2022-11-22 16:51:57 +08:00
whyour 2c5bf2bd6b 更新 readme 2022-11-22 14:42:27 +08:00
whyour 83763b00ae 上传 pnpm-lock 2022-11-22 13:41:39 +08:00
whyour 47fc795c97 增加action缓存 2022-11-22 13:37:44 +08:00
whyour 6a59bf7af3 更新版本 v2.15.1 2022-11-20 21:04:58 +08:00
whyour 6f185570d6 pushDeer支持自架版 2022-11-20 20:05:02 +08:00
whyour 8b8336d9d0 系统升级增加gitee源 2022-11-17 22:01:47 +08:00
whyour a1e1d6ed3f 更新 readme 2022-11-17 11:47:44 +08:00
whyour df3e209534 修改 workflow 2022-11-17 11:37:45 +08:00
whyour 851408860e 修复 workflow 2022-11-17 11:18:15 +08:00
whyour d92ea23b0e 修改 workflow 2022-11-17 11:11:05 +08:00
whyour d83641db92 修复workflow 2022-11-17 00:28:28 +08:00
whyour 48f262e1f3 增加 workflow 2022-11-17 00:23:24 +08:00
whyour bc49a7db50 同步gitee 2022-11-16 23:04:39 +08:00
whyour 49975b461c 修改typescript版本 2022-11-16 22:52:34 +08:00
whyour 3ccb635956 修复移动端最后更新时间展示 2022-11-16 22:34:28 +08:00
whyour 074a90e0ac 修改同步gitlab私钥 2022-11-16 00:02:40 +08:00
whyour 526f927436 增加 gitlab workflow 2022-11-15 23:55:28 +08:00
whyour fae7fad915 更新版本 v2.15.0 2022-11-15 12:15:06 +08:00
whyour 56132ceb8b 修复初始化新增字段 2022-11-15 12:14:33 +08:00
whyour 3c3fce0a5a 修复初始化表字段 2022-11-15 11:32:42 +08:00
whyour 8139384961 移除filterRelation默认值 2022-11-14 23:31:27 +08:00
whyour 734cc94048 修改filterRelation字段限制 2022-11-14 22:59:01 +08:00
whyour 91dbb7770d 修改订阅github ssh生成逻辑 2022-11-14 22:00:16 +08:00
whyour 707709837d 初始化更新不存在filterRelation的任务视图 2022-11-14 21:36:31 +08:00
whyour e868e65208 修改日志管理默认排序 2022-11-14 15:12:17 +08:00
whyour b19ad2a13b 修复debounce方法引入错误 2022-11-14 11:35:51 +08:00
whyour 98ccccf6ab 修复openapi token生成逻辑 2022-11-14 09:28:30 +08:00
whyour 0a6166c557 订阅增加代理参数 2022-11-13 23:58:42 +08:00
whyour 5ee00e52a8 修复新建视图筛选条件样式 2022-11-13 22:12:03 +08:00
whyour ff2b4e0b2f 订阅支持添加代理 2022-11-12 22:45:46 +08:00
whyour 8fdd8db51b 修改创建任务视图样式 2022-11-11 15:03:27 +08:00
whyour f113eb7ba8 修改系统设置样式 2022-11-11 14:55:09 +08:00
whyour 40bc25ed5f 修复获取最后commit时间 2022-11-10 10:52:57 +08:00
whyour cd3eab5d35 修复版本信息展示 2022-11-10 01:34:18 +08:00
whyour 7038e15ad2 支持定时任务视图筛选条件关系切换 2022-11-10 01:31:21 +08:00
whyour c72abd29ec 任务视图筛选条件增加条件关系 2022-11-09 20:43:22 +08:00
whyour 5efc3d2228 更新版本 v2.14.12 2022-11-05 16:43:28 +08:00
whyour 8ac3f83c79 修复可能出现移动或者创建环境变量出错 2022-11-05 16:41:19 +08:00
whyour b31b054d0c 更新版本 v2.14.11 2022-11-05 15:24:16 +08:00
whyour fb4a87f5ce 修复版本文件缓存 2022-11-05 15:23:17 +08:00
whyour 049c73880b 修改pre样式 2022-11-05 14:57:21 +08:00
whyour 7b2c54f6a6 修改版本信息获取 2022-11-05 12:14:46 +08:00
whyour 23bda39812 关于增加版本信息展示 2022-11-05 11:01:58 +08:00
whyour 90af5801ee 修复表格滚动 2022-11-04 19:14:03 +08:00
whyour 543c241bc6 更新版本 v2.14.10 2022-11-03 21:16:46 +08:00
whyour 19a58a3510 环境变量值增加复制功能 2022-11-03 17:34:36 +08:00
whyour 73c6fd18ed 修复资源预警通知 2022-11-03 13:42:14 +08:00
whyour 409297c6aa 修改调试日志样式 2022-11-02 19:54:49 +08:00
whyour 4272b9ea35 修复task命令参数 2022-11-02 19:40:16 +08:00
whyour 3c5c5b0bbc 修复&符号转义 2022-11-02 19:25:03 +08:00
whyour 802b2d722e 修复任务命令中的&和引号 2022-11-02 18:51:09 +08:00
whyour 8f90d3f8ff 修复环境变量位置计算逻辑 2022-11-02 00:02:15 +08:00
whyour f93cbbf508 修复 notify.js 飞书通知 2022-10-30 18:59:48 +08:00
whyour 2ba8756bd3 修改cdn协议 2022-10-30 12:00:12 +08:00
whyour 8013de56c9 修改sqlite配置 2022-10-30 10:54:39 +08:00
whyour 4af9b22442 更新版本 v2.14.9 2022-10-29 18:50:16 +08:00
whyour 8a392170cf webhook通知支持嵌套body 2022-10-29 18:43:16 +08:00
whyour 6385e053ae 增加飞书机器人通知 2022-10-29 16:41:37 +08:00
Leo_chenandGitHub 2789119882 添加智能微秘书通知 (#1680) 2022-10-29 15:45:16 +08:00
whyour 3e0553c3e8 修改登陆逻辑 2022-10-29 10:05:54 +08:00
whyour 5e16ace1e7 修改检查更新日志 2022-10-27 10:34:55 +08:00
whyour 0ca23cc4ca 修改环境变量默认排序 2022-10-27 00:02:50 +08:00
whyour 4949457001 修复ql命令日志打印 2022-10-26 23:50:07 +08:00
whyour 8ae13fe823 增加 -max 指定环境变量 2022-10-26 14:27:14 +08:00
whyour fdf672092a 修改获取version文件超时时间 2022-10-25 23:52:30 +08:00
whyour 48dd6ea826 修复表格滚动高度 2022-10-25 23:12:38 +08:00
whyour 68bb4f15e2 修复shell获取磁盘使用量 2022-10-24 22:45:06 +08:00
whyour 9c6d9ec22d 更新版本 v2.14.8 2022-10-22 18:36:38 +08:00
whyour ac075ace73 修复调试脚本语言判断 2022-10-22 00:31:34 +08:00
whyour eb58774ee9 优化task.sh 2022-10-22 00:25:54 +08:00
whyour 04613bf9a9 修复tsconfig 2022-10-20 15:30:22 +08:00
whyour 4194f1964d 修改日志结束符判定 2022-10-20 15:16:17 +08:00
whyour 42c64c82d7 修复 ql 命令日志目录 2022-10-16 21:59:19 +08:00
whyour d44a8966f2 修复环境变量名称验证 2022-10-16 09:22:37 +08:00
whyour ff44330aa1 修复 ql 命令日志目录 2022-10-15 18:40:15 +08:00
whyour ed259e8168 修复 task 命令执行时参数引号丢失 2022-10-15 18:29:34 +08:00
whyour 444836975d 修改 ql 命令打印日志逻辑 2022-10-14 23:02:20 +08:00
whyour 3756fa93e4 修复 ql 命令 2022-10-14 21:17:04 +08:00
whyour 064b52b306 修改 timezone action 2022-10-14 20:55:58 +08:00
whyour 648dc51474 更新ql指令 2022-10-14 15:17:37 +08:00
whyour 7c0bc32759 修改环境变量导入 2022-10-13 23:41:54 +08:00
whyour d4b6cc5e4d 修复调试脚本选择脚本 2022-10-13 23:39:28 +08:00
whyour 75fca715e3 增加环境变量导入 2022-10-11 22:16:36 +08:00
whyour 6e9a4a29ef 修复shell语法 2022-10-09 13:49:19 +08:00
whyour 78f643c50f 更新版本v2.14.7 2022-10-01 23:12:57 +08:00
whyour 2db4f1ba87 修改资源判定逻辑 2022-10-01 23:04:12 +08:00
whyour 5741dbb78c 修复获取task_before参数 2022-09-30 15:32:42 +08:00
whyour 88cf671465 更新版本v2.14.6 2022-09-29 23:06:31 +08:00
whyour 29f082dea2 增加config资源配置 2022-09-29 23:03:22 +08:00
whyour 9a55968ed6 修改资源异常推送 2022-09-29 13:29:22 +08:00
whyour 6ec6e25855 修改资源提示 2022-09-29 00:59:27 +08:00
whyour af48bc378b 修复获取cpu占用 2022-09-29 00:57:31 +08:00
whyour 3f5ae4bcb9 修复 echo 2022-09-29 00:44:07 +08:00
whyour c6624e9001 增加资源占用提示 2022-09-29 00:43:07 +08:00
whyour 5530ce76e3 添加系统资源消耗提示 2022-09-29 00:32:17 +08:00
whyour 654b51e476 修复脚本和日志搜索逻辑 2022-09-25 21:03:11 +08:00
whyour 6428ac3624 修复SplitPane类型错误 2022-09-25 14:02:27 +08:00
whyour c36450f436 修复ant-tree高度获取 2022-09-25 13:38:10 +08:00
whyour dcea231249 更新版本 v2.14.5 2022-09-24 21:00:47 +08:00
whyour f236119bd6 修改输入框自动填充样式 2022-09-24 14:40:13 +08:00
whyour cb870fad85 修改检查更新逻辑 2022-09-24 14:20:35 +08:00
whyour 44e2caedfc 修改更新日志清除评率 2022-09-24 13:59:56 +08:00
whyour c0b5192296 修改移动端脚本高亮 2022-09-24 11:27:16 +08:00
whyour ba43882c0b 修复脚本管理删除 2022-09-23 23:16:57 +08:00
whyour dfc706e16d 日志管理支持删除日志文件和目录 2022-09-23 20:09:10 +08:00
whyour 25b03d4345 脚本管理支持删除文件夹 2022-09-23 19:12:51 +08:00
whyour 4e8f36d9a4 修复登录错误提示 2022-09-23 17:44:48 +08:00
whyour 8f4d67ffa7 修改task命令ID获取 2022-09-23 14:37:56 +08:00
whyour ad47277149 修复登录验证 2022-09-23 12:15:15 +08:00
whyour 98c2a37ab6 修复初始化逻辑 2022-09-23 00:17:53 +08:00
whyour f2fea47336 重构前端错误提示 2022-09-22 23:59:23 +08:00
whyour e274d3e2f9 sentry增加版本号 2022-09-22 12:14:42 +08:00
whyour 36f4c3c02c 修改任务启动参数 2022-09-21 21:28:31 +08:00
whyour ae891f8e55 修复docker启动逻辑 2022-09-21 17:42:13 +08:00
whyour c874dc9705 更新readme 2022-09-21 16:03:53 +08:00
whyour 23bfbeb995 修改新建订阅提示 2022-09-21 14:54:45 +08:00
whyour a75df03243 更新版本 2.14.4 2022-09-20 19:59:51 +08:00
whyour e44a2b5995 修改tsconfig 2022-09-20 19:43:49 +08:00
whyour f23e7a9e49 修复自定义通知 2022-09-20 19:13:11 +08:00
whyour e08f0cd308 修复设置自定义通知 2022-09-20 18:00:36 +08:00
whyour 1ebcfd8001 修复自定义通知 2022-09-20 17:34:36 +08:00
whyour 5168b044a3 修复遍历key值 2022-09-20 01:23:33 +08:00
whyour 931638b687 增加自定义hook通知 2022-09-20 01:16:36 +08:00
whyour 4d9bbcfd3f 升级antd 2022-09-19 18:45:56 +08:00
whyour b0e3b26369 修改环境变量默认排序 2022-09-18 20:47:42 +08:00
whyour 1e55f4065d 升级umi4.0,修复新建脚本 2022-09-18 20:40:59 +08:00
whyour 518e8903a3 兼容Modal属性 2022-09-17 22:50:58 +08:00
whyour 6e56a55f6f 修改弹框表单逻辑 2022-09-16 00:28:04 +08:00
whyour 967071ad4e 脚本管理支持新增文件夹 2022-09-16 00:14:10 +08:00
whyour d23fcfaa5a 修复创建应用 2022-09-15 23:45:12 +08:00
whyour a2f2306430 增加批量重新安装依赖 2022-09-11 22:47:43 +08:00
whyour 144b543091 修改依赖管理默认排序 2022-09-11 22:26:33 +08:00
whyour e4ad8f512e 修改订阅默认排序 2022-09-11 22:10:17 +08:00
whyour 591a5006da 修复运行任务shell 2022-09-11 19:53:57 +08:00
whyour 281ed6cfba 修改任务执行日志目录规则 2022-09-11 19:43:37 +08:00
whyour 7354a61184 修改config配置说明 2022-09-10 14:23:16 +08:00
whyour 04037f29e5 更新版本 v2.14.3 2022-09-10 14:12:26 +08:00
whyour 6b124bad89 修复pnpm registry兼容 2022-09-09 00:21:10 +08:00
155 changed files with 24943 additions and 4972 deletions
+1
View File
@@ -10,3 +10,4 @@ SECRET='whyour'
QINIU_AK=''
QINIU_SK=''
QINIU_SCOPE=''
TEMP=''
+70 -17
View File
@@ -5,28 +5,55 @@ on:
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
- cron: '00 14 * * *' # GMT 15:00 => 北京时间 23:00
workflow_dispatch:
jobs:
to_gitlab:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url:
git@gitlab.com:whyour/qinglong.git
ssh_private_key:
${{ secrets.GITLAB_SSH_PK }}
to_gitee:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url:
git@gitee.com:whyour/qinglong.git
ssh_private_key:
${{ secrets.GITLAB_SSH_PK }}
build-static:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: '8.3.1'
- uses: actions/setup-node@v3
with:
cache: 'pnpm'
- name: build front and back
run: |
npm i -g pnpm
pnpm install
pnpm install --frozen-lockfile
pnpm build:front
pnpm build:back
@@ -34,6 +61,9 @@ jobs:
env:
GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static
GITHUB_BRANCH: ${{ github.ref_name }}
REPO_GITEE: git@gitee.com:whyour/qinglong-static.git
REPO_GITLAB: git@gitlab.com:whyour/qinglong-static.git
PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}
run: |
mkdir -p tmp
cd ./tmp
@@ -44,6 +74,16 @@ jobs:
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}
mkdir -p ~/.ssh
echo "${PRIVATE_KEY}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
export GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no -l git"
git remote add gitee "${REPO_GITEE}"
git remote add gitlab "${REPO_GITLAB}"
git push --force --quiet gitee ${GITHUB_BRANCH}:${GITHUB_BRANCH}
git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH}
build:
needs: build-static
@@ -55,14 +95,17 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- name: Set time zone
uses: szenius/set-timezone@v1.0
- uses: pnpm/action-setup@v2
with:
timezoneLinux: "Asia/Shanghai"
timezoneMacos: "Asia/Shanghai"
timezoneWindows: "China Standard Time"
version: '8.3.1'
- uses: actions/setup-node@v3
with:
cache: 'pnpm'
- name: Setup timezone
uses: zcong1993/setup-timezone@master
with:
timezone: Asia/Shanghai
- name: Login to DockerHub
uses: docker/login-action@v2
@@ -86,11 +129,14 @@ jobs:
ghcr.io/${{ github.repository }}
# generate Docker tags based on the following events/attributes
# nightly, master, pr-2, 1.2.3, 1.2, 1
flavor: |
latest=false
tags: |
type=schedule,pattern=nightly
type=edge
type=ref,event=branch
type=ref,event=pr
type=ref,event=branch,enable=${{ github.ref != format('refs/heads/{0}', 'master') }}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
@@ -108,9 +154,16 @@ jobs:
build-args: |
MAINTAINER=${{ github.repository_owner }}
QL_BRANCH=${{ github.ref_name }}
SOURCE_COMMIT=${{ github.sha }}
network: host
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
context: docker/
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386
context: .
file: ./docker/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=whyour/qinglong:cache
cache-to: type=registry,ref=whyour/qinglong:cache,mode=max
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
+1 -1
View File
@@ -5,7 +5,6 @@
/npm-debug.log*
/yarn-error.log
/yarn.lock
/pnpm-lock.yaml
/package-lock.json
# production
@@ -30,3 +29,4 @@
/manual_log
/scripts
/bak
/.tmp
+18 -18
View File
@@ -1,32 +1,31 @@
import { defineConfig } from 'umi';
import { defineConfig } from '@umijs/max';
const CompressionPlugin = require('compression-webpack-plugin');
const baseUrl = process.env.QlBaseUrl || '/';
export default defineConfig({
hash: true,
layout: false,
antd: {},
outputPath: 'static/dist',
nodeModulesTransform: {
type: 'none',
fastRefresh: true,
favicons: [`https://qn.whyour.cn/favicon.svg`],
mfsu: {
strategy: 'eager',
},
fastRefresh: {},
esbuild: {},
webpack5: {},
dynamicImport: {
loading: '@/components/pageLoading',
},
favicon: '/images/favicon.svg',
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
proxy: {
'/api/public': {
[`${baseUrl}api/public`]: {
target: 'http://127.0.0.1:5400/',
changeOrigin: true,
pathRewrite: { [`^${baseUrl}api/public`]: '/api' },
},
'/api': {
[`${baseUrl}api`]: {
target: 'http://127.0.0.1:5600/',
changeOrigin: true,
ws: true,
pathRewrite: { [`^${baseUrl}api`]: '/api' },
},
},
chainWebpack: (config) => {
chainWebpack: ((config: any) => {
config.plugin('compression-webpack-plugin').use(
new CompressionPlugin({
algorithm: 'gzip',
@@ -35,13 +34,14 @@ export default defineConfig({
minRatio: 0.6,
}),
);
},
}) as any,
externals: {
react: 'window.React',
'react-dom': 'window.ReactDOM',
},
scripts: [
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
headScripts: [
`./api/env.js`,
'https://gw.alipayobjects.com/os/lib/react/18.2.0/umd/react.production.min.js',
'https://gw.alipayobjects.com/os/lib/react-dom/18.2.0/umd/react-dom.production.min.js',
],
});
+75 -94
View File
@@ -1,30 +1,29 @@
<p align="center">
<a href="https://github.com/whyour/qinglong">
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
</a>
</p>
<h1 align="center">Green Dragon</h1>
<div align="center">
<img width="100" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
Timed task management panel with python3, javaScript, shell, typescript support
<h1 align="center">Qinglong</h1>
[![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]
[简体中文](./README.md) | English
Timed task management platform supporting Python3, JavaScript, Shell, Typescript
[![npm version][npm-version-image]][npm-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]
[npm-version-image]: https://img.shields.io/npm/v/@whyour/qinglong?style=flat
[npm-version-url]: https://www.npmjs.com/package/@whyour/qinglong?activeTab=readme
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
[docker-version-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=flat
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
[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
[Demo](http://demo.dlww.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
[演示](http://demo.dlww.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
</div>
[![](https://user-images.githubusercontent.com/22700758/161788855-c4e51cb8-d4e9-44fe-bb17-ee1a56c8549b.png)](https://whyour.cn)
[简体中文](./README.md) | English
![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png)
## Features
@@ -36,65 +35,30 @@ Timed task management panel with python3, javaScript, shell, typescript support
- Support dark mode
- Support cell phone operation
## Version
### docker
The `latest` image is built on `alpine` and the `debian` image is built on `debian-slim`. If you need to use a dependency that is not supported by `alpine`, it is recommended that you use the `debian` image.
```bash
docker pull whyour/qinglong:latest
docker pull whyour/qinglong:debian
```
### npm
The npm version supports `debian/ubuntu/centos/alpine` systems and requires `node/python3` to be installed.
```bash
npm i @whyour/qinglong
```
## Deployment
### Local Deployment
```bash
# To be completed
```
### Podman Deployment
1. podman installation
```bash
https://podman.io/getting-started/installation
```
2. start the container
```bash
podman run -dit \
--network bridge \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
docker.io/whyour/qinglong:latest
```
### Docker Deployment
1. docker installation
```bash
sudo curl -sSL get.docker.com | sh
```
2. configure domestic mirror sources
```bash
mkdir -p /etc/docker
tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
"https://ypzju6vq.mirror.aliyuncs.com",
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://docker.mirrors.ustc.edu.cn"
]
}
EOF
systemctl daemon-reload
systemctl restart docker
```
3. start the container
### Docker (Recommended)
```bash
# curl -sSL get.docker.com | sh
docker run -dit \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
@@ -104,17 +68,10 @@ docker run -dit \
whyour/qinglong:latest
```
### Docker-compose Deployment
1. docker-compose installation
```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. start the container
### Docker-compose (Recommended)
```bash
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
@@ -124,9 +81,32 @@ docker-compose up -d
docker-compose down
```
3. access
### Podman (Recommended)
Open your browser and visit http://{ip}:5700
```bash
# https://podman.io/getting-started/installation
podman run -dit \
--network bridge \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
docker.io/whyour/qinglong:latest
```
### Local
It is recommended to use a pure system installation to avoid losing the original system data, you need to install node/npm/python3/pip3 yourself
```bash
npm install -g @whyour/qinglong
qinglong
# Add the environment variables QL_DIR and QL_DATA_DIR when prompted
export QL_DIR=""
export QL_DATA_DIR=""
# Run again
qinglong
```
## Use
@@ -162,6 +142,10 @@ task <file_path> now
task <file_path> conc <env_name> <account_number>(Optional)
# Specify the account to execute and run immediately regardless of whether a random delay is set
task <file_path> desi <env_name> <account_number>
# Set task timeout
task -m <max_time> <file_path>
# Print task log in real time, no need to carry this parameter when creating timed tasks
task -l <file_path>
```
2. parameter description
@@ -176,16 +160,7 @@ task <file_path> desi <env_name> <account_number>
* file_path: File path for task execution
* env_name: The name of the environment variable that needs to be concurrent or specified at the time of task execution
* account_number: Specify the account number of an environment variable to be executed when the task is executed
## Links
- [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)
* max_time: Timeout, suffix "s" for seconds (default), "m" for minutes, "h" for hours, "d" for days
## Development
@@ -194,16 +169,22 @@ $ git clone git@github.com:whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
# Recommended use pnpm https://pnpm.io/zh/installation
$ npm install -g pnpm
$ npm install -g pnpm@8.3.1
$ pnpm install
$ pnpm start
```
Open your browser and visit http://127.0.0.1:5700
## Communication
## Links
[telegram channel](https://t.me/jiao_long)
- [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/)
- [Umijs](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server)
## Name Origin
+79 -93
View File
@@ -1,30 +1,31 @@
<p align="center">
<a href="https://github.com/whyour/qinglong">
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
</a>
</p>
<div align="center">
<img width="100" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
<h1 align="center">青龙</h1>
<div align="center">
简体中文 | [English](./README-en.md)
支持python3、javaScript、shell、typescript 的定时任务管理面板
支持 Python3、JavaScript、Shell、Typescript 的定时任务管理平台
[![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]
Timed task management platform supporting Python3, JavaScript, Shell, Typescript
[![npm version][npm-version-image]][npm-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]
[npm-version-image]: https://img.shields.io/npm/v/@whyour/qinglong?style=flat
[npm-version-url]: https://www.npmjs.com/package/@whyour/qinglong?activeTab=readme
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
[docker-version-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=flat
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
[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
[Demo](http://demo.dlww.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
[演示](http://demo.dlww.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
</div>
[![](https://user-images.githubusercontent.com/22700758/161788855-c4e51cb8-d4e9-44fe-bb17-ee1a56c8549b.png)](https://whyour.cn)
简体中文 | [English](./README-en.md)
![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png)
## 功能
@@ -36,86 +37,45 @@
- 支持暗黑模式
- 支持手机端操作
## 版本
### docker
`latest` 镜像是基于 `alpine` 构建,`debian` 镜像是基于 `debian-slim` 构建。如果需要使用 `alpine` 不支持的依赖,建议使用 `debian` 镜像
```bash
docker pull whyour/qinglong:latest
docker pull whyour/qinglong:debian
```
### npm
npm 版本支持 `debian/ubuntu/centos/alpine` 系统,需要自行安装 `node/python3`
```bash
npm i @whyour/qinglong
```
## 部署
### 本机部署
```bash
# 待完善
```
### podman 部署
1. podman 安装
```bash
https://podman.io/getting-started/installation
```
2. 启动容器
```bash
podman run -dit \
--network bridge \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
docker.io/whyour/qinglong:latest
```
### docker 部署
1. docker 安装
```bash
sudo curl -sSL get.docker.com | sh
```
2. 配置国内镜像源
Configure domestic mirror sources
```bash
mkdir -p /etc/docker
tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
"https://ypzju6vq.mirror.aliyuncs.com",
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://docker.mirrors.ustc.edu.cn"
]
}
EOF
systemctl daemon-reload
systemctl restart docker
```
3. 启动容器
### docker (推荐)
```bash
# curl -sSL get.docker.com | sh
docker run -dit \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
-e QlBaseUrl="/" \
--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. 启动容器
### docker-compose (推荐)
```bash
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
@@ -125,9 +85,34 @@ docker-compose up -d
docker-compose down
```
3. 访问
### podman (推荐)
打开你的浏览器,访问 http://{ip}:5700
```bash
# https://podman.io/getting-started/installation
podman run -dit \
--network bridge \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
-e QlBaseUrl="/" \
--name qinglong \
--hostname qinglong \
docker.io/whyour/qinglong:latest
```
### 本机
建议使用纯净系统安装,避免系统原有数据丢失,需要自己安装 node/npm/python3/pip3
```bash
npm install -g @whyour/qinglong
qinglong
# 根据提示增加环境变量 QL_DIR 和 QL_DATA_DIR
export QL_DIR=""
export QL_DATA_DIR=""
# 再次执行
qinglong
```
## 使用
@@ -161,6 +146,10 @@ task <file_path> now
task <file_path> conc <env_name> <account_number>(可选的)
# 指定账号执行,无论是否设置了随机延迟,均立即运行
task <file_path> desi <env_name> <account_number>
# 设置任务超时时间
task -m <max_time> <file_path>
# 实时打印任务日志,创建定时任务时,不用携带此参数
task -l <file_path>
```
2. 参数说明
@@ -176,16 +165,7 @@ task <file_path> desi <env_name> <account_number>
* 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)
* max_time: 超时时间,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
## 开发
@@ -194,16 +174,22 @@ $ git clone git@github.com:whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
# 推荐使用 pnpm https://pnpm.io/zh/installation
$ npm install -g pnpm
$ npm install -g pnpm@8.3.1
$ pnpm install
$ pnpm start
```
打开你的浏览器,访问 http://127.0.0.1:5700
## 交流
## 链接
[telegram频道](https://t.me/jiao_long)
- [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/)
- [Umijs](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server)
## 名称来源
+4 -3
View File
@@ -5,6 +5,7 @@ import { Logger } from 'winston';
import config from '../config';
import * as fs from 'fs';
import { celebrate, Joi } from 'celebrate';
import { join } from 'path';
const route = Router();
export default (app: Router) => {
@@ -41,11 +42,11 @@ export default (app: Router) => {
}
if (req.params.file.includes('sample')) {
content = getFileContentByName(
`${config.samplePath}${req.params.file}`,
join(config.samplePath, req.params.file),
);
} else {
content = getFileContentByName(
`${config.configPath}${req.params.file}`,
join(config.configPath, req.params.file),
);
}
res.send({ code: 200, data: content });
@@ -70,7 +71,7 @@ export default (app: Router) => {
if (config.blackFileList.includes(name)) {
res.send({ code: 403, message: '文件无法访问' });
}
const path = `${config.configPath}${name}`;
const path = join(config.configPath, name);
fs.writeFileSync(path, content);
res.send({ code: 200, message: '保存成功' });
} catch (e) {
+25 -2
View File
@@ -30,6 +30,7 @@ export default (app: Router) => {
name: Joi.string().required(),
sorts: Joi.array().optional().allow(null),
filters: Joi.array().optional(),
filterRelation: Joi.string().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
@@ -51,13 +52,18 @@ export default (app: Router) => {
id: Joi.number().required(),
sorts: Joi.array().optional().allow(null),
filters: Joi.array().optional(),
filterRelation: Joi.string().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.update(req.body);
return res.send({ code: 200, data });
if (req.body.type === 1) {
return res.send({ code: 400, message: '参数错误' });
} else {
const data = await cronViewService.update(req.body);
return res.send({ code: 200, data });
}
} catch (e) {
return next(e);
}
@@ -146,6 +152,21 @@ export default (app: Router) => {
}
});
route.get(
'/detail',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.find(req.query as any);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/',
celebrate({
@@ -154,6 +175,7 @@ export default (app: Router) => {
schedule: Joi.string().required(),
name: Joi.string().optional(),
labels: Joi.array().optional(),
sub_id: Joi.number().optional().allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
@@ -310,6 +332,7 @@ export default (app: Router) => {
command: Joi.string().required(),
schedule: Joi.string().required(),
name: Joi.string().optional().allow(null),
sub_id: Joi.number().optional().allow(null),
id: Joi.number().required(),
}),
}),
+49 -2
View File
@@ -3,8 +3,21 @@ import { Container } from 'typedi';
import EnvService from '../services/env';
import { Logger } from 'winston';
import { celebrate, Joi } from 'celebrate';
import multer from 'multer';
import config from '../config';
import fs from 'fs';
const route = Router();
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, config.scriptPath);
},
filename: function (req, file, cb) {
cb(null, file.originalname);
},
});
const upload = multer({ storage: storage });
export default (app: Router) => {
app.use('/envs', route);
@@ -26,7 +39,9 @@ export default (app: Router) => {
body: Joi.array().items(
Joi.object({
value: Joi.string().required(),
name: Joi.string().required(),
name: Joi.string()
.required()
.pattern(/^[a-zA-Z_][0-9a-zA-Z_]*$/),
remarks: Joi.string().optional().allow(''),
}),
),
@@ -94,7 +109,6 @@ export default (app: Router) => {
}),
}),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const envService = Container.get(EnvService);
const data = await envService.move(req.params.id, req.body);
@@ -177,4 +191,37 @@ export default (app: Router) => {
}
},
);
route.post(
'/upload',
upload.single('env'),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const envService = Container.get(EnvService);
const fileContent = await fs.promises.readFile(req!.file!.path, 'utf8');
const parseContent = JSON.parse(fileContent);
const data = Array.isArray(parseContent)
? parseContent
: [parseContent];
if (data.every((x) => x.name && x.value)) {
const result = await envService.create(
data.map((x) => ({
name: x.name,
value: x.value,
remarks: x.remarks,
})),
);
return res.send({ code: 200, data: result });
} else {
return res.send({
code: 400,
message: '文件缺少name或者value字段,参考导出文件格式',
});
}
} catch (e) {
return next(e);
}
},
);
};
+32 -1
View File
@@ -3,8 +3,9 @@ import { Container } from 'typedi';
import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config';
import { getFileContentByName, readDirs } from '../config/util';
import { emptyDir, getFileContentByName, readDirs } from '../config/util';
import { join } from 'path';
import { celebrate, Joi } from 'celebrate';
const route = Router();
const blacklist = ['.tmp'];
@@ -45,4 +46,34 @@ export default (app: Router) => {
}
},
);
route.delete(
'/',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
type: Joi.string().optional()
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
let { filename, path, type } = req.body as {
filename: string;
path: string;
type: string;
};
const filePath = join(config.logPath, path, filename);
if (type === 'directory') {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
};
+60 -16
View File
@@ -4,6 +4,7 @@ import {
readDirs,
getLastModifyFilePath,
readDir,
emptyDir,
} from '../config/util';
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
@@ -77,12 +78,14 @@ export default (app: Router) => {
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;
};
let { filename, path, content, originFilename, directory } =
req.body as {
filename: string;
path: string;
content: string;
originFilename: string;
directory: string;
};
if (!path) {
path = config.scriptPath;
@@ -91,7 +94,7 @@ export default (app: Router) => {
path += '/';
}
if (!path.startsWith('/')) {
path = `${config.scriptPath}${path}`;
path = join(config.scriptPath, path);
}
if (config.writePathList.every((x) => !path.startsWith(x))) {
return res.send({
@@ -105,15 +108,23 @@ export default (app: Router) => {
return res.send({ code: 200 });
}
if (directory) {
fs.mkdirSync(join(path, directory), { recursive: true });
return res.send({ code: 200 });
}
if (!originFilename) {
originFilename = filename;
}
const originFilePath = `${path}${originFilename.replace(/\//g, '')}`;
const filePath = `${path}${filename.replace(/\//g, '')}`;
const originFilePath = join(
path,
`${originFilename.replace(/\//g, '')}`,
);
const filePath = join(path, `${filename.replace(/\//g, '')}`);
if (fs.existsSync(originFilePath)) {
fs.copyFileSync(
originFilePath,
`${config.bakPath}${originFilename.replace(/\//g, '')}`,
join(config.bakPath, originFilename.replace(/\//g, '')),
);
if (filename !== originFilename) {
fs.unlinkSync(originFilePath);
@@ -159,17 +170,23 @@ export default (app: Router) => {
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
type: Joi.string().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, path } = req.body as {
let { filename, path, type } = req.body as {
filename: string;
path: string;
type: string;
};
const filePath = join(config.scriptPath, path, filename);
fs.unlinkSync(filePath);
if (type === 'directory') {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
res.send({ code: 200 });
} catch (e) {
return next(e);
@@ -190,7 +207,7 @@ export default (app: Router) => {
let { filename } = req.body as {
filename: string;
};
const filePath = `${config.scriptPath}${filename}`;
const filePath = join(config.scriptPath, filename);
// const stats = fs.statSync(filePath);
// res.set({
// 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件
@@ -238,23 +255,50 @@ export default (app: Router) => {
celebrate({
body: Joi.object({
filename: Joi.string().required(),
content: Joi.string().optional().allow(''),
path: Joi.string().optional().allow(''),
pid: Joi.number().optional().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, content, path } = req.body;
let { filename, path, pid } = req.body;
const { name, ext } = parse(filename);
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
const scriptService = Container.get(ScriptService);
const result = await scriptService.stopScript(filePath);
const result = await scriptService.stopScript(filePath, pid);
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.put(
'/rename',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
newFilename: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
let { filename, path, type, newFilename } = req.body as {
filename: string;
path: string;
type: string;
newFilename: string;
};
const filePath = join(config.scriptPath, path, filename);
const newPath = join(config.scriptPath, path, newFilename);
fs.renameSync(filePath, newPath);
res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
};
+6
View File
@@ -49,6 +49,9 @@ export default (app: Router) => {
sub_after: Joi.string().optional().allow('').allow(null),
schedule_type: Joi.string().required(),
alias: Joi.string().required(),
proxy: Joi.string().optional().allow('').allow(null),
autoAddCron: Joi.boolean().optional().allow('').allow(null),
autoDelCron: Joi.boolean().optional().allow('').allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
@@ -177,6 +180,9 @@ export default (app: Router) => {
sub_before: Joi.string().optional().allow('').allow(null),
sub_after: Joi.string().optional().allow('').allow(null),
alias: Joi.string().required(),
proxy: Joi.string().optional().allow('').allow(null),
autoAddCron: Joi.boolean().optional().allow('').allow(null),
autoDelCron: Joi.boolean().optional().allow('').allow(null),
id: Joi.number().required(),
}),
}),
+137 -14
View File
@@ -7,7 +7,25 @@ import SystemService from '../services/system';
import { celebrate, Joi } from 'celebrate';
import UserService from '../services/user';
import { EnvModel } from '../data/env';
import {
getUniqPath,
handleLogPath,
parseVersion,
promiseExec,
} from '../config/util';
import dayjs from 'dayjs';
import multer from 'multer';
const route = Router();
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, config.tmpPath);
},
filename: function (req, file, cb) {
cb(null, 'data.tgz');
},
});
const upload = multer({ storage: storage });
export default (app: Router) => {
app.use('/system', route);
@@ -17,18 +35,15 @@ export default (app: Router) => {
try {
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
const envCount = await EnvModel.count();
const versionRegx = /.*export const version = \'(.*)\'\;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const version = currentVersionFile.match(versionRegx)![1];
const { version, changeLog, changeLogLink, publishTime } = await parseVersion(
config.versionFile,
);
let isInitialized = true;
if (
Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' &&
authInfo.password === 'admin' &&
envCount === 0
authInfo.password === 'admin'
) {
isInitialized = false;
}
@@ -37,6 +52,10 @@ export default (app: Router) => {
data: {
isInitialized,
version,
publishTime: dayjs(publishTime).unix(),
branch: process.env.QL_BRANCH || 'master',
changeLog,
changeLogLink,
},
});
} catch (e) {
@@ -46,12 +65,12 @@ export default (app: Router) => {
});
route.get(
'/log/remove',
'/config',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const data = await systemService.getLogRemoveFrequency();
const data = await systemService.getSystemConfig();
res.send({ code: 200, data });
} catch (e) {
return next(e);
@@ -60,19 +79,18 @@ export default (app: Router) => {
);
route.put(
'/log/remove',
'/config',
celebrate({
body: Joi.object({
frequency: Joi.number().required(),
logRemoveFrequency: Joi.number().optional().allow(null),
cronConcurrency: Joi.number().optional().allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const result = await systemService.updateLogRemoveFrequency(
req.body.frequency,
);
const result = await systemService.updateSystemConfig(req.body);
res.send(result);
} catch (e) {
return next(e);
@@ -108,6 +126,25 @@ export default (app: Router) => {
},
);
route.put(
'/reload',
celebrate({
body: Joi.object({
type: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const result = await systemService.reloadSystem(req.body.type);
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.put(
'/notify',
celebrate({
@@ -127,4 +164,90 @@ export default (app: Router) => {
}
},
);
route.put(
'/command-run',
celebrate({
body: Joi.object({
command: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const uniqPath = await getUniqPath(req.body.command);
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
const logPath = `${uniqPath}/${logTime}.log`;
res.setHeader('Content-type', 'application/octet-stream');
await systemService.run(
{ ...req.body, logPath },
{
onStart: async (cp, startTime) => {
res.setHeader('QL-Task-Pid', `${cp.pid}`);
},
onEnd: async (cp, endTime, diff) => {
res.end();
},
onError: async (message: string) => {
res.write(`\n${message}`);
const absolutePath = await handleLogPath(logPath);
fs.appendFileSync(absolutePath, `\n${message}`);
},
onLog: async (message: string) => {
res.write(`\n${message}`);
const absolutePath = await handleLogPath(logPath);
fs.appendFileSync(absolutePath, `\n${message}`);
},
},
);
} catch (e) {
return next(e);
}
},
);
route.put(
'/command-stop',
celebrate({
body: Joi.object({
command: Joi.string().optional(),
pid: Joi.number().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.stop(req.body);
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.put(
'/data/export',
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
await systemService.exportData(res);
} catch (e) {
return next(e);
}
},
);
route.put(
'/data/import',
upload.single('data'),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.importData();
res.send(result);
} catch (e) {
return next(e);
}
},
);
};
+5
View File
@@ -6,6 +6,7 @@ import { celebrate, Joi } from 'celebrate';
import multer from 'multer';
import path from 'path';
import { v4 as uuidV4 } from 'uuid';
import rateLimit from 'express-rate-limit';
import config from '../config';
const route = Router();
@@ -26,6 +27,10 @@ export default (app: Router) => {
route.post(
'/login',
rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
}),
celebrate({
body: Joi.object({
username: Joi.string().required(),
+2 -1
View File
@@ -17,7 +17,8 @@ async function startServer() {
const server = app
.listen(config.port, () => {
Logger.debug(`✌️ Back server launched on port ${config.port}`);
Logger.debug(`✌️ 后端服务启动成功!`);
process.send?.('ready');
})
.on('error', (err) => {
Logger.error(err);
+7
View File
@@ -0,0 +1,7 @@
export const LOG_END_SYMBOL = '     ';
export const TASK_COMMAND = 'task';
export const QL_COMMAND = 'ql';
export const TASK_PREFIX = `${TASK_COMMAND} `;
export const QL_PREFIX = `${QL_COMMAND} `;
+15 -3
View File
@@ -11,15 +11,16 @@ if (!process.env.QL_DIR) {
if (qlHomePath.endsWith('/static/')) {
qlHomePath = path.join(qlHomePath, '../');
}
process.env.QL_DIR = qlHomePath;
process.env.QL_DIR = qlHomePath.replace(/\/$/g, '');
}
const lastVersionFile = `http://qn.whyour.cn/version.ts?v=${Date.now()}`;
const lastVersionFile = `https://qn.whyour.cn/version.yaml`;
const rootPath = process.env.QL_DIR as string;
const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
const dataPath = path.join(rootPath, 'data/');
const tmpPath = path.join(rootPath, '.tmp/');
const samplePath = path.join(rootPath, 'sample/');
const configPath = path.join(dataPath, 'config/');
const scriptPath = path.join(dataPath, 'scripts/');
@@ -27,6 +28,7 @@ const bakPath = path.join(dataPath, 'bak/');
const logPath = path.join(dataPath, 'log/');
const dbPath = path.join(dataPath, 'db/');
const uploadPath = path.join(dataPath, 'upload/');
const sshdPath = path.join(dataPath, 'ssh.d/');
const envFile = path.join(configPath, 'env.sh');
const confFile = path.join(configPath, 'config.sh');
@@ -40,7 +42,8 @@ const sqliteFile = path.join(samplePath, 'database.sqlite');
const authError = '错误的用户名密码,请重试';
const loginFaild = '请先登录!';
const configString = 'config sample crontab shareCode diy';
const versionFile = path.join(rootPath, 'src/version.ts');
const versionFile = path.join(rootPath, 'version.yaml');
const dataTgzFile = path.join(tmpPath, 'data.tgz');
if (envFound.error) {
throw new Error("⚠️ Couldn't find .env file ⚠️");
@@ -58,6 +61,9 @@ export default {
prefix: '/api',
},
rootPath,
tmpPath,
dataPath,
dataTgzFile,
configString,
loginFaild,
authError,
@@ -91,8 +97,14 @@ export default {
'/api/system',
'/api/user/init',
'/api/user/notification/init',
'/open/user/login',
'/open/user/two-factor/login',
'/open/system',
'/open/user/init',
'/open/user/notification/init',
],
versionFile,
lastVersionFile,
sqliteFile,
sshdPath,
};
+20
View File
@@ -0,0 +1,20 @@
import { Request, Response } from 'express';
import { pick } from 'lodash';
let pickedEnv: Record<string, string>;
function getPickedEnv() {
if (pickedEnv) return pickedEnv;
const picked = pick(process.env, ['QlBaseUrl', 'DeployEnv']);
pickedEnv = picked as Record<string, string>;
return picked;
}
export function serveEnv(_req: Request, res: Response) {
res.type('.js');
res.send(
Object.entries(getPickedEnv())
.map(([k, v]) => `window.__ENV__${k}=${JSON.stringify(v)};`)
.join('\n'),
);
}
+42
View File
@@ -0,0 +1,42 @@
import { Subscription } from '../data/subscription';
import isNil from 'lodash/isNil';
export function formatUrl(doc: Subscription) {
let url = doc.url;
let host = '';
if (doc.type === 'private-repo') {
if (doc.pull_type === 'ssh-key') {
host = doc.url!.replace(/.*\@([^\:]+)\:.*/, '$1');
url = doc.url!.replace(host, doc.alias);
} else {
host = doc.url!.replace(/.*\:\/\/([^\/]+)\/.*/, '$1');
const { username, password } = doc.pull_option as any;
url = doc.url!.replace(host, `${username}:${password}@${host}`);
}
}
return { url, host };
}
export function formatCommand(doc: Subscription, url?: string) {
let command = `SUB_ID=${doc.id} ql `;
let _url = url || formatUrl(doc).url;
const {
type,
whitelist,
blacklist,
dependences,
branch,
extensions,
proxy,
autoAddCron,
autoDelCron,
} = doc;
if (type === 'file') {
command += `raw "${_url}"`;
} else {
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${dependences || ''
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}" "${isNil(autoAddCron) ? true : Boolean(autoAddCron)
}" "${isNil(autoDelCron) ? true : Boolean(autoDelCron)}"`;
}
return command;
}
+217 -8
View File
@@ -3,6 +3,12 @@ import * as path from 'path';
import got from 'got';
import iconv from 'iconv-lite';
import { exec } from 'child_process';
import FormData from 'form-data';
import psTreeFun from 'pstree.remy';
import { promisify } from 'util';
import { load } from 'js-yaml';
import config from './index';
import { TASK_COMMAND } from './const';
export function getFileContentByName(fileName: string) {
if (fs.existsSync(fileName)) {
@@ -241,6 +247,18 @@ export async function createFile(file: string, data: string = '') {
});
}
export async function handleLogPath(
logPath: string,
data: string = '',
): Promise<string> {
const absolutePath = path.resolve(config.logPath, logPath);
const logFileExist = await fileExist(absolutePath);
if (!logFileExist) {
await createFile(absolutePath, data);
}
return absolutePath;
}
export async function concurrentRun(
fnList: Array<() => Promise<any>> = [],
max = 5,
@@ -283,6 +301,20 @@ enum FileType {
'file',
}
interface IFile {
title: string;
key: string;
type: 'directory' | 'file';
parent: string;
mtime: number;
children?: IFile[];
}
export function dirSort(a: IFile, b: IFile) {
if (a.type !== b.type) return FileType[a.type] < FileType[b.type] ? -1 : 1;
else if (a.mtime !== b.mtime) return a.mtime > b.mtime ? -1 : 1;
}
export function readDirs(
dir: string,
baseDir: string = '',
@@ -301,24 +333,21 @@ export function readDirs(
title: file,
key,
type: 'directory',
disabled: true,
parent: relativePath,
children: readDirs(subPath, baseDir).sort(
(a: any, b: any) =>
(FileType as any)[a.type] - (FileType as any)[b.type],
),
mtime: stats.mtime.getTime(),
children: readDirs(subPath, baseDir).sort(dirSort),
};
}
return {
title: file,
type: 'file',
isLeaf: true,
key,
parent: relativePath,
mtime: stats.mtime.getTime(),
};
});
return result.sort(
(a: any, b: any) => (FileType as any)[a.type] - (FileType as any)[b.type],
);
return result.sort(dirSort);
}
export function readDir(
@@ -344,6 +373,20 @@ export function readDir(
return result;
}
export function emptyDir(path: string) {
const files = fs.readdirSync(path);
files.forEach((file) => {
const filePath = `${path}/${file}`;
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
});
fs.rmdirSync(path);
}
export function promiseExec(command: string): Promise<string> {
return new Promise((resolve, reject) => {
exec(
@@ -355,3 +398,169 @@ export function promiseExec(command: string): Promise<string> {
);
});
}
export function promiseExecSuccess(command: string): Promise<string> {
return new Promise((resolve) => {
exec(
command,
{ maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' },
(err, stdout, stderr) => {
resolve(stdout || '');
},
);
});
}
export function parseHeaders(headers: string) {
if (!headers) return {};
const parsed: any = {};
let key;
let val;
let i;
headers &&
headers.split('\n').forEach(function parser(line) {
i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim();
if (!key) {
return;
}
parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
});
return parsed;
}
export function parseBody(
body: string,
contentType:
| 'application/json'
| 'multipart/form-data'
| 'application/x-www-form-urlencoded',
) {
if (!body) return '';
const parsed: any = {};
let key;
let val;
let i;
body &&
body.split('\n').forEach(function parser(line) {
i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim();
if (!key || parsed[key]) {
return;
}
try {
const jsonValue = JSON.parse(val);
parsed[key] = jsonValue;
} catch (error) {
parsed[key] = val;
}
});
switch (contentType) {
case 'multipart/form-data':
return Object.keys(parsed).reduce((p, c) => {
p.append(c, parsed[c]);
return p;
}, new FormData());
case 'application/x-www-form-urlencoded':
return Object.keys(parsed).reduce((p, c) => {
return p ? `${p}&${c}=${parsed[c]}` : `${c}=${parsed[c]}`;
});
}
return parsed;
}
export function psTree(pid: number): Promise<number[]> {
return new Promise((resolve, reject) => {
psTreeFun(pid, (err: any, pids: number[]) => {
if (err) {
reject(err);
}
resolve(pids.filter((x) => !isNaN(x)));
});
});
}
export async function killTask(pid: number) {
const pids = await psTree(pid);
// SIGINT 2 程序终止(interrupt)信号,不会打印额外信息
if (pids.length) {
try {
[pid, ...pids].forEach((x) => {
process.kill(x, 2);
});
} catch (error) {}
} else {
process.kill(pid, 2);
}
}
export async function getPid(name: string) {
const taskCommand = `ps -eo pid,command | grep "${name}" | grep -v grep | awk '{print $1}' | head -1 | xargs echo -n`;
const pid = await promiseExec(taskCommand);
return pid ? Number(pid) : undefined;
}
interface IVersion {
version: string;
changeLogLink: string;
changeLog: string;
publishTime: string;
}
export async function parseVersion(path: string): Promise<IVersion> {
return load(await promisify(fs.readFile)(path, 'utf8')) as IVersion;
}
export async function parseContentVersion(content: string): Promise<IVersion> {
return load(content) as IVersion;
}
export async function getUniqPath(command: string): Promise<string> {
const idStr = `cat ${config.crontabFile} | grep -E "${command}" | perl -pe "s|.*ID=(.*) ${command}.*|\\1|" | head -1 | awk -F " " '{print $1}' | xargs echo -n`;
let id = await promiseExec(idStr);
if (/^\d\d*\d$/.test(id)) {
id = `_${id}`;
} else {
id = '';
}
const items = command.split(/ +/);
let str = items[0];
if (items[0] === TASK_COMMAND) {
str = items[1];
}
const dotIndex = str.lastIndexOf('.');
if (dotIndex !== -1) {
str = str.slice(0, dotIndex);
}
const slashIndex = str.lastIndexOf('/');
let tempStr = '';
if (slashIndex !== -1) {
tempStr = str.slice(0, slashIndex);
const _slashIndex = tempStr.lastIndexOf('/');
if (_slashIndex !== -1) {
tempStr = tempStr.slice(_slashIndex + 1);
}
str = `${tempStr}_${str.slice(slashIndex + 1)}`;
}
return `${str}${id}`;
}
+19 -2
View File
@@ -1,10 +1,11 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
import { NotificationInfo } from './notify';
export class AuthInfo {
ip?: string;
type: AuthDataType;
info?: any;
info?: AuthModelInfo;
id?: number;
constructor(options: AuthInfo) {
@@ -25,9 +26,25 @@ export enum AuthDataType {
'authToken' = 'authToken',
'notification' = 'notification',
'removeLogFrequency' = 'removeLogFrequency',
'systemConfig' = 'systemConfig',
}
interface AuthInstance extends Model<AuthInfo, AuthInfo>, AuthInfo {}
export interface SystemConfigInfo {
logRemoveFrequency?: number;
cronConcurrency?: number;
}
export interface LoginLogInfo {
timestamp?: number;
address?: string;
ip?: string;
platform?: string;
status?: LoginStatus,
}
export type AuthModelInfo = SystemConfigInfo & Partial<NotificationInfo> & LoginLogInfo;
export interface AuthInstance extends Model<AuthInfo, AuthInfo>, AuthInfo { }
export const AuthModel = sequelize.define<AuthInstance>('Auth', {
ip: DataTypes.STRING,
type: DataTypes.STRING,
+8 -5
View File
@@ -17,6 +17,7 @@ export class Crontab {
labels?: string[];
last_running_time?: number;
last_execution_time?: number;
sub_id?: number;
constructor(options: Crontab) {
this.name = options.name;
@@ -25,7 +26,7 @@ export class Crontab {
this.saved = options.saved;
this.id = options.id;
this.status =
options.status && CrontabStatus[options.status]
typeof options.status === 'number' && CrontabStatus[options.status]
? options.status
: CrontabStatus.idle;
this.timestamp = new Date().toString();
@@ -37,17 +38,18 @@ export class Crontab {
this.labels = options.labels || [];
this.last_running_time = options.last_running_time || 0;
this.last_execution_time = options.last_execution_time || 0;
this.sub_id = options.sub_id;
}
}
export enum CrontabStatus {
'running',
'idle',
'running' = 0,
'queued' = 0.5,
'idle' = 1,
'disabled',
'queued',
}
interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
export interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
name: {
unique: 'compositeIndex',
@@ -72,4 +74,5 @@ export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
labels: DataTypes.JSON,
last_running_time: DataTypes.NUMBER,
last_execution_time: DataTypes.NUMBER,
sub_id: { type: DataTypes.NUMBER, allowNull: true },
});
+17 -2
View File
@@ -1,13 +1,19 @@
import { sequelize } from '.';
import { DataTypes, Model } from 'sequelize';
export enum CronViewType {
'系统' = 1,
'个人',
}
interface SortType {
type: 'ASC' | 'DESC';
value: string;
}
interface FilterType {
type: 'or' | 'and';
property: string;
operation: string;
value: string;
}
@@ -18,6 +24,8 @@ export class CrontabView {
isDisabled?: 1 | 0;
filters?: FilterType[];
sorts?: SortType[];
filterRelation?: 'and' | 'or';
type?: CronViewType;
constructor(options: CrontabView) {
this.name = options.name;
@@ -26,10 +34,12 @@ export class CrontabView {
this.isDisabled = options.isDisabled || 0;
this.filters = options.filters;
this.sorts = options.sorts;
this.filterRelation = options.filterRelation;
this.type = options.type || CronViewType.;
}
}
interface CronViewInstance
export interface CronViewInstance
extends Model<CrontabView, CrontabView>,
CrontabView {}
export const CrontabViewModel = sequelize.define<CronViewInstance>(
@@ -43,5 +53,10 @@ export const CrontabViewModel = sequelize.define<CronViewInstance>(
isDisabled: DataTypes.NUMBER,
filters: DataTypes.JSON,
sorts: DataTypes.JSON,
filterRelation: {
type: DataTypes.STRING,
allowNull: true,
},
type: DataTypes.NUMBER,
},
);
+25 -9
View File
@@ -4,18 +4,21 @@ import { DataTypes, Model, ModelDefined } from 'sequelize';
export class Dependence {
timestamp?: string;
id?: number;
status?: DependenceStatus;
type?: DependenceTypes;
name?: number;
status: DependenceStatus;
type: DependenceTypes;
name: string;
log?: string[];
remark?: string;
constructor(options: Dependence) {
this.id = options.id;
this.status = options.status || DependenceStatus.installing;
this.status =
typeof options.status === 'number' && DependenceStatus[options.status]
? options.status
: DependenceStatus.queued;
this.type = options.type || DependenceTypes.nodejs;
this.timestamp = new Date().toString();
this.name = options.name;
this.name = options.name.trim();
this.log = options.log || [];
this.remark = options.remark || '';
}
@@ -28,6 +31,7 @@ export enum DependenceStatus {
'removing',
'removed',
'removeFailed',
'queued',
}
export enum DependenceTypes {
@@ -38,19 +42,31 @@ export enum DependenceTypes {
export enum InstallDependenceCommandTypes {
'pnpm add -g',
'pip3 install',
'pip3 install --disable-pip-version-check --root-user-action=ignore',
'apk add',
}
export enum GetDependenceCommandTypes {
'pnpm ls -g ',
'pip3 show --disable-pip-version-check',
'apk info',
}
export enum versionDependenceCommandTypes {
'@',
'==',
'=',
}
export enum unInstallDependenceCommandTypes {
'pnpm remove -g',
'pip3 uninstall -y',
'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y',
'apk del',
}
interface DependenceInstance
export interface DependenceInstance
extends Model<Dependence, Dependence>,
Dependence {}
Dependence { }
export const DependenceModel = sequelize.define<DependenceInstance>(
'Dependence',
{
+9 -3
View File
@@ -13,7 +13,10 @@ export class Env {
constructor(options: Env) {
this.value = options.value;
this.id = options.id;
this.status = options.status || EnvStatus.normal;
this.status =
typeof options.status === 'number' && EnvStatus[options.status]
? options.status
: EnvStatus.normal;
this.timestamp = new Date().toString();
this.position = options.position;
this.name = options.name;
@@ -26,9 +29,12 @@ export enum EnvStatus {
'disabled',
}
export const initEnvPosition = 9999999999;
export const maxPosition = 9000000000000000;
export const initPosition = 4500000000000000;
export const stepPosition = 10000000000;
export const minPosition = 100;
interface EnvInstance extends Model<Env, Env>, Env {}
export interface EnvInstance extends Model<Env, Env>, Env {}
export const EnvModel = sequelize.define<EnvInstance>('Env', {
value: { type: DataTypes.STRING, unique: 'compositeIndex' },
timestamp: DataTypes.STRING,
+14 -6
View File
@@ -1,15 +1,23 @@
import { Sequelize } from 'sequelize';
import { Sequelize, Transaction } from 'sequelize';
import config from '../config/index';
import { join } from 'path';
export const sequelize = new Sequelize({
dialect: 'sqlite',
storage: `${config.dbPath}database.sqlite`,
storage: join(config.dbPath, 'database.sqlite'),
logging: false,
pool: {
max: 6,
min: 0,
idle: 30000,
retry: {
max: 10,
match: ['SQLITE_BUSY: database is locked'],
},
pool: {
max: 5,
min: 2,
idle: 30000,
acquire: 30000,
evict: 10000,
},
transactionType: Transaction.TYPES.IMMEDIATE,
});
export type ResponseType<T> = { code: number; data?: T; message?: string };
+40 -2
View File
@@ -1,3 +1,5 @@
import { IncomingHttpHeaders } from 'http';
export enum NotificationMode {
'gotify' = 'gotify',
'goCqHttpBot' = 'goCqHttpBot',
@@ -9,9 +11,13 @@ export enum NotificationMode {
'dingtalkBot' = 'dingtalkBot',
'weWorkBot' = 'weWorkBot',
'weWorkApp' = 'weWorkApp',
'aibotk' = 'aibotk',
'iGot' = 'iGot',
'pushPlus' = 'pushPlus',
'email' = 'email',
'pushMe' = 'pushMe',
'feishu' = 'feishu',
'webhook' = 'webhook',
}
abstract class NotificationBaseInfo {
@@ -36,6 +42,7 @@ export class ServerChanNotification extends NotificationBaseInfo {
export class PushDeerNotification extends NotificationBaseInfo {
public pushDeerKey = '';
public pushDeerUrl = '';
}
export class ChatNotification extends NotificationBaseInfo {
@@ -45,7 +52,7 @@ export class ChatNotification extends NotificationBaseInfo {
export class BarkNotification extends NotificationBaseInfo {
public barkPush = '';
public barkIcon = 'http://qn.whyour.cn/logo.png';
public barkIcon = 'https://qn.whyour.cn/logo.png';
public barkSound = '';
public barkGroup = 'qinglong';
}
@@ -66,10 +73,18 @@ export class DingtalkBotNotification extends NotificationBaseInfo {
export class WeWorkBotNotification extends NotificationBaseInfo {
public weWorkBotKey = '';
public weWorkOrigin = '';
}
export class WeWorkAppNotification extends NotificationBaseInfo {
public weWorkAppKey = '';
public weWorkOrigin = '';
}
export class AibotkNotification extends NotificationBaseInfo {
public aibotkKey: string = '';
public aibotkType: 'room' | 'contact' = 'room';
public aibotkName: string = '';
}
export class IGotNotification extends NotificationBaseInfo {
@@ -87,6 +102,25 @@ export class EmailNotification extends NotificationBaseInfo {
public emailPass: string = '';
}
export class PushMeNotification extends NotificationBaseInfo {
public pushMeKey: string = '';
}
export class WebhookNotification extends NotificationBaseInfo {
public webhookHeaders: string = '';
public webhookBody: string = '';
public webhookUrl: string = '';
public webhookMethod: 'GET' | 'POST' | 'PUT' = 'GET';
public webhookContentType:
| 'application/json'
| 'multipart/form-data'
| 'application/x-www-form-urlencoded' = 'application/json';
}
export class LarkNotification extends NotificationBaseInfo {
public larkKey = '';
}
export interface NotificationInfo
extends GoCqHttpBotNotification,
GotifyNotification,
@@ -98,6 +132,10 @@ export interface NotificationInfo
DingtalkBotNotification,
WeWorkBotNotification,
WeWorkAppNotification,
AibotkNotification,
IGotNotification,
PushPlusNotification,
EmailNotification {}
EmailNotification,
PushMeNotification,
WebhookNotification,
LarkNotification {}
+1 -8
View File
@@ -26,14 +26,7 @@ export interface AppToken {
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
export enum CrontabStatus {
'running',
'idle',
'disabled',
'queued',
}
interface AppInstance extends Model<App, App>, App {}
export interface AppInstance extends Model<App, App>, App {}
export const AppModel = sequelize.define<AppInstance>('App', {
name: { type: DataTypes.STRING, unique: 'name' },
scopes: DataTypes.JSON,
+2 -1
View File
@@ -16,4 +16,5 @@ export type SockMessageType =
| 'uninstallDependence'
| 'updateSystemVersion'
| 'manuallyRunScript'
| 'runSubscriptionEnd';
| 'runSubscriptionEnd'
| 'reloadSystem';
+12 -3
View File
@@ -28,14 +28,17 @@ export class Subscription {
extensions?: string;
sub_before?: string;
sub_after?: string;
proxy?: string;
autoAddCron?: 1 | 0;
autoDelCron?: 1 | 0;
constructor(options: Subscription) {
this.id = options.id;
this.name = options.name || options.alias;
this.type = options.type;
this.schedule = options.schedule;
this.status =
options.status && SubscriptionStatus[options.status]
this.status = this.status =
typeof options.status === 'number' && SubscriptionStatus[options.status]
? options.status
: SubscriptionStatus.idle;
this.url = options.url;
@@ -54,6 +57,9 @@ export class Subscription {
this.extensions = options.extensions;
this.sub_before = options.sub_before;
this.sub_after = options.sub_after;
this.proxy = options.proxy;
this.autoAddCron = options.autoAddCron ? 1 : 0;
this.autoDelCron = options.autoDelCron ? 1 : 0;
}
}
@@ -64,7 +70,7 @@ export enum SubscriptionStatus {
'queued',
}
interface SubscriptionInstance
export interface SubscriptionInstance
extends Model<Subscription, Subscription>,
Subscription {}
export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
@@ -102,5 +108,8 @@ export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
log_path: DataTypes.STRING,
schedule_type: DataTypes.STRING,
alias: { type: DataTypes.STRING, unique: 'alias' },
proxy: { type: DataTypes.STRING, allowNull: true },
autoAddCron: { type: DataTypes.NUMBER, allowNull: true },
autoDelCron: { type: DataTypes.NUMBER, allowNull: true },
},
);
+28 -6
View File
@@ -10,6 +10,7 @@ import { fileExist } from '../config/util';
import { SubscriptionModel } from '../data/subscription';
import { CrontabViewModel } from '../data/cronView';
import config from '../config';
import { sequelize } from '../data';
export default async () => {
try {
@@ -21,12 +22,33 @@ export default async () => {
await SubscriptionModel.sync();
await CrontabViewModel.sync();
// try {
// const queryInterface = sequelize.getQueryInterface();
// await queryInterface.addIndex('Crontabs', ['command'], { unique: true });
// await queryInterface.addIndex('Envs', ['name', 'value'], { unique: true });
// await queryInterface.addIndex('Apps', ['name'], { unique: true });
// } catch (error) { }
// 初始化新增字段
try {
await sequelize.query(
'alter table CrontabViews add column filterRelation VARCHAR(255)',
);
} catch (error) {}
try {
await sequelize.query(
'alter table Subscriptions add column proxy VARCHAR(255)',
);
} catch (error) {}
try {
await sequelize.query('alter table CrontabViews add column type NUMBER');
} catch (error) {}
try {
await sequelize.query(
'alter table Subscriptions add column autoAddCron NUMBER',
);
} catch (error) {}
try {
await sequelize.query(
'alter table Subscriptions add column autoDelCron NUMBER',
);
} catch (error) {}
try {
await sequelize.query('alter table Crontabs add column sub_id NUMBER');
} catch (error) {}
// 2.10-2.11 升级
const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
+13 -25
View File
@@ -10,26 +10,27 @@ 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';
import { EnvModel } from '../data/env';
import { errors } from 'celebrate';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { serveEnv } from '../config/serverEnv';
export default ({ app }: { app: Application }) => {
app.enable('trust proxy');
app.use(cors());
app.get(`${config.api.prefix}/env.js`, serveEnv);
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
app.use((req, res, next) => {
if (req.path.startsWith('/api') || req.path.startsWith('/open')) {
next();
} else {
return handler(req, res, {
public: 'static/dist',
rewrites: [{ source: '**', destination: '/index.html' }],
});
}
});
app.use(
'/api/public',
createProxyMiddleware({
target: `http://localhost:${config.publicPort}/api`,
changeOrigin: true,
pathRewrite: { '/api/public': '' },
}),
);
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
@@ -108,8 +109,7 @@ export default ({ app }: { app: Application }) => {
if (
Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' &&
authInfo.password === 'admin' &&
envCount === 0
authInfo.password === 'admin'
) {
isInitialized = false;
}
@@ -170,18 +170,6 @@ export default ({ app }: { app: Application }) => {
},
);
app.use(
Sentry.Handlers.errorHandler({
shouldHandleError(error) {
// 排除 SequelizeUniqueConstraintError / NotFound
return (
!['SequelizeUniqueConstraintError'].includes(error.name) ||
!['Not Found'].includes(error.message)
);
},
}),
);
app.use(
(
err: Error & { status: number },
+24 -11
View File
@@ -4,20 +4,35 @@ import { Container } from 'typedi';
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import CronService from '../services/cron';
import EnvService from '../services/env';
import _ from 'lodash';
import { DependenceModel } from '../data/dependence';
import { DependenceModel, DependenceStatus } from '../data/dependence';
import { Op } from 'sequelize';
import config from '../config';
import { CrontabViewModel, CronViewType } from '../data/cronView';
import { initPosition } from '../data/env';
export default async () => {
const cronService = Container.get(CronService);
const envService = Container.get(EnvService);
const dependenceService = Container.get(DependenceService);
// 初始化新增默认全部任务视图
CrontabViewModel.findAll({
where: { type: CronViewType., name: '全部任务' },
raw: true,
}).then((docs) => {
if (docs.length === 0) {
CrontabViewModel.create({
name: '全部任务',
type: CronViewType.,
position: initPosition / 2,
});
}
});
// 初始化更新所有任务状态为空闲
await CrontabModel.update(
{ status: CrontabStatus.idle },
{ where: { status: [CrontabStatus.running, CrontabStatus.queued] } },
{ where: {} },
);
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
@@ -26,16 +41,14 @@ export default async () => {
order: [['type', 'DESC']],
raw: true,
}).then(async (docs) => {
const groups = _.groupBy(docs, 'type');
const keys = Object.keys(groups).sort((a, b) => parseInt(b) - parseInt(a));
for (const key of keys) {
const group = groups[key];
const depIds = group.map((x) => x.id);
await dependenceService.reInstall(depIds as number[]);
}
await DependenceModel.update(
{ status: DependenceStatus.queued, log: [] },
{ where: { id: docs.map((x) => x.id!) } },
);
dependenceService.installDependenceOneByOne(docs);
});
// 初始化时执行一次所有的ql repo 任务
// 初始化时执行一次所有的 ql repo 任务
CrontabModel.findAll({
where: {
isDisabled: { [Op.ne]: 1 },
+48 -8
View File
@@ -13,12 +13,21 @@ const logPath = path.join(dataPath, 'log/');
const uploadPath = path.join(dataPath, 'upload/');
const bakPath = path.join(dataPath, 'bak/');
const samplePath = path.join(rootPath, 'sample/');
const tmpPath = path.join(logPath, '.tmp/');
const confFile = path.join(configPath, 'config.sh');
const authConfigFile = path.join(configPath, 'auth.json');
const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
const sampleAuthFile = path.join(samplePath, 'auth.sample.json');
const sampleTaskShellFile = path.join(samplePath, 'task.sample.sh');
const sampleNotifyJsFile = path.join(samplePath, 'notify.js');
const sampleNotifyPyFile = path.join(samplePath, 'notify.py');
const scriptNotifyJsFile = path.join(scriptPath, 'sendNotify.js');
const scriptNotifyPyFile = path.join(scriptPath, 'notify.py');
const TaskBeforeFile = path.join(configPath, 'task_before.sh');
const TaskAfterFile = path.join(configPath, 'task_after.sh');
const homedir = os.homedir();
const sshPath = path.resolve(homedir, '.ssh');
const sshdPath = path.join(dataPath, 'ssh.d');
export default async () => {
const authFileExist = await fileExist(authConfigFile);
@@ -29,19 +38,17 @@ export default async () => {
const uploadDirExist = await fileExist(uploadPath);
const sshDirExist = await fileExist(sshPath);
const bakDirExist = await fileExist(bakPath);
const sshdDirExist = await fileExist(sshdPath);
const tmpDirExist = await fileExist(tmpPath);
const scriptNotifyJsFileExist = await fileExist(scriptNotifyJsFile);
const scriptNotifyPyFileExist = await fileExist(scriptNotifyPyFile);
const TaskBeforeFileExist = await fileExist(TaskBeforeFile);
const TaskAfterFileExist = await fileExist(TaskAfterFile);
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);
}
@@ -50,6 +57,10 @@ export default async () => {
fs.mkdirSync(logPath);
}
if (!tmpDirExist) {
fs.mkdirSync(tmpPath);
}
if (!uploadDirExist) {
fs.mkdirSync(uploadPath);
}
@@ -62,6 +73,35 @@ export default async () => {
fs.mkdirSync(bakPath);
}
if (!sshdDirExist) {
fs.mkdirSync(sshdPath);
}
// 初始化文件
if (!authFileExist) {
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
}
if (!confFileExist) {
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
}
if (!scriptNotifyJsFileExist) {
fs.writeFileSync(scriptNotifyJsFile, fs.readFileSync(sampleNotifyJsFile));
}
if (!scriptNotifyPyFileExist) {
fs.writeFileSync(scriptNotifyPyFile, fs.readFileSync(sampleNotifyPyFile));
}
if (!TaskBeforeFileExist) {
fs.writeFileSync(TaskBeforeFile, fs.readFileSync(sampleTaskShellFile));
}
if (!TaskAfterFileExist) {
fs.writeFileSync(TaskAfterFile, fs.readFileSync(sampleTaskShellFile));
}
dotenv.config({ path: confFile });
Logger.info('✌️ Init file down');
+17 -20
View File
@@ -1,10 +1,10 @@
import { Container } from 'typedi';
import _ from 'lodash';
import SystemService from '../services/system';
import ScheduleService from '../services/schedule';
import SubscriptionService from '../services/subscription';
import config from '../config';
import { fileExist } from '../config/util';
import { join } from 'path';
export default async () => {
const systemService = Container.get(SystemService);
@@ -12,43 +12,40 @@ export default async () => {
const subscriptionService = Container.get(SubscriptionService);
// 生成内置token
let tokenCommand = `ts-node-transpile-only ${config.rootPath}/back/token.ts`;
const tokenFile = `${config.rootPath}/static/build/token.js`;
let tokenCommand = `tsx ${join(config.rootPath, 'back/token.ts')}`;
const tokenFile = join(config.rootPath, 'static/build/token.js');
if (await fileExist(tokenFile)) {
tokenCommand = `node ${tokenFile}`;
}
const cron = {
id: 'token',
id: NaN,
name: '生成token',
command: tokenCommand,
};
scheduleService.createIntervalTask(cron as any, {
await scheduleService.cancelIntervalTask(cron);
scheduleService.createIntervalTask(cron, {
days: 28,
runImmediately: true,
});
// 运行删除日志任务
const data = await systemService.getLogRemoveFrequency();
if (data && data.info && data.info.frequency) {
const cron = {
id: data.id,
const data = await systemService.getSystemConfig();
if (data && data.info && data.info.logRemoveFrequency) {
const rmlogCron = {
id: data.id as number,
name: '删除日志',
command: `ql rmlog ${data.info.frequency}`,
command: `ql rmlog ${data.info.logRemoveFrequency}`,
};
scheduleService.createIntervalTask(cron, {
days: data.info.frequency,
runImmediately: true,
await scheduleService.cancelIntervalTask(rmlogCron);
scheduleService.createIntervalTask(rmlogCron, {
days: data.info.logRemoveFrequency,
});
}
// 运行所有订阅
await subscriptionService.setSshConfig();
const subs = await subscriptionService.list();
for (const sub of subs) {
await subscriptionService.handleTask(
sub,
!sub.is_disabled,
true,
!sub.is_disabled,
);
subscriptionService.handleTask(sub, !sub.is_disabled, !sub.is_disabled);
}
};
+14 -3
View File
@@ -2,16 +2,27 @@ import { Application } from 'express';
import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing';
import Logger from './logger';
import config from '../config';
import fs from 'fs';
import { parseVersion } from '../config/util';
export default async ({ expressApp }: { expressApp: Application }) => {
const { version } = await parseVersion(config.versionFile);
export default ({ expressApp }: { expressApp: Application }) => {
Sentry.init({
ignoreErrors: [
/SequelizeUniqueConstraintError/i,
/Validation error/i,
/UnauthorizedError/i,
/celebrate request validation failed/i,
],
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
new Tracing.Integrations.Express({ app: expressApp }),
],
tracesSampleRate: 1.0,
tracesSampleRate: 0.8,
release: version,
});
expressApp.use(Sentry.Handlers.requestHandler());
+26 -4
View File
@@ -5,12 +5,34 @@ import Sock from './sock';
export default async ({ server }: { server: Server }) => {
await Sock({ server });
Logger.info('✌️ Sock loaded');
let exitTime = 0;
let timer: NodeJS.Timeout;
process.on('SIGINT', () => {
Logger.info('✌️ Server need close');
process.on('SIGINT', (singal) => {
Logger.warn(`Server need close, singal ${singal}`);
exitTime++;
if (exitTime >= 3) {
Logger.warn('Forcing server close');
clearTimeout(timer);
process.exit(1);
}
server.close(() => {
Logger.info('✌️ Server closed');
process.exit(0);
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
process.exit();
}, 15000);
});
});
process.on('uncaughtException', (error) => {
Logger.error('Uncaught exception:', error);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
Logger.error('Unhandled rejection:', reason);
process.exit(1);
});
};
+22
View File
@@ -0,0 +1,22 @@
syntax = "proto3";
package com.ql.cron;
service Cron {
rpc addCron(AddCronRequest) returns (AddCronResponse);
rpc delCron(DeleteCronRequest) returns (DeleteCronResponse);
}
message ICron {
string id = 1;
string schedule = 2;
string command = 3;
}
message AddCronRequest { repeated ICron crons = 1; }
message AddCronResponse {}
message DeleteCronRequest { repeated string ids = 1; }
message DeleteCronResponse {}
+482
View File
@@ -0,0 +1,482 @@
/* eslint-disable */
import {
CallOptions,
ChannelCredentials,
Client,
ClientOptions,
ClientUnaryCall,
handleUnaryCall,
makeGenericClientConstructor,
Metadata,
ServiceError,
UntypedServiceImplementation,
} from '@grpc/grpc-js';
import _m0 from 'protobufjs/minimal';
export const protobufPackage = 'com.ql.cron';
export interface ICron {
id: string;
schedule: string;
command: string;
}
export interface AddCronRequest {
crons: ICron[];
}
export interface AddCronResponse {}
export interface DeleteCronRequest {
ids: string[];
}
export interface DeleteCronResponse {}
function createBaseICron(): ICron {
return { id: '', schedule: '', command: '' };
}
export const ICron = {
encode(message: ICron, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.id !== '') {
writer.uint32(10).string(message.id);
}
if (message.schedule !== '') {
writer.uint32(18).string(message.schedule);
}
if (message.command !== '') {
writer.uint32(26).string(message.command);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): ICron {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseICron();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag != 10) {
break;
}
message.id = reader.string();
continue;
case 2:
if (tag != 18) {
break;
}
message.schedule = reader.string();
continue;
case 3:
if (tag != 26) {
break;
}
message.command = reader.string();
continue;
}
if ((tag & 7) == 4 || tag == 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): ICron {
return {
id: isSet(object.id) ? String(object.id) : '',
schedule: isSet(object.schedule) ? String(object.schedule) : '',
command: isSet(object.command) ? String(object.command) : '',
};
},
toJSON(message: ICron): unknown {
const obj: any = {};
message.id !== undefined && (obj.id = message.id);
message.schedule !== undefined && (obj.schedule = message.schedule);
message.command !== undefined && (obj.command = message.command);
return obj;
},
create<I extends Exact<DeepPartial<ICron>, I>>(base?: I): ICron {
return ICron.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<ICron>, I>>(object: I): ICron {
const message = createBaseICron();
message.id = object.id ?? '';
message.schedule = object.schedule ?? '';
message.command = object.command ?? '';
return message;
},
};
function createBaseAddCronRequest(): AddCronRequest {
return { crons: [] };
}
export const AddCronRequest = {
encode(
message: AddCronRequest,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
for (const v of message.crons) {
ICron.encode(v!, writer.uint32(10).fork()).ldelim();
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronRequest {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseAddCronRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag != 10) {
break;
}
message.crons.push(ICron.decode(reader, reader.uint32()));
continue;
}
if ((tag & 7) == 4 || tag == 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): AddCronRequest {
return {
crons: Array.isArray(object?.crons)
? object.crons.map((e: any) => ICron.fromJSON(e))
: [],
};
},
toJSON(message: AddCronRequest): unknown {
const obj: any = {};
if (message.crons) {
obj.crons = message.crons.map((e) => (e ? ICron.toJSON(e) : undefined));
} else {
obj.crons = [];
}
return obj;
},
create<I extends Exact<DeepPartial<AddCronRequest>, I>>(
base?: I,
): AddCronRequest {
return AddCronRequest.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(
object: I,
): AddCronRequest {
const message = createBaseAddCronRequest();
message.crons = object.crons?.map((e) => ICron.fromPartial(e)) || [];
return message;
},
};
function createBaseAddCronResponse(): AddCronResponse {
return {};
}
export const AddCronResponse = {
encode(
_: AddCronResponse,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronResponse {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseAddCronResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
}
if ((tag & 7) == 4 || tag == 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(_: any): AddCronResponse {
return {};
},
toJSON(_: AddCronResponse): unknown {
const obj: any = {};
return obj;
},
create<I extends Exact<DeepPartial<AddCronResponse>, I>>(
base?: I,
): AddCronResponse {
return AddCronResponse.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(
_: I,
): AddCronResponse {
const message = createBaseAddCronResponse();
return message;
},
};
function createBaseDeleteCronRequest(): DeleteCronRequest {
return { ids: [] };
}
export const DeleteCronRequest = {
encode(
message: DeleteCronRequest,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
for (const v of message.ids) {
writer.uint32(10).string(v!);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronRequest {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseDeleteCronRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag != 10) {
break;
}
message.ids.push(reader.string());
continue;
}
if ((tag & 7) == 4 || tag == 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): DeleteCronRequest {
return {
ids: Array.isArray(object?.ids)
? object.ids.map((e: any) => String(e))
: [],
};
},
toJSON(message: DeleteCronRequest): unknown {
const obj: any = {};
if (message.ids) {
obj.ids = message.ids.map((e) => e);
} else {
obj.ids = [];
}
return obj;
},
create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(
base?: I,
): DeleteCronRequest {
return DeleteCronRequest.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(
object: I,
): DeleteCronRequest {
const message = createBaseDeleteCronRequest();
message.ids = object.ids?.map((e) => e) || [];
return message;
},
};
function createBaseDeleteCronResponse(): DeleteCronResponse {
return {};
}
export const DeleteCronResponse = {
encode(
_: DeleteCronResponse,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronResponse {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseDeleteCronResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
}
if ((tag & 7) == 4 || tag == 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(_: any): DeleteCronResponse {
return {};
},
toJSON(_: DeleteCronResponse): unknown {
const obj: any = {};
return obj;
},
create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(
base?: I,
): DeleteCronResponse {
return DeleteCronResponse.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(
_: I,
): DeleteCronResponse {
const message = createBaseDeleteCronResponse();
return message;
},
};
export type CronService = typeof CronService;
export const CronService = {
addCron: {
path: '/com.ql.cron.Cron/addCron',
requestStream: false,
responseStream: false,
requestSerialize: (value: AddCronRequest) =>
Buffer.from(AddCronRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => AddCronRequest.decode(value),
responseSerialize: (value: AddCronResponse) =>
Buffer.from(AddCronResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => AddCronResponse.decode(value),
},
delCron: {
path: '/com.ql.cron.Cron/delCron',
requestStream: false,
responseStream: false,
requestSerialize: (value: DeleteCronRequest) =>
Buffer.from(DeleteCronRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => DeleteCronRequest.decode(value),
responseSerialize: (value: DeleteCronResponse) =>
Buffer.from(DeleteCronResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => DeleteCronResponse.decode(value),
},
} as const;
export interface CronServer extends UntypedServiceImplementation {
addCron: handleUnaryCall<AddCronRequest, AddCronResponse>;
delCron: handleUnaryCall<DeleteCronRequest, DeleteCronResponse>;
}
export interface CronClient extends Client {
addCron(
request: AddCronRequest,
callback: (error: ServiceError | null, response: AddCronResponse) => void,
): ClientUnaryCall;
addCron(
request: AddCronRequest,
metadata: Metadata,
callback: (error: ServiceError | null, response: AddCronResponse) => void,
): ClientUnaryCall;
addCron(
request: AddCronRequest,
metadata: Metadata,
options: Partial<CallOptions>,
callback: (error: ServiceError | null, response: AddCronResponse) => void,
): ClientUnaryCall;
delCron(
request: DeleteCronRequest,
callback: (
error: ServiceError | null,
response: DeleteCronResponse,
) => void,
): ClientUnaryCall;
delCron(
request: DeleteCronRequest,
metadata: Metadata,
callback: (
error: ServiceError | null,
response: DeleteCronResponse,
) => void,
): ClientUnaryCall;
delCron(
request: DeleteCronRequest,
metadata: Metadata,
options: Partial<CallOptions>,
callback: (
error: ServiceError | null,
response: DeleteCronResponse,
) => void,
): ClientUnaryCall;
}
export const CronClient = makeGenericClientConstructor(
CronService,
'com.ql.cron.Cron',
) as unknown as {
new (
address: string,
credentials: ChannelCredentials,
options?: Partial<ClientOptions>,
): CronClient;
service: typeof CronService;
};
type Builtin =
| Date
| Function
| Uint8Array
| string
| number
| boolean
| undefined;
export type DeepPartial<T> = T extends Builtin
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;
type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & {
[K in Exclude<keyof I, KeysOfUnion<P>>]: never;
};
function isSet(value: any): boolean {
return value !== null && value !== undefined;
}
+22
View File
@@ -0,0 +1,22 @@
syntax = "proto3";
package com.ql.health;
message HealthCheckRequest {
string service = 1;
}
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
SERVICE_UNKNOWN = 3;
}
ServingStatus status = 1;
}
service Health {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}
+317
View File
@@ -0,0 +1,317 @@
/* eslint-disable */
import {
CallOptions,
ChannelCredentials,
Client,
ClientOptions,
ClientReadableStream,
ClientUnaryCall,
handleServerStreamingCall,
handleUnaryCall,
makeGenericClientConstructor,
Metadata,
ServiceError,
UntypedServiceImplementation,
} from '@grpc/grpc-js';
import _m0 from 'protobufjs/minimal';
export const protobufPackage = 'com.ql.health';
export interface HealthCheckRequest {
service: string;
}
export interface HealthCheckResponse {
status: HealthCheckResponse_ServingStatus;
}
export enum HealthCheckResponse_ServingStatus {
UNKNOWN = 0,
SERVING = 1,
NOT_SERVING = 2,
SERVICE_UNKNOWN = 3,
UNRECOGNIZED = -1,
}
export function healthCheckResponse_ServingStatusFromJSON(
object: any,
): HealthCheckResponse_ServingStatus {
switch (object) {
case 0:
case 'UNKNOWN':
return HealthCheckResponse_ServingStatus.UNKNOWN;
case 1:
case 'SERVING':
return HealthCheckResponse_ServingStatus.SERVING;
case 2:
case 'NOT_SERVING':
return HealthCheckResponse_ServingStatus.NOT_SERVING;
case 3:
case 'SERVICE_UNKNOWN':
return HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN;
case -1:
case 'UNRECOGNIZED':
default:
return HealthCheckResponse_ServingStatus.UNRECOGNIZED;
}
}
export function healthCheckResponse_ServingStatusToJSON(
object: HealthCheckResponse_ServingStatus,
): string {
switch (object) {
case HealthCheckResponse_ServingStatus.UNKNOWN:
return 'UNKNOWN';
case HealthCheckResponse_ServingStatus.SERVING:
return 'SERVING';
case HealthCheckResponse_ServingStatus.NOT_SERVING:
return 'NOT_SERVING';
case HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN:
return 'SERVICE_UNKNOWN';
case HealthCheckResponse_ServingStatus.UNRECOGNIZED:
default:
return 'UNRECOGNIZED';
}
}
function createBaseHealthCheckRequest(): HealthCheckRequest {
return { service: '' };
}
export const HealthCheckRequest = {
encode(
message: HealthCheckRequest,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.service !== '') {
writer.uint32(10).string(message.service);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckRequest {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHealthCheckRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag != 10) {
break;
}
message.service = reader.string();
continue;
}
if ((tag & 7) == 4 || tag == 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): HealthCheckRequest {
return { service: isSet(object.service) ? String(object.service) : '' };
},
toJSON(message: HealthCheckRequest): unknown {
const obj: any = {};
message.service !== undefined && (obj.service = message.service);
return obj;
},
create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(
base?: I,
): HealthCheckRequest {
return HealthCheckRequest.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(
object: I,
): HealthCheckRequest {
const message = createBaseHealthCheckRequest();
message.service = object.service ?? '';
return message;
},
};
function createBaseHealthCheckResponse(): HealthCheckResponse {
return { status: 0 };
}
export const HealthCheckResponse = {
encode(
message: HealthCheckResponse,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.status !== 0) {
writer.uint32(8).int32(message.status);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckResponse {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHealthCheckResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag != 8) {
break;
}
message.status = reader.int32() as any;
continue;
}
if ((tag & 7) == 4 || tag == 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): HealthCheckResponse {
return {
status: isSet(object.status)
? healthCheckResponse_ServingStatusFromJSON(object.status)
: 0,
};
},
toJSON(message: HealthCheckResponse): unknown {
const obj: any = {};
message.status !== undefined &&
(obj.status = healthCheckResponse_ServingStatusToJSON(message.status));
return obj;
},
create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(
base?: I,
): HealthCheckResponse {
return HealthCheckResponse.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(
object: I,
): HealthCheckResponse {
const message = createBaseHealthCheckResponse();
message.status = object.status ?? 0;
return message;
},
};
export type HealthService = typeof HealthService;
export const HealthService = {
check: {
path: '/com.ql.health.Health/Check',
requestStream: false,
responseStream: false,
requestSerialize: (value: HealthCheckRequest) =>
Buffer.from(HealthCheckRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
responseSerialize: (value: HealthCheckResponse) =>
Buffer.from(HealthCheckResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
},
watch: {
path: '/com.ql.health.Health/Watch',
requestStream: false,
responseStream: true,
requestSerialize: (value: HealthCheckRequest) =>
Buffer.from(HealthCheckRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
responseSerialize: (value: HealthCheckResponse) =>
Buffer.from(HealthCheckResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
},
} as const;
export interface HealthServer extends UntypedServiceImplementation {
check: handleUnaryCall<HealthCheckRequest, HealthCheckResponse>;
watch: handleServerStreamingCall<HealthCheckRequest, HealthCheckResponse>;
}
export interface HealthClient extends Client {
check(
request: HealthCheckRequest,
callback: (
error: ServiceError | null,
response: HealthCheckResponse,
) => void,
): ClientUnaryCall;
check(
request: HealthCheckRequest,
metadata: Metadata,
callback: (
error: ServiceError | null,
response: HealthCheckResponse,
) => void,
): ClientUnaryCall;
check(
request: HealthCheckRequest,
metadata: Metadata,
options: Partial<CallOptions>,
callback: (
error: ServiceError | null,
response: HealthCheckResponse,
) => void,
): ClientUnaryCall;
watch(
request: HealthCheckRequest,
options?: Partial<CallOptions>,
): ClientReadableStream<HealthCheckResponse>;
watch(
request: HealthCheckRequest,
metadata?: Metadata,
options?: Partial<CallOptions>,
): ClientReadableStream<HealthCheckResponse>;
}
export const HealthClient = makeGenericClientConstructor(
HealthService,
'com.ql.health.Health',
) as unknown as {
new (
address: string,
credentials: ChannelCredentials,
options?: Partial<ClientOptions>,
): HealthClient;
service: typeof HealthService;
};
type Builtin =
| Date
| Function
| Uint8Array
| string
| number
| boolean
| undefined;
export type DeepPartial<T> = T extends Builtin
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;
type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & {
[K in Exclude<keyof I, KeysOfUnion<P>>]: never;
};
function isSet(value: any): boolean {
return value !== null && value !== undefined;
}
+15 -12
View File
@@ -1,20 +1,22 @@
import express from 'express';
import { exec } from 'child_process';
import Logger from './loaders/logger';
import config from './config';
import { HealthClient } from './protos/health';
import { credentials } from '@grpc/grpc-js';
const app = express();
const client = new HealthClient(
`localhost:${config.cronPort}`,
credentials.createInsecure(),
);
app.get('/api/public/panel/log', (req, res) => {
exec(
'pm2 logs panel --lines 500 --nostream --timestamp',
(err, stdout, stderr) => {
if (err || stderr) {
return res.send({ code: 400, message: (err && err.message) || stderr });
}
return res.send({ code: 200, data: stdout });
},
);
app.get('/api/health', (req, res) => {
client.check({ service: 'cron' }, (err, response) => {
if (err) {
return res.status(500).send({ code: 500, error: err });
}
return res.status(200).send({ code: 200, data: response });
});
});
app
@@ -22,7 +24,8 @@ app
await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default();
Logger.debug(`✌️ Back server launched on port ${config.publicPort}`);
Logger.debug(`✌️ 公共服务启动成功!`);
process.send?.('ready');
})
.on('error', (err) => {
Logger.error(err);
-55
View File
@@ -1,55 +0,0 @@
import schedule from 'node-schedule';
import express from 'express';
import { exec } from 'child_process';
import Logger from './loaders/logger';
import { CrontabModel, CrontabStatus } from './data/cron';
import config from './config';
const app = express();
const run = async () => {
CrontabModel.findAll({ where: {} })
.then((docs) => {
if (docs && docs.length > 0) {
for (let i = 0; i < docs.length; i++) {
const task = docs[i];
const _schedule = task.schedule && task.schedule.split(/ +/);
if (
_schedule &&
_schedule.length > 5 &&
task.status !== CrontabStatus.disabled &&
!task.isDisabled
) {
schedule.scheduleJob(task.schedule, function () {
let command = task.command as string;
if (!command.includes('task ') && !command.includes('ql ')) {
command = `task ${command}`;
}
exec(command);
});
}
}
}
})
.catch((err) => {
Logger.error(err);
process.exit(1);
});
};
app
.listen(config.cronPort, async () => {
await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default();
await run();
Logger.info(`
################################################
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
################################################
`);
})
.on('error', (err) => {
Logger.error(err);
process.exit(1);
});
+27
View File
@@ -0,0 +1,27 @@
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
import { AddCronRequest, AddCronResponse } from '../protos/cron';
import nodeSchedule from 'node-schedule';
import { scheduleStacks } from './data';
import { runCron } from '../shared/runCron';
const addCron = (
call: ServerUnaryCall<AddCronRequest, AddCronResponse>,
callback: sendUnaryData<AddCronResponse>,
) => {
for (const item of call.request.crons) {
const { id, schedule, command } = item;
if (scheduleStacks.has(id)) {
scheduleStacks.get(id)?.cancel();
}
scheduleStacks.set(
id,
nodeSchedule.scheduleJob(id, schedule, async () => {
runCron(`ID=${id} ${command}`)
}),
);
}
callback(null, null);
};
export { addCron };
+40
View File
@@ -0,0 +1,40 @@
import { credentials } from '@grpc/grpc-js';
import {
AddCronRequest,
AddCronResponse,
CronClient,
DeleteCronRequest,
DeleteCronResponse,
} from '../protos/cron';
import config from '../config';
class Client {
private client = new CronClient(
`localhost:${config.cronPort}`,
credentials.createInsecure(),
);
addCron(request: AddCronRequest['crons']): Promise<AddCronResponse> {
return new Promise((resolve, reject) => {
this.client.addCron({ crons: request }, (err, res) => {
if (err) {
reject(err);
}
resolve(res);
});
});
}
delCron(request: DeleteCronRequest['ids']): Promise<DeleteCronResponse> {
return new Promise((resolve, reject) => {
this.client.delCron({ ids: request }, (err, res) => {
if (err) {
reject(err);
}
resolve(res);
});
});
}
}
export default new Client();
+6
View File
@@ -0,0 +1,6 @@
import nodeSchedule from 'node-schedule';
import { ToadScheduler } from 'toad-scheduler';
export const scheduleStacks = new Map<string, nodeSchedule.Job>();
export const intervalSchedule = new ToadScheduler();
+19
View File
@@ -0,0 +1,19 @@
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
import { DeleteCronRequest, DeleteCronResponse } from '../protos/cron';
import { scheduleStacks } from './data';
const delCron = (
call: ServerUnaryCall<DeleteCronRequest, DeleteCronResponse>,
callback: sendUnaryData<DeleteCronResponse>,
) => {
for (const id of call.request.ids) {
if (scheduleStacks.has(id)) {
scheduleStacks.get(id)?.cancel();
scheduleStacks.delete(id);
}
}
callback(null, null);
};
export { delCron };
+35
View File
@@ -0,0 +1,35 @@
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
import { HealthCheckRequest, HealthCheckResponse } from '../protos/health';
import config from '../config';
import { promiseExec } from '../config/util';
const check = async (
call: ServerUnaryCall<HealthCheckRequest, HealthCheckResponse>,
callback: sendUnaryData<HealthCheckResponse>,
) => {
switch (call.request.service) {
case 'cron':
const res = await promiseExec(
`curl -s http://localhost:${config.port}/api/system`,
);
if (res.includes('200')) {
return callback(null, { status: 1 });
}
const panelErrLog = await promiseExec(
`tail -n 300 ~/.pm2/logs/panel-error.log`,
);
const scheduleErrLog = await promiseExec(
`tail -n 300 ~/.pm2/logs/schedule-error.log`,
);
return callback(
new Error(`${scheduleErrLog || ''}\n${panelErrLog || ''}\n${res}`),
);
default:
return callback(null, { status: 1 });
}
};
export { check };
+24
View File
@@ -0,0 +1,24 @@
import { Server, ServerCredentials } from '@grpc/grpc-js';
import { CronService } from '../protos/cron';
import { addCron } from './addCron';
import { delCron } from './delCron';
import { HealthService } from '../protos/health';
import { check } from './health';
import config from '../config';
import Logger from '../loaders/logger';
const server = new Server();
server.addService(HealthService, { check });
server.addService(CronService, { addCron, delCron });
server.bindAsync(
`localhost:${config.cronPort}`,
ServerCredentials.createInsecure(),
(err, port) => {
if (err) {
throw err;
}
server.start();
Logger.debug(`✌️ 定时服务启动成功!`);
process.send?.('ready');
},
);
+180 -217
View File
@@ -2,19 +2,25 @@ import { Service, Inject } from 'typedi';
import winston from 'winston';
import config from '../config';
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import { exec, execSync, spawn } from 'child_process';
import { exec, execSync } from 'child_process';
import fs from 'fs';
import cron_parser from 'cron-parser';
import { getFileContentByName, concurrentRun, fileExist } from '../config/util';
import {
getFileContentByName,
fileExist,
killTask,
} from '../config/util';
import { promises, existsSync } from 'fs';
import { promisify } from 'util';
import { Op } from 'sequelize';
import { Op, where, col as colFn, FindOptions, fn } from 'sequelize';
import path from 'path';
import dayjs from 'dayjs';
import { TASK_PREFIX, QL_PREFIX } from '../config/const';
import cronClient from '../schedule/client';
import taskLimit from '../shared/pLimit';
import { spawn } from 'cross-spawn';
@Service()
export default class CronService {
constructor(@Inject('logger') private logger: winston.Logger) {}
constructor(@Inject('logger') private logger: winston.Logger) { }
private isSixCron(cron: Crontab) {
const { schedule } = cron;
@@ -28,7 +34,12 @@ export default class CronService {
const tab = new Crontab(payload);
tab.saved = false;
const doc = await this.insert(tab);
await this.set_crontab(this.isSixCron(doc));
if (this.isSixCron(doc)) {
await cronClient.addCron([
{ id: String(doc.id), schedule: doc.schedule!, command: doc.command },
]);
}
await this.set_crontab();
return doc;
}
@@ -37,9 +48,26 @@ export default class CronService {
}
public async update(payload: Crontab): Promise<Crontab> {
payload.saved = false;
const newDoc = await this.updateDb(payload);
await this.set_crontab(this.isSixCron(newDoc));
const doc = await this.getDb({ id: payload.id });
const tab = new Crontab({ ...doc, ...payload });
tab.saved = false;
const newDoc = await this.updateDb(tab);
if (doc.isDisabled === 1) {
return newDoc;
}
if (this.isSixCron(doc)) {
await cronClient.delCron([String(newDoc.id)]);
}
if (this.isSixCron(newDoc)) {
await cronClient.addCron([
{
id: String(newDoc.id),
schedule: newDoc.schedule!,
command: newDoc.command,
},
]);
}
await this.set_crontab();
return newDoc;
}
@@ -78,7 +106,8 @@ export default class CronService {
public async remove(ids: number[]) {
await CrontabModel.destroy({ where: { id: ids } });
await this.set_crontab(true);
await cronClient.delCron(ids.map(String));
await this.set_crontab();
}
public async pin(ids: number[]) {
@@ -115,8 +144,9 @@ export default class CronService {
private formatViewQuery(query: any, viewQuery: any) {
if (viewQuery.filters && viewQuery.filters.length > 0) {
if (!query[Op.and]) {
query[Op.and] = [];
const primaryOperate = viewQuery.filterRelation === 'or' ? Op.or : Op.and;
if (!query[primaryOperate]) {
query[primaryOperate] = [];
}
for (const col of viewQuery.filters) {
const { property, value, operation } = col;
@@ -159,13 +189,23 @@ export default class CronService {
}
if (operate && operate2) {
q[property] = {
[operate2]: [
{ [operate]: `%${value}%` },
{ [operate]: `%${encodeURIComponent(value)}%` },
[Op.or]: [
{
[operate2]: [
{ [operate]: `%${value}%` },
{ [operate]: `%${encodeURI(value)}%` },
],
},
{
[operate2]: [
where(colFn(property), operate, `%${value}%`),
where(colFn(property), operate, `%${encodeURI(value)}%`),
],
},
],
};
}
query[Op.and].push(q);
query[primaryOperate].push(q);
}
}
}
@@ -186,7 +226,7 @@ export default class CronService {
q[column] = {
[Op.or]: [
{ [Op.like]: `%${textArray[1]}%` },
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
{ [Op.like]: `%${encodeURI(textArray[1])}%` },
],
};
break;
@@ -194,7 +234,7 @@ export default class CronService {
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchText}%` },
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
{ [Op.like]: `%${encodeURI(searchText)}%` },
],
};
q[Op.or] = [
@@ -225,8 +265,15 @@ export default class CronService {
const filterKeys: any = Object.keys(filterQuery);
for (const key of filterKeys) {
let q: any = {};
if (filterKeys[key]) {
q[key] = filterKeys[key];
if (!filterQuery[key]) continue;
if (key === 'status') {
if (filterQuery[key].includes(CrontabStatus.disabled)) {
q = { [Op.or]: [{ [key]: filterQuery[key] }, { isDisabled: 1 }] };
} else {
q = { [Op.and]: [{ [key]: filterQuery[key] }, { isDisabled: 0 }] };
}
} else {
q[key] = filterQuery[key];
}
query[Op.and].push(q);
}
@@ -241,6 +288,19 @@ export default class CronService {
}
}
public async find({
log_path,
}: {
log_path: string;
}): Promise<Crontab | null> {
try {
const result = await CrontabModel.findOne({ where: { log_path } });
return result;
} catch (error) {
throw error;
}
}
public async crontabs(params?: {
searchValue: string;
page: string;
@@ -292,7 +352,7 @@ export default class CronService {
}
}
public async getDb(query: any): Promise<Crontab> {
public async getDb(query: FindOptions<Crontab>['where']): Promise<Crontab> {
const doc: any = await CrontabModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Crontab);
}
@@ -302,10 +362,9 @@ export default class CronService {
{ status: CrontabStatus.queued },
{ where: { id: ids } },
);
concurrentRun(
ids.map((id) => async () => await this.runSingle(id)),
10,
);
ids.forEach((id) => {
this.runSingle(id);
});
}
public async stop(ids: number[]) {
@@ -313,31 +372,11 @@ export default class CronService {
for (const doc of docs) {
if (doc.pid) {
try {
process.kill(-doc.pid);
await killTask(doc.pid);
} catch (error) {
this.logger.silly(error);
}
}
const err = await this.killTask(doc.command);
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
const logFileExist = doc.log_path && (await fileExist(absolutePath));
const endTime = dayjs();
const diffTimeStr = doc.last_execution_time
? `,耗时 ${endTime.diff(
dayjs(doc.last_execution_time * 1000),
'second',
)}`
: '';
if (logFileExist) {
const str = err ? `\n${err}` : '';
fs.appendFileSync(
`${absolutePath}`,
`${str}\n## 执行结束... ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)}${diffTimeStr}`,
);
}
}
await CrontabModel.update(
@@ -346,112 +385,88 @@ export default class CronService {
);
}
public async killTask(name: string) {
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
const execAsync = promisify(exec);
try {
let pid = (await execAsync(taskCommand)).stdout;
if (pid) {
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
} else {
return;
}
let pids = pid.match(/\(\d+/g);
const killLogs = [];
if (pids && pids.length > 0) {
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
pids = pids.slice(0, 3);
for (const id of pids) {
const c = `kill -9 ${id.slice(1)}`;
try {
const { stdout, stderr } = await execAsync(c);
if (stderr) {
killLogs.push(stderr);
}
if (stdout) {
killLogs.push(stdout);
}
} catch (error: any) {
killLogs.push(error.message);
}
}
}
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
} catch (e) {
return JSON.stringify(e);
}
}
private async runSingle(cronId: number): Promise<number> {
return new Promise(async (resolve: any) => {
const cron = await this.getDb({ id: cronId });
if (cron.status !== CrontabStatus.queued) {
resolve();
return;
}
let { id, command, log_path } = cron;
const absolutePath = path.resolve(config.logPath, `${log_path}`);
const logFileExist = log_path && (await fileExist(absolutePath));
this.logger.silly('Running job');
this.logger.silly('ID: ' + id);
this.logger.silly('Original command: ' + command);
let cmdStr = command;
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
cmdStr = `task ${cmdStr}`;
}
if (
cmdStr.endsWith('.js') ||
cmdStr.endsWith('.py') ||
cmdStr.endsWith('.pyc') ||
cmdStr.endsWith('.sh') ||
cmdStr.endsWith('.ts')
) {
cmdStr = `${cmdStr} now`;
}
const cp = spawn(cmdStr, { shell: '/bin/bash' });
await CrontabModel.update(
{ status: CrontabStatus.running, pid: cp.pid },
{ where: { id } },
);
cp.stderr.on('data', (data) => {
if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${data}`);
return taskLimit.runWithCpuLimit(() => {
return new Promise(async (resolve: any) => {
const cron = await this.getDb({ id: cronId });
if (cron.status !== CrontabStatus.queued) {
resolve();
return;
}
});
cp.on('error', (err) => {
if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
}
});
cp.on('exit', async (code, signal) => {
this.logger.info(
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
);
});
cp.on('close', async (code) => {
let { id, command, log_path } = cron;
const absolutePath = path.resolve(config.logPath, `${log_path}`);
const logFileExist = log_path && (await fileExist(absolutePath));
this.logger.silly('Running job');
this.logger.silly('ID: ' + id);
this.logger.silly('Original command: ' + command);
let cmdStr = command;
if (!cmdStr.startsWith(TASK_PREFIX) && !cmdStr.startsWith(QL_PREFIX)) {
cmdStr = `${TASK_PREFIX}${cmdStr}`;
}
if (
cmdStr.endsWith('.js') ||
cmdStr.endsWith('.py') ||
cmdStr.endsWith('.pyc') ||
cmdStr.endsWith('.sh') ||
cmdStr.endsWith('.ts')
) {
cmdStr = `${cmdStr} now`;
}
const cp = spawn(`ID=${id} ${cmdStr}`, { shell: '/bin/bash' });
await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined },
{ status: CrontabStatus.running, pid: cp.pid },
{ where: { id } },
);
resolve();
cp.stderr.on('data', (data) => {
if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${data.toString()}`);
}
});
cp.on('error', (err) => {
if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
}
});
cp.on('exit', async (code, signal) => {
this.logger.info(
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
);
});
cp.on('close', async (code) => {
await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined },
{ where: { id } },
);
resolve();
});
});
});
}
public async disabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
await this.set_crontab(true);
await cronClient.delCron(ids.map(String));
await this.set_crontab();
}
public async enabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
await this.set_crontab(true);
const docs = await CrontabModel.findAll({ where: { id: ids } });
const sixCron = docs
.filter((x) => this.isSixCron(x))
.map((doc) => ({
id: String(doc.id),
schedule: doc.schedule!,
command: doc.command,
}));
await cronClient.addCron(sixCron);
await this.set_crontab();
}
public async log(id: number) {
@@ -464,69 +479,26 @@ export default class CronService {
const logFileExist = doc.log_path && (await fileExist(absolutePath));
if (logFileExist) {
return getFileContentByName(`${absolutePath}`);
}
const [, commandStr, url] = doc.command.split(/ +/);
let logPath = this.getKey(commandStr);
const isQlCommand = doc.command.startsWith('ql ');
const key =
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
logPath;
if (isQlCommand) {
logPath = 'update';
}
let logDir = `${config.logPath}${logPath}`;
if (existsSync(logDir)) {
let files = await promises.readdir(logDir);
if (isQlCommand) {
files = files.filter((x) => x.includes(key));
}
return getFileContentByName(`${logDir}/${files[files.length - 1]}`);
} else {
return '';
return '任务未运行';
}
}
public async logs(id: number) {
const doc = await this.getDb({ id });
if (!doc) {
if (!doc || !doc.log_path) {
return [];
}
if (doc.log_path) {
const relativeDir = path.dirname(`${doc.log_path}`);
const dir = path.resolve(config.logPath, relativeDir);
if (existsSync(dir)) {
let files = await promises.readdir(dir);
return files
.map((x) => ({
filename: x,
directory: relativeDir.replace(config.logPath, ''),
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time);
}
}
const [, commandStr, url] = doc.command.split(/ +/);
let logPath = this.getKey(commandStr);
const isQlCommand = doc.command.startsWith('ql ');
const key =
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
logPath;
if (isQlCommand) {
logPath = 'update';
}
let logDir = `${config.logPath}${logPath}`;
if (existsSync(logDir)) {
let files = await promises.readdir(logDir);
if (isQlCommand) {
files = files.filter((x) => x.includes(key));
}
const relativeDir = path.dirname(`${doc.log_path}`);
const dir = path.resolve(config.logPath, relativeDir);
if (existsSync(dir)) {
let files = await promises.readdir(dir);
return files
.map((x) => ({
filename: x,
directory: logPath,
time: fs.statSync(`${logDir}/${x}`).mtime.getTime(),
directory: relativeDir.replace(config.logPath, ''),
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time);
} else {
@@ -534,35 +506,19 @@ export default class CronService {
}
}
private getKey(command: string): string {
const start =
command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0;
const end =
command.lastIndexOf('.') !== -1
? command.lastIndexOf('.')
: command.length;
const tmpStr = command.substring(0, start - 1);
let index = 0;
if (tmpStr.lastIndexOf('/') !== -1 && tmpStr.startsWith('http')) {
index = tmpStr.lastIndexOf('/');
} else if (tmpStr.lastIndexOf(':') !== -1 && tmpStr.startsWith('git@')) {
index = tmpStr.lastIndexOf(':');
}
if (index) {
return `${tmpStr.substring(index + 1)}_${command.substring(start, end)}`;
} else {
return command.substring(start, end);
}
}
private make_command(tab: Crontab) {
if (
!tab.command.startsWith(TASK_PREFIX) &&
!tab.command.startsWith(QL_PREFIX)
) {
tab.command = `${TASK_PREFIX}${tab.command}`;
}
const crontab_job_string = `ID=${tab.id} ${tab.command}`;
return crontab_job_string;
}
private async set_crontab(needReloadSchedule: boolean = false) {
const tabs = await this.crontabs();
private async set_crontab(data?: { data: Crontab[]; total: number }) {
const tabs = data ?? (await this.crontabs());
var crontab_string = '';
tabs.data.forEach((tab) => {
const _schedule = tab.schedule && tab.schedule.split(/ +/);
@@ -584,9 +540,6 @@ export default class CronService {
fs.writeFileSync(config.crontabFile, crontab_string);
execSync(`crontab ${config.crontabFile}`);
if (needReloadSchedule) {
exec(`pm2 reload schedule`);
}
await CrontabModel.update({ saved: true }, { where: {} });
}
@@ -624,7 +577,17 @@ export default class CronService {
});
}
public autosave_crontab() {
return this.set_crontab();
public async autosave_crontab() {
const tabs = await this.crontabs();
this.set_crontab(tabs);
const sixCron = tabs.data
.filter((x) => this.isSixCron(x) && x.isDisabled !== 1)
.map((doc) => ({
id: String(doc.id),
schedule: doc.schedule!,
command: doc.command,
}));
await cronClient.addCron(sixCron);
}
}
+37 -9
View File
@@ -1,14 +1,20 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { CrontabView, CrontabViewModel } from '../data/cronView';
import { initEnvPosition } from '../data/env';
import {
initPosition,
maxPosition,
minPosition,
stepPosition,
} from '../data/env';
import { FindOptions } from 'sequelize';
@Service()
export default class CronViewService {
constructor(@Inject('logger') private logger: winston.Logger) {}
public async create(payload: CrontabView): Promise<CrontabView> {
let position = initEnvPosition;
let position = initPosition;
const views = await this.list();
if (views && views.length > 0 && views[views.length - 1].position) {
position = views[views.length - 1].position as number;
@@ -16,6 +22,8 @@ export default class CronViewService {
position = position / 2;
const tab = new CrontabView({ ...payload, position });
const doc = await this.insert(tab);
await this.checkPosition(tab.position!);
return doc;
}
@@ -24,7 +32,9 @@ export default class CronViewService {
}
public async update(payload: CrontabView): Promise<CrontabView> {
const newDoc = await this.updateDb(payload);
const doc = await this.getDb({ id: payload.id })
const tab = new CrontabView({ ...doc, ...payload });
const newDoc = await this.updateDb(tab);
return newDoc;
}
@@ -49,7 +59,7 @@ export default class CronViewService {
}
}
public async getDb(query: any): Promise<CrontabView> {
public async getDb(query: FindOptions<CrontabView>['where']): Promise<CrontabView> {
const doc: any = await CrontabViewModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as CrontabView);
}
@@ -62,6 +72,22 @@ export default class CronViewService {
await CrontabViewModel.update({ isDisabled: 0 }, { where: { id: ids } });
}
private async checkPosition(position: number) {
const precisionPosition = parseFloat(position.toPrecision(16));
if (precisionPosition < minPosition || precisionPosition > maxPosition) {
const envs = await this.list();
let position = initPosition;
for (const env of envs) {
position = position - stepPosition;
await this.updateDb({ id: env.id, position });
}
}
}
private getPrecisionPosition(position: number): number {
return parseFloat(position.toPrecision(16));
}
public async move({
id,
fromIndex,
@@ -76,17 +102,19 @@ export default class CronViewService {
const views = await this.list();
if (toIndex === 0 || toIndex === views.length - 1) {
targetPosition = isUpward
? views[0].position * 2
: views[toIndex].position / 2;
? views[0].position! * 2
: views[toIndex].position! / 2;
} else {
targetPosition = isUpward
? (views[toIndex].position + views[toIndex - 1].position) / 2
: (views[toIndex].position + views[toIndex + 1].position) / 2;
? (views[toIndex].position! + views[toIndex - 1].position!) / 2
: (views[toIndex].position! + views[toIndex + 1].position!) / 2;
}
const newDoc = await this.update({
id,
position: targetPosition,
position: this.getPrecisionPosition(targetPosition),
});
await this.checkPosition(targetPosition);
return newDoc;
}
}
+163 -105
View File
@@ -8,24 +8,26 @@ import {
DependenceTypes,
unInstallDependenceCommandTypes,
DependenceModel,
GetDependenceCommandTypes,
versionDependenceCommandTypes,
} from '../data/dependence';
import _ from 'lodash';
import { spawn } from 'child_process';
import { spawn } from 'cross-spawn';
import SockService from './sock';
import { Op } from 'sequelize';
import { concurrentRun } from '../config/util';
import { FindOptions, Op } from 'sequelize';
import { promiseExecSuccess } from '../config/util';
import dayjs from 'dayjs';
import taskLimit from '../shared/pLimit';
@Service()
export default class DependenceService {
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 });
const tab = new Dependence({ ...x, status: DependenceStatus.queued });
return tab;
});
const docs = await this.insert(tabs);
@@ -46,7 +48,7 @@ export default class DependenceService {
const tab = new Dependence({
...doc,
...other,
status: DependenceStatus.installing,
status: DependenceStatus.queued,
});
const newDoc = await this.updateDb(tab);
this.installDependenceOneByOne([newDoc]);
@@ -60,7 +62,7 @@ export default class DependenceService {
public async remove(ids: number[], force = false): Promise<Dependence[]> {
await DependenceModel.update(
{ status: DependenceStatus.removing, log: [] },
{ status: DependenceStatus.queued, log: [] },
{ where: { id: ids } },
);
const docs = await DependenceModel.findAll({ where: { id: ids } });
@@ -79,7 +81,7 @@ export default class DependenceService {
): Promise<Dependence[]> {
let condition = { ...query, type: DependenceTypes[type as any] };
if (searchValue) {
const encodeText = encodeURIComponent(searchValue);
const encodeText = encodeURI(searchValue);
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchValue}%` },
@@ -100,23 +102,19 @@ export default class DependenceService {
}
}
private installDependenceOneByOne(
public installDependenceOneByOne(
docs: Dependence[],
isInstall: boolean = true,
force: boolean = false,
) {
concurrentRun(
docs.map(
(dep) => async () =>
await this.installOrUninstallDependencies([dep], isInstall, force),
),
1,
);
docs.forEach((dep) => {
this.installOrUninstallDependency(dep, isInstall, force);
});
}
public async reInstall(ids: number[]): Promise<Dependence[]> {
await DependenceModel.update(
{ status: DependenceStatus.installing, log: [] },
{ status: DependenceStatus.queued, log: [] },
{ where: { id: ids } },
);
@@ -125,118 +123,178 @@ export default class DependenceService {
return docs;
}
private async find(query: any, sort?: any): Promise<Dependence[]> {
const docs = await DependenceModel.findAll({ where: { ...query } });
private async find(query: any, sort: any = []): Promise<Dependence[]> {
const docs = await DependenceModel.findAll({
where: { ...query },
order: [...sort, ['createdAt', 'DESC']],
});
return docs;
}
public async getDb(query: any): Promise<Dependence> {
public async getDb(
query: FindOptions<Dependence>['where'],
): Promise<Dependence> {
const doc: any = await DependenceModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Dependence);
}
private async updateLog(ids: number[], log: string): Promise<void> {
const doc = await DependenceModel.findOne({ where: { id: ids } });
const newLog = doc?.log ? [...doc.log, log] : [log];
await DependenceModel.update({ log: newLog }, { where: { id: ids } });
taskLimit.updateDepLog(async () => {
const docs = await DependenceModel.findAll({ where: { id: ids } });
for (const doc of docs) {
const newLog = doc?.log ? [...doc.log, log] : [log];
await DependenceModel.update(
{ log: newLog },
{ where: { id: doc.id } },
);
}
return null;
});
}
public installOrUninstallDependencies(
dependencies: Dependence[],
public installOrUninstallDependency(
dependency: Dependence,
isInstall: boolean = true,
force: boolean = false,
) {
return new Promise(async (resolve) => {
if (dependencies.length === 0) {
resolve(null);
return;
}
const socketMessageType = !force
? 'installDependence'
: 'uninstallDependence';
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 number[];
const startTime = dayjs();
return taskLimit.runOneByOne(() => {
return new Promise(async (resolve) => {
const depIds = [dependency.id!];
const status = isInstall
? DependenceStatus.installing
: DependenceStatus.removing;
await DependenceModel.update({ status }, { where: { id: depIds } });
const message = `开始${actionText}依赖 ${depNames},开始时间 ${startTime.format(
'YYYY-MM-DD HH:mm:ss',
)}\n\n`;
this.sockService.sendMessage({
type: socketMessageType,
message,
references: depIds,
});
await this.updateLog(depIds, message);
const socketMessageType = isInstall
? 'installDependence'
: 'uninstallDependence';
const depName = dependency.name.trim();
const depRunCommand = (
isInstall
? InstallDependenceCommandTypes
: unInstallDependenceCommandTypes
)[dependency.type];
const actionText = isInstall ? '安装' : '删除';
const startTime = dayjs();
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
cp.stdout.on('data', async (data) => {
this.sockService.sendMessage({
type: socketMessageType,
message: data.toString(),
references: depIds,
});
await this.updateLog(depIds, data.toString());
});
cp.stderr.on('data', async (data) => {
this.sockService.sendMessage({
type: socketMessageType,
message: data.toString(),
references: depIds,
});
await this.updateLog(depIds, data.toString());
});
cp.on('error', async (err) => {
this.sockService.sendMessage({
type: socketMessageType,
message: JSON.stringify(err),
references: depIds,
});
await this.updateLog(depIds, JSON.stringify(err));
resolve(null);
});
cp.on('close', async (code) => {
const endTime = dayjs();
const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败';
const message = `\n依赖${actionText}${resultText},结束时间 ${endTime.format(
const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format(
'YYYY-MM-DD HH:mm:ss',
)},耗时 ${endTime.diff(startTime, 'second')}`;
)}\n\n`;
this.sockService.sendMessage({
type: socketMessageType,
message,
references: depIds,
});
await this.updateLog(depIds, message);
this.updateLog(depIds, message);
let status = null;
if (isSucceed) {
status = isInstall
? DependenceStatus.installed
: DependenceStatus.removed;
} else {
status = isInstall
? DependenceStatus.installFailed
: DependenceStatus.removeFailed;
}
await DependenceModel.update({ status }, { where: { id: depIds } });
// 判断是否已经安装过依赖
if (isInstall) {
const getCommandPrefix = GetDependenceCommandTypes[dependency.type];
const depVersionStr = versionDependenceCommandTypes[dependency.type];
const [_depName, _depVersion] = dependency.name
.trim()
.split(depVersionStr);
const isNodeDependence = dependency.type === DependenceTypes.nodejs;
const isLinuxDependence = dependency.type === DependenceTypes.linux;
const isPythonDependence = dependency.type === DependenceTypes.python3;
const depInfo = (
await promiseExecSuccess(
isNodeDependence
? `${getCommandPrefix} | grep "${_depName}" | head -1`
: `${getCommandPrefix} ${_depName}`,
)
).replace(/\s{2,}/, ' ');
// 如果删除依赖成功或者强制删除
if ((isSucceed || force) && !isInstall) {
this.removeDb(depIds);
if (
depInfo &&
((isNodeDependence && depInfo.split(' ')?.[0] === _depName) ||
(isLinuxDependence && depInfo.toLocaleLowerCase().includes('installed')) ||
isPythonDependence) &&
(!_depVersion || depInfo.includes(_depVersion))
) {
const endTime = dayjs();
const _message = `检测到已经安装 ${_depName}\n\n${depInfo}\n跳过安装\n\n依赖${actionText}成功,结束时间 ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)},耗时 ${endTime.diff(startTime, 'second')}`;
this.sockService.sendMessage({
type: socketMessageType,
message: _message,
references: depIds,
});
this.updateLog(depIds, _message);
await DependenceModel.update(
{ status: DependenceStatus.installed },
{ where: { id: depIds } },
);
return resolve(null);
}
}
resolve(null);
const cp = spawn(`${depRunCommand} ${depName}`, {
shell: '/bin/bash',
});
cp.stdout.on('data', async (data) => {
this.sockService.sendMessage({
type: socketMessageType,
message: data.toString(),
references: depIds,
});
this.updateLog(depIds, data.toString());
});
cp.stderr.on('data', async (data) => {
this.sockService.sendMessage({
type: socketMessageType,
message: data.toString(),
references: depIds,
});
this.updateLog(depIds, data.toString());
});
cp.on('error', async (err) => {
this.sockService.sendMessage({
type: socketMessageType,
message: JSON.stringify(err),
references: depIds,
});
this.updateLog(depIds, JSON.stringify(err));
});
cp.on('close', async (code) => {
const endTime = dayjs();
const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败';
const message = `\n依赖${actionText}${resultText},结束时间 ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)},耗时 ${endTime.diff(startTime, 'second')}`;
this.sockService.sendMessage({
type: socketMessageType,
message,
references: depIds,
});
this.updateLog(depIds, message);
let status = null;
if (isSucceed) {
status = isInstall
? DependenceStatus.installed
: DependenceStatus.removed;
} else {
status = isInstall
? DependenceStatus.installFailed
: DependenceStatus.removeFailed;
}
await DependenceModel.update({ status }, { where: { id: depIds } });
// 如果删除依赖成功或者强制删除
if ((isSucceed || force) && !isInstall) {
this.removeDb(depIds);
}
resolve(null);
});
});
});
}
+66 -32
View File
@@ -1,11 +1,18 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { getFileContentByName } from '../config/util';
import config from '../config';
import * as fs from 'fs';
import { Env, EnvModel, EnvStatus, initEnvPosition } from '../data/env';
import _ from 'lodash';
import { Op } from 'sequelize';
import {
Env,
EnvModel,
EnvStatus,
initPosition,
maxPosition,
minPosition,
stepPosition,
} from '../data/env';
import groupBy from 'lodash/groupBy';
import { FindOptions, Op } from 'sequelize';
@Service()
export default class EnvService {
@@ -13,17 +20,22 @@ export default class EnvService {
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;
let position = initPosition;
if (
envs &&
envs.length > 0 &&
typeof envs[envs.length - 1].position === 'number'
) {
position = envs[envs.length - 1].position!;
}
const tabs = payloads.map((x) => {
position = position / 2;
position = position - stepPosition;
const tab = new Env({ ...x, position });
return tab;
});
const docs = await this.insert(tabs);
await this.set_envs();
await this.checkPosition(tabs[tabs.length - 1].position!);
return docs;
}
@@ -37,7 +49,9 @@ export default class EnvService {
}
public async update(payload: Env): Promise<Env> {
const newDoc = await this.updateDb(payload);
const doc = await this.getDb({ id: payload.id });
const tab = new Env({ ...doc, ...payload });
const newDoc = await this.updateDb(tab);
await this.set_envs();
return newDoc;
}
@@ -67,28 +81,47 @@ export default class EnvService {
const envs = await this.envs();
if (toIndex === 0 || toIndex === envs.length - 1) {
targetPosition = isUpward
? envs[0].position * 2
: envs[toIndex].position / 2;
? envs[0].position! + stepPosition
: envs[toIndex].position! - stepPosition;
} else {
targetPosition = isUpward
? (envs[toIndex].position + envs[toIndex - 1].position) / 2
: (envs[toIndex].position + envs[toIndex + 1].position) / 2;
? (envs[toIndex].position! + envs[toIndex - 1].position!) / 2
: (envs[toIndex].position! + envs[toIndex + 1].position!) / 2;
}
const newDoc = await this.update({
id,
position: targetPosition,
position: this.getPrecisionPosition(targetPosition),
});
await this.checkPosition(targetPosition, envs[toIndex].position!);
return newDoc;
}
public async envs(
searchText: string = '',
sort: any = { position: -1 },
query: any = {},
): Promise<Env[]> {
private async checkPosition(position: number, edge: number = 0) {
const precisionPosition = parseFloat(position.toPrecision(16));
if (
precisionPosition < minPosition ||
precisionPosition > maxPosition ||
Math.abs(precisionPosition - edge) < minPosition
) {
const envs = await this.envs();
let position = initPosition;
for (const env of envs) {
position = position - stepPosition;
await this.updateDb({ id: env.id, position });
}
}
}
private getPrecisionPosition(position: number): number {
return parseFloat(position.toPrecision(16));
}
public async envs(searchText: string = '', query: any = {}): Promise<Env[]> {
let condition = { ...query };
if (searchText) {
const encodeText = encodeURIComponent(searchText);
const encodeText = encodeURI(searchText);
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchText}%` },
@@ -112,7 +145,10 @@ export default class EnvService {
};
}
try {
const result = await this.find(condition, [['position', 'DESC']]);
const result = await this.find(condition, [
['position', 'DESC'],
['createdAt', 'ASC'],
]);
return result as any;
} catch (error) {
throw error;
@@ -127,7 +163,7 @@ export default class EnvService {
return docs;
}
public async getDb(query: any): Promise<Env> {
public async getDb(query: FindOptions<Env>['where']): Promise<Env> {
const doc: any = await EnvModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Env);
}
@@ -151,22 +187,20 @@ export default class EnvService {
}
public async set_envs() {
const envs = await this.envs(
'',
{ position: -1 },
{ name: { [Op.not]: null } },
);
const groups = _.groupBy(envs, 'name');
const envs = await this.envs('', {
name: { [Op.not]: null },
status: EnvStatus.normal,
});
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)) {
let value = _(group)
.filter((x) => x.status !== EnvStatus.disabled)
.map('value')
if (/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(key)) {
let value = group
.map((x) => x.value)
.join('&')
.replace(/(\\)[^n]/g, '\\\\')
.replace(/(\\$)/, '\\\\')
+427 -161
View File
@@ -6,6 +6,7 @@ import got from 'got';
import nodemailer from 'nodemailer';
import crypto from 'crypto';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
import { parseBody, parseHeaders } from '../config/util';
@Service()
export default class NotificationService {
@@ -23,15 +24,22 @@ export default class NotificationService {
['dingtalkBot', this.dingtalkBot],
['weWorkBot', this.weWorkBot],
['weWorkApp', this.weWorkApp],
['aibotk', this.aibotk],
['iGot', this.iGot],
['pushPlus', this.pushPlus],
['email', this.email],
['pushMe', this.pushMe],
['webhook', this.webhook],
['lark', this.lark],
]);
private timeout = 10000;
private title = '';
private content = '';
private params!: Omit<NotificationInfo, 'type'>;
private gotOption = {
timeout: 10000,
retry: 1,
};
constructor(@Inject('logger') private logger: winston.Logger) {}
@@ -71,35 +79,49 @@ export default class NotificationService {
}
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';
const { gotifyUrl, gotifyToken, gotifyPriority = 1 } = this.params;
try {
const res: any = await got
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
...this.gotOption,
body: `title=${encodeURIComponent(
this.title,
)}&message=${encodeURIComponent(
this.content,
)}&priority=${gotifyPriority}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
})
.json();
if (typeof res.id === 'number') {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async goCqHttpBot() {
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
const res: any = await got
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
timeout: this.timeout,
retry: 0,
json: { message: `${this.title}\n${this.content}` },
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
})
.json();
return res.retcode === 0;
try {
const res: any = await got
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
...this.gotOption,
json: { message: `${this.title}\n${this.content}` },
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
})
.json();
if (res.retcode === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async serverChan() {
@@ -107,67 +129,97 @@ export default class NotificationService {
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;
try {
const res: any = await got
.post(url, {
...this.gotOption,
body: `title=${this.title}&desp=${this.content}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
if (res.errno === 0 || res.data.errno === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async pushDeer() {
const { pushDeerKey } = this.params;
const url = `https://api2.pushdeer.com/message/push`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
this.title,
)}&desp=${encodeURIComponent(this.content)}&type=markdown`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return (
res.content.result.length !== undefined && res.content.result.length > 0
);
const { pushDeerKey, pushDeerUrl } = this.params;
const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`;
try {
const res: any = await got
.post(url, {
...this.gotOption,
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
this.title,
)}&desp=${encodeURIComponent(this.content)}&type=markdown`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
if (
res.content.result.length !== undefined &&
res.content.result.length > 0
) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async chat() {
const { chatUrl, chatToken } = this.params;
const url = `${chatUrl}${chatToken}`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
body: `payload={"text":"${this.title}\n${this.content}"}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return res.success;
try {
const res: any = await got
.post(url, {
...this.gotOption,
body: `payload={"text":"${this.title}\n${this.content}"}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
if (res.success) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async bark() {
let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
if (!barkPush.startsWith('http')) {
barkPush = `https://api.day.app/${barkPush}`;
}
const url = `${barkPush}/${encodeURIComponent(
this.title,
)}/${encodeURIComponent(
this.content,
)}?icon=${barkIcon}?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;
)}?icon=${barkIcon}&sound=${barkSound}&group=${barkGroup}`;
try {
const res: any = await got
.get(url, {
...this.gotOption,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
if (res.code === 200) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async telegramBot() {
@@ -199,16 +251,23 @@ export default class NotificationService {
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;
try {
const res: any = await got
.post(url, {
...this.gotOption,
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();
if (res.ok) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async dingtalkBot() {
@@ -222,48 +281,61 @@ export default class NotificationService {
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}`,
try {
const res: any = await got
.post(url, {
...this.gotOption,
json: {
msgtype: 'text',
text: {
content: ` ${this.title}\n\n${this.content}`,
},
},
},
})
.json();
return res.errcode === 0;
})
.json();
if (res.errcode === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
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}`,
const { weWorkBotKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } = this.params;
const url = `${weWorkOrigin}/cgi-bin/webhook/send?key=${weWorkBotKey}`;
try {
const res: any = await got
.post(url, {
...this.gotOption,
json: {
msgtype: 'text',
text: {
content: ` ${this.title}\n\n${this.content}`,
},
},
},
})
.json();
return res.errcode === 0;
})
.json();
if (res.errcode === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async weWorkApp() {
const { weWorkAppKey } = this.params;
const { weWorkAppKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } = this.params;
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
weWorkAppKey.split(',');
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
const url = `${weWorkOrigin}/cgi-bin/gettoken`;
const tokenRes: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
json: {
corpid,
corpsecret,
@@ -309,78 +381,272 @@ export default class NotificationService {
break;
}
const res: any = await got
.post(
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
{
timeout: this.timeout,
retry: 0,
json: {
touser,
agentid,
safe: '0',
...options,
try {
const res: any = await got
.post(
`${weWorkOrigin}/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
{
...this.gotOption,
json: {
touser,
agentid,
safe: '0',
...options,
},
},
},
)
.json();
)
.json();
return res.errcode === 0;
if (res.errcode === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async aibotk() {
const { aibotkKey, aibotkType, aibotkName } = this.params;
let url = '';
let json = {};
switch (aibotkType) {
case 'room':
url = 'https://api-bot.aibotk.com/openapi/v1/chat/room';
json = {
apiKey: `${aibotkKey}`,
roomName: `${aibotkName}`,
message: {
type: 1,
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
},
};
break;
case 'contact':
url = 'https://api-bot.aibotk.com/openapi/v1/chat/contact';
json = {
apiKey: `${aibotkKey}`,
name: `${aibotkName}`,
message: {
type: 1,
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
},
};
break;
}
try {
const res: any = await got
.post(url, {
...this.gotOption,
json: {
...json,
},
})
.json();
if (res.code === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
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();
try {
const res: any = await got
.post(url, {
...this.gotOption,
body: `title=${this.title}&content=${this.content}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return res.ret === 0;
if (res.ret === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
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();
try {
const res: any = await got
.post(url, {
...this.gotOption,
json: {
token: `${pushPlusToken}`,
title: `${this.title}`,
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
topic: `${pushPlusUser || ''}`,
},
})
.json();
return res.code === 200;
if (res.code === 200) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async lark() {
let { larkKey } = this.params;
if (!larkKey.startsWith('http')) {
larkKey = `https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`;
}
try {
const res: any = await got
.post(larkKey, {
...this.gotOption,
json: {
msg_type: 'text',
content: { text: `${this.title}\n\n${this.content}` },
},
headers: { 'Content-Type': 'application/json' },
})
.json();
if (res.StatusCode === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
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/>')}`,
});
try {
const transporter = nodemailer.createTransport({
service: emailService,
auth: {
user: emailUser,
pass: emailPass,
},
});
transporter.close();
const info = await transporter.sendMail({
from: `"青龙快讯" <${emailUser}>`,
to: `${emailUser}`,
subject: `${this.title}`,
html: `${this.content.replace(/\n/g, '<br/>')}`,
});
return !!info.messageId;
transporter.close();
if (info.messageId) {
return true;
} else {
throw new Error(JSON.stringify(info));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async pushMe() {
const { pushMeKey } = this.params;
try {
const res: any = await got
.post(`https://push.i-i.me/?push_key=${pushMeKey}`, {
...this.gotOption,
json: {
title: this.title,
content: this.content
},
headers: { 'Content-Type': 'application/json' },
});
if (res.body === 'success') {
return true;
} else {
throw new Error(res.body);
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async webhook() {
const {
webhookUrl,
webhookBody,
webhookHeaders,
webhookMethod,
webhookContentType,
} = this.params;
const { formatBody, formatUrl } = this.formatNotifyContent(
webhookUrl,
webhookBody,
);
if (!formatUrl && !formatBody) {
return false;
}
const headers = parseHeaders(webhookHeaders);
const body = parseBody(formatBody, webhookContentType);
const bodyParam = this.formatBody(webhookContentType, body);
const options = {
method: webhookMethod,
headers,
...this.gotOption,
allowGetBody: true,
...bodyParam,
};
try {
const res = await got(formatUrl, options);
if (String(res.statusCode).startsWith('20')) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private formatBody(contentType: string, body: any): object {
if (!body) return {};
switch (contentType) {
case 'application/json':
return { json: body };
case 'multipart/form-data':
return { form: body };
case 'application/x-www-form-urlencoded':
return { body };
}
return {};
}
private formatNotifyContent(url: string, body: string) {
if (!url.includes('$title') && !body.includes('$title')) {
return {};
}
return {
formatUrl: url
?.replaceAll('$title', encodeURIComponent(this.title))
?.replaceAll('$content', encodeURIComponent(this.content)),
formatBody: body
?.replaceAll('$title', this.title)
?.replaceAll('$content', this.content),
};
}
}
+2 -2
View File
@@ -75,7 +75,7 @@ export default class OpenService {
): Promise<App[]> {
let condition = { ...query };
if (searchText) {
const encodeText = encodeURIComponent(searchText);
const encodeText = encodeURI(searchText);
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchText}%` },
@@ -119,7 +119,7 @@ export default class OpenService {
(x) => x.expiration >= timestamp,
);
let tokens = invalidTokens;
if (invalidTokens.length > 5) {
if (invalidTokens.length >= 5) {
tokens = [
...invalidTokens.slice(0, 4),
{ ...invalidTokens[4], expiration },
+74 -66
View File
@@ -1,14 +1,16 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import nodeSchedule from 'node-schedule';
import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process';
import { ChildProcessWithoutNullStreams } from 'child_process';
import {
ToadScheduler,
LongIntervalJob,
AsyncTask,
SimpleIntervalSchedule,
Task,
} from 'toad-scheduler';
import dayjs from 'dayjs';
import taskLimit from '../shared/pLimit';
import { spawn } from 'cross-spawn';
interface ScheduleTaskType {
id: number;
@@ -40,69 +42,74 @@ export default class ScheduleService {
private maxBuffer = 200 * 1024 * 1024;
constructor(@Inject('logger') private logger: winston.Logger) {}
constructor(@Inject('logger') private logger: winston.Logger) { }
async runTask(command: string, callbacks: TaskCallbacks = {}) {
return new Promise(async (resolve, reject) => {
try {
const startTime = dayjs();
await callbacks.onBefore?.(startTime);
async runTask(
command: string,
callbacks: TaskCallbacks = {},
completionTime: 'start' | 'end' = 'end',
) {
return taskLimit.runWithCpuLimit(() => {
return new Promise(async (resolve, reject) => {
try {
const startTime = dayjs();
await callbacks.onBefore?.(startTime);
const cp = spawn(command, { shell: '/bin/bash' });
const cp = spawn(command, { shell: '/bin/bash' });
// TODO:
callbacks.onStart?.(cp, startTime);
callbacks.onStart?.(cp, startTime);
completionTime === 'start' && resolve(cp.pid);
cp.stdout.on('data', async (data) => {
await callbacks.onLog?.(data.toString());
});
cp.stdout.on('data', async (data) => {
await callbacks.onLog?.(data.toString());
});
cp.stderr.on('data', async (data) => {
this.logger.error(
'执行任务 %s 失败,时间:%s, 错误信息:%j',
cp.stderr.on('data', async (data) => {
this.logger.info(
'[执行任务失败] %s,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
data.toString(),
);
await callbacks.onError?.(data.toString());
});
cp.on('error', async (err) => {
this.logger.error(
'[创建任务失败] %s,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
err,
);
await callbacks.onError?.(JSON.stringify(err));
});
cp.on('exit', async (code, signal) => {
this.logger.info(
`[任务退出] ${command} 进程id: ${cp.pid},退出码 ${code}`,
);
});
cp.on('close', async (code) => {
const endTime = dayjs();
await callbacks.onEnd?.(
cp,
endTime,
endTime.diff(startTime, 'seconds'),
);
resolve(null);
});
} catch (error) {
await this.logger.error(
'执行任务%s失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
data.toString(),
error,
);
await callbacks.onError?.(data.toString());
});
cp.on('error', async (err) => {
this.logger.error(
'创建任务 %s 失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
err,
);
await callbacks.onError?.(JSON.stringify(err));
});
cp.on('exit', async (code, signal) => {
this.logger.info(
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
);
});
cp.on('close', async (code) => {
const endTime = dayjs();
await callbacks.onEnd?.(
cp,
endTime,
endTime.diff(startTime, 'seconds'),
);
resolve(null);
});
} catch (error) {
await this.logger.error(
'执行任务%s失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
error,
);
await callbacks.onError?.(JSON.stringify(error));
resolve(null);
}
});
await callbacks.onError?.(JSON.stringify(error));
}
});
})
}
async createCronTask(
@@ -122,19 +129,22 @@ export default class ScheduleService {
this.scheduleStacks.set(
_id,
nodeSchedule.scheduleJob(_id, schedule, async () => {
await this.runTask(command, callbacks);
this.runTask(command, callbacks);
}),
);
if (runImmediately) {
await this.runTask(command, callbacks);
this.runTask(command, callbacks);
}
}
async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
const _id = this.formatId(id);
this.logger.info('[取消定时任务],任务名:%s', name);
this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel();
if (this.scheduleStacks.has(_id)) {
this.scheduleStacks.get(_id)?.cancel();
this.scheduleStacks.delete(_id);
}
}
async createIntervalTask(
@@ -150,12 +160,10 @@ export default class ScheduleService {
name,
command,
);
const task = new AsyncTask(
const task = new Task(
name,
async () => {
return new Promise(async (resolve, reject) => {
await this.runTask(command, callbacks);
});
() => {
this.runTask(command, callbacks);
},
(err) => {
this.logger.error(
@@ -176,7 +184,7 @@ export default class ScheduleService {
this.intervalSchedule.addIntervalJob(job);
if (runImmediately) {
await this.runTask(command, callbacks);
this.runTask(command, callbacks);
}
}
+18 -14
View File
@@ -6,6 +6,8 @@ import SockService from './sock';
import CronService from './cron';
import ScheduleService, { TaskCallbacks } from './schedule';
import config from '../config';
import { TASK_COMMAND } from '../config/const';
import { getPid, killTask } from '../config/util';
@Service()
export default class ScriptService {
@@ -40,23 +42,25 @@ export default class ScriptService {
public async runScript(filePath: string) {
const relativePath = path.relative(config.scriptPath, filePath);
const command = `task -l ${relativePath} now`;
this.scheduleService.runTask(command, this.taskCallbacks(filePath));
const command = `${TASK_COMMAND} -l ${relativePath} now`;
const pid = await this.scheduleService.runTask(
command,
this.taskCallbacks(filePath),
'start',
);
return { code: 200 };
return { code: 200, data: pid };
}
public async stopScript(filePath: string) {
const relativePath = path.relative(config.scriptPath, filePath);
const err = await this.cronService.killTask(`task -l ${relativePath} now`);
const str = err ? `\n${err}` : '';
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: `${str}\n## 执行结束... ${new Date()
.toLocaleString('zh', { hour12: false })
.replace(' 24:', ' 00:')} `,
});
public async stopScript(filePath: string, pid: number) {
let str = '';
if (!pid) {
const relativePath = path.relative(config.scriptPath, filePath);
pid = await getPid(`${TASK_COMMAND} -l ${relativePath} now`);
}
try {
await killTask(pid);
} catch (error) {}
return { code: 200 };
}
+73 -41
View File
@@ -1,20 +1,42 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import fs from 'fs';
import fs, { existsSync } from 'fs';
import os from 'os';
import path from 'path';
import { Subscription } from '../data/subscription';
import { formatUrl } from '../config/subscription';
import config from '../config';
@Service()
export default class SshKeyService {
private homedir = os.homedir();
private sshPath = path.resolve(this.homedir, '.ssh');
private sshConfigFilePath = path.resolve(this.sshPath, 'config');
private sshPath = config.sshdPath;
private sshConfigFilePath = path.resolve(this.homedir, '.ssh', 'config');
private sshConfigHeader = `Include ${path.join(this.sshPath, '*.config')}`;
constructor(@Inject('logger') private logger: winston.Logger) {}
constructor(@Inject('logger') private logger: winston.Logger) {
this.initSshConfigFile();
}
private initSshConfigFile() {
let config = '';
if (existsSync(this.sshConfigFilePath)) {
config = fs.readFileSync(this.sshConfigFilePath, { encoding: 'utf-8' });
} else {
fs.writeFileSync(this.sshConfigFilePath, '');
}
if (!config.includes(this.sshConfigHeader)) {
fs.writeFileSync(
this.sshConfigFilePath,
`${this.sshConfigHeader}\n\n${config}`,
{ encoding: 'utf-8' },
);
}
}
private generatePrivateKeyFile(alias: string, key: string): void {
try {
fs.writeFileSync(`${this.sshPath}/${alias}`, key, {
fs.writeFileSync(path.join(this.sshPath, alias), `${key}${os.EOL}`, {
encoding: 'utf8',
mode: '400',
});
@@ -23,62 +45,72 @@ export default class SshKeyService {
}
}
private getConfigRegx(alias: string) {
return new RegExp(
`Host ${alias}\n.*[^StrictHostKeyChecking]*.*[\n]*.*StrictHostKeyChecking no`,
'g',
);
}
private removePrivateKeyFile(alias: string): void {
try {
fs.unlinkSync(`${this.sshPath}/${alias}`);
const filePath = path.join(this.sshPath, alias);
if (existsSync(filePath)) {
fs.unlinkSync(filePath);
}
} catch (error) {
this.logger.error('删除私钥文件失败', error);
}
}
private generateSingleSshConfig(alias: string, host: string): string {
return `\nHost ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no`;
}
private generateSshConfig(configs: string[]) {
try {
for (const config of configs) {
fs.appendFileSync(this.sshConfigFilePath, config, {
encoding: 'utf8',
});
}
} catch (error) {
this.logger.error('写入ssh配置文件失败', error);
private generateSingleSshConfig(alias: string, host: string, proxy?: string) {
if (host === 'github.com') {
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa\n PubkeyAcceptedAlgorithms +ssh-rsa`;
}
const proxyStr = proxy
? ` ProxyCommand nc -v -x ${proxy} %h %p 2>/dev/null\n`
: '';
const config = `Host ${alias}\n Hostname ${host}\n IdentityFile ${path.join(
this.sshPath,
alias,
)}\n StrictHostKeyChecking no\n${proxyStr}`;
fs.writeFileSync(`${path.join(this.sshPath, `${alias}.config`)}`, config, {
encoding: 'utf8',
});
}
private removeSshConfig(alias: string) {
try {
const configRegx = this.getConfigRegx(alias);
const data = fs
.readFileSync(this.sshConfigFilePath, { encoding: 'utf8' })
.replace(configRegx, '')
.replace(/\n[\n]+/g, '\n');
fs.writeFileSync(this.sshConfigFilePath, data, {
encoding: 'utf8',
});
const filePath = path.join(this.sshPath, `${alias}.config`);
if (existsSync(filePath)) {
fs.unlinkSync(filePath);
}
} catch (error) {
this.logger.error(`删除ssh配置文件${alias}失败`, error);
}
}
public addSSHKey(key: string, alias: string, host: string): void {
public addSSHKey(
key: string,
alias: string,
host: string,
proxy?: string,
): void {
this.generatePrivateKeyFile(alias, key);
const config = this.generateSingleSshConfig(alias, host);
this.removeSshConfig(alias);
this.generateSshConfig([config]);
this.generateSingleSshConfig(alias, host, proxy);
}
public removeSSHKey(alias: string, host: string): void {
public removeSSHKey(alias: string, host: string, proxy?: string): void {
this.removePrivateKeyFile(alias);
const config = this.generateSingleSshConfig(alias, host);
this.removeSshConfig(config);
this.removeSshConfig(alias);
}
public setSshConfig(docs: Subscription[]) {
for (const doc of docs) {
if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {
const { alias, proxy } = doc;
const { host } = formatUrl(doc);
this.removePrivateKeyFile(alias);
this.removeSshConfig(alias);
this.generatePrivateKeyFile(
alias,
(doc.pull_option as any).private_key,
);
this.generateSingleSshConfig(alias, host, proxy);
}
}
}
}
+50 -141
View File
@@ -6,29 +6,27 @@ import {
SubscriptionModel,
SubscriptionStatus,
} from '../data/subscription';
import {
ChildProcessWithoutNullStreams,
exec,
execSync,
spawn,
} from 'child_process';
import { ChildProcessWithoutNullStreams } from 'child_process';
import fs from 'fs';
import cron_parser from 'cron-parser';
import {
getFileContentByName,
concurrentRun,
fileExist,
createFile,
killTask,
handleLogPath,
promiseExec,
} from '../config/util';
import { promises, existsSync } from 'fs';
import { promisify } from 'util';
import { Op } from 'sequelize';
import { FindOptions, Op } from 'sequelize';
import path from 'path';
import ScheduleService, { TaskCallbacks } from './schedule';
import { SimpleIntervalSchedule } from 'toad-scheduler';
import SockService from './sock';
import SshKeyService from './sshKey';
import dayjs from 'dayjs';
import { LOG_END_SYMBOL } from '../config/const';
import { formatCommand, formatUrl } from '../config/subscription';
@Service()
export default class SubscriptionService {
@@ -45,7 +43,7 @@ export default class SubscriptionService {
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchText}%` },
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
{ [Op.like]: `%${encodeURI(searchText)}%` },
],
};
query = {
@@ -62,64 +60,25 @@ export default class SubscriptionService {
try {
const result = await SubscriptionModel.findAll({
where: query,
order: [['createdAt', 'DESC']],
order: [
['is_disabled', 'ASC'],
['createdAt', 'DESC'],
],
});
return result as any;
return result;
} catch (error) {
throw error;
}
}
private formatCommand(doc: Subscription, url?: string) {
let command = 'ql ';
let _url = url || this.formatUrl(doc).url;
const { type, whitelist, blacklist, dependences, branch, extensions } = doc;
if (type === 'file') {
command += `raw "${_url}"`;
} else {
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
dependences || ''
}" "${branch || ''}" "${extensions || ''}"`;
}
return command;
}
private formatUrl(doc: Subscription) {
let url = doc.url;
let host = '';
if (doc.type === 'private-repo') {
if (doc.pull_type === 'ssh-key') {
host = doc.url!.replace(/.*\@([^\:]+)\:.*/, '$1');
url = doc.url!.replace(host, doc.alias);
} else {
host = doc.url!.replace(/.*\:\/\/([^\/]+)\/.*/, '$1');
const { username, password } = doc.pull_option as any;
url = doc.url!.replace(host, `${username}:${password}@${host}`);
}
}
return { url, host };
}
public async handleTask(
doc: Subscription,
needCreate = true,
needAddKey = true,
runImmediately = false,
) {
const { url, host } = this.formatUrl(doc);
if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {
if (needAddKey) {
this.sshKeyService.addSSHKey(
(doc.pull_option as any).private_key,
doc.alias,
host,
);
} else {
this.sshKeyService.removeSSHKey(doc.alias, host);
}
}
const { url } = formatUrl(doc);
doc.command = this.formatCommand(doc, url as string);
doc.command = formatCommand(doc, url as string);
if (doc.schedule_type === 'crontab') {
this.scheduleService.cancelCronTask(doc as any);
@@ -142,28 +101,9 @@ export default class SubscriptionService {
}
}
private async promiseExec(command: string): Promise<string> {
return new Promise((resolve, reject) => {
exec(
command,
{ maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' },
(err, stdout, stderr) => {
resolve(stdout || stderr || JSON.stringify(err));
},
);
});
}
private async handleLogPath(
logPath: string,
data: string = '',
): Promise<string> {
const absolutePath = path.resolve(config.logPath, logPath);
const logFileExist = await fileExist(absolutePath);
if (!logFileExist) {
await createFile(absolutePath, data);
}
return absolutePath;
public async setSshConfig() {
const docs = await SubscriptionModel.findAll();
this.sshKeyService.setSshConfig(docs);
}
private taskCallbacks(doc: Subscription): TaskCallbacks {
@@ -178,7 +118,7 @@ export default class SubscriptionService {
},
{ where: { id: doc.id } },
);
const absolutePath = await this.handleLogPath(
const absolutePath = await handleLogPath(
logPath as string,
`## 开始执行... ${startTime.format('YYYY-MM-DD HH:mm:ss')}\n`,
);
@@ -188,7 +128,7 @@ export default class SubscriptionService {
try {
if (doc.sub_before) {
fs.appendFileSync(absolutePath, `\n## 执行before命令...\n\n`);
beforeStr = await this.promiseExec(doc.sub_before);
beforeStr = await promiseExec(doc.sub_before);
}
} catch (error: any) {
beforeStr =
@@ -208,14 +148,14 @@ export default class SubscriptionService {
},
onEnd: async (cp, endTime, diff) => {
const sub = await this.getDb({ id: doc.id });
const absolutePath = await this.handleLogPath(sub.log_path as string);
const absolutePath = await handleLogPath(sub.log_path as string);
// 执行 sub_after
let afterStr = '';
try {
if (sub.sub_after) {
fs.appendFileSync(absolutePath, `\n\n## 执行after命令...\n\n`);
afterStr = await this.promiseExec(sub.sub_after);
afterStr = await promiseExec(sub.sub_after);
}
} catch (error: any) {
afterStr =
@@ -229,7 +169,7 @@ export default class SubscriptionService {
absolutePath,
`\n## 执行结束... ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)} 耗时 ${diff}`,
)} 耗时 ${diff}${LOG_END_SYMBOL}`,
);
await SubscriptionModel.update(
@@ -245,12 +185,12 @@ export default class SubscriptionService {
},
onError: async (message: string) => {
const sub = await this.getDb({ id: doc.id });
const absolutePath = await this.handleLogPath(sub.log_path as string);
const absolutePath = await handleLogPath(sub.log_path as string);
fs.appendFileSync(absolutePath, `\n${message}`);
},
onLog: async (message: string) => {
const sub = await this.getDb({ id: doc.id });
const absolutePath = await this.handleLogPath(sub.log_path as string);
const absolutePath = await handleLogPath(sub.log_path as string);
fs.appendFileSync(absolutePath, `\n${message}`);
},
};
@@ -260,6 +200,7 @@ export default class SubscriptionService {
const tab = new Subscription(payload);
const doc = await this.insert(tab);
await this.handleTask(doc);
await this.setSshConfig();
return doc;
}
@@ -268,8 +209,11 @@ export default class SubscriptionService {
}
public async update(payload: Subscription): Promise<Subscription> {
const newDoc = await this.updateDb(payload);
const doc = await this.getDb({ id: payload.id });
const tab = new Subscription({ ...doc, ...payload });
const newDoc = await this.updateDb(tab);
await this.handleTask(newDoc, !newDoc.is_disabled);
await this.setSshConfig();
return newDoc;
}
@@ -312,12 +256,15 @@ export default class SubscriptionService {
public async remove(ids: number[]) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) {
await this.handleTask(doc, false, false);
await this.handleTask(doc, false);
}
await SubscriptionModel.destroy({ where: { id: ids } });
await this.setSshConfig();
}
public async getDb(query: any): Promise<Subscription> {
public async getDb(
query: FindOptions<Subscription>['where'],
): Promise<Subscription> {
const doc: any = await SubscriptionModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Subscription);
}
@@ -327,10 +274,9 @@ export default class SubscriptionService {
{ status: SubscriptionStatus.queued },
{ where: { id: ids } },
);
concurrentRun(
ids.map((id) => async () => await this.runSingle(id)),
10,
);
ids.forEach((id) => {
this.runSingle(id);
});
}
public async stop(ids: number[]) {
@@ -338,19 +284,18 @@ export default class SubscriptionService {
for (const doc of docs) {
if (doc.pid) {
try {
process.kill(-doc.pid);
await killTask(doc.pid);
} catch (error) {
this.logger.silly(error);
}
}
const command = this.formatCommand(doc);
const err = await this.killTask(command);
const absolutePath = await this.handleLogPath(doc.log_path as string);
const str = err ? `\n${err}` : '';
const absolutePath = await handleLogPath(doc.log_path as string);
fs.appendFileSync(
`${absolutePath}`,
`${str}\n## 执行结束... ${dayjs().format('YYYY-MM-DD HH:mm:ss')} `,
`\n## 执行结束... ${dayjs().format(
'YYYY-MM-DD HH:mm:ss',
)}${LOG_END_SYMBOL}`,
);
}
@@ -360,69 +305,33 @@ export default class SubscriptionService {
);
}
public async killTask(name: string) {
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
const execAsync = promisify(exec);
try {
let pid = (await execAsync(taskCommand)).stdout;
if (pid) {
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
} else {
return;
}
let pids = pid.match(/\(\d+/g);
const killLogs = [];
if (pids && pids.length > 0) {
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
for (const id of pids) {
const c = `kill -9 ${id.slice(1)}`;
try {
const { stdout, stderr } = await execAsync(c);
if (stderr) {
killLogs.push(stderr);
}
if (stdout) {
killLogs.push(stdout);
}
} catch (error: any) {
killLogs.push(error.message);
}
}
}
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
} catch (e) {
return JSON.stringify(e);
}
}
private async runSingle(subscriptionId: number) {
const subscription = await this.getDb({ id: subscriptionId });
if (subscription.status !== SubscriptionStatus.queued) {
return;
}
const command = this.formatCommand(subscription);
const command = formatCommand(subscription);
await this.scheduleService.runTask(
command,
this.taskCallbacks(subscription),
);
this.scheduleService.runTask(command, this.taskCallbacks(subscription));
}
public async disabled(ids: number[]) {
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
await this.setSshConfig();
for (const doc of docs) {
await this.handleTask(doc, false);
}
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
}
public async enabled(ids: number[]) {
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
await this.setSshConfig();
for (const doc of docs) {
await this.handleTask(doc);
}
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
}
public async log(id: number) {
@@ -431,7 +340,7 @@ export default class SubscriptionService {
return '';
}
const absolutePath = await this.handleLogPath(doc.log_path as string);
const absolutePath = await handleLogPath(doc.log_path as string);
return getFileContentByName(absolutePath);
}
+151 -53
View File
@@ -1,15 +1,31 @@
import { Response } from 'express';
import { Service, Inject } from 'typedi';
import winston from 'winston';
import config from '../config';
import * as fs from 'fs';
import _ from 'lodash';
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
import {
AuthDataType,
AuthInfo,
AuthInstance,
AuthModel,
AuthModelInfo,
} from '../data/auth';
import { NotificationInfo } from '../data/notify';
import NotificationService from './notify';
import ScheduleService from './schedule';
import { spawn } from 'child_process';
import ScheduleService, { TaskCallbacks } from './schedule';
import { spawn } from 'cross-spawn';
import SockService from './sock';
import got from 'got';
import {
getPid,
killTask,
parseContentVersion,
parseVersion,
promiseExec,
} from '../config/util';
import { TASK_COMMAND } from '../config/const';
import taskLimit from '../shared/pLimit';
import tar from 'tar';
import path from 'path';
@Service()
export default class SystemService {
@@ -20,22 +36,22 @@ export default class SystemService {
@Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService,
private sockService: SockService,
) {}
) { }
public async getLogRemoveFrequency() {
const doc = await this.getDb({ type: AuthDataType.removeLogFrequency });
return doc || {};
public async getSystemConfig() {
const doc = await this.getDb({ type: AuthDataType.systemConfig });
return doc || ({} as AuthInstance);
}
private async updateAuthDb(payload: AuthInfo): Promise<any> {
private async updateAuthDb(payload: AuthInfo): Promise<AuthInstance> {
await AuthModel.upsert({ ...payload });
const doc = await this.getDb({ type: payload.type });
return doc;
}
public async getDb(query: any): Promise<any> {
public async getDb(query: any): Promise<AuthInstance> {
const doc: any = await AuthModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as any);
return doc && doc.get({ plain: true });
}
public async updateNotificationMode(notificationInfo: NotificationInfo) {
@@ -56,56 +72,61 @@ export default class SystemService {
}
}
public async updateLogRemoveFrequency(frequency: number) {
const oDoc = await this.getLogRemoveFrequency();
public async updateSystemConfig(info: AuthModelInfo) {
const oDoc = await this.getSystemConfig();
const result = await this.updateAuthDb({
...oDoc,
type: AuthDataType.removeLogFrequency,
info: { frequency },
type: AuthDataType.systemConfig,
info,
});
const cron = {
id: result.id,
name: '删除日志',
command: `ql rmlog ${frequency}`,
};
await this.scheduleService.cancelIntervalTask(cron);
if (frequency > 0) {
await this.scheduleService.createIntervalTask(cron, {
days: frequency,
runImmediately: true,
});
if (info.logRemoveFrequency) {
const cron = {
id: result.id,
name: '删除日志',
command: `ql rmlog ${info.logRemoveFrequency}`,
};
await this.scheduleService.cancelIntervalTask(cron);
if (info.logRemoveFrequency > 0) {
this.scheduleService.createIntervalTask(cron, {
days: info.logRemoveFrequency,
});
}
}
return { code: 200, data: { ...cron } };
if (info.cronConcurrency) {
await taskLimit.setCustomLimit(info.cronConcurrency);
}
return { code: 200, data: info };
}
public async checkUpdate() {
try {
const versionRegx = /.*export const version = \'(.*)\'\;/;
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
const currentVersionContent = await parseVersion(config.versionFile);
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
let lastVersion = '';
let lastLog = '';
let lastVersionContent;
try {
const result = await got.get(config.lastVersionFile, {
timeout: 6000,
retry: 0,
});
const lastVersionFileContent = result.body;
lastVersion = lastVersionFileContent.match(versionRegx)![1];
lastLog = lastVersionFileContent.match(logRegx)
? lastVersionFileContent.match(logRegx)![1]
: '';
} catch (error) {}
const result = await got.get(
`${config.lastVersionFile}?t=${Date.now()}`,
{
timeout: 30000,
},
);
lastVersionContent = await parseContentVersion(result.body);
} catch (error) { }
if (!lastVersionContent) {
lastVersionContent = currentVersionContent;
}
return {
code: 200,
data: {
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion),
lastVersion,
lastLog,
hasNewVersion: this.checkHasNewVersion(
currentVersionContent.version,
lastVersionContent.version,
),
lastVersion: lastVersionContent.version,
lastLog: lastVersionContent.changeLog,
lastLogLink: lastVersionContent.changeLogLink,
},
};
} catch (error: any) {
@@ -136,12 +157,8 @@ export default class SystemService {
}
public async updateSystem() {
const cp = spawn('ql -l update', { shell: '/bin/bash' });
const cp = spawn('ql -l update false', { shell: '/bin/bash' });
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: `开始更新系统`,
});
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
@@ -166,6 +183,33 @@ export default class SystemService {
return { code: 200 };
}
public async reloadSystem(target: 'system' | 'data') {
const cp = spawn(`ql -l reload ${target || ''}`, { shell: '/bin/bash' });
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'reloadSystem',
message: data.toString(),
});
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'reloadSystem',
message: data.toString(),
});
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'reloadSystem',
message: JSON.stringify(err),
});
});
return { code: 200 };
}
public async notify({ title, content }: { title: string; content: string }) {
const isSuccess = await this.notificationService.notify(title, content);
if (isSuccess) {
@@ -174,4 +218,58 @@ export default class SystemService {
return { code: 400, message: '通知发送失败,请检查系统设置/通知配置' };
}
}
public async run(
{ command, logPath }: { command: string; logPath: string },
callback: TaskCallbacks,
) {
if (!command.startsWith(TASK_COMMAND)) {
command = `${TASK_COMMAND} ${command}`;
}
this.scheduleService.runTask(
`real_log_path=${logPath} real_time=true ${command}`,
callback,
);
}
public async stop({ command, pid }: { command: string; pid: number }) {
if (!pid && !command) {
return { code: 400, message: '参数错误' };
}
if (pid) {
await killTask(pid);
return { code: 200 };
}
if (!command.startsWith(TASK_COMMAND)) {
command = `${TASK_COMMAND} ${command}`;
}
const _pid = await getPid(command);
if (_pid) {
await killTask(_pid);
return { code: 200 };
} else {
return { code: 400, message: '任务未找到' };
}
}
public async exportData(res: Response) {
try {
await tar.create({ gzip: true, file: config.dataTgzFile, cwd: config.rootPath }, ['data'])
res.download(config.dataTgzFile);
} catch (error: any) {
return res.send({ code: 400, message: error.message });
}
}
public async importData() {
try {
await promiseExec(`rm -rf ${path.join(config.tmpPath, 'data')}`);
await tar.x({ file: config.dataTgzFile, cwd: config.tmpPath });
return { code: 200 };
} catch (error: any) {
return { code: 400, message: error.message };
}
}
}
+59 -40
View File
@@ -1,17 +1,20 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { createRandomString, getNetIp, getPlatform } from '../config/util';
import {
createRandomString,
fileExist,
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 { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
import { AuthDataType, AuthInfo, AuthModel, AuthModelInfo, 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 dayjs from 'dayjs';
@@ -24,7 +27,7 @@ export default class UserService {
@Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService,
private sockService: SockService,
) {}
) { }
public async login(
payloads: {
@@ -65,32 +68,37 @@ export default class UserService {
return this.initAuthInfo();
}
if (retries > 2 && Date.now() - lastlogon < Math.pow(3, retries) * 1000) {
const retriesTime = Math.pow(3, retries) * 1000;
if (retries > 2 && timestamp - lastlogon < retriesTime) {
const waitTime = Math.ceil(
(retriesTime - (timestamp - lastlogon)) / 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,
),
message: `失败次数过多,请${waitTime}秒后重试`,
data: waitTime,
};
}
if (
username === cUsername &&
password === cPassword &&
twoFactorActivated &&
needTwoFactor
) {
this.updateAuthInfo(content, {
isTwoFactorChecking: true,
});
return {
code: 420,
message: '',
};
}
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;
const expiration = twoFactorActivated ? 60 : 20;
let token = jwt.sign({ data }, config.secret as any, {
expiresIn: 60 * 60 * 24 * expiration,
algorithm: 'HS384',
@@ -111,8 +119,7 @@ export default class UserService {
});
await this.notificationService.notify(
'登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${req.platform
}端 登录成功,ip地址 ${ip}`,
);
await this.getLoginLog();
@@ -140,8 +147,7 @@ export default class UserService {
});
await this.notificationService.notify(
'登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${req.platform
}端 登录失败,ip地址 ${ip}`,
);
await this.getLoginLog();
@@ -155,7 +161,16 @@ export default class UserService {
status: LoginStatus.fail,
},
});
return { code: 400, message: config.authError };
if (retries > 2) {
const waitTime = Math.round(Math.pow(3, retries + 1));
return {
code: 410,
message: `失败次数过多,请${waitTime}秒后重试`,
data: waitTime,
};
} else {
return { code: 400, message: config.authError };
}
}
} else {
return this.initAuthInfo();
@@ -170,12 +185,12 @@ export default class UserService {
});
}
public async getLoginLog(): Promise<AuthInfo[]> {
public async getLoginLog(): Promise<Array<AuthModelInfo | undefined>> {
const docs = await AuthModel.findAll({
where: { type: AuthDataType.loginLog },
});
if (docs && docs.length > 0) {
const result = docs.sort((a, b) => b.info.timestamp - a.info.timestamp);
const result = docs.sort((a, b) => b.info!.timestamp! - a.info!.timestamp!);
if (result.length > 100) {
await AuthModel.destroy({
where: { id: result[result.length - 1].id },
@@ -192,17 +207,16 @@ export default class UserService {
}
private initAuthInfo() {
const newPassword = createRandomString(16, 22);
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({
username: 'admin',
password: newPassword,
password: 'admin',
}),
);
return {
code: 100,
message: '已初始化密码,请前往auth.json查看并重新登录',
message: '未找到认证文件,重新初始化',
};
}
@@ -227,13 +241,18 @@ export default class UserService {
return { code: 200, data: avatar, 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 async getUserInfo(): Promise<any> {
const authFileExist = await fileExist(config.authConfigFile);
if (!authFileExist) {
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({
username: 'admin',
password: 'admin',
}),
);
}
return this.getAuthInfo();
}
public initTwoFactor() {
+38
View File
@@ -0,0 +1,38 @@
import pLimit from "p-limit";
import os from 'os';
import { AuthDataType, AuthModel } from "../data/auth";
class TaskLimit {
private oneLimit = pLimit(1);
private updateLogLimit = pLimit(1);
private cpuLimit = pLimit(Math.max(os.cpus().length, 4));
constructor() {
this.setCustomLimit();
}
public async setCustomLimit(limit?: number) {
if (limit) {
this.cpuLimit = pLimit(limit);
return;
}
const doc = await AuthModel.findOne({ where: { type: AuthDataType.systemConfig } });
if (doc?.info?.cronConcurrency) {
this.cpuLimit = pLimit(doc?.info?.cronConcurrency);
}
}
public runWithCpuLimit<T>(fn: () => Promise<T>): Promise<T> {
return this.cpuLimit(fn);
}
public runOneByOne<T>(fn: () => Promise<T>): Promise<T> {
return this.oneLimit(fn);
}
public updateDepLog<T>(fn: () => Promise<T>): Promise<T> {
return this.updateLogLimit(fn);
}
}
export default new TaskLimit();
+37
View File
@@ -0,0 +1,37 @@
import { spawn } from 'cross-spawn';
import taskLimit from "./pLimit";
import Logger from '../loaders/logger';
export function runCron(cmd: string): Promise<number> {
return taskLimit.runWithCpuLimit(() => {
return new Promise(async (resolve: any) => {
Logger.silly('运行命令: ' + cmd);
const cp = spawn(cmd, { shell: '/bin/bash' });
cp.stderr.on('data', (data) => {
Logger.info(
'[执行任务失败] %s,时间:%s, 错误信息:%j',
cmd,
new Date().toLocaleString(),
data.toString(),
);
});
cp.on('error', (err) => {
Logger.error(
'[创建任务失败] %s,时间:%s, 错误信息:%j',
cmd,
new Date().toLocaleString(),
err,
);
});
cp.on('close', async (code) => {
Logger.info(
`[任务退出] ${cmd} 进程id: ${cp.pid} 退出,退出码 ${code}`,
);
resolve();
});
});
})
}
+9 -3
View File
@@ -5,6 +5,7 @@ import LoggerInstance from './loaders/logger';
import fs from 'fs';
import config from './config';
import path from 'path';
import os from 'os';
const tokenFile = path.join(config.configPath, 'token.json');
@@ -25,9 +26,14 @@ async function getToken() {
async function writeFile(data: any) {
return new Promise<void>((resolve, reject) => {
fs.writeFile(tokenFile, JSON.stringify(data), { encoding: 'utf8' }, () => {
resolve();
});
fs.writeFile(
tokenFile,
`${JSON.stringify(data)}${os.EOL}`,
{ encoding: 'utf8' },
() => {
resolve();
},
);
});
}
+66 -48
View File
@@ -1,4 +1,13 @@
FROM python:alpine
FROM python:3.10-alpine as builder
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
RUN set -x \
&& apk update \
&& apk add nodejs npm git \
&& npm i -g pnpm@8.3.1 \
&& cd /tmp/build \
&& pnpm --registry https://registry.npmmirror.com install --prod
FROM python:3.10-alpine
ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}"
@@ -6,55 +15,64 @@ ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
ARG QL_BRANCH=develop
ENV PNPM_HOME=/root/.local/share/pnpm \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
LANG=zh_CN.UTF-8 \
SHELL=/bin/bash \
PS1="\u@\h:\w \$ " \
QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH}
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules \
LANG=C.UTF-8 \
SHELL=/bin/bash \
PS1="\u@\h:\w \$ " \
QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH}
RUN set -x \
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk update -f \
&& apk upgrade \
&& apk --no-cache add -f bash \
coreutils \
git \
curl \
wget \
tzdata \
perl \
openssl \
nginx \
nodejs \
jq \
openssh \
procps \
netcat-openbsd \
npm \
&& rm -rf /var/cache/apk/* \
&& apk update \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& git config --global user.email "qinglong@@users.noreply.github.com" \
&& git config --global user.name "qinglong" \
&& git config --global http.postBuffer 524288000 \
&& npm install -g pnpm@8.3.1 \
&& cd && pnpm config set registry https://registry.npmmirror.com \
&& pnpm add -g pm2 tsx \
&& rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \
&& rm -rf /root/.cache \
&& rm -rf /root/.npm
ARG SOURCE_COMMIT
RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
&& cd ${QL_DIR} \
&& cp -f .env.example .env \
&& chmod 777 ${QL_DIR}/shell/*.sh \
&& chmod 777 ${QL_DIR}/docker/*.sh \
&& git clone --depth=1 -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
&& mkdir -p ${QL_DIR}/static \
&& cp -rf /static/* ${QL_DIR}/static \
&& rm -rf /static
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
WORKDIR ${QL_DIR}
RUN set -x \
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk update -f \
&& apk upgrade \
&& apk --no-cache add -f bash \
coreutils \
moreutils \
git \
curl \
wget \
tzdata \
perl \
openssl \
nginx \
nodejs \
jq \
openssh \
npm \
&& rm -rf /var/cache/apk/* \
&& apk update \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& git config --global user.email "qinglong@@users.noreply.github.com" \
&& git config --global user.name "qinglong" \
&& git config --global http.postBuffer 524288000 \
&& npm install -g pnpm \
&& pnpm add -g pm2 ts-node typescript tslib \
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
&& cd ${QL_DIR} \
&& cp -f .env.example .env \
&& chmod 777 ${QL_DIR}/shell/*.sh \
&& chmod 777 ${QL_DIR}/docker/*.sh \
&& pnpm install --prod \
&& rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \
&& rm -rf /root/.cache \
&& rm -rf /root/.npm \
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
&& mkdir -p ${QL_DIR}/static \
&& cp -rf /static/* ${QL_DIR}/static \
&& rm -rf /static
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
CMD curl -sf http://127.0.0.1:5400/api/health || exit 1
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
+6
View File
@@ -1,9 +1,15 @@
version: '2'
services:
web:
# alpine 基础镜像版本
image: whyour/qinglong:latest
# debian-slim 基础镜像版本
# image: whyour/qinglong:debian
volumes:
- ./data:/ql/data
ports:
- "0.0.0.0:5700:5700"
environment:
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
QlBaseUrl: '/'
restart: unless-stopped
+20 -28
View File
@@ -7,57 +7,49 @@ link_shell
echo -e "======================1. 检测配置文件========================\n"
make_dir /etc/nginx/conf.d
make_dir /run/nginx
cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
init_nginx
fix_config
pm2 l &>/dev/null
echo -e "======================2. 安装依赖========================\n"
patch_version
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
npm config set registry $NpmMirror
current_npm_registry=$(cd && pnpm config get registry)
is_equal_registry=$(echo $current_npm_registry | grep "${NpmMirror}")
if [[ "$is_equal_registry" == "" ]]; then
cd && pnpm config set registry $NpmMirror
pnpm install -g
fi
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts
fi
echo
echo -e "======================2. 安装依赖========================\n"
update_depend
echo
echo -e "======================3. 启动nginx========================\n"
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "nginx启动成功...\n"
echo -e "======================4. 启动面板监控========================\n"
pm2 delete public &>/dev/null
pm2 start $dir_static/build/public.js -n public --source-map-support --time
echo -e "监控服务启动成功...\n"
echo -e "======================5. 启动控制面板========================\n"
pm2 delete panel &>/dev/null
pm2 start $dir_static/build/app.js -n panel --source-map-support --time
echo -e "控制面板启动成功...\n"
echo -e "======================6. 启动定时任务========================\n"
pm2 delete schedule &>/dev/null
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time
echo -e "定时任务启动成功...\n"
echo -e "======================4. 启动pm2服务========================\n"
reload_pm2
if [[ $AutoStartBot == true ]]; then
echo -e "======================7. 启动bot========================\n"
nohup ql bot >>$dir_log/start.log 2>&1 &
echo -e "======================5. 启动bot========================\n"
nohup ql -l bot >$dir_log/bot.log 2>&1 &
echo -e "bot后台启动中...\n"
fi
if [[ $EnableExtraShell == true ]]; then
echo -e "======================8. 执行自定义脚本========================\n"
nohup ql extra >>$dir_log/start.log 2>&1 &
echo -e "====================6. 执行自定义脚本========================\n"
nohup ql -l extra >$dir_log/extra.log 2>&1 &
echo -e "自定义脚本后台执行中...\n"
fi
echo -e "############################################################\n"
echo -e "容器启动成功..."
echo -e "\n请先访问5700端口,登录成功面板之后再执行添加定时任务..."
echo -e "############################################################\n"
crond -f >/dev/null
+12 -7
View File
@@ -13,31 +13,34 @@ map $http_upgrade $connection_upgrade {
server {
listen 5700;
root /ql/static/dist;
IPV6_CONFIG
ssl_session_timeout 5m;
location QL_BASE_URL/api/public/ {
location QL_BASE_URLapi/public/ {
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://publicApi/api/public/;
proxy_pass http://publicApi/api/;
proxy_buffering off;
}
location QL_BASE_URL/api/ {
location QL_BASE_URLapi/ {
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://baseApi/api/;
proxy_buffering off;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
location QL_BASE_URL/open/ {
location QL_BASE_URLopen/ {
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://baseApi/open/;
proxy_buffering off;
}
gzip on;
@@ -48,10 +51,12 @@ server {
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.0;
QL_ROOT_CONFIG
location QL_BASE_URL/ {
location QL_BASE_URL_LOCATION {
QL_ALIAS_CONFIG
index index.html index.htm;
try_files $uri $uri/ QL_BASE_URL/index.html;
try_files $uri QL_BASE_URLindex.html;
}
location ~ .*\.(html)$ {
+1 -1
View File
@@ -14,7 +14,7 @@ http {
server_tokens off;
client_max_body_size 20m;
client_max_body_size 4096m;
client_body_buffer_size 20m;
keepalive_timeout 65;
+34
View File
@@ -0,0 +1,34 @@
module.exports = {
apps: [
{
name: 'schedule',
max_restarts: 10,
kill_timeout: 15000,
wait_ready: true,
listen_timeout: 10000,
source_map_support: true,
time: true,
script: 'static/build/schedule/index.js',
},
{
name: 'public',
max_restarts: 10,
kill_timeout: 15000,
wait_ready: true,
listen_timeout: 10000,
source_map_support: true,
time: true,
script: 'static/build/public.js',
},
{
name: 'panel',
max_restarts: 10,
kill_timeout: 15000,
wait_ready: true,
listen_timeout: 10000,
source_map_support: true,
time: true,
script: 'static/build/app.js',
},
],
};
+64 -25
View File
@@ -2,16 +2,18 @@
"private": true,
"scripts": {
"start": "concurrently -n w: npm:start:*",
"start:front": "umi dev",
"start:front": "max dev",
"start:back": "nodemon",
"start:public": "ts-node back/public.ts",
"build:front": "umi build",
"start:public": "ts-node --transpile-only ./back/public.ts",
"start:rpc": "ts-node --transpile-only ./back/schedule/index.ts",
"build:front": "max build",
"build:back": "tsc -p tsconfig.back.json",
"panel": "npm run build:back && node static/build/app.js",
"schedule": "npm run build:back && node static/build/schedule.js",
"schedule": "npm run build:back && node static/build/schedule/index.js",
"public": "npm run build:back && node static/build/public.js",
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"postinstall": "umi generate tmp 2>/dev/null || true",
"postinstall": "max setup 2>/dev/null || true",
"test": "umi-test",
"test:coverage": "umi-test --coverage"
},
@@ -28,13 +30,32 @@
},
"pnpm": {
"peerDependencyRules": {
"ignoreMissing": [
"react",
"react-dom",
"antd",
"dva",
"postcss",
"webpack",
"eslint",
"stylelint",
"redux",
"@babel/core",
"monaco-editor",
"rc-field-form",
"@types/lodash.merge",
"rollup",
"styled-components"
],
"allowedVersions": {
"react": "17",
"react-dom": "17"
"react": "18",
"react-dom": "18",
"dva-core": "2"
}
}
},
"dependencies": {
"@grpc/grpc-js": "^1.8.13",
"@otplib/preset-default": "^12.0.1",
"@sentry/node": "^7.12.1",
"@sentry/tracing": "^7.12.1",
@@ -43,26 +64,34 @@
"chokidar": "^3.5.3",
"cors": "^2.8.5",
"cron-parser": "^4.2.1",
"cross-spawn": "^7.0.3",
"dayjs": "^1.11.2",
"dotenv": "^16.0.0",
"express": "^4.17.3",
"express-jwt": "^6.1.1",
"express-rate-limit": "^6.7.0",
"express-urlrewrite": "^1.4.0",
"form-data": "^4.0.0",
"got": "^11.8.2",
"hpagent": "^0.1.2",
"http-proxy-middleware": "^2.0.6",
"iconv-lite": "^0.6.3",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21",
"multer": "^1.4.4",
"nedb": "^1.8.0",
"node-schedule": "^2.1.0",
"nodemailer": "^6.7.2",
"p-queue": "7.2.0",
"p-limit": "3.1.0",
"protobufjs": "^7.2.3",
"pstree.remy": "^1.1.8",
"reflect-metadata": "^0.1.13",
"sequelize": "^6.20.1",
"sequelize": "^6.25.5",
"serve-handler": "^6.1.3",
"sockjs": "^0.3.24",
"sqlite3": "npm:@louislam/sqlite3@^15.0.6",
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3",
"tar": "^6.1.15",
"toad-scheduler": "^1.6.0",
"typedi": "^0.10.0",
"uuid": "^8.3.2",
@@ -71,57 +100,67 @@
},
"devDependencies": {
"@ant-design/icons": "^4.7.0",
"@ant-design/pro-layout": "^6.33.1",
"@monaco-editor/react": "^4.3.1",
"@ant-design/pro-layout": "6.38.22",
"@monaco-editor/react": "4.2.1",
"@react-hook/resize-observer": "^1.2.6",
"@sentry/react": "^7.12.1",
"@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12",
"@types/cross-spawn": "^6.0.2",
"@types/express": "^4.17.13",
"@types/express-jwt": "^6.0.4",
"@types/file-saver": "^2.0.5",
"@types/js-yaml": "^4.0.5",
"@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.179",
"@types/lodash": "^4.14.185",
"@types/multer": "^1.4.7",
"@types/nedb": "^1.8.12",
"@types/node": "^17.0.21",
"@types/node-schedule": "^1.3.2",
"@types/nodemailer": "^6.4.4",
"@types/qrcode.react": "^1.0.2",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.13",
"@types/react": "^18.0.20",
"@types/react-copy-to-clipboard": "^5.0.4",
"@types/react-dom": "^18.0.6",
"@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1",
"@types/tar": "^6.1.5",
"@types/uuid": "^8.3.4",
"@umijs/plugin-antd": "^0.15.0",
"@umijs/plugin-esbuild": "^1.4.1",
"@umijs/max": "^4.0.72",
"@umijs/ssr-darkreader": "^4.9.45",
"@umijs/test": "^3.5.21",
"ansi-to-react": "^6.1.6",
"antd": "^4.20.5",
"antd": "^4.24.8",
"antd-img-crop": "^4.2.3",
"axios": "^1.4.0",
"codemirror": "^5.65.2",
"compression-webpack-plugin": "9.2.0",
"concurrently": "^7.0.0",
"lint-staged": "^12.3.4",
"file-saver": "^2.0.5",
"lint-staged": "^13.0.3",
"monaco-editor": "0.33.0",
"nodemon": "^2.0.15",
"prettier": "^2.5.1",
"qiniu": "^7.4.0",
"qrcode.react": "^1.0.1",
"query-string": "^7.1.1",
"rc-tween-one": "^3.0.6",
"react": "17.x",
"react": "18.2.0",
"react-codemirror2": "^7.2.1",
"react-copy-to-clipboard": "^5.1.0",
"react-diff-viewer": "^3.1.1",
"react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0",
"react-dom": "17.x",
"react-dom": "18.2.0",
"react-split-pane": "^0.1.92",
"sockjs-client": "^1.6.0",
"ts-node": "^10.6.0",
"ts-proto": "^1.146.0",
"tslib": "^2.4.0",
"typescript": "^4.6.2",
"umi": "^3.5.21",
"umi-request": "^1.4.0",
"tsx": "^3.12.3",
"typescript": "4.8.4",
"vh-check": "^2.0.5",
"virtualizedtableforantd4": "1.3.0",
"webpack": "^5.70.0",
"yorkie": "^2.0.0"
}
+15472
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.8 KiB

+36 -14
View File
@@ -1,9 +1,3 @@
## Version: v2.8.0
## Date: 2021-06-20
## Update Content: 可持续发展纲要\n1. session管理破坏性修改\n2. 配置管理可编辑config下文件\n3. 自定义脚本改为查看脚本\n4. 移除互助相关
## 上面版本号中,如果第2位数字有变化,那么代表增加了新的参数,如果只有第3位数字有变化,仅代表更新了注释,没有增加新的参数,可更新可不更新
## 在运行 ql repo 命令时,是否自动删除失效的脚本与定时任务
AutoDelCron="true"
@@ -16,14 +10,16 @@ DefaultCronRule=""
## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可
RepoFileExtensions="js py"
## 代理地址,支持http/https/socks,例如 http://127.0.0.1:7890
## 代理地址,支持HTTP/SOCK5,例如 http://127.0.0.1:7890
ProxyUrl=""
## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
CommandTimeoutTime="1h"
## 资源告警阙值,默认CPU 80%、内存80%、磁盘90%
CpuWarn=80
MemoryWarn=80
DiskWarn=90
## 设置批量执行任务时的并发数,默认同时执行5个任务
MaxConcurrentNum="5"
## 设置定时任务执行的超时时间,例如1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
CommandTimeoutTime=""
## 在运行 task 命令时,随机延迟启动任务的最大延迟时间
## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
@@ -46,10 +42,10 @@ AutoStartBot=""
## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
BotRepoUrl=""
## 安装python依赖时指定pip源,默认使用清华源
## 安装python依赖时指定pip源
PipMirror="https://pypi.doubanio.com/simple/"
## 安装node依赖时指定pip源,默认使用清华
## 安装node依赖时指定npm
NpmMirror="https://registry.npmmirror.com"
## 通知环境变量
@@ -62,7 +58,7 @@ export PUSH_KEY=""
## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123
export BARK_PUSH=""
## 下方填写推送图标设置,自定义推送图标(需iOS15或以上)
export BARK_ICON="http://qn.whyour.cn/logo.png"
export BARK_ICON="https://qn.whyour.cn/logo.png"
## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声
export BARK_SOUND=""
## 下方填写推送消息分组,默认为"QingLong"
@@ -95,6 +91,10 @@ export TG_API_HOST=""
export DD_BOT_TOKEN=""
export DD_BOT_SECRET=""
## 企业微信反向代理地址
## (环境变量名 QYWX_ORIGIN)
export QYWX_ORIGIN=""
## 5. 企业微信机器人
## 官方说明文档:https://work.weixin.qq.com/api/doc/90000/90136/91770
## 下方填写密钥,企业微信推送 webhook 后面的 key
@@ -146,4 +146,26 @@ export DEER_KEY=""
export CHAT_URL=""
export CHAT_TOKEN=""
## 13. aibotk
## 官方说明文档:http://wechat.aibotk.com/oapi/oapi?from=ql
## aibotk_key (必填)填写智能微秘书个人中心的apikey
export AIBOTK_KEY=""
## aibotk_type (必填)填写发送的目标 room 或 contact, 填其他的不生效
export AIBOTK_TYPE=""
## aibotk_name (必填)填写群名或用户昵称,和上面的type类型要对应
export AIBOTK_NAME=""
## 14. SMTP
## 暂时只支持在 Python 中调用 notify.py 以发送 SMTP 邮件通知
## smtp_server 填写 SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
export SMTP_SERVER=""
## smtp_ssl 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
export SMTP_SSL="false"
## smtp_email 填写 SMTP 收发件邮箱,通知将会由自己发给自己
export SMTP_EMAIL=""
## smtp_password 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
export SMTP_PASSWORD=""
## smtp_name 填写 SMTP 收发件人姓名,可随意填写
export SMTP_NAME=""
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+338 -160
View File
File diff suppressed because one or more lines are too long
+183 -20
View File
@@ -9,6 +9,10 @@ import re
import threading
import time
import urllib.parse
import smtplib
from email.mime.text import MIMEText
from email.header import Header
from email.utils import formataddr
import requests
@@ -61,6 +65,7 @@ push_config = {
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY
'DEER_URL': '', # PushDeer 的 PUSHDEER_URL
'CHAT_URL': '', # synology chat url
'CHAT_TOKEN': '', # synology chat token
@@ -71,6 +76,8 @@ push_config = {
'QMSG_KEY': '', # qmsg 酱的 QMSG_KEY
'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE
'QYWX_ORIGIN': '', # 企业微信代理地址
'QYWX_AM': '', # 企业微信应用
'QYWX_KEY': '', # 企业微信机器人
@@ -81,6 +88,18 @@ push_config = {
'TG_PROXY_AUTH': '', # tg 代理认证参数
'TG_PROXY_HOST': '', # tg 机器人的 TG_PROXY_HOST
'TG_PROXY_PORT': '', # tg 机器人的 TG_PROXY_PORT
'AIBOTK_KEY': '', # 智能微秘书 个人中心的apikey 文档地址:http://wechat.aibotk.com/docs/about
'AIBOTK_TYPE': '', # 智能微秘书 发送目标 room 或 contact
'AIBOTK_NAME': '', # 智能微秘书 发送群名 或者好友昵称和type要对应好
'SMTP_SERVER': '', # SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
'SMTP_SSL': 'false', # SMTP 发送邮件服务器是否使用 SSL,填写 true 或 false
'SMTP_EMAIL': '', # SMTP 收发件邮箱,通知将会由自己发给自己
'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
'PUSHME_KEY': '', # PushMe 酱的 PUSHME_KEY
}
notify_function = []
# fmt: on
@@ -205,7 +224,7 @@ def go_cqhttp(title: str, content: str) -> None:
print("go-cqhttp 推送失败!")
def gotify(title:str,content:str) -> None:
def gotify(title: str, content: str) -> None:
"""
使用 gotify 推送消息。
"""
@@ -215,8 +234,12 @@ def gotify(title:str,content:str) -> None:
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()
data = {
"title": title,
"message": content,
"priority": push_config.get("GOTIFY_PRIORITY"),
}
response = requests.post(url, data=data).json()
if response.get("id"):
print("gotify 推送成功!")
@@ -254,10 +277,10 @@ def serverJ(title: str, content: str) -> None:
print("serverJ 服务启动")
data = {"text": title, "desp": content.replace("\n", "\n\n")}
if push_config.get("PUSH_KEY").index("SCT") != -1:
if push_config.get("PUSH_KEY").find("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'
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:
@@ -274,8 +297,16 @@ def pushdeer(title: str, content: str) -> None:
print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送")
return
print("PushDeer 服务启动")
data = {"text": title, "desp": content, "type": "markdown", "pushkey": push_config.get("DEER_KEY")}
url = 'https://api2.pushdeer.com/message/push'
data = {
"text": title,
"desp": content,
"type": "markdown",
"pushkey": push_config.get("DEER_KEY"),
}
url = "https://api2.pushdeer.com/message/push"
if push_config.get("DEER_URL"):
url = push_config.get("DEER_URL")
response = requests.post(url, data=data).json()
if len(response.get("content").get("result")) > 0:
@@ -292,7 +323,7 @@ def chat(title: str, content: str) -> None:
print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送")
return
print("chat 服务启动")
data = 'payload=' + json.dumps({'text': title + '\n' + content})
data = "payload=" + json.dumps({"text": title + "\n" + content})
url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN")
response = requests.post(url, data=data)
@@ -302,7 +333,6 @@ def chat(title: str, content: str) -> None:
print("Chat 推送失败!错误信息:", response)
def pushplus_bot(title: str, content: str) -> None:
"""
通过 push+ 推送消息。
@@ -327,7 +357,6 @@ def pushplus_bot(title: str, content: str) -> None:
print("PUSHPLUS 推送成功!")
else:
url_old = "http://pushplus.hxtrip.com/send"
headers["Accept"] = "application/json"
response = requests.post(url=url_old, data=body, headers=headers).json()
@@ -398,9 +427,12 @@ class WeCom:
self.CORPID = corpid
self.CORPSECRET = corpsecret
self.AGENTID = agentid
self.ORIGIN = "https://qyapi.weixin.qq.com"
if push_config.get("QYWX_ORIGIN"):
self.ORIGIN = push_config.get("QYWX_ORIGIN")
def get_access_token(self):
url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
url = f"{self.ORIGIN}/cgi-bin/gettoken"
values = {
"corpid": self.CORPID,
"corpsecret": self.CORPSECRET,
@@ -410,10 +442,7 @@ class WeCom:
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_url = f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
send_values = {
"touser": touser,
"msgtype": "text",
@@ -427,10 +456,7 @@ class WeCom:
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_url = f"https://{self.HOST}/cgi-bin/message/send?access_token={self.get_access_token()}"
send_values = {
"touser": touser,
"msgtype": "mpnews",
@@ -463,7 +489,11 @@ def wecom_bot(title: str, content: str) -> None:
return
print("企业微信机器人服务启动")
url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={push_config.get('QYWX_KEY')}"
origin = "https://qyapi.weixin.qq.com"
if push_config.get("QYWX_ORIGIN"):
origin = push_config.get("QYWX_ORIGIN")
url = f"{origin}/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(
@@ -521,6 +551,116 @@ def telegram_bot(title: str, content: str) -> None:
print("tg 推送失败!")
def aibotk(title: str, content: str) -> None:
"""
使用 智能微秘书 推送消息。
"""
if (
not push_config.get("AIBOTK_KEY")
or not push_config.get("AIBOTK_TYPE")
or not push_config.get("AIBOTK_NAME")
):
print("智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送")
return
print("智能微秘书 服务启动")
if push_config.get("AIBOTK_TYPE") == "room":
url = "https://api-bot.aibotk.com/openapi/v1/chat/room"
data = {
"apiKey": push_config.get("AIBOTK_KEY"),
"roomName": push_config.get("AIBOTK_NAME"),
"message": {"type": 1, "content": f"【青龙快讯】\n\n{title}\n{content}"},
}
else:
url = "https://api-bot.aibotk.com/openapi/v1/chat/contact"
data = {
"apiKey": push_config.get("AIBOTK_KEY"),
"name": push_config.get("AIBOTK_NAME"),
"message": {"type": 1, "content": f"【青龙快讯】\n\n{title}\n{content}"},
}
body = json.dumps(data).encode(encoding="utf-8")
headers = {"Content-Type": "application/json"}
response = requests.post(url=url, data=body, headers=headers).json()
print(response)
if response["code"] == 0:
print("智能微秘书 推送成功!")
else:
print(f'智能微秘书 推送失败!{response["error"]}')
def smtp(title: str, content: str) -> None:
"""
使用 SMTP 邮件 推送消息。
"""
if (
not push_config.get("SMTP_SERVER")
or not push_config.get("SMTP_SSL")
or not push_config.get("SMTP_EMAIL")
or not push_config.get("SMTP_PASSWORD")
or not push_config.get("SMTP_NAME")
):
print(
"SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送"
)
return
print("SMTP 邮件 服务启动")
message = MIMEText(content, "plain", "utf-8")
message["From"] = formataddr(
(
Header(push_config.get("SMTP_NAME"), "utf-8").encode(),
push_config.get("SMTP_EMAIL"),
)
)
message["To"] = formataddr(
(
Header(push_config.get("SMTP_NAME"), "utf-8").encode(),
push_config.get("SMTP_EMAIL"),
)
)
message["Subject"] = Header(title, "utf-8")
try:
smtp_server = (
smtplib.SMTP_SSL(push_config.get("SMTP_SERVER"))
if push_config.get("SMTP_SSL") == "true"
else smtplib.SMTP(push_config.get("SMTP_SERVER"))
)
smtp_server.login(
push_config.get("SMTP_EMAIL"), push_config.get("SMTP_PASSWORD")
)
smtp_server.sendmail(
push_config.get("SMTP_EMAIL"),
push_config.get("SMTP_EMAIL"),
message.as_bytes(),
)
smtp_server.close()
print("SMTP 邮件 推送成功!")
except Exception as e:
print(f"SMTP 邮件 推送失败!{e}")
def pushme(title: str, content: str) -> None:
"""
使用 PushMe 推送消息。
"""
if not push_config.get("PUSHME_KEY"):
print("PushMe 服务的 PUSHME_KEY 未设置!!\n取消推送")
return
print("PushMe 服务启动")
url = f'https://push.i-i.me/?push_key={push_config.get("PUSHME_KEY")}'
data = {
"title": title,
"content": content,
}
response = requests.post(url, data=data)
if response == 'success':
print("PushMe 推送成功!")
else:
print("PushMe 推送失败!{response}")
def one() -> str:
"""
获取一条一言。
@@ -561,6 +701,22 @@ if push_config.get("QYWX_KEY"):
notify_function.append(wecom_bot)
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
notify_function.append(telegram_bot)
if (
push_config.get("AIBOTK_KEY")
and push_config.get("AIBOTK_TYPE")
and push_config.get("AIBOTK_NAME")
):
notify_function.append(aibotk)
if (
push_config.get("SMTP_SERVER")
and push_config.get("SMTP_SSL")
and push_config.get("SMTP_EMAIL")
and push_config.get("SMTP_PASSWORD")
and push_config.get("SMTP_NAME")
):
notify_function.append(smtp)
if push_config.get("PUSHME_KEY"):
notify_function.append(pushme)
def send(title: str, content: str) -> None:
@@ -568,6 +724,13 @@ def send(title: str, content: str) -> None:
print(f"{title} 推送内容为空!")
return
# 根据标题跳过一些消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔
skipTitle = os.getenv("SKIP_PUSH_TITLE")
if skipTitle:
if title in re.split("\n", skipTitle):
print(f"{title} 在SKIP_PUSH_TITLE环境变量内,跳过推送!")
return
hitokoto = push_config.get("HITOKOTO")
text = one() if hitokoto else ""
+1 -5
View File
@@ -3,12 +3,8 @@
"author": "",
"license": "ISC",
"dependencies": {
"crypto-js": "^4.0.0",
"download": "^8.0.0",
"http-server": "^0.12.3",
"got": "^11.5.1",
"qrcode-terminal": "^0.12.0",
"request": "^2.88.2",
"nodemailer": "^6.8.0",
"tough-cookie": "^4.0.0",
"tunnel": "0.0.6",
"ws": "^7.4.3"
+2 -2
View File
@@ -5,14 +5,14 @@ const envFound = dotenv.config();
const accessKey = process.env.QINIU_AK;
const secretKey = process.env.QINIU_SK;
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
const key = 'version.ts';
const key = 'version.yaml';
const options = {
scope: `${process.env.QINIU_SCOPE}:${key}`,
};
const putPolicy = new qiniu.rs.PutPolicy(options);
const uploadToken = putPolicy.uploadToken(mac);
const localFile = 'src/version.ts';
const localFile = 'version.yaml';
const config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z1 });
const formUploader = new qiniu.form_up.FormUploader(config);
const putExtra = new qiniu.form_up.PutExtra(
+206 -159
View File
@@ -1,187 +1,234 @@
#!/usr/bin/env bash
create_token() {
local token_command="tsx ${dir_root}/back/token.ts"
local token_file="${dir_root}/static/build/token.js"
if [[ -f $token_file ]]; then
token_command="node ${token_file}"
fi
token=$(eval "$token_command")
}
get_token() {
if [[ -f $file_auth_token ]]; then
token=$(cat $file_auth_token | jq -r .value)
local expiration=$(cat $file_auth_token | jq -r .expiration)
local currentTimeStamp=$(date +%s)
if [[ $currentTimeStamp -ge $expiration ]]; then
create_token
fi
else
create_token
fi
}
add_cron_api() {
local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}')
else
local schedule=$1
local command=$2
local name=$3
fi
local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}')
local sub_id=$(echo "$1" | awk -F ":" '{print $4}')
else
local schedule="$1"
local command="$2"
local name="$3"
local sub_id="$4"
fi
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$name -> 添加成功"
else
echo -e "$name -> 添加失败(${message})"
fi
if [[ ! $sub_id ]];then
sub_id="null"
fi
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"sub_id\":$sub_id}" \
--compressed
)
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$name -> 添加成功"
else
echo -e "$name -> 添加失败(${message})"
fi
}
update_cron_api() {
local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}')
local id=$(echo "$1" | awk -F ":" '{print $4}')
else
local schedule=$1
local command=$2
local name=$3
local id=$4
fi
local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}')
local id=$(echo "$1" | awk -F ":" '{print $4}')
else
local schedule="$1"
local command="$2"
local name="$3"
local id="$4"
fi
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$name -> 更新成功"
else
echo -e "$name -> 更新失败(${message})"
fi
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
--compressed
)
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$name -> 更新成功"
else
echo -e "$name -> 更新失败(${message})"
fi
}
update_cron_command_api() {
local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then
local command=$(echo "$1" | awk -F ":" '{print $1}')
local id=$(echo "$1" | awk -F ":" '{print $2}')
else
local command=$1
local id=$2
fi
local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then
local command=$(echo "$1" | awk -F ":" '{print $1}')
local id=$(echo "$1" | awk -F ":" '{print $2}')
else
local command="$1"
local id="$2"
fi
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$command -> 更新成功"
else
echo -e "$command -> 更新失败(${message})"
fi
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \
--compressed
)
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$command -> 更新成功"
else
echo -e "$command -> 更新失败(${message})"
fi
}
del_cron_api() {
local ids=$1
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'DELETE' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "[$ids]" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "成功"
else
echo -e "失败(${message})"
fi
local ids="$1"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'DELETE' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "[$ids]" \
--compressed
)
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "成功"
else
echo -e "失败(${message})"
fi
}
update_cron() {
local ids="$1"
local status="$2"
local pid="${3:-''}"
local logPath="$4"
local lastExecutingTime="${5:-0}"
local runningTime="${6:-0}"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/status?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code != 200 ]]; then
echo -e "\n## 更新任务状态失败(${message})\n" >> $dir_log/$log_path
fi
local ids="$1"
local status="$2"
local pid="${3:-''}"
local logPath="$4"
local lastExecutingTime="${5:-0}"
local runningTime="${6:-0}"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/status?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
--compressed
)
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code != 200 ]]; then
echo -e "\n## 更新任务状态失败(${message})\n"
fi
}
notify_api() {
local title=$1
local content=$2
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "通知发送成功"
else
echo -e "通知失败(${message})"
fi
local title="$1"
local content="$2"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
--compressed
)
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "通知发送成功🎉"
else
echo -e "通知失败(${message})"
fi
}
find_cron_api() {
local params="$1"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/detail?$params&t=$currentTimeStamp" \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--compressed
)
data=$(echo "$api" | jq -r .data)
if [[ $data == 'null' ]]; then
echo -e ""
else
name=$(echo "$api" | jq -r .data.name)
echo -e "$name"
fi
}
get_token
Regular → Executable
+5 -10
View File
@@ -1,9 +1,5 @@
#!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
if [[ -z ${BotRepoUrl} ]]; then
url="https://github.com/SuMaiKaDe/bot.git"
repo_path="${dir_repo}/dockerbot"
@@ -35,11 +31,10 @@ fi
cp -f "$repo_path/jbot/requirements.txt" "$dir_data"
cd $dir_data
cat requirements.txt | while read LREAD
do
if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
pip3 --default-timeout=100 install ${LREAD}
fi
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"
@@ -47,6 +42,6 @@ echo -e "\npython3依赖安装成功...\n"
echo -e "4、启动bot程序...\n"
make_dir $dir_log/bot
cd $dir_data
ps -ef | grep "python3 -m jbot" | grep -v grep | awk '{print $1}' | xargs kill -9 2>/dev/null
ps -eo pid,command | grep "python3 -m jbot" | grep -v grep | awk '{print $1}' | xargs kill -9 2>/dev/null
nohup python3 -m jbot >$dir_log/bot/nohup.log 2>&1 &
echo -e "bot启动成功...\n"
Regular → Executable
+20 -20
View File
@@ -1,9 +1,5 @@
#!/usr/bin/env bash
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
. $dir_shell/api.sh
reset_env() {
echo -e "---> 1. 开始检测配置文件\n"
fix_config
@@ -29,9 +25,7 @@ copy_dep() {
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
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
init_nginx
echo -e "---> 配置文件复制完成\n"
}
@@ -39,12 +33,12 @@ pm2_log() {
echo -e "---> pm2日志"
local panelOut="/root/.pm2/logs/panel-out.log"
local panelError="/root/.pm2/logs/panel-error.log"
tail -n 100 "$panelOut"
tail -n 100 "$panelError"
tail -n 300 "$panelOut"
tail -n 300 "$panelError"
}
check_nginx() {
local nginxPid=$(ps -ef | grep nginx | grep -v grep)
local nginxPid=$(ps -eo pid,command | grep nginx | grep -v grep)
echo -e "=====> 检测nginx服务\n$nginxPid"
if [[ $nginxPid ]]; then
echo -e "\n=====> nginx服务正常\n"
@@ -60,9 +54,6 @@ check_ql() {
echo -e "\n=====> 检测面板\n\n$api\n"
if [[ $api =~ "<div id=\"root\"></div>" ]]; then
echo -e "=====> 面板服务启动正常\n"
else
echo -e "=====> 面板服务异常,重置基础环境\n"
reset_env
fi
}
@@ -70,9 +61,8 @@ check_pm2() {
pm2_log
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/api/user?t=$currentTimeStamp" \
curl -s --noproxy "*" "http://0.0.0.0:5600/api/system?t=$currentTimeStamp" \
-H 'Accept: */*' \
-H "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' \
@@ -81,16 +71,26 @@ check_pm2() {
echo -e "\n=====> 检测后台\n\n$api\n"
if [[ $api =~ "{\"code\"" ]]; then
echo -e "=====> 后台服务启动正常\n"
else
echo -e "=====> 后台服务异常,重置基础环境并重启后台\n"
reset_env
fi
}
main() {
echo -e "=====> 开始检测"
npm i -g pnpm
pnpm add -g pm2
npm i -g pnpm@8.3.1
patch_version
apk add procps netcat-openbsd
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
cd && pnpm config set registry $NpmMirror
pnpm install -g
fi
pnpm add -g pm2 tsx
reset_env
copy_dep
check_ql
check_nginx
Executable
+238
View File
@@ -0,0 +1,238 @@
#!/usr/bin/env bash
random_delay() {
local random_delay_max=$RandomDelay
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
local file_param=$1
local file_extensions=${RandomDelayFileExtensions-"js"}
local ignored_minutes=${RandomDelayIgnoredMinutes-"0 30"}
if [[ -n $file_extensions ]]; then
if ! echo "$file_param" | grep -qE "\.${file_extensions// /$|\\.}$"; then
# echo -e "\n当前文件需要准点运行, 放弃随机延迟\n"
return
fi
fi
local current_min
current_min=$(date "+%-M")
for minute in $ignored_minutes; do
if [[ $current_min -eq $minute ]]; then
# echo -e "\n当前时间需要准点运行, 放弃随机延迟\n"
return
fi
done
local delay_second=$(($(gen_random_num "$random_delay_max") + 1))
echo -e "任务随机延迟 $delay_second 秒,配置文件参数 RandomDelay 置空可取消延迟 \n"
sleep $delay_second
fi
}
## scripts目录下所有可运行脚本数组
gen_array_scripts() {
local dir_current=$(pwd)
local i="-1"
cd $dir_scripts
for file in $(ls); do
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
let i++
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
array_scripts_name[i]=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
[[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
fi
done
cd $dir_current
}
## 使用说明
usage() {
gen_array_scripts
echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:"
echo -e "1.$cmd_task <file_name> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数"
echo -e "2.$cmd_task <file_name> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中"
echo -e "3.$cmd_task <file_name> conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行"
echo -e "4.$cmd_task <file_name> desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行"
if [[ ${#array_scripts[*]} -gt 0 ]]; then
echo -e "\n当前有以下脚本可以运行:"
for ((i = 0; i < ${#array_scripts[*]}; i++)); do
echo -e "$(($i + 1)). ${array_scripts_name[i]}${array_scripts[i]}"
done
else
echo -e "\n暂无脚本可以执行"
fi
}
## run nohup,$1:文件名,不含路径,带后缀
run_nohup() {
local file_name=$1
nohup node $file_name &>$log_path &
}
check_server() {
if [[ $cpu_warn ]] && [[ $mem_warn ]] && [[ $disk_warn ]]; then
local top_result=$(top -b -n 1)
cpu_use=$(echo "$top_result" | grep CPU | grep -v -E 'grep|PID' | awk '{print $2}' | cut -f 1 -d "%" | head -n 1)
mem_free=$(free -m | grep "Mem" | awk '{print $3}' | head -n 1)
mem_total=$(free -m | grep "Mem" | awk '{print $2}' | head -n 1)
mem_use=$(printf "%d%%" $((mem_free * 100 / mem_total)) | cut -f 1 -d "%" | head -n 1)
disk_use=$(df -P | grep /dev | grep -v -E '(tmp|boot|shm)' | awk '{print $5}' | cut -f 1 -d "%" | head -n 1)
if [[ $cpu_use -gt $cpu_warn ]] && [[ $cpu_warn ]] || [[ $mem_free -lt $mem_warn ]] || [[ $disk_use -gt $disk_warn ]]; then
local resource=$(echo "$top_result" | grep -v -E 'grep|Mem|idle|Load|tr' | awk '{$2="";$3="";$4="";$5="";$7="";print $0}' | head -n 10 | tr '\n' '|' | sed s/\|/\\\\n/g)
notify_api "服务器资源异常警告" "当前CPU占用 $cpu_use% 内存占用 $mem_use% 磁盘占用 $disk_use% \n资源占用详情 \n\n $resource"
fi
fi
}
## 正常运行单个脚本,$1:传入参数
run_normal() {
local file_param=$1
if [[ $# -eq 1 ]] && [[ "$real_time" != "true" ]]; then
random_delay "$file_param"
fi
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ${file_param} != /* ]] && [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
$timeoutCmd $which_program $file_param
}
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
run_concurrent() {
local file_param="$1"
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]]; then
echo -e "\n 缺少并发运行的环境变量参数"
exit 1
fi
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | 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')
[[ ! -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.%3N")
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
for i in "${!array[@]}"; do
export ${env_param}=${array[i]}
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
eval $timeoutCmd $which_program $file_param &>$single_log_path &
done
wait
for i in "${!array[@]}"; do
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
cat $single_log_path
[[ -f $single_log_path ]] && rm -f $single_log_path
done
}
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
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local 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')
[[ ! -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
$timeoutCmd $which_program $file_param
}
## 运行其他命令
run_else() {
local file_param="$1"
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
shift
$timeoutCmd $which_program $file_param "$@"
}
## 命令检测
main() {
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
case $# in
1)
run_normal "$1"
;;
*)
case $2 in
now)
run_normal "$1" "$2"
;;
conc)
run_concurrent "$1" "$3" "$*"
;;
desi)
run_designated "$1" "$3" "$*"
;;
*)
run_else "$@"
;;
esac
;;
esac
elif [[ $# -eq 0 ]]; then
echo
usage
else
run_else "$@"
fi
}
handle_task_before "$@"
main "$@"
handle_task_after "$@"
Regular → Executable
+2 -2
View File
@@ -11,9 +11,9 @@ echo -e "提交master代码"
git push
echo -e "更新cdn文件"
ts-node sample/tool.ts
tsx sample/tool.ts
string=$(cat src/version.ts | grep "version" | egrep "[^\']*" -o | egrep "\d\.*")
string=$(cat version.yaml | grep "version" | egrep "[^ ]*" -o | egrep "\d\.*")
version="v$string"
echo -e "当前版本$version"
+33 -27
View File
@@ -1,42 +1,48 @@
#!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
days=$1
## 删除运行js脚本的旧日志
## 删除运行脚本的旧日志
remove_js_log() {
local log_full_path_list=$(find $dir_log/ -name "*.log")
local diff_time
for log in $log_full_path_list; do
local log_date=$(echo $log | awk -F "/" '{print $NF}' | cut -c1-10) #文件名比文件属性获得的日期要可靠
if [[ $(date +%s -d $log_date 2>/dev/null) ]]; then
if [[ $is_macos -eq 1 ]]; then
diff_time=$(($(date +%s) - $(date -j -f "%Y-%m-%d" "$log_date" +%s)))
else
diff_time=$(($(date +%s) - $(date +%s -d "$log_date")))
fi
[[ $diff_time -gt $((${days} * 86400)) ]] && rm -vf $log
local log_full_path_list=$(find $dir_log/ -name "*.log")
local diff_time
for log in $log_full_path_list; do
local log_date=$(echo $log | awk -F "/" '{print $NF}' | cut -c1-10) #文件名比文件属性获得的日期要可靠
if [[ $(date +%s -d $log_date 2>/dev/null) ]]; then
if [[ $is_macos -eq 1 ]]; then
diff_time=$(($(date +%s) - $(date -j -f "%Y-%m-%d" "$log_date" +%s)))
else
diff_time=$(($(date +%s) - $(date +%s -d "$log_date")))
fi
if [[ $diff_time -gt $((${days} * 86400)) ]]; then
local log_path=$(echo "$log" | sed "s,${dir_log}/,,g")
local result=$(find_cron_api "log_path=$log_path")
echo -e "查询文件 $log_path"
if [[ -z $result ]]; then
echo -e "删除中~"
rm -vf $log
else
echo -e "正在被 $result 使用,跳过~"
fi
done
fi
fi
done
}
## 删除空文件夹
remove_empty_dir() {
cd $dir_log
for dir in $(ls); do
if [[ -d $dir ]] && [[ -z $(ls $dir) ]]; then
rm -rf $dir
fi
done
cd $dir_log
for dir in $(ls); do
if [[ -d $dir ]] && [[ -z $(ls $dir) ]]; then
rm -rf $dir
fi
done
}
## 运行
if [[ ${days} ]]; then
echo -e "查找旧日志文件中...\n"
remove_js_log
remove_empty_dir
echo -e "删除旧日志执行完毕\n"
echo -e "查找旧日志文件中...\n"
remove_js_log
remove_empty_dir
echo -e "删除旧日志执行完毕\n"
fi
+308 -282
View File
@@ -2,6 +2,7 @@
## 目录
dir_root=$QL_DIR
dir_tmp=$dir_root/.tmp
dir_data=$dir_root/data
dir_shell=$dir_root/shell
dir_sample=$dir_root/sample
@@ -18,6 +19,7 @@ dir_update_log=$dir_log/update
ql_static_repo=$dir_repo/static
## 文件
file_ecosystem_js=$dir_root/ecosystem.config.js
file_config_sample=$dir_sample/config.sample.sh
file_env=$dir_config/env.sh
file_sharecode=$dir_config/sharecode.sh
@@ -34,7 +36,6 @@ file_notify_js_sample=$dir_sample/notify.js
file_notify_py_sample=$dir_sample/notify.py
file_notify_py=$dir_scripts/notify.py
file_notify_js=$dir_scripts/sendNotify.js
task_error_log_path=$dir_log/task_error.log
nginx_app_conf=$dir_root/docker/front.conf
nginx_conf=$dir_root/docker/nginx.conf
dep_notify_py=$dir_dep/notify.py
@@ -50,383 +51,408 @@ list_own_drop=$dir_list_tmp/own_drop.list
## 软连接及其原始文件对应关系
link_name=(
task
ql
task
ql
)
original_name=(
task.sh
update.sh
task.sh
update.sh
)
init_env() {
export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules
export PYTHONUNBUFFERED=1
export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules
export PYTHONUNBUFFERED=1
}
import_config() {
[[ -f $file_config_user ]] && . $file_config_user
[[ -f $file_env ]] && . $file_env
[[ -f $file_config_user ]] && . $file_config_user
[[ -f $file_env ]] && . $file_env
ql_base_url=${QlBaseUrl:-""}
command_timeout_time=${CommandTimeoutTime:-"1h"}
proxy_url=${ProxyUrl:-""}
file_extensions=${RepoFileExtensions:-"js py"}
current_branch=${QL_BRANCH}
ql_base_url=${QlBaseUrl:-"/"}
command_timeout_time=${CommandTimeoutTime:-""}
proxy_url=${ProxyUrl:-""}
file_extensions=${RepoFileExtensions:-"js py"}
current_branch=${QL_BRANCH}
if [[ -n "${DefaultCronRule}" ]]; then
default_cron="${DefaultCronRule}"
else
default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
fi
if [[ -n "${DefaultCronRule}" ]]; then
default_cron="${DefaultCronRule}"
else
default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
fi
cpu_warn=${CpuWarn}
mem_warn=${MemoryWarn}
disk_warn=${DiskWarn}
}
set_proxy() {
if [[ $proxy_url ]]; then
export http_proxy="${proxy_url}"
export https_proxy="${proxy_url}"
fi
local proxy="$1"
if [[ $proxy ]]; then
proxy_url="$proxy"
fi
if [[ $proxy_url ]]; then
export http_proxy="${proxy_url}"
export https_proxy="${proxy_url}"
fi
}
unset_proxy() {
unset http_proxy
unset https_proxy
unset http_proxy
unset https_proxy
unset ftp_proxy
unset all_proxy
unset no_proxy
}
make_dir() {
local dir=$1
if [[ ! -d $dir ]]; then
mkdir -p $dir
fi
local dir=$1
if [[ ! -d $dir ]]; then
mkdir -p $dir
fi
}
detect_termux() {
if [[ $PATH == *com.termux* ]]; then
is_termux=1
else
is_termux=0
fi
if [[ $PATH == *com.termux* ]]; then
is_termux=1
else
is_termux=0
fi
}
detect_macos() {
[[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0
[[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0
}
gen_random_num() {
local divi=$1
echo $((${RANDOM} % $divi))
local divi=$1
echo $((${RANDOM} % $divi))
}
link_shell_sub() {
local link_path="$1"
local original_path="$2"
if [[ ! -L $link_path ]] || [[ $(readlink -f $link_path) != $original_path ]]; then
rm -f $link_path 2>/dev/null
ln -sf $original_path $link_path
fi
local link_path="$1"
local original_path="$2"
if [[ ! -L $link_path ]] || [[ $(readlink -f $link_path) != $original_path ]]; then
rm -f $link_path 2>/dev/null
ln -sf $original_path $link_path
fi
}
link_shell() {
if [[ $is_termux -eq 1 ]]; then
local path="/data/data/com.termux/files/usr/bin/"
elif [[ $PATH == */usr/local/bin* ]] && [[ -d /usr/local/bin ]]; then
local path="/usr/local/bin/"
else
local path=""
echo -e "脚本功能受限,请自行添加命令的软连接...\n"
fi
if [[ $path ]]; then
for ((i = 0; i < ${#link_name[*]}; i++)); do
link_shell_sub "$path${link_name[i]}" "$dir_shell/${original_name[i]}"
done
fi
if [[ $is_termux -eq 1 ]]; then
local path="/data/data/com.termux/files/usr/bin/"
elif [[ $PATH == */usr/local/bin* ]] && [[ -d /usr/local/bin ]]; then
local path="/usr/local/bin/"
else
local path=""
echo -e "脚本功能受限,请自行添加命令的软连接...\n"
fi
if [[ $path ]]; then
for ((i = 0; i < ${#link_name[*]}; i++)); do
link_shell_sub "$path${link_name[i]}" "$dir_shell/${original_name[i]}"
done
fi
}
define_cmd() {
local cmd_prefix cmd_suffix
if type task &>/dev/null; then
cmd_suffix=""
if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix=""
else
cmd_prefix="bash "
fi
local cmd_prefix cmd_suffix
if type task &>/dev/null; then
cmd_suffix=""
if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix=""
else
cmd_suffix=".sh"
if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix="$dir_shell/"
else
cmd_prefix="bash $dir_shell/"
fi
cmd_prefix="bash "
fi
for ((i = 0; i < ${#link_name[*]}; i++)); do
export cmd_${link_name[i]}="${cmd_prefix}${link_name[i]}${cmd_suffix}"
done
else
cmd_suffix=".sh"
if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix="$dir_shell/"
else
cmd_prefix="bash $dir_shell/"
fi
fi
for ((i = 0; i < ${#link_name[*]}; i++)); do
export cmd_${link_name[i]}="${cmd_prefix}${link_name[i]}${cmd_suffix}"
done
}
fix_config() {
make_dir $dir_static
make_dir $dir_data
make_dir $dir_config
make_dir $dir_log
make_dir $dir_db
make_dir $dir_scripts
make_dir $dir_list_tmp
make_dir $dir_repo
make_dir $dir_raw
make_dir $dir_update_log
make_dir $dir_dep
make_dir $dir_tmp
make_dir $dir_static
make_dir $dir_data
make_dir $dir_config
make_dir $dir_log
make_dir $dir_db
make_dir $dir_scripts
make_dir $dir_list_tmp
make_dir $dir_repo
make_dir $dir_raw
make_dir $dir_update_log
make_dir $dir_dep
if [[ ! -s $file_config_user ]]; then
echo -e "复制一份 $file_config_sample$file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
cp -fv $file_config_sample $file_config_user
echo
fi
if [[ ! -s $file_config_user ]]; then
echo -e "复制一份 $file_config_sample$file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
cp -fv $file_config_sample $file_config_user
echo
fi
if [[ ! -f $file_env ]]; then
echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n"
touch $file_env
echo
fi
if [[ ! -f $file_env ]]; then
echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n"
touch $file_env
echo
fi
if [[ ! -f $file_task_before ]]; then
echo -e "复制一份 $file_task_sample$file_task_before\n"
cp -fv $file_task_sample $file_task_before
echo
fi
if [[ ! -f $file_task_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_task_after ]]; then
echo -e "复制一份 $file_task_sample$file_task_after\n"
cp -fv $file_task_sample $file_task_after
echo
fi
if [[ ! -f $file_extra_shell ]]; then
echo -e "复制一份 $file_extra_sample$file_extra_shell\n"
cp -fv $file_extra_sample $file_extra_shell
echo
fi
if [[ ! -f $file_extra_shell ]]; then
echo -e "复制一份 $file_extra_sample$file_extra_shell\n"
cp -fv $file_extra_sample $file_extra_shell
echo
fi
if [[ ! -s $file_auth_user ]]; then
echo -e "复制一份 $file_auth_sample$file_auth_user\n"
cp -fv $file_auth_sample $file_auth_user
echo
fi
if [[ ! -s $file_auth_user ]]; then
echo -e "复制一份 $file_auth_sample$file_auth_user\n"
cp -fv $file_auth_sample $file_auth_user
echo
fi
if [[ ! -s $file_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$file_notify_py\n"
cp -fv $file_notify_py_sample $file_notify_py
echo
fi
if [[ ! -s $file_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$file_notify_py\n"
cp -fv $file_notify_py_sample $file_notify_py
echo
fi
if [[ ! -s $file_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$file_notify_js\n"
cp -fv $file_notify_js_sample $file_notify_js
echo
fi
if [[ ! -s $file_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$file_notify_js\n"
cp -fv $file_notify_js_sample $file_notify_js
echo
fi
if [[ -s /etc/nginx/conf.d/default.conf ]]; then
echo -e "检测到默认nginx配置文件,清空...\n"
cat /dev/null >/etc/nginx/conf.d/default.conf
echo
fi
if [[ -s /etc/nginx/conf.d/default.conf ]]; then
echo -e "检测到默认nginx配置文件,清空...\n"
cat /dev/null >/etc/nginx/conf.d/default.conf
echo
fi
if [[ ! -s $dep_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$dep_notify_js\n"
cp -fv $file_notify_js_sample $dep_notify_js
echo
fi
if [[ ! -s $dep_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$dep_notify_js\n"
cp -fv $file_notify_js_sample $dep_notify_js
echo
fi
if [[ ! -s $dep_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$dep_notify_py\n"
cp -fv $file_notify_py_sample $dep_notify_py
echo
fi
if [[ ! -s $dep_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$dep_notify_py\n"
cp -fv $file_notify_py_sample $dep_notify_py
echo
fi
}
npm_install_sub() {
if [ $is_termux -eq 1 ]; then
npm install --production --no-bin-links
elif ! type pnpm &>/dev/null; then
npm install --production
else
pnpm install --loglevel error --production
fi
}
npm_install_1() {
local dir_current=$(pwd)
local dir_work=$1
cd $dir_work
echo -e "运行 npm install...\n"
npm_install_sub
[[ $? -ne 0 ]] && echo -e "\nnpm install 运行不成功,请进入 $dir_work 目录后手动运行 npm install...\n"
cd $dir_current
if [ $is_termux -eq 1 ]; then
npm install --production --no-bin-links
elif ! type pnpm &>/dev/null; then
npm install --production
else
pnpm install --loglevel error --production
fi
exit_status=$?
}
npm_install_2() {
local dir_current=$(pwd)
local dir_work=$1
local dir_current=$(pwd)
local dir_work=$1
cd $dir_work
echo -e "检测到 $dir_work 依赖包有变化,运行 npm install...\n"
npm_install_sub
if [[ $? -ne 0 ]]; then
echo -e "\n安装 $dir_work 的依赖包运行不成功,再次尝试一遍...\n"
npm_install_1 $dir_work
fi
cd $dir_current
cd $dir_work
echo -e "安装 $dir_work 依赖包...\n"
npm_install_sub
cd $dir_current
}
diff_and_copy() {
local copy_source=$1
local copy_to=$2
if [[ ! -s $copy_to ]] || [[ $(diff $copy_source $copy_to) ]]; then
cp -f $copy_source $copy_to
fi
}
update_depend() {
local dir_current=$(pwd)
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts
fi
cd $dir_current
local copy_source=$1
local copy_to=$2
if [[ ! -s $copy_to ]] || [[ $(diff $copy_source $copy_to) ]]; then
cp -f $copy_source $copy_to
fi
}
git_clone_scripts() {
local url=$1
local dir=$2
local branch=$3
[[ $branch ]] && local part_cmd="-b $branch "
echo -e "开始克隆仓库 $url$dir\n"
local url="$1"
local dir="$2"
local branch="$3"
local proxy="$4"
[[ $branch ]] && local part_cmd="-b $branch "
echo -e "开始拉取仓库 ${uniq_path}$dir\n"
set_proxy
git clone $part_cmd $url $dir
exit_status=$?
unset_proxy
}
set_proxy "$proxy"
git_pull_scripts() {
local dir_current=$(pwd)
local dir_work="$1"
local branch="$2"
cd $dir_work
echo -e "开始更新仓库:$dir_work\n"
git clone --depth=1 $part_cmd $url $dir
exit_status=$?
set_proxy
git fetch --all
exit_status=$?
git pull &>/dev/null
unset_proxy
reset_branch "$branch"
cd $dir_current
}
reset_romote_url() {
local dir_current=$(pwd)
local dir_work=$1
local url=$2
local branch="$3"
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
local part_cmd=""
reset_branch "$branch"
cd $dir_current
fi
}
reset_branch() {
local branch="$1"
if [[ $branch ]]; then
part_cmd="origin/${branch}"
git checkout -B "$branch"
git branch --set-upstream-to=$part_cmd $branch
fi
git reset --hard $part_cmd &>/dev/null
unset_proxy
}
random_range() {
local beg=$1
local end=$2
echo $((RANDOM % ($end - $beg) + $beg))
local beg=$1
local end=$2
echo $((RANDOM % ($end - $beg) + $beg))
}
reload_pm2() {
pm2 l &>/dev/null
echo -e "启动面板服务\n"
pm2 delete panel --source-map-support --time &>/dev/null
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
echo -e "启动定时任务服务\n"
pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
echo -e "启动公开服务\n"
pm2 delete public --source-map-support --time &>/dev/null
pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null
cd $dir_root
# 代理会影响 grpc 服务
unset_proxy
pm2 flush &>/dev/null
pm2 startOrGracefulReload $file_ecosystem_js --update-env
pm2 sendSignal SIGKILL panel &>/dev/null
}
diff_time() {
local format="$1"
local begin_time="$2"
local end_time="$3"
local format="$1"
local begin_time="$2"
local end_time="$3"
if [[ $is_macos -eq 1 ]]; then
diff_time=$(($(date -j -f "$format" "$end_time" +%s) - $(date -j -f "$format" "$begin_time" +%s)))
else
diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
fi
echo "$diff_time"
if [[ $is_macos -eq 1 ]]; then
diff_time=$(($(date -j -f "$format" "$end_time" +%s) - $(date -j -f "$format" "$begin_time" +%s)))
else
diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
fi
echo "$diff_time"
}
format_time() {
local format="$1"
local time="$2"
local format="$1"
local time="$2"
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d %H:%M:%S")
else
echo $(date -d "$time" "+%Y-%m-%d %H:%M:%S")
fi
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d %H:%M:%S")
else
echo $(date -d "$time" "+%Y-%m-%d %H:%M:%S")
fi
}
format_log_time() {
local format="$1"
local time="$2"
local format="$1"
local time="$2"
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S")
else
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S")
fi
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S-%3N")
else
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S-%3N")
fi
}
format_timestamp() {
local format="$1"
local time="$2"
local format="$1"
local time="$2"
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%s")
else
echo $(date -d "$time" "+%s")
fi
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%s")
else
echo $(date -d "$time" "+%s")
fi
}
patch_version() {
git config --global pull.rebase false
cp -f $dir_root/.env.example $dir_root/.env
if [[ -f "$dir_root/db/cookie.db" ]]; then
echo -e "检测到旧的db文件,拷贝为新db...\n"
mv $dir_root/db/cookie.db $dir_root/db/env.db
rm -rf $dir_root/db/cookie.db
echo
fi
if [[ -d "$dir_root/db" ]]; then
echo -e "检测到旧的db目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data
echo
fi
if [[ -d "$dir_root/scripts" ]]; then
echo -e "检测到旧的scripts目录,拷贝到data目录...\n"
cp -rf $dir_root/scripts $dir_root/data
echo
fi
if [[ -d "$dir_root/log" ]]; then
echo -e "检测到旧的log目录,拷贝到data目录...\n"
cp -rf $dir_root/log $dir_root/data
echo
fi
if [[ -d "$dir_root/config" ]]; then
echo -e "检测到旧的config目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data
echo
fi
}
init_nginx() {
cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
local location_url="/"
local aliasStr=""
local rootStr=""
if [[ $ql_base_url != "/" ]]; then
location_url="^~${ql_base_url%*/}"
aliasStr="alias ${dir_static}/dist;"
else
rootStr="root ${dir_static}/dist;"
fi
sed -i "s,QL_ALIAS_CONFIG,${aliasStr},g" /etc/nginx/conf.d/front.conf
sed -i "s,QL_ROOT_CONFIG,${rootStr},g" /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL_LOCATION,${location_url},g" /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${ql_base_url},g" /etc/nginx/conf.d/front.conf
ipv6=$(ip a | grep inet6)
ipv6Str=""
if [[ $ipv6 ]]; then
ipv6Str="listen [::]:5700 ipv6only=on;"
fi
sed -i "s,IPV6_CONFIG,${ipv6Str},g" /etc/nginx/conf.d/front.conf
}
handle_task_before() {
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
echo -e "## 开始执行... $begin_time\n"
[[ $is_macos -eq 0 ]] && check_server
. $file_task_before "$@"
}
handle_task_after() {
. $file_task_after "$@"
local etime=$(date "+$time_format")
local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp))
[[ "$diff_time" == 0 ]] && diff_time=1
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time 秒     "
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
}
init_env
detect_termux
detect_macos
define_cmd
fix_config
import_config $1 >$task_error_log_path 2>&1
import_config $1
+104 -382
View File
@@ -5,403 +5,125 @@ dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
. $dir_shell/api.sh
trap "single_hanle" 2 3 20 15 14
single_hanle() {
eval handle_task_after "$@" "$cmd"
exit 1
}
## 选择python3还是node
define_program() {
local file_param=$1
if [[ $file_param == *.js ]]; then
which_program="node"
elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then
which_program="python3"
elif [[ $file_param == *.sh ]]; then
which_program="bash"
elif [[ $file_param == *.ts ]]; then
which_program="ts-node-transpile-only"
local file_param=$1
if [[ $file_param == *.js ]]; then
which_program="node"
elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then
which_program="python3"
elif [[ $file_param == *.sh ]]; then
which_program="bash"
elif [[ $file_param == *.ts ]]; then
if ! type tsx &>/dev/null; then
which_program="ts-node-transpile-only"
else
which_program=""
which_program="tsx"
fi
else
which_program=""
fi
}
random_delay() {
local random_delay_max=$RandomDelay
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
local file_param=$1
local file_extensions=${RandomDelayFileExtensions-"js"}
local ignored_minutes=${RandomDelayIgnoredMinutes-"0 30"}
handle_log_path() {
local file_param=$1
if [[ -n $file_extensions ]]; then
if ! echo "$file_param" | grep -qE "\.${file_extensions// /$|\\.}$"; then
# echo -e "\n当前文件需要准点运行, 放弃随机延迟\n"
return
fi
fi
if [[ -z $file_param ]]; then
file_param="task"
fi
local current_min
current_min=$(date "+%-M")
for minute in $ignored_minutes; do
if [[ $current_min -eq $minute ]]; then
# echo -e "\n当前时间需要准点运行, 放弃随机延迟\n"
return
fi
done
local delay_second=$(($(gen_random_num "$random_delay_max") + 1))
echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后再执行任务,如需立即终止,请按 CTRL+C...\n"
sleep $delay_second
fi
}
## scripts目录下所有可运行脚本数组
gen_array_scripts() {
local dir_current=$(pwd)
local i="-1"
cd $dir_scripts
for file in $(ls); do
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
let i++
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1)
[[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
fi
done
cd $dir_current
}
## 使用说明
usage() {
define_cmd
gen_array_scripts
echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:"
echo -e "1.$cmd_task <file_name> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数"
echo -e "2.$cmd_task <file_name> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中"
echo -e "3.$cmd_task <file_name> conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行"
echo -e "4.$cmd_task <file_name> desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行"
if [[ ${#array_scripts[*]} -gt 0 ]]; then
echo -e "\n当前有以下脚本可以运行:"
for ((i = 0; i < ${#array_scripts[*]}; i++)); do
echo -e "$(($i + 1)). ${array_scripts_name[i]}${array_scripts[i]}"
done
if [[ -z $ID ]]; then
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}')
fi
local suffix=""
if [[ ! -z $ID ]]; then
if [[ "$ID" -gt 0 ]] 2>/dev/null; then
suffix="_${ID}"
else
echo -e "\n暂无脚本可以执行"
ID=""
fi
fi
time=$(date "+$mtime_format")
log_time=$(format_log_time "$mtime_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}${suffix}"
log_path="$log_dir/$log_time.log"
if [[ $real_log_path ]]; then
log_path="$real_log_path"
fi
cmd=">> $dir_log/$log_path 2>&1"
make_dir "$dir_log/$log_dir"
if [[ "$show_log" == "true" ]]; then
cmd="2>&1 | tee -a $dir_log/$log_path"
fi
if [[ "$real_time" == "true" ]]; then
cmd=""
fi
}
## run nohup,$1:文件名,不含路径,带后缀
run_nohup() {
local file_name=$1
nohup node $file_name &>$log_path &
}
## 正常运行单个脚本,$1:传入参数
run_normal() {
local file_param=$1
define_program "$file_param"
if [[ $# -eq 1 ]]; then
random_delay "$file_param"
fi
local time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$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 $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
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 $timeoutCmd $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")
local diff_time=$(expr $end_timestamp - $begin_timestamp)
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
}
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
run_concurrent() {
local file_param="$1"
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]]; then
echo -e "\n 缺少并发运行的环境变量参数"
exit 1
fi
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<< ${runArr[@]}))
local n=0
for i in ${runArr[@]}; do
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
define_program "$file_param"
local time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$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 $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
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="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
for i in "${!array[@]}"; do
export ${env_param}=${array[i]}
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
eval $timeoutCmd $which_program $file_param &>$single_log_path &
done
wait
for i in "${!array[@]}"; do
single_log_path="$dir_log/$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")
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"
local time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$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')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
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
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 $timeoutCmd $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")
local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
}
## 运行其他命令
run_else() {
local file_param="$1"
define_program "$file_param"
local time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$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 $@" | perl -pe "s|.*ID=(.*) $cmd_task $@\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
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
shift
eval $timeoutCmd $which_program "$file_param" "$@" $cmd
eval . $file_task_after "$file_param" "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
}
## 命令检测
main() {
show_log="false"
while getopts ":l" opt
do
case $opt in
l)
show_log="true"
;;
esac
done
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
timeoutCmd=""
format_params() {
time_format="%Y-%m-%d %H:%M:%S"
if [[ $is_macos -eq 1 ]]; then
mtime_format=$time_format
else
mtime_format="%Y-%m-%d %H:%M:%S.%3N"
fi
timeoutCmd=""
if [[ $command_timeout_time ]]; then
if type timeout &>/dev/null; then
timeoutCmd="timeout -k 10s $command_timeout_time "
fi
time_format="%Y-%m-%d %H:%M:%S"
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
case $# in
1)
run_normal "$1"
;;
*)
case $2 in
now)
run_normal "$1" "$2"
;;
conc)
run_concurrent "$1" "$3" "$*"
;;
desi)
run_designated "$1" "$3" "$*"
;;
*)
run_else "$@"
;;
esac
;;
esac
[[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path"
elif [[ $# -eq 0 ]]; then
echo
usage
else
run_else "$@"
timeoutCmd="timeout --foreground -s 2 -k 10s $command_timeout_time "
fi
fi
# params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g')
}
main "$@"
init_begin_time() {
begin_time=$(format_time "$time_format" "$time")
begin_timestamp=$(format_timestamp "$time_format" "$time")
}
while getopts ":lm:" opt; do
case $opt in
l)
show_log="true"
;;
m)
max_time="$OPTARG"
;;
esac
done
[[ $show_log ]] && shift $(($OPTIND - 1))
if [[ $max_time ]]; then
shift $(($OPTIND - 1))
command_timeout_time="$max_time"
fi
format_params "$@"
define_program "$@"
handle_log_path "$@"
init_begin_time
eval . $dir_shell/otask.sh "$cmd"
[[ -f "$dir_log/$log_path" ]] && [[ ! $show_log ]] && [[ "$real_time" != "true" ]] && cat "$dir_log/$log_path"
exit 0
+466 -458
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
const baseUrl = window.__ENV__QlBaseUrl || '/';
export function modifyClientRenderOpts(memo: any) {
return {
...memo,
publicPath: baseUrl,
basename: baseUrl,
};
}
export function modifyContextOpts(memo: any) {
return {
...memo,
basename: baseUrl,
};
}
+39
View File
@@ -0,0 +1,39 @@
import React, { useRef, useState, useEffect } from 'react';
import { Tooltip, Typography } from 'antd';
import { CopyOutlined, CheckOutlined } from '@ant-design/icons';
import { CopyToClipboard } from 'react-copy-to-clipboard';
const { Link } = Typography;
const Copy = ({ text }: { text: string }) => {
const [copied, setCopied] = useState(false);
const copyIdRef = useRef<number>();
const copyText = (e?: React.MouseEvent) => {
e?.preventDefault();
e?.stopPropagation();
setCopied(true);
cleanCopyId();
copyIdRef.current = window.setTimeout(() => {
setCopied(false);
}, 3000);
};
const cleanCopyId = () => {
window.clearTimeout(copyIdRef.current!);
};
return (
<Link onClick={copyText} style={{ marginLeft: 1 }}>
<CopyToClipboard text={text}>
<Tooltip key="copy" title={copied ? '复制成功' : '复制'}>
{copied ? <CheckOutlined /> : <CopyOutlined />}
</Tooltip>
</CopyToClipboard>
</Link>
);
};
export default Copy;
+1 -1
View File
@@ -1,7 +1,7 @@
import { createFromIconfontCN } from '@ant-design/icons';
const IconFont = createFromIconfontCN({
scriptUrl: ['//at.alicdn.com/t/font_3354854_ds8pa06q1qa.js'],
scriptUrl: ['//at.alicdn.com/t/c/font_3354854_ob5y15ewlyq.js'],
});
export default IconFont;
+1
View File
@@ -50,6 +50,7 @@ const EditableTagGroup = ({
{tag}
</Tag>
);
return (
<span key={tag} style={{ display: 'inline-block', marginBottom: 8 }}>
{tagElem}
+404
View File
@@ -0,0 +1,404 @@
import React, {
useRef,
useEffect,
useContext,
createContext,
useReducer,
useState,
useMemo,
} from 'react';
import { throttle, isNumber, debounce } from 'lodash';
const initialState = {
// 行高度
rowHeight: 0,
// 当前的scrollTop
curScrollTop: 0,
// 总行数
totalLen: 0,
};
function reducer(state, action) {
const { curScrollTop, totalLen, ifScrollTopClear, scrollTop } = action;
let stateScrollTop = state.curScrollTop;
switch (action.type) {
// 改变trs 即 改变渲染的列表trs
case 'changeTrs':
return {
...state,
curScrollTop,
};
// 更改totalLen
case 'changeTotalLen':
if (totalLen === 0) {
stateScrollTop = 0;
}
return {
...state,
totalLen,
curScrollTop: stateScrollTop,
};
case 'reset':
return {
...state,
curScrollTop: ifScrollTopClear ? 0 : scrollTop ?? state.curScrollTop,
};
default:
throw new Error();
}
}
// ==============全局常量 ================== //
const DEFAULT_VID = 'vtable';
const vidMap = new Map();
let preData = 0;
// ===============context ============== //
const ScrollContext = createContext({
dispatch: undefined,
renderLen: 1,
start: 0,
offsetStart: 0,
// =============
rowHeight: initialState.rowHeight,
totalLen: 0,
vid: DEFAULT_VID,
});
// =============组件 =================== //
function VCell(props: any): JSX.Element {
const { children, ...restProps } = props;
return (
<td {...restProps}>
<div>{children[1]}</div>
</td>
);
}
function VRow(props: any, ref: any): JSX.Element {
const { rowHeight } = useContext(ScrollContext);
const { children, style, ...restProps } = props;
const trRef = useRef<HTMLTableRowElement>(null);
return (
<tr
{...restProps}
ref={Object.prototype.hasOwnProperty.call(ref, 'current') ? ref : trRef}
style={{
...style,
height: rowHeight || 'auto',
boxSizing: 'border-box',
}}
>
{children}
</tr>
);
}
function VWrapper(props: any): JSX.Element {
const { children, ...restProps } = props;
const { renderLen, start, dispatch, vid, totalLen } =
useContext(ScrollContext);
const contents = useMemo(() => {
return children[1];
}, [children]);
const contentsLen = useMemo(() => {
return contents?.length ?? 0;
}, [contents]);
useEffect(() => {
if (totalLen !== contentsLen) {
dispatch({
type: 'changeTotalLen',
totalLen: contentsLen ?? 0,
});
}
}, [contentsLen, dispatch, vid, totalLen]);
let tempNode = null;
if (Array.isArray(contents) && contents.length) {
tempNode = [
children[0],
contents.slice(start, start + (renderLen ?? 1)).map((item) => {
if (Array.isArray(item)) {
// 兼容antd v4.3.5 --- rc-table 7.8.1及以下
return item[0];
}
// 处理antd ^v4.4.0 --- rc-table ^7.8.2
return item;
}),
];
} else {
tempNode = children;
}
return <tbody {...restProps}>{tempNode}</tbody>;
}
function VTable(props: any, otherParams): JSX.Element {
const { style, children, ...rest } = props;
const { width, ...rest_style } = style;
const { vid, scrollY, resetScrollTopWhenDataChange, rowHeight, scrollTop } =
otherParams ?? {};
const [state, dispatch] = useReducer(reducer, {
...initialState,
curScrollTop: scrollTop,
rowHeight,
});
const wrap_tableRef = useRef<HTMLDivElement>(null);
const tableRef = useRef<HTMLTableElement>(null);
const ifChangeRef = useRef(false);
// 数据的总条数
const [totalLen, setTotalLen] = useState<number>(
children[1]?.props?.data?.length ?? 0,
);
useEffect(() => {
setTotalLen(state.totalLen);
}, [state.totalLen]);
// 组件卸载的清除操作
useEffect(() => {
return () => {
vidMap.delete(vid);
};
}, [vid]);
// 数据变更
useEffect(() => {
ifChangeRef.current = true;
// console.log('数据变更')
if (isNumber(children[1]?.props?.data?.length)) {
dispatch({
type: 'changeTotalLen',
totalLen: children[1]?.props?.data?.length ?? 0,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [children[1].props.data]);
// table总高度
const tableHeight = useMemo<string | number>(() => {
let temp: string | number = 'auto';
if (rowHeight && totalLen) {
temp = rowHeight * totalLen;
}
return temp;
}, [totalLen]);
// table的scrollY值
const [tableScrollY, setTableScrollY] = useState(0);
// tableScrollY 随scrollY / tableHeight 进行变更
useEffect(() => {
let temp = 0;
if (typeof scrollY === 'string') {
temp =
(wrap_tableRef.current?.parentNode as HTMLElement)?.offsetHeight ?? 0;
} else {
temp = scrollY;
}
// if (isNumber(tableHeight) && tableHeight < temp) {
// temp = tableHeight;
// }
// 处理tableScrollY <= 0的情况
if (temp <= 0) {
temp = 0;
}
setTableScrollY(temp);
}, [scrollY, tableHeight]);
// 渲染的条数
const renderLen = useMemo<number>(() => {
let temp = 1;
if (rowHeight && totalLen && tableScrollY) {
if (tableScrollY <= 0) {
temp = 0;
} else {
const tempRenderLen = ((tableScrollY / rowHeight) | 0) + 10;
// console.log('tempRenderLen', tempRenderLen)
// temp = tempRenderLen > totalLen ? totalLen : tempRenderLen;
temp = tempRenderLen;
}
}
return temp;
}, [totalLen, tableScrollY]);
// 渲染中的第一条
let start = rowHeight ? (state.curScrollTop / rowHeight) | 0 : 0;
start = start < 5 ? 0 : start - 5 + 1;
// 偏移量
let offsetStart = state.curScrollTop % (rowHeight * 5);
if (start > 0) {
offsetStart = offsetStart % rowHeight;
}
// console.log(offsetStart)
// offsetStart= offsetStart%rowHeight
// 用来优化向上滚动出现的空白
if (state.curScrollTop && state.curScrollTop >= rowHeight * 5) {
// start -= 1
// if (offsetStart >= rowHeight) {
// offsetStart +=
// } else {
offsetStart += rowHeight * 4;
// }
} else {
start = 0;
}
// console.log(state.curScrollTop, start, offsetStart)
// 数据变更 操作scrollTop
useEffect(() => {
const scrollNode = wrap_tableRef.current?.parentNode as HTMLElement;
if (ifChangeRef?.current) {
// console.log(scrollNode)
ifChangeRef.current = false;
if (resetScrollTopWhenDataChange) {
// 重置scrollTop
if (scrollNode) {
scrollNode.scrollTop = 0;
}
dispatch({ type: 'reset', ifScrollTopClear: true });
} else {
// console.log(preData)
// scrollNode.scrollTop = preData+53
// 不重置scrollTop 不清空curScrollTop
dispatch({ type: 'reset', ifScrollTopClear: false });
}
}
if (vidMap.has(vid)) {
vidMap.set(vid, {
...vidMap.get(vid),
scrollNode,
});
}
}, [totalLen, resetScrollTopWhenDataChange, vid, children]);
useEffect(() => {
const throttleScroll = throttle((e) => {
const scrollTop: number = e?.target?.scrollTop ?? 0;
// const scrollHeight: number = e?.target?.scrollHeight ?? 0
// const clientHeight: number = e?.target?.clientHeight ?? 0
if (scrollTop) {
preData = scrollTop;
}
// 到底了 没有滚动条就不会触发reachEnd. 建议设置scrolly高度少点或者数据量多点.
// 若renderLen大于totalLen, 置空curScrollTop. => table paddingTop会置空.
dispatch({
type: 'changeTrs',
curScrollTop: renderLen <= totalLen ? scrollTop : 0,
});
}, 60);
const ref = wrap_tableRef?.current?.parentNode as HTMLElement;
if (ref) {
ref.addEventListener('scroll', throttleScroll, { passive: true });
}
return () => {
ref.removeEventListener('scroll', throttleScroll);
};
}, [renderLen, totalLen]);
return (
<div
className="virtuallist"
ref={wrap_tableRef}
style={{
width: '100%',
position: 'relative',
height: tableHeight,
boxSizing: 'border-box',
paddingTop: state.curScrollTop,
}}
>
<ScrollContext.Provider
value={{
dispatch,
start,
offsetStart,
renderLen,
totalLen,
vid,
rowHeight,
}}
>
<table
{...rest}
ref={tableRef}
style={{
...rest_style,
width,
position: 'relative',
transform: `translateY(-${offsetStart}px)`,
}}
>
{children}
</table>
</ScrollContext.Provider>
</div>
);
}
// ================导出===================
export function VList(props: {
height: number;
// 唯一标识
vid?: string;
rowHeight: number | string;
reset?: boolean;
scrollTop?: number | string;
}): any {
const { vid = DEFAULT_VID, rowHeight, height, reset, scrollTop } = props;
const resetScrollTopWhenDataChange = reset ?? true;
if (!vidMap.has(vid)) {
vidMap.set(vid, { _id: vid });
}
return {
table: (p) =>
VTable(p, {
vid,
scrollY: height,
rowHeight,
resetScrollTopWhenDataChange,
scrollTop,
}),
body: {
wrapper: VWrapper,
row: VRow,
cell: VCell,
},
};
}
+48
View File
@@ -0,0 +1,48 @@
import { useMemo, useState } from 'react';
export default (
treeData: any[],
searchValue: string,
{
treeNodeFilterProp,
}: {
treeNodeFilterProp: string;
},
) => {
return useMemo(() => {
const keys: string[] = [];
if (!searchValue) {
return { treeData, keys };
}
const upperStr = searchValue.toUpperCase();
function filterOptionFunc(_: string, dataNode: any[]) {
const value = dataNode[treeNodeFilterProp as any];
return String(value).toUpperCase().includes(upperStr);
}
function dig(list: any[], keepAll: boolean = false): any[] {
return list
.map((dataNode) => {
const children = dataNode.children;
const match = keepAll || filterOptionFunc!(searchValue, dataNode);
const childList = dig(children || [], match);
if (match || childList.length) {
childList.length && keys.push(dataNode.key);
return {
...dataNode,
children: childList,
};
}
return null;
})
.filter((node) => node);
}
return { treeData: dig(treeData), keys };
}, [treeData, searchValue, treeNodeFilterProp]);
};
+19
View File
@@ -0,0 +1,19 @@
import { MutableRefObject, useLayoutEffect, useState } from 'react';
import useResizeObserver from '@react-hook/resize-observer';
import { getTableScroll } from '@/utils';
export default <T extends HTMLElement>(
target: MutableRefObject<T>,
extraHeight?: number,
) => {
const [height, setHeight] = useState<number>(0);
useResizeObserver(target, (entry) => {
let _target = entry.target as any;
if (!_target.classList.contains('ant-table-wrapper')) {
_target = entry.target.querySelector('.ant-table-wrapper');
}
setHeight(getTableScroll({ extraHeight, target: _target as HTMLElement }));
});
return height;
};
+8 -7
View File
@@ -10,6 +10,7 @@ import {
ContainerOutlined,
} from '@ant-design/icons';
import IconFont from '@/components/iconfont';
import { BasicLayoutProps } from '@ant-design/pro-layout';
export default {
route: {
@@ -68,18 +69,18 @@ export default {
icon: <IconFont type="ql-icon-dependence" />,
component: '@/pages/dependence/index',
},
{
path: '/log',
name: '日志管理',
icon: <IconFont type="ql-icon-log" />,
component: '@/pages/log/index',
},
{
path: '/diff',
name: '对比工具',
icon: <IconFont type="ql-icon-diff" />,
component: '@/pages/diff/index',
},
{
path: '/log',
name: '任务日志',
icon: <IconFont type="ql-icon-log" />,
component: '@/pages/log/index',
},
{
path: '/setting',
name: '系统设置',
@@ -93,4 +94,4 @@ export default {
contentWidth: 'Fixed',
splitMenus: false,
siderWidth: 180,
} as any;
} as BasicLayoutProps;
+114 -45
View File
@@ -9,9 +9,25 @@
url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype');
}
body {
// 禁止手机页面下拉刷新
overflow: hidden;
// 禁止手机页面弹簧效果
position: fixed;
top: 0;
left: 0;
}
#root {
height: 100vh;
height: calc(100vh - var(--vh-offset, 0px));
-webkit-overflow-scrolling: touch;
}
.ant-modal-body {
max-height: calc(85vh - 110px);
max-height: calc(85vh - var(--vh-offset, 110px));
max-height: calc(90vh - 110px);
max-height: calc(90vh - var(--vh-offset, 110px));
overflow-y: auto;
}
@@ -161,7 +177,7 @@
&.env-wrapper,
&.config-wrapper {
.CodeMirror {
width: calc(100vw - 80px);
width: calc(100vw - 24px);
}
}
}
@@ -179,8 +195,8 @@
.Resizer {
background: @component-background;
opacity: 0.2;
z-index: 1;
opacity: 0.8;
z-index: 100;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
@@ -243,31 +259,29 @@
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;
}
/* 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 {
box-shadow: none;
transition: background-color 5000s ease-in-out 0s;
-webkit-text-fill-color: @text-color;
caret-color: @text-color;
color: @text-color;
}
::placeholder {
opacity: 0.5 !important;
}
::placeholder {
opacity: 0.5 !important;
}
.ant-select-selection-placeholder {
opacity: 0.5 !important;
}
.ant-select-selection-placeholder {
opacity: 0.5 !important;
}
.ant-pro-basicLayout-content {
@@ -313,28 +327,25 @@
.side-menu-user-drop-menu {
position: relative;
text-align: left;
outline: none;
padding: 4px 0;
border: 1px solid fade(@component-background, 0.12);
border-radius: 4px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
padding: 2px 10px;
overflow: auto;
background-color: @component-background;
}
[data-dark='true'] .side-menu-user-drop-menu {
background-color: #373739;
}
.ant-layout-sider {
border-right: 1px solid rgba(0, 0, 0, 0.06) !important;
}
.ant-pro-sider-logo {
padding: 16px 8px !important;
.title {
display: flex;
height: 32px;
margin: 0 5px;
font-weight: 600;
font-size: 16px;
line-height: 32px;
vertical-align: middle;
animation: pro-layout-title-hide 0.3s;
h1 {
margin-left: 5px !important;
a {
display: inline-flex;
align-items: center;
}
}
img {
@@ -348,4 +359,62 @@
// 移动端logo被拉伸
width: auto !important;
}
.ant-image {
display: inline-flex;
}
}
pre {
word-break: break-all !important;
white-space: break-spaces !important;
padding: 0 !important;
}
.virtuallist {
.ant-table-tbody > tr > td > div {
white-space: unset !important;
}
}
.virtuallist .ant-table-tbody > tr > td > div {
box-sizing: border-box;
white-space: nowrap;
vertical-align: middle;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.virtuallist .ant-table-tbody > tr > td.ant-table-row-expand-icon-cell > div {
overflow: inherit;
}
.ant-table-bordered .virtuallist > table > .ant-table-tbody > tr > td {
border-right: 1px solid #f0f0f0;
}
.ant-table-column-title {
flex: unset;
}
.ant-table-column-sorters,
.ant-table-filter-column {
justify-content: unset;
}
textarea.ant-input {
word-break: break-all;
}
.ant-pro-sider-collapsed,
body[data-mode='phone'] header {
.title {
display: none;
}
}
.ant-design-pro .ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed {
box-shadow: none;
border-right: 1px solid #eee;
}
+139 -80
View File
@@ -2,7 +2,7 @@ import React, { useEffect, useState, useRef } from 'react';
import ProLayout, { PageLoading } from '@ant-design/pro-layout';
import * as DarkReader from '@umijs/ssr-darkreader';
import defaultProps from './defaultProps';
import { Link, history } from 'umi';
import { Link, history, Outlet, useLocation } from '@umijs/max';
import {
LogoutOutlined,
MenuFoldOutlined,
@@ -13,23 +13,59 @@ import config from '@/utils/config';
import { request } from '@/utils/http';
import './index.less';
import vhCheck from 'vh-check';
import { version, changeLogLink, changeLog } from '../version';
import { useCtx, useTheme } from '@/utils/hooks';
import { message, Badge, Modal, Avatar, Dropdown, Menu, Image } from 'antd';
import {
message,
Badge,
Modal,
Avatar,
Dropdown,
Menu,
Image,
Popover,
Descriptions,
Tooltip,
MenuProps,
} from 'antd';
// @ts-ignore
import SockJS from 'sockjs-client';
import * as Sentry from '@sentry/react';
import { init } from '../utils/init';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/python/python';
import 'codemirror/mode/shell/shell';
export default function (props: any) {
export interface SharedContext {
headerStyle: React.CSSProperties;
isPhone: boolean;
theme: 'vs' | 'vs-dark';
user: any;
reloadUser: (needLoading?: boolean) => void;
reloadTheme: () => void;
socketMessage: any;
systemInfo: TSystemInfo;
}
interface TSystemInfo {
branch: 'develop' | 'master';
isInitialized: boolean;
publishTime: number;
version: string;
changeLog: string;
changeLogLink: string;
}
export default function () {
const location = useLocation();
const ctx = useCtx();
const { theme, reloadTheme } = useTheme();
const [user, setUser] = useState<any>({});
const [loading, setLoading] = useState<boolean>(true);
const [systemInfo, setSystemInfo] = useState<{ isInitialized: boolean }>();
const [systemInfo, setSystemInfo] = useState<TSystemInfo>();
const ws = useRef<any>(null);
const [socketMessage, setSocketMessage] = useState<any>();
const [collapsed, setCollapsed] = useState(false);
const [initLoading, setInitLoading] = useState<boolean>(true);
const {
enable: enableDarkMode,
disable: disableDarkMode,
@@ -53,12 +89,10 @@ export default function (props: any) {
setSystemInfo(data);
if (!data.isInitialized) {
history.push('/initialization');
setLoading(false);
} else {
init(data.version);
getUser();
}
} else {
message.error(data);
}
})
.catch((error) => {
@@ -73,11 +107,9 @@ export default function (props: any) {
.then(({ code, data }) => {
if (code === 200 && data.username) {
setUser(data);
if (props.location.pathname === '/') {
if (location.pathname === '/') {
history.push('/crontab');
}
} else {
message.error(data);
}
needLoading && setLoading(false);
})
@@ -86,6 +118,22 @@ export default function (props: any) {
});
};
const getHealthStatus = () => {
request
.get(`${config.apiPrefix}public/health`)
.then((res) => {
if (res?.data?.status === 1) {
getSystemInfo();
} else {
history.push('/error');
}
})
.catch((error) => {
history.push('/error');
})
.finally(() => setInitLoading(false));
};
const reloadUser = (needLoading = false) => {
getUser(needLoading);
};
@@ -94,13 +142,11 @@ export default function (props: any) {
if (systemInfo && systemInfo.isInitialized && !user) {
getUser();
}
}, [props.location.pathname]);
}, [location.pathname]);
useEffect(() => {
if (!systemInfo) {
getSystemInfo();
}
}, [systemInfo]);
getHealthStatus();
}, []);
useEffect(() => {
if (theme === 'vs-dark') {
@@ -112,7 +158,6 @@ export default function (props: any) {
useEffect(() => {
vhCheck();
init();
const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
if (typeof window === 'undefined') return;
@@ -136,9 +181,11 @@ export default function (props: any) {
}, []);
useEffect(() => {
if (!user) return;
if (!user || !user.username) return;
ws.current = new SockJS(
`${location.origin}/api/ws?token=${localStorage.getItem(config.authKey)}`,
`${window.location.origin}/api/ws?token=${localStorage.getItem(
config.authKey,
)}`,
);
ws.current.onmessage = (e: any) => {
@@ -146,9 +193,9 @@ export default function (props: any) {
const data = JSON.parse(e.data);
if (data.type === 'ping') {
if (data && data.message === 'hanhh') {
console.log('websocket连接成功', e);
console.log('WS connection succeeded !!!');
} else {
console.log('websocket连接失败', e);
console.log('WS connection Failed !!!', e);
}
}
setSocketMessage(data);
@@ -183,29 +230,28 @@ export default function (props: any) {
};
}, []);
if (
['/login', '/initialization', '/error'].includes(props.location.pathname)
) {
document.title = `${(config.documentTitleMap as any)[props.location.pathname]
} - `;
if (
systemInfo?.isInitialized &&
props.location.pathname === '/initialization'
) {
if (initLoading) {
return <PageLoading />;
}
if (['/login', '/initialization', '/error'].includes(location.pathname)) {
if (systemInfo?.isInitialized && location.pathname === '/initialization') {
history.push('/crontab');
}
if (systemInfo || props.location.pathname === '/error') {
return React.Children.map(props.children, (child) => {
return React.cloneElement(child, {
...ctx,
theme,
user,
reloadUser,
reloadTheme,
ws: ws.current,
});
});
if (systemInfo || location.pathname === '/error') {
return (
<Outlet
context={{
...ctx,
theme,
user,
reloadUser,
reloadTheme,
ws: ws.current,
}}
/>
);
}
}
@@ -215,46 +261,59 @@ export default function (props: any) {
!navigator.userAgent.includes('Chrome');
const isQQBrowser = navigator.userAgent.includes('QQBrowser');
const menu = (
<Menu
className="side-menu-user-drop-menu"
items={[{ label: '退出登录', key: 'logout', icon: <LogoutOutlined /> }]}
onClick={logout}
/>
);
const menu: MenuProps = {
items: [
{
label: '退出登录',
className: 'side-menu-user-drop-menu',
onClick: logout,
key: 'logout',
icon: <LogoutOutlined />,
},
],
};
return loading ? (
<PageLoading />
) : (
<ProLayout
selectedKeys={[props.location.pathname]}
selectedKeys={[location.pathname]}
loading={loading}
ErrorBoundary={Sentry.ErrorBoundary}
logo={<Image preview={false} src="http://qn.whyour.cn/logo.png" />}
title={
logo={
<>
<span style={{ fontSize: 16 }}></span>
<a
href={changeLogLink}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => {
e.stopPropagation();
}}
>
<span
style={{
fontSize: isFirefox ? 9 : 12,
color: '#666',
marginLeft: 2,
zoom: isSafari ? 0.66 : 0.8,
letterSpacing: isQQBrowser ? -2 : 0,
<Image preview={false} src="https://qn.whyour.cn/logo.png" />
<div className="title">
<span className="title"></span>
<a
href={systemInfo?.changeLogLink}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => {
e.stopPropagation();
}}
>
v{version}
</span>
</a>
<Tooltip
title={systemInfo?.branch === 'develop' ? '开发版' : '正式版'}
>
<Badge size="small" dot={systemInfo?.branch === 'develop'}>
<span
style={{
fontSize: isFirefox ? 9 : 12,
color: '#666',
marginLeft: 2,
zoom: isSafari ? 0.66 : 0.8,
letterSpacing: isQQBrowser ? -2 : 0,
}}
>
v{systemInfo?.version}
</span>
</Badge>
</Tooltip>
</a>
</div>
</>
}
title={false}
menuItemRender={(menuItemProps: any, defaultDom: any) => {
if (
menuItemProps.isUrl ||
@@ -266,16 +325,15 @@ export default function (props: any) {
return <Link to={menuItemProps.path}>{defaultDom}</Link>;
}}
pageTitleRender={(props, pageName, info) => {
if (info && typeof info.pageName === 'string') {
return `${info.pageName} - 控制面板`;
}
return '控制面板';
const title =
(config.documentTitleMap as any)[location.pathname] || '未找到';
return `${title} - 青龙`;
}}
onCollapse={setCollapsed}
collapsed={collapsed}
rightContentRender={() =>
ctx.isPhone && (
<Dropdown overlay={menu} placement="bottomRight" trigger={['click']}>
<Dropdown menu={menu} placement="bottomRight" trigger={['click']}>
<span className="side-menu-user-wrapper">
<Avatar
shape="square"
@@ -297,7 +355,7 @@ export default function (props: any) {
}}
>
{!collapsed && !ctx.isPhone && (
<Dropdown overlay={menu} placement="topLeft" trigger={['hover']}>
<Dropdown menu={menu} placement="topLeft" trigger={['hover']}>
<span className="side-menu-user-wrapper">
<Avatar
shape="square"
@@ -319,16 +377,17 @@ export default function (props: any) {
)}
{...defaultProps}
>
{React.Children.map(props.children, (child) => {
return React.cloneElement(child, {
<Outlet
context={{
...ctx,
theme,
user,
reloadUser,
reloadTheme,
socketMessage,
});
})}
systemInfo,
}}
/>
</ProLayout>
);
}
+18
View File
@@ -0,0 +1,18 @@
import React from 'react';
import { Button, Result, Typography } from 'antd';
const { Link } = Typography;
const NotFound: React.FC = () => (
<Result
status="404"
title="404"
extra={
<Button type="primary">
<Link href="/"></Link>
</Button>
}
/>
);
export default NotFound;
+18 -10
View File
@@ -11,8 +11,11 @@ import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http';
import Editor from '@monaco-editor/react';
import { Controlled as CodeMirror } from 'react-codemirror2';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
const Config = ({ headerStyle, isPhone, theme }: any) => {
const Config = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const [value, setValue] = useState('');
const [loading, setLoading] = useState(true);
const [title, setTitle] = useState('config.sh');
@@ -22,8 +25,10 @@ const Config = ({ headerStyle, isPhone, theme }: any) => {
const [confirmLoading, setConfirmLoading] = useState(false);
const getConfig = (name: string) => {
request.get(`${config.apiPrefix}configs/${name}`).then((data: any) => {
setValue(data.data);
request.get(`${config.apiPrefix}configs/${name}`).then(({ code, data }) => {
if (code === 200) {
setValue(data);
}
});
};
@@ -31,8 +36,10 @@ const Config = ({ headerStyle, isPhone, theme }: any) => {
setLoading(true);
request
.get(`${config.apiPrefix}configs/files`)
.then((data: any) => {
setData(data.data);
.then(({ code, data }) => {
if (code === 200) {
setData(data);
}
})
.finally(() => setLoading(false));
};
@@ -44,11 +51,11 @@ const Config = ({ headerStyle, isPhone, theme }: any) => {
: value;
request
.post(`${config.apiPrefix}configs/save`, {
data: { content, name: select },
})
.then((data: any) => {
message.success(data.message);
.post(`${config.apiPrefix}configs/save`, { content, name: select })
.then(({ code, data }) => {
if (code === 200) {
message.success('保存成功');
}
setConfirmLoading(false);
});
};
@@ -71,6 +78,7 @@ const Config = ({ headerStyle, isPhone, theme }: any) => {
loading={loading}
extra={[
<TreeSelect
treeExpandAction="click"
className="config-select"
value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}

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