Compare commits

...
227 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
142 changed files with 12934 additions and 7966 deletions
+2 -1
View File
@@ -9,4 +9,5 @@ SECRET='whyour'
QINIU_AK='' QINIU_AK=''
QINIU_SK='' QINIU_SK=''
QINIU_SCOPE='' QINIU_SCOPE=''
TEMP=''
+13 -10
View File
@@ -5,15 +5,11 @@ on:
branches: branches:
- 'master' - 'master'
- 'develop' - 'develop'
# Sequence of patterns matched against refs/tags
tags: tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
schedule: schedule:
# 参考 https://jasonet.co/posts/scheduled-actions/ # 参考 https://jasonet.co/posts/scheduled-actions/
# note: 这里是GMT时间,北京时间减去八小时即可。如北京时间 22:30 => GMT 14:30 - cron: '00 14 * * *' # GMT 15:00 => 北京时间 23:00
# minute hour day month dayOfWeek
- cron: '00 14 * * *' # GMT 14:00 => 北京时间 22:00
#- cron: '30 16 * * *' # GMT 16:30(前一天) => 北京时间 00:30
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@@ -49,7 +45,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: latest version: '8.3.1'
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
@@ -101,7 +97,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: latest version: '8.3.1'
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: 'pnpm' cache: 'pnpm'
@@ -133,11 +129,14 @@ jobs:
ghcr.io/${{ github.repository }} ghcr.io/${{ github.repository }}
# generate Docker tags based on the following events/attributes # generate Docker tags based on the following events/attributes
# nightly, master, pr-2, 1.2.3, 1.2, 1 # nightly, master, pr-2, 1.2.3, 1.2, 1
flavor: |
latest=false
tags: | tags: |
type=schedule,pattern=nightly type=schedule,pattern=nightly
type=edge type=edge
type=ref,event=branch
type=ref,event=pr 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={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}} type=semver,pattern={{major}}
@@ -155,12 +154,16 @@ jobs:
build-args: | build-args: |
MAINTAINER=${{ github.repository_owner }} MAINTAINER=${{ github.repository_owner }}
QL_BRANCH=${{ github.ref_name }} QL_BRANCH=${{ github.ref_name }}
SOURCE_COMMIT=${{ github.sha }}
network: host network: host
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386
context: docker/ context: .
file: ./docker/Dockerfile
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=whyour/qinglong:cache
cache-to: type=registry,ref=whyour/qinglong:cache,mode=max
- name: Image digest - name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }} run: echo ${{ steps.docker_build.outputs.digest }}
+2 -1
View File
@@ -28,4 +28,5 @@
/db /db
/manual_log /manual_log
/scripts /scripts
/bak /bak
/.tmp
+8 -3
View File
@@ -1,24 +1,28 @@
import { defineConfig } from '@umijs/max'; import { defineConfig } from '@umijs/max';
const CompressionPlugin = require('compression-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin');
const baseUrl = process.env.QlBaseUrl || '/';
export default defineConfig({ export default defineConfig({
hash: true, hash: true,
antd: {}, antd: {},
outputPath: 'static/dist', outputPath: 'static/dist',
fastRefresh: true, fastRefresh: true,
favicons: ['/images/favicon.svg'], favicons: [`https://qn.whyour.cn/favicon.svg`],
mfsu: { mfsu: {
strategy: 'eager', strategy: 'eager',
}, },
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
proxy: { proxy: {
'/api/public': { [`${baseUrl}api/public`]: {
target: 'http://127.0.0.1:5400/', target: 'http://127.0.0.1:5400/',
changeOrigin: true, changeOrigin: true,
pathRewrite: { [`^${baseUrl}api/public`]: '/api' },
}, },
'/api': { [`${baseUrl}api`]: {
target: 'http://127.0.0.1:5600/', target: 'http://127.0.0.1:5600/',
changeOrigin: true, changeOrigin: true,
ws: true, ws: true,
pathRewrite: { [`^${baseUrl}api`]: '/api' },
}, },
}, },
chainWebpack: ((config: any) => { chainWebpack: ((config: any) => {
@@ -36,6 +40,7 @@ export default defineConfig({
'react-dom': 'window.ReactDOM', 'react-dom': 'window.ReactDOM',
}, },
headScripts: [ 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/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', 'https://gw.alipayobjects.com/os/lib/react-dom/18.2.0/umd/react-dom.production.min.js',
], ],
+75 -98
View File
@@ -1,30 +1,29 @@
<p align="center">
<a href="https://github.com/whyour/qinglong">
<img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
</a>
</p>
<h1 align="center">Green Dragon</h1>
<div align="center"> <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-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong [docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
[docker-version-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-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong [docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat [docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong [docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
[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> </div>
[![](https://user-images.githubusercontent.com/22700758/203243067-1a8a570d-b1b4-4837-9f12-d78d83e31f35.jpg)](https://whyour.cn) ![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png)
[简体中文](./README.md) | English
## Features ## Features
@@ -36,64 +35,30 @@ Timed task management panel with python3, javaScript, shell, typescript support
- Support dark mode - Support dark mode
- Support cell phone operation - 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 ## Deployment
### Local Deployment ### Docker (Recommended)
```bash
# To be refined, see the development steps first (not supported on windows yet)
```
### 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 \
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
```bash ```bash
# curl -sSL get.docker.com | sh
docker run -dit \ docker run -dit \
-v $PWD/ql/data:/ql/data \ -v $PWD/ql/data:/ql/data \
-p 5700:5700 \ -p 5700:5700 \
@@ -103,17 +68,10 @@ docker run -dit \
whyour/qinglong:latest whyour/qinglong:latest
``` ```
### Docker-compose Deployment ### Docker-compose (Recommended)
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
```bash ```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 mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
@@ -123,9 +81,32 @@ docker-compose up -d
docker-compose down 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 ## Use
@@ -181,6 +162,20 @@ task -l <file_path>
* account_number: Specify the account number of an environment variable to be executed when the task is executed * account_number: Specify the account number of an environment variable to be executed when the task is executed
* max_time: Timeout, suffix "s" for seconds (default), "m" for minutes, "h" for hours, "d" for days * max_time: Timeout, suffix "s" for seconds (default), "m" for minutes, "h" for hours, "d" for days
## Development
```bash
$ 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@8.3.1
$ pnpm install
$ pnpm start
```
Open your browser and visit http://127.0.0.1:5700
## Links ## Links
- [nevinee](https://gitee.com/evine) - [nevinee](https://gitee.com/evine)
@@ -191,24 +186,6 @@ task -l <file_path>
- [darkreader](https://github.com/darkreader/darkreader) - [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server) - [admin-server](https://github.com/sunpu007/admin-server)
## Development
```bash
$ 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
$ pnpm install
$ pnpm start
```
Open your browser and visit http://127.0.0.1:5700
## Communication
[telegram channel](https://t.me/jiao_long)
## Name Origin ## Name Origin
The Green Dragon, also known as the Canglong, is one of the four elephants and one of the [four spirits of the heavens](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5) in traditional Chinese culture. According to the Five Elements, it is a spirit animal representing the East as a green dragon, the five elements are wood, and the season represented is spring, with the eight trigrams dominating vibration. Like the Ying Long, the Cang Long has feathered wings. According to the Zhang Guo Xing Zong (Zhang Guo Xing Zong), "a true dragon is one that has complementary wings". The Green Dragon, also known as the Canglong, is one of the four elephants and one of the [four spirits of the heavens](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5) in traditional Chinese culture. According to the Five Elements, it is a spirit animal representing the East as a green dragon, the five elements are wood, and the season represented is spring, with the eight trigrams dominating vibration. Like the Ying Long, the Cang Long has feathered wings. According to the Zhang Guo Xing Zong (Zhang Guo Xing Zong), "a true dragon is one that has complementary wings".
+79 -97
View File
@@ -1,30 +1,31 @@
<p align="center"> <div align="center">
<a href="https://github.com/whyour/qinglong"> <img width="100" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
<img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
</a>
</p>
<h1 align="center">青龙</h1> <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-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong [docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
[docker-version-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-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong [docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat [docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong [docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
[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> </div>
[![](https://user-images.githubusercontent.com/22700758/203243067-1a8a570d-b1b4-4837-9f12-d78d83e31f35.jpg)](https://whyour.cn) ![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png)
简体中文 | [English](./README-en.md)
## 功能 ## 功能
@@ -36,85 +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
```
## 部署 ## 部署
### 本机部署 ### docker (推荐)
```bash
# 待完善,可先参考开发步骤 (windows暂时不支持)
```
### 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 \
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. 启动容器
```bash ```bash
# curl -sSL get.docker.com | sh
docker run -dit \ docker run -dit \
-v $PWD/ql/data:/ql/data \ -v $PWD/ql/data:/ql/data \
-p 5700:5700 \ -p 5700:5700 \
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
-e QlBaseUrl="/" \
--name qinglong \ --name qinglong \
--hostname qinglong \ --hostname qinglong \
--restart unless-stopped \ --restart unless-stopped \
whyour/qinglong:latest whyour/qinglong:latest
``` ```
### docker-compose 部署 ### docker-compose (推荐)
1. docker-compose 安装
```bash
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
```
2. 启动容器
```bash ```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 mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
@@ -124,9 +85,34 @@ docker-compose up -d
docker-compose down 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
```
## 使用 ## 使用
@@ -181,6 +167,20 @@ task -l <file_path>
* account_number: 任务执行时指定某个环境变量需要执行的账号序号 * account_number: 任务执行时指定某个环境变量需要执行的账号序号
* max_time: 超时时间,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天 * max_time: 超时时间,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
## 开发
```bash
$ git clone git@github.com:whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
# 推荐使用 pnpm https://pnpm.io/zh/installation
$ npm install -g pnpm@8.3.1
$ pnpm install
$ pnpm start
```
打开你的浏览器,访问 http://127.0.0.1:5700
## 链接 ## 链接
- [nevinee](https://gitee.com/evine) - [nevinee](https://gitee.com/evine)
@@ -191,24 +191,6 @@ task -l <file_path>
- [darkreader](https://github.com/darkreader/darkreader) - [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server) - [admin-server](https://github.com/sunpu007/admin-server)
## 开发
```bash
$ git clone git@github.com:whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
# 推荐使用 pnpm https://pnpm.io/zh/installation
$ npm install -g pnpm
$ pnpm install
$ pnpm start
```
打开你的浏览器,访问 http://127.0.0.1:5700
## 交流
[telegram频道](https://t.me/jiao_long)
## 名称来源 ## 名称来源
青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。 青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
+4 -3
View File
@@ -5,6 +5,7 @@ import { Logger } from 'winston';
import config from '../config'; import config from '../config';
import * as fs from 'fs'; import * as fs from 'fs';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import { join } from 'path';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
@@ -41,11 +42,11 @@ export default (app: Router) => {
} }
if (req.params.file.includes('sample')) { if (req.params.file.includes('sample')) {
content = getFileContentByName( content = getFileContentByName(
`${config.samplePath}${req.params.file}`, join(config.samplePath, req.params.file),
); );
} else { } else {
content = getFileContentByName( content = getFileContentByName(
`${config.configPath}${req.params.file}`, join(config.configPath, req.params.file),
); );
} }
res.send({ code: 200, data: content }); res.send({ code: 200, data: content });
@@ -70,7 +71,7 @@ export default (app: Router) => {
if (config.blackFileList.includes(name)) { if (config.blackFileList.includes(name)) {
res.send({ code: 403, message: '文件无法访问' }); res.send({ code: 403, message: '文件无法访问' });
} }
const path = `${config.configPath}${name}`; const path = join(config.configPath, name);
fs.writeFileSync(path, content); fs.writeFileSync(path, content);
res.send({ code: 200, message: '保存成功' }); res.send({ code: 200, message: '保存成功' });
} catch (e) { } catch (e) {
+17
View File
@@ -152,6 +152,21 @@ export default (app: Router) => {
} }
}); });
route.get(
'/detail',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.find(req.query as any);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post( route.post(
'/', '/',
celebrate({ celebrate({
@@ -160,6 +175,7 @@ export default (app: Router) => {
schedule: Joi.string().required(), schedule: Joi.string().required(),
name: Joi.string().optional(), name: Joi.string().optional(),
labels: Joi.array().optional(), labels: Joi.array().optional(),
sub_id: Joi.number().optional().allow(null),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -316,6 +332,7 @@ export default (app: Router) => {
command: Joi.string().required(), command: Joi.string().required(),
schedule: Joi.string().required(), schedule: Joi.string().required(),
name: Joi.string().optional().allow(null), name: Joi.string().optional().allow(null),
sub_id: Joi.number().optional().allow(null),
id: Joi.number().required(), id: Joi.number().required(),
}), }),
}), }),
+30 -3
View File
@@ -94,7 +94,7 @@ export default (app: Router) => {
path += '/'; path += '/';
} }
if (!path.startsWith('/')) { if (!path.startsWith('/')) {
path = `${config.scriptPath}${path}`; path = join(config.scriptPath, path);
} }
if (config.writePathList.every((x) => !path.startsWith(x))) { if (config.writePathList.every((x) => !path.startsWith(x))) {
return res.send({ return res.send({
@@ -124,7 +124,7 @@ export default (app: Router) => {
if (fs.existsSync(originFilePath)) { if (fs.existsSync(originFilePath)) {
fs.copyFileSync( fs.copyFileSync(
originFilePath, originFilePath,
`${config.bakPath}${originFilename.replace(/\//g, '')}`, join(config.bakPath, originFilename.replace(/\//g, '')),
); );
if (filename !== originFilename) { if (filename !== originFilename) {
fs.unlinkSync(originFilePath); fs.unlinkSync(originFilePath);
@@ -207,7 +207,7 @@ export default (app: Router) => {
let { filename } = req.body as { let { filename } = req.body as {
filename: string; filename: string;
}; };
const filePath = `${config.scriptPath}${filename}`; const filePath = join(config.scriptPath, filename);
// const stats = fs.statSync(filePath); // const stats = fs.statSync(filePath);
// res.set({ // res.set({
// 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件 // 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件
@@ -274,4 +274,31 @@ export default (app: Router) => {
} }
}, },
); );
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);
}
},
);
}; };
+4
View File
@@ -50,6 +50,8 @@ export default (app: Router) => {
schedule_type: Joi.string().required(), schedule_type: Joi.string().required(),
alias: Joi.string().required(), alias: Joi.string().required(),
proxy: Joi.string().optional().allow('').allow(null), proxy: Joi.string().optional().allow('').allow(null),
autoAddCron: Joi.boolean().optional().allow('').allow(null),
autoDelCron: Joi.boolean().optional().allow('').allow(null),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -179,6 +181,8 @@ export default (app: Router) => {
sub_after: Joi.string().optional().allow('').allow(null), sub_after: Joi.string().optional().allow('').allow(null),
alias: Joi.string().required(), alias: Joi.string().required(),
proxy: Joi.string().optional().allow('').allow(null), proxy: Joi.string().optional().allow('').allow(null),
autoAddCron: Joi.boolean().optional().allow('').allow(null),
autoDelCron: Joi.boolean().optional().allow('').allow(null),
id: Joi.number().required(), id: Joi.number().required(),
}), }),
}), }),
+135 -31
View File
@@ -7,10 +7,25 @@ import SystemService from '../services/system';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import UserService from '../services/user'; import UserService from '../services/user';
import { EnvModel } from '../data/env'; import { EnvModel } from '../data/env';
import { promiseExec } from '../config/util'; import {
getUniqPath,
handleLogPath,
parseVersion,
promiseExec,
} from '../config/util';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import multer from 'multer';
const route = Router(); 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) => { export default (app: Router) => {
app.use('/system', route); app.use('/system', route);
@@ -20,31 +35,15 @@ export default (app: Router) => {
try { try {
const userService = Container.get(UserService); const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo(); const authInfo = await userService.getUserInfo();
const envCount = await EnvModel.count(); const { version, changeLog, changeLogLink, publishTime } = await parseVersion(
const versionRegx = /.*export const version = \'(.*)\'\;/; config.versionFile,
);
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const version = currentVersionFile.match(versionRegx)![1];
const lastCommitTime = (
await promiseExec(
`cd ${config.rootPath} && git show -s --format=%ai | head -1`,
)
).replace('\n', '');
const lastCommitId = (
await promiseExec(`cd ${config.rootPath} && git rev-parse --short HEAD`)
).replace('\n', '');
const branch = (
await promiseExec(
`cd ${config.rootPath} && git symbolic-ref --short HEAD`,
)
).replace('\n', '');
let isInitialized = true; let isInitialized = true;
if ( if (
Object.keys(authInfo).length === 2 && Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' && authInfo.username === 'admin' &&
authInfo.password === 'admin' && authInfo.password === 'admin'
envCount === 0
) { ) {
isInitialized = false; isInitialized = false;
} }
@@ -53,9 +52,10 @@ export default (app: Router) => {
data: { data: {
isInitialized, isInitialized,
version, version,
lastCommitTime: dayjs(lastCommitTime).unix(), publishTime: dayjs(publishTime).unix(),
lastCommitId, branch: process.env.QL_BRANCH || 'master',
branch, changeLog,
changeLogLink,
}, },
}); });
} catch (e) { } catch (e) {
@@ -65,12 +65,12 @@ export default (app: Router) => {
}); });
route.get( route.get(
'/log/remove', '/config',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const systemService = Container.get(SystemService); const systemService = Container.get(SystemService);
const data = await systemService.getLogRemoveFrequency(); const data = await systemService.getSystemConfig();
res.send({ code: 200, data }); res.send({ code: 200, data });
} catch (e) { } catch (e) {
return next(e); return next(e);
@@ -79,19 +79,18 @@ export default (app: Router) => {
); );
route.put( route.put(
'/log/remove', '/config',
celebrate({ celebrate({
body: Joi.object({ 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) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const systemService = Container.get(SystemService); const systemService = Container.get(SystemService);
const result = await systemService.updateLogRemoveFrequency( const result = await systemService.updateSystemConfig(req.body);
req.body.frequency,
);
res.send(result); res.send(result);
} catch (e) { } catch (e) {
return next(e); return next(e);
@@ -127,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( route.put(
'/notify', '/notify',
celebrate({ celebrate({
@@ -146,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 multer from 'multer';
import path from 'path'; import path from 'path';
import { v4 as uuidV4 } from 'uuid'; import { v4 as uuidV4 } from 'uuid';
import rateLimit from 'express-rate-limit';
import config from '../config'; import config from '../config';
const route = Router(); const route = Router();
@@ -26,6 +27,10 @@ export default (app: Router) => {
route.post( route.post(
'/login', '/login',
rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
}),
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
username: Joi.string().required(), username: Joi.string().required(),
+1
View File
@@ -18,6 +18,7 @@ async function startServer() {
const server = app const server = app
.listen(config.port, () => { .listen(config.port, () => {
Logger.debug(`✌️ 后端服务启动成功!`); Logger.debug(`✌️ 后端服务启动成功!`);
process.send?.('ready');
}) })
.on('error', (err) => { .on('error', (err) => {
Logger.error(err); Logger.error(err);
+7 -1
View File
@@ -1 +1,7 @@
export const LOG_END_SYMBOL = '\n          '; export const LOG_END_SYMBOL = '     ';
export const TASK_COMMAND = 'task';
export const QL_COMMAND = 'ql';
export const TASK_PREFIX = `${TASK_COMMAND} `;
export const QL_PREFIX = `${QL_COMMAND} `;
+14 -2
View File
@@ -14,12 +14,13 @@ if (!process.env.QL_DIR) {
process.env.QL_DIR = qlHomePath.replace(/\/$/g, ''); process.env.QL_DIR = qlHomePath.replace(/\/$/g, '');
} }
const lastVersionFile = `https://qn.whyour.cn/version.ts`; const lastVersionFile = `https://qn.whyour.cn/version.yaml`;
const rootPath = process.env.QL_DIR as string; const rootPath = process.env.QL_DIR as string;
const envFound = dotenv.config({ path: path.join(rootPath, '.env') }); const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
const dataPath = path.join(rootPath, 'data/'); const dataPath = path.join(rootPath, 'data/');
const tmpPath = path.join(rootPath, '.tmp/');
const samplePath = path.join(rootPath, 'sample/'); const samplePath = path.join(rootPath, 'sample/');
const configPath = path.join(dataPath, 'config/'); const configPath = path.join(dataPath, 'config/');
const scriptPath = path.join(dataPath, 'scripts/'); const scriptPath = path.join(dataPath, 'scripts/');
@@ -27,6 +28,7 @@ const bakPath = path.join(dataPath, 'bak/');
const logPath = path.join(dataPath, 'log/'); const logPath = path.join(dataPath, 'log/');
const dbPath = path.join(dataPath, 'db/'); const dbPath = path.join(dataPath, 'db/');
const uploadPath = path.join(dataPath, 'upload/'); const uploadPath = path.join(dataPath, 'upload/');
const sshdPath = path.join(dataPath, 'ssh.d/');
const envFile = path.join(configPath, 'env.sh'); const envFile = path.join(configPath, 'env.sh');
const confFile = path.join(configPath, 'config.sh'); const confFile = path.join(configPath, 'config.sh');
@@ -40,7 +42,8 @@ const sqliteFile = path.join(samplePath, 'database.sqlite');
const authError = '错误的用户名密码,请重试'; const authError = '错误的用户名密码,请重试';
const loginFaild = '请先登录!'; const loginFaild = '请先登录!';
const configString = 'config sample crontab shareCode diy'; const configString = 'config sample crontab shareCode diy';
const versionFile = path.join(rootPath, 'src/version.ts'); const versionFile = path.join(rootPath, 'version.yaml');
const dataTgzFile = path.join(tmpPath, 'data.tgz');
if (envFound.error) { if (envFound.error) {
throw new Error("⚠️ Couldn't find .env file ⚠️"); throw new Error("⚠️ Couldn't find .env file ⚠️");
@@ -58,6 +61,9 @@ export default {
prefix: '/api', prefix: '/api',
}, },
rootPath, rootPath,
tmpPath,
dataPath,
dataTgzFile,
configString, configString,
loginFaild, loginFaild,
authError, authError,
@@ -91,8 +97,14 @@ export default {
'/api/system', '/api/system',
'/api/user/init', '/api/user/init',
'/api/user/notification/init', '/api/user/notification/init',
'/open/user/login',
'/open/user/two-factor/login',
'/open/system',
'/open/user/init',
'/open/user/notification/init',
], ],
versionFile, versionFile,
lastVersionFile, lastVersionFile,
sqliteFile, 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;
}
+89 -7
View File
@@ -6,6 +6,9 @@ import { exec } from 'child_process';
import FormData from 'form-data'; import FormData from 'form-data';
import psTreeFun from 'pstree.remy'; import psTreeFun from 'pstree.remy';
import { promisify } from 'util'; import { promisify } from 'util';
import { load } from 'js-yaml';
import config from './index';
import { TASK_COMMAND } from './const';
export function getFileContentByName(fileName: string) { export function getFileContentByName(fileName: string) {
if (fs.existsSync(fileName)) { if (fs.existsSync(fileName)) {
@@ -244,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( export async function concurrentRun(
fnList: Array<() => Promise<any>> = [], fnList: Array<() => Promise<any>> = [],
max = 5, max = 5,
@@ -384,6 +399,18 @@ 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) { export function parseHeaders(headers: string) {
if (!headers) return {}; if (!headers) return {};
@@ -468,17 +495,72 @@ export function psTree(pid: number): Promise<number[]> {
export async function killTask(pid: number) { export async function killTask(pid: number) {
const pids = await psTree(pid); const pids = await psTree(pid);
// SIGALRM 14 时钟信号 // SIGINT 2 程序终止(interrupt)信号,不会打印额外信息
if (pids.length) { if (pids.length) {
process.kill(pids[0], 14); try {
[pid, ...pids].forEach((x) => {
process.kill(x, 2);
});
} catch (error) {}
} else { } else {
process.kill(pid, 14); process.kill(pid, 2);
} }
} }
export async function getPid(name: string) { export async function getPid(name: string) {
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`; const taskCommand = `ps -eo pid,command | grep "${name}" | grep -v grep | awk '{print $1}' | head -1 | xargs echo -n`;
const execAsync = promisify(exec); const pid = await promiseExec(taskCommand);
let pid = (await execAsync(taskCommand)).stdout; return pid ? Number(pid) : undefined;
return Number(pid); }
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 { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize'; import { DataTypes, Model, ModelDefined } from 'sequelize';
import { NotificationInfo } from './notify';
export class AuthInfo { export class AuthInfo {
ip?: string; ip?: string;
type: AuthDataType; type: AuthDataType;
info?: any; info?: AuthModelInfo;
id?: number; id?: number;
constructor(options: AuthInfo) { constructor(options: AuthInfo) {
@@ -25,9 +26,25 @@ export enum AuthDataType {
'authToken' = 'authToken', 'authToken' = 'authToken',
'notification' = 'notification', 'notification' = 'notification',
'removeLogFrequency' = 'removeLogFrequency', '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', { export const AuthModel = sequelize.define<AuthInstance>('Auth', {
ip: DataTypes.STRING, ip: DataTypes.STRING,
type: DataTypes.STRING, type: DataTypes.STRING,
+8 -5
View File
@@ -17,6 +17,7 @@ export class Crontab {
labels?: string[]; labels?: string[];
last_running_time?: number; last_running_time?: number;
last_execution_time?: number; last_execution_time?: number;
sub_id?: number;
constructor(options: Crontab) { constructor(options: Crontab) {
this.name = options.name; this.name = options.name;
@@ -25,7 +26,7 @@ export class Crontab {
this.saved = options.saved; this.saved = options.saved;
this.id = options.id; this.id = options.id;
this.status = this.status =
options.status && CrontabStatus[options.status] typeof options.status === 'number' && CrontabStatus[options.status]
? options.status ? options.status
: CrontabStatus.idle; : CrontabStatus.idle;
this.timestamp = new Date().toString(); this.timestamp = new Date().toString();
@@ -37,17 +38,18 @@ export class Crontab {
this.labels = options.labels || []; this.labels = options.labels || [];
this.last_running_time = options.last_running_time || 0; this.last_running_time = options.last_running_time || 0;
this.last_execution_time = options.last_execution_time || 0; this.last_execution_time = options.last_execution_time || 0;
this.sub_id = options.sub_id;
} }
} }
export enum CrontabStatus { export enum CrontabStatus {
'running', 'running' = 0,
'idle', 'queued' = 0.5,
'idle' = 1,
'disabled', 'disabled',
'queued',
} }
interface CronInstance extends Model<Crontab, Crontab>, Crontab {} export interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
export const CrontabModel = sequelize.define<CronInstance>('Crontab', { export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
name: { name: {
unique: 'compositeIndex', unique: 'compositeIndex',
@@ -72,4 +74,5 @@ export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
labels: DataTypes.JSON, labels: DataTypes.JSON,
last_running_time: DataTypes.NUMBER, last_running_time: DataTypes.NUMBER,
last_execution_time: DataTypes.NUMBER, last_execution_time: DataTypes.NUMBER,
sub_id: { type: DataTypes.NUMBER, allowNull: true },
}); });
+1 -1
View File
@@ -39,7 +39,7 @@ export class CrontabView {
} }
} }
interface CronViewInstance export interface CronViewInstance
extends Model<CrontabView, CrontabView>, extends Model<CrontabView, CrontabView>,
CrontabView {} CrontabView {}
export const CrontabViewModel = sequelize.define<CronViewInstance>( export const CrontabViewModel = sequelize.define<CronViewInstance>(
+25 -9
View File
@@ -4,18 +4,21 @@ import { DataTypes, Model, ModelDefined } from 'sequelize';
export class Dependence { export class Dependence {
timestamp?: string; timestamp?: string;
id?: number; id?: number;
status?: DependenceStatus; status: DependenceStatus;
type?: DependenceTypes; type: DependenceTypes;
name?: number; name: string;
log?: string[]; log?: string[];
remark?: string; remark?: string;
constructor(options: Dependence) { constructor(options: Dependence) {
this.id = options.id; 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.type = options.type || DependenceTypes.nodejs;
this.timestamp = new Date().toString(); this.timestamp = new Date().toString();
this.name = options.name; this.name = options.name.trim();
this.log = options.log || []; this.log = options.log || [];
this.remark = options.remark || ''; this.remark = options.remark || '';
} }
@@ -28,6 +31,7 @@ export enum DependenceStatus {
'removing', 'removing',
'removed', 'removed',
'removeFailed', 'removeFailed',
'queued',
} }
export enum DependenceTypes { export enum DependenceTypes {
@@ -38,19 +42,31 @@ export enum DependenceTypes {
export enum InstallDependenceCommandTypes { export enum InstallDependenceCommandTypes {
'pnpm add -g', 'pnpm add -g',
'pip3 install', 'pip3 install --disable-pip-version-check --root-user-action=ignore',
'apk add', 'apk add',
} }
export enum GetDependenceCommandTypes {
'pnpm ls -g ',
'pip3 show --disable-pip-version-check',
'apk info',
}
export enum versionDependenceCommandTypes {
'@',
'==',
'=',
}
export enum unInstallDependenceCommandTypes { export enum unInstallDependenceCommandTypes {
'pnpm remove -g', 'pnpm remove -g',
'pip3 uninstall -y', 'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y',
'apk del', 'apk del',
} }
interface DependenceInstance export interface DependenceInstance
extends Model<Dependence, Dependence>, extends Model<Dependence, Dependence>,
Dependence {} Dependence { }
export const DependenceModel = sequelize.define<DependenceInstance>( export const DependenceModel = sequelize.define<DependenceInstance>(
'Dependence', 'Dependence',
{ {
+6 -3
View File
@@ -13,7 +13,10 @@ export class Env {
constructor(options: Env) { constructor(options: Env) {
this.value = options.value; this.value = options.value;
this.id = options.id; 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.timestamp = new Date().toString();
this.position = options.position; this.position = options.position;
this.name = options.name; this.name = options.name;
@@ -28,10 +31,10 @@ export enum EnvStatus {
export const maxPosition = 9000000000000000; export const maxPosition = 9000000000000000;
export const initPosition = 4500000000000000; export const initPosition = 4500000000000000;
export const stepPosition = 10000000; export const stepPosition = 10000000000;
export const minPosition = 100; export const minPosition = 100;
interface EnvInstance extends Model<Env, Env>, Env {} export interface EnvInstance extends Model<Env, Env>, Env {}
export const EnvModel = sequelize.define<EnvInstance>('Env', { export const EnvModel = sequelize.define<EnvInstance>('Env', {
value: { type: DataTypes.STRING, unique: 'compositeIndex' }, value: { type: DataTypes.STRING, unique: 'compositeIndex' },
timestamp: DataTypes.STRING, timestamp: DataTypes.STRING,
+2 -1
View File
@@ -1,9 +1,10 @@
import { Sequelize, Transaction } from 'sequelize'; import { Sequelize, Transaction } from 'sequelize';
import config from '../config/index'; import config from '../config/index';
import { join } from 'path';
export const sequelize = new Sequelize({ export const sequelize = new Sequelize({
dialect: 'sqlite', dialect: 'sqlite',
storage: `${config.dbPath}database.sqlite`, storage: join(config.dbPath, 'database.sqlite'),
logging: false, logging: false,
retry: { retry: {
max: 10, max: 10,
+8
View File
@@ -15,6 +15,7 @@ export enum NotificationMode {
'iGot' = 'iGot', 'iGot' = 'iGot',
'pushPlus' = 'pushPlus', 'pushPlus' = 'pushPlus',
'email' = 'email', 'email' = 'email',
'pushMe' = 'pushMe',
'feishu' = 'feishu', 'feishu' = 'feishu',
'webhook' = 'webhook', 'webhook' = 'webhook',
} }
@@ -72,10 +73,12 @@ export class DingtalkBotNotification extends NotificationBaseInfo {
export class WeWorkBotNotification extends NotificationBaseInfo { export class WeWorkBotNotification extends NotificationBaseInfo {
public weWorkBotKey = ''; public weWorkBotKey = '';
public weWorkOrigin = '';
} }
export class WeWorkAppNotification extends NotificationBaseInfo { export class WeWorkAppNotification extends NotificationBaseInfo {
public weWorkAppKey = ''; public weWorkAppKey = '';
public weWorkOrigin = '';
} }
export class AibotkNotification extends NotificationBaseInfo { export class AibotkNotification extends NotificationBaseInfo {
@@ -99,6 +102,10 @@ export class EmailNotification extends NotificationBaseInfo {
public emailPass: string = ''; public emailPass: string = '';
} }
export class PushMeNotification extends NotificationBaseInfo {
public pushMeKey: string = '';
}
export class WebhookNotification extends NotificationBaseInfo { export class WebhookNotification extends NotificationBaseInfo {
public webhookHeaders: string = ''; public webhookHeaders: string = '';
public webhookBody: string = ''; public webhookBody: string = '';
@@ -129,5 +136,6 @@ export interface NotificationInfo
IGotNotification, IGotNotification,
PushPlusNotification, PushPlusNotification,
EmailNotification, EmailNotification,
PushMeNotification,
WebhookNotification, WebhookNotification,
LarkNotification {} LarkNotification {}
+1 -8
View File
@@ -26,14 +26,7 @@ export interface AppToken {
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs'; export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
export enum CrontabStatus { export interface AppInstance extends Model<App, App>, App {}
'running',
'idle',
'disabled',
'queued',
}
interface AppInstance extends Model<App, App>, App {}
export const AppModel = sequelize.define<AppInstance>('App', { export const AppModel = sequelize.define<AppInstance>('App', {
name: { type: DataTypes.STRING, unique: 'name' }, name: { type: DataTypes.STRING, unique: 'name' },
scopes: DataTypes.JSON, scopes: DataTypes.JSON,
+2 -1
View File
@@ -16,4 +16,5 @@ export type SockMessageType =
| 'uninstallDependence' | 'uninstallDependence'
| 'updateSystemVersion' | 'updateSystemVersion'
| 'manuallyRunScript' | 'manuallyRunScript'
| 'runSubscriptionEnd'; | 'runSubscriptionEnd'
| 'reloadSystem';
+9 -3
View File
@@ -29,14 +29,16 @@ export class Subscription {
sub_before?: string; sub_before?: string;
sub_after?: string; sub_after?: string;
proxy?: string; proxy?: string;
autoAddCron?: 1 | 0;
autoDelCron?: 1 | 0;
constructor(options: Subscription) { constructor(options: Subscription) {
this.id = options.id; this.id = options.id;
this.name = options.name || options.alias; this.name = options.name || options.alias;
this.type = options.type; this.type = options.type;
this.schedule = options.schedule; this.schedule = options.schedule;
this.status = this.status = this.status =
options.status && SubscriptionStatus[options.status] typeof options.status === 'number' && SubscriptionStatus[options.status]
? options.status ? options.status
: SubscriptionStatus.idle; : SubscriptionStatus.idle;
this.url = options.url; this.url = options.url;
@@ -56,6 +58,8 @@ export class Subscription {
this.sub_before = options.sub_before; this.sub_before = options.sub_before;
this.sub_after = options.sub_after; this.sub_after = options.sub_after;
this.proxy = options.proxy; this.proxy = options.proxy;
this.autoAddCron = options.autoAddCron ? 1 : 0;
this.autoDelCron = options.autoDelCron ? 1 : 0;
} }
} }
@@ -66,7 +70,7 @@ export enum SubscriptionStatus {
'queued', 'queued',
} }
interface SubscriptionInstance export interface SubscriptionInstance
extends Model<Subscription, Subscription>, extends Model<Subscription, Subscription>,
Subscription {} Subscription {}
export const SubscriptionModel = sequelize.define<SubscriptionInstance>( export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
@@ -105,5 +109,7 @@ export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
schedule_type: DataTypes.STRING, schedule_type: DataTypes.STRING,
alias: { type: DataTypes.STRING, unique: 'alias' }, alias: { type: DataTypes.STRING, unique: 'alias' },
proxy: { type: DataTypes.STRING, allowNull: true }, proxy: { type: DataTypes.STRING, allowNull: true },
autoAddCron: { type: DataTypes.NUMBER, allowNull: true },
autoDelCron: { type: DataTypes.NUMBER, allowNull: true },
}, },
); );
+13
View File
@@ -36,6 +36,19 @@ export default async () => {
try { try {
await sequelize.query('alter table CrontabViews add column type NUMBER'); await sequelize.query('alter table CrontabViews add column type NUMBER');
} catch (error) {} } catch (error) {}
try {
await sequelize.query(
'alter table Subscriptions add column autoAddCron NUMBER',
);
} catch (error) {}
try {
await sequelize.query(
'alter table Subscriptions add column autoDelCron NUMBER',
);
} catch (error) {}
try {
await sequelize.query('alter table Crontabs add column sub_id NUMBER');
} catch (error) {}
// 2.10-2.11 升级 // 2.10-2.11 升级
const cronDbFile = path.join(config.rootPath, 'db/crontab.db'); const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
+13 -25
View File
@@ -10,26 +10,27 @@ import Container from 'typedi';
import OpenService from '../services/open'; import OpenService from '../services/open';
import rewrite from 'express-urlrewrite'; import rewrite from 'express-urlrewrite';
import UserService from '../services/user'; import UserService from '../services/user';
import handler from 'serve-handler';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { EnvModel } from '../data/env'; import { EnvModel } from '../data/env';
import { errors } from 'celebrate'; import { errors } from 'celebrate';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { serveEnv } from '../config/serverEnv';
export default ({ app }: { app: Application }) => { export default ({ app }: { app: Application }) => {
app.enable('trust proxy'); app.enable('trust proxy');
app.use(cors()); app.use(cors());
app.get(`${config.api.prefix}/env.js`, serveEnv);
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath)); app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
app.use((req, res, next) => { app.use(
if (req.path.startsWith('/api') || req.path.startsWith('/open')) { '/api/public',
next(); createProxyMiddleware({
} else { target: `http://localhost:${config.publicPort}/api`,
return handler(req, res, { changeOrigin: true,
public: 'static/dist', pathRewrite: { '/api/public': '' },
rewrites: [{ source: '**', destination: '/index.html' }], }),
}); );
}
});
app.use(bodyParser.json({ limit: '50mb' })); app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
@@ -108,8 +109,7 @@ export default ({ app }: { app: Application }) => {
if ( if (
Object.keys(authInfo).length === 2 && Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' && authInfo.username === 'admin' &&
authInfo.password === 'admin' && authInfo.password === 'admin'
envCount === 0
) { ) {
isInitialized = false; 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( app.use(
( (
err: Error & { status: number }, err: Error & { status: number },
+8 -11
View File
@@ -4,8 +4,7 @@ import { Container } from 'typedi';
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron'; import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import CronService from '../services/cron'; import CronService from '../services/cron';
import EnvService from '../services/env'; import EnvService from '../services/env';
import groupBy from 'lodash/groupBy'; import { DependenceModel, DependenceStatus } from '../data/dependence';
import { DependenceModel } from '../data/dependence';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import config from '../config'; import config from '../config';
import { CrontabViewModel, CronViewType } from '../data/cronView'; import { CrontabViewModel, CronViewType } from '../data/cronView';
@@ -33,7 +32,7 @@ export default async () => {
// 初始化更新所有任务状态为空闲 // 初始化更新所有任务状态为空闲
await CrontabModel.update( await CrontabModel.update(
{ status: CrontabStatus.idle }, { status: CrontabStatus.idle },
{ where: { status: [CrontabStatus.running, CrontabStatus.queued] } }, { where: {} },
); );
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖 // 初始化时安装所有处于安装中,安装成功,安装失败的依赖
@@ -42,16 +41,14 @@ export default async () => {
order: [['type', 'DESC']], order: [['type', 'DESC']],
raw: true, raw: true,
}).then(async (docs) => { }).then(async (docs) => {
const groups = groupBy(docs, 'type'); await DependenceModel.update(
const keys = Object.keys(groups).sort((a, b) => parseInt(b) - parseInt(a)); { status: DependenceStatus.queued, log: [] },
for (const key of keys) { { where: { id: docs.map((x) => x.id!) } },
const group = groups[key]; );
const depIds = group.map((x) => x.id); dependenceService.installDependenceOneByOne(docs);
await dependenceService.reInstall(depIds as number[]);
}
}); });
// 初始化时执行一次所有的ql repo 任务 // 初始化时执行一次所有的 ql repo 任务
CrontabModel.findAll({ CrontabModel.findAll({
where: { where: {
isDisabled: { [Op.ne]: 1 }, 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 uploadPath = path.join(dataPath, 'upload/');
const bakPath = path.join(dataPath, 'bak/'); const bakPath = path.join(dataPath, 'bak/');
const samplePath = path.join(rootPath, 'sample/'); const samplePath = path.join(rootPath, 'sample/');
const tmpPath = path.join(logPath, '.tmp/');
const confFile = path.join(configPath, 'config.sh'); const confFile = path.join(configPath, 'config.sh');
const authConfigFile = path.join(configPath, 'auth.json'); const authConfigFile = path.join(configPath, 'auth.json');
const sampleConfigFile = path.join(samplePath, 'config.sample.sh'); const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
const sampleAuthFile = path.join(samplePath, 'auth.sample.json'); 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 homedir = os.homedir();
const sshPath = path.resolve(homedir, '.ssh'); const sshPath = path.resolve(homedir, '.ssh');
const sshdPath = path.join(dataPath, 'ssh.d');
export default async () => { export default async () => {
const authFileExist = await fileExist(authConfigFile); const authFileExist = await fileExist(authConfigFile);
@@ -29,19 +38,17 @@ export default async () => {
const uploadDirExist = await fileExist(uploadPath); const uploadDirExist = await fileExist(uploadPath);
const sshDirExist = await fileExist(sshPath); const sshDirExist = await fileExist(sshPath);
const bakDirExist = await fileExist(bakPath); 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) { if (!configDirExist) {
fs.mkdirSync(configPath); fs.mkdirSync(configPath);
} }
if (!authFileExist) {
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
}
if (!confFileExist) {
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
}
if (!scriptDirExist) { if (!scriptDirExist) {
fs.mkdirSync(scriptPath); fs.mkdirSync(scriptPath);
} }
@@ -50,6 +57,10 @@ export default async () => {
fs.mkdirSync(logPath); fs.mkdirSync(logPath);
} }
if (!tmpDirExist) {
fs.mkdirSync(tmpPath);
}
if (!uploadDirExist) { if (!uploadDirExist) {
fs.mkdirSync(uploadPath); fs.mkdirSync(uploadPath);
} }
@@ -62,6 +73,35 @@ export default async () => {
fs.mkdirSync(bakPath); 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 }); dotenv.config({ path: confFile });
Logger.info('✌️ Init file down'); Logger.info('✌️ Init file down');
+15 -15
View File
@@ -4,6 +4,7 @@ import ScheduleService from '../services/schedule';
import SubscriptionService from '../services/subscription'; import SubscriptionService from '../services/subscription';
import config from '../config'; import config from '../config';
import { fileExist } from '../config/util'; import { fileExist } from '../config/util';
import { join } from 'path';
export default async () => { export default async () => {
const systemService = Container.get(SystemService); const systemService = Container.get(SystemService);
@@ -11,8 +12,9 @@ export default async () => {
const subscriptionService = Container.get(SubscriptionService); const subscriptionService = Container.get(SubscriptionService);
// 生成内置token // 生成内置token
let tokenCommand = `ts-node-transpile-only ${config.rootPath}/back/token.ts`; let tokenCommand = `tsx ${join(config.rootPath, 'back/token.ts')}`;
const tokenFile = `${config.rootPath}static/build/token.js`; const tokenFile = join(config.rootPath, 'static/build/token.js');
if (await fileExist(tokenFile)) { if (await fileExist(tokenFile)) {
tokenCommand = `node ${tokenFile}`; tokenCommand = `node ${tokenFile}`;
} }
@@ -21,31 +23,29 @@ export default async () => {
name: '生成token', name: '生成token',
command: tokenCommand, command: tokenCommand,
}; };
await scheduleService.cancelIntervalTask(cron);
scheduleService.createIntervalTask(cron, { scheduleService.createIntervalTask(cron, {
days: 28, days: 28,
}); });
// 运行删除日志任务 // 运行删除日志任务
const data = await systemService.getLogRemoveFrequency(); const data = await systemService.getSystemConfig();
if (data && data.info && data.info.frequency) { if (data && data.info && data.info.logRemoveFrequency) {
const cron = { const rmlogCron = {
id: data.id, id: data.id as number,
name: '删除日志', name: '删除日志',
command: `ql rmlog ${data.info.frequency}`, command: `ql rmlog ${data.info.logRemoveFrequency}`,
}; };
scheduleService.createIntervalTask(cron, { await scheduleService.cancelIntervalTask(rmlogCron);
days: data.info.frequency, scheduleService.createIntervalTask(rmlogCron, {
days: data.info.logRemoveFrequency,
}); });
} }
// 运行所有订阅 // 运行所有订阅
await subscriptionService.setSshConfig();
const subs = await subscriptionService.list(); const subs = await subscriptionService.list();
for (const sub of subs) { for (const sub of subs) {
await subscriptionService.handleTask( subscriptionService.handleTask(sub, !sub.is_disabled, !sub.is_disabled);
sub,
!sub.is_disabled,
true,
!sub.is_disabled,
);
} }
}; };
+11 -7
View File
@@ -4,21 +4,25 @@ import * as Tracing from '@sentry/tracing';
import Logger from './logger'; import Logger from './logger';
import config from '../config'; import config from '../config';
import fs from 'fs'; import fs from 'fs';
import { parseVersion } from '../config/util';
export default ({ expressApp }: { expressApp: Application }) => { export default async ({ expressApp }: { expressApp: Application }) => {
const versionRegx = /.*export const version = \'(.*)\'\;/; const { version } = await parseVersion(config.versionFile);
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
Sentry.init({ Sentry.init({
ignoreErrors: [
/SequelizeUniqueConstraintError/i,
/Validation error/i,
/UnauthorizedError/i,
/celebrate request validation failed/i,
],
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819', dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
integrations: [ integrations: [
new Sentry.Integrations.Http({ tracing: true }), new Sentry.Integrations.Http({ tracing: true }),
new Tracing.Integrations.Express({ app: expressApp }), new Tracing.Integrations.Express({ app: expressApp }),
], ],
tracesSampleRate: 0.1, tracesSampleRate: 0.8,
release: currentVersion, release: version,
}); });
expressApp.use(Sentry.Handlers.requestHandler()); expressApp.use(Sentry.Handlers.requestHandler());
+26 -4
View File
@@ -5,12 +5,34 @@ import Sock from './sock';
export default async ({ server }: { server: Server }) => { export default async ({ server }: { server: Server }) => {
await Sock({ server }); await Sock({ server });
Logger.info('✌️ Sock loaded'); Logger.info('✌️ Sock loaded');
let exitTime = 0;
let timer: NodeJS.Timeout;
process.on('SIGINT', () => { process.on('SIGINT', (singal) => {
Logger.info('✌️ Server need close'); Logger.warn(`Server need close, singal ${singal}`);
exitTime++;
if (exitTime >= 3) {
Logger.warn('Forcing server close');
clearTimeout(timer);
process.exit(1);
}
server.close(() => { server.close(() => {
Logger.info('✌️ Server closed'); if (timer) {
process.exit(0); 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;
}
+14 -11
View File
@@ -1,20 +1,22 @@
import express from 'express'; import express from 'express';
import { exec } from 'child_process';
import Logger from './loaders/logger'; import Logger from './loaders/logger';
import config from './config'; import config from './config';
import { HealthClient } from './protos/health';
import { credentials } from '@grpc/grpc-js';
const app = express(); const app = express();
const client = new HealthClient(
`localhost:${config.cronPort}`,
credentials.createInsecure(),
);
app.get('/api/public/panel/log', (req, res) => { app.get('/api/health', (req, res) => {
exec( client.check({ service: 'cron' }, (err, response) => {
'pm2 logs panel --lines 500 --nostream --timestamp', if (err) {
(err, stdout, stderr) => { return res.status(500).send({ code: 500, error: err });
if (err || stderr) { }
return res.send({ code: 400, message: (err && err.message) || stderr }); return res.status(200).send({ code: 200, data: response });
} });
return res.send({ code: 200, data: stdout });
},
);
}); });
app app
@@ -23,6 +25,7 @@ app
await require('./loaders/db').default(); await require('./loaders/db').default();
Logger.debug(`✌️ 公共服务启动成功!`); Logger.debug(`✌️ 公共服务启动成功!`);
process.send?.('ready');
}) })
.on('error', (err) => { .on('error', (err) => {
Logger.error(err); Logger.error(err);
-52
View File
@@ -1,52 +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 &&
task.schedule
) {
schedule.scheduleJob(task.schedule, function () {
let command = task.command as string;
if (!command.includes('task ') && !command.includes('ql ')) {
command = `task ${command}`;
}
exec(`ID=${task.id} ${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.debug('定时任务服务启动成功!');
})
.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');
},
);
+159 -158
View File
@@ -2,22 +2,25 @@ import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import config from '../config'; import config from '../config';
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron'; 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 fs from 'fs';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import { import {
getFileContentByName, getFileContentByName,
concurrentRun,
fileExist, fileExist,
killTask, killTask,
} from '../config/util'; } from '../config/util';
import { promises, existsSync } from 'fs'; import { promises, existsSync } from 'fs';
import { Op, where, col as colFn } from 'sequelize'; import { Op, where, col as colFn, FindOptions, fn } from 'sequelize';
import path from 'path'; import path from 'path';
import { TASK_PREFIX, QL_PREFIX } from '../config/const';
import cronClient from '../schedule/client';
import taskLimit from '../shared/pLimit';
import { spawn } from 'cross-spawn';
@Service() @Service()
export default class CronService { export default class CronService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) { }
private isSixCron(cron: Crontab) { private isSixCron(cron: Crontab) {
const { schedule } = cron; const { schedule } = cron;
@@ -31,7 +34,12 @@ export default class CronService {
const tab = new Crontab(payload); const tab = new Crontab(payload);
tab.saved = false; tab.saved = false;
const doc = await this.insert(tab); const doc = await this.insert(tab);
await this.set_crontab(this.isSixCron(doc)); if (this.isSixCron(doc)) {
await cronClient.addCron([
{ id: String(doc.id), schedule: doc.schedule!, command: doc.command },
]);
}
await this.set_crontab();
return doc; return doc;
} }
@@ -40,9 +48,26 @@ export default class CronService {
} }
public async update(payload: Crontab): Promise<Crontab> { public async update(payload: Crontab): Promise<Crontab> {
payload.saved = false; const doc = await this.getDb({ id: payload.id });
const newDoc = await this.updateDb(payload); const tab = new Crontab({ ...doc, ...payload });
await this.set_crontab(this.isSixCron(newDoc)); 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; return newDoc;
} }
@@ -81,7 +106,8 @@ export default class CronService {
public async remove(ids: number[]) { public async remove(ids: number[]) {
await CrontabModel.destroy({ where: { id: ids } }); await CrontabModel.destroy({ where: { id: ids } });
await this.set_crontab(true); await cronClient.delCron(ids.map(String));
await this.set_crontab();
} }
public async pin(ids: number[]) { public async pin(ids: number[]) {
@@ -167,17 +193,13 @@ export default class CronService {
{ {
[operate2]: [ [operate2]: [
{ [operate]: `%${value}%` }, { [operate]: `%${value}%` },
{ [operate]: `%${encodeURIComponent(value)}%` }, { [operate]: `%${encodeURI(value)}%` },
], ],
}, },
{ {
[operate2]: [ [operate2]: [
where(colFn(property), operate, `%${value}%`), where(colFn(property), operate, `%${value}%`),
where( where(colFn(property), operate, `%${encodeURI(value)}%`),
colFn(property),
operate,
`%${encodeURIComponent(value)}%`,
),
], ],
}, },
], ],
@@ -204,7 +226,7 @@ export default class CronService {
q[column] = { q[column] = {
[Op.or]: [ [Op.or]: [
{ [Op.like]: `%${textArray[1]}%` }, { [Op.like]: `%${textArray[1]}%` },
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` }, { [Op.like]: `%${encodeURI(textArray[1])}%` },
], ],
}; };
break; break;
@@ -212,7 +234,7 @@ export default class CronService {
const reg = { const reg = {
[Op.or]: [ [Op.or]: [
{ [Op.like]: `%${searchText}%` }, { [Op.like]: `%${searchText}%` },
{ [Op.like]: `%${encodeURIComponent(searchText)}%` }, { [Op.like]: `%${encodeURI(searchText)}%` },
], ],
}; };
q[Op.or] = [ q[Op.or] = [
@@ -243,8 +265,15 @@ export default class CronService {
const filterKeys: any = Object.keys(filterQuery); const filterKeys: any = Object.keys(filterQuery);
for (const key of filterKeys) { for (const key of filterKeys) {
let q: any = {}; let q: any = {};
if (filterKeys[key]) { if (!filterQuery[key]) continue;
q[key] = filterKeys[key]; if (key === 'status') {
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); query[Op.and].push(q);
} }
@@ -259,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?: { public async crontabs(params?: {
searchValue: string; searchValue: string;
page: string; page: string;
@@ -310,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 } }); const doc: any = await CrontabModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Crontab); return doc && (doc.get({ plain: true }) as Crontab);
} }
@@ -320,10 +362,9 @@ export default class CronService {
{ status: CrontabStatus.queued }, { status: CrontabStatus.queued },
{ where: { id: ids } }, { where: { id: ids } },
); );
concurrentRun( ids.forEach((id) => {
ids.map((id) => async () => await this.runSingle(id)), this.runSingle(id);
10, });
);
} }
public async stop(ids: number[]) { public async stop(ids: number[]) {
@@ -345,75 +386,87 @@ export default class CronService {
} }
private async runSingle(cronId: number): Promise<number> { private async runSingle(cronId: number): Promise<number> {
return new Promise(async (resolve: any) => { return taskLimit.runWithCpuLimit(() => {
const cron = await this.getDb({ id: cronId }); return new Promise(async (resolve: any) => {
if (cron.status !== CrontabStatus.queued) { const cron = await this.getDb({ id: cronId });
resolve(); if (cron.status !== CrontabStatus.queued) {
return; 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(`ID=${id} ${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.toString()}`);
} }
});
cp.on('error', (err) => {
if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
}
});
cp.on('exit', async (code, signal) => { let { id, command, log_path } = cron;
this.logger.info( const absolutePath = path.resolve(config.logPath, `${log_path}`);
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`, const logFileExist = log_path && (await fileExist(absolutePath));
);
}); this.logger.silly('Running job');
cp.on('close', async (code) => { 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( await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined }, { status: CrontabStatus.running, pid: cp.pid },
{ where: { id } }, { 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[]) { public async disabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } }); await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
await this.set_crontab(true); await cronClient.delCron(ids.map(String));
await this.set_crontab();
} }
public async enabled(ids: number[]) { public async enabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } }); await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
await this.set_crontab(true); 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) { public async log(id: number) {
@@ -426,69 +479,26 @@ export default class CronService {
const logFileExist = doc.log_path && (await fileExist(absolutePath)); const logFileExist = doc.log_path && (await fileExist(absolutePath));
if (logFileExist) { if (logFileExist) {
return getFileContentByName(`${absolutePath}`); return getFileContentByName(`${absolutePath}`);
}
const [, commandStr, url] = doc.command.split(/ +/);
let logPath = this.getKey(commandStr);
const isQlCommand = doc.command.startsWith('ql ');
const key =
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
logPath;
if (isQlCommand) {
logPath = 'update';
}
let logDir = `${config.logPath}${logPath}`;
if (existsSync(logDir)) {
let files = await promises.readdir(logDir);
if (isQlCommand) {
files = files.filter((x) => x.includes(key));
}
return getFileContentByName(`${logDir}/${files[files.length - 1]}`);
} else { } else {
return ''; return '任务未运行';
} }
} }
public async logs(id: number) { public async logs(id: number) {
const doc = await this.getDb({ id }); const doc = await this.getDb({ id });
if (!doc) { if (!doc || !doc.log_path) {
return []; return [];
} }
if (doc.log_path) { const relativeDir = path.dirname(`${doc.log_path}`);
const relativeDir = path.dirname(`${doc.log_path}`); const dir = path.resolve(config.logPath, relativeDir);
const dir = path.resolve(config.logPath, relativeDir); if (existsSync(dir)) {
if (existsSync(dir)) { let files = await promises.readdir(dir);
let files = await promises.readdir(dir);
return files
.map((x) => ({
filename: x,
directory: relativeDir.replace(config.logPath, ''),
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time);
}
}
const [, commandStr, url] = doc.command.split(/ +/);
let logPath = this.getKey(commandStr);
const isQlCommand = doc.command.startsWith('ql ');
const key =
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
logPath;
if (isQlCommand) {
logPath = 'update';
}
let logDir = `${config.logPath}${logPath}`;
if (existsSync(logDir)) {
let files = await promises.readdir(logDir);
if (isQlCommand) {
files = files.filter((x) => x.includes(key));
}
return files return files
.map((x) => ({ .map((x) => ({
filename: x, filename: x,
directory: logPath, directory: relativeDir.replace(config.logPath, ''),
time: fs.statSync(`${logDir}/${x}`).mtime.getTime(), time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
})) }))
.sort((a, b) => b.time - a.time); .sort((a, b) => b.time - a.time);
} else { } else {
@@ -496,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) { private make_command(tab: Crontab) {
if (
!tab.command.startsWith(TASK_PREFIX) &&
!tab.command.startsWith(QL_PREFIX)
) {
tab.command = `${TASK_PREFIX}${tab.command}`;
}
const crontab_job_string = `ID=${tab.id} ${tab.command}`; const crontab_job_string = `ID=${tab.id} ${tab.command}`;
return crontab_job_string; return crontab_job_string;
} }
private async set_crontab(needReloadSchedule: boolean = false) { private async set_crontab(data?: { data: Crontab[]; total: number }) {
const tabs = await this.crontabs(); const tabs = data ?? (await this.crontabs());
var crontab_string = ''; var crontab_string = '';
tabs.data.forEach((tab) => { tabs.data.forEach((tab) => {
const _schedule = tab.schedule && tab.schedule.split(/ +/); const _schedule = tab.schedule && tab.schedule.split(/ +/);
@@ -546,9 +540,6 @@ export default class CronService {
fs.writeFileSync(config.crontabFile, crontab_string); fs.writeFileSync(config.crontabFile, crontab_string);
execSync(`crontab ${config.crontabFile}`); execSync(`crontab ${config.crontabFile}`);
if (needReloadSchedule) {
exec(`pm2 reload schedule`);
}
await CrontabModel.update({ saved: true }, { where: {} }); await CrontabModel.update({ saved: true }, { where: {} });
} }
@@ -586,7 +577,17 @@ export default class CronService {
}); });
} }
public autosave_crontab() { public async autosave_crontab() {
return this.set_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);
} }
} }
+5 -2
View File
@@ -7,6 +7,7 @@ import {
minPosition, minPosition,
stepPosition, stepPosition,
} from '../data/env'; } from '../data/env';
import { FindOptions } from 'sequelize';
@Service() @Service()
export default class CronViewService { export default class CronViewService {
@@ -31,7 +32,9 @@ export default class CronViewService {
} }
public async update(payload: CrontabView): Promise<CrontabView> { public async update(payload: CrontabView): Promise<CrontabView> {
const newDoc = await this.updateDb(payload); const doc = await this.getDb({ id: payload.id })
const tab = new CrontabView({ ...doc, ...payload });
const newDoc = await this.updateDb(tab);
return newDoc; return newDoc;
} }
@@ -56,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 } }); const doc: any = await CrontabViewModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as CrontabView); return doc && (doc.get({ plain: true }) as CrontabView);
} }
+158 -102
View File
@@ -8,23 +8,26 @@ import {
DependenceTypes, DependenceTypes,
unInstallDependenceCommandTypes, unInstallDependenceCommandTypes,
DependenceModel, DependenceModel,
GetDependenceCommandTypes,
versionDependenceCommandTypes,
} from '../data/dependence'; } from '../data/dependence';
import { spawn } from 'child_process'; import { spawn } from 'cross-spawn';
import SockService from './sock'; import SockService from './sock';
import { Op } from 'sequelize'; import { FindOptions, Op } from 'sequelize';
import { concurrentRun } from '../config/util'; import { promiseExecSuccess } from '../config/util';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import taskLimit from '../shared/pLimit';
@Service() @Service()
export default class DependenceService { export default class DependenceService {
constructor( constructor(
@Inject('logger') private logger: winston.Logger, @Inject('logger') private logger: winston.Logger,
private sockService: SockService, private sockService: SockService,
) {} ) { }
public async create(payloads: Dependence[]): Promise<Dependence[]> { public async create(payloads: Dependence[]): Promise<Dependence[]> {
const tabs = payloads.map((x) => { const tabs = payloads.map((x) => {
const tab = new Dependence({ ...x, status: DependenceStatus.installing }); const tab = new Dependence({ ...x, status: DependenceStatus.queued });
return tab; return tab;
}); });
const docs = await this.insert(tabs); const docs = await this.insert(tabs);
@@ -45,7 +48,7 @@ export default class DependenceService {
const tab = new Dependence({ const tab = new Dependence({
...doc, ...doc,
...other, ...other,
status: DependenceStatus.installing, status: DependenceStatus.queued,
}); });
const newDoc = await this.updateDb(tab); const newDoc = await this.updateDb(tab);
this.installDependenceOneByOne([newDoc]); this.installDependenceOneByOne([newDoc]);
@@ -59,7 +62,7 @@ export default class DependenceService {
public async remove(ids: number[], force = false): Promise<Dependence[]> { public async remove(ids: number[], force = false): Promise<Dependence[]> {
await DependenceModel.update( await DependenceModel.update(
{ status: DependenceStatus.removing, log: [] }, { status: DependenceStatus.queued, log: [] },
{ where: { id: ids } }, { where: { id: ids } },
); );
const docs = await DependenceModel.findAll({ where: { id: ids } }); const docs = await DependenceModel.findAll({ where: { id: ids } });
@@ -78,7 +81,7 @@ export default class DependenceService {
): Promise<Dependence[]> { ): Promise<Dependence[]> {
let condition = { ...query, type: DependenceTypes[type as any] }; let condition = { ...query, type: DependenceTypes[type as any] };
if (searchValue) { if (searchValue) {
const encodeText = encodeURIComponent(searchValue); const encodeText = encodeURI(searchValue);
const reg = { const reg = {
[Op.or]: [ [Op.or]: [
{ [Op.like]: `%${searchValue}%` }, { [Op.like]: `%${searchValue}%` },
@@ -99,23 +102,19 @@ export default class DependenceService {
} }
} }
private installDependenceOneByOne( public installDependenceOneByOne(
docs: Dependence[], docs: Dependence[],
isInstall: boolean = true, isInstall: boolean = true,
force: boolean = false, force: boolean = false,
) { ) {
concurrentRun( docs.forEach((dep) => {
docs.map( this.installOrUninstallDependency(dep, isInstall, force);
(dep) => async () => });
await this.installOrUninstallDependencies([dep], isInstall, force),
),
1,
);
} }
public async reInstall(ids: number[]): Promise<Dependence[]> { public async reInstall(ids: number[]): Promise<Dependence[]> {
await DependenceModel.update( await DependenceModel.update(
{ status: DependenceStatus.installing, log: [] }, { status: DependenceStatus.queued, log: [] },
{ where: { id: ids } }, { where: { id: ids } },
); );
@@ -132,113 +131,170 @@ export default class DependenceService {
return docs; 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 } }); const doc: any = await DependenceModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Dependence); return doc && (doc.get({ plain: true }) as Dependence);
} }
private async updateLog(ids: number[], log: string): Promise<void> { private async updateLog(ids: number[], log: string): Promise<void> {
const doc = await DependenceModel.findOne({ where: { id: ids } }); taskLimit.updateDepLog(async () => {
const newLog = doc?.log ? [...doc.log, log] : [log]; const docs = await DependenceModel.findAll({ where: { id: ids } });
await DependenceModel.update({ log: newLog }, { 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( public installOrUninstallDependency(
dependencies: Dependence[], dependency: Dependence,
isInstall: boolean = true, isInstall: boolean = true,
force: boolean = false, force: boolean = false,
) { ) {
return new Promise(async (resolve) => { return taskLimit.runOneByOne(() => {
if (dependencies.length === 0) { return new Promise(async (resolve) => {
resolve(null); const depIds = [dependency.id!];
return; const status = isInstall
} ? DependenceStatus.installing
const socketMessageType = !force : DependenceStatus.removing;
? 'installDependence' await DependenceModel.update({ status }, { where: { id: depIds } });
: '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();
const message = `开始${actionText}依赖 ${depNames},开始时间 ${startTime.format( const socketMessageType = isInstall
'YYYY-MM-DD HH:mm:ss', ? 'installDependence'
)}\n\n`; : 'uninstallDependence';
this.sockService.sendMessage({ const depName = dependency.name.trim();
type: socketMessageType, const depRunCommand = (
message, isInstall
references: depIds, ? InstallDependenceCommandTypes
}); : unInstallDependenceCommandTypes
await this.updateLog(depIds, message); )[dependency.type];
const actionText = isInstall ? '安装' : '删除';
const startTime = dayjs();
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' }); const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format(
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(
'YYYY-MM-DD HH:mm:ss', 'YYYY-MM-DD HH:mm:ss',
)} ${endTime.diff(startTime, 'second')} `; )}\n\n`;
this.sockService.sendMessage({ this.sockService.sendMessage({
type: socketMessageType, type: socketMessageType,
message, message,
references: depIds, references: depIds,
}); });
await this.updateLog(depIds, message); this.updateLog(depIds, message);
let status = null; // 判断是否已经安装过依赖
if (isSucceed) { if (isInstall) {
status = isInstall const getCommandPrefix = GetDependenceCommandTypes[dependency.type];
? DependenceStatus.installed const depVersionStr = versionDependenceCommandTypes[dependency.type];
: DependenceStatus.removed; const [_depName, _depVersion] = dependency.name
} else { .trim()
status = isInstall .split(depVersionStr);
? DependenceStatus.installFailed const isNodeDependence = dependency.type === DependenceTypes.nodejs;
: DependenceStatus.removeFailed; const isLinuxDependence = dependency.type === DependenceTypes.linux;
} const isPythonDependence = dependency.type === DependenceTypes.python3;
await DependenceModel.update({ status }, { where: { id: depIds } }); const depInfo = (
await promiseExecSuccess(
isNodeDependence
? `${getCommandPrefix} | grep "${_depName}" | head -1`
: `${getCommandPrefix} ${_depName}`,
)
).replace(/\s{2,}/, ' ');
// 如果删除依赖成功或者强制删除 if (
if ((isSucceed || force) && !isInstall) { depInfo &&
this.removeDb(depIds); ((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);
});
}); });
}); });
} }
+13 -8
View File
@@ -12,7 +12,7 @@ import {
stepPosition, stepPosition,
} from '../data/env'; } from '../data/env';
import groupBy from 'lodash/groupBy'; import groupBy from 'lodash/groupBy';
import { Op } from 'sequelize'; import { FindOptions, Op } from 'sequelize';
@Service() @Service()
export default class EnvService { export default class EnvService {
@@ -49,7 +49,9 @@ export default class EnvService {
} }
public async update(payload: Env): Promise<Env> { public async update(payload: Env): Promise<Env> {
const newDoc = await this.updateDb(payload); const doc = await this.getDb({ id: payload.id });
const tab = new Env({ ...doc, ...payload });
const newDoc = await this.updateDb(tab);
await this.set_envs(); await this.set_envs();
return newDoc; return newDoc;
} }
@@ -92,13 +94,17 @@ export default class EnvService {
position: this.getPrecisionPosition(targetPosition), position: this.getPrecisionPosition(targetPosition),
}); });
await this.checkPosition(targetPosition); await this.checkPosition(targetPosition, envs[toIndex].position!);
return newDoc; return newDoc;
} }
private async checkPosition(position: number) { private async checkPosition(position: number, edge: number = 0) {
const precisionPosition = parseFloat(position.toPrecision(16)); const precisionPosition = parseFloat(position.toPrecision(16));
if (precisionPosition < minPosition || precisionPosition > maxPosition) { if (
precisionPosition < minPosition ||
precisionPosition > maxPosition ||
Math.abs(precisionPosition - edge) < minPosition
) {
const envs = await this.envs(); const envs = await this.envs();
let position = initPosition; let position = initPosition;
for (const env of envs) { for (const env of envs) {
@@ -115,7 +121,7 @@ export default class EnvService {
public async envs(searchText: string = '', query: any = {}): Promise<Env[]> { public async envs(searchText: string = '', query: any = {}): Promise<Env[]> {
let condition = { ...query }; let condition = { ...query };
if (searchText) { if (searchText) {
const encodeText = encodeURIComponent(searchText); const encodeText = encodeURI(searchText);
const reg = { const reg = {
[Op.or]: [ [Op.or]: [
{ [Op.like]: `%${searchText}%` }, { [Op.like]: `%${searchText}%` },
@@ -140,7 +146,6 @@ export default class EnvService {
} }
try { try {
const result = await this.find(condition, [ const result = await this.find(condition, [
['status', 'ASC'],
['position', 'DESC'], ['position', 'DESC'],
['createdAt', 'ASC'], ['createdAt', 'ASC'],
]); ]);
@@ -158,7 +163,7 @@ export default class EnvService {
return docs; 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 } }); const doc: any = await EnvModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Env); return doc && (doc.get({ plain: true }) as Env);
} }
+331 -173
View File
@@ -28,6 +28,7 @@ export default class NotificationService {
['iGot', this.iGot], ['iGot', this.iGot],
['pushPlus', this.pushPlus], ['pushPlus', this.pushPlus],
['email', this.email], ['email', this.email],
['pushMe', this.pushMe],
['webhook', this.webhook], ['webhook', this.webhook],
['lark', this.lark], ['lark', this.lark],
]); ]);
@@ -36,7 +37,7 @@ export default class NotificationService {
private content = ''; private content = '';
private params!: Omit<NotificationInfo, 'type'>; private params!: Omit<NotificationInfo, 'type'>;
private gotOption = { private gotOption = {
timeout: 30000, timeout: 10000,
retry: 1, retry: 1,
}; };
@@ -78,33 +79,49 @@ export default class NotificationService {
} }
private async gotify() { private async gotify() {
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params; const { gotifyUrl, gotifyToken, gotifyPriority = 1 } = this.params;
const res: any = await got try {
.post(`${gotifyUrl}/message?token=${gotifyToken}`, { const res: any = await got
...this.gotOption, .post(`${gotifyUrl}/message?token=${gotifyToken}`, {
body: `title=${encodeURIComponent( ...this.gotOption,
this.title, body: `title=${encodeURIComponent(
)}&message=${encodeURIComponent( this.title,
this.content, )}&message=${encodeURIComponent(
)}&priority=${gotifyPriority}`, this.content,
headers: { )}&priority=${gotifyPriority}`,
'Content-Type': 'application/x-www-form-urlencoded', headers: {
}, 'Content-Type': 'application/x-www-form-urlencoded',
}) },
.json(); })
return typeof res.id === 'number'; .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() { private async goCqHttpBot() {
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params; const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
const res: any = await got try {
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, { const res: any = await got
...this.gotOption, .post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
json: { message: `${this.title}\n${this.content}` }, ...this.gotOption,
headers: { Authorization: 'Bearer ' + goCqHttpBotToken }, json: { message: `${this.title}\n${this.content}` },
}) headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
.json(); })
return res.retcode === 0; .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() { private async serverChan() {
@@ -112,63 +129,97 @@ export default class NotificationService {
const url = serverChanKey.startsWith('SCT') const url = serverChanKey.startsWith('SCT')
? `https://sctapi.ftqq.com/${serverChanKey}.send` ? `https://sctapi.ftqq.com/${serverChanKey}.send`
: `https://sc.ftqq.com/${serverChanKey}.send`; : `https://sc.ftqq.com/${serverChanKey}.send`;
const res: any = await got try {
.post(url, { const res: any = await got
...this.gotOption, .post(url, {
body: `title=${this.title}&desp=${this.content}`, ...this.gotOption,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `title=${this.title}&desp=${this.content}`,
}) headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
.json(); })
return res.errno === 0 || res.data.errno === 0; .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() { private async pushDeer() {
const { pushDeerKey, pushDeerUrl } = this.params; const { pushDeerKey, pushDeerUrl } = this.params;
const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`; const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`;
const res: any = await got try {
.post(url, { const res: any = await got
...this.gotOption, .post(url, {
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent( ...this.gotOption,
this.title, body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
)}&desp=${encodeURIComponent(this.content)}&type=markdown`, this.title,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, )}&desp=${encodeURIComponent(this.content)}&type=markdown`,
}) headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
.json(); })
return ( .json();
res.content.result.length !== undefined && res.content.result.length > 0 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() { private async chat() {
const { chatUrl, chatToken } = this.params; const { chatUrl, chatToken } = this.params;
const url = `${chatUrl}${chatToken}`; const url = `${chatUrl}${chatToken}`;
const res: any = await got try {
.post(url, { const res: any = await got
...this.gotOption, .post(url, {
body: `payload={"text":"${this.title}\n${this.content}"}`, ...this.gotOption,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `payload={"text":"${this.title}\n${this.content}"}`,
}) headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
.json(); })
return res.success; .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() { private async bark() {
let { barkPush, barkIcon, barkSound, barkGroup } = this.params; let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) { if (!barkPush.startsWith('http')) {
barkPush = `https://api.day.app/${barkPush}`; barkPush = `https://api.day.app/${barkPush}`;
} }
const url = `${barkPush}/${encodeURIComponent( const url = `${barkPush}/${encodeURIComponent(
this.title, this.title,
)}/${encodeURIComponent( )}/${encodeURIComponent(
this.content, this.content,
)}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`; )}?icon=${barkIcon}&sound=${barkSound}&group=${barkGroup}`;
const res: any = await got
.get(url, { try {
...this.gotOption, const res: any = await got
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, .get(url, {
}) ...this.gotOption,
.json(); headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
return res.code === 200; })
.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() { private async telegramBot() {
@@ -200,15 +251,23 @@ export default class NotificationService {
https: httpsAgent, https: httpsAgent,
}; };
} }
const res: any = await got try {
.post(url, { const res: any = await got
...this.gotOption, .post(url, {
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`, ...this.gotOption,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
agent, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}) agent,
.json(); })
return !!res.ok; .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() { private async dingtalkBot() {
@@ -222,42 +281,58 @@ export default class NotificationService {
secretParam = `&timestamp=${dateNow}&sign=${result}`; secretParam = `&timestamp=${dateNow}&sign=${result}`;
} }
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`; const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
const res: any = await got try {
.post(url, { const res: any = await got
...this.gotOption, .post(url, {
json: { ...this.gotOption,
msgtype: 'text', json: {
text: { msgtype: 'text',
content: ` ${this.title}\n\n${this.content}`, text: {
content: ` ${this.title}\n\n${this.content}`,
},
}, },
}, })
}) .json();
.json(); if (res.errcode === 0) {
return 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() { private async weWorkBot() {
const { weWorkBotKey } = this.params; const { weWorkBotKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } = this.params;
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`; const url = `${weWorkOrigin}/cgi-bin/webhook/send?key=${weWorkBotKey}`;
const res: any = await got try {
.post(url, { const res: any = await got
...this.gotOption, .post(url, {
json: { ...this.gotOption,
msgtype: 'text', json: {
text: { msgtype: 'text',
content: ` ${this.title}\n\n${this.content}`, text: {
content: ` ${this.title}\n\n${this.content}`,
},
}, },
}, })
}) .json();
.json(); if (res.errcode === 0) {
return 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() { 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'] = const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
weWorkAppKey.split(','); weWorkAppKey.split(',');
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`; const url = `${weWorkOrigin}/cgi-bin/gettoken`;
const tokenRes: any = await got const tokenRes: any = await got
.post(url, { .post(url, {
...this.gotOption, ...this.gotOption,
@@ -306,22 +381,30 @@ export default class NotificationService {
break; break;
} }
const res: any = await got try {
.post( const res: any = await got
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${tokenRes.access_token}`, .post(
{ `${weWorkOrigin}/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
...this.gotOption, {
json: { ...this.gotOption,
touser, json: {
agentid, touser,
safe: '0', agentid,
...options, 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() { private async aibotk() {
@@ -353,85 +436,152 @@ export default class NotificationService {
break; break;
} }
const res: any = await got try {
.post(url, { const res: any = await got
...this.gotOption, .post(url, {
json: { ...this.gotOption,
...json, json: {
}, ...json,
}) },
.json(); })
.json();
return res.code === 0; 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() { private async iGot() {
const { iGotPushKey } = this.params; const { iGotPushKey } = this.params;
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`; const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
const res: any = await got try {
.post(url, { const res: any = await got
...this.gotOption, .post(url, {
body: `title=${this.title}&content=${this.content}`, ...this.gotOption,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `title=${this.title}&content=${this.content}`,
}) headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
.json(); })
.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() { private async pushPlus() {
const { pushPlusToken, pushPlusUser } = this.params; const { pushPlusToken, pushPlusUser } = this.params;
const url = `https://www.pushplus.plus/send`; const url = `https://www.pushplus.plus/send`;
const res: any = await got try {
.post(url, { const res: any = await got
...this.gotOption, .post(url, {
json: { ...this.gotOption,
token: `${pushPlusToken}`, json: {
title: `${this.title}`, token: `${pushPlusToken}`,
content: `${this.content.replace(/[\n\r]/g, '<br>')}`, title: `${this.title}`,
topic: `${pushPlusUser || ''}`, content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
}, topic: `${pushPlusUser || ''}`,
}) },
.json(); })
.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() { private async lark() {
const { larkKey } = this.params; let { larkKey } = this.params;
const res: any = await got
.post(`https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`, { if (!larkKey.startsWith('http')) {
...this.gotOption, larkKey = `https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`;
json: { }
msg_type: 'text',
content: { text: `${this.title}\n\n${this.content}` }, try {
}, const res: any = await got
headers: { 'Content-Type': 'application/json' }, .post(larkKey, {
}) ...this.gotOption,
.json(); json: {
return res.StatusCode === 0; 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() { private async email() {
const { emailPass, emailService, emailUser } = this.params; const { emailPass, emailService, emailUser } = this.params;
const transporter = nodemailer.createTransport({
service: emailService,
auth: {
user: emailUser,
pass: emailPass,
},
});
const info = await transporter.sendMail({ try {
from: `"青龙快讯" <${emailUser}>`, const transporter = nodemailer.createTransport({
to: `${emailUser}`, service: emailService,
subject: `${this.title}`, auth: {
html: `${this.content.replace(/\n/g, '<br/>')}`, 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() { private async webhook() {
@@ -460,8 +610,16 @@ export default class NotificationService {
allowGetBody: true, allowGetBody: true,
...bodyParam, ...bodyParam,
}; };
const res = await got(formatUrl, options); try {
return String(res.statusCode).startsWith('20'); 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 { private formatBody(contentType: string, body: any): object {
@@ -484,11 +642,11 @@ export default class NotificationService {
return { return {
formatUrl: url formatUrl: url
.replaceAll('$title', encodeURIComponent(this.title)) ?.replaceAll('$title', encodeURIComponent(this.title))
.replaceAll('$content', encodeURIComponent(this.content)), ?.replaceAll('$content', encodeURIComponent(this.content)),
formatBody: body formatBody: body
.replaceAll('$title', this.title) ?.replaceAll('$title', this.title)
.replaceAll('$content', this.content), ?.replaceAll('$content', this.content),
}; };
} }
} }
+1 -1
View File
@@ -75,7 +75,7 @@ export default class OpenService {
): Promise<App[]> { ): Promise<App[]> {
let condition = { ...query }; let condition = { ...query };
if (searchText) { if (searchText) {
const encodeText = encodeURIComponent(searchText); const encodeText = encodeURI(searchText);
const reg = { const reg = {
[Op.or]: [ [Op.or]: [
{ [Op.like]: `%${searchText}%` }, { [Op.like]: `%${searchText}%` },
+69 -65
View File
@@ -1,14 +1,16 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import nodeSchedule from 'node-schedule'; import nodeSchedule from 'node-schedule';
import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process'; import { ChildProcessWithoutNullStreams } from 'child_process';
import { import {
ToadScheduler, ToadScheduler,
LongIntervalJob, LongIntervalJob,
AsyncTask,
SimpleIntervalSchedule, SimpleIntervalSchedule,
Task,
} from 'toad-scheduler'; } from 'toad-scheduler';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import taskLimit from '../shared/pLimit';
import { spawn } from 'cross-spawn';
interface ScheduleTaskType { interface ScheduleTaskType {
id: number; id: number;
@@ -40,73 +42,74 @@ export default class ScheduleService {
private maxBuffer = 200 * 1024 * 1024; private maxBuffer = 200 * 1024 * 1024;
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) { }
async runTask( async runTask(
command: string, command: string,
callbacks: TaskCallbacks = {}, callbacks: TaskCallbacks = {},
completionTime: 'start' | 'end' = 'end', completionTime: 'start' | 'end' = 'end',
) { ) {
return new Promise(async (resolve, reject) => { return taskLimit.runWithCpuLimit(() => {
try { return new Promise(async (resolve, reject) => {
const startTime = dayjs(); try {
await callbacks.onBefore?.(startTime); 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);
completionTime === 'start' && resolve(cp.pid);
cp.stdout.on('data', async (data) => { cp.stdout.on('data', async (data) => {
await callbacks.onLog?.(data.toString()); await callbacks.onLog?.(data.toString());
}); });
cp.stderr.on('data', async (data) => { cp.stderr.on('data', async (data) => {
this.logger.error( this.logger.info(
'执行任务 %s 失败,时间:%s, 错误信息:%j', '[执行任务失败] %s,时间:%s, 错误信息:%j',
command,
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, command,
new Date().toLocaleString(), new Date().toLocaleString(),
data.toString(), error,
); );
await callbacks.onError?.(data.toString()); await callbacks.onError?.(JSON.stringify(error));
}); }
});
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));
}
});
} }
async createCronTask( async createCronTask(
@@ -126,19 +129,22 @@ export default class ScheduleService {
this.scheduleStacks.set( this.scheduleStacks.set(
_id, _id,
nodeSchedule.scheduleJob(_id, schedule, async () => { nodeSchedule.scheduleJob(_id, schedule, async () => {
await this.runTask(command, callbacks); this.runTask(command, callbacks);
}), }),
); );
if (runImmediately) { if (runImmediately) {
await this.runTask(command, callbacks); this.runTask(command, callbacks);
} }
} }
async cancelCronTask({ id = 0, name }: ScheduleTaskType) { async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info('[取消定时任务],任务名:%s', name); 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( async createIntervalTask(
@@ -154,12 +160,10 @@ export default class ScheduleService {
name, name,
command, command,
); );
const task = new AsyncTask( const task = new Task(
name, name,
async () => { () => {
return new Promise(async (resolve, reject) => { this.runTask(command, callbacks);
await this.runTask(command, callbacks);
});
}, },
(err) => { (err) => {
this.logger.error( this.logger.error(
@@ -180,7 +184,7 @@ export default class ScheduleService {
this.intervalSchedule.addIntervalJob(job); this.intervalSchedule.addIntervalJob(job);
if (runImmediately) { if (runImmediately) {
await this.runTask(command, callbacks); this.runTask(command, callbacks);
} }
} }
+3 -3
View File
@@ -6,7 +6,7 @@ import SockService from './sock';
import CronService from './cron'; import CronService from './cron';
import ScheduleService, { TaskCallbacks } from './schedule'; import ScheduleService, { TaskCallbacks } from './schedule';
import config from '../config'; import config from '../config';
import { LOG_END_SYMBOL } from '../config/const'; import { TASK_COMMAND } from '../config/const';
import { getPid, killTask } from '../config/util'; import { getPid, killTask } from '../config/util';
@Service() @Service()
@@ -42,7 +42,7 @@ export default class ScriptService {
public async runScript(filePath: string) { public async runScript(filePath: string) {
const relativePath = path.relative(config.scriptPath, filePath); const relativePath = path.relative(config.scriptPath, filePath);
const command = `task -l ${relativePath} now`; const command = `${TASK_COMMAND} -l ${relativePath} now`;
const pid = await this.scheduleService.runTask( const pid = await this.scheduleService.runTask(
command, command,
this.taskCallbacks(filePath), this.taskCallbacks(filePath),
@@ -56,7 +56,7 @@ export default class ScriptService {
let str = ''; let str = '';
if (!pid) { if (!pid) {
const relativePath = path.relative(config.scriptPath, filePath); const relativePath = path.relative(config.scriptPath, filePath);
pid = await getPid(`task -l ${relativePath} now`); pid = await getPid(`${TASK_COMMAND} -l ${relativePath} now`);
} }
try { try {
await killTask(pid); await killTask(pid);
+64 -45
View File
@@ -1,20 +1,42 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import fs from 'fs'; import fs, { existsSync } from 'fs';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
import { Subscription } from '../data/subscription';
import { formatUrl } from '../config/subscription';
import config from '../config';
@Service() @Service()
export default class SshKeyService { export default class SshKeyService {
private homedir = os.homedir(); private homedir = os.homedir();
private sshPath = path.resolve(this.homedir, '.ssh'); private sshPath = config.sshdPath;
private sshConfigFilePath = path.resolve(this.sshPath, 'config'); 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 { private generatePrivateKeyFile(alias: string, key: string): void {
try { try {
fs.writeFileSync(`${this.sshPath}/${alias}`, `${key}${os.EOL}`, { fs.writeFileSync(path.join(this.sshPath, alias), `${key}${os.EOL}`, {
encoding: 'utf8', encoding: 'utf8',
mode: '400', mode: '400',
}); });
@@ -23,55 +45,39 @@ export default class SshKeyService {
} }
} }
private getConfigRegx(alias: string) {
return new RegExp(
`Host ${alias}\n.*[^StrictHostKeyChecking]*.*[\n]*.*StrictHostKeyChecking no`,
'g',
);
}
private removePrivateKeyFile(alias: string): void { private removePrivateKeyFile(alias: string): void {
try { try {
fs.unlinkSync(`${this.sshPath}/${alias}`); const filePath = path.join(this.sshPath, alias);
if (existsSync(filePath)) {
fs.unlinkSync(filePath);
}
} catch (error) { } catch (error) {
this.logger.error('删除私钥文件失败', error); this.logger.error('删除私钥文件失败', error);
} }
} }
private generateSingleSshConfig( private generateSingleSshConfig(alias: string, host: string, proxy?: string) {
alias: string,
host: string,
proxy?: string,
): string {
if (host === 'github.com') { if (host === 'github.com') {
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa\n PubkeyAcceptedAlgorithms +ssh-rsa`; host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa\n PubkeyAcceptedAlgorithms +ssh-rsa`;
} }
const proxyStr = proxy ? ` ProxyCommand nc -v -x ${proxy} %h %p\n` : ''; const proxyStr = proxy
return `\nHost ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no\n${proxyStr}`; ? ` ProxyCommand nc -v -x ${proxy} %h %p 2>/dev/null\n`
} : '';
const config = `Host ${alias}\n Hostname ${host}\n IdentityFile ${path.join(
private generateSshConfig(configs: string[]) { this.sshPath,
try { alias,
for (const config of configs) { )}\n StrictHostKeyChecking no\n${proxyStr}`;
fs.appendFileSync(this.sshConfigFilePath, config, { fs.writeFileSync(`${path.join(this.sshPath, `${alias}.config`)}`, config, {
encoding: 'utf8', encoding: 'utf8',
}); });
}
} catch (error) {
this.logger.error('写入ssh配置文件失败', error);
}
} }
private removeSshConfig(alias: string) { private removeSshConfig(alias: string) {
try { try {
const configRegx = this.getConfigRegx(alias); const filePath = path.join(this.sshPath, `${alias}.config`);
const data = fs if (existsSync(filePath)) {
.readFileSync(this.sshConfigFilePath, { encoding: 'utf8' }) fs.unlinkSync(filePath);
.replace(configRegx, '') }
.replace(/\n[\n]+/g, '\n');
fs.writeFileSync(this.sshConfigFilePath, data, {
encoding: 'utf8',
});
} catch (error) { } catch (error) {
this.logger.error(`删除ssh配置文件${alias}失败`, error); this.logger.error(`删除ssh配置文件${alias}失败`, error);
} }
@@ -84,14 +90,27 @@ export default class SshKeyService {
proxy?: string, proxy?: string,
): void { ): void {
this.generatePrivateKeyFile(alias, key); this.generatePrivateKeyFile(alias, key);
const config = this.generateSingleSshConfig(alias, host, proxy); this.generateSingleSshConfig(alias, host, proxy);
this.removeSshConfig(alias);
this.generateSshConfig([config]);
} }
public removeSSHKey(alias: string, host: string, proxy?: string): void { public removeSSHKey(alias: string, host: string, proxy?: string): void {
this.removePrivateKeyFile(alias); this.removePrivateKeyFile(alias);
const config = this.generateSingleSshConfig(alias, host, proxy); this.removeSshConfig(alias);
this.removeSshConfig(config); }
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);
}
}
} }
} }
+39 -107
View File
@@ -6,23 +6,19 @@ import {
SubscriptionModel, SubscriptionModel,
SubscriptionStatus, SubscriptionStatus,
} from '../data/subscription'; } from '../data/subscription';
import { import { ChildProcessWithoutNullStreams } from 'child_process';
ChildProcessWithoutNullStreams,
exec,
execSync,
spawn,
} from 'child_process';
import fs from 'fs'; import fs from 'fs';
import cron_parser from 'cron-parser';
import { import {
getFileContentByName, getFileContentByName,
concurrentRun, concurrentRun,
fileExist, fileExist,
createFile, createFile,
killTask, killTask,
handleLogPath,
promiseExec,
} from '../config/util'; } from '../config/util';
import { promises, existsSync } from 'fs'; import { promises, existsSync } from 'fs';
import { Op } from 'sequelize'; import { FindOptions, Op } from 'sequelize';
import path from 'path'; import path from 'path';
import ScheduleService, { TaskCallbacks } from './schedule'; import ScheduleService, { TaskCallbacks } from './schedule';
import { SimpleIntervalSchedule } from 'toad-scheduler'; import { SimpleIntervalSchedule } from 'toad-scheduler';
@@ -30,6 +26,7 @@ import SockService from './sock';
import SshKeyService from './sshKey'; import SshKeyService from './sshKey';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { LOG_END_SYMBOL } from '../config/const'; import { LOG_END_SYMBOL } from '../config/const';
import { formatCommand, formatUrl } from '../config/subscription';
@Service() @Service()
export default class SubscriptionService { export default class SubscriptionService {
@@ -46,7 +43,7 @@ export default class SubscriptionService {
const reg = { const reg = {
[Op.or]: [ [Op.or]: [
{ [Op.like]: `%${searchText}%` }, { [Op.like]: `%${searchText}%` },
{ [Op.like]: `%${encodeURIComponent(searchText)}%` }, { [Op.like]: `%${encodeURI(searchText)}%` },
], ],
}; };
query = { query = {
@@ -68,71 +65,20 @@ export default class SubscriptionService {
['createdAt', 'DESC'], ['createdAt', 'DESC'],
], ],
}); });
return result as any; return result;
} catch (error) { } catch (error) {
throw error; throw error;
} }
} }
private formatCommand(doc: Subscription, url?: string) {
let command = 'ql ';
let _url = url || this.formatUrl(doc).url;
const {
type,
whitelist,
blacklist,
dependences,
branch,
extensions,
proxy,
} = doc;
if (type === 'file') {
command += `raw "${_url}"`;
} else {
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
dependences || ''
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}"`;
}
return command;
}
private formatUrl(doc: Subscription) {
let url = doc.url;
let host = '';
if (doc.type === 'private-repo') {
if (doc.pull_type === 'ssh-key') {
host = doc.url!.replace(/.*\@([^\:]+)\:.*/, '$1');
url = doc.url!.replace(host, doc.alias);
} else {
host = doc.url!.replace(/.*\:\/\/([^\/]+)\/.*/, '$1');
const { username, password } = doc.pull_option as any;
url = doc.url!.replace(host, `${username}:${password}@${host}`);
}
}
return { url, host };
}
public async handleTask( public async handleTask(
doc: Subscription, doc: Subscription,
needCreate = true, needCreate = true,
needAddKey = true,
runImmediately = false, runImmediately = false,
) { ) {
const { url, host } = this.formatUrl(doc); const { url } = formatUrl(doc);
if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {
if (needAddKey) {
this.sshKeyService.addSSHKey(
(doc.pull_option as any).private_key,
doc.alias,
host,
doc.proxy,
);
} else {
this.sshKeyService.removeSSHKey(doc.alias, host, doc.proxy);
}
}
doc.command = this.formatCommand(doc, url as string); doc.command = formatCommand(doc, url as string);
if (doc.schedule_type === 'crontab') { if (doc.schedule_type === 'crontab') {
this.scheduleService.cancelCronTask(doc as any); this.scheduleService.cancelCronTask(doc as any);
@@ -155,28 +101,9 @@ export default class SubscriptionService {
} }
} }
private async promiseExec(command: string): Promise<string> { public async setSshConfig() {
return new Promise((resolve, reject) => { const docs = await SubscriptionModel.findAll();
exec( this.sshKeyService.setSshConfig(docs);
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;
} }
private taskCallbacks(doc: Subscription): TaskCallbacks { private taskCallbacks(doc: Subscription): TaskCallbacks {
@@ -191,7 +118,7 @@ export default class SubscriptionService {
}, },
{ where: { id: doc.id } }, { where: { id: doc.id } },
); );
const absolutePath = await this.handleLogPath( const absolutePath = await handleLogPath(
logPath as string, logPath as string,
`## 开始执行... ${startTime.format('YYYY-MM-DD HH:mm:ss')}\n`, `## 开始执行... ${startTime.format('YYYY-MM-DD HH:mm:ss')}\n`,
); );
@@ -201,7 +128,7 @@ export default class SubscriptionService {
try { try {
if (doc.sub_before) { if (doc.sub_before) {
fs.appendFileSync(absolutePath, `\n## 执行before命令...\n\n`); fs.appendFileSync(absolutePath, `\n## 执行before命令...\n\n`);
beforeStr = await this.promiseExec(doc.sub_before); beforeStr = await promiseExec(doc.sub_before);
} }
} catch (error: any) { } catch (error: any) {
beforeStr = beforeStr =
@@ -221,14 +148,14 @@ export default class SubscriptionService {
}, },
onEnd: async (cp, endTime, diff) => { onEnd: async (cp, endTime, diff) => {
const sub = await this.getDb({ id: doc.id }); 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 // 执行 sub_after
let afterStr = ''; let afterStr = '';
try { try {
if (sub.sub_after) { if (sub.sub_after) {
fs.appendFileSync(absolutePath, `\n\n## 执行after命令...\n\n`); fs.appendFileSync(absolutePath, `\n\n## 执行after命令...\n\n`);
afterStr = await this.promiseExec(sub.sub_after); afterStr = await promiseExec(sub.sub_after);
} }
} catch (error: any) { } catch (error: any) {
afterStr = afterStr =
@@ -258,12 +185,12 @@ export default class SubscriptionService {
}, },
onError: async (message: string) => { onError: async (message: string) => {
const sub = await this.getDb({ id: doc.id }); 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}`); fs.appendFileSync(absolutePath, `\n${message}`);
}, },
onLog: async (message: string) => { onLog: async (message: string) => {
const sub = await this.getDb({ id: doc.id }); 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}`); fs.appendFileSync(absolutePath, `\n${message}`);
}, },
}; };
@@ -273,6 +200,7 @@ export default class SubscriptionService {
const tab = new Subscription(payload); const tab = new Subscription(payload);
const doc = await this.insert(tab); const doc = await this.insert(tab);
await this.handleTask(doc); await this.handleTask(doc);
await this.setSshConfig();
return doc; return doc;
} }
@@ -281,8 +209,11 @@ export default class SubscriptionService {
} }
public async update(payload: Subscription): Promise<Subscription> { public async update(payload: Subscription): Promise<Subscription> {
const newDoc = await this.updateDb(payload); const 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.handleTask(newDoc, !newDoc.is_disabled);
await this.setSshConfig();
return newDoc; return newDoc;
} }
@@ -325,12 +256,15 @@ export default class SubscriptionService {
public async remove(ids: number[]) { public async remove(ids: number[]) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } }); const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) { for (const doc of docs) {
await this.handleTask(doc, false, false); await this.handleTask(doc, false);
} }
await SubscriptionModel.destroy({ where: { id: ids } }); await SubscriptionModel.destroy({ where: { id: ids } });
await this.setSshConfig();
} }
public async getDb(query: any): Promise<Subscription> { public async getDb(
query: FindOptions<Subscription>['where'],
): Promise<Subscription> {
const doc: any = await SubscriptionModel.findOne({ where: { ...query } }); const doc: any = await SubscriptionModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Subscription); return doc && (doc.get({ plain: true }) as Subscription);
} }
@@ -340,10 +274,9 @@ export default class SubscriptionService {
{ status: SubscriptionStatus.queued }, { status: SubscriptionStatus.queued },
{ where: { id: ids } }, { where: { id: ids } },
); );
concurrentRun( ids.forEach((id) => {
ids.map((id) => async () => await this.runSingle(id)), this.runSingle(id);
10, });
);
} }
public async stop(ids: number[]) { public async stop(ids: number[]) {
@@ -356,7 +289,7 @@ export default class SubscriptionService {
this.logger.silly(error); this.logger.silly(error);
} }
} }
const absolutePath = await this.handleLogPath(doc.log_path as string); const absolutePath = await handleLogPath(doc.log_path as string);
fs.appendFileSync( fs.appendFileSync(
`${absolutePath}`, `${absolutePath}`,
@@ -378,28 +311,27 @@ export default class SubscriptionService {
return; return;
} }
const command = this.formatCommand(subscription); const command = formatCommand(subscription);
await this.scheduleService.runTask( this.scheduleService.runTask(command, this.taskCallbacks(subscription));
command,
this.taskCallbacks(subscription),
);
} }
public async disabled(ids: number[]) { public async disabled(ids: number[]) {
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
const docs = await SubscriptionModel.findAll({ where: { id: ids } }); const docs = await SubscriptionModel.findAll({ where: { id: ids } });
await this.setSshConfig();
for (const doc of docs) { for (const doc of docs) {
await this.handleTask(doc, false); await this.handleTask(doc, false);
} }
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
} }
public async enabled(ids: number[]) { public async enabled(ids: number[]) {
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
const docs = await SubscriptionModel.findAll({ where: { id: ids } }); const docs = await SubscriptionModel.findAll({ where: { id: ids } });
await this.setSshConfig();
for (const doc of docs) { for (const doc of docs) {
await this.handleTask(doc); await this.handleTask(doc);
} }
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
} }
public async log(id: number) { public async log(id: number) {
@@ -408,7 +340,7 @@ export default class SubscriptionService {
return ''; return '';
} }
const absolutePath = await this.handleLogPath(doc.log_path as string); const absolutePath = await handleLogPath(doc.log_path as string);
return getFileContentByName(absolutePath); return getFileContentByName(absolutePath);
} }
+145 -43
View File
@@ -1,14 +1,31 @@
import { Response } from 'express';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import config from '../config'; import config from '../config';
import * as fs from 'fs'; import {
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth'; AuthDataType,
AuthInfo,
AuthInstance,
AuthModel,
AuthModelInfo,
} from '../data/auth';
import { NotificationInfo } from '../data/notify'; import { NotificationInfo } from '../data/notify';
import NotificationService from './notify'; import NotificationService from './notify';
import ScheduleService from './schedule'; import ScheduleService, { TaskCallbacks } from './schedule';
import { spawn } from 'child_process'; import { spawn } from 'cross-spawn';
import SockService from './sock'; import SockService from './sock';
import got from 'got'; 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() @Service()
export default class SystemService { export default class SystemService {
@@ -19,22 +36,22 @@ export default class SystemService {
@Inject('logger') private logger: winston.Logger, @Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService, private scheduleService: ScheduleService,
private sockService: SockService, private sockService: SockService,
) {} ) { }
public async getLogRemoveFrequency() { public async getSystemConfig() {
const doc = await this.getDb({ type: AuthDataType.removeLogFrequency }); const doc = await this.getDb({ type: AuthDataType.systemConfig });
return doc || {}; return doc || ({} as AuthInstance);
} }
private async updateAuthDb(payload: AuthInfo): Promise<any> { private async updateAuthDb(payload: AuthInfo): Promise<AuthInstance> {
await AuthModel.upsert({ ...payload }); await AuthModel.upsert({ ...payload });
const doc = await this.getDb({ type: payload.type }); const doc = await this.getDb({ type: payload.type });
return doc; return doc;
} }
public async getDb(query: any): Promise<any> { public async getDb(query: any): Promise<AuthInstance> {
const doc: any = await AuthModel.findOne({ where: { ...query } }); 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) { public async updateNotificationMode(notificationInfo: NotificationInfo) {
@@ -55,37 +72,37 @@ export default class SystemService {
} }
} }
public async updateLogRemoveFrequency(frequency: number) { public async updateSystemConfig(info: AuthModelInfo) {
const oDoc = await this.getLogRemoveFrequency(); const oDoc = await this.getSystemConfig();
const result = await this.updateAuthDb({ const result = await this.updateAuthDb({
...oDoc, ...oDoc,
type: AuthDataType.removeLogFrequency, type: AuthDataType.systemConfig,
info: { frequency }, info,
}); });
const cron = { if (info.logRemoveFrequency) {
id: result.id, const cron = {
name: '删除日志', id: result.id,
command: `ql rmlog ${frequency}`, name: '删除日志',
}; command: `ql rmlog ${info.logRemoveFrequency}`,
await this.scheduleService.cancelIntervalTask(cron); };
if (frequency > 0) { await this.scheduleService.cancelIntervalTask(cron);
this.scheduleService.createIntervalTask(cron, { if (info.logRemoveFrequency > 0) {
days: frequency, 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() { public async checkUpdate() {
try { try {
const versionRegx = /.*export const version = \'(.*)\'\;/; const currentVersionContent = await parseVersion(config.versionFile);
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8'); let lastVersionContent;
const currentVersion = currentVersionFile.match(versionRegx)![1];
let lastVersion = '';
let lastLog = '';
try { try {
const result = await got.get( const result = await got.get(
`${config.lastVersionFile}?t=${Date.now()}`, `${config.lastVersionFile}?t=${Date.now()}`,
@@ -93,19 +110,23 @@ export default class SystemService {
timeout: 30000, timeout: 30000,
}, },
); );
const lastVersionFileContent = result.body; lastVersionContent = await parseContentVersion(result.body);
lastVersion = lastVersionFileContent.match(versionRegx)![1]; } catch (error) { }
lastLog = lastVersionFileContent.match(logRegx)
? lastVersionFileContent.match(logRegx)![1] if (!lastVersionContent) {
: ''; lastVersionContent = currentVersionContent;
} catch (error) {} }
return { return {
code: 200, code: 200,
data: { data: {
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion), hasNewVersion: this.checkHasNewVersion(
lastVersion, currentVersionContent.version,
lastLog, lastVersionContent.version,
),
lastVersion: lastVersionContent.version,
lastLog: lastVersionContent.changeLog,
lastLogLink: lastVersionContent.changeLogLink,
}, },
}; };
} catch (error: any) { } catch (error: any) {
@@ -136,7 +157,7 @@ export default class SystemService {
} }
public async updateSystem() { public async updateSystem() {
const cp = spawn('ql -l update', { shell: '/bin/bash' }); const cp = spawn('ql -l update false', { shell: '/bin/bash' });
cp.stdout.on('data', (data) => { cp.stdout.on('data', (data) => {
this.sockService.sendMessage({ this.sockService.sendMessage({
@@ -162,6 +183,33 @@ export default class SystemService {
return { code: 200 }; 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 }) { public async notify({ title, content }: { title: string; content: string }) {
const isSuccess = await this.notificationService.notify(title, content); const isSuccess = await this.notificationService.notify(title, content);
if (isSuccess) { if (isSuccess) {
@@ -170,4 +218,58 @@ export default class SystemService {
return { code: 400, message: '通知发送失败,请检查系统设置/通知配置' }; 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 };
}
}
} }
+26 -20
View File
@@ -1,16 +1,20 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import { createRandomString, getNetIp, getPlatform } from '../config/util'; import {
createRandomString,
fileExist,
getNetIp,
getPlatform,
} from '../config/util';
import config from '../config'; import config from '../config';
import * as fs from 'fs'; import * as fs from 'fs';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { authenticator } from '@otplib/preset-default'; 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 { NotificationInfo } from '../data/notify';
import NotificationService from './notify'; import NotificationService from './notify';
import { Request } from 'express'; import { Request } from 'express';
import ScheduleService from './schedule'; import ScheduleService from './schedule';
import { spawn } from 'child_process';
import SockService from './sock'; import SockService from './sock';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@@ -23,7 +27,7 @@ export default class UserService {
@Inject('logger') private logger: winston.Logger, @Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService, private scheduleService: ScheduleService,
private sockService: SockService, private sockService: SockService,
) {} ) { }
public async login( public async login(
payloads: { payloads: {
@@ -115,8 +119,7 @@ export default class UserService {
}); });
await this.notificationService.notify( await this.notificationService.notify(
'登录通知', '登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${ `你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${req.platform
req.platform
} ip地址 ${ip}`, } ip地址 ${ip}`,
); );
await this.getLoginLog(); await this.getLoginLog();
@@ -144,8 +147,7 @@ export default class UserService {
}); });
await this.notificationService.notify( await this.notificationService.notify(
'登录通知', '登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${ `你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${req.platform
req.platform
} ip地址 ${ip}`, } ip地址 ${ip}`,
); );
await this.getLoginLog(); await this.getLoginLog();
@@ -183,12 +185,12 @@ export default class UserService {
}); });
} }
public async getLoginLog(): Promise<AuthInfo[]> { public async getLoginLog(): Promise<Array<AuthModelInfo | undefined>> {
const docs = await AuthModel.findAll({ const docs = await AuthModel.findAll({
where: { type: AuthDataType.loginLog }, where: { type: AuthDataType.loginLog },
}); });
if (docs && docs.length > 0) { 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) { if (result.length > 100) {
await AuthModel.destroy({ await AuthModel.destroy({
where: { id: result[result.length - 1].id }, where: { id: result[result.length - 1].id },
@@ -205,17 +207,16 @@ export default class UserService {
} }
private initAuthInfo() { private initAuthInfo() {
const newPassword = createRandomString(16, 22);
fs.writeFileSync( fs.writeFileSync(
config.authConfigFile, config.authConfigFile,
JSON.stringify({ JSON.stringify({
username: 'admin', username: 'admin',
password: newPassword, password: 'admin',
}), }),
); );
return { return {
code: 100, code: 100,
message: '已初始化密码,请前往auth.json查看并重新登录', message: '未找到认证文件,重新初始化',
}; };
} }
@@ -240,13 +241,18 @@ export default class UserService {
return { code: 200, data: avatar, message: '更新成功' }; return { code: 200, data: avatar, message: '更新成功' };
} }
public getUserInfo(): Promise<any> { public async getUserInfo(): Promise<any> {
return new Promise((resolve) => { const authFileExist = await fileExist(config.authConfigFile);
fs.readFile(config.authConfigFile, 'utf8', (err, data) => { if (!authFileExist) {
if (err) console.log(err); fs.writeFileSync(
resolve(JSON.parse(data)); config.authConfigFile,
}); JSON.stringify({
}); username: 'admin',
password: 'admin',
}),
);
}
return this.getAuthInfo();
} }
public initTwoFactor() { 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();
});
});
})
}
+66 -48
View File
@@ -1,3 +1,12 @@
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 FROM python:3.10-alpine
ARG QL_MAINTAINER="whyour" ARG QL_MAINTAINER="whyour"
@@ -6,55 +15,64 @@ ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
ARG QL_BRANCH=develop ARG QL_BRANCH=develop
ENV PNPM_HOME=/root/.local/share/pnpm \ ENV PNPM_HOME=/root/.local/share/pnpm \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
LANG=zh_CN.UTF-8 \ 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 \
SHELL=/bin/bash \ LANG=C.UTF-8 \
PS1="\u@\h:\w \$ " \ SHELL=/bin/bash \
QL_DIR=/ql \ PS1="\u@\h:\w \$ " \
QL_BRANCH=${QL_BRANCH} 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} WORKDIR ${QL_DIR}
RUN set -x \ HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ CMD curl -sf http://127.0.0.1:5400/api/health || exit 1
&& 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
ENTRYPOINT ["./docker/docker-entrypoint.sh"] ENTRYPOINT ["./docker/docker-entrypoint.sh"]
+6
View File
@@ -1,9 +1,15 @@
version: '2' version: '2'
services: services:
web: web:
# alpine 基础镜像版本
image: whyour/qinglong:latest image: whyour/qinglong:latest
# debian-slim 基础镜像版本
# image: whyour/qinglong:debian
volumes: volumes:
- ./data:/ql/data - ./data:/ql/data
ports: ports:
- "0.0.0.0:5700:5700" - "0.0.0.0:5700:5700"
environment:
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
QlBaseUrl: '/'
restart: unless-stopped restart: unless-stopped
+23 -28
View File
@@ -4,57 +4,52 @@ dir_shell=/ql/shell
. $dir_shell/share.sh . $dir_shell/share.sh
link_shell link_shell
export isFirstStartServer=true
echo -e "======================1. 检测配置文件========================\n" echo -e "======================1. 检测配置文件========================\n"
make_dir /etc/nginx/conf.d make_dir /etc/nginx/conf.d
make_dir /run/nginx make_dir /run/nginx
cp -fv $nginx_conf /etc/nginx/nginx.conf init_nginx
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf fix_config
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
pm2 l &>/dev/null pm2 l &>/dev/null
patch_version &>/dev/null
echo
echo -e "======================2. 安装依赖========================\n" echo -e "======================2. 安装依赖========================\n"
update_depend patch_version
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
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
echo -e "======================3. 启动nginx========================\n" echo -e "======================3. 启动nginx========================\n"
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "nginx启动成功...\n" echo -e "nginx启动成功...\n"
echo -e "======================4. 启动面板监控========================\n" echo -e "======================4. 启动pm2服务========================\n"
pm2 delete public &>/dev/null reload_pm2
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"
if [[ $AutoStartBot == true ]]; then if [[ $AutoStartBot == true ]]; then
echo -e "======================7. 启动bot========================\n" echo -e "======================5. 启动bot========================\n"
nohup ql bot >$dir_log/bot.log 2>&1 & nohup ql -l bot >$dir_log/bot.log 2>&1 &
echo -e "bot后台启动中...\n" echo -e "bot后台启动中...\n"
fi fi
if [[ $EnableExtraShell == true ]]; then if [[ $EnableExtraShell == true ]]; then
echo -e "======================8. 执行自定义脚本========================\n" echo -e "====================6. 执行自定义脚本========================\n"
nohup ql extra >$dir_log/extra.log 2>&1 & nohup ql -l extra >$dir_log/extra.log 2>&1 &
echo -e "自定义脚本后台执行中...\n" echo -e "自定义脚本后台执行中...\n"
fi fi
echo -e "############################################################\n" echo -e "############################################################\n"
echo -e "容器启动成功..." echo -e "容器启动成功..."
echo -e "\n请先访问5700端口,登录成功面板之后再执行添加定时任务..."
echo -e "############################################################\n" echo -e "############################################################\n"
crond -f >/dev/null crond -f >/dev/null
+12 -7
View File
@@ -13,31 +13,34 @@ map $http_upgrade $connection_upgrade {
server { server {
listen 5700; listen 5700;
root /ql/static/dist; IPV6_CONFIG
ssl_session_timeout 5m; ssl_session_timeout 5m;
location QL_BASE_URL/api/public/ { location QL_BASE_URLapi/public/ {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 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 Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://baseApi/api/; proxy_pass http://baseApi/api/;
proxy_buffering off;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_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 Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://baseApi/open/; proxy_pass http://baseApi/open/;
proxy_buffering off;
} }
gzip on; gzip on;
@@ -48,10 +51,12 @@ server {
gzip_comp_level 6; gzip_comp_level 6;
gzip_buffers 16 8k; gzip_buffers 16 8k;
gzip_http_version 1.0; 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; index index.html index.htm;
try_files $uri $uri/ QL_BASE_URL/index.html; try_files $uri QL_BASE_URLindex.html;
} }
location ~ .*\.(html)$ { location ~ .*\.(html)$ {
+1 -1
View File
@@ -14,7 +14,7 @@ http {
server_tokens off; server_tokens off;
client_max_body_size 20m; client_max_body_size 4096m;
client_body_buffer_size 20m; client_body_buffer_size 20m;
keepalive_timeout 65; 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',
},
],
};
+30 -8
View File
@@ -4,12 +4,14 @@
"start": "concurrently -n w: npm:start:*", "start": "concurrently -n w: npm:start:*",
"start:front": "max dev", "start:front": "max dev",
"start:back": "nodemon", "start:back": "nodemon",
"start:public": "ts-node back/public.ts", "start:public": "ts-node --transpile-only ./back/public.ts",
"start:rpc": "ts-node --transpile-only ./back/schedule/index.ts",
"build:front": "max build", "build:front": "max build",
"build:back": "tsc -p tsconfig.back.json", "build:back": "tsc -p tsconfig.back.json",
"panel": "npm run build:back && node static/build/app.js", "panel": "npm run build:back && node static/build/app.js",
"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", "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}'", "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"postinstall": "max setup 2>/dev/null || true", "postinstall": "max setup 2>/dev/null || true",
"test": "umi-test", "test": "umi-test",
@@ -42,7 +44,8 @@
"monaco-editor", "monaco-editor",
"rc-field-form", "rc-field-form",
"@types/lodash.merge", "@types/lodash.merge",
"rollup" "rollup",
"styled-components"
], ],
"allowedVersions": { "allowedVersions": {
"react": "18", "react": "18",
@@ -52,6 +55,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.8.13",
"@otplib/preset-default": "^12.0.1", "@otplib/preset-default": "^12.0.1",
"@sentry/node": "^7.12.1", "@sentry/node": "^7.12.1",
"@sentry/tracing": "^7.12.1", "@sentry/tracing": "^7.12.1",
@@ -60,27 +64,34 @@
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron-parser": "^4.2.1", "cron-parser": "^4.2.1",
"cross-spawn": "^7.0.3",
"dayjs": "^1.11.2", "dayjs": "^1.11.2",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"express": "^4.17.3", "express": "^4.17.3",
"express-jwt": "^6.1.1", "express-jwt": "^6.1.1",
"express-rate-limit": "^6.7.0",
"express-urlrewrite": "^1.4.0", "express-urlrewrite": "^1.4.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"got": "^11.8.2", "got": "^11.8.2",
"hpagent": "^0.1.2", "hpagent": "^0.1.2",
"http-proxy-middleware": "^2.0.6",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"multer": "^1.4.4", "multer": "^1.4.4",
"nedb": "^1.8.0", "nedb": "^1.8.0",
"node-schedule": "^2.1.0", "node-schedule": "^2.1.0",
"nodemailer": "^6.7.2", "nodemailer": "^6.7.2",
"p-limit": "3.1.0",
"protobufjs": "^7.2.3",
"pstree.remy": "^1.1.8", "pstree.remy": "^1.1.8",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"sequelize": "^6.25.5", "sequelize": "^6.25.5",
"serve-handler": "^6.1.3", "serve-handler": "^6.1.3",
"sockjs": "^0.3.24", "sockjs": "^0.3.24",
"sqlite3": "npm:@louislam/sqlite3@^15.0.6", "sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3",
"tar": "^6.1.15",
"toad-scheduler": "^1.6.0", "toad-scheduler": "^1.6.0",
"typedi": "^0.10.0", "typedi": "^0.10.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
@@ -89,14 +100,17 @@
}, },
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^4.7.0", "@ant-design/icons": "^4.7.0",
"@ant-design/pro-layout": "^6.33.1", "@ant-design/pro-layout": "6.38.22",
"@monaco-editor/react": "4.2.1", "@monaco-editor/react": "4.2.1",
"@react-hook/resize-observer": "^1.2.6", "@react-hook/resize-observer": "^1.2.6",
"@sentry/react": "^7.12.1", "@sentry/react": "^7.12.1",
"@types/body-parser": "^1.19.2", "@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/cross-spawn": "^6.0.2",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/express-jwt": "^6.0.4", "@types/express-jwt": "^6.0.4",
"@types/file-saver": "^2.0.5",
"@types/js-yaml": "^4.0.5",
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.185", "@types/lodash": "^4.14.185",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",
@@ -106,20 +120,25 @@
"@types/nodemailer": "^6.4.4", "@types/nodemailer": "^6.4.4",
"@types/qrcode.react": "^1.0.2", "@types/qrcode.react": "^1.0.2",
"@types/react": "^18.0.20", "@types/react": "^18.0.20",
"@types/react-copy-to-clipboard": "^5.0.4",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@types/serve-handler": "^6.1.1", "@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33", "@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1", "@types/sockjs-client": "^1.5.1",
"@types/tar": "^6.1.5",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@umijs/max": "^4.0.21", "@umijs/max": "^4.0.72",
"@umijs/ssr-darkreader": "^4.9.45", "@umijs/ssr-darkreader": "^4.9.45",
"ansi-to-react": "^6.1.6", "ansi-to-react": "^6.1.6",
"antd": "^4.23.0", "antd": "^4.24.8",
"antd-img-crop": "^4.2.3", "antd-img-crop": "^4.2.3",
"axios": "^1.4.0",
"codemirror": "^5.65.2", "codemirror": "^5.65.2",
"compression-webpack-plugin": "9.2.0", "compression-webpack-plugin": "9.2.0",
"concurrently": "^7.0.0", "concurrently": "^7.0.0",
"file-saver": "^2.0.5",
"lint-staged": "^13.0.3", "lint-staged": "^13.0.3",
"monaco-editor": "0.33.0",
"nodemon": "^2.0.15", "nodemon": "^2.0.15",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"qiniu": "^7.4.0", "qiniu": "^7.4.0",
@@ -128,6 +147,7 @@
"rc-tween-one": "^3.0.6", "rc-tween-one": "^3.0.6",
"react": "18.2.0", "react": "18.2.0",
"react-codemirror2": "^7.2.1", "react-codemirror2": "^7.2.1",
"react-copy-to-clipboard": "^5.1.0",
"react-diff-viewer": "^3.1.1", "react-diff-viewer": "^3.1.1",
"react-dnd": "^14.0.2", "react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0", "react-dnd-html5-backend": "^14.0.0",
@@ -135,10 +155,12 @@
"react-split-pane": "^0.1.92", "react-split-pane": "^0.1.92",
"sockjs-client": "^1.6.0", "sockjs-client": "^1.6.0",
"ts-node": "^10.6.0", "ts-node": "^10.6.0",
"ts-proto": "^1.146.0",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"tsx": "^3.12.3",
"typescript": "4.8.4", "typescript": "4.8.4",
"umi-request": "^1.4.0",
"vh-check": "^2.0.5", "vh-check": "^2.0.5",
"virtualizedtableforantd4": "1.3.0",
"webpack": "^5.70.0", "webpack": "^5.70.0",
"yorkie": "^2.0.0" "yorkie": "^2.0.0"
} }
+6934 -5165
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

+19 -11
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 命令时,是否自动删除失效的脚本与定时任务 ## 在运行 ql repo 命令时,是否自动删除失效的脚本与定时任务
AutoDelCron="true" AutoDelCron="true"
@@ -24,11 +18,8 @@ CpuWarn=80
MemoryWarn=80 MemoryWarn=80
DiskWarn=90 DiskWarn=90
## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天 ## 设置定时任务执行的超时时间,例如1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
CommandTimeoutTime="1h" CommandTimeoutTime=""
## 设置批量执行任务时的并发数,默认同时执行5个任务
MaxConcurrentNum="5"
## 在运行 task 命令时,随机延迟启动任务的最大延迟时间 ## 在运行 task 命令时,随机延迟启动任务的最大延迟时间
## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空 ## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
@@ -100,6 +91,10 @@ export TG_API_HOST=""
export DD_BOT_TOKEN="" export DD_BOT_TOKEN=""
export DD_BOT_SECRET="" export DD_BOT_SECRET=""
## 企业微信反向代理地址
## (环境变量名 QYWX_ORIGIN)
export QYWX_ORIGIN=""
## 5. 企业微信机器人 ## 5. 企业微信机器人
## 官方说明文档:https://work.weixin.qq.com/api/doc/90000/90136/91770 ## 官方说明文档:https://work.weixin.qq.com/api/doc/90000/90136/91770
## 下方填写密钥,企业微信推送 webhook 后面的 key ## 下方填写密钥,企业微信推送 webhook 后面的 key
@@ -160,4 +155,17 @@ export AIBOTK_TYPE=""
## aibotk_name (必填)填写群名或用户昵称,和上面的type类型要对应 ## aibotk_name (必填)填写群名或用户昵称,和上面的type类型要对应
export AIBOTK_NAME="" export AIBOTK_NAME=""
## 14. SMTP
## 暂时只支持在 Python 中调用 notify.py 以发送 SMTP 邮件通知
## smtp_server 填写 SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
export SMTP_SERVER=""
## smtp_ssl 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
export SMTP_SSL="false"
## smtp_email 填写 SMTP 收发件邮箱,通知将会由自己发给自己
export SMTP_EMAIL=""
## smtp_password 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
export SMTP_PASSWORD=""
## smtp_name 填写 SMTP 收发件人姓名,可随意填写
export SMTP_NAME=""
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可 ## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+144 -5
View File
File diff suppressed because one or more lines are too long
+155 -35
View File
@@ -9,6 +9,10 @@ import re
import threading import threading
import time import time
import urllib.parse import urllib.parse
import smtplib
from email.mime.text import MIMEText
from email.header import Header
from email.utils import formataddr
import requests import requests
@@ -62,16 +66,18 @@ push_config = {
'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY 'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY
'DEER_URL': '', # PushDeer 的 PUSHDEER_URL 'DEER_URL': '', # PushDeer 的 PUSHDEER_URL
'CHAT_URL': '', # synology chat url 'CHAT_URL': '', # synology chat url
'CHAT_TOKEN': '', # synology chat token 'CHAT_TOKEN': '', # synology chat token
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌 'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码 'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
'QMSG_KEY': '', # qmsg 酱的 QMSG_KEY 'QMSG_KEY': '', # qmsg 酱的 QMSG_KEY
'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE 'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE
'QYWX_ORIGIN': '', # 企业微信代理地址
'QYWX_AM': '', # 企业微信应用 'QYWX_AM': '', # 企业微信应用
'QYWX_KEY': '', # 企业微信机器人 'QYWX_KEY': '', # 企业微信机器人
@@ -86,6 +92,14 @@ push_config = {
'AIBOTK_KEY': '', # 智能微秘书 个人中心的apikey 文档地址:http://wechat.aibotk.com/docs/about 'AIBOTK_KEY': '', # 智能微秘书 个人中心的apikey 文档地址:http://wechat.aibotk.com/docs/about
'AIBOTK_TYPE': '', # 智能微秘书 发送目标 room 或 contact 'AIBOTK_TYPE': '', # 智能微秘书 发送目标 room 或 contact
'AIBOTK_NAME': '', # 智能微秘书 发送群名 或者好友昵称和type要对应好 'AIBOTK_NAME': '', # 智能微秘书 发送群名 或者好友昵称和type要对应好
'SMTP_SERVER': '', # SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
'SMTP_SSL': 'false', # SMTP 发送邮件服务器是否使用 SSL,填写 true 或 false
'SMTP_EMAIL': '', # SMTP 收发件邮箱,通知将会由自己发给自己
'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
'PUSHME_KEY': '', # PushMe 酱的 PUSHME_KEY
} }
notify_function = [] notify_function = []
# fmt: on # fmt: on
@@ -210,7 +224,7 @@ def go_cqhttp(title: str, content: str) -> None:
print("go-cqhttp 推送失败!") print("go-cqhttp 推送失败!")
def gotify(title:str,content:str) -> None: def gotify(title: str, content: str) -> None:
""" """
使用 gotify 推送消息 使用 gotify 推送消息
""" """
@@ -220,8 +234,12 @@ def gotify(title:str,content:str) -> None:
print("gotify 服务启动") print("gotify 服务启动")
url = f'{push_config.get("GOTIFY_URL")}/message?token={push_config.get("GOTIFY_TOKEN")}' 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")} data = {
response = requests.post(url,data=data).json() "title": title,
"message": content,
"priority": push_config.get("GOTIFY_PRIORITY"),
}
response = requests.post(url, data=data).json()
if response.get("id"): if response.get("id"):
print("gotify 推送成功!") print("gotify 推送成功!")
@@ -259,10 +277,10 @@ def serverJ(title: str, content: str) -> None:
print("serverJ 服务启动") print("serverJ 服务启动")
data = {"text": title, "desp": content.replace("\n", "\n\n")} 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' url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send'
else: 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() response = requests.post(url, data=data).json()
if response.get("errno") == 0 or response.get("code") == 0: if response.get("errno") == 0 or response.get("code") == 0:
@@ -279,19 +297,24 @@ def pushdeer(title: str, content: str) -> None:
print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送") print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送")
return return
print("PushDeer 服务启动") print("PushDeer 服务启动")
data = {"text": title, "desp": content, "type": "markdown", "pushkey": push_config.get("DEER_KEY")} data = {
url = 'https://api2.pushdeer.com/message/push' "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"): if push_config.get("DEER_URL"):
url = push_config.get("DEER_URL") url = push_config.get("DEER_URL")
response = requests.post(url, data=data).json() response = requests.post(url, data=data).json()
if len(response.get("content").get("result")) > 0: if len(response.get("content").get("result")) > 0:
print("PushDeer 推送成功!") print("PushDeer 推送成功!")
else: else:
print("PushDeer 推送失败!错误信息:", response) print("PushDeer 推送失败!错误信息:", response)
def chat(title: str, content: str) -> None: def chat(title: str, content: str) -> None:
""" """
通过Chat 推送消息 通过Chat 推送消息
@@ -300,17 +323,16 @@ def chat(title: str, content: str) -> None:
print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送") print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送")
return return
print("chat 服务启动") 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") url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN")
response = requests.post(url, data=data) response = requests.post(url, data=data)
if response.status_code == 200: if response.status_code == 200:
print("Chat 推送成功!") print("Chat 推送成功!")
else: else:
print("Chat 推送失败!错误信息:", response) print("Chat 推送失败!错误信息:", response)
def pushplus_bot(title: str, content: str) -> None: def pushplus_bot(title: str, content: str) -> None:
""" """
通过 push+ 推送消息 通过 push+ 推送消息
@@ -335,7 +357,6 @@ def pushplus_bot(title: str, content: str) -> None:
print("PUSHPLUS 推送成功!") print("PUSHPLUS 推送成功!")
else: else:
url_old = "http://pushplus.hxtrip.com/send" url_old = "http://pushplus.hxtrip.com/send"
headers["Accept"] = "application/json" headers["Accept"] = "application/json"
response = requests.post(url=url_old, data=body, headers=headers).json() response = requests.post(url=url_old, data=body, headers=headers).json()
@@ -406,9 +427,12 @@ class WeCom:
self.CORPID = corpid self.CORPID = corpid
self.CORPSECRET = corpsecret self.CORPSECRET = corpsecret
self.AGENTID = agentid 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): def get_access_token(self):
url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken" url = f"{self.ORIGIN}/cgi-bin/gettoken"
values = { values = {
"corpid": self.CORPID, "corpid": self.CORPID,
"corpsecret": self.CORPSECRET, "corpsecret": self.CORPSECRET,
@@ -418,10 +442,7 @@ class WeCom:
return data["access_token"] return data["access_token"]
def send_text(self, message, touser="@all"): def send_text(self, message, touser="@all"):
send_url = ( send_url = f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
+ self.get_access_token()
)
send_values = { send_values = {
"touser": touser, "touser": touser,
"msgtype": "text", "msgtype": "text",
@@ -435,10 +456,7 @@ class WeCom:
return respone["errmsg"] return respone["errmsg"]
def send_mpnews(self, title, message, media_id, touser="@all"): def send_mpnews(self, title, message, media_id, touser="@all"):
send_url = ( send_url = f"https://{self.HOST}/cgi-bin/message/send?access_token={self.get_access_token()}"
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
+ self.get_access_token()
)
send_values = { send_values = {
"touser": touser, "touser": touser,
"msgtype": "mpnews", "msgtype": "mpnews",
@@ -471,7 +489,11 @@ def wecom_bot(title: str, content: str) -> None:
return return
print("企业微信机器人服务启动") 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"} headers = {"Content-Type": "application/json;charset=utf-8"}
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}} data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
response = requests.post( response = requests.post(
@@ -533,24 +555,28 @@ 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"): 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取消推送") print("智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送")
return return
print("智能微秘书 服务启动") print("智能微秘书 服务启动")
if push_config.get("AIBOTK_TYPE") == 'room': if push_config.get("AIBOTK_TYPE") == "room":
url = "https://api-bot.aibotk.com/openapi/v1/chat/room" url = "https://api-bot.aibotk.com/openapi/v1/chat/room"
data = { data = {
"apiKey": push_config.get("AIBOTK_KEY"), "apiKey": push_config.get("AIBOTK_KEY"),
"roomName": push_config.get("AIBOTK_NAME"), "roomName": push_config.get("AIBOTK_NAME"),
"message": {"type": 1, "content": f'【青龙快讯】\n\n${title}\n${content}' } "message": {"type": 1, "content": f"【青龙快讯】\n\n{title}\n{content}"},
} }
else: else:
url = "https://api-bot.aibotk.com/openapi/v1/chat/contact" url = "https://api-bot.aibotk.com/openapi/v1/chat/contact"
data = { data = {
"apiKey": push_config.get("AIBOTK_KEY"), "apiKey": push_config.get("AIBOTK_KEY"),
"name": push_config.get("AIBOTK_NAME"), "name": push_config.get("AIBOTK_NAME"),
"message": {"type": 1, "content": f'【青龙快讯】\n\n${title}\n${content}' } "message": {"type": 1, "content": f"【青龙快讯】\n\n{title}\n{content}"},
} }
body = json.dumps(data).encode(encoding="utf-8") body = json.dumps(data).encode(encoding="utf-8")
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
@@ -562,6 +588,79 @@ def aibotk(title: str, content: str) -> None:
print(f'智能微秘书 推送失败!{response["error"]}') print(f'智能微秘书 推送失败!{response["error"]}')
def smtp(title: str, content: str) -> None:
"""
使用 SMTP 邮件 推送消息
"""
if (
not push_config.get("SMTP_SERVER")
or not push_config.get("SMTP_SSL")
or not push_config.get("SMTP_EMAIL")
or not push_config.get("SMTP_PASSWORD")
or not push_config.get("SMTP_NAME")
):
print(
"SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送"
)
return
print("SMTP 邮件 服务启动")
message = MIMEText(content, "plain", "utf-8")
message["From"] = formataddr(
(
Header(push_config.get("SMTP_NAME"), "utf-8").encode(),
push_config.get("SMTP_EMAIL"),
)
)
message["To"] = formataddr(
(
Header(push_config.get("SMTP_NAME"), "utf-8").encode(),
push_config.get("SMTP_EMAIL"),
)
)
message["Subject"] = Header(title, "utf-8")
try:
smtp_server = (
smtplib.SMTP_SSL(push_config.get("SMTP_SERVER"))
if push_config.get("SMTP_SSL") == "true"
else smtplib.SMTP(push_config.get("SMTP_SERVER"))
)
smtp_server.login(
push_config.get("SMTP_EMAIL"), push_config.get("SMTP_PASSWORD")
)
smtp_server.sendmail(
push_config.get("SMTP_EMAIL"),
push_config.get("SMTP_EMAIL"),
message.as_bytes(),
)
smtp_server.close()
print("SMTP 邮件 推送成功!")
except Exception as e:
print(f"SMTP 邮件 推送失败!{e}")
def 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: def one() -> str:
""" """
获取一条一言 获取一条一言
@@ -602,8 +701,22 @@ if push_config.get("QYWX_KEY"):
notify_function.append(wecom_bot) notify_function.append(wecom_bot)
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"): if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
notify_function.append(telegram_bot) notify_function.append(telegram_bot)
if push_config.get("AIBOTK_KEY") and push_config.get("AIBOTK_TYPE") and push_config.get("AIBOTK_NAME"): if (
notify_function.append(aibotk) 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: def send(title: str, content: str) -> None:
@@ -611,6 +724,13 @@ def send(title: str, content: str) -> None:
print(f"{title} 推送内容为空!") print(f"{title} 推送内容为空!")
return return
# 根据标题跳过一些消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔
skipTitle = os.getenv("SKIP_PUSH_TITLE")
if skipTitle:
if title in re.split("\n", skipTitle):
print(f"{title} 在SKIP_PUSH_TITLE环境变量内,跳过推送!")
return
hitokoto = push_config.get("HITOKOTO") hitokoto = push_config.get("HITOKOTO")
text = one() if hitokoto else "" text = one() if hitokoto else ""
+2 -6
View File
@@ -3,14 +3,10 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"crypto-js": "^4.0.0",
"download": "^8.0.0",
"http-server": "^0.12.3",
"got": "^11.5.1", "got": "^11.5.1",
"qrcode-terminal": "^0.12.0", "nodemailer": "^6.8.0",
"request": "^2.88.2",
"tough-cookie": "^4.0.0", "tough-cookie": "^4.0.0",
"tunnel": "0.0.6", "tunnel": "0.0.6",
"ws": "^7.4.3" "ws": "^7.4.3"
} }
} }
+2 -2
View File
@@ -5,14 +5,14 @@ const envFound = dotenv.config();
const accessKey = process.env.QINIU_AK; const accessKey = process.env.QINIU_AK;
const secretKey = process.env.QINIU_SK; const secretKey = process.env.QINIU_SK;
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey); const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
const key = 'version.ts'; const key = 'version.yaml';
const options = { const options = {
scope: `${process.env.QINIU_SCOPE}:${key}`, scope: `${process.env.QINIU_SCOPE}:${key}`,
}; };
const putPolicy = new qiniu.rs.PutPolicy(options); const putPolicy = new qiniu.rs.PutPolicy(options);
const uploadToken = putPolicy.uploadToken(mac); const uploadToken = putPolicy.uploadToken(mac);
const localFile = 'src/version.ts'; const localFile = 'version.yaml';
const config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z1 }); const config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z1 });
const formUploader = new qiniu.form_up.FormUploader(config); const formUploader = new qiniu.form_up.FormUploader(config);
const putExtra = new qiniu.form_up.PutExtra( const putExtra = new qiniu.form_up.PutExtra(
+71 -33
View File
@@ -1,15 +1,24 @@
#!/usr/bin/env bash #!/usr/bin/env bash
create_token() {
local token_command="tsx ${dir_root}/back/token.ts"
local token_file="${dir_root}/static/build/token.js"
if [[ -f $token_file ]]; then
token_command="node ${token_file}"
fi
token=$(eval "$token_command")
}
get_token() { get_token() {
if [[ -f $file_auth_token ]]; then if [[ -f $file_auth_token ]]; then
token=$(cat $file_auth_token | jq -r .value) token=$(cat $file_auth_token | jq -r .value)
else local expiration=$(cat $file_auth_token | jq -r .expiration)
local token_command="ts-node-transpile-only ${dir_root}/back/token.ts" local currentTimeStamp=$(date +%s)
local token_file="${dir_root}static/build/token.js" if [[ $currentTimeStamp -ge $expiration ]]; then
if [[ -f $token_file ]]; then create_token
token_command="node ${token_file}"
fi fi
token=$(eval "$token_command") else
create_token
fi fi
} }
@@ -19,10 +28,16 @@ add_cron_api() {
local schedule=$(echo "$1" | awk -F ":" '{print $1}') local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}') local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}') local name=$(echo "$1" | awk -F ":" '{print $3}')
local sub_id=$(echo "$1" | awk -F ":" '{print $4}')
else else
local schedule=$1 local schedule="$1"
local command=$2 local command="$2"
local name=$3 local name="$3"
local sub_id="$4"
fi
if [[ ! $sub_id ]];then
sub_id="null"
fi fi
local api=$( local api=$(
@@ -34,11 +49,11 @@ add_cron_api() {
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \ --data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"sub_id\":$sub_id}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "$name -> 添加成功" echo -e "$name -> 添加成功"
else else
@@ -54,10 +69,10 @@ update_cron_api() {
local name=$(echo "$1" | awk -F ":" '{print $3}') local name=$(echo "$1" | awk -F ":" '{print $3}')
local id=$(echo "$1" | awk -F ":" '{print $4}') local id=$(echo "$1" | awk -F ":" '{print $4}')
else else
local schedule=$1 local schedule="$1"
local command=$2 local command="$2"
local name=$3 local name="$3"
local id=$4 local id="$4"
fi fi
local api=$( local api=$(
@@ -73,8 +88,8 @@ update_cron_api() {
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \ --data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "$name -> 更新成功" echo -e "$name -> 更新成功"
else else
@@ -88,8 +103,8 @@ update_cron_command_api() {
local command=$(echo "$1" | awk -F ":" '{print $1}') local command=$(echo "$1" | awk -F ":" '{print $1}')
local id=$(echo "$1" | awk -F ":" '{print $2}') local id=$(echo "$1" | awk -F ":" '{print $2}')
else else
local command=$1 local command="$1"
local id=$2 local id="$2"
fi fi
local api=$( local api=$(
@@ -105,8 +120,8 @@ update_cron_command_api() {
--data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \ --data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "$command -> 更新成功" echo -e "$command -> 更新成功"
else else
@@ -115,7 +130,7 @@ update_cron_command_api() {
} }
del_cron_api() { del_cron_api() {
local ids=$1 local ids="$1"
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
@@ -130,8 +145,8 @@ del_cron_api() {
--data-raw "[$ids]" \ --data-raw "[$ids]" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "成功" echo -e "成功"
else else
@@ -160,16 +175,16 @@ update_cron() {
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \ --data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code != 200 ]]; then if [[ $code != 200 ]]; then
echo -e "\n## 更新任务状态失败(${message})\n" >>$dir_log/$log_path echo -e "\n## 更新任务状态失败(${message})\n"
fi fi
} }
notify_api() { notify_api() {
local title=$1 local title="$1"
local content=$2 local content="$2"
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \
@@ -184,13 +199,36 @@ notify_api() {
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \ --data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "通知发送成功" echo -e "通知发送成功🎉"
else else
echo -e "通知失败(${message})" echo -e "通知失败(${message})"
fi fi
} }
find_cron_api() {
local params="$1"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/detail?$params&t=$currentTimeStamp" \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--compressed
)
data=$(echo "$api" | jq -r .data)
if [[ $data == 'null' ]]; then
echo -e ""
else
name=$(echo "$api" | jq -r .data.name)
echo -e "$name"
fi
}
get_token get_token
Regular → Executable
+1 -5
View File
@@ -1,9 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
if [[ -z ${BotRepoUrl} ]]; then if [[ -z ${BotRepoUrl} ]]; then
url="https://github.com/SuMaiKaDe/bot.git" url="https://github.com/SuMaiKaDe/bot.git"
repo_path="${dir_repo}/dockerbot" repo_path="${dir_repo}/dockerbot"
@@ -46,6 +42,6 @@ echo -e "\npython3依赖安装成功...\n"
echo -e "4、启动bot程序...\n" echo -e "4、启动bot程序...\n"
make_dir $dir_log/bot make_dir $dir_log/bot
cd $dir_data 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 & nohup python3 -m jbot >$dir_log/bot/nohup.log 2>&1 &
echo -e "bot启动成功...\n" echo -e "bot启动成功...\n"
Regular → Executable
+19 -27
View File
@@ -1,9 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
. $dir_shell/api.sh
reset_env() { reset_env() {
echo -e "---> 1. 开始检测配置文件\n" echo -e "---> 1. 开始检测配置文件\n"
fix_config fix_config
@@ -29,9 +25,7 @@ copy_dep() {
echo -e "---> 通知文件复制完成\n" echo -e "---> 通知文件复制完成\n"
echo -e "---> 2. 复制nginx配置文件\n" echo -e "---> 2. 复制nginx配置文件\n"
cp -fv $nginx_conf /etc/nginx/nginx.conf init_nginx
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
echo -e "---> 配置文件复制完成\n" echo -e "---> 配置文件复制完成\n"
} }
@@ -39,12 +33,12 @@ pm2_log() {
echo -e "---> pm2日志" echo -e "---> pm2日志"
local panelOut="/root/.pm2/logs/panel-out.log" local panelOut="/root/.pm2/logs/panel-out.log"
local panelError="/root/.pm2/logs/panel-error.log" local panelError="/root/.pm2/logs/panel-error.log"
tail -n 100 "$panelOut" tail -n 300 "$panelOut"
tail -n 100 "$panelError" tail -n 300 "$panelError"
} }
check_nginx() { 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" echo -e "=====> 检测nginx服务\n$nginxPid"
if [[ $nginxPid ]]; then if [[ $nginxPid ]]; then
echo -e "\n=====> nginx服务正常\n" echo -e "\n=====> nginx服务正常\n"
@@ -60,9 +54,6 @@ check_ql() {
echo -e "\n=====> 检测面板\n\n$api\n" echo -e "\n=====> 检测面板\n\n$api\n"
if [[ $api =~ "<div id=\"root\"></div>" ]]; then if [[ $api =~ "<div id=\"root\"></div>" ]]; then
echo -e "=====> 面板服务启动正常\n" echo -e "=====> 面板服务启动正常\n"
else
echo -e "=====> 面板服务异常,重置基础环境\n"
reset_env
fi fi
} }
@@ -70,9 +61,8 @@ check_pm2() {
pm2_log pm2_log
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/api/user?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/api/system?t=$currentTimeStamp" \
-H 'Accept: */*' \ -H 'Accept: */*' \
-H "Authorization: Bearer $token" \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \ -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \
-H 'Referer: http://0.0.0.0:5700/crontab' \ -H 'Referer: http://0.0.0.0:5700/crontab' \
-H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \ -H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \
@@ -81,24 +71,26 @@ check_pm2() {
echo -e "\n=====> 检测后台\n\n$api\n" echo -e "\n=====> 检测后台\n\n$api\n"
if [[ $api =~ "{\"code\"" ]]; then if [[ $api =~ "{\"code\"" ]]; then
echo -e "=====> 后台服务启动正常\n" echo -e "=====> 后台服务启动正常\n"
else
echo -e "=====> 后台服务异常,重置基础环境并重启后台\n"
reset_env
fi fi
} }
start_public() {
echo -e "=====> 启动公开服务\n"
pm2 delete public --source-map-support --time &>/dev/null
pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null
}
main() { main() {
echo -e "=====> 开始检测" echo -e "=====> 开始检测"
npm i -g pnpm npm i -g pnpm@8.3.1
pnpm add -g pm2
patch_version patch_version
start_public
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 copy_dep
check_ql check_ql
check_nginx check_nginx
Regular → Executable
+16 -57
View File
@@ -1,16 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
. $dir_shell/api.sh
trap "single_hanle" 2 20 15 14
single_hanle() {
handle_task_after "$@"
exit 1
}
random_delay() { random_delay() {
local random_delay_max=$RandomDelay local random_delay_max=$RandomDelay
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
@@ -35,7 +24,7 @@ random_delay() {
done done
local delay_second=$(($(gen_random_num "$random_delay_max") + 1)) local delay_second=$(($(gen_random_num "$random_delay_max") + 1))
echo -e "\n命令未添加 \"now\"随机延迟 $delay_second后再执行任务,如需立即终止,请按 CTRL+C...\n" echo -e "任务随机延迟 $delay_second,配置文件参数 RandomDelay 置空可取消延迟 \n"
sleep $delay_second sleep $delay_second
fi fi
} }
@@ -49,7 +38,7 @@ gen_array_scripts() {
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
let i++ let i++
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g") array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1) array_scripts_name[i]=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
[[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>" [[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
fi fi
done done
@@ -81,56 +70,26 @@ run_nohup() {
} }
check_server() { check_server() {
local top_result=$(top -b -n 1) if [[ $cpu_warn ]] && [[ $mem_warn ]] && [[ $disk_warn ]]; then
cpu_use=$(echo "$top_result" | grep CPU | grep -v -E 'grep|PID' | awk '{print $2}' | cut -f 1 -d "%" | head -n 1) local top_result=$(top -b -n 1)
cpu_use=$(echo "$top_result" | grep CPU | grep -v -E 'grep|PID' | awk '{print $2}' | cut -f 1 -d "%" | head -n 1)
mem_free=$(free -m | grep "Mem" | awk '{print $3}' | head -n 1) mem_free=$(free -m | grep "Mem" | awk '{print $3}' | head -n 1)
mem_total=$(free -m | grep "Mem" | awk '{print $2}' | head -n 1) mem_total=$(free -m | grep "Mem" | awk '{print $2}' | head -n 1)
mem_use=$(printf "%d%%" $((mem_free * 100 / mem_total)) | cut -f 1 -d "%" | head -n 1) mem_use=$(printf "%d%%" $((mem_free * 100 / mem_total)) | cut -f 1 -d "%" | head -n 1)
disk_use=$(df -P | grep /dev | grep -v -E '(tmp|boot|shm)' | awk '{print $5}' | cut -f 1 -d "%" | head -n 1) disk_use=$(df -P | grep /dev | grep -v -E '(tmp|boot|shm)' | awk '{print $5}' | cut -f 1 -d "%" | head -n 1)
if [[ $cpu_use -gt $cpu_warn ]] && [[ $cpu_warn ]] || [[ $mem_free -lt $mem_warn ]] || [[ $disk_use -gt $disk_warn ]]; then
if [[ $cpu_use -gt $cpu_warn ]] || [[ $mem_free -lt $mem_warn ]] || [[ $disk_use -gt $disk_warn ]]; then local resource=$(echo "$top_result" | grep -v -E 'grep|Mem|idle|Load|tr' | awk '{$2="";$3="";$4="";$5="";$7="";print $0}' | head -n 10 | tr '\n' '|' | sed s/\|/\\\\n/g)
local resource=$(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"
notify_api "服务器资源异常警告" "当前CPU占用 $cpu_use% 内存占用 $mem_use% 磁盘占用 $disk_use% \n资源占用详情 \n\n $resource" fi
fi fi
} }
handle_task_before() {
begin_time=$(format_time "$time_format" "$time")
begin_timestamp=$(format_timestamp "$time_format" "$time")
echo -e "## 开始执行... $begin_time\n"
[[ $is_macos -eq 0 ]] && check_server
if [[ -s $task_error_log_path ]]; then
eval cat $task_error_log_path $cmd
eval echo -e "加载 config.sh 出错,请手动检查" $cmd
eval echo $cmd
fi
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
. $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))
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time"
echo -e "\n     "
}
## 正常运行单个脚本,$1:传入参数 ## 正常运行单个脚本,$1:传入参数
run_normal() { run_normal() {
local file_param=$1 local file_param=$1
if [[ $# -eq 1 ]]; then if [[ $# -eq 1 ]] && [[ "$real_time" != "true" ]]; then
random_delay "$file_param" random_delay "$file_param"
fi fi
@@ -171,7 +130,7 @@ run_concurrent() {
local envs=$(eval echo "\$${env_param}") local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g')) local array=($(echo $envs | sed 's/&/ /g'))
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N") single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%3N")
cd $dir_scripts cd $dir_scripts
local relative_path="${file_param%/*}" local relative_path="${file_param%/*}"
@@ -234,7 +193,7 @@ run_else() {
local relative_path="${file_param%/*}" local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path} cd ${relative_path}
file_param=${file_param/$relative_path\//} file_param=${file_param/$relative_path\//.\/}
fi fi
shift shift
Regular → Executable
+2 -2
View File
@@ -11,9 +11,9 @@ echo -e "提交master代码"
git push git push
echo -e "更新cdn文件" echo -e "更新cdn文件"
ts-node sample/tool.ts tsx sample/tool.ts
string=$(cat src/version.ts | grep "version" | egrep "[^\']*" -o | egrep "\d\.*") string=$(cat version.yaml | grep "version" | egrep "[^ ]*" -o | egrep "\d\.*")
version="v$string" version="v$string"
echo -e "当前版本$version" echo -e "当前版本$version"
+12 -6
View File
@@ -1,12 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
days=$1 days=$1
## 删除运行js脚本的旧日志 ## 删除运行脚本的旧日志
remove_js_log() { remove_js_log() {
local log_full_path_list=$(find $dir_log/ -name "*.log") local log_full_path_list=$(find $dir_log/ -name "*.log")
local diff_time local diff_time
@@ -18,7 +14,17 @@ remove_js_log() {
else else
diff_time=$(($(date +%s) - $(date +%s -d "$log_date"))) diff_time=$(($(date +%s) - $(date +%s -d "$log_date")))
fi fi
[[ $diff_time -gt $((${days} * 86400)) ]] && rm -vf $log if [[ $diff_time -gt $((${days} * 86400)) ]]; then
local log_path=$(echo "$log" | sed "s,${dir_log}/,,g")
local result=$(find_cron_api "log_path=$log_path")
echo -e "查询文件 $log_path"
if [[ -z $result ]]; then
echo -e "删除中~"
rm -vf $log
else
echo -e "正在被 $result 使用,跳过~"
fi
fi
fi fi
done done
} }
+78 -112
View File
@@ -2,6 +2,7 @@
## 目录 ## 目录
dir_root=$QL_DIR dir_root=$QL_DIR
dir_tmp=$dir_root/.tmp
dir_data=$dir_root/data dir_data=$dir_root/data
dir_shell=$dir_root/shell dir_shell=$dir_root/shell
dir_sample=$dir_root/sample dir_sample=$dir_root/sample
@@ -18,6 +19,7 @@ dir_update_log=$dir_log/update
ql_static_repo=$dir_repo/static ql_static_repo=$dir_repo/static
## 文件 ## 文件
file_ecosystem_js=$dir_root/ecosystem.config.js
file_config_sample=$dir_sample/config.sample.sh file_config_sample=$dir_sample/config.sample.sh
file_env=$dir_config/env.sh file_env=$dir_config/env.sh
file_sharecode=$dir_config/sharecode.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_sample=$dir_sample/notify.py
file_notify_py=$dir_scripts/notify.py file_notify_py=$dir_scripts/notify.py
file_notify_js=$dir_scripts/sendNotify.js file_notify_js=$dir_scripts/sendNotify.js
task_error_log_path=$dir_log/task_error.log
nginx_app_conf=$dir_root/docker/front.conf nginx_app_conf=$dir_root/docker/front.conf
nginx_conf=$dir_root/docker/nginx.conf nginx_conf=$dir_root/docker/nginx.conf
dep_notify_py=$dir_dep/notify.py dep_notify_py=$dir_dep/notify.py
@@ -67,8 +68,8 @@ import_config() {
[[ -f $file_config_user ]] && . $file_config_user [[ -f $file_config_user ]] && . $file_config_user
[[ -f $file_env ]] && . $file_env [[ -f $file_env ]] && . $file_env
ql_base_url=${QlBaseUrl:-""} ql_base_url=${QlBaseUrl:-"/"}
command_timeout_time=${CommandTimeoutTime:-"1h"} command_timeout_time=${CommandTimeoutTime:-""}
proxy_url=${ProxyUrl:-""} proxy_url=${ProxyUrl:-""}
file_extensions=${RepoFileExtensions:-"js py"} file_extensions=${RepoFileExtensions:-"js py"}
current_branch=${QL_BRANCH} current_branch=${QL_BRANCH}
@@ -79,9 +80,9 @@ import_config() {
default_cron="$(random_range 0 59) $(random_range 0 23) * * *" default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
fi fi
cpu_warn=${CpuWarn:-80} cpu_warn=${CpuWarn}
mem_warn=${MemoryWarn:-80} mem_warn=${MemoryWarn}
disk_warn=${DiskWarn:-90} disk_warn=${DiskWarn}
} }
set_proxy() { set_proxy() {
@@ -98,6 +99,9 @@ set_proxy() {
unset_proxy() { unset_proxy() {
unset http_proxy unset http_proxy
unset https_proxy unset https_proxy
unset ftp_proxy
unset all_proxy
unset no_proxy
} }
make_dir() { make_dir() {
@@ -172,6 +176,7 @@ define_cmd() {
} }
fix_config() { fix_config() {
make_dir $dir_tmp
make_dir $dir_static make_dir $dir_static
make_dir $dir_data make_dir $dir_data
make_dir $dir_config make_dir $dir_config
@@ -260,17 +265,7 @@ npm_install_sub() {
else else
pnpm install --loglevel error --production pnpm install --loglevel error --production
fi fi
} exit_status=$?
npm_install_1() {
local dir_current=$(pwd)
local dir_work=$1
cd $dir_work
echo -e "运行 npm install...\n"
npm_install_sub
[[ $? -ne 0 ]] && echo -e "\nnpm install 运行不成功,请进入 $dir_work 目录后手动运行 npm install...\n"
cd $dir_current
} }
npm_install_2() { npm_install_2() {
@@ -278,12 +273,8 @@ npm_install_2() {
local dir_work=$1 local dir_work=$1
cd $dir_work cd $dir_work
echo -e "检测到 $dir_work 依赖包有变化,运行 npm install...\n" echo -e "安装 $dir_work 依赖包...\n"
npm_install_sub npm_install_sub
if [[ $? -ne 0 ]]; then
echo -e "\n安装 $dir_work 的依赖包运行不成功,再次尝试一遍...\n"
npm_install_1 $dir_work
fi
cd $dir_current cd $dir_current
} }
@@ -295,77 +286,22 @@ diff_and_copy() {
fi fi
} }
update_depend() {
local dir_current=$(pwd)
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts
fi
cd $dir_current
}
git_clone_scripts() { git_clone_scripts() {
local url="$1" local url="$1"
local dir="$2" local dir="$2"
local branch="$3" local branch="$3"
local proxy="$4" local proxy="$4"
[[ $branch ]] && local part_cmd="-b $branch " [[ $branch ]] && local part_cmd="-b $branch "
echo -e "开始克隆仓库 $url$dir\n" echo -e "开始拉取仓库 ${uniq_path}$dir\n"
set_proxy "$proxy" set_proxy "$proxy"
git clone $part_cmd $url $dir
git clone --depth=1 $part_cmd $url $dir
exit_status=$? exit_status=$?
unset_proxy unset_proxy
} }
git_pull_scripts() {
local dir_current=$(pwd)
local dir_work="$1"
local branch="$2"
local proxy="$3"
cd $dir_work
echo -e "开始更新仓库:$dir_work\n"
set_proxy "$proxy"
git fetch --all
exit_status=$?
git pull &>/dev/null
unset_proxy
cd $dir_current
}
reset_romote_url() {
local dir_current=$(pwd)
local dir_work=$1
local url=$2
local branch="$3"
cd $dir_work
if [[ -d "$dir_work/.git" ]]; then
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
git remote set-url origin $url &>/dev/null
else
git init
git remote add origin $url &>/dev/null
fi
reset_branch "$branch"
cd $dir_current
}
reset_branch() {
local branch="$1"
local part_cmd=""
if [[ $branch ]]; then
part_cmd="origin/${branch}"
git checkout -B "$branch" &>/dev/null
git branch --set-upstream-to=$part_cmd $branch &>/dev/null
fi
git reset --hard $part_cmd &>/dev/null
}
random_range() { random_range() {
local beg=$1 local beg=$1
local end=$2 local end=$2
@@ -373,15 +309,12 @@ random_range() {
} }
reload_pm2() { reload_pm2() {
pm2 l &>/dev/null cd $dir_root
# 代理会影响 grpc 服务
echo -e "启动面板服务\n" unset_proxy
pm2 delete panel --source-map-support --time &>/dev/null pm2 flush &>/dev/null
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null pm2 startOrGracefulReload $file_ecosystem_js --update-env
pm2 sendSignal SIGKILL panel &>/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
} }
diff_time() { diff_time() {
@@ -413,9 +346,9 @@ format_log_time() {
local time="$2" local time="$2"
if [[ $is_macos -eq 1 ]]; then if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S") echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S-%3N")
else else
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S") echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S-%3N")
fi fi
} }
@@ -431,17 +364,9 @@ format_timestamp() {
} }
patch_version() { patch_version() {
if [[ $PipMirror ]]; then git config --global pull.rebase false
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
npm config set registry $NpmMirror
fi
# 兼容pnpm@7 cp -f $dir_root/.env.example $dir_root/.env
pnpm setup &>/dev/null
source ~/.bashrc
pnpm install -g &>/dev/null
if [[ -f "$dir_root/db/cookie.db" ]]; then if [[ -f "$dir_root/db/cookie.db" ]]; then
echo -e "检测到旧的db文件,拷贝为新db...\n" echo -e "检测到旧的db文件,拷贝为新db...\n"
@@ -450,14 +375,6 @@ patch_version() {
echo echo
fi fi
if ! type ts-node &>/dev/null; then
pnpm add -g ts-node typescript tslib
fi
git config --global pull.rebase false
cp -f $dir_root/.env.example $dir_root/.env
if [[ -d "$dir_root/db" ]]; then if [[ -d "$dir_root/db" ]]; then
echo -e "检测到旧的db目录,拷贝到data目录...\n" echo -e "检测到旧的db目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data cp -rf $dir_root/config $dir_root/data
@@ -483,10 +400,59 @@ patch_version() {
fi fi
} }
init_nginx() {
cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
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 init_env
detect_termux detect_termux
detect_macos detect_macos
define_cmd define_cmd
fix_config
import_config $1 2>$task_error_log_path import_config $1
+54 -12
View File
@@ -5,6 +5,12 @@ dir_shell=$QL_DIR/shell
. $dir_shell/share.sh . $dir_shell/share.sh
. $dir_shell/api.sh . $dir_shell/api.sh
trap "single_hanle" 2 3 20 15 14
single_hanle() {
eval handle_task_after "$@" "$cmd"
exit 1
}
## 选择python3还是node ## 选择python3还是node
define_program() { define_program() {
local file_param=$1 local file_param=$1
@@ -15,7 +21,11 @@ define_program() {
elif [[ $file_param == *.sh ]]; then elif [[ $file_param == *.sh ]]; then
which_program="bash" which_program="bash"
elif [[ $file_param == *.ts ]]; then elif [[ $file_param == *.ts ]]; then
which_program="ts-node-transpile-only" if ! type tsx &>/dev/null; then
which_program="ts-node-transpile-only"
else
which_program="tsx"
fi
else else
which_program="" which_program=""
fi fi
@@ -24,16 +34,24 @@ define_program() {
handle_log_path() { handle_log_path() {
local file_param=$1 local file_param=$1
if [[ -z $file_param ]];then if [[ -z $file_param ]]; then
file_param="task" file_param="task"
fi fi
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="" local suffix=""
if [[ ! -z $ID ]]; then if [[ ! -z $ID ]]; then
suffix="_${ID}" if [[ "$ID" -gt 0 ]] 2>/dev/null; then
suffix="_${ID}"
else
ID=""
fi
fi fi
time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time") time=$(date "+$mtime_format")
log_time=$(format_log_time "$mtime_format" "$time")
log_dir_tmp="${file_param##*/}" log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then if [[ $file_param == /* ]]; then
@@ -47,18 +65,41 @@ handle_log_path() {
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}" [[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}${suffix}" log_dir="${log_dir_tmp%.*}${suffix}"
log_path="$log_dir/$log_time.log" log_path="$log_dir/$log_time.log"
if [[ $real_log_path ]]; then
log_path="$real_log_path"
fi
cmd=">> $dir_log/$log_path 2>&1" cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir" 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
} }
format_params() { format_params() {
time_format="%Y-%m-%d %H:%M:%S" time_format="%Y-%m-%d %H:%M:%S"
timeoutCmd="" if [[ $is_macos -eq 1 ]]; then
if type timeout &>/dev/null; then mtime_format=$time_format
timeoutCmd="timeout --foreground -s 14 -k 10s $command_timeout_time " else
mtime_format="%Y-%m-%d %H:%M:%S.%3N"
fi fi
params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g') timeoutCmd=""
if [[ $command_timeout_time ]]; then
if type timeout &>/dev/null; then
timeoutCmd="timeout --foreground -s 2 -k 10s $command_timeout_time "
fi
fi
# params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g')
}
init_begin_time() {
begin_time=$(format_time "$time_format" "$time")
begin_timestamp=$(format_timestamp "$time_format" "$time")
} }
while getopts ":lm:" opt; do while getopts ":lm:" opt; do
@@ -80,8 +121,9 @@ fi
format_params "$@" format_params "$@"
define_program "$@" define_program "$@"
handle_log_path "$@" handle_log_path "$@"
init_begin_time
eval . $dir_shell/otask.sh "$params" "$cmd" eval . $dir_shell/otask.sh "$cmd"
[[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path" [[ -f "$dir_log/$log_path" ]] && [[ ! $show_log ]] && [[ "$real_time" != "true" ]] && cat "$dir_log/$log_path"
exit 0 exit 0
+159 -132
View File
@@ -26,39 +26,12 @@ diff_cron() {
fi fi
} }
## 检测配置文件版本
detect_config_version() {
## 识别出两个文件的版本号
ver_config_sample=$(grep " Version: " $file_config_sample | perl -pe "s|.+v((\d+\.?){3})|\1|")
[[ -f $file_config_user ]] && ver_config_user=$(grep " Version: " $file_config_user | perl -pe "s|.+v((\d+\.?){3})|\1|")
## 删除旧的发送记录文件
[[ -f $send_mark ]] && [[ $(cat $send_mark) != $ver_config_sample ]] && rm -f $send_mark
## 识别出更新日期和更新内容
update_date=$(grep " Date: " $file_config_sample | awk -F ": " '{print $2}')
update_content=$(grep " Update Content: " $file_config_sample | awk -F ": " '{print $2}')
## 如果是今天,并且版本号不一致,则发送通知
if [[ -f $file_config_user ]] && [[ $ver_config_user != $ver_config_sample ]] && [[ $update_date == $(date "+%Y-%m-%d") ]]; then
if [[ ! -f $send_mark ]]; then
local notify_title="配置文件更新通知"
local notify_content="更新日期: $update_date\n用户版本: $ver_config_user\n新的版本: $ver_config_sample\n更新内容: $update_content\n更新说明: 如需使用新功能请对照config.sample.sh,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。\n"
echo -e $notify_content
notify_api "$notify_title" "$notify_content"
[[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark
fi
else
[[ -f $send_mark ]] && rm -f $send_mark
fi
}
## 输出是否有新的或失效的定时任务,$1:新的或失效的任务清单文件路径,$2:新/失效 ## 输出是否有新的或失效的定时任务,$1:新的或失效的任务清单文件路径,$2:新/失效
output_list_add_drop() { output_list_add_drop() {
local list=$1 local list=$1
local type=$2 local type=$2
if [[ -s $list ]]; then if [[ -s $list ]]; then
echo -e "检测到有$type的定时任务\n" echo -e "检测到有${type}的定时任务:\n"
cat $list cat $list
echo echo
fi fi
@@ -71,9 +44,9 @@ del_cron() {
local path=$2 local path=$2
local detail="" local detail=""
local ids="" local ids=""
echo -e "开始尝试自动删除失效的定时任务...\n" echo -e "开始尝试自动删除失效的定时任务..."
for cron in $(cat $list_drop); do for cron in $(cat $list_drop); do
local id=$(cat $list_crontab_user | grep -E "$cmd_task $cron" | perl -pe "s|.*ID=(.*) $cmd_task $cron\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}') local id=$(cat $list_crontab_user | grep -E "$cmd_task.* $cron" | perl -pe "s|.*ID=(.*) $cmd_task.* $cron\.*|\1|" | head -1 | awk -F " " '{print $1}')
if [[ $ids ]]; then if [[ $ids ]]; then
ids="$ids,\"$id\"" ids="$ids,\"$id\""
else else
@@ -81,7 +54,7 @@ del_cron() {
fi fi
cron_file="$dir_scripts/${cron}" cron_file="$dir_scripts/${cron}"
if [[ -f $cron_file ]]; then if [[ -f $cron_file ]]; then
cron_name=$(grep "new Env" $cron_file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1) cron_name=$(grep "new Env" $cron_file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
rm -f $cron_file rm -f $cron_file
fi fi
[[ -z $cron_name ]] && cron_name="$cron" [[ -z $cron_name ]] && cron_name="$cron"
@@ -102,7 +75,7 @@ del_cron() {
add_cron() { add_cron() {
local list_add=$1 local list_add=$1
local path=$2 local path=$2
echo -e "开始尝试自动添加定时任务...\n" echo -e "开始尝试自动添加定时任务..."
local detail="" local detail=""
cd $dir_scripts cd $dir_scripts
for file in $(cat $list_add); do for file in $(cat $list_add); do
@@ -119,12 +92,12 @@ add_cron() {
s| | |g; s| | |g;
}" | sort -u | head -1 }" | sort -u | head -1
) )
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1) cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
[[ -z $cron_name ]] && cron_name="$file_name" [[ -z $cron_name ]] && cron_name="$file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron" [[ -z $cron_line ]] && cron_line="$default_cron"
result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name") result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name:$SUB_ID")
echo -e "$result" echo -e "$result"
if [[ $detail ]]; then if [[ $detail ]]; then
detail="${detail}${result}\n" detail="${detail}${result}\n"
@@ -145,6 +118,8 @@ update_repo() {
local branch="$5" local branch="$5"
local extensions="$6" local extensions="$6"
local proxy="$7" local proxy="$7"
local autoAddCron="$8"
local autoDelCron="$9"
local tmp="${url%/*}" local tmp="${url%/*}"
local authorTmp1="${tmp##*/}" local authorTmp1="${tmp##*/}"
local authorTmp2="${authorTmp1##*:}" local authorTmp2="${authorTmp1##*:}"
@@ -155,16 +130,12 @@ update_repo() {
make_dir "${dir_scripts}/${uniq_path}" make_dir "${dir_scripts}/${uniq_path}"
local formatUrl="$url" local formatUrl="$url"
if [[ -d ${repo_path}/.git ]]; then rm -rf ${repo_path} &>/dev/null
reset_romote_url ${repo_path} "${formatUrl}" "${branch}" git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" "${proxy}"
git_pull_scripts ${repo_path} "${branch}" "${proxy}"
else
rm -rf ${repo_path} &>/dev/null
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" "${proxy}"
fi
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新${repo_path}成功...\n" echo -e "\n更新${repo_path}成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" "$autoAddCron" "$autoDelCron"
else else
echo -e "\n更新${repo_path}失败,请检查网络...\n" echo -e "\n更新${repo_path}失败,请检查网络...\n"
fi fi
@@ -172,41 +143,58 @@ update_repo() {
## 更新所有 raw 文件 ## 更新所有 raw 文件
update_raw() { update_raw() {
echo -e "--------------------------------------------------------------\n"
local url="$1" local url="$1"
local proxy="$2"
local autoAddCron="$3"
local autoDelCron="$4"
if [[ ! $autoAddCron ]]; then
autoAddCron=${AutoAddCron}
fi
if [[ ! $autoDelCron ]]; then
autoDelCron=${AutoDelCron}
fi
local proxyStr=""
if [[ $proxy ]]; then
if [[ $url == http:* ]]; then
proxyStr="-e \"http_proxy=${proxy}\""
elif [[ $url == https:* ]]; then
proxyStr="-e \"http_proxy=${proxy};https_proxy=${proxy}\""
fi
fi
local raw_url="$url" local raw_url="$url"
local suffix="${raw_url##*.}" local suffix="${raw_url##*.}"
local raw_file_name="${uniq_path}.${suffix}" local raw_file_name="${uniq_path}.${suffix}"
echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n" echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
set_proxy wget -q --no-check-certificate $proxyStr -O "$dir_raw/${raw_file_name}.new" ${raw_url}
wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
unset_proxy
if [[ $? -eq 0 ]]; then if [[ $? -eq 0 ]]; then
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}" mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
echo -e "下载 ${raw_file_name} 成功...\n" echo -e "下载 ${raw_file_name} 成功...\n"
cd $dir_raw cd $dir_raw
local filename="raw_${raw_file_name}" local filename="raw_${raw_file_name}"
local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* $filename" | perl -pe "s|.*ID=(.*) $cmd_task.* $filename\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}') local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* $filename" | perl -pe "s|.*ID=(.*) $cmd_task.* $filename\.*|\1|" | head -1 | awk -F " " '{print $1}')
cp -f $raw_file_name $dir_scripts/${filename} cp -f $raw_file_name $dir_scripts/${filename}
cron_line=$( if [[ -z $cron_id ]] && [[ ${autoAddCron} == true ]]; then
perl -ne "{ cron_line=$(
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$raw_file_name/ perl -ne "{
}" $raw_file_name | print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$raw_file_name/
perl -pe "{ }" $raw_file_name |
s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$raw_file_name.*|\1|g; perl -pe "{
s|\*([\d\*])(.*)|\1\2|g; s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$raw_file_name.*|\1|g;
s| | |g; s|\*([\d\*])(.*)|\1\2|g;
}" | sort -u | head -1 s| | |g;
) }" | sort -u | head -1
cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1) )
[[ -z $cron_name ]] && cron_name="$raw_file_name" cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs) [[ -z $cron_name ]] && cron_name="$raw_file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron" [[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
if [[ -z $cron_id ]]; then [[ -z $cron_line ]] && cron_line="$default_cron"
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name") result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name:$SUB_ID")
echo -e "$result\n" echo -e "$result\n"
notify_api "新增任务通知" "\n$result" notify_api "新增任务通知" "\n$result"
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id" # update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
@@ -220,13 +208,10 @@ update_raw() {
## 调用用户自定义的extra.sh ## 调用用户自定义的extra.sh
run_extra_shell() { run_extra_shell() {
if [[ ${EnableExtraShell} == true ]]; then if [[ -f $file_extra_shell ]]; then
if [[ -f $file_extra_shell ]]; then . $file_extra_shell
echo -e "--------------------------------------------------------------\n" else
. $file_extra_shell echo -e "$file_extra_shell文件不存在,跳过执行...\n"
else
echo -e "$file_extra_shell文件不存在,跳过执行...\n"
fi
fi fi
} }
@@ -244,66 +229,98 @@ usage() {
echo -e "9. $cmd_update resettfa # 禁用两步登录" echo -e "9. $cmd_update resettfa # 禁用两步登录"
} }
reload_qinglong() {
local reload_target="${1}"
local primary_branch="master"
if [[ "${QL_BRANCH}" == "develop" ]]; then
primary_branch="develop"
fi
if [[ "$reload_target" == 'system' ]]; then
cp -rf ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/
rm -rf $dir_static/*
cp -rf ${dir_tmp}/qinglong-static-${primary_branch}/* ${dir_static}/
cp -f $file_config_sample $dir_config/config.sample.sh
fi
if [[ "$reload_target" == 'data' ]]; then
cp -rf ${dir_tmp}/data ${dir_root}/
fi
reload_pm2
}
## 更新qinglong ## 更新qinglong
update_qinglong() { update_qinglong() {
local mirror="github" rm -rf ${dir_tmp}/*
local githubStatus=$(curl -s -m 2 -IL "https://github.com" | grep 200) local mirror="gitee"
if [ "$githubStatus" == "" ]; then local downloadQLUrl="https://gitee.com/whyour/qinglong/repository/archive"
mirror="gitee" local downloadStaticUrl="https://gitee.com/whyour/qinglong-static/repository/archive"
local githubStatus=$(curl -s -m 2 -IL "https://google.com" | grep 200)
if [[ ! -z $githubStatus ]]; then
mirror="github"
downloadQLUrl="https://github.com/whyour/qinglong/archive/refs/heads"
downloadStaticUrl="https://github.com/whyour/qinglong-static/archive/refs/heads"
fi fi
echo -e "\n使用 ${mirror} 源更新...\n" echo -e "使用 ${mirror} 源更新...\n"
export isFirstStartServer=false
local all_branch=$(cd ${dir_root} && git branch -a)
local primary_branch="master" local primary_branch="master"
if [[ "${all_branch}" =~ "${current_branch}" ]]; then if [[ "${QL_BRANCH}" == "develop" ]]; then
primary_branch="${current_branch}" primary_branch="develop"
fi fi
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
reset_romote_url ${dir_root} "https://${mirror}.com/whyour/qinglong.git" ${primary_branch} wget -cqO "${dir_tmp}/ql.zip" "${downloadQLUrl}/${primary_branch}.zip"
git_pull_scripts $dir_root ${primary_branch} exit_status=$?
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙源文件成功...\n" echo -e "\n更新青龙源文件成功...\n"
cp -f $file_config_sample $dir_config/config.sample.sh
detect_config_version
update_depend
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json) unzip -oq ${dir_tmp}/ql.zip -d ${dir_tmp}
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
update_qinglong_static "$1" "$primary_branch" update_qinglong_static
else else
echo -e "\n更新青龙源文件失败,请检查网络...\n" echo -e "\n更新青龙源文件失败,请检查网络...\n"
fi fi
} }
update_qinglong_static() { update_qinglong_static() {
local no_restart="$1" wget -cqO "${dir_tmp}/static.zip" "${downloadStaticUrl}/${primary_branch}.zip"
local primary_branch="$2" exit_status=$?
local url="https://${mirror}.com/whyour/qinglong-static.git"
if [[ -d ${ql_static_repo}/.git ]]; then
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
git_pull_scripts ${ql_static_repo} ${primary_branch}
else
rm -rf ${ql_static_repo} &>/dev/null
git_clone_scripts ${url} ${ql_static_repo} ${primary_branch}
fi
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙静态资源成功...\n" echo -e "\n更新青龙静态资源成功...\n"
local static_version=$(cat $dir_root/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1) unzip -oq ${dir_tmp}/static.zip -d ${dir_tmp}
echo -e "\n当前版本 $static_version...\n"
check_update_dep
else
echo -e "\n更新青龙静态资源失败,请检查网络...\n"
fi
}
check_update_dep() {
echo -e "\n开始检测依赖...\n"
if [[ $(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
if [[ $(diff $dir_root/package.json ${dir_tmp}/qinglong-${primary_branch}/package.json) ]]; then
npm_install_2 "${dir_tmp}/qinglong-${primary_branch}"
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n依赖检测安装成功...\n"
echo -e "\n更新包下载成功...\n"
if [[ "$needRestart" == 'true' ]]; then
cp -rf ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/
rm -rf $dir_static/*
cp -rf ${dir_tmp}/qinglong-static-${primary_branch}/* ${dir_static}/
cp -f $file_config_sample $dir_config/config.sample.sh
rm -rf $dir_static/*
cp -rf $ql_static_repo/* $dir_static
if [[ $no_restart != "no-restart" ]]; then
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "重启面板中..."
sleep 3
reload_pm2 reload_pm2
fi fi
else else
echo -e "\n更新青龙静态资源失败,请检查网络...\n" echo -e "\n依赖检测安装失败,请检查网络...\n"
fi fi
} }
@@ -316,6 +333,15 @@ diff_scripts() {
local blackword="$4" local blackword="$4"
local dependence="$5" local dependence="$5"
local extensions="$6" local extensions="$6"
local autoAddCron="$7"
local autoDelCron="$8"
if [[ ! $autoAddCron ]]; then
autoAddCron=${AutoAddCron}
fi
if [[ ! $autoDelCron ]]; then
autoDelCron=${AutoDelCron}
fi
gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions"
@@ -325,13 +351,13 @@ diff_scripts() {
if [[ -s $list_drop ]]; then if [[ -s $list_drop ]]; then
output_list_add_drop $list_drop "失效" output_list_add_drop $list_drop "失效"
if [[ ${AutoDelCron} == true ]]; then if [[ ${autoDelCron} == true ]]; then
del_cron $list_drop $uniq_path del_cron $list_drop $uniq_path
fi fi
fi fi
if [[ -s $list_add ]]; then if [[ -s $list_add ]]; then
output_list_add_drop $list_add "新" output_list_add_drop $list_add "新"
if [[ ${AutoAddCron} == true ]]; then if [[ ${autoAddCron} == true ]]; then
add_cron $list_add $uniq_path add_cron $list_add $uniq_path
fi fi
fi fi
@@ -369,10 +395,10 @@ gen_list_repo() {
done done
files=$(eval $cmd | sed 's/^..//') files=$(eval $cmd | sed 's/^..//')
if [[ $path ]]; then if [[ $path ]]; then
files=$(echo "$files" | egrep $path) files=$(echo "$files" | egrep "$path")
fi fi
if [[ $blackword ]]; then if [[ $blackword ]]; then
files=$(echo "$files" | egrep -v $blackword) files=$(echo "$files" | egrep -v "$blackword")
fi fi
cp -f $file_notify_js "${dir_scripts}/${uniq_path}" cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
@@ -380,7 +406,7 @@ gen_list_repo() {
if [[ $dependence ]]; then if [[ $dependence ]]; then
cd ${repo_path} cd ${repo_path}
results=$(eval $cmd | sed 's/^..//' | egrep $dependence) results=$(eval $cmd | sed 's/^..//' | egrep "$dependence")
for _file in ${results}; do for _file in ${results}; do
file_path=$(dirname $_file) file_path=$(dirname $_file)
make_dir "${dir_scripts}/${uniq_path}/${file_path}" make_dir "${dir_scripts}/${uniq_path}/${file_path}"
@@ -393,7 +419,7 @@ gen_list_repo() {
fi fi
for file in ${files}; do for file in ${files}; do
filename=$(basename $file) filename=$(basename "$file")
cp -f $file "$dir_scripts/${uniq_path}/${filename}" cp -f $file "$dir_scripts/${uniq_path}/${filename}"
echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list" echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
# cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task.* ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}') # cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task.* ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
@@ -432,13 +458,16 @@ main() {
done done
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1)) [[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
local p1=$1 local p1="${1}"
local p2=$2 local p2="${2}"
local p3=$3 local p3="${3}"
local p4=$4 local p4="${4}"
local p5=$5 local p5="${5}"
local p6=$6 local p6="${6}"
local p7=$7 local p7="${7}"
local p8="${8}"
local p9="${9}"
local p10="${10}"
local log_dir="${p1}" local log_dir="${p1}"
make_dir "$dir_log/$log_dir" make_dir "$dir_log/$log_dir"
local log_time=$(date "+%Y-%m-%d-%H-%M-%S") local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
@@ -459,12 +488,6 @@ main() {
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
fi fi
if [[ -s $task_error_log_path ]]; then
eval cat $task_error_log_path $cmd
eval echo -e "加载 config.sh 出错,请手动检查" $cmd
eval echo $cmd
fi
if [[ "$show_log" == "true" ]] && [[ $ID ]]; then if [[ "$show_log" == "true" ]] && [[ $ID ]]; then
eval echo -e "请移除 -l 参数" $cmd eval echo -e "请移除 -l 参数" $cmd
exit 1 exit 1
@@ -472,7 +495,12 @@ main() {
case $p1 in case $p1 in
update) update)
eval update_qinglong "$2" $cmd fix_config
local needRestart=${p2:-"true"}
eval update_qinglong $cmd
;;
reload)
eval reload_qinglong "$p2" $cmd
;; ;;
extra) extra)
eval run_extra_shell $cmd eval run_extra_shell $cmd
@@ -480,7 +508,7 @@ main() {
repo) repo)
get_uniq_path "$p2" "$p6" get_uniq_path "$p2" "$p6"
if [[ -n $p2 ]]; then if [[ -n $p2 ]]; then
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" "$p7" "$p8" update_repo "$p2" "$p3" "$p4" "$p5" "$p6" "$p7" "$p8" "$p9" "$p10"
else else
eval echo -e "命令输入错误...\\\n" $cmd eval echo -e "命令输入错误...\\\n" $cmd
eval usage $cmd eval usage $cmd
@@ -489,7 +517,7 @@ main() {
raw) raw)
get_uniq_path "$p2" get_uniq_path "$p2"
if [[ -n $p2 ]]; then if [[ -n $p2 ]]; then
update_raw "$p2" update_raw "$p2" "$p3" "$p4"
else else
eval echo -e "命令输入错误...\\\n" $cmd eval echo -e "命令输入错误...\\\n" $cmd
eval usage $cmd eval usage $cmd
@@ -527,8 +555,7 @@ main() {
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time" [[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time     " $cmd
eval echo -e "\\\n          " $cmd
fi fi
if [[ -f $file_path ]]; then if [[ -f $file_path ]]; then
+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'; import { createFromIconfontCN } from '@ant-design/icons';
const IconFont = createFromIconfontCN({ const IconFont = createFromIconfontCN({
scriptUrl: ['//at.alicdn.com/t/c/font_3354854_z0d9rbri1ci.js'], scriptUrl: ['//at.alicdn.com/t/c/font_3354854_ob5y15ewlyq.js'],
}); });
export default IconFont; export default IconFont;
+404
View File
@@ -0,0 +1,404 @@
import React, {
useRef,
useEffect,
useContext,
createContext,
useReducer,
useState,
useMemo,
} from 'react';
import { throttle, isNumber, debounce } from 'lodash';
const initialState = {
// 行高度
rowHeight: 0,
// 当前的scrollTop
curScrollTop: 0,
// 总行数
totalLen: 0,
};
function reducer(state, action) {
const { curScrollTop, totalLen, ifScrollTopClear, scrollTop } = action;
let stateScrollTop = state.curScrollTop;
switch (action.type) {
// 改变trs 即 改变渲染的列表trs
case 'changeTrs':
return {
...state,
curScrollTop,
};
// 更改totalLen
case 'changeTotalLen':
if (totalLen === 0) {
stateScrollTop = 0;
}
return {
...state,
totalLen,
curScrollTop: stateScrollTop,
};
case 'reset':
return {
...state,
curScrollTop: ifScrollTopClear ? 0 : scrollTop ?? state.curScrollTop,
};
default:
throw new Error();
}
}
// ==============全局常量 ================== //
const DEFAULT_VID = 'vtable';
const vidMap = new Map();
let preData = 0;
// ===============context ============== //
const ScrollContext = createContext({
dispatch: undefined,
renderLen: 1,
start: 0,
offsetStart: 0,
// =============
rowHeight: initialState.rowHeight,
totalLen: 0,
vid: DEFAULT_VID,
});
// =============组件 =================== //
function VCell(props: any): JSX.Element {
const { children, ...restProps } = props;
return (
<td {...restProps}>
<div>{children[1]}</div>
</td>
);
}
function VRow(props: any, ref: any): JSX.Element {
const { rowHeight } = useContext(ScrollContext);
const { children, style, ...restProps } = props;
const trRef = useRef<HTMLTableRowElement>(null);
return (
<tr
{...restProps}
ref={Object.prototype.hasOwnProperty.call(ref, 'current') ? ref : trRef}
style={{
...style,
height: rowHeight || 'auto',
boxSizing: 'border-box',
}}
>
{children}
</tr>
);
}
function VWrapper(props: any): JSX.Element {
const { children, ...restProps } = props;
const { renderLen, start, dispatch, vid, totalLen } =
useContext(ScrollContext);
const contents = useMemo(() => {
return children[1];
}, [children]);
const contentsLen = useMemo(() => {
return contents?.length ?? 0;
}, [contents]);
useEffect(() => {
if (totalLen !== contentsLen) {
dispatch({
type: 'changeTotalLen',
totalLen: contentsLen ?? 0,
});
}
}, [contentsLen, dispatch, vid, totalLen]);
let tempNode = null;
if (Array.isArray(contents) && contents.length) {
tempNode = [
children[0],
contents.slice(start, start + (renderLen ?? 1)).map((item) => {
if (Array.isArray(item)) {
// 兼容antd v4.3.5 --- rc-table 7.8.1及以下
return item[0];
}
// 处理antd ^v4.4.0 --- rc-table ^7.8.2
return item;
}),
];
} else {
tempNode = children;
}
return <tbody {...restProps}>{tempNode}</tbody>;
}
function VTable(props: any, otherParams): JSX.Element {
const { style, children, ...rest } = props;
const { width, ...rest_style } = style;
const { vid, scrollY, resetScrollTopWhenDataChange, rowHeight, scrollTop } =
otherParams ?? {};
const [state, dispatch] = useReducer(reducer, {
...initialState,
curScrollTop: scrollTop,
rowHeight,
});
const wrap_tableRef = useRef<HTMLDivElement>(null);
const tableRef = useRef<HTMLTableElement>(null);
const ifChangeRef = useRef(false);
// 数据的总条数
const [totalLen, setTotalLen] = useState<number>(
children[1]?.props?.data?.length ?? 0,
);
useEffect(() => {
setTotalLen(state.totalLen);
}, [state.totalLen]);
// 组件卸载的清除操作
useEffect(() => {
return () => {
vidMap.delete(vid);
};
}, [vid]);
// 数据变更
useEffect(() => {
ifChangeRef.current = true;
// console.log('数据变更')
if (isNumber(children[1]?.props?.data?.length)) {
dispatch({
type: 'changeTotalLen',
totalLen: children[1]?.props?.data?.length ?? 0,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [children[1].props.data]);
// table总高度
const tableHeight = useMemo<string | number>(() => {
let temp: string | number = 'auto';
if (rowHeight && totalLen) {
temp = rowHeight * totalLen;
}
return temp;
}, [totalLen]);
// table的scrollY值
const [tableScrollY, setTableScrollY] = useState(0);
// tableScrollY 随scrollY / tableHeight 进行变更
useEffect(() => {
let temp = 0;
if (typeof scrollY === 'string') {
temp =
(wrap_tableRef.current?.parentNode as HTMLElement)?.offsetHeight ?? 0;
} else {
temp = scrollY;
}
// if (isNumber(tableHeight) && tableHeight < temp) {
// temp = tableHeight;
// }
// 处理tableScrollY <= 0的情况
if (temp <= 0) {
temp = 0;
}
setTableScrollY(temp);
}, [scrollY, tableHeight]);
// 渲染的条数
const renderLen = useMemo<number>(() => {
let temp = 1;
if (rowHeight && totalLen && tableScrollY) {
if (tableScrollY <= 0) {
temp = 0;
} else {
const tempRenderLen = ((tableScrollY / rowHeight) | 0) + 10;
// console.log('tempRenderLen', tempRenderLen)
// temp = tempRenderLen > totalLen ? totalLen : tempRenderLen;
temp = tempRenderLen;
}
}
return temp;
}, [totalLen, tableScrollY]);
// 渲染中的第一条
let start = rowHeight ? (state.curScrollTop / rowHeight) | 0 : 0;
start = start < 5 ? 0 : start - 5 + 1;
// 偏移量
let offsetStart = state.curScrollTop % (rowHeight * 5);
if (start > 0) {
offsetStart = offsetStart % rowHeight;
}
// console.log(offsetStart)
// offsetStart= offsetStart%rowHeight
// 用来优化向上滚动出现的空白
if (state.curScrollTop && state.curScrollTop >= rowHeight * 5) {
// start -= 1
// if (offsetStart >= rowHeight) {
// offsetStart +=
// } else {
offsetStart += rowHeight * 4;
// }
} else {
start = 0;
}
// console.log(state.curScrollTop, start, offsetStart)
// 数据变更 操作scrollTop
useEffect(() => {
const scrollNode = wrap_tableRef.current?.parentNode as HTMLElement;
if (ifChangeRef?.current) {
// console.log(scrollNode)
ifChangeRef.current = false;
if (resetScrollTopWhenDataChange) {
// 重置scrollTop
if (scrollNode) {
scrollNode.scrollTop = 0;
}
dispatch({ type: 'reset', ifScrollTopClear: true });
} else {
// console.log(preData)
// scrollNode.scrollTop = preData+53
// 不重置scrollTop 不清空curScrollTop
dispatch({ type: 'reset', ifScrollTopClear: false });
}
}
if (vidMap.has(vid)) {
vidMap.set(vid, {
...vidMap.get(vid),
scrollNode,
});
}
}, [totalLen, resetScrollTopWhenDataChange, vid, children]);
useEffect(() => {
const throttleScroll = throttle((e) => {
const scrollTop: number = e?.target?.scrollTop ?? 0;
// const scrollHeight: number = e?.target?.scrollHeight ?? 0
// const clientHeight: number = e?.target?.clientHeight ?? 0
if (scrollTop) {
preData = scrollTop;
}
// 到底了 没有滚动条就不会触发reachEnd. 建议设置scrolly高度少点或者数据量多点.
// 若renderLen大于totalLen, 置空curScrollTop. => table paddingTop会置空.
dispatch({
type: 'changeTrs',
curScrollTop: renderLen <= totalLen ? scrollTop : 0,
});
}, 60);
const ref = wrap_tableRef?.current?.parentNode as HTMLElement;
if (ref) {
ref.addEventListener('scroll', throttleScroll, { passive: true });
}
return () => {
ref.removeEventListener('scroll', throttleScroll);
};
}, [renderLen, totalLen]);
return (
<div
className="virtuallist"
ref={wrap_tableRef}
style={{
width: '100%',
position: 'relative',
height: tableHeight,
boxSizing: 'border-box',
paddingTop: state.curScrollTop,
}}
>
<ScrollContext.Provider
value={{
dispatch,
start,
offsetStart,
renderLen,
totalLen,
vid,
rowHeight,
}}
>
<table
{...rest}
ref={tableRef}
style={{
...rest_style,
width,
position: 'relative',
transform: `translateY(-${offsetStart}px)`,
}}
>
{children}
</table>
</ScrollContext.Provider>
</div>
);
}
// ================导出===================
export function VList(props: {
height: number;
// 唯一标识
vid?: string;
rowHeight: number | string;
reset?: boolean;
scrollTop?: number | string;
}): any {
const { vid = DEFAULT_VID, rowHeight, height, reset, scrollTop } = props;
const resetScrollTopWhenDataChange = reset ?? true;
if (!vidMap.has(vid)) {
vidMap.set(vid, { _id: vid });
}
return {
table: (p) =>
VTable(p, {
vid,
scrollY: height,
rowHeight,
resetScrollTopWhenDataChange,
scrollTop,
}),
body: {
wrapper: VWrapper,
row: VRow,
cell: VCell,
},
};
}
+13 -10
View File
@@ -1,16 +1,19 @@
import { MutableRefObject, useLayoutEffect, useState } from 'react'; import { MutableRefObject, useLayoutEffect, useState } from 'react';
import useResizeObserver from '@react-hook/resize-observer' import useResizeObserver from '@react-hook/resize-observer';
import { getTableScroll } from '@/utils'; import { getTableScroll } from '@/utils';
export default <T extends HTMLElement>(target: MutableRefObject<T>, extraHeight?: number) => { export default <T extends HTMLElement>(
const [height, setHeight] = useState<number>() target: MutableRefObject<T>,
extraHeight?: number,
) => {
const [height, setHeight] = useState<number>(0);
useResizeObserver(target, (entry) => { useResizeObserver(target, (entry) => {
let _targe = entry.target as any let _target = entry.target as any;
if (!_targe.classList.contains('ant-table-wrapper')) { if (!_target.classList.contains('ant-table-wrapper')) {
_targe = entry.target.querySelector('.ant-table-wrapper') _target = entry.target.querySelector('.ant-table-wrapper');
} }
setHeight(getTableScroll({ extraHeight, target: _targe as HTMLElement })) setHeight(getTableScroll({ extraHeight, target: _target as HTMLElement }));
}) });
return height return height;
} };
+2 -1
View File
@@ -10,6 +10,7 @@ import {
ContainerOutlined, ContainerOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import IconFont from '@/components/iconfont'; import IconFont from '@/components/iconfont';
import { BasicLayoutProps } from '@ant-design/pro-layout';
export default { export default {
route: { route: {
@@ -93,4 +94,4 @@ export default {
contentWidth: 'Fixed', contentWidth: 'Fixed',
splitMenus: false, splitMenus: false,
siderWidth: 180, siderWidth: 180,
} as any; } as BasicLayoutProps;
+85 -15
View File
@@ -9,9 +9,25 @@
url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype'); 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 { .ant-modal-body {
max-height: calc(80vh - 110px); max-height: calc(90vh - 110px);
max-height: calc(80vh - var(--vh-offset, 110px)); max-height: calc(90vh - var(--vh-offset, 110px));
overflow-y: auto; overflow-y: auto;
} }
@@ -311,24 +327,25 @@ select:-webkit-autofill:focus {
.side-menu-user-drop-menu { .side-menu-user-drop-menu {
position: relative; position: relative;
text-align: left; text-align: left;
outline: none; padding: 2px 10px;
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);
overflow: auto; overflow: auto;
background-color: @component-background;
}
[data-dark='true'] .side-menu-user-drop-menu {
background-color: #373739;
} }
.ant-pro-sider-logo { .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 { a {
margin-left: 5px !important; display: inline-flex;
align-items: center;
}
} }
img { img {
@@ -342,9 +359,62 @@ select:-webkit-autofill:focus {
// 移动端logo被拉伸 // 移动端logo被拉伸
width: auto !important; width: auto !important;
} }
.ant-image {
display: inline-flex;
}
} }
pre { pre {
word-break: break-all !important; word-break: break-all !important;
white-space: break-spaces !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;
} }
+71 -54
View File
@@ -13,7 +13,6 @@ import config from '@/utils/config';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import './index.less'; import './index.less';
import vhCheck from 'vh-check'; import vhCheck from 'vh-check';
import { version, changeLogLink, changeLog } from '../version';
import { useCtx, useTheme } from '@/utils/hooks'; import { useCtx, useTheme } from '@/utils/hooks';
import { import {
message, message,
@@ -26,6 +25,7 @@ import {
Popover, Popover,
Descriptions, Descriptions,
Tooltip, Tooltip,
MenuProps,
} from 'antd'; } from 'antd';
// @ts-ignore // @ts-ignore
import SockJS from 'sockjs-client'; import SockJS from 'sockjs-client';
@@ -49,9 +49,10 @@ export interface SharedContext {
interface TSystemInfo { interface TSystemInfo {
branch: 'develop' | 'master'; branch: 'develop' | 'master';
isInitialized: boolean; isInitialized: boolean;
lastCommitId: string; publishTime: number;
lastCommitTime: number;
version: string; version: string;
changeLog: string;
changeLogLink: string;
} }
export default function () { export default function () {
@@ -89,14 +90,14 @@ export default function () {
if (!data.isInitialized) { if (!data.isInitialized) {
history.push('/initialization'); history.push('/initialization');
} else { } else {
init(data.version);
getUser(); getUser();
} }
} }
}) })
.catch((error) => { .catch((error) => {
console.log(error); console.log(error);
}) });
.finally(() => setInitLoading(false));
}; };
const getUser = (needLoading = true) => { const getUser = (needLoading = true) => {
@@ -117,6 +118,22 @@ export default function () {
}); });
}; };
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) => { const reloadUser = (needLoading = false) => {
getUser(needLoading); getUser(needLoading);
}; };
@@ -128,10 +145,8 @@ export default function () {
}, [location.pathname]); }, [location.pathname]);
useEffect(() => { useEffect(() => {
if (!systemInfo) { getHealthStatus();
getSystemInfo(); }, []);
}
}, [systemInfo]);
useEffect(() => { useEffect(() => {
if (theme === 'vs-dark') { if (theme === 'vs-dark') {
@@ -143,7 +158,6 @@ export default function () {
useEffect(() => { useEffect(() => {
vhCheck(); vhCheck();
init();
const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto'; const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
if (typeof window === 'undefined') return; if (typeof window === 'undefined') return;
@@ -221,9 +235,6 @@ export default function () {
} }
if (['/login', '/initialization', '/error'].includes(location.pathname)) { if (['/login', '/initialization', '/error'].includes(location.pathname)) {
document.title = `${
(config.documentTitleMap as any)[location.pathname]
} - `;
if (systemInfo?.isInitialized && location.pathname === '/initialization') { if (systemInfo?.isInitialized && location.pathname === '/initialization') {
history.push('/crontab'); history.push('/crontab');
} }
@@ -250,13 +261,17 @@ export default function () {
!navigator.userAgent.includes('Chrome'); !navigator.userAgent.includes('Chrome');
const isQQBrowser = navigator.userAgent.includes('QQBrowser'); const isQQBrowser = navigator.userAgent.includes('QQBrowser');
const menu = ( const menu: MenuProps = {
<Menu items: [
className="side-menu-user-drop-menu" {
items={[{ label: '退出登录', key: 'logout', icon: <LogoutOutlined /> }]} label: '退出登录',
onClick={logout} className: 'side-menu-user-drop-menu',
/> onClick: logout,
); key: 'logout',
icon: <LogoutOutlined />,
},
],
};
return loading ? ( return loading ? (
<PageLoading /> <PageLoading />
) : ( ) : (
@@ -264,38 +279,41 @@ export default function () {
selectedKeys={[location.pathname]} selectedKeys={[location.pathname]}
loading={loading} loading={loading}
ErrorBoundary={Sentry.ErrorBoundary} ErrorBoundary={Sentry.ErrorBoundary}
logo={<Image preview={false} src="https://qn.whyour.cn/logo.png" />} logo={
title={
<> <>
<span style={{ fontSize: 16 }}></span> <Image preview={false} src="https://qn.whyour.cn/logo.png" />
<a <div className="title">
href={changeLogLink} <span className="title"></span>
target="_blank" <a
rel="noopener noreferrer" href={systemInfo?.changeLogLink}
onClick={(e) => { target="_blank"
e.stopPropagation(); rel="noopener noreferrer"
}} onClick={(e) => {
> e.stopPropagation();
<Tooltip }}
title={systemInfo?.branch === 'develop' ? '开发版' : '正式版'}
> >
<Badge size="small" dot={systemInfo?.branch === 'develop'}> <Tooltip
<span title={systemInfo?.branch === 'develop' ? '开发版' : '正式版'}
style={{ >
fontSize: isFirefox ? 9 : 12, <Badge size="small" dot={systemInfo?.branch === 'develop'}>
color: '#666', <span
marginLeft: 2, style={{
zoom: isSafari ? 0.66 : 0.8, fontSize: isFirefox ? 9 : 12,
letterSpacing: isQQBrowser ? -2 : 0, color: '#666',
}} marginLeft: 2,
> zoom: isSafari ? 0.66 : 0.8,
v{version} letterSpacing: isQQBrowser ? -2 : 0,
</span> }}
</Badge> >
</Tooltip> v{systemInfo?.version}
</a> </span>
</Badge>
</Tooltip>
</a>
</div>
</> </>
} }
title={false}
menuItemRender={(menuItemProps: any, defaultDom: any) => { menuItemRender={(menuItemProps: any, defaultDom: any) => {
if ( if (
menuItemProps.isUrl || menuItemProps.isUrl ||
@@ -307,16 +325,15 @@ export default function () {
return <Link to={menuItemProps.path}>{defaultDom}</Link>; return <Link to={menuItemProps.path}>{defaultDom}</Link>;
}} }}
pageTitleRender={(props, pageName, info) => { pageTitleRender={(props, pageName, info) => {
if (info && typeof info.pageName === 'string') { const title =
return `${info.pageName} - 控制面板`; (config.documentTitleMap as any)[location.pathname] || '未找到';
} return `${title} - 青龙`;
return '控制面板';
}} }}
onCollapse={setCollapsed} onCollapse={setCollapsed}
collapsed={collapsed} collapsed={collapsed}
rightContentRender={() => rightContentRender={() =>
ctx.isPhone && ( ctx.isPhone && (
<Dropdown overlay={menu} placement="bottomRight" trigger={['click']}> <Dropdown menu={menu} placement="bottomRight" trigger={['click']}>
<span className="side-menu-user-wrapper"> <span className="side-menu-user-wrapper">
<Avatar <Avatar
shape="square" shape="square"
@@ -338,7 +355,7 @@ export default function () {
}} }}
> >
{!collapsed && !ctx.isPhone && ( {!collapsed && !ctx.isPhone && (
<Dropdown overlay={menu} placement="topLeft" trigger={['hover']}> <Dropdown menu={menu} placement="topLeft" trigger={['hover']}>
<span className="side-menu-user-wrapper"> <span className="side-menu-user-wrapper">
<Avatar <Avatar
shape="square" shape="square"
+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;
+1 -3
View File
@@ -51,9 +51,7 @@ const Config = () => {
: value; : value;
request request
.post(`${config.apiPrefix}configs/save`, { .post(`${config.apiPrefix}configs/save`, { content, name: select })
data: { content, name: select },
})
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
message.success('保存成功'); message.success('保存成功');
+13 -30
View File
@@ -22,13 +22,14 @@ import {
PauseCircleOutlined, PauseCircleOutlined,
FullscreenOutlined, FullscreenOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { CrontabStatus } from './index'; import { CrontabStatus } from './type';
import { diffTime } from '@/utils/date'; import { diffTime } from '@/utils/date';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
import CronLogModal from './logModal'; import CronLogModal from './logModal';
import Editor from '@monaco-editor/react'; import Editor from '@monaco-editor/react';
import IconFont from '@/components/iconfont'; import IconFont from '@/components/iconfont';
import { getCommandScript } from '@/utils';
const { Text } = Typography; const { Text } = Typography;
@@ -147,22 +148,10 @@ const CronDetailModal = ({
}; };
const getScript = () => { const getScript = () => {
const cmd = cron.command.split(' ') as string[]; const result = getCommandScript(cron.command);
if (cmd[0] === 'task') { if (Array.isArray(result)) {
setValidTabs(validTabs); setValidTabs(validTabs);
if (cmd[1].startsWith('/ql/data/scripts')) { const [s, p] = result;
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
}
let p: string, s: string;
let index = cmd[1].lastIndexOf('/');
if (index >= 0) {
s = cmd[1].slice(index + 1);
p = cmd[1].slice(0, index);
} else {
s = cmd[1];
p = '';
}
setScriptInfo({ parent: p, filename: s }); setScriptInfo({ parent: p, filename: s });
request request
.get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`) .get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`)
@@ -171,7 +160,7 @@ const CronDetailModal = ({
setValue(data); setValue(data);
} }
}); });
} else { } else if (result) {
setValidTabs([validTabs[0]]); setValidTabs([validTabs[0]]);
} }
}; };
@@ -195,11 +184,9 @@ const CronDetailModal = ({
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request request
.put(`${config.apiPrefix}scripts`, { .put(`${config.apiPrefix}scripts`, {
data: { filename: scriptInfo.filename,
filename: scriptInfo.filename, path: scriptInfo.parent || '',
path: scriptInfo.parent || '', content,
content,
},
}) })
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
@@ -231,7 +218,7 @@ const CronDetailModal = ({
), ),
onOk() { onOk() {
request request
.put(`${config.apiPrefix}crons/run`, { data: [currentCron.id] }) .put(`${config.apiPrefix}crons/run`, [currentCron.id])
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setCurrentCron({ ...currentCron, status: CrontabStatus.running }); setCurrentCron({ ...currentCron, status: CrontabStatus.running });
@@ -261,7 +248,7 @@ const CronDetailModal = ({
), ),
onOk() { onOk() {
request request
.put(`${config.apiPrefix}crons/stop`, { data: [currentCron.id] }) .put(`${config.apiPrefix}crons/stop`, [currentCron.id] )
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setCurrentCron({ ...currentCron, status: CrontabStatus.idle }); setCurrentCron({ ...currentCron, status: CrontabStatus.idle });
@@ -293,9 +280,7 @@ const CronDetailModal = ({
`${config.apiPrefix}crons/${ `${config.apiPrefix}crons/${
currentCron.isDisabled === 1 ? 'enable' : 'disable' currentCron.isDisabled === 1 ? 'enable' : 'disable'
}`, }`,
{ [currentCron.id],
data: [currentCron.id],
},
) )
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
@@ -331,9 +316,7 @@ const CronDetailModal = ({
`${config.apiPrefix}crons/${ `${config.apiPrefix}crons/${
currentCron.isPinned === 1 ? 'unpin' : 'pin' currentCron.isPinned === 1 ? 'unpin' : 'pin'
}`, }`,
{ [currentCron.id],
data: [currentCron.id],
},
) )
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
+25 -3
View File
@@ -6,8 +6,9 @@
.card-wrapper { .card-wrapper {
.ant-card:last-child { .ant-card:last-child {
.ant-card-body { .ant-card-body {
height: calc(80vh - 367px); min-height: 300px;
height: calc(80vh - var(--vh-offset, 0px) - 367px); height: calc(90vh - 367px);
height: calc(90vh - var(--vh-offset, 0px) - 367px);
overflow-y: auto; overflow-y: auto;
} }
} }
@@ -155,7 +156,7 @@
.view-create-modal-sorts { .view-create-modal-sorts {
display: flex; display: flex;
.ant-space-item:nth-child(2) { .ant-space-item:nth-child(1) {
flex: 1; flex: 1;
} }
} }
@@ -177,5 +178,26 @@ tr.drop-over-upward td {
.view-filters-container.active { .view-filters-container.active {
.filter-item > div > .ant-form-item-control { .filter-item > div > .ant-form-item-control {
margin-left: 40px; margin-left: 40px;
width: calc(100% - 40px);
}
}
.log-modal {
.log-container {
overflow-y: auto;
min-height: 300px;
max-height: calc(80vh - 110px);
max-height: calc(80vh - var(--vh-offset, 110px));
padding: 24px;
margin: -24px;
}
}
body[data-mode='desktop'] {
.crontab-wrapper {
tbody .ant-table-cell {
height: 69px !important;
}
} }
} }
+101 -175
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef, useMemo } from 'react';
import { import {
Button, Button,
message, message,
@@ -14,6 +14,7 @@ import {
Popover, Popover,
Tabs, Tabs,
TablePaginationConfig, TablePaginationConfig,
MenuProps,
} from 'antd'; } from 'antd';
import { import {
ClockCircleOutlined, ClockCircleOutlined,
@@ -50,46 +51,22 @@ import ViewManageModal from './viewManageModal';
import { FilterValue, SorterResult } from 'antd/lib/table/interface'; import { FilterValue, SorterResult } from 'antd/lib/table/interface';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight'; import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import { getCommandScript, parseCrontab } from '@/utils';
import { ColumnProps } from 'antd/lib/table';
import { useVT } from 'virtualizedtableforantd4';
import { ICrontab, OperationName, OperationPath, CrontabStatus } from './type';
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
const { Search } = Input; const { Search } = Input;
export enum CrontabStatus {
'running',
'idle',
'disabled',
'queued',
}
const CrontabSort: any = { 0: 0, 5: 1, 3: 2, 1: 3, 4: 4 };
enum OperationName {
'启用',
'禁用',
'运行',
'停止',
'置顶',
'取消置顶',
}
enum OperationPath {
'enable',
'disable',
'run',
'stop',
'pin',
'unpin',
}
const Crontab = () => { const Crontab = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>(); const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const columns: any = [ const columns: ColumnProps<ICrontab>[] = [
{ {
title: '名称', title: '名称',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
width: 150, width: 150,
align: 'center' as const,
render: (text: string, record: any) => ( render: (text: string, record: any) => (
<> <>
<a <a
@@ -137,22 +114,20 @@ const Crontab = () => {
</> </>
), ),
sorter: { sorter: {
compare: (a: any, b: any) => a?.name?.localeCompare(b?.name), compare: (a, b) => a?.name?.localeCompare(b?.name),
}, },
}, },
{ {
title: '命令', title: '命令/脚本',
dataIndex: 'command', dataIndex: 'command',
key: 'command', key: 'command',
width: 300, width: 300,
align: 'center' as const, render: (text, record) => {
render: (text: string, record: any) => {
return ( return (
<Paragraph <Paragraph
style={{ style={{
wordBreak: 'break-all', wordBreak: 'break-all',
marginBottom: 0, marginBottom: 0,
textAlign: 'left',
}} }}
ellipsis={{ tooltip: text, rows: 2 }} ellipsis={{ tooltip: text, rows: 2 }}
> >
@@ -175,23 +150,21 @@ const Crontab = () => {
dataIndex: 'schedule', dataIndex: 'schedule',
key: 'schedule', key: 'schedule',
width: 110, width: 110,
align: 'center' as const,
sorter: { sorter: {
compare: (a: any, b: any) => a.schedule.localeCompare(b.schedule), compare: (a, b) => a.schedule.localeCompare(b.schedule),
}, },
}, },
{ {
title: '最后运行时间', title: '最后运行时间',
align: 'center' as const,
dataIndex: 'last_execution_time', dataIndex: 'last_execution_time',
key: 'last_execution_time', key: 'last_execution_time',
width: 150, width: 150,
sorter: { sorter: {
compare: (a: any, b: any) => { compare: (a, b) => {
return a.last_execution_time - b.last_execution_time; return (a.last_execution_time || 0) - (b.last_execution_time || 0);
}, },
}, },
render: (text: string, record: any) => { render: (text, record) => {
const language = navigator.language || navigator.languages[0]; const language = navigator.language || navigator.languages[0];
return ( return (
<span <span
@@ -212,7 +185,6 @@ const Crontab = () => {
}, },
{ {
title: '最后运行时长', title: '最后运行时长',
align: 'center' as const,
width: 120, width: 120,
dataIndex: 'last_running_time', dataIndex: 'last_running_time',
key: 'last_running_time', key: 'last_running_time',
@@ -221,7 +193,7 @@ const Crontab = () => {
return a.last_running_time - b.last_running_time; return a.last_running_time - b.last_running_time;
}, },
}, },
render: (text: string, record: any) => { render: (text, record) => {
return record.last_running_time return record.last_running_time
? diffTime(record.last_running_time) ? diffTime(record.last_running_time)
: '-'; : '-';
@@ -229,14 +201,13 @@ const Crontab = () => {
}, },
{ {
title: '下次运行时间', title: '下次运行时间',
align: 'center' as const,
width: 150, width: 150,
sorter: { sorter: {
compare: (a: any, b: any) => { compare: (a: any, b: any) => {
return a.nextRunTime - b.nextRunTime; return a.nextRunTime - b.nextRunTime;
}, },
}, },
render: (text: string, record: any) => { render: (text, record) => {
const language = navigator.language || navigator.languages[0]; const language = navigator.language || navigator.languages[0];
return record.nextRunTime return record.nextRunTime
.toLocaleString(language, { .toLocaleString(language, {
@@ -245,39 +216,30 @@ const Crontab = () => {
.replace(' 24:', ' 00:'); .replace(' 24:', ' 00:');
}, },
}, },
{ {
title: '状态', title: '状态',
key: 'status', key: 'status',
dataIndex: 'status', dataIndex: 'status',
align: 'center' as const, width: 88,
width: 85,
filters: [ filters: [
{ {
text: '运行中', text: '运行中',
value: 0, value: CrontabStatus.running,
}, },
{ {
text: '空闲中', text: '空闲中',
value: 1, value: CrontabStatus.idle,
}, },
{ {
text: '已禁用', text: '已禁用',
value: 2, value: CrontabStatus.disabled,
}, },
{ {
text: '队列中', text: '队列中',
value: 3, value: CrontabStatus.queued,
}, },
], ],
onFilter: (value: number, record: any) => { render: (text, record) => (
if (record.isDisabled && record.status !== 0) {
return value === 2;
} else {
return record.status === value;
}
},
render: (text: string, record: any) => (
<> <>
{(!record.isDisabled || record.status !== CrontabStatus.idle) && ( {(!record.isDisabled || record.status !== CrontabStatus.idle) && (
<> <>
@@ -312,9 +274,8 @@ const Crontab = () => {
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
align: 'center' as const, width: 130,
width: 100, render: (text, record, index) => {
render: (text: string, record: any, index: number) => {
const isPc = !isPhone; const isPc = !isPhone;
return ( return (
<Space size="middle"> <Space size="middle">
@@ -390,24 +351,12 @@ const Crontab = () => {
const tableScrollHeight = useTableScrollHeight(tableRef); const tableScrollHeight = useTableScrollHeight(tableRef);
const goToScriptManager = (record: any) => { const goToScriptManager = (record: any) => {
const cmd = record.command.split(' ') as string[]; const result = getCommandScript(record.command);
if (cmd[0] === 'task') { if (Array.isArray(result)) {
if (cmd[1].startsWith('/ql/data/scripts')) { const [s, p] = result;
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
}
let p: string, s: string;
let index = cmd[1].lastIndexOf('/');
if (index >= 0) {
s = cmd[1].slice(index + 1);
p = cmd[1].slice(0, index);
} else {
s = cmd[1];
p = '';
}
history.push(`/script?p=${p}&s=${s}`); history.push(`/script?p=${p}&s=${s}`);
} else if (cmd[1] === 'repo') { } else if (result) {
location.href = cmd[2]; location.href = result;
} }
}; };
@@ -419,9 +368,9 @@ const Crontab = () => {
}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify( }crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(
filters, filters,
)}`; )}`;
if (sorter && sorter.field) { if (sorter && sorter.column && sorter.order) {
url += `&sorter=${JSON.stringify({ url += `&sorter=${JSON.stringify({
field: sorter.field, field: sorter.column.key,
type: sorter.order === 'ascend' ? 'ASC' : 'DESC', type: sorter.order === 'ascend' ? 'ASC' : 'DESC',
})}`; })}`;
} }
@@ -441,10 +390,7 @@ const Crontab = () => {
data.map((x) => { data.map((x) => {
return { return {
...x, ...x,
nextRunTime: cron_parser nextRunTime: parseCrontab(x.schedule),
.parseExpression(x.schedule)
.next()
.toDate(),
}; };
}), }),
); );
@@ -511,7 +457,7 @@ const Crontab = () => {
), ),
onOk() { onOk() {
request request
.put(`${config.apiPrefix}crons/run`, { data: [record.id] }) .put(`${config.apiPrefix}crons/run`, [record.id])
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
const result = [...value]; const result = [...value];
@@ -546,7 +492,7 @@ const Crontab = () => {
), ),
onOk() { onOk() {
request request
.put(`${config.apiPrefix}crons/stop`, { data: [record.id] }) .put(`${config.apiPrefix}crons/stop`, [record.id])
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
const result = [...value]; const result = [...value];
@@ -587,9 +533,7 @@ const Crontab = () => {
`${config.apiPrefix}crons/${ `${config.apiPrefix}crons/${
record.isDisabled === 1 ? 'enable' : 'disable' record.isDisabled === 1 ? 'enable' : 'disable'
}`, }`,
{ [record.id],
data: [record.id],
},
) )
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
@@ -631,9 +575,7 @@ const Crontab = () => {
`${config.apiPrefix}crons/${ `${config.apiPrefix}crons/${
record.isPinned === 1 ? 'unpin' : 'pin' record.isPinned === 1 ? 'unpin' : 'pin'
}`, }`,
{ [record.id],
data: [record.id],
},
) )
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
@@ -679,18 +621,15 @@ const Crontab = () => {
index: number; index: number;
}> = ({ record, index }) => ( }> = ({ record, index }) => (
<Dropdown <Dropdown
arrow={{ pointAtCenter: true }}
placement="bottomRight" placement="bottomRight"
trigger={['click']} trigger={['click']}
overlay={ menu={{
<Menu items: getMenuItems(record),
items={getMenuItems(record)} onClick: ({ key, domEvent }) => {
onClick={({ key, domEvent }) => { domEvent.stopPropagation();
domEvent.stopPropagation(); action(key, record, index);
action(key, record, index); },
}} }}
/>
}
> >
<a onClick={(e) => e.stopPropagation()}> <a onClick={(e) => e.stopPropagation()}>
<EllipsisOutlined /> <EllipsisOutlined />
@@ -717,34 +656,15 @@ const Crontab = () => {
} }
}; };
const handleCancel = (cron?: any) => { const handleCancel = () => {
setIsModalVisible(false); setIsModalVisible(false);
if (cron) { getCrons();
handleCrons(cron);
}
}; };
const onSearch = (value: string) => { const onSearch = (value: string) => {
setSearchText(value.trim()); setSearchText(value.trim());
}; };
const handleCrons = (cron: any) => {
const index = value.findIndex((x) => x.id === cron.id);
const result = [...value];
cron.nextRunTime = cron_parser
.parseExpression(cron.schedule)
.next()
.toDate();
if (index === -1) {
result.unshift(cron);
} else {
result.splice(index, 1, {
...cron,
});
}
setValue(result);
};
const getCronDetail = (cron: any) => { const getCronDetail = (cron: any) => {
request request
.get(`${config.apiPrefix}crons/${cron.id}`) .get(`${config.apiPrefix}crons/${cron.id}`)
@@ -752,10 +672,7 @@ const Crontab = () => {
if (code === 200) { if (code === 200) {
const index = value.findIndex((x) => x.id === cron.id); const index = value.findIndex((x) => x.id === cron.id);
const result = [...value]; const result = [...value];
data.nextRunTime = cron_parser data.nextRunTime = parseCrontab(data.schedule);
.parseExpression(data.schedule)
.next()
.toDate();
if (index !== -1) { if (index !== -1) {
result.splice(index, 1, { result.splice(index, 1, {
...cron, ...cron,
@@ -804,9 +721,10 @@ const Crontab = () => {
content: <>{OperationName[operationStatus]}</>, content: <>{OperationName[operationStatus]}</>,
onOk() { onOk() {
request request
.put(`${config.apiPrefix}crons/${OperationPath[operationStatus]}`, { .put(
data: selectedRowIds, `${config.apiPrefix}crons/${OperationPath[operationStatus]}`,
}) selectedRowIds,
)
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
getCrons(); getCrons();
@@ -863,12 +781,6 @@ const Crontab = () => {
}, [viewConf, enabledCronViews]); }, [viewConf, enabledCronViews]);
useEffect(() => { useEffect(() => {
setPageConf({
page: 1,
size: parseInt(localStorage.getItem('pageSize') || '20'),
sorter: {},
filters: {},
});
getCronViews(); getCronViews();
}, []); }, []);
@@ -887,41 +799,39 @@ const Crontab = () => {
} }
}; };
const menu = ( const menu: MenuProps = {
<Menu onClick: ({ key, domEvent }) => {
onClick={({ key, domEvent }) => { domEvent.stopPropagation();
domEvent.stopPropagation(); viewAction(key);
viewAction(key); },
}} items: [
items={[ ...[...enabledCronViews].slice(4).map((x) => ({
...[...enabledCronViews].slice(4).map((x) => ({ label: (
label: ( <Space style={{ display: 'flex', justifyContent: 'space-between' }}>
<Space style={{ display: 'flex', justifyContent: 'space-between' }}> <span>{x.name}</span>
<span>{x.name}</span> {viewConf?.id === x.id && (
{viewConf?.id === x.id && ( <CheckOutlined style={{ color: '#1890ff' }} />
<CheckOutlined style={{ color: '#1890ff' }} /> )}
)} </Space>
</Space> ),
), key: x.id,
key: x.id, icon: <UnorderedListOutlined />,
icon: <UnorderedListOutlined />, })),
})), {
{ type: 'divider' as 'group',
type: 'divider', },
}, {
{ label: '新建视图',
label: '新建视图', key: 'new',
key: 'new', icon: <PlusOutlined />,
icon: <PlusOutlined />, },
}, {
{ label: '视图管理',
label: '视图管理', key: 'manage',
key: 'manage', icon: <SettingOutlined />,
icon: <SettingOutlined />, },
}, ],
]} };
/>
);
const getCronViews = () => { const getCronViews = () => {
setLoading(true); setLoading(true);
@@ -930,7 +840,17 @@ const Crontab = () => {
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setCronViews(data); setCronViews(data);
setEnabledCronViews(data.filter((x) => !x.isDisabled)); const firstEnableView = data.filter((x) => !x.isDisabled);
setEnabledCronViews(firstEnableView);
setPageConf({
page: 1,
size: parseInt(localStorage.getItem('pageSize') || '20'),
sorter: {},
filters: {},
});
setViewConf({
...firstEnableView[0],
});
} }
}) })
.finally(() => { .finally(() => {
@@ -945,6 +865,11 @@ const Crontab = () => {
setViewConf(view ? view : null); setViewConf(view ? view : null);
}; };
const [vt] = useVT(
() => ({ scroll: { y: tableScrollHeight } }),
[tableScrollHeight],
);
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper crontab-wrapper ql-container-wrapper-has-tab" className="ql-container-wrapper crontab-wrapper ql-container-wrapper-has-tab"
@@ -975,7 +900,7 @@ const Crontab = () => {
className={`crontab-view ${moreMenuActive ? 'more-active' : ''}`} className={`crontab-view ${moreMenuActive ? 'more-active' : ''}`}
tabBarExtraContent={ tabBarExtraContent={
<Dropdown <Dropdown
overlay={menu} menu={menu}
trigger={['click']} trigger={['click']}
overlayStyle={{ minWidth: 200 }} overlayStyle={{ minWidth: 200 }}
> >
@@ -1079,6 +1004,7 @@ const Crontab = () => {
rowSelection={rowSelection} rowSelection={rowSelection}
rowClassName={getRowClassName} rowClassName={getRowClassName}
onChange={onPageChange} onChange={onPageChange}
// components={isPhone ? undefined : vt}
/> />
</div> </div>
<CronLogModal <CronLogModal
+52 -52
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { Modal, message, Input, Form, Statistic, Button } from 'antd'; import { Modal, message, Input, Form, Statistic, Button } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
@@ -8,13 +8,8 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-layout'; import { PageLoading } from '@ant-design/pro-layout';
import { logEnded } from '@/utils'; import { logEnded } from '@/utils';
import { CrontabStatus } from './type';
enum CrontabStatus {
'running',
'idle',
'disabled',
'queued',
}
const { Countdown } = Statistic; const { Countdown } = Statistic;
const CronLogModal = ({ const CronLogModal = ({
@@ -34,7 +29,7 @@ const CronLogModal = ({
const [loading, setLoading] = useState<any>(true); const [loading, setLoading] = useState<any>(true);
const [executing, setExecuting] = useState<any>(true); const [executing, setExecuting] = useState<any>(true);
const [isPhone, setIsPhone] = useState(false); const [isPhone, setIsPhone] = useState(false);
const [theme, setTheme] = useState<string>(''); const scrollInfoRef = useRef({ value: 0, down: true });
const getCronLog = (isFirst?: boolean) => { const getCronLog = (isFirst?: boolean) => {
if (isFirst) { if (isFirst) {
@@ -45,41 +40,21 @@ const CronLogModal = ({
.then(({ code, data }) => { .then(({ code, data }) => {
if ( if (
code === 200 && code === 200 &&
localStorage.getItem('logCron') === String(cron.id) localStorage.getItem('logCron') === String(cron.id) &&
data !== value
) { ) {
const log = data as string; const log = data as string;
setValue(log || '暂无日志'); setValue(log || '暂无日志');
setExecuting( const hasNext = Boolean(
log && !logEnded(log) && !log.includes('重启面板'), log && !logEnded(log) && !log.includes('任务未运行'),
); );
if (log && !logEnded(log) && !log.includes('重启面板')) { setExecuting(hasNext);
autoScroll();
if (hasNext) {
setTimeout(() => { setTimeout(() => {
getCronLog(); getCronLog();
}, 2000); }, 2000);
} }
if (
log &&
log.includes('重启面板') &&
cron.status === CrontabStatus.running
) {
message.warning({
content: (
<span>
<Countdown
className="inline-countdown"
format="ss"
value={Date.now() + 1000 * 30}
/>
</span>
),
duration: 10,
});
setTimeout(() => {
window.location.reload();
}, 30000);
}
} }
}) })
.finally(() => { .finally(() => {
@@ -89,11 +64,33 @@ const CronLogModal = ({
}); });
}; };
const autoScroll = () => {
if (!scrollInfoRef.current.down) {
return;
}
setTimeout(() => {
document
.querySelector('#log-flag')!
.scrollIntoView({ behavior: 'smooth' });
}, 1000);
};
const cancel = () => { const cancel = () => {
localStorage.removeItem('logCron'); localStorage.removeItem('logCron');
handleCancel(); handleCancel();
}; };
const handleScroll = (e) => {
const sTop = e.target.scrollTop;
if (scrollInfoRef.current.down) {
scrollInfoRef.current = {
value: sTop,
down: sTop > scrollInfoRef.current.value,
};
}
};
const titleElement = () => { const titleElement = () => {
return ( return (
<> <>
@@ -138,23 +135,26 @@ const CronLogModal = ({
</Button>, </Button>,
]} ]}
> >
{loading ? ( <div onScroll={handleScroll} className="log-container">
<PageLoading /> {loading ? (
) : ( <PageLoading />
<pre ) : (
style={ <pre
isPhone style={
? { isPhone
fontFamily: 'Source Code Pro', ? {
width: 375, fontFamily: 'Source Code Pro',
zoom: 0.83, width: 375,
} zoom: 0.83,
: {} }
} : {}
> }
{value} >
</pre> {value}
)} </pre>
)}
<div id="log-flag"></div>
</div>
</Modal> </Modal>
); );
}; };
+7 -8
View File
@@ -25,9 +25,10 @@ const CronModal = ({
payload.id = cron.id; payload.id = cron.id;
} }
try { try {
const { code, data } = await request[method](`${config.apiPrefix}crons`, { const { code, data } = await request[method](
data: payload, `${config.apiPrefix}crons`,
}); payload,
);
if (code === 200) { if (code === 200) {
message.success(cron ? '更新Cron成功' : '新建Cron成功'); message.success(cron ? '更新Cron成功' : '新建Cron成功');
@@ -74,13 +75,13 @@ const CronModal = ({
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="command" name="command"
label="命令" label="命令/脚本"
rules={[{ required: true, whitespace: true }]} rules={[{ required: true, whitespace: true }]}
> >
<Input.TextArea <Input.TextArea
rows={4} rows={4}
autoSize={true} autoSize={true}
placeholder="请输入要执行命令" placeholder="支持输入脚本路径/任意系统可执行命令/task 脚本路径"
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@@ -130,9 +131,7 @@ const CronLabelModal = ({
try { try {
const { code, data } = await request[action]( const { code, data } = await request[action](
`${config.apiPrefix}crons/labels`, `${config.apiPrefix}crons/labels`,
{ payload,
data: payload,
},
); );
if (code === 200) { if (code === 200) {
+38
View File
@@ -0,0 +1,38 @@
export enum CrontabStatus {
'running' = 0,
'queued' = 0.5,
'idle' = 1,
'disabled',
}
export enum OperationName {
'启用',
'禁用',
'运行',
'停止',
'置顶',
'取消置顶',
}
export enum OperationPath {
'enable',
'disable',
'run',
'stop',
'pin',
'unpin',
}
export interface ICrontab {
name: string;
command: string;
schedule: string;
id: number;
status: number;
isDisabled?: 1 | 0;
isPinned?: 1 | 0;
labels?: string[];
last_running_time?: number;
last_execution_time?: number;
nextRunTime: Date;
}

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