Compare commits

...
867 Commits
Author SHA1 Message Date
whyour 54d99a4954 更新版本 v2.14.2 2022-09-07 19:30:19 +08:00
whyour 9947594366 更新issue模板 2022-09-07 17:28:57 +08:00
whyour d7ff934336 增加github issue模版 2022-09-07 16:03:15 +08:00
whyour dc27856441 修复定时任务状态筛选 2022-09-07 11:30:11 +08:00
whyour bd28682769 排除认证失败错误上报 2022-09-06 23:57:21 +08:00
whyour 2060e62da4 更新sentry版本 2022-09-06 21:50:03 +08:00
whyour 70eac1e747 修改pnpm安装全局模块命令 2022-09-06 15:53:21 +08:00
whyour 6f5a5fe066 修改定时任务打开详情交互 2022-09-06 14:49:42 +08:00
景大侠andGitHub 8d24949202 修复#1614 正确解析多级目录的脚本路径 (#1616) 2022-09-06 13:25:12 +08:00
whyour f7b6237355 修复新建任务视图交互 2022-09-06 00:40:06 +08:00
景大侠andGitHub daf6f94c51 修复任务视图bug (#1612)
* 修复了视图无排序时无法再次修改的问题

* 修复在视图管理中编辑、新建视图点击确定后不能关闭页面的问题

* 修复#1611 避免查询条件被覆盖

* 修复视图筛选不能正确处理`不包含`
2022-09-06 00:25:05 +08:00
whyour 687d16e21b 增加npm镜像源配置 2022-09-06 00:21:53 +08:00
whyour 70571a9d34 更新版本 v2.14.1 2022-09-04 18:28:28 +08:00
whyour 4da8d8fe1e 修复node pushdeer通知 2022-09-04 18:25:40 +08:00
AppoipandGitHub 952cdc0e3f 添加群晖synology chat通知推送 (#1610) 2022-09-04 18:10:20 +08:00
whyour 2de189d189 修复视图切换页码初始值 2022-09-03 18:27:52 +08:00
whyour 3edc6e83d7 修复定时任务搜索 2022-09-03 18:23:35 +08:00
whyour 31a5386cfe 更新版本 v2.14.0 2022-09-03 15:26:04 +08:00
whyour 0198ebbbff 修复任务视图tab激活状态 2022-09-03 15:16:35 +08:00
whyour ce3df8704d 修复任务视图tab样式 2022-09-03 15:02:05 +08:00
whyour 1b4ca3a684 修复视图拖拽数据同步 2022-09-03 14:56:32 +08:00
whyour e31c2c0955 完善定时任务视图拖拽 2022-09-03 12:39:47 +08:00
whyour 4e389865b5 定时任务视图tab高亮 2022-09-03 11:58:21 +08:00
whyour e5d8adf955 修复定时任务视图排序 2022-09-03 02:15:32 +08:00
whyour 9a3181bc44 任务视图增加状态筛选 2022-09-03 01:56:47 +08:00
whyour 2f05c95422 完善定时任务视图 2022-09-02 17:52:06 +08:00
whyour bc5a3a2028 修复nginx配置文件 2022-09-02 11:28:08 +08:00
whyour 1fe91508f6 修复baseUrl 2022-09-02 10:54:44 +08:00
whyour b9253c8191 修复定时任务搜索api 2022-09-02 00:35:20 +08:00
whyour 7516acbc41 修复定时任务搜索 2022-08-31 00:02:35 +08:00
whyour 3b3d45da29 修复定时任务排序 2022-08-28 15:45:30 +08:00
whyour 918d68d140 修复布局样式 2022-08-28 00:44:17 +08:00
whyour a3f56be299 修改整体布局,减少无用间隔 2022-08-28 00:38:25 +08:00
whyour f8f63890e5 定时任务增加分组 2022-08-27 14:50:27 +08:00
whyour 9e997410ab 测试dockerfile 2022-08-26 18:42:25 +08:00
whyour 529880642f 修复typescript类型错误 2022-08-26 17:52:37 +08:00
景大侠andGitHub d2590edab3 支持非js任务随机延迟 (#1580)
* 修复非JS任务不能随机延迟的问题
2022-08-26 17:11:16 +08:00
whyour 32eec68278 更新版本 v2.13.9 2022-08-26 00:04:59 +08:00
whyour 05ed8c9f4b 修改定时任务列表布局 2022-08-25 23:58:20 +08:00
whyour bc281ee4d7 修改定时任务pageSize,增加全部展示为一页 2022-08-25 23:12:12 +08:00
whyour 3e88314d0a 修改二次验证交互 2022-08-23 22:38:03 +08:00
whyour 141defd845 修复定时任务默认pageSize 2022-08-20 23:21:31 +08:00
whyour b9e49b181a 修改定时任务排序 2022-08-20 20:27:19 +08:00
whyour 102e447f78 修改定时任务分页数据 2022-08-20 20:05:35 +08:00
whyour f3de8435f1 修改 favicon 2022-08-20 17:31:01 +08:00
whyour 06723407d2 更新版本 v2.13.8 2022-08-11 19:41:59 +08:00
whyour db8dfb32cf 修改生成系统token逻辑 2022-08-11 19:37:14 +08:00
whyour a7117e4442 修改内置token获取方式 2022-08-11 13:13:37 +08:00
whyour 53414e5d70 更新版本 v2.13.7 2022-07-31 15:58:34 +08:00
whyour 7bbc776759 修复环境变量转义 2022-07-31 15:35:13 +08:00
whyour f123c0bfe1 更新dockerfile 2022-07-31 15:26:08 +08:00
whyour 830865ff77 修改dockerfile 2022-07-31 15:22:17 +08:00
whyour 15fc6f975c repo命令后缀参数竖线分割 2022-07-31 15:09:32 +08:00
whyour 4582711867 修复搜索订阅 2022-07-31 15:00:27 +08:00
迷人的幽幽andGitHub 1eb64720d1 fix: 🐛 修复ts运行环境下发送企业微信通知错误问题 (#1553) 2022-07-24 13:33:44 +08:00
whyour 29f5134fb4 更新版本 v2.13.6 2022-07-18 19:52:37 +08:00
whyour 55ffd85d48 修改环境变量加载路径 2022-07-18 16:16:12 +08:00
whyour 075165f89d 修复ql_dir环境变量 2022-07-18 16:09:34 +08:00
whyour 42dabbf4c0 修复内置token获取 2022-07-18 16:06:04 +08:00
whyour 999b5d325f 修复初始化环境变量 2022-07-18 16:05:57 +08:00
whyour 027c3f584c 修改QL_DIR环境变量 2022-07-18 16:05:42 +08:00
whyour 2d3ebe6087 更新版本 v2.13.5 2022-07-17 22:29:25 +08:00
whyour f016e6734a 修复shell获取时间 2022-07-17 13:24:59 +08:00
whyour 9f0503f138 更新版本 v2.13.4 2022-07-16 12:31:50 +08:00
whyour f74821895e 修改npm镜像源 2022-07-16 12:25:14 +08:00
whyour 872519a213 修复获取环境变量详情 2022-07-16 12:16:33 +08:00
whyour dcd39ea6d8 移除内置token判断 2022-07-11 21:21:35 +08:00
whyour a4a76ec235 任务详情编辑脚本支持全屏 2022-07-04 22:44:51 +08:00
whyour 87a265191c 环境变量增加导出功能 2022-07-04 22:24:50 +08:00
whyour 6f4736bddf 脚本管理按文件类型排序 2022-06-26 22:25:21 +08:00
whyour e85742a293 修复mac和linux兼容性 2022-06-26 18:59:39 +08:00
whyour 28ea160004 修复手动停止任务耗时日志 2022-06-19 14:32:09 +08:00
whyour 99d881fc0d 修复系统token验证 2022-06-18 19:24:56 +08:00
whyour 5d6ac7f07c 修复系统token权限 2022-06-18 19:18:14 +08:00
whyour a23dbfe5e1 修复repo命令 2022-06-18 19:03:50 +08:00
whyour 2d818a19d1 修复repo命令 2022-06-18 19:00:22 +08:00
whyour be89fd4ff7 修改结束message 2022-06-18 12:45:38 +08:00
whyour 6d2edaaaf8 修改日期打印 2022-06-18 12:43:23 +08:00
whyour 19afbd06c0 Merge branch 'develop' of github.com:whyour/qinglong into develop 2022-06-18 12:30:11 +08:00
whyour 718964ab86 修复node kill进程逻辑 2022-06-18 12:30:02 +08:00
二毛andGitHub 0dadd4e6cc 区分原生termux和termux中安装的Linux发行版 (#1498)
检测termux时只看环境变量,以便于区分原生termux和termux中安装的Linux发行版
测试发现${ANDROID_RUNTIME_ROOT}${ANDROID_ROOT}在termux中安装的Linux发行版中仍旧存在,但是发行版中应该不用设置软连接吧
2022-06-14 22:55:49 +08:00
whyour 57e7d756cb 修改系统内部获取token方式 2022-06-14 22:43:18 +08:00
whyour 9f5fb30334 修改更新定时任务参数验证 2022-06-12 21:12:01 +08:00
whyour ec8c339f6e 移除notify软链命令 2022-06-10 21:23:43 +08:00
whyour 728d84cb15 修复根目录环境变量 2022-06-09 16:44:58 +08:00
whyour a7b5a0f4c5 修复shell本地开发兼容性 2022-06-09 12:10:41 +08:00
whyour b009d3e3fd 修复无法更新脚本内容为空 2022-06-08 23:57:22 +08:00
whyour 559defc8d6 修改默认pip源 2022-06-08 22:54:44 +08:00
whyour 98cc983473 更新版本 v2.13.3 2022-06-08 18:39:51 +08:00
whyour 6d42a680b1 移除console 2022-06-08 18:39:10 +08:00
whyour 1602f829ba 修改npm peer dependence策略 2022-06-08 11:19:54 +08:00
whyour c4a4764762 移除无用日志 2022-06-08 11:12:15 +08:00
whyour ac32117e78 修复调试脚本结束条件 2022-06-06 21:25:09 +08:00
whyour 3b5bcf8f16 修复调试脚本目录 2022-06-05 21:25:09 +08:00
whyour 8735321e34 修复调试脚本运行需要先保存 2022-06-05 19:26:32 +08:00
whyour be76ff4675 修复脚本管理和日志管理树字段 2022-06-05 18:44:35 +08:00
whyour 15678b029c 修复粘贴订阅私有仓库字段 2022-06-05 11:56:48 +08:00
whyour 05d47be1c8 修复创建脚本选择目录 2022-06-04 01:26:01 +08:00
whyour 106b34e33a 新建脚本支持文件上传 2022-06-04 00:12:26 +08:00
whyour 488fbd2bbb 更新版本 v2.13.2 2022-06-01 00:28:35 +08:00
whyour b82c630e3f 修改订阅执行逻辑 2022-06-01 00:25:32 +08:00
whyour 5a01c41bbb 修复私钥配置文件匹配 2022-06-01 00:03:58 +08:00
whyour 471e778a61 修改interval_schedule验证 2022-05-30 10:26:30 +08:00
whyour 68e576a5e3 修复订阅添加定时规则 2022-05-30 10:21:15 +08:00
whyour 8c74fca362 修复新建订阅定时 2022-05-30 09:58:51 +08:00
whyour 026fc43695 修复订阅任务创建逻辑 2022-05-29 22:51:36 +08:00
whyour 4f925b92d0 修复依赖安装顺序,优先linux依赖安装 2022-05-29 15:01:48 +08:00
whyour 4200d5e530 系统启动安装依赖优先安装linux依赖 2022-05-29 11:35:31 +08:00
whyour 32ca72133a 更新dockerfile 2022-05-29 11:13:32 +08:00
whyour 7708638267 修复dockerfile 2022-05-29 10:18:08 +08:00
whyour cb24d3c9fa 修复pnpm环境变量 2022-05-29 10:02:03 +08:00
whyour 07c63e285e 更新版本 v2.13.1 2022-05-29 01:50:46 +08:00
whyour a5e8d5cb80 修复pnpm版本兼容 2022-05-29 01:50:05 +08:00
whyour 8892e8833e 修复前端列表查询 2022-05-28 21:49:34 +08:00
whyour 25af639eef 修复依赖日志查看 2022-05-28 21:10:24 +08:00
whyour 58383e9022 修复依赖安装日志 2022-05-28 20:51:25 +08:00
whyour d244650432 修复订阅日志查看 2022-05-28 20:11:38 +08:00
whyour f338537ea0 修复系统启动执行订阅任务 2022-05-28 19:56:12 +08:00
whyour 8d2c62d6d1 依赖改为串行安装 2022-05-28 19:41:37 +08:00
whyour 8b1906c786 修改订阅日志排版 2022-05-28 19:04:46 +08:00
whyour 3f29c1aa4e 修复execSync卡死主进程 2022-05-28 18:55:52 +08:00
whyour 31ff0476a6 修复任务详情日志访问,修复新建订阅粘贴repo命令 2022-05-28 17:36:41 +08:00
whyour b3ed824b2e 修复七牛sdk参数 2022-05-28 00:11:43 +08:00
whyour ec97242415 更新版本 2.13.0 2022-05-28 00:08:48 +08:00
whyour d54faf8044 移除sentry上报信息可能出现的token 2022-05-27 23:47:17 +08:00
whyour 5f380b55d1 修改workflow 2022-05-26 15:12:45 +08:00
whyour 8d46115823 修复切换订阅interval 2022-05-26 14:41:22 +08:00
whyour cc24e51cd7 修改workflow 2022-05-26 00:25:31 +08:00
whyour 7a5e6dba51 更新readme 2022-05-26 00:23:25 +08:00
whyour ad1ee0d2ef 修复定时任务标签默认值 2022-05-26 00:18:04 +08:00
whyour e4f7461b62 修复安装依赖逻辑 2022-05-25 02:04:13 +08:00
whyour a9e0a934a7 修改linux依赖安装命令 2022-05-25 01:23:33 +08:00
whyour 2a23b838f6 修改node依赖安装命令 2022-05-25 01:20:23 +08:00
whyour 1afc791e0b 修改文本 2022-05-25 01:10:04 +08:00
whyour 05cf51afa0 新建订阅支持粘贴导入 2022-05-25 01:09:05 +08:00
whyour 76b6677f5a 修复日志获取 2022-05-24 10:00:42 +08:00
whyour af74afd10d 修复获取脚本列表 2022-05-23 19:22:20 +08:00
whyour 46512142fa 修复主题切换 2022-05-23 12:03:01 +08:00
whyour 6f13cc6e6f 修改订阅before/after执行时机 2022-05-22 22:14:32 +08:00
whyour 9dab6660d0 修改新建订阅文件后缀提示 2022-05-22 21:35:02 +08:00
whyour 89ddfd20cd 修复错误界面user判断 2022-05-22 21:28:30 +08:00
whyour c9bd27ec4f 修复登录跳转 2022-05-22 18:07:48 +08:00
whyour 2e937ad5ae 修改antd样式目录 2022-05-22 00:44:13 +08:00
whyour 39d3d53614 使用ssr-darkreader代替darkreader 2022-05-21 21:19:44 +08:00
whyour 456b4c4136 修复根组件刷新异常 2022-05-21 11:51:16 +08:00
whyour a90e536403 修复spawn监听事件 2022-05-21 00:47:05 +08:00
whyour d96206ef66 修改新建订阅字段提示 2022-05-21 00:12:12 +08:00
whyour ef71e13139 订阅同步表字段 2022-05-20 11:18:40 +08:00
whyour 189826c5db 订阅增加before/after 2022-05-20 01:15:45 +08:00
whyour bdecdce118 修复登录成功获取用户信息 2022-05-19 23:40:19 +08:00
whyour 460a1750f4 增加初始化订阅任务 2022-05-18 23:30:59 +08:00
whyour d2c2ea8d3b 修改生成ssh配置 2022-05-18 20:10:28 +08:00
whyour cb12d8ffec 修复删除私钥 2022-05-18 20:06:46 +08:00
whyour 5bb1afe5c6 修复私钥权限 2022-05-18 19:23:32 +08:00
whyour 7caabe9063 完善拉取私有仓库 2022-05-18 01:14:10 +08:00
whyour 1695b8c0fe 统一api错误提示 2022-05-17 23:31:29 +08:00
whyour 0927682abf 修复订阅执行日志输出 2022-05-17 22:27:41 +08:00
whyour cde4646fe1 修复环境变量PATH 2022-05-17 13:24:19 +08:00
whyour 8c410377b9 修改dockerfile 2022-05-17 12:38:58 +08:00
whyour 312aaf18de action安装pnpm 2022-05-17 12:18:29 +08:00
whyour ac2979381f 移除pnpm action缓存 2022-05-17 12:14:44 +08:00
whyour 03c5c7bbea pnpm action缓存 2022-05-17 12:13:26 +08:00
whyour 513de1a792 pnpm action缓存 2022-05-17 12:12:25 +08:00
whyour c6a8b93b70 pnpm action缓存 2022-05-17 12:10:59 +08:00
whyour 4c8e097448 删除pnpm缓存 2022-05-17 12:06:29 +08:00
whyour 4e7a5c637d 完善拉取公开仓库 2022-05-17 00:42:18 +08:00
whyour f1a51638a5 兼容antd Menu 2022-05-16 01:47:03 +08:00
whyour 9dcc547ac7 重构任务执行逻辑 2022-05-15 20:40:29 +08:00
whyour bb47d67d0b 修复编译错误 2022-05-15 15:35:52 +08:00
whyour f6a122e5ea 更新新建文件订阅 2022-05-15 15:25:23 +08:00
whyour 5523d537dc 完善订阅接口 2022-05-14 21:38:26 +08:00
whyour 5a21247cbb 完善新建订阅页面 2022-05-14 19:21:35 +08:00
whyour 419c5a7c5b 增加订阅api 2022-05-10 19:54:05 +08:00
whyour 468ff9c1af 修复编译错误 2022-05-10 14:42:25 +08:00
whyour ebb6290468 增加新建订阅 2022-05-10 14:38:06 +08:00
whyour 85d8565241 修复设置头像 2022-05-09 15:33:18 +08:00
whyour bd35725b3c 修复设置头像 2022-05-09 15:32:44 +08:00
whyour fb6a80e306 支持更换头像 2022-05-09 15:31:41 +08:00
whyour 2c9b283b75 增加ssh key操作service 2022-05-08 22:41:56 +08:00
whyour 0218405998 更新readme 2022-05-08 11:33:39 +08:00
whyour aa59e9a44b 修复用户名显示 2022-05-08 10:06:56 +08:00
whyour f9e2e22b85 添加订阅管理前端 2022-05-08 09:41:06 +08:00
whyour d27ca56a2d 修复生产环境安装依赖prepare失败 2022-05-03 18:44:00 +08:00
whyour bf40447ba4 修复npm prepare命令 2022-05-03 18:25:52 +08:00
whyour 89f7b99a94 修复pnpm全局安装 2022-05-03 18:07:12 +08:00
whyour c71d34cbac 修复peerDependence 2022-05-03 15:55:39 +08:00
whyour b97ad5cb6b 修复pnpm依赖 2022-05-03 15:43:15 +08:00
whyour 081793e1fb 修改action依赖 2022-05-03 15:06:28 +08:00
whyour f76b7d3b35 修改依赖 2022-05-03 14:59:48 +08:00
whyour 95448418c0 测试pnpm版本 2022-05-03 14:47:59 +08:00
whyour 199070f829 测试pnpm版本 2022-05-03 14:44:10 +08:00
whyour 1fa296a53e 添加依赖rc-tween-one 2022-05-01 14:20:00 +08:00
whyour caa8bd7e7f 修复pnpm版本 2022-05-01 14:15:29 +08:00
whyour 22d097aa08 修改定时任务添加标签交互 2022-05-01 13:51:33 +08:00
whyour 5dbfc65e7f 修复环境变量转义 2022-04-25 23:55:25 +08:00
whyour 03e6f18e54 修复获取最新version文件 2022-04-24 18:43:09 +08:00
whyour 84357e9dcc 更新版本 v2.12.2 2022-04-24 17:50:03 +08:00
whyour 8bb8a29400 任务详情增加运行、禁用、置顶操作 2022-04-24 17:45:50 +08:00
whyour 1b893f35ba 依赖管理增加直接强制删除 2022-04-23 15:02:54 +08:00
JerryWnandGitHub 5be421b4ea 修复双引号转义逻辑 #1331 (#1337) 2022-04-18 22:55:37 +08:00
whyour 8ad325686d 修复定时删除日志设置,主持设置大于24天 2022-04-18 22:29:14 +08:00
whyour 54e58c1e08 修复shell发送通知 2022-04-09 11:13:08 +08:00
whyour 8818a779b5 修复并发日志目录 2022-04-06 12:18:16 +08:00
whyour cc65b6c8bb 更新readme 2022-04-05 23:24:27 +08:00
whyour 523f583516 更新发布脚本 2022-04-05 23:17:03 +08:00
whyour 89539deff1 更新发布脚本 2022-04-05 23:14:47 +08:00
whyour 6907d520ed 增加发布脚本 2022-04-05 23:11:48 +08:00
whyour eae2d6d17a 修复日志目录判断 2022-04-05 00:28:14 +08:00
whyour 82dd9114fd 修复日志目录获取 2022-04-05 00:21:32 +08:00
whyour 58e047fe0e 修复task日志打印 2022-04-04 23:32:27 +08:00
whyour d703769d43 修复日志创建目录 2022-04-04 22:40:02 +08:00
whyour be6ca54d9c 修复目录获取 2022-04-04 18:06:21 +08:00
whyour a5cadfbcab 修改版本说明 2022-04-04 17:28:15 +08:00
whyour 5e3be8fcd1 更新版本号 v2.12.1 2022-04-04 17:24:42 +08:00
whyour 8b5347dd44 修改获取版本链接 2022-04-04 17:17:34 +08:00
whyour 24f6070f9e 修复shell日志路径截取 2022-04-03 22:31:11 +08:00
whyour d00f2cd33b 修改yarn命令 2022-04-03 21:49:20 +08:00
whyour 8276a19012 修复定时任务获取日志 2022-04-03 20:00:31 +08:00
whyour c769c598b8 删除console 2022-04-03 19:50:44 +08:00
whyour d687e79b6c 修复日志路径拼接规则 2022-04-03 19:32:44 +08:00
whyour 8fac58d73f 七牛云改为http访问 2022-04-01 08:29:16 +08:00
whyour b2ba86b212 修复获取日志相对目录 2022-03-30 11:34:36 +08:00
whyour 92673b4a17 修改日志路径合并方式 2022-03-30 11:17:11 +08:00
whyour 85facc039a 修复获取任务日志路径 2022-03-30 10:57:43 +08:00
kan3GitandGitHub 24f33431de tg发送消息改json (#1269)
发现有带&的消息都会漏掉&后面的内容,改为JSON发送
2022-03-26 17:23:54 +08:00
chen310andGitHub 36533ab7e2 修复PushDeer推送Python版本错误 (#1270)
* 修复PushDeer推送Python版本错误
2022-03-26 17:20:02 +08:00
whyour 97d170c8b1 修复系统pushDeer通知 2022-03-26 17:17:13 +08:00
whyour 1413999ac3 修改任务最大内存占用200兆 2022-03-26 16:53:03 +08:00
whyour 0d23f8a962 修复定时任务获取日志目录 2022-03-26 00:29:42 +08:00
whyour d0b13a04cc 更新版本 v2.12.0 2022-03-24 08:47:35 +08:00
whyour fca7b02656 修复bot依赖安装目录 2022-03-19 16:44:00 +08:00
whyour 2afbfa08c6 更新bot.sh 2022-03-17 23:45:37 +08:00
whyour 5fe63666b0 修复任务详情展示脚本逻辑 2022-03-13 13:21:00 +08:00
whyour 9ee08acf29 修复任务详情交互样式 2022-03-13 11:09:31 +08:00
whyour bc7f406968 修复定时任务名称标签显示 2022-03-13 00:06:24 +08:00
whyour 00ee90792f 修复系统更新时间弹窗 2022-03-13 00:03:58 +08:00
whyour 4a8c90365e 修复任务列表名称标签提示 2022-03-12 23:57:38 +08:00
whyour d0e46224dc 修复定时任务详情日志查看 2022-03-12 23:49:09 +08:00
whyour 54dd90e7c5 修复退出登录样式 2022-03-12 18:15:42 +08:00
whyour 7ef3de1367 修复退出登录和定时任务搜索 2022-03-12 16:14:18 +08:00
whyour ce6aa23d5b 修改退出登录样式 2022-03-10 23:58:57 +08:00
whyour dc819bcf8e 修改退出登录样式 2022-03-10 23:53:39 +08:00
whyour b0595656bb 修复编辑应用初始值 2022-03-10 22:37:44 +08:00
whyour 1dac54b7e7 添加podman安装 2022-03-10 00:29:28 +08:00
whyour 95ea085e1e 修复任务详情样式 2022-03-09 23:40:59 +08:00
whyour ec5f6d20cb 修复任务详情手机样式 2022-03-09 22:32:50 +08:00
whyour aa288c89b3 修复dockerfile 2022-03-09 00:24:36 +08:00
whyour ea419a98ae 修复dockerfile 2022-03-09 00:17:33 +08:00
whyour 0f0c79f52f 任务详情脚本增加编辑功能 2022-03-08 00:11:37 +08:00
whyour 38df66a5b1 修复任务详情样式 2022-03-07 23:56:15 +08:00
whyour 0051260171 修改更新青龙日志 2022-03-07 23:47:20 +08:00
whyour f6dc0e020f 修改重置分支shell 2022-03-07 23:40:49 +08:00
whyour 6f2e7b9459 更新git pull参数 2022-03-07 23:27:16 +08:00
whyour 384e3f09da 修复编译静态资源目录 2022-03-07 23:21:18 +08:00
whyour 747f4552e0 修改shell根目录获取,移除notify.sh 2022-03-07 23:16:36 +08:00
whyour 1db13653bf 修复更新青龙命令 2022-03-07 22:33:15 +08:00
whyour d05304f839 修复重置repo地址命令 2022-03-07 22:31:46 +08:00
whyour 0b551ddc0d 修复更新青龙命令 2022-03-07 22:27:04 +08:00
whyour f117f6b39c 修复shell更新命令 2022-03-06 11:27:48 +08:00
whyour 0c54ca95d9 修改定时任务初始化查询条件 2022-03-06 00:07:59 +08:00
whyour bcbc8a633f 更新版本拷贝2.11.3以前版本数据到data目录 2022-03-06 00:07:08 +08:00
whyour 73963c89fc 自动更新定时任务2.11.3以前脚本路径 2022-03-06 00:01:03 +08:00
whyour 793d14f04b 修改任务详情样式 2022-03-05 20:52:21 +08:00
whyour ef32181f36 移除s390x支持,修改sqlite3包 2022-03-05 14:39:06 +08:00
whyour 419f59818b 修改sqlite3包 2022-03-05 13:42:45 +08:00
whyour 2bda3a276a 修改sqlite3包 2022-03-05 12:57:48 +08:00
whyour 5e5b28ef29 修改 sqlite3 依赖 2022-03-05 12:18:30 +08:00
whyour 8dfbe93135 修改update对应分支 2022-03-05 11:46:21 +08:00
whyour 810c6ca76c 任务详情添加日志列表,更新依赖 2022-03-05 01:24:52 +08:00
whyour 912b4ddb76 logo使用七牛云 2022-03-04 00:15:42 +08:00
whyour 6b41df3497 增加任务详情 2022-03-04 00:08:33 +08:00
NekoMioandGitHub 3e62fedc73 增加PushDeer推送 #1161 (#1194)
* 增加PushDeer推送
2022-03-03 18:42:11 +08:00
whyour a10ffef38f 修复task修改环境变量时机 2022-03-02 21:37:45 +08:00
whyour 816ce31279 修改openapi获取token逻辑 2022-02-28 22:30:58 +08:00
whyour 4e58a640e4 修改error页面样式 2022-02-26 14:24:23 +08:00
whyour 43925d6630 修复error页面样式 2022-02-26 14:16:51 +08:00
whyour b688461137 修改错误日志页面 2022-02-26 14:14:10 +08:00
whyour 1e85528889 修改默认跳转 2022-02-26 00:13:52 +08:00
whyour eb4976ed6b 添加系统错误页面 2022-02-26 00:12:12 +08:00
whyour 7dd87220ab 修改public服务 2022-02-25 23:06:35 +08:00
whyour 9927461ea9 修复public服务 2022-02-25 22:53:24 +08:00
whyour 84c7ffc509 增加2.10-2.11升级代码 2022-02-25 22:49:17 +08:00
whyour ee52f09bcb 增加public服务,查询panel日志 2022-02-24 23:50:04 +08:00
whyour 531fc48bdf 修改pane样式 2022-02-24 23:24:21 +08:00
whyour 0581a7ab10 更新git pull参数 2022-02-24 22:36:49 +08:00
whyour f0d2d57584 修复git reset 2022-02-24 22:22:33 +08:00
whyour cb154437ac 增加使用系统通知配置发送通知api 2022-02-24 22:10:03 +08:00
whyour 7ea9a8aa57 修改编辑器cdn 2022-02-20 21:36:01 +08:00
whyour 7e34f4990d 修复sql查询 2022-02-20 10:15:17 +08:00
whyour f0f2c6b537 修改杀死进程逻辑 2022-02-19 22:54:27 +08:00
whyour f71b3d0378 版本文件改为七牛云存储 2022-02-19 22:48:06 +08:00
whyour 7d6e1d3e3d 修复定时删除日志 2022-02-19 17:36:49 +08:00
whyour cf5f1b6f25 调整数据目录 2022-02-19 13:08:14 +08:00
whyour 903007e6ca 修复删除环境变量无效 2022-02-17 11:14:43 +08:00
whyour d6614acf66 更新版本 v2.11.3 2022-02-16 23:09:33 +08:00
whyour 86df495b67 修改check命令,安装依赖使用代理
修改apk源地址
2022-02-16 23:06:12 +08:00
whyour 71b0c10997 修改logo地址 2022-02-16 22:56:54 +08:00
whyour e273c59d78 移除esbuild 2022-02-11 13:52:12 +08:00
whyour baea40f157 环境变量增加更新时间排序 2022-02-11 13:41:48 +08:00
Ghost LeeandGitHub a809b89bb0 修复goCqHttpBotToken中存在&字符导致认证失败的问题 (#993)
修改goCqHttpBot鉴权方式,调整为使用Header模式
2022-02-08 18:45:24 +08:00
UkennandGitHub 89e08953cb bark支持自定义推送图标 (#1146)
* bark添加自定义推送图标

* 脚本bark通知添加自定义推送图标
2022-02-08 18:21:37 +08:00
whyour e154cf65b6 修复定时任务排序 2022-02-01 22:33:18 +08:00
whyour d7162a24be 修复.env 2022-01-30 20:22:00 +08:00
whyour 16532720d7 更新版本 v2.11.2 2022-01-30 18:25:18 +08:00
whyour 012ee2419d 修改placeholder 2022-01-30 17:39:27 +08:00
whyour 7e4fe2f1b3 更新dockerfile 2022-01-30 14:59:41 +08:00
whyour 5a95b72607 修改dockerfile 2022-01-30 14:03:57 +08:00
whyour 4d27055605 修改base context 2022-01-30 12:37:20 +08:00
whyour e2b9578253 修改dockerfile 2022-01-30 12:31:49 +08:00
whyour 57fd8f378c 修改定时任务唯一键 2022-01-30 10:11:47 +08:00
whyour a726f224f8 修改定时任务排序 2022-01-30 09:59:33 +08:00
whyour d502ac6e0f 修改dockerfile 2022-01-29 14:23:19 +08:00
whyour f6a828f0af 修改dockerfile 2022-01-29 12:53:21 +08:00
whyour 0a53a7001e 修改dockerfile 2022-01-29 12:15:25 +08:00
whyour 0ab5494744 修复dockerfile 2022-01-29 11:45:06 +08:00
whyour e60cf15f39 更新dockerfile 2022-01-29 11:20:43 +08:00
whyour b53db97b98 修改dockerfile 2022-01-29 00:09:37 +08:00
whyour f12c75f91b 移除prepare scripts 2022-01-28 23:46:17 +08:00
whyour 3df1a14f72 修改sentry依赖 2022-01-28 23:19:39 +08:00
whyour 712f105d34 修改sentry cli版本 2022-01-28 23:14:12 +08:00
whyour 4ea757fece 修改sentry依赖 2022-01-28 22:47:46 +08:00
whyour d8b32f63ea 增加esbuild 2022-01-28 21:53:05 +08:00
whyour 771b81afaf 修改dockfile 2022-01-28 20:54:00 +08:00
whyour a4f4c6ac15 修改dockerfile 2022-01-28 20:27:38 +08:00
whyour ae22cf064e 修复dockerfile 2022-01-28 19:33:01 +08:00
whyour fbb8e1bd31 修改dockerfile 2022-01-28 19:23:57 +08:00
whyour 47202ef66c 修改base镜像 2022-01-27 23:09:29 +08:00
whyour f46c70dcbd 测试青龙基础镜像 2022-01-27 23:06:35 +08:00
whyour c7a837e33d 测试最新alpine 2022-01-27 22:21:34 +08:00
whyour 544e2e8331 修复获取克隆仓库状态 2022-01-27 22:15:45 +08:00
whyour a62ccbc0db 去除sql执行日志 2022-01-27 00:56:37 +08:00
whyour 891619ad55 修改表唯一约束 2022-01-27 00:49:44 +08:00
whyour a3a62f65dd python默认实时输出 2022-01-25 23:03:56 +08:00
whyour 3131f197b8 修复修改任务状态本地免认证 2022-01-25 22:52:27 +08:00
whyour e13d9ed51b 添加关于页 2022-01-24 23:17:12 +08:00
whyour 1b5f4a0f9d 修复日志加载状态 2022-01-24 22:50:01 +08:00
whyour c9c4043a4e 修改logo地址 2022-01-23 16:55:11 +08:00
whyour bb59dd977a 修改node基础镜像版本 2022-01-22 22:09:16 +08:00
whyour c2b213a37c 测试基础镜像node版本 2022-01-22 21:57:02 +08:00
whyour 559f4418d1 修改darkreader版本 2022-01-22 21:09:01 +08:00
whyour 2149bca7b8 增加sqlite连接池 2022-01-22 20:53:22 +08:00
whyour be5db82b79 移除cdn依赖 2022-01-22 20:25:22 +08:00
whyour 01163e3ea3 定时任务默认改为创建时间倒序 2022-01-21 22:38:13 +08:00
whyour 7f986fdb2d 修复更新系统通知 2022-01-21 22:27:50 +08:00
whyour 38e1ba67af 修复sqlite加载顺序 2022-01-20 22:19:39 +08:00
whyour d3d2e0887e 更新版本 v2.11.1 2022-01-20 19:11:41 +08:00
whyour a2650715b3 修改cdn地址,增加重启服务延迟 2022-01-20 19:02:29 +08:00
whyour 8c0685202e 移除默认github代理,增加http或者socks代理参数 2022-01-19 23:29:55 +08:00
whyour 5b6843b5c0 定时任务默认倒序 2022-01-18 23:50:18 +08:00
whyour ba341b69ac 修改迁移数据库逻辑 2022-01-18 22:56:12 +08:00
whyour ca89f03724 修复环境变量更新时间显示 2022-01-17 22:47:23 +08:00
whyour 5461325e69 修复重置应用秘钥 2022-01-16 18:55:18 +08:00
whyour e96a2818b7 修复停止任务后任务状态修改 2022-01-14 23:44:22 +08:00
whyour 355f06a3cf 修改数据迁移逻辑 2022-01-14 23:17:16 +08:00
whyour f6e6cfa71c 修复环境变量搜索 2022-01-13 22:38:20 +08:00
ChiupamandGitHub 9cdfa2ef79 支持运行pyc格式文件 (#1072) 2022-01-13 14:39:49 +08:00
whyour 6fab22099d 修复更新定时任务 2022-01-13 14:37:56 +08:00
whyour 925819b3aa 修复环境变量搜索 2022-01-13 13:15:00 +08:00
whyour 6f96900ab9 修复openapi token 查询 2022-01-13 12:02:36 +08:00
whyour 43bc29b25c 修复openapi认证,环境变量排序 2022-01-13 11:13:04 +08:00
whyour e13ff86ff6 更新版本 v2.11.0 2022-01-12 23:43:32 +08:00
whyour 3f3d572932 更新task命令获取时间戳 2022-01-12 23:39:40 +08:00
whyour a0ce1562ac 更新dockerfile 2022-01-12 00:55:14 +08:00
whyour ee84cbca51 修复dockerfile 2022-01-11 00:44:22 +08:00
whyour 3ed5cb48b5 修复dockerfile 2022-01-11 00:35:30 +08:00
whyour 499ba5fb99 修复dockerfile 2022-01-10 23:35:47 +08:00
whyour c2f036c370 修复dockerfile 2022-01-10 23:17:33 +08:00
whyour 326580b3c8 测试编译sqlite 2022-01-10 23:10:59 +08:00
whyour bf59351cf9 测试nodejs14 sqlite 2022-01-09 23:17:50 +08:00
whyour a954fc3942 测试打包镜像时间 2022-01-09 12:38:20 +08:00
whyour 160a66af23 更新dockerfile 2022-01-08 23:56:45 +08:00
whyour 4edbd29b19 测试arm/v7 2022-01-08 23:29:20 +08:00
whyour b6b97478d1 修改dockerfile 2022-01-08 22:49:17 +08:00
whyour 27a871e537 更新node gyp 2022-01-08 22:19:21 +08:00
whyour 738b0f9a4a 修改node gyp版本 2022-01-08 21:48:32 +08:00
whyour 1a56dd8ac6 添加node-pre-gyp依赖 2022-01-08 21:12:38 +08:00
whyour 672d440725 修复方法名称 2022-01-08 01:26:34 +08:00
whyour 3625b7c0de 更新dockerfile 2022-01-08 01:10:10 +08:00
whyour e75a683173 修复sqlite数据操作类型 2022-01-08 01:09:38 +08:00
kilo5hzandGitHub 89ed8527d6 添加标签功能 (#1026)
* 添加标签功能
2022-01-07 22:01:13 +08:00
whyour 5a5f4b8065 dockerfile使用yarn install 2022-01-07 00:18:12 +08:00
whyour d2e23341eb 修改sqlite版本 2022-01-06 23:25:48 +08:00
whyour 233081149b 修复param类型 2022-01-06 22:59:29 +08:00
whyour 5d19ee0ab5 使用sqlite替换nedb 2022-01-06 22:51:12 +08:00
ChiupamandGitHub 653b1cef20 支持自定义BOT仓库 (#1051) 2022-01-04 13:26:16 +08:00
whyour f3ecf6e7a0 更新版本 v2.10.13 2021-12-25 21:31:52 +08:00
whyour 35d8f0a7e8 添加sentry-cli cdn 2021-12-25 20:55:13 +08:00
whyour e863dd322a 修改热更新目录 2021-12-25 20:13:49 +08:00
whyour fa6dec969f 修改两步验证界面逻辑 2021-12-25 20:09:02 +08:00
whyour 58cd05fd09 修改批量运行任务逻辑 2021-12-24 23:31:11 +08:00
whyour 2e6ca5419d 调试脚本增加停止,修复调试交互 2021-12-23 23:25:39 +08:00
whyour a29392bb08 修改文本 2021-12-23 13:39:16 +08:00
whyour 4dc9c9aa75 优化初始化通知设置交互 2021-12-21 23:49:52 +08:00
whyour 03c9f79549 修复登录退出接口路径 2021-12-21 23:42:05 +08:00
whyour 46aaeb4eac 增加系统openapi 2021-12-21 23:22:34 +08:00
whyour 4d20bb0de6 system接口增加版本号,应用权限增加依赖管理 2021-12-21 22:53:19 +08:00
琚涛涛andGitHub e001686627 优化定时任务名称展示效果,提升用户体验 (#1017) 2021-12-21 22:39:17 +08:00
whyour 931f02ef8e 修复任务跳转脚本参数,task运行脚本支持更多参数 2021-12-21 00:23:20 +08:00
whyour a9be3e6376 更新版本号 v2.10.12 2021-12-09 23:24:10 +08:00
whyour aedd369fc4 修复darkreader最新版bug 2021-12-09 23:22:17 +08:00
whyour a1016b7f22 更新readme 2021-12-06 23:42:14 +08:00
whyour 603432aa87 修复logo样式 2021-12-06 11:18:19 +08:00
whyour cd23967ae0 更新版本号 v2.10.11 2021-12-05 23:15:49 +08:00
whyour dfb159c4ad 修复搜索环境变量urlencode 2021-12-05 23:04:26 +08:00
whyour 0318f03283 修复内容区高度和logo样式 2021-11-29 23:02:42 +08:00
whyour 9ddf6986e4 修复最后运行时长被覆盖 2021-11-29 21:49:32 +08:00
雪狐andGitHub a28ed9281e 避免环境变量中出现"字符的时候提前截断字符串 (#966) 2021-11-29 21:43:29 +08:00
whyour 9cc776aded 更新版本 v2.10.10 2021-11-27 20:43:18 +08:00
colinxuandGitHub 349f14a7fc 修复使用旧pushplus推送后结果解析失败的问题 (#958) 2021-11-27 20:33:49 +08:00
whyour fae26efb88 检测更新增加强制更新 2021-11-27 20:19:49 +08:00
whyour 64d0bb18b1 依赖目录通知文件没有时,拷贝示例的通知文件 2021-11-27 17:41:26 +08:00
whyour 864ce071f6 修复deps映射目录 2021-11-27 17:19:50 +08:00
whyour 5a55934029 修复build args 2021-11-27 17:12:45 +08:00
whyour 1f061ad059 测试yml 2021-11-27 17:05:55 +08:00
whyour 32f0774df2 添加deps目录软链 2021-11-27 16:20:24 +08:00
whyour 3206ff6a1e 修复删除JavaScript进程,修改依赖重新安装逻辑 2021-11-27 13:41:38 +08:00
whyour 2f5e946979 修复调试运行时文件路径 2021-11-23 23:36:43 +08:00
phoenixandGitHub 5a6d3739d4 fix nginx can not run without /run/nginx (#944)
alpine3.12 和 alpine3.14 兼容性问题
2021-11-23 18:32:22 +08:00
whyour fa018b84a7 更新版本 v2.10.9 2021-11-22 22:27:15 +08:00
whyour a686206e21 默认拷贝deps目录下所有文件 2021-11-22 22:20:13 +08:00
风之凌殇andGitHub 6132d13261 表格选择pageSize增加200/500/1000 (#934) 2021-11-22 19:26:02 +08:00
kilo5hzandGitHub f64c78919d 系统通知增加gotify (#936) 2021-11-22 19:23:40 +08:00
whyour 1a1067966c 修改docker基础镜像为alpine3.12,解决arm32无法启动 2021-11-22 19:22:39 +08:00
whyour ec545cabcd 更新readme 2021-11-21 22:26:22 +08:00
whyour 40b0e90c0d 修复调试功能 2021-11-20 01:06:25 +08:00
kilo5hzandwhyour 5e94be21aa 点击任务名跳转脚本管理编辑脚本 (#929)
* 增加捷径:点击任务名跳转脚本管理编辑脚本

* 修正树节点选中,父节点自动展开
2021-11-20 01:06:25 +08:00
whyour 417ae8bbde 更新readme图片链接 2021-11-20 01:06:25 +08:00
whyour acde7ebef6 更新readme 2021-11-20 01:06:25 +08:00
whyour a424e8df7c 更新readme 2021-11-20 01:06:25 +08:00
whyour 2eed5cba14 增加初始化依赖目录及文件 2021-11-20 01:06:25 +08:00
风之凌殇andwhyour 19e7c238f6 移除dockerfile中不必要的环境变量 (#925)
* 移除上次pr新增的不必要的环境变量
2021-11-20 01:06:16 +08:00
whyour f3c939e442 更新readme 2021-11-17 19:22:40 +08:00
whyour 3bcf3431d3 修复拷贝deps已存在依赖 2021-11-17 10:23:45 +08:00
whyour 20d6308bc7 更新版本 v2.10.8 2021-11-16 14:36:13 +08:00
whyour 96b0127811 修复脚本管理删除、新建增加选择父目录 2021-11-16 14:18:02 +08:00
kilo5hzandGitHub 68cc792d6c 增加gotify推送 (#905)
* 增加gotify推送
2021-11-15 23:04:55 +08:00
whyour d0f7449b57 修复拷贝deps 2021-11-13 22:05:54 +08:00
whyour b41c1f1d2e 修复dep目录 2021-11-13 21:54:45 +08:00
whyour 9a8b72fe3f 更新版本 v2.10.7 2021-11-13 21:21:35 +08:00
whyour b16f8b96dc 修复bot安装依赖判断 2021-11-13 20:58:27 +08:00
whyour 128429a6a9 修改拷贝依赖 2021-11-13 19:05:34 +08:00
whyour b849d8b526 修复脚本管理搜索 2021-11-13 17:42:06 +08:00
whyour 47b06c3487 外部依赖改为系统直接分发通知文件js和py版 2021-11-13 17:21:27 +08:00
whyour 40e0d143f4 修复环境变量表格样式 2021-11-13 16:44:21 +08:00
whyour c1269d9dde 修复schedule服务 2021-11-13 12:34:49 +08:00
whyour 6a81ba9069 修复脚本管理树结构 2021-11-13 11:14:43 +08:00
whyour 61883776f2 更新版本 v2.10.6 2021-11-13 00:03:12 +08:00
whyour f7752cec3a 修复检查更新 2021-11-12 23:55:57 +08:00
whyour 6f96c84a16 修复定时匹配 2021-11-12 23:42:11 +08:00
whyour a2ce3304de 修复脚本管理数据 2021-11-12 23:18:24 +08:00
whyour ee0b47d101 修改workflow变量 2021-11-12 22:09:02 +08:00
MiniersandGitHub 3b3dd4c26d 增加环境变量更新时间 (#887) 2021-11-12 21:55:03 +08:00
风之凌殇andGitHub bee1f9968e 增加在添加语义化版本tag时自动生成static并构建多个不同tag的容器的workflow,简化版本发布流程 (#888) 2021-11-12 21:54:42 +08:00
whyour 65691cf7c9 更新版本号 v2.10.5 2021-11-12 14:23:16 +08:00
whyour 7b7bcf600d 修复文件判断引起服务异常 2021-11-12 14:22:21 +08:00
whyour 18cd7bfa6f 更新版本号 2021-11-12 13:18:32 +08:00
whyour 8c14a31967 修复entrypoint启动失败 2021-11-12 13:16:53 +08:00
whyour 6c840b435e 修改版本说明序号 2021-11-11 23:04:29 +08:00
whyour eca615c066 更新版本号 v2.10.3 2021-11-11 23:03:28 +08:00
whyour 22bb785139 服务重启时依赖改为单独安装,避免同时失败 2021-11-11 22:56:47 +08:00
whyour 62fca3dfe2 修复获取日志判断 2021-11-11 22:50:28 +08:00
whyour 9936097086 单个数据库文件大于500兆时,裁剪文件,避免服务无法启动 2021-11-11 22:41:12 +08:00
whyour 50dd235e39 脚本管理改成树结构 2021-11-11 00:10:05 +08:00
whyour 606bac2e43 修复pushplus通知设置 2021-11-10 23:33:19 +08:00
whyour 90ede97797 修复依赖文件获取 2021-11-10 23:26:21 +08:00
whyour 1545cde58a 修复repo依赖路径 2021-11-10 10:35:53 +08:00
whyour 8481d81257 修复shell语法 2021-11-09 23:55:50 +08:00
whyour 0688d4f318 修复repo文件比对 2021-11-09 23:23:07 +08:00
whyour ab5ab951d7 修改ql repo逻辑 2021-11-09 23:07:43 +08:00
whyour 0ca640acbd 添加动态引入 2021-11-09 22:34:24 +08:00
whyour 235f99c0ca 修改退出下拉样式 2021-11-08 22:56:53 +08:00
whyour 31fdf7f12c 修改数据库加载 2021-11-08 22:47:46 +08:00
whyour bd6d711212 修复centos -x兼容性 2021-11-07 21:54:31 +08:00
whyour 62596f27ef 回退shell [ 2021-11-07 19:18:36 +08:00
whyour 1b88853a8d 修改pnpm安装源 2021-11-07 12:51:10 +08:00
whyour 3c5925ff67 修复entrypoint 2021-11-07 11:25:18 +08:00
whyour 7d5687d504 修复 arm 没有nginx conf.d 目录 2021-11-07 11:08:21 +08:00
OreoandGitHub 6b74cccc34 更新 Python 通知文件 (#832)
已在 Python3.6.x(云函数平台),3.8.x,3.10.0 测试通过
特性如下
1. 支持 11 种推送方式,未来还会继续添加
2. 多线程通知
3. 支持一言(一句话)和控制台输出开关
4. 对于现有推送方式,补充了配置变量
2021-11-06 20:45:53 +08:00
whyour 1becb26aaf 更新dockerfile 2021-11-06 20:19:07 +08:00
whyour bd2917822e 修改dockerfile 2021-11-06 19:04:01 +08:00
whyour 9c82db4471 更新docker yml 2021-11-06 18:58:18 +08:00
whyour e4f11156f6 修复任务日志结束时间展示 2021-11-06 00:50:44 +08:00
whyour c7de19d3b5 修复toLocaleString 0点显示24点 2021-11-05 23:56:16 +08:00
whyour 65ade4b996 修改shell语法 2021-11-05 23:39:22 +08:00
whyour 2b70851074 修复移动端展开导航 2021-11-04 19:24:55 +08:00
whyour b7b983229b 更新版本号 v2.10.2 2021-11-04 18:10:43 +08:00
whyour ac548433c0 修复基础镜像pip3依赖 2021-11-04 18:10:14 +08:00
whyour 02a1afb477 修复openapi路由 2021-11-04 18:07:34 +08:00
whyour e959466898 更新版本号 v2.10.1 2021-11-04 15:09:07 +08:00
whyour 5c5aef64b0 添加白屏时间及错误上报 2021-11-04 15:05:34 +08:00
whyour 86e8b1383c 修复check命令 2021-11-03 16:35:18 +08:00
whyour 9f7bf1f6b8 修改失败任务日志 2021-11-01 23:06:23 +08:00
whyour 2ff3869608 shell 中 [ 改为 [[ 2021-11-01 23:04:10 +08:00
whyour 8d21401f88 移动退出登录位置 2021-11-01 15:23:06 +08:00
whyour b92daedea2 修复表格滚动高度 2021-10-31 19:45:51 +08:00
whyour 021d89379a 修复无法覆盖安装依赖 2021-10-31 17:34:19 +08:00
2b61204233 修改错误文字 (#809)
Co-authored-by: 14K <9112790+the14k@user.noreply.gitee.com>
2021-10-29 23:26:44 +08:00
whyour 1da1659b51 支持服务端端口直接访问前端 2021-10-28 23:35:21 +08:00
whyour 1302a6daa0 修复置顶后编辑置顶消失 2021-10-28 22:38:34 +08:00
whyour 160dbe780c 更新版本号 v2.10.0 2021-10-24 17:51:20 +08:00
whyour d047cf0d77 修复python移除依赖命令 2021-10-24 17:19:14 +08:00
whyour 0c2a2af308 添加依赖管理 2021-10-24 17:07:16 +08:00
whyour bd8953ea81 依赖安装日志增加安装时间 2021-10-24 11:11:23 +08:00
whyour a479f0fbce 修复表格初次获取滚动高度 2021-10-23 23:44:11 +08:00
whyour 2e00e80e96 由于浏览器兼容性,移除dynamicImport 2021-10-23 23:31:46 +08:00
whyour 0bf4aec74e 修复python3任务无法停止 2021-10-23 23:22:08 +08:00
whyour bcac3deb6e 修复service引入路径 2021-10-23 18:46:24 +08:00
whyour 596f93623e 初始化安装所有依赖 2021-10-23 18:44:25 +08:00
whyour c74a1bc20c 修改依赖安装结束标识 2021-10-23 18:27:20 +08:00
whyour 795d1b938d 添加依赖管理 2021-10-23 18:23:32 +08:00
whyour bad247c51c 两步验证输入框自动聚焦 2021-10-21 22:25:15 +08:00
whyour 950aed3522 更新版本号 v2.9.10 2021-10-21 21:59:24 +08:00
longjuanandGitHub 8a7bd53f24 更新install.md (#789) 2021-10-21 21:55:26 +08:00
whyour 04230cbd1c 修改websocket日志 2021-10-21 21:52:06 +08:00
whyour d0a7e67138 修复table滚动高度 2021-10-21 19:13:38 +08:00
whyour 56517fd345 更新monaco 2021-10-19 22:41:59 +08:00
whyour 6315425a7b 检查密码默认值 2021-10-19 21:46:24 +08:00
whyour c42299766c 修复tg系统通知认证参数 2021-10-18 19:18:46 +08:00
whyour ef9fa02145 修复检查更新日志展示 2021-10-18 17:05:38 +08:00
whyour 51869b6a84 修复shell变量重复 2021-10-16 19:32:23 +08:00
whyour d12953a345 升级umi 2021-10-16 17:21:19 +08:00
whyour 9f727cd54b 添加 yarn.lock 2021-10-16 17:07:05 +08:00
whyour 3de567191e 更新版本号 v2.9.9 2021-10-16 15:19:53 +08:00
whyour b212762417 脚本管理移动端支持添加脚本、编辑脚本 2021-10-16 15:11:23 +08:00
hanhh d5f619ff74 移除无用代码 2021-10-16 13:39:18 +08:00
hanhh 87027f8a8c 修复表格样式 2021-10-16 10:53:14 +08:00
hanhh 6fa8a678bf 修改pm2任务日志 2021-10-15 22:32:32 +08:00
hanhh f2aa355389 修复更新日志匹配正则 2021-10-15 22:14:42 +08:00
hanhh b3ef432c6f 放开ql update删除操作 2021-10-15 22:01:34 +08:00
hanhh 860a1e4dc6 更新版本号 v2.9.8-001 2021-10-15 11:47:32 +08:00
hanhh f5b322649d 修复设置删除日志 2021-10-15 10:53:27 +08:00
phoenixandGitHub 746650b49a 修复nginx.conf错误 (#776)
Fix nginx: [emerg] unknown directive.
Log
`nginx: [emerg] unknown directive "client_body_buffer_size:" in /etc/nginx/nginx.conf:18
`
2021-10-15 10:40:58 +08:00
hanhh 43ddc54a70 更新版本 2.9.8 2021-10-14 22:07:21 +08:00
hanhh bdfa08bb78 修复初始化流程 2021-10-14 22:00:36 +08:00
hanhh 07e58a484c 增加日志删除频率设置 2021-10-14 10:58:54 +08:00
hanhh a652e1e190 修改版本更新执行命令 2021-10-13 23:50:25 +08:00
hanhh ec0deeff06 添加ql -l update 2021-10-13 23:45:46 +08:00
hanhh 7335e2c299 更新nginx配置文件 2021-10-13 23:31:38 +08:00
hanhh 003cdd1e06 修复websocket连接域名 2021-10-13 23:09:48 +08:00
hanhh efa1fcfbdf 修复版本号匹配正则 2021-10-13 22:53:10 +08:00
hanhh 7acda0c039 修复版本号匹配 2021-10-13 22:45:24 +08:00
hanhh cb86fda169 移除远程版本更新链接 2021-10-13 22:41:59 +08:00
hanhh 324d106693 修复获取最后版本文件 2021-10-13 22:39:59 +08:00
hanhh 0644edb62e 修复获取最新版本号 2021-10-13 22:05:54 +08:00
hanhh 0f31cb5fcc 容器启动忽略config和env中的错误 2021-10-13 14:25:38 +08:00
hanhh c80a9d1e65 修复版本检查 2021-10-12 23:06:28 +08:00
hanhh b26d0b4887 完善检查更新操作 2021-10-12 22:54:17 +08:00
hanhh ed93b919ba 并发命令支持指定账号 2021-10-12 21:58:41 +08:00
hanhh b1077443a3 添加系统更新操作和设置删除日志频率 2021-10-12 00:27:42 +08:00
hanhh 9455ca64a2 添加检查更新和删除日志频率操作 2021-10-08 23:02:45 +08:00
hanhh d6ebd2699e 修复表格滚动样式和交互 2021-10-08 22:39:17 +08:00
hanhh 632514d36e 增加新版本检测 2021-10-07 15:26:25 +08:00
hanhh 686baa56f4 增加环境变量名称正则校验 2021-10-06 16:38:46 +08:00
hanhh a3006af7b8 添加新版本提醒 2021-10-06 16:22:53 +08:00
hanhh ec529bfca6 更新版本号 v2.9.7 2021-10-03 22:26:16 +08:00
SowevoandGitHub a661b6bd38 增加企业微信机器人推送的python版本 (#748) 2021-10-03 22:16:07 +08:00
hanhh 73e85c2def 修复登录两步提醒样式 2021-10-03 22:05:56 +08:00
hanhh 6819487a43 修改初始化判断条件 2021-10-03 22:01:44 +08:00
hanhh 14b20873c7 增加初始化过程 2021-10-03 20:58:55 +08:00
hanhh bd1ca7e975 修复entrypoint启动nginx配置 2021-09-30 23:49:19 +08:00
hanhh c01ec828e2 ql update添加patch 2021-09-30 19:14:40 +08:00
hanhh 47abdb0f36 移除check中init git 2021-09-30 19:08:13 +08:00
hanhh 6d4568cdcd 更新版本号 v2.9.6 2021-09-29 23:16:59 +08:00
hanhh 2068ef7053 修复类型检查 2021-09-29 23:15:09 +08:00
hanhh 5655591215 修复退出登录获取token出错 2021-09-29 23:09:13 +08:00
hanhh feb22f25b8 对比工具支持对比config目录下的文件 2021-09-29 22:48:49 +08:00
hanhh c054259806 重启服务时自动压缩db 2021-09-29 21:40:35 +08:00
hanhh b179649078 修复loading状态位置 2021-09-29 21:19:25 +08:00
hanhh a75561399d 修复清空文件操作 2021-09-27 22:04:47 +08:00
hanhh fa02df0d8f task和ql命令添加基础目录检测 2021-09-27 19:38:06 +08:00
hanhh f17642b243 测试动态加载 2021-09-25 12:11:13 +08:00
hanhh 43773222f8 修改定时任务列表滚动宽度 2021-09-25 11:40:55 +08:00
hanhh 91a5e13cf2 修复status接口参数 2021-09-25 02:00:50 +08:00
hanhh be4efbad5d 修复并发参数 2021-09-25 01:51:18 +08:00
hanhh da3e63e6c3 更新版本号 2021-09-25 00:43:05 +08:00
hanhh 2d427c0670 增加登录设备展示 2021-09-25 00:42:03 +08:00
hanhh 97184d22c3 修复更新任务状态api 2021-09-25 00:28:18 +08:00
hanhh f1cc7641fb 修复ql resettfa命令 2021-09-25 00:15:23 +08:00
hanhh ca9d742359 ql resettfa增加兼容字段 2021-09-24 23:33:52 +08:00
hanhh 1d82310ca1 移除定时任务左右列固定n 2021-09-24 23:31:50 +08:00
hanhh 687afce245 ql bot移除自动更新远程仓库 2021-09-24 23:22:40 +08:00
hanhh 26d986d4a8 修复服务端ts类型检查 2021-09-24 22:49:14 +08:00
hanhh 120599c778 更新版本号 2021-09-24 22:30:09 +08:00
hanhh 54945f2183 修复定时任务操作宽度 2021-09-24 22:28:25 +08:00
hanhh f48b91dc0a 支持多端登录 2021-09-24 22:24:39 +08:00
hanhh 4c1551c309 修复task运行指定账号 2021-09-24 00:24:54 +08:00
hanhh 558478be16 修复定时任务列表下次运行时间 2021-09-24 00:11:14 +08:00
hanhh 4e403d3d8a task支持指定账号运行 2021-09-23 23:59:53 +08:00
hanhh a79fd74130 修改定时任务表格布局 2021-09-23 22:12:19 +08:00
hanhh af2f3deeab 添加最后运行时间和运行时长 2021-09-23 13:42:32 +08:00
hanhh c15ff89746 修复主题切换全局class未变化 2021-09-21 23:29:38 +08:00
hanhh 5b585e0cda 定时任务添加下次运行时间 2021-09-21 20:11:39 +08:00
hanhh 6619374571 修改登录通知消息 2021-09-20 23:56:20 +08:00
hanhh 9fc8fdd9d1 修复pushplus通知参数 2021-09-20 22:23:56 +08:00
hanhh 900d279bf6 修复新建文件 2021-09-20 00:59:38 +08:00
hanhh 265760146f 添加bak目录ignore 2021-09-20 00:47:58 +08:00
hanhh 74fa8ffcd1 修改测试通知内容 2021-09-19 23:10:58 +08:00
hanhh e5313e63a9 更新版本号 2021-09-19 23:00:53 +08:00
hanhh f482796cdf 修复通知 2021-09-19 23:00:21 +08:00
hanhh aa08f489d7 定时任务增加置顶功能 2021-09-19 22:25:57 +08:00
hanhh 1cc54b5da6 添加全局主题className 2021-09-19 21:27:20 +08:00
hanhh 0baf0d23ae 脚本管理支持新增文件 2021-09-19 21:23:03 +08:00
hanhh 646443deb5 修复pageTitle 2021-09-18 23:27:50 +08:00
hanhh 39fb23c025 任务详情日志添加loading 2021-09-18 16:33:12 +08:00
hanhh 698c463c3a 添加全局loading 2021-09-18 15:08:45 +08:00
hanhh 6c87634bb3 修复关闭通知 2021-09-18 02:09:06 +08:00
hanhh 0a9e9e972d 添加通知设置页面 2021-09-18 01:44:55 +08:00
whyour bb0ad6c325 添加通知api 2021-09-17 18:06:14 +08:00
hanhh f2dac51a62 更新通知服务 2021-09-17 00:35:27 +08:00
whyour 9534cda1f9 更新通知服务 2021-09-16 21:01:29 +08:00
whyour 27226e7222 添加通知服务 2021-09-16 18:01:25 +08:00
prcxuandGitHub c51621ee04 更新安装说明 (#705) 2021-09-15 11:18:58 +08:00
JH2628andGitHub c78e4447fa 修复config sample序号 (#694) 2021-09-14 19:25:00 +08:00
hanhh 2d7e525024 修改系统异常提醒 2021-09-13 00:12:44 +08:00
hanhh ecee9e2acd 更新tg链接 2021-09-12 20:28:43 +08:00
hanhh d607619f8f 更新版本号 2021-09-11 13:53:58 +08:00
hanhh e8e275cd79 修复写入任务错误日志逻辑 2021-09-11 13:53:24 +08:00
hanhh 4147025406 修改获取用户数据逻辑 2021-09-11 02:03:31 +08:00
hanhh 35539a4ccc 移除自动压缩db数据 2021-09-09 13:02:49 +08:00
hanhh b7a359714f 更新版本号 2021-09-08 12:26:21 +08:00
hanhh bf43bfbd92 修复开启两步失败提醒 2021-09-08 12:26:05 +08:00
hanhh 1265e465d0 添加应用名称改为选填 2021-09-08 12:15:30 +08:00
hanhh 8f195b964f 修复两步验证逻辑 2021-09-08 12:13:19 +08:00
hanhh a285efdd85 更新版本号 2021-09-07 23:53:01 +08:00
hanhh fafb68dbf0 修改登陆日志title 2021-09-07 23:52:40 +08:00
L1n3andGitHub 3dc87329f1 添加安装说明 (#667) 2021-09-07 17:46:53 +08:00
LeonandGitHub 7fc15518b3 修复 Python 和 JavaScript Server酱和企业微信推送的环境变量不一致 (#673) 2021-09-07 17:46:14 +08:00
hanhh eb196bd40d 增加登录日志 2021-09-07 01:40:41 +08:00
Zy143LandGitHub 0cbfca979e 修复python和JavaScript钉钉推送的环境变量不一致 (#659) 2021-09-05 23:53:18 +08:00
hanhh b3ac81b7b7 config文件读取添加黑名单 2021-09-05 23:48:58 +08:00
hanhh bb40111d38 添加环境变量命令input改为textarea 2021-09-05 23:38:36 +08:00
hanhh 29a3bea6e4 修改ql check依赖安装逻辑 2021-09-03 22:30:24 +08:00
hanhh ab4f167bda 修改两步验证逻辑 2021-09-03 19:09:09 +08:00
hanhh 035c69898f 修复环境变量拖拽 2021-09-02 19:16:28 +08:00
hanhh 00cb915f48 更新两步验证逻辑 2021-09-01 23:03:27 +08:00
L1n3andGitHub 97baff8baf 添加 docker-compose.yml (#629) 2021-08-31 15:52:24 +08:00
hanhh e4da2edf56 增加上次登录状态 2021-08-31 15:29:23 +08:00
hanhh c1e08ba9f0 增加ql resettfa禁用两步验证 2021-08-31 11:06:20 +08:00
hanhh f00566624d 更新版本号 2021-08-31 10:56:37 +08:00
hanhh 2bb55294fc 两步验证失败增加ip记录 2021-08-31 10:54:46 +08:00
hanhh 86c3e9a843 添加两步验证 2021-08-30 23:37:26 +08:00
hanhh 3a998a37f0 增加nedb数据自定比对,测试对db文件不断增大的影响 2021-08-29 00:52:21 +08:00
hanhh 7eed47cd8e 修复移动端无法打开脚本管理、任务日志 2021-08-29 00:34:47 +08:00
hanhh 8cef0b6216 更换ip地址查询接口 2021-08-27 14:00:11 +08:00
hanhh 930541d5d6 更换ip查询接口 2021-08-27 13:27:32 +08:00
hanhh f3518d0cad 更新版本号 2021-08-27 11:08:08 +08:00
hanhh 45e2c43997 更新nginx配置 2021-08-27 11:04:31 +08:00
hanhh 6c0f932ae8 修改新建应用placeholder 2021-08-27 10:52:49 +08:00
hanhh 5832bf9140 搜索时去掉前后空格 2021-08-27 10:47:30 +08:00
HeroWongandGitHub 47bba19bec fix: 只有一页时不隐藏分页栏,确保用户可以更改 pagesize (#613) 2021-08-26 21:27:22 +08:00
hanhh 1e58254f4c 添加openapi模块 2021-08-26 19:01:39 +08:00
hanhh 7739cef7b8 更新版本号 2021-08-25 23:55:16 +08:00
hanhh 9b73392c5e 修复cron分隔符,修复登录获取ip逻辑 2021-08-25 23:54:42 +08:00
hanhh 79e5b2fb84 修复登录倒计时颜色 2021-08-25 18:58:45 +08:00
hanhh 8ce1a33bd3 更新版本号 2021-08-25 18:20:29 +08:00
hanhh 8ae61f1b23 修复6位cron判断 2021-08-25 18:19:22 +08:00
hanhh a973c93c04 修复群辉update之后403 2021-08-25 17:58:21 +08:00
hanhh 9c0f14aa89 更新版本号 2021-08-24 18:21:19 +08:00
hanhh f78d72db4c 添加重置登录错误次数命令 2021-08-24 18:20:36 +08:00
hanhh fb0c1c6993 添加默认定时规则配置 2021-08-23 19:10:57 +08:00
hanhh 4d99695e4c task传递参数给task_before和task_after 2021-08-23 15:58:25 +08:00
hanhh 18321943d6 更新readme图片 2021-08-23 15:31:13 +08:00
hanhh 67819d2817 修复readme图片 2021-08-23 15:26:30 +08:00
hanhh ad74e014ef 更新版本号 2021-08-23 13:50:44 +08:00
hanhh bf661dffcf 修复获取ip api失败时,无法登陆
修复禁用或者运行定时任务出现重复
2021-08-23 13:44:54 +08:00
hanhh 11484175c8 修复定时任务搜索后页码 2021-08-23 12:29:29 +08:00
hanhh 7bc5004683 修复error_log文件读取 2021-08-23 11:41:34 +08:00
hanhh 1d01508164 更新版本号 2021-08-23 10:47:11 +08:00
hanhh 4cae9bcfb9 回退update.sh 2021-08-23 10:43:26 +08:00
hanhh 9c84c7bd38 修复task_before eval 2021-08-22 22:33:07 +08:00
hanhh 5a5e81eae0 修复未产生日志文件cat报错 2021-08-22 22:02:31 +08:00
hanhh b0f6bb3ea6 ql命令增加-l参数 2021-08-22 21:57:10 +08:00
hanhh 0c004606b5 修改task.sh任务顺序 2021-08-22 20:54:40 +08:00
hanhh fa4978389c 修复task语法 2021-08-22 18:57:52 +08:00
hanhh 8b8359b08f 修复task.sh执行语法 2021-08-22 18:21:31 +08:00
hanhh 1d4fec3979 修改task.sh中参数名 2021-08-22 16:05:58 +08:00
hanhh bb4123805c 修复最后登录ip读取 2021-08-22 15:25:16 +08:00
hanhh f49f605c11 更新版本号 2021-08-22 15:23:58 +08:00
hanhh 5496689d7c 修复任务执行参数出错 2021-08-22 15:23:12 +08:00
hanhh bad8bbdb7c 修改登陆成功提示文本 2021-08-22 14:47:07 +08:00
hanhh 76184227a4 更新版本号 2021-08-22 14:07:43 +08:00
hanhh bbc9ee120d 修复获取访问ip逻辑 2021-08-22 14:07:00 +08:00
hanhh 85093b2856 更新版本号 2021-08-22 12:59:54 +08:00
hanhh 8c355b6159 修复登录重试判断 2021-08-22 12:59:34 +08:00
hanhh 622037ee98 登录增加错误延迟提醒 2021-08-22 12:28:45 +08:00
hanhh 54bb13aad0 登录增加最后登录时间地点,登录失败增加重试间隔 2021-08-22 01:22:23 +08:00
hanhh 61e41269ba 默认定时改为随机时间 2021-08-21 19:26:37 +08:00
hanhh f7cffe3d2d 修复更新环境变量备注无法删除 2021-08-21 18:34:53 +08:00
hanhh 0cd39f0c97 增加命令行运行日志实时打印 2021-08-21 17:26:41 +08:00
hanhh c09b22358b 定时任务,环境变量添加状态筛选 2021-08-20 16:31:24 +08:00
hanhh 18215b8336 更新版本号 2021-08-20 16:17:04 +08:00
hanhh f0fae716cc 移除console 2021-08-20 14:26:12 +08:00
hanhh 7092be97eb 手动运行任务添加错误捕获 2021-08-20 10:59:59 +08:00
hanhh d9a9d2ee4f 修复脚本管理编辑交互,修复ico无法加载 2021-08-19 23:46:26 +08:00
hanhh f86892cef0 更新版本号 2021-08-19 14:47:07 +08:00
hanhh a48b915675 定时任务支持任务名,定时,命令排序
环境变量支持名称排序
2021-08-19 14:45:42 +08:00
hanhh 5e13264393 脚本管理支持编辑删除文件 2021-08-19 14:18:25 +08:00
hanhh ff481ef7f3 修复未勾选拆分环境变量参数验证失败 2021-08-17 22:31:39 +08:00
hanhh 82a4485792 修复未勾选拆分环境变量参数验证失败 2021-08-17 22:26:33 +08:00
hanhh 538876f529 修复手动运行任务进程 2021-08-17 22:06:04 +08:00
hanhh 609a31455b 更新版本号 2021-08-17 18:35:53 +08:00
hanhh 2d6da9ebfe 支持批量创建同名称环境变量,日志和脚本管理PC页支持宽度拖拽 2021-08-17 18:34:55 +08:00
hanhh 594e871744 修改暗黑模式默认值 2021-08-16 18:05:52 +08:00
DerekandGitHub 95074eb028 更正ql"本脚本用法"中的序号 (#552) 2021-08-16 17:17:45 +08:00
hanhh 87231fac25 localhost替换为0.0.0.0 2021-08-16 16:59:49 +08:00
hanhh 2a5f0eee92 修复删除错误日志文件 2021-08-16 16:36:39 +08:00
hanhh df2a659050 修复check.sh 2021-08-16 15:10:27 +08:00
hanhh adc8f35e83 修复bot.sh 2021-08-16 15:06:19 +08:00
hanhh dfb0d73ef3 ql命令行执行最后打印日志 2021-08-16 14:48:21 +08:00
hanhh 3a8101aad1 更新版本号 2021-08-16 14:38:55 +08:00
hanhh 65d6432dad 增加重启刷新延迟 2021-08-16 14:31:14 +08:00
hanhh 4929fa0440 移除tee,添加config错误日志捕获,合并并发任务日志 2021-08-16 12:16:40 +08:00
hanhh 7a02fe0d8e 修复内容区样式 2021-08-15 15:45:18 +08:00
hanhh d06fccd0d1 修复添加定时任务插入顺序
ql repo拉取失败自动删除目录重新拉取一次
2021-08-12 00:00:39 +08:00
hanhh 8be71b129a 更新版本号 2021-08-11 16:52:21 +08:00
hanhh 1b02668014 修改kill日志 2021-08-11 16:51:38 +08:00
hanhh 3f536b5062 修复停止任务时进程无法删除 2021-08-11 16:50:59 +08:00
hanhh c3517ccc41 停止任务添加调试日志 2021-08-11 16:36:50 +08:00
hanhh bdc3763a6f 修复body样式 2021-08-10 16:20:05 +08:00
hanhh fcfdee4988 修复conc任务 2021-08-10 16:03:21 +08:00
eb15e096e3 修改 cqhttp 的发送的数据类型为 JSON ,避免使用表单可能出现的转码问题 (#543)
Co-authored-by: shaochuan.wu <shaochuan.wu@fit2cloud.com>
2021-08-09 14:04:49 +08:00
hanhh 6e36e06dda 更新版本号 2021-08-07 20:12:38 +08:00
hanhh cbd1ddc5e7 修复github_proxy_url参数影响拉取gitee仓库或者链接 2021-08-07 20:11:06 +08:00
hanhh ebc2e0e4b6 PC端支持对比工具修改配置文件 2021-08-06 19:10:02 +08:00
hanhh 95e104dc9c 更新readme 2021-08-06 18:42:45 +08:00
hanhh 99f02e7c54 更新readme 2021-08-06 18:41:06 +08:00
hanhh bc110ff949 修复定时匹配 2021-08-06 16:05:33 +08:00
hanhh 69a5ba8996 更新readme 2021-08-06 15:09:17 +08:00
hanhh 25236958ff 修复日志弹框样式 2021-08-06 14:46:42 +08:00
hanhh f2dbb64984 移除编辑器rename input 2021-08-05 21:18:50 +08:00
hanhh 069e236674 修改文件判断语法 2021-08-05 18:02:00 +08:00
hanhh e06175c3da 修复raw参数 2021-08-05 16:29:42 +08:00
hanhh dfab2cb7f1 更新版本号 2021-08-05 15:06:00 +08:00
hanhh f1e0823a10 修复单个文件唯一路径 2021-08-05 15:05:40 +08:00
hanhh 5fbad9956f 修复repo命令日志文件唯一值 2021-08-05 14:42:53 +08:00
hanhh d6f5198893 修复首次拉区repo cdn无效 2021-08-05 01:07:47 +08:00
hanhh 603dc07581 修复手机端配置文件编辑 2021-08-03 21:37:28 +08:00
hanhh ace820efd9 修复windows换行符 2021-08-03 21:12:31 +08:00
hanhh 6ed4aa6bf4 修复脚本管理editorRef错误 2021-08-03 19:05:01 +08:00
hanhh 9e4b41ce48 修复文件为空无法保存 2021-08-03 19:03:41 +08:00
hanhh d8587b64a5 修改set envs逻辑 2021-08-03 18:45:24 +08:00
hanhh eba78ca84f 测试内容获取 2021-08-03 18:06:40 +08:00
hanhh 5fd79173d6 测试kill任务进程 2021-08-03 12:06:38 +08:00
hanhh b86ee5674d 修复批量停止任务 2021-08-03 11:23:06 +08:00
hanhh 42fbed157f 修复页面标题显示 2021-08-03 11:14:33 +08:00
hanhh c51306ef27 修复ql update中reset操作 2021-08-03 11:07:05 +08:00
hanhh e260dcb985 更新版本号 2021-08-02 21:46:29 +08:00
hanhh 2ddf69a79a 修复matchMedia兼容性 2021-08-02 21:44:42 +08:00
hanhh 2f4fb657e6 修复matchMedia兼容性 2021-08-02 21:21:28 +08:00
hanhh 2a9751e051 清空默认nginx配置文件 2021-08-02 19:07:45 +08:00
hanhh 43e02905c8 更新版本号 2021-08-02 19:04:01 +08:00
hanhh 635306e2d3 更新任务状态日志 2021-08-02 19:02:21 +08:00
hanhh 63f21b1698 去掉有可能中断的tee 2021-08-02 18:57:48 +08:00
hanhh 59c9b17c40 修复移动端判断 2021-08-02 16:00:45 +08:00
hanhh dc5b4de5fa 重构平台判断代码 2021-08-02 15:19:20 +08:00
hanhh 5d846beabd 更新版本号 2021-07-31 23:13:41 +08:00
hanhh 9728119101 手机端使用codemirror 2021-07-31 23:09:53 +08:00
hanhh 38609feee9 monaco-editor测试版本 2021-07-28 21:39:43 +08:00
hanhh 0b99882193 修复表格样式 2021-07-27 00:09:06 +08:00
hanhh e03a9d2c1d 删除冗余代码 2021-07-26 23:07:22 +08:00
hanhh 105c0f802f 修复windows换行符 2021-07-26 22:57:59 +08:00
hanhh da70b0799a 修复task.sh 2021-07-26 08:37:35 +08:00
hanhh 9d89ecc51e 修复task参数提示 2021-07-26 01:06:27 +08:00
hanhh c77e302874 修复弹框日志样式 2021-07-26 00:56:53 +08:00
hanhh fd7153403d 测试monaco代替codemirror 2021-07-26 00:26:14 +08:00
hanhh eff109bf81 修复bot.sh 2021-07-24 23:17:33 +08:00
hanhh 1614f3c8ad 更新版本号 2021-07-22 22:00:53 +08:00
hanhh 7ced425e01 修改task_before逻辑 2021-07-22 19:13:54 +08:00
hanhh b183e9cd29 增加codemirror搜索功能 2021-07-20 00:10:18 +08:00
hanhh 9320074a02 添加配置文件编辑黑名单 2021-07-17 22:24:29 +08:00
hanhh ecdfdff50d 优化面板样式 2021-07-17 22:20:08 +08:00
hanhh 20ddb5a3cf 修复task_before, task_after 2021-07-17 01:33:10 +08:00
hanhh 9d8c592e69 修复初始化任务状态 2021-07-17 00:01:16 +08:00
hanhh 66266fd545 修复ql repo依赖文件目录嵌套 2021-07-16 01:00:40 +08:00
hanhh cc238dedbc 修改tee参数 2021-07-14 15:40:59 +08:00
hanhh dac694776d 修改spawn参数 2021-07-14 15:13:17 +08:00
hanhh 1f42d0245c 修复task_before影响任务执行 2021-07-13 17:54:10 +08:00
hanhh a6319698e2 更新版本号 2021-07-13 17:44:01 +08:00
hanhh 9ab702e344 支持任务读取全局模块 2021-07-13 17:43:41 +08:00
hanhh 49045c39c7 忽略ts脚本语法检查 2021-07-13 10:53:17 +08:00
hanhh 52490bf320 回退task修改 2021-07-12 23:32:07 +08:00
hanhh faf982b156 更新版本号 2021-07-12 23:18:56 +08:00
hanhh cad1d86caf 修复task_before出错任务中断 2021-07-12 23:18:40 +08:00
LicardoandGitHub dd424237e2 添加bark推送消息分组 (#479) 2021-07-12 20:17:11 +08:00
hanhh 0214c6f81b 撤回task日志修改 2021-07-09 22:39:54 +08:00
hanhh f7e1c2ae5e 暂时移除docker action 条件 2021-07-09 18:52:29 +08:00
hanhh 5e52398a43 测试docker action 2021-07-09 18:48:22 +08:00
hanhh 7bc24a4b62 测试版本号action 2021-07-09 18:42:40 +08:00
hanhh 105073ec23 测试版本号action 2021-07-09 18:42:21 +08:00
hanhh bbb1fc4e1b 测试docker打包 2021-07-09 18:35:33 +08:00
hanhh 381427fd60 更新版本号 2.8.0-068 2021-07-09 18:30:35 +08:00
hanhh 6c63fb83ef 修复执行任务时task_before出错造成任务日志中断 2021-07-09 18:28:06 +08:00
hanhh 602ffdcd5a 修改docker build条件 2021-07-09 15:22:10 +08:00
hanhh 72240a7834 更新版本号2.8.0-067 2021-07-08 23:17:52 +08:00
hanhh 8336434d3f 容器启动时bot和自定义脚本改成后台运行 2021-07-08 23:11:00 +08:00
hanhh dd2d3127a0 修复文件名 2021-07-08 19:13:00 +08:00
hanhh c960717d92 修改workflow 2021-07-08 18:52:48 +08:00
hanhh 23db56df57 移除临时文件 2021-07-08 18:15:25 +08:00
hanhh ed95d66441 测试 2021-07-08 13:05:39 +08:00
hanhh 6cd0feaace 撤销spawn修改 2021-07-08 10:24:40 +08:00
hanhh 22a23fe05e 更新版本号 2021-07-07 18:27:19 +08:00
hanhh 517fdcc8ec 修复spawn可能中断,修复token过期任务状态修改 2021-07-07 18:26:56 +08:00
hanhh 71d7d3818f 修复更新任务状态日志 2021-07-06 13:42:25 +08:00
hanhh 7b7d62760e 初始化更新任务为空闲状态 2021-07-04 19:35:35 +08:00
hanhh f554a5b1a0 更新版本号 2021-07-04 19:26:47 +08:00
hanhh f26d6f1449 去除console.log 2021-07-04 19:25:40 +08:00
hanhh 0728f1f231 修复手动停止任务日志目录 2021-07-04 19:15:09 +08:00
hanhh e687511f32 修复shell任务id获取 2021-07-04 19:12:45 +08:00
hanhh a8a2a54bea 重构任务日志,修复手动停止任务 2021-07-04 19:00:02 +08:00
hanhh c1a3ac6e4d 修复环境变量tooltip 2021-07-03 18:38:02 +08:00
hanhh b1d55422ac 修复环境变量tooltip样式 2021-07-03 17:17:08 +08:00
hanhh f6630d84ab 更新版本号 2021-07-03 16:49:29 +08:00
hanhh 5c2bbc941f 修复获取sample文件 2021-07-03 16:49:02 +08:00
hanhh 53123af832 环境变量值超出显示... 2021-07-03 16:27:13 +08:00
孙善明andGitHub 41c3f21863 增加强制提取 cron 时间 (#373) 2021-07-01 21:51:58 +08:00
hanhh 356f8db728 修复curl proxy 2021-07-01 21:32:54 +08:00
hanhh a14a8cc067 修复entrypoint步骤 2021-07-01 18:52:50 +08:00
hanhh 7ecd38ad1f 修改dockerfile 2021-07-01 15:27:13 +08:00
hanhh 4bc1deda42 修复req参数 2021-07-01 11:49:44 +08:00
hanhh f9c1dfc8cb 修复dockerfile 2021-07-01 00:40:20 +08:00
hanhh 7d417a11c9 修复dockerfile 2021-07-01 00:38:33 +08:00
hanhh a35c0cb64e 修复dockerfile 2021-07-01 00:12:32 +08:00
hanhh 1aaa0f0fa6 修复docker action 2021-06-30 23:52:39 +08:00
hanhh 2fe8c38293 修改镜像打包流程 2021-06-30 23:38:46 +08:00
hanhh 2a1adb2e0e 更新action yml 2021-06-30 23:02:22 +08:00
hanhh 5b76213354 修复check命令 2021-06-30 22:38:19 +08:00
hanhh ebcf8f96c8 修复token验证 2021-06-30 12:26:54 +08:00
hanhh 0c3b3c206e 修复token验证 2021-06-30 12:20:42 +08:00
hanhh ef16622a7e 更新版本号 2021-06-30 12:16:40 +08:00
hanhh e7aff6d1d6 修复token唯一性验证 2021-06-30 12:16:05 +08:00
hanhh b1bee910b2 修复ql check逻辑 2021-06-29 12:06:15 +08:00
hanhh 2049cf9aa9 增加ql check日志显示 2021-06-29 10:51:27 +08:00
Xinglin QiangandGitHub 6c3f848b2c 兼容使用force update的repo (#341) 2021-06-29 10:48:01 +08:00
hanhh 2f8d31658b 更新版本号 2021-06-29 00:05:52 +08:00
hanhh c77eb3d28d 添加ql check运行环境监测 2021-06-29 00:05:37 +08:00
hanhh 1a801c2d31 修复查看脚本高亮 2021-06-27 17:23:13 +08:00
hanhh 0a6c7e3719 修复弹出数据不重置 2021-06-27 00:19:48 +08:00
hanhh 9f5c8fa70a 修改环境变量文本 2021-06-26 23:37:54 +08:00
hanhh ecb035d0b4 修复删除日志log 2021-06-26 23:08:38 +08:00
hanhh b78b382ba1 修复replaceAll方法 2021-06-26 20:32:29 +08:00
hanhh 49088f0d3a 过滤env中的空格 2021-06-26 20:27:25 +08:00
hanhh 0abf657a9c 更新Readme图片 2021-06-26 16:00:19 +08:00
hanhh eb2390135f 更新Readme 2021-06-26 15:57:44 +08:00
hanhh 0753ef9ce9 更新Readme 2021-06-26 15:56:15 +08:00
hanhh f3aca5575c 修复日志结束标识 2021-06-25 16:06:12 +08:00
hanhh 54dd82dc77 修复日志参数获取 2021-06-25 15:58:14 +08:00
hanhh 86e5f208fb 修复ql命令日志 2021-06-25 15:50:04 +08:00
hanhh ce3543779e 修复手动执行任务日志 2021-06-24 23:46:02 +08:00
hanhh e11d0c25f3 更新版本号 2021-06-24 18:56:22 +08:00
hanhh e03c64dceb 任务面板支持查看最新日志 2021-06-24 18:55:26 +08:00
hanhh 4d4d31431d 修复执行shell脚本任务 2021-06-23 18:58:32 +08:00
hanhh c9fecae67a ql bot添加重置remote地址 2021-06-21 22:41:19 +08:00
hanhh 173b87a7b1 修复env启用禁用 2021-06-21 17:22:23 +08:00
hanhh 49ab1ad93a 环境变量支持搜索 2021-06-21 14:16:51 +08:00
hanhh 6c943ac97a 修复环境变量position 2021-06-20 23:28:46 +08:00
hanhh 325b423c4d 修复查看脚本页搜索 2021-06-20 22:59:03 +08:00
hanhh 164126ddc7 修复diff apipath 2021-06-20 22:28:42 +08:00
hanhh e728faf519 修复任务运行依赖 2021-06-20 22:13:04 +08:00
hanhh b00acc7ef5 修复cookie.db迁移 2021-06-20 22:06:29 +08:00
hanhh e44a7aca1a 过滤scripts目录下的子目录 2021-06-20 22:05:43 +08:00
hanhh aef57d94d6 修复package.json依赖 2021-06-20 21:38:26 +08:00
hanhh 9124f1acf3 ql update 添加patch操作 2021-06-20 20:49:14 +08:00
hanhh 01e0a89ba9 更新版本号 2021-06-20 20:37:04 +08:00
hanhh 3052b48f51 删除无用代码 2021-06-20 20:33:18 +08:00
hanhh d630f34505 修复任务并发参数 2021-06-20 20:15:29 +08:00
hanhh 2692351e0a 导入env.sh 2021-06-20 18:06:32 +08:00
hanhh 0fade7a5a9 重构环境变量管理,添加脚本查看 2021-06-20 17:47:12 +08:00
167 changed files with 18485 additions and 13278 deletions
+7 -4
View File
@@ -1,9 +1,12 @@
MONGODB_URI='mongodb://'
YIYAN_MONGODB_URI=''
PUBLIC_PORT=5400
CRON_PORT=5500
PORT=5600
BACK_PORT=5600
PORT=5700
LOG_LEVEL='debug'
SECRET='whyour'
QINIU_AK=''
QINIU_SK=''
QINIU_SCOPE=''
+42
View File
@@ -0,0 +1,42 @@
name: "\U0001F41E Bug report"
description: Create a report to help us improve
body:
- type: input
id: version
attributes:
label: Qinglong version
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to reproduce
description: |
What do we need to do after opening your repro in order to make the bug happen? Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format lists and code.
placeholder: Steps to reproduce
validations:
required: true
- type: textarea
id: expected
attributes:
label: What is expected?
validations:
required: true
- type: textarea
id: actually-happening
attributes:
label: What is actually happening?
validations:
required: true
- type: textarea
id: system-info
attributes:
label: System Info
description: Output of `npx envinfo --system --binaries --browsers`
render: shell
placeholder: System, Binaries, Browsers
- type: textarea
id: additional-comments
attributes:
label: Any additional comments?
description: e.g. some background/context of how you ran into this bug.
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Telegram Chat
url: https://t.me/jiao_long
about: Ask questions and discuss with other Qinglong users in real time.
- name: Questions & Discussions
url: https://github.com/whyour/qinglong/discussions/new?category=q-a
about: Use GitHub discussions for message-board style questions and discussions.
@@ -0,0 +1,40 @@
name: "\U0001F680 New feature proposal"
description: Suggest an idea for this project
labels: [":sparkles: feature request"]
body:
- type: markdown
attributes:
value: |
Thanks for your interest in the project and taking the time to fill out this feature report!
- type: textarea
id: feature-description
attributes:
label: Clear and concise description of the problem
description: "Explain your use case, context, and rationale behind this feature request. More importantly, what is the **end user experience** you are trying to build that led to the need for this feature?"
validations:
required: true
- type: textarea
id: suggested-solution
attributes:
label: Suggested solution
description: "In module [xy] we could provide following implementation..."
validations:
required: true
- type: textarea
id: alternative
attributes:
label: Alternative
description: Clear and concise description of any alternative solutions or features you've considered.
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Any other context or screenshots about the feature request here.
- type: checkboxes
id: checkboxes
attributes:
label: Validations
description: Before submitting the issue, please make sure you do the following
options:
- label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
required: true
+31
View File
@@ -0,0 +1,31 @@
## PR Type
What kind of change does this PR introduce?
<!-- Please check the one that applies to this PR using "x". -->
- [ ] Bugfix
- [ ] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Other... Please describe:
## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
Issue Number: N/A
## What is the new behavior?
## Does this PR introduce a breaking change?
- [ ] Yes
- [ ] No
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
## Other information
+7
View File
@@ -0,0 +1,7 @@
# Comment to be posted to on PRs from first time contributors in your repository
newPRWelcomeComment: |
💖 Thanks for opening this pull request! 💖
Please be patient and we will get back to you as soon as we can.
# Comment to be posted to on pull requests merged by a first time user
firstPRMergeComment: >
Congrats on merging your first pull request! 🎉🎉🎉
-34
View File
@@ -1,34 +0,0 @@
name: build static
on:
push:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '14'
- name: build front and back
run: |
yarn install
yarn build
yarn build-back
- name: copy to static repo
env:
# GITHUB_REPO: gitee.com/whyour/qinglong-static
GITHUB_REPO: github.com/whyour/qinglong-static
run: |
mkdir -p static
cd ./static
cp -rf ../dist ./ && cp -rf ../build ./
git init && git add .
git config user.name "whyour"
git config user.email "imwhyour@gmail.com"
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
git push --force --quiet "https://whyour:${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" master:master
+116
View File
@@ -0,0 +1,116 @@
name: Build And Push Docker Image
on:
push:
branches:
- 'master'
- 'develop'
# Sequence of patterns matched against refs/tags
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
schedule:
# 参考 https://jasonet.co/posts/scheduled-actions/
# note: 这里是GMT时间,北京时间减去八小时即可。如北京时间 22:30 => GMT 14:30
# minute hour day month dayOfWeek
- cron: '00 14 * * *' # GMT 14:00 => 北京时间 22:00
#- cron: '30 16 * * *' # GMT 16:30(前一天) => 北京时间 00:30
workflow_dispatch:
jobs:
build-static:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- name: build front and back
run: |
npm i -g pnpm
pnpm install
pnpm build:front
pnpm build:back
- name: copy to static repo
env:
GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static
GITHUB_BRANCH: ${{ github.ref_name }}
run: |
mkdir -p tmp
cd ./tmp
cp -rf ../static/* ./
git init -b ${GITHUB_BRANCH} && git add .
git config --local user.name 'github-actions[bot]'
git config --local user.email 'github-actions[bot]@users.noreply.github.com'
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH}
build:
needs: build-static
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- name: Set time zone
uses: szenius/set-timezone@v1.0
with:
timezoneLinux: "Asia/Shanghai"
timezoneMacos: "Asia/Shanghai"
timezoneWindows: "China Standard Time"
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: |
${{ github.repository }}
ghcr.io/${{ github.repository }}
# generate Docker tags based on the following events/attributes
# nightly, master, pr-2, 1.2.3, 1.2, 1
tags: |
type=schedule,pattern=nightly
type=edge
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
id: docker_build
uses: docker/build-push-action@v3
with:
build-args: |
MAINTAINER=${{ github.repository_owner }}
QL_BRANCH=${{ github.ref_name }}
network: host
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
context: docker/
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
-53
View File
@@ -1,53 +0,0 @@
name: Publish Docker Image
on:
push:
tags:
- v*
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout base
uses: actions/checkout@v2
with:
fetch-depth: 0
# https://github.com/docker/setup-qemu-action
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
# https://github.com/docker/setup-buildx-action
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Docker login
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin
- name: Docker buildx image and push on master branch
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
if: github.ref == 'refs/heads/master'
run: |
docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x --tag whyour/qinglong:latest docker/
- name: Replace tag without `v`
if: startsWith(github.ref, 'refs/tags/')
uses: actions/github-script@v1
id: version
with:
script: |
return context.payload.ref.replace(/\/?refs\/tags\/v/, '')
result-encoding: string
- name: Docker buildx image and push on release
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
if: startsWith(github.ref, 'refs/tags/')
run: |
docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x --tag whyour/qinglong:${{steps.version.outputs.result}} docker/
docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x --tag whyour/qinglong:latest docker/
+7 -3
View File
@@ -5,11 +5,12 @@
/npm-debug.log*
/yarn-error.log
/yarn.lock
/pnpm-lock.yaml
/package-lock.json
# production
/dist
/build
/static
/data
# misc
.DS_Store
@@ -21,8 +22,11 @@
/.env.local
.env
.history
.version.ts
/config
/log
/db
/manual_log
/manual_log
/scripts
/bak
+2
View File
@@ -0,0 +1,2 @@
sentrycli_cdnurl=https://npmmirror.com/mirrors/sentry-cli/
strict-peer-dependencies=false
+14 -8
View File
@@ -4,16 +4,27 @@ const CompressionPlugin = require('compression-webpack-plugin');
export default defineConfig({
hash: true,
layout: false,
outputPath: 'static/dist',
nodeModulesTransform: {
type: 'none',
},
fastRefresh: {},
favicon: 'https://qinglong.whyour.cn/g5.ico',
esbuild: {},
webpack5: {},
dynamicImport: {
loading: '@/components/pageLoading',
},
favicon: '/images/favicon.svg',
proxy: {
'/api': {
target: 'http://127.0.0.1:5678/',
'/api/public': {
target: 'http://127.0.0.1:5400/',
changeOrigin: true,
},
'/api': {
target: 'http://127.0.0.1:5600/',
changeOrigin: true,
ws: true,
},
},
chainWebpack: (config) => {
config.plugin('compression-webpack-plugin').use(
@@ -28,14 +39,9 @@ export default defineConfig({
externals: {
react: 'window.React',
'react-dom': 'window.ReactDOM',
codemirror: 'window.CodeMirror',
darkreader: 'window.DarkReader',
},
scripts: [
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
'https://cdn.jsdelivr.net/npm/codemirror@5.60.0/lib/codemirror.min.js',
'https://cdn.jsdelivr.net/npm/darkreader@4.9.27/darkreader.min.js',
'https://cdn.jsdelivr.net/npm/codemirror@5.60.0/mode/shell/shell.js',
],
});
+197 -18
View File
@@ -1,22 +1,201 @@
MIT LICENSE
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Copyright (c) 2021-present whyour
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
1. Definitions.
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2021 WHYOUR <https://github.com/whyour>.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+214
View File
@@ -0,0 +1,214 @@
<p align="center">
<a href="https://github.com/whyour/qinglong">
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
</a>
</p>
<h1 align="center">Green Dragon</h1>
<div align="center">
Timed task management panel with python3, javaScript, shell, typescript support
[![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]
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
[docker-version-url]: https://hub.docker.com/r/whyour/qinglong/tags?page=1&ordering=last_updated
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
</div>
[![](https://user-images.githubusercontent.com/22700758/161788855-c4e51cb8-d4e9-44fe-bb17-ee1a56c8549b.png)](https://whyour.cn)
[简体中文](./README.md) | English
## Features
- Support for multiple scripting languages (python3, javaScript, shell, typescript)
- Support online management of scripts, environment variables, configuration files
- Support online view task log
- Support second-level task setting
- Support system level notification
- Support dark mode
- Support cell phone operation
## Deployment
### Local Deployment
```bash
# To be completed
```
### Podman Deployment
1. podman installation
```bash
https://podman.io/getting-started/installation
```
2. start the container
```bash
podman run -dit \
--network bridge \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
docker.io/whyour/qinglong:latest
```
### Docker Deployment
1. docker installation
```bash
sudo curl -sSL get.docker.com | sh
```
2. configure domestic mirror sources
```bash
mkdir -p /etc/docker
tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
"https://ypzju6vq.mirror.aliyuncs.com",
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://docker.mirrors.ustc.edu.cn"
]
}
EOF
systemctl daemon-reload
systemctl restart docker
```
3. start the container
```bash
docker run -dit \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
whyour/qinglong:latest
```
### Docker-compose Deployment
1. docker-compose installation
```bash
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
```
2. start the container
```bash
mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
# start
docker-compose up -d
# stop
docker-compose down
```
3. access
Open your browser and visit http://{ip}:5700
## Use
1. built-in commands
```bash
# Update and restart Green Dragon
ql update
# Run custom scripts extra.sh
ql extra
# Adding a single script file
ql raw <file_url>
# Add a specific script for a single repository
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch>
# Delete old logs
ql rmlog <days>
# Start bot
ql bot
# Detecting the Green Dragon environment and repairing it
ql check
# Reset the number of login errors
ql resetlet
# Disable two-step login
ql resettfa
# Execute in sequence, if a random delay is set, it will be randomly delayed by a certain number of seconds
task <file_path>
# Execute in sequence, regardless of whether a random delay is set, all run immediately,
# and the foreground will output the day, while recorded in the log file
task <file_path> now
# Concurrent execution, regardless of whether a random delay is set, are run immediately,
# the foreground does not generate the day, directly recorded in the log file, and can be specified account execution
task <file_path> conc <env_name> <account_number>(Optional)
# Specify the account to execute and run immediately regardless of whether a random delay is set
task <file_path> desi <env_name> <account_number>
```
2. parameter description
* file_url: Script address
* repo_url: Repository address
* whitelist: The whitelist when pulling the repository, i.e., the string contained in the path of the script to be pulled
* blacklist: Blacklisting when pulling repositories, i.e. strings that are not included in the path of the script to be pulled
* dependence: Pulling the dependencies needed for the repository will be copied directly from the repository to the repository directory under scripts, regardless of the blacklist
* branch: Pull the branch of the repository
* days: Number of days of logs to be kept
* file_path: File path for task execution
* env_name: The name of the environment variable that needs to be concurrent or specified at the time of task execution
* account_number: Specify the account number of an environment variable to be executed when the task is executed
## Links
- [nevinee](https://gitee.com/evine)
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/)
- [Umijs3.0](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server)
## 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
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".
In the Book of the Later Han Dynasty (後漢書-律曆志下), it is written: "The sun is in the sky, a cold and a summer, the four seasons are ready, all things are changed, the regency moves, and the green dragon moves to the star, which is called the year. (The Year of the Star)
Among the [twenty-eight Chinese constellations](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF), the Green Dragon is the generic name for the seven eastern constellations (Horn, Hyper, Diao, Fang, Heart, Tail and Minchi). It is known in Taoism as "Mengzhang" and in different Taoist scriptures as "Dijun", "Shengjian", "Shenjian" and He is also known in different Daoist scriptures as "Dijun", "Shengjun", "Shenjun" and "Ghost Catcher"[1], and is the guardian deity of Daoism, together with the White Tiger Supervisor of Soldiers.
+193 -43
View File
@@ -1,64 +1,214 @@
<p align="center">
<a href="https://github.com/whyour/qinglong">
<img width="150" src="https://qinglong.whyour.cn/qinglong.png">
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
</a>
</p>
<h1 align="center">青龙(WIP)</h1>
<h1 align="center">青龙</h1>
<div align="center">
pythonjavaScript的定时任务管理面板
支持python3、javaScript、shell、typescript 的定时任务管理面板
[![donate][donate-image]][donate-url] [![build status][build-status-image]][build-status-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker version][docker-version-image]][docker-version-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
[donate-image]: https://img.shields.io/badge/donate-wechat-green?style=for-the-badge
[donate-url]: https://qinglong.whyour.cn/nice.png
[build-status-image]: https://img.shields.io/docker/cloud/build/whyour/qinglong?style=for-the-badge
[build-status-url]: https://img.shields.io/docker/cloud/build/whyour/qinglong
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=for-the-badge
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=for-the-badge
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
[docker-version-url]: https://hub.docker.com/r/whyour/qinglong/tags?page=1&ordering=last_updated
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=for-the-badge
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=for-the-badge
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
</div>
[![](https://user-images.githubusercontent.com/22700758/161788855-c4e51cb8-d4e9-44fe-bb17-ee1a56c8549b.png)](https://whyour.cn)
简体中文 | [English](./README-en.md)
## 功能
- 支持多种脚本语言(python3、javaScript、shell、typescript
- 支持在线管理脚本、环境变量、配置文件
- 支持在线查看任务日志
- 支持秒级任务设置
- 支持系统级通知
- 支持暗黑模式
- 支持手机端操作
## 部署
### 本机部署
```bash
# 待完善
```
### podman 部署
1. podman 安装
```bash
https://podman.io/getting-started/installation
```
2. 启动容器
```bash
podman run -dit \
--network bridge \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
docker.io/whyour/qinglong:latest
```
### docker 部署
1. docker 安装
```bash
sudo curl -sSL get.docker.com | sh
```
2. 配置国内镜像源
Configure domestic mirror sources
```bash
mkdir -p /etc/docker
tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
"https://ypzju6vq.mirror.aliyuncs.com",
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://docker.mirrors.ustc.edu.cn"
]
}
EOF
systemctl daemon-reload
systemctl restart docker
```
3. 启动容器
```bash
docker run -dit \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
whyour/qinglong:latest
```
### docker-compose 部署
1. docker-compose 安装
```bash
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
```
2. 启动容器
```bash
mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
# 启动
docker-compose up -d
# 停止
docker-compose down
```
3. 访问
打开你的浏览器,访问 http://{ip}:5700
## 使用
1. 内置命令
```bash
# 更新并重启青龙
ql update
# 运行自定义脚本extra.sh
ql extra
# 添加单个脚本文件
ql raw <file_url>
# 添加单个仓库的指定脚本
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch> <extensions>
# 删除旧日志
ql rmlog <days>
# 启动tg-bot
ql bot
# 检测青龙环境并修复
ql check
# 重置登录错误次数
ql resetlet
# 禁用两步登录
ql resettfa
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
task <file_path>
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
task <file_path> now
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
task <file_path> conc <env_name> <account_number>(可选的)
# 指定账号执行,无论是否设置了随机延迟,均立即运行
task <file_path> desi <env_name> <account_number>
```
2. 参数说明
* file_url: 脚本地址
* repo_url: 仓库地址
* whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割
* blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割
* dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割
* extensions: 拉取仓库的文件后缀,多个竖线分割
* branch: 拉取仓库的分支
* days: 需要保留的日志的天数
* file_path: 任务执行时的文件路径
* env_name: 任务执行时需要并发或者指定时的环境变量名称
* account_number: 任务执行时指定某个环境变量需要执行的账号序号
## 链接
- [nevinee](https://gitee.com/evine)
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/)
- [Umijs3.0](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server)
## 开发
```bash
$ git clone git@github.com:whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
# 推荐使用 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/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF)中,青龙是东方七宿(角、亢、氐、房、心、尾、箕)的总称。 在早期星宿信仰中,祂是最尊贵的天神。 但被道教信仰吸纳入其神系后,神格大跌,道教将其称为“孟章”,在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼,与白虎监兵神君一起,是道教的护卫天神。
## 多谢
* [nevinee](https://gitee.com/evine)
* [crontab-ui](https://github.com/alseambusher/crontab-ui)
* [Ant Design](https://ant.design)
* [Ant Design Pro](https://pro.ant.design/)
* [Umijs3.0](https://umijs.org)
* [darkreader](https://github.com/darkreader/darkreader)
## 免责声明
1. 此仓储脚本仅用于学习研究,不保证其合法性、准确性、有效性,请根据情况自行判断,本人对此不承担任何保证责任。
2. 由于此仓储脚本仅用于学习研究,您必须在下载后 24 小时内将所有内容从您的计算机或手机或任何存储设备中完全删除,若违反规定引起任何事件本人对此均不负责。
3. 请勿将此仓储脚本用于任何商业或非法目的,若违反规定请自行对此负责。
4. 此仓储脚本涉及应用与本人无关,本人对因此引起的任何隐私泄漏或其他后果不承担任何责任。
5. 本人对任何脚本引发的问题概不负责,包括但不限于由脚本错误引起的任何损失和损害。
6. 如果任何单位或个人认为此仓储脚本可能涉嫌侵犯其权利,应及时通知并提供身份证明,所有权证明,我们将在收到认证文件确认后删除此仓储脚本。
7. 所有直接或间接使用、查看此仓储脚本的人均应该仔细阅读此声明。本人保留随时更改或补充此声明的权利。一旦您使用或复制了此仓储脚本,即视为您已接受此免责声明。
-128
View File
@@ -1,128 +0,0 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config';
import jwt from 'jsonwebtoken';
import { createPassword } from '../config/util';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.post(
'/login',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let username = req.body.username;
let password = req.body.password;
fs.readFile(config.authConfigFile, 'utf8', function (err, data) {
if (err) console.log(err);
const authInfo = JSON.parse(data);
if (username && password) {
if (
authInfo.username === 'admin' &&
authInfo.password === 'adminadmin'
) {
const newPassword = createPassword(16, 22);
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({
username: authInfo.username,
password: newPassword,
}),
);
return res.send({
code: 100,
msg: '已初始化密码,请前往auth.json查看并重新登录',
});
}
if (
username == authInfo.username &&
password == authInfo.password
) {
let token = jwt.sign(
{ username, password },
config.secret as any,
{ expiresIn: 60 * 60 * 24 * 7, algorithm: 'HS384' },
);
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({
username: authInfo.username,
password: authInfo.password,
token,
}),
);
res.send({ code: 200, token });
} else {
res.send({ code: 400, msg: config.authError });
}
} else {
res.send({ err: 400, msg: '请输入用户名密码!' });
}
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/logout',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
fs.readFile(config.authConfigFile, 'utf8', function (err, data) {
if (err) console.log(err);
const authInfo = JSON.parse(data);
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({
username: authInfo.username,
password: authInfo.password,
}),
);
res.send({ code: 200 });
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/user',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
fs.writeFile(config.authConfigFile, JSON.stringify(req.body), (err) => {
if (err) console.log(err);
res.send({ code: 200, msg: '更新成功' });
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/user',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
if (err) console.log(err);
const authInfo = JSON.parse(data);
res.send({ code: 200, data: { username: authInfo.username } });
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+41 -27
View File
@@ -5,36 +5,51 @@ import { Logger } from 'winston';
import config from '../config';
import * as fs from 'fs';
import { celebrate, Joi } from 'celebrate';
import { execSync } from 'child_process';
const route = Router();
export default (app: Router) => {
app.use('/', route);
app.use('/configs', route);
route.get(
'/config/:key',
'/files',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let content = '未找到文件';
switch (req.params.key) {
case 'config':
content = getFileContentByName(config.confFile);
break;
case 'sample':
content = getFileContentByName(config.sampleFile);
break;
case 'crontab':
content = getFileContentByName(config.crontabFile);
break;
case 'extra':
content = getFileContentByName(config.extraFile);
break;
default:
break;
const fileList = fs.readdirSync(config.configPath, 'utf-8');
res.send({
code: 200,
data: fileList
.filter((x) => !config.blackFileList.includes(x))
.map((x) => {
return { title: x, value: x };
}),
});
} catch (e) {
return next(e);
}
},
);
route.get(
'/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let content = '';
if (config.blackFileList.includes(req.params.file)) {
res.send({ code: 403, message: '文件无法访问' });
}
if (req.params.file.includes('sample')) {
content = getFileContentByName(
`${config.samplePath}${req.params.file}`,
);
} else {
content = getFileContentByName(
`${config.configPath}${req.params.file}`,
);
}
res.send({ code: 200, data: content });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
@@ -45,21 +60,20 @@ export default (app: Router) => {
celebrate({
body: Joi.object({
name: Joi.string().required(),
content: Joi.string().required(),
content: Joi.string().allow('').optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const { name, content } = req.body;
const path = (config.fileMap as any)[name];
fs.writeFileSync(path, content);
if (name === 'crontab.list') {
execSync(`crontab ${path}`);
if (config.blackFileList.includes(name)) {
res.send({ code: 403, message: '文件无法访问' });
}
res.send({ code: 200, msg: '保存成功' });
const path = `${config.configPath}${name}`;
fs.writeFileSync(path, content);
res.send({ code: 200, message: '保存成功' });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
+258 -45
View File
@@ -2,36 +2,158 @@ import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import CronService from '../services/cron';
import CronViewService from '../services/cronView';
import { celebrate, Joi } from 'celebrate';
import cron_parser from 'cron-parser';
const route = Router();
export default (app: Router) => {
app.use('/', route);
app.use('/crons', route);
route.get(
'/crons',
'/views',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.crontabs(
req.query.searchValue as string,
);
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.list();
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/crons',
'/views',
celebrate({
body: Joi.object({
name: Joi.string().required(),
sorts: Joi.array().optional().allow(null),
filters: Joi.array().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.create(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views',
celebrate({
body: Joi.object({
name: Joi.string().required(),
id: Joi.number().required(),
sorts: Joi.array().optional().allow(null),
filters: Joi.array().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.update(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.delete(
'/views',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.remove(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views/move',
celebrate({
body: Joi.object({
fromIndex: Joi.number().required(),
toIndex: Joi.number().required(),
id: Joi.number().required(),
}),
}),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.move(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views/disable',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.disabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views/enable',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.enabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.crontabs(req.query as any);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.post(
'/',
celebrate({
body: Joi.object({
command: Joi.string().required(),
schedule: Joi.string().required(),
name: Joi.string().optional(),
labels: Joi.array().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
@@ -45,16 +167,15 @@ export default (app: Router) => {
return res.send({ code: 400, message: 'param schedule error' });
}
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/crons/run',
'/run',
celebrate({
body: Joi.array().items(Joi.string().required()),
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
@@ -63,16 +184,15 @@ export default (app: Router) => {
const data = await cronService.run(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/crons/stop',
'/stop',
celebrate({
body: Joi.array().items(Joi.string().required()),
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
@@ -81,16 +201,58 @@ export default (app: Router) => {
const data = await cronService.stop(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.delete(
'/labels',
celebrate({
body: Joi.object({
ids: Joi.array().items(Joi.number().required()),
labels: Joi.array().items(Joi.string().required()),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.removeLabels(
req.body.ids,
req.body.labels,
);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.post(
'/labels',
celebrate({
body: Joi.object({
ids: Joi.array().items(Joi.number().required()),
labels: Joi.array().items(Joi.string().required()),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.addLabels(req.body.ids, req.body.labels);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/crons/disable',
'/disable',
celebrate({
body: Joi.array().items(Joi.string().required()),
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
@@ -99,16 +261,15 @@ export default (app: Router) => {
const data = await cronService.disabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/crons/enable',
'/enable',
celebrate({
body: Joi.array().items(Joi.string().required()),
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
@@ -117,40 +278,39 @@ export default (app: Router) => {
const data = await cronService.enabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/crons/:id/log',
'/:id/log',
celebrate({
params: Joi.object({
id: Joi.string().required(),
id: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.log(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/crons',
'/',
celebrate({
body: Joi.object({
command: Joi.string().optional(),
schedule: Joi.string().optional(),
name: Joi.string().optional(),
_id: Joi.string().required(),
labels: Joi.array().optional().allow(null),
command: Joi.string().required(),
schedule: Joi.string().required(),
name: Joi.string().optional().allow(null),
id: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
@@ -167,16 +327,15 @@ export default (app: Router) => {
return res.send({ code: 400, message: 'param schedule error' });
}
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.delete(
'/crons',
'/',
celebrate({
body: Joi.array().items(Joi.string().required()),
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
@@ -185,14 +344,47 @@ export default (app: Router) => {
const data = await cronService.remove(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/pin',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.pin(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/unpin',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.unPin(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.get(
'/crons/import',
'/import',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -200,38 +392,40 @@ export default (app: Router) => {
const data = await cronService.import_crontab();
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/crons/:id',
'/:id',
celebrate({
params: Joi.object({
id: Joi.string().required(),
id: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.get(req.params.id);
const data = await cronService.getDb({ id: req.params.id });
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/crons/status',
'/status',
celebrate({
body: Joi.object({
ids: Joi.array().items(Joi.string().required()),
ids: Joi.array().items(Joi.number().required()),
status: Joi.string().required(),
pid: Joi.string().optional().allow(null),
log_path: Joi.string().optional().allow(null),
last_running_time: Joi.number().optional().allow(null),
last_execution_time: Joi.number().optional().allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
@@ -241,10 +435,29 @@ export default (app: Router) => {
const data = await cronService.status({
...req.body,
status: parseInt(req.body.status),
pid: parseInt(req.body.pid) || '',
});
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/:id/logs',
celebrate({
params: Joi.object({
id: Joi.number().required(),
}),
}),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.logs(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
+137
View File
@@ -0,0 +1,137 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import DependenceService from '../services/dependence';
import { Logger } from 'winston';
import { celebrate, Joi } from 'celebrate';
const route = Router();
export default (app: Router) => {
app.use('/dependencies', route);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const dependenceService = Container.get(DependenceService);
const data = await dependenceService.dependencies(req.query as any);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.post(
'/',
celebrate({
body: Joi.array().items(
Joi.object({
name: Joi.string().required(),
type: Joi.number().required(),
remark: Joi.string().optional().allow(''),
}),
),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const dependenceService = Container.get(DependenceService);
const data = await dependenceService.create(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/',
celebrate({
body: Joi.object({
name: Joi.string().required(),
id: Joi.number().required(),
type: Joi.number().required(),
remark: Joi.string().optional().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const dependenceService = Container.get(DependenceService);
const data = await dependenceService.update(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.delete(
'/',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const dependenceService = Container.get(DependenceService);
const data = await dependenceService.remove(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.delete(
'/force',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const dependenceService = Container.get(DependenceService);
const data = await dependenceService.remove(req.body, true);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.get(
'/:id',
celebrate({
params: Joi.object({
id: Joi.number().required(),
}),
}),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const dependenceService = Container.get(DependenceService);
const data = await dependenceService.getDb({ id: req.params.id });
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/reinstall',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const dependenceService = Container.get(DependenceService);
const data = await dependenceService.reInstall(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
};
+72 -73
View File
@@ -1,179 +1,178 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import CookieService from '../services/cookie';
import EnvService from '../services/env';
import { Logger } from 'winston';
import { celebrate, Joi } from 'celebrate';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.get(
'/cookies',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.cookies('', { position: -1 }, true);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
app.use('/envs', route);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const envService = Container.get(EnvService);
const data = await envService.envs(req.query.searchValue as string);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.post(
'/cookies',
'/',
celebrate({
body: Joi.array().items(Joi.string().required()).min(1),
body: Joi.array().items(
Joi.object({
value: Joi.string().required(),
name: Joi.string().required(),
remarks: Joi.string().optional().allow(''),
}),
),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.create(req.body);
const envService = Container.get(EnvService);
const data = await envService.create(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/cookies',
'/',
celebrate({
body: Joi.object({
value: Joi.string().required(),
_id: Joi.string().required(),
name: Joi.string().required(),
remarks: Joi.string().optional().allow('').allow(null),
id: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.update(req.body);
const envService = Container.get(EnvService);
const data = await envService.update(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.delete(
'/cookies',
'/',
celebrate({
body: Joi.array().items(Joi.string().required()),
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.remove(req.body);
const envService = Container.get(EnvService);
const data = await envService.remove(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/cookies/:id/move',
'/:id/move',
celebrate({
params: Joi.object({
id: Joi.string().required(),
id: Joi.number().required(),
}),
body: Joi.object({
fromIndex: Joi.number().required(),
toIndex: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.move(req.params.id, req.body);
const envService = Container.get(EnvService);
const data = await envService.move(req.params.id, req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/cookies/:id/refresh',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.refreshCookie(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/cookies/disable',
'/disable',
celebrate({
body: Joi.array().items(Joi.string().required()),
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.disabled(req.body);
const envService = Container.get(EnvService);
const data = await envService.disabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/cookies/enable',
'/enable',
celebrate({
body: Joi.array().items(Joi.string().required()),
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.enabled(req.body);
const envService = Container.get(EnvService);
const data = await envService.enabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/name',
celebrate({
body: Joi.object({
ids: Joi.array().items(Joi.number().required()),
name: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const envService = Container.get(EnvService);
const data = await envService.updateNames(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/cookies/:id',
'/:id',
celebrate({
params: Joi.object({
id: Joi.string().required(),
id: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cookieService = Container.get(CookieService);
const data = await cookieService.get(req.params.id);
const envService = Container.get(EnvService);
const data = await envService.getDb({ id: req.params.id });
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
+14 -4
View File
@@ -1,17 +1,27 @@
import { Router } from 'express';
import auth from './auth';
import cookie from './cookie';
import user from './user';
import env from './env';
import config from './config';
import log from './log';
import cron from './cron';
import script from './script';
import open from './open';
import dependence from './dependence';
import system from './system';
import subscription from './subscription';
export default () => {
const app = Router();
auth(app);
cookie(app);
user(app);
env(app);
config(app);
log(app);
cron(app);
script(app);
open(app);
dependence(app);
system(app);
subscription(app);
return app;
};
+27 -56
View File
@@ -3,73 +3,44 @@ import { Container } from 'typedi';
import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config';
import { getFileContentByName } from '../config/util';
import { getFileContentByName, readDirs } from '../config/util';
import { join } from 'path';
const route = Router();
const blacklist = ['.tmp'];
export default (app: Router) => {
app.use('/', route);
app.use('/logs', route);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const result = readDirs(config.logPath, config.logPath, blacklist);
res.send({
code: 200,
data: result,
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.get(
'/logs',
'/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const fileList = fs.readdirSync(config.logPath, 'utf-8');
const dirs = [];
for (let i = 0; i < fileList.length; i++) {
const stat = fs.lstatSync(config.logPath + fileList[i]);
if (stat.isDirectory()) {
const fileListTmp = fs.readdirSync(
`${config.logPath}/${fileList[i]}`,
'utf-8',
);
dirs.push({
name: fileList[i],
isDir: true,
files: fileListTmp.reverse(),
});
} else {
dirs.push({
name: fileList[i],
isDir: false,
files: [],
});
}
if (blacklist.includes(req.path)) {
return res.send({ code: 403, message: '暂无权限' });
}
res.send({ code: 200, dirs });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/logs/:dir/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const { dir, file } = req.params;
const content = getFileContentByName(
`${config.logPath}/${dir}/${file}`,
const filePath = join(
config.logPath,
(req.query.path || '') as string,
req.params.file,
);
const content = getFileContentByName(filePath);
res.send({ code: 200, data: content });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/logs/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const { file } = req.params;
const content = getFileContentByName(`${config.logPath}/${file}`);
res.send({ code: 200, data: content });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
+120
View File
@@ -0,0 +1,120 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import OpenService from '../services/open';
import { Logger } from 'winston';
import { celebrate, Joi } from 'celebrate';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.get(
'/apps',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const openService = Container.get(OpenService);
const data = await openService.list();
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.post(
'/apps',
celebrate({
body: Joi.object({
name: Joi.string().optional().allow('').disallow('system'),
scopes: Joi.array().items(Joi.string().required()),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const openService = Container.get(OpenService);
const data = await openService.create(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/apps',
celebrate({
body: Joi.object({
name: Joi.string().optional().allow(''),
scopes: Joi.array().items(Joi.string()),
id: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const openService = Container.get(OpenService);
const data = await openService.update(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.delete(
'/apps',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const openService = Container.get(OpenService);
const data = await openService.remove(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/apps/:id/reset-secret',
celebrate({
params: Joi.object({
id: Joi.number().required(),
}),
}),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const openService = Container.get(OpenService);
const data = await openService.resetSecret(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.get(
'/auth/token',
celebrate({
query: {
client_id: Joi.string().required(),
client_secret: Joi.string().required(),
},
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const openService = Container.get(OpenService);
const result = await openService.authToken(req.query as any);
return res.send(result);
} catch (e) {
return next(e);
}
},
);
};
+260
View File
@@ -0,0 +1,260 @@
import {
fileExist,
getFileContentByName,
readDirs,
getLastModifyFilePath,
readDir,
} from '../config/util';
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import config from '../config';
import * as fs from 'fs';
import { celebrate, Joi } from 'celebrate';
import path, { join, parse } from 'path';
import ScriptService from '../services/script';
import multer from 'multer';
const route = Router();
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, config.scriptPath);
},
filename: function (req, file, cb) {
cb(null, file.originalname);
},
});
const upload = multer({ storage: storage });
export default (app: Router) => {
app.use('/scripts', route);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let result = [];
const blacklist = ['node_modules', '.git'];
if (req.query.path) {
const targetPath = path.join(
config.scriptPath,
req.query.path as string,
);
result = readDir(targetPath, config.scriptPath, blacklist);
} else {
result = readDirs(config.scriptPath, config.scriptPath, blacklist);
}
res.send({
code: 200,
data: result,
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.get(
'/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const filePath = join(
config.scriptPath,
req.query.path as string,
req.params.file,
);
const content = getFileContentByName(filePath);
res.send({ code: 200, data: content });
} catch (e) {
return next(e);
}
},
);
route.post(
'/',
upload.single('file'),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, path, content, originFilename } = req.body as {
filename: string;
path: string;
content: string;
originFilename: string;
};
if (!path) {
path = config.scriptPath;
}
if (!path.endsWith('/')) {
path += '/';
}
if (!path.startsWith('/')) {
path = `${config.scriptPath}${path}`;
}
if (config.writePathList.every((x) => !path.startsWith(x))) {
return res.send({
code: 430,
message: '文件路径禁止访问',
});
}
if (req.file) {
fs.renameSync(req.file.path, join(path, req.file.filename));
return res.send({ code: 200 });
}
if (!originFilename) {
originFilename = filename;
}
const originFilePath = `${path}${originFilename.replace(/\//g, '')}`;
const filePath = `${path}${filename.replace(/\//g, '')}`;
if (fs.existsSync(originFilePath)) {
fs.copyFileSync(
originFilePath,
`${config.bakPath}${originFilename.replace(/\//g, '')}`,
);
if (filename !== originFilename) {
fs.unlinkSync(originFilePath);
}
}
fs.writeFileSync(filePath, content);
return res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
route.put(
'/',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().optional().allow(''),
content: Joi.string().required().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, content, path } = req.body as {
filename: string;
content: string;
path: string;
};
const filePath = join(config.scriptPath, path, filename);
fs.writeFileSync(filePath, content);
return res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
route.delete(
'/',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, path } = req.body as {
filename: string;
path: string;
};
const filePath = join(config.scriptPath, path, filename);
fs.unlinkSync(filePath);
res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
route.post(
'/download',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename } = req.body as {
filename: string;
};
const filePath = `${config.scriptPath}${filename}`;
// const stats = fs.statSync(filePath);
// res.set({
// 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件
// 'Content-Disposition': 'attachment; filename=' + filename, //告诉浏览器这是一个需要下载的文件
// 'Content-Length': stats.size //文件大小
// });
// fs.createReadStream(filePath).pipe(res);
return res.download(filePath, filename, (err) => {
return next(err);
});
} catch (e) {
return next(e);
}
},
);
route.put(
'/run',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
content: Joi.string().optional().allow(''),
path: Joi.string().optional().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, content, path } = req.body;
const { name, ext } = parse(filename);
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
fs.writeFileSync(filePath, content || '', { encoding: 'utf8' });
const scriptService = Container.get(ScriptService);
const result = await scriptService.runScript(filePath);
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.put(
'/stop',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
content: Joi.string().optional().allow(''),
path: Joi.string().optional().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, content, path } = req.body;
const { name, ext } = parse(filename);
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
const scriptService = Container.get(ScriptService);
const result = await scriptService.stopScript(filePath);
res.send(result);
} catch (e) {
return next(e);
}
},
);
};
+283
View File
@@ -0,0 +1,283 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import SubscriptionService from '../services/subscription';
import { celebrate, Joi } from 'celebrate';
import cron_parser from 'cron-parser';
const route = Router();
export default (app: Router) => {
app.use('/subscriptions', route);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.list(
req.query.searchValue as string,
);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.post(
'/',
celebrate({
body: Joi.object({
type: Joi.string().required(),
schedule: Joi.string().optional().allow('').allow(null),
interval_schedule: Joi.object({
type: Joi.string().required(),
value: Joi.number().min(1).required(),
})
.optional()
.allow('')
.allow(null),
name: Joi.string().optional().allow('').allow(null),
url: Joi.string().required(),
whitelist: Joi.string().optional().allow('').allow(null),
blacklist: Joi.string().optional().allow('').allow(null),
branch: Joi.string().optional().allow('').allow(null),
dependences: Joi.string().optional().allow('').allow(null),
pull_type: Joi.string().optional().allow('').allow(null),
pull_option: Joi.object().optional().allow('').allow(null),
extensions: Joi.string().optional().allow('').allow(null),
sub_before: Joi.string().optional().allow('').allow(null),
sub_after: Joi.string().optional().allow('').allow(null),
schedule_type: Joi.string().required(),
alias: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
if (
!req.body.schedule ||
cron_parser.parseExpression(req.body.schedule).hasNext()
) {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.create(req.body);
return res.send({ code: 200, data });
} else {
return res.send({ code: 400, message: 'param schedule error' });
}
} catch (e) {
return next(e);
}
},
);
route.put(
'/run',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.run(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/stop',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.stop(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/disable',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.disabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/enable',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.enabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.get(
'/:id/log',
celebrate({
params: Joi.object({
id: Joi.number().required(),
}),
}),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.log(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/',
celebrate({
body: Joi.object({
type: Joi.string().required(),
schedule: Joi.string().optional().allow('').allow(null),
interval_schedule: Joi.object().optional().allow('').allow(null),
name: Joi.string().optional().allow('').allow(null),
url: Joi.string().required(),
whitelist: Joi.string().optional().allow('').allow(null),
blacklist: Joi.string().optional().allow('').allow(null),
branch: Joi.string().optional().allow('').allow(null),
dependences: Joi.string().optional().allow('').allow(null),
pull_type: Joi.string().optional().allow('').allow(null),
pull_option: Joi.object().optional().allow('').allow(null),
schedule_type: Joi.string().optional().allow('').allow(null),
extensions: Joi.string().optional().allow('').allow(null),
sub_before: Joi.string().optional().allow('').allow(null),
sub_after: Joi.string().optional().allow('').allow(null),
alias: Joi.string().required(),
id: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
if (
!req.body.schedule ||
typeof req.body.schedule === 'object' ||
cron_parser.parseExpression(req.body.schedule).hasNext()
) {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.update(req.body);
return res.send({ code: 200, data });
} else {
return res.send({ code: 400, message: 'param schedule error' });
}
} catch (e) {
return next(e);
}
},
);
route.delete(
'/',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.remove(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.get(
'/:id',
celebrate({
params: Joi.object({
id: Joi.number().required(),
}),
}),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.getDb({ id: req.params.id });
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/status',
celebrate({
body: Joi.object({
ids: Joi.array().items(Joi.number().required()),
status: Joi.string().required(),
pid: Joi.string().optional(),
log_path: Joi.string().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.status({
...req.body,
status: parseInt(req.body.status),
pid: parseInt(req.body.pid) || '',
});
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.get(
'/:id/logs',
celebrate({
params: Joi.object({
id: Joi.number().required(),
}),
}),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.logs(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
};
+130
View File
@@ -0,0 +1,130 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config';
import SystemService from '../services/system';
import { celebrate, Joi } from 'celebrate';
import UserService from '../services/user';
import { EnvModel } from '../data/env';
const route = Router();
export default (app: Router) => {
app.use('/system', route);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
const envCount = await EnvModel.count();
const versionRegx = /.*export const version = \'(.*)\'\;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const version = currentVersionFile.match(versionRegx)![1];
let isInitialized = true;
if (
Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' &&
authInfo.password === 'admin' &&
envCount === 0
) {
isInitialized = false;
}
res.send({
code: 200,
data: {
isInitialized,
version,
},
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.get(
'/log/remove',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const data = await systemService.getLogRemoveFrequency();
res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/log/remove',
celebrate({
body: Joi.object({
frequency: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const result = await systemService.updateLogRemoveFrequency(
req.body.frequency,
);
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.put(
'/update-check',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const result = await systemService.checkUpdate();
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.put(
'/update',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const result = await systemService.updateSystem();
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.put(
'/notify',
celebrate({
body: Joi.object({
title: Joi.string().required(),
content: 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.notify(req.body);
res.send(result);
} catch (e) {
return next(e);
}
},
);
};
+258
View File
@@ -0,0 +1,258 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import UserService from '../services/user';
import { celebrate, Joi } from 'celebrate';
import multer from 'multer';
import path from 'path';
import { v4 as uuidV4 } from 'uuid';
import config from '../config';
const route = Router();
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, config.uploadPath);
},
filename: function (req, file, cb) {
const ext = path.parse(file.originalname).ext;
const key = uuidV4();
cb(null, key + ext);
},
});
const upload = multer({ storage: storage });
export default (app: Router) => {
app.use('/user', route);
route.post(
'/login',
celebrate({
body: Joi.object({
username: Joi.string().required(),
password: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const data = await userService.login({ ...req.body }, req);
return res.send(data);
} catch (e) {
return next(e);
}
},
);
route.post(
'/logout',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
await userService.logout(req.platform);
res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
route.put(
'/',
celebrate({
body: Joi.object({
username: Joi.string().required(),
password: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
await userService.updateUsernameAndPassword(req.body);
res.send({ code: 200, message: '更新成功' });
} catch (e) {
return next(e);
}
},
);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
res.send({
code: 200,
data: {
username: authInfo.username,
avatar: authInfo.avatar,
twoFactorActivated: authInfo.twoFactorActivated,
},
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.get(
'/two-factor/init',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const data = await userService.initTwoFactor();
res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/two-factor/active',
celebrate({
body: Joi.object({
code: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const data = await userService.activeTwoFactor(req.body.code);
res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/two-factor/deactive',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const data = await userService.deactiveTwoFactor();
res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/two-factor/login',
celebrate({
body: Joi.object({
code: Joi.string().required(),
username: Joi.string().required(),
password: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const data = await userService.twoFactorLogin(req.body, req);
res.send(data);
} catch (e) {
return next(e);
}
},
);
route.get(
'/login-log',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const data = await userService.getLoginLog();
res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.get(
'/notification',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const data = await userService.getNotificationMode();
res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/notification',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const result = await userService.updateNotificationMode(req.body);
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.put(
'/init',
celebrate({
body: Joi.object({
username: Joi.string().required(),
password: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
await userService.updateUsernameAndPassword(req.body);
res.send({ code: 200, message: '更新成功' });
} catch (e) {
return next(e);
}
},
);
route.put(
'/notification/init',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const result = await userService.updateNotificationMode(req.body);
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.put(
'/avatar',
upload.single('avatar'),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const result = await userService.updateAvatar(req.file!.filename);
res.send(result);
} catch (e) {
return next(e);
}
},
);
};
+12 -10
View File
@@ -1,28 +1,30 @@
import 'reflect-metadata'; // We need this in order to use @Decorators
import config from './config';
import express from 'express';
import Logger from './loaders/logger';
import path from 'path';
async function startServer() {
const app = express();
await require('./loaders').default({ expressApp: app });
await require('./loaders/db').default();
app
await require('./loaders/initFile').default();
await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/app').default({ expressApp: app });
const server = app
.listen(config.port, () => {
Logger.info(`
################################################
🛡️ Server listening on port: ${config.port} 🛡️
################################################
`);
Logger.debug(`✌️ Back server launched on port ${config.port}`);
})
.on('error', (err) => {
Logger.error(err);
process.exit(1);
});
await require('./loaders/server').default({ server });
}
startServer();
+65 -30
View File
@@ -1,45 +1,63 @@
import dotenv from 'dotenv';
import path from 'path';
import { createRandomString } from './util';
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
const envFound = dotenv.config();
const rootPath = path.resolve(__dirname, '../../');
const cookieFile = path.join(rootPath, 'config/cookie.sh');
const confFile = path.join(rootPath, 'config/config.sh');
const sampleFile = path.join(rootPath, 'sample/config.sample.sh');
const crontabFile = path.join(rootPath, 'config/crontab.list');
const confBakDir = path.join(rootPath, 'config/bak/');
const authConfigFile = path.join(rootPath, 'config/auth.json');
const extraFile = path.join(rootPath, 'config/extra.sh');
const logPath = path.join(rootPath, 'log/');
if (!process.env.QL_DIR) {
// 声明QL_DIR环境变量
let qlHomePath = path.join(__dirname, '../../');
// 生产环境
if (qlHomePath.endsWith('/static/')) {
qlHomePath = path.join(qlHomePath, '../');
}
process.env.QL_DIR = qlHomePath;
}
const lastVersionFile = `http://qn.whyour.cn/version.ts?v=${Date.now()}`;
const rootPath = process.env.QL_DIR as string;
const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
const dataPath = path.join(rootPath, 'data/');
const samplePath = path.join(rootPath, 'sample/');
const configPath = path.join(dataPath, 'config/');
const scriptPath = path.join(dataPath, 'scripts/');
const bakPath = path.join(dataPath, 'bak/');
const logPath = path.join(dataPath, 'log/');
const dbPath = path.join(dataPath, 'db/');
const uploadPath = path.join(dataPath, 'upload/');
const envFile = path.join(configPath, 'env.sh');
const confFile = path.join(configPath, 'config.sh');
const crontabFile = path.join(configPath, 'crontab.list');
const authConfigFile = path.join(configPath, 'auth.json');
const extraFile = path.join(configPath, 'extra.sh');
const confBakDir = path.join(dataPath, 'config/bak/');
const sampleFile = path.join(samplePath, 'config.sample.sh');
const sqliteFile = path.join(samplePath, 'database.sqlite');
const authError = '错误的用户名密码,请重试';
const loginFaild = '请先登录!';
const configString = 'config sample crontab shareCode diy';
const dbPath = path.join(rootPath, 'db/');
const manualLogPath = path.join(rootPath, 'manual_log/');
const cronDbFile = path.join(rootPath, 'db/crontab.db');
const cookieDbFile = path.join(rootPath, 'db/cookie.db');
const configFound = dotenv.config({ path: confFile });
const versionFile = path.join(rootPath, 'src/version.ts');
if (envFound.error) {
throw new Error("⚠️ Couldn't find .env file ⚠️");
}
if (configFound.error) {
throw new Error("⚠️ Couldn't find config.sh file ⚠️");
}
export default {
port: parseInt(process.env.PORT as string, 10),
port: parseInt(process.env.BACK_PORT as string, 10),
cronPort: parseInt(process.env.CRON_PORT as string, 10),
secret: process.env.SECRET,
publicPort: parseInt(process.env.PUBLIC_PORT as string, 10),
secret: process.env.SECRET || createRandomString(16, 32),
logs: {
level: process.env.LOG_LEVEL || 'silly',
},
api: {
prefix: '/api',
},
rootPath,
configString,
loginFaild,
authError,
@@ -50,14 +68,31 @@ export default {
crontabFile,
sampleFile,
confFile,
cookieFile,
fileMap: {
'config.sh': confFile,
'crontab.list': crontabFile,
'extra.sh': extraFile,
},
envFile,
dbPath,
cronDbFile,
cookieDbFile,
manualLogPath,
uploadPath,
configPath,
scriptPath,
samplePath,
blackFileList: [
'auth.json',
'config.sh.sample',
'cookie.sh',
'crontab.list',
'env.sh',
'token.json',
],
writePathList: [configPath, scriptPath],
bakPath,
apiWhiteList: [
'/api/user/login',
'/open/auth/token',
'/api/user/two-factor/login',
'/api/system',
'/api/user/init',
'/api/user/notification/init',
],
versionFile,
lastVersionFile,
sqliteFile,
};
+247 -3
View File
@@ -1,5 +1,8 @@
import * as fs from 'fs';
import * as path from 'path';
import got from 'got';
import iconv from 'iconv-lite';
import { exec } from 'child_process';
export function getFileContentByName(fileName: string) {
if (fs.existsSync(fileName)) {
@@ -27,7 +30,7 @@ export function getLastModifyFilePath(dir: string) {
return filePath;
}
export function createPassword(min: number, max: number): string {
export function createRandomString(min: number, max: number): string {
const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const english = [
'a',
@@ -85,7 +88,7 @@ export function createPassword(min: number, max: number): string {
'Y',
'Z',
];
const special = ['-', '_', '#'];
const special = ['-', '_'];
const config = num.concat(english).concat(ENGLISH).concat(special);
const arr = [];
@@ -105,9 +108,250 @@ export function createPassword(min: number, max: number): string {
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
}
function getOne(arr) {
function getOne(arr: any[]) {
return arr[Math.floor(Math.random() * arr.length)];
}
return newArr.join('');
}
export function getToken(req: any) {
const { authorization = '' } = req.headers;
if (authorization && authorization.split(' ')[0] === 'Bearer') {
return (authorization as string)
.replace('Bearer ', '')
.replace('mobile-', '')
.replace('desktop-', '');
}
return '';
}
export async function getNetIp(req: any) {
const ipArray = [
...new Set([
...(req.headers['x-real-ip'] || '').split(','),
...(req.headers['x-forwarded-for'] || '').split(','),
req.ip,
...req.ips,
req.socket.remoteAddress,
]),
];
let ip = ipArray[0];
if (ipArray.length > 1) {
for (let i = 0; i < ipArray.length; i++) {
const ipNumArray = ipArray[i].split('.');
const tmp = ipNumArray[0] + '.' + ipNumArray[1];
if (
tmp === '192.168' ||
(ipNumArray[0] === '172' &&
ipNumArray[1] >= 16 &&
ipNumArray[1] <= 32) ||
tmp === '10.7' ||
tmp === '127.0'
) {
continue;
}
ip = ipArray[i];
break;
}
}
ip = ip.substr(ip.lastIndexOf(':') + 1, ip.length);
if (ip.includes('127.0') || ip.includes('192.168') || ip.includes('10.7')) {
ip = '';
}
try {
const baiduApi = got
.get(`https://www.cip.cc/${ip}`, { timeout: 10000, retry: 0 })
.text();
const ipApi = got
.get(`https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`, {
timeout: 10000,
retry: 0,
})
.buffer();
const [data, ipApiBody] = await await Promise.all<any>([baiduApi, ipApi]);
const ipRegx = /.*IP :(.*)\n/;
const addrRegx = /.*数据二 :(.*)\n/;
if (data && ipRegx.test(data) && addrRegx.test(data)) {
const ip = data.match(ipRegx)[1];
const addr = data.match(addrRegx)[1];
return { address: addr, ip };
} else if (ipApiBody) {
const { addr, ip } = JSON.parse(iconv.decode(ipApiBody, 'GBK'));
return { address: `${addr}`, ip };
} else {
return { address: `获取失败`, ip };
}
} catch (error) {
return { address: `获取失败`, ip };
}
}
export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
const ua = userAgent.toLowerCase();
const testUa = (regexp: RegExp) => regexp.test(ua);
const testVs = (regexp: RegExp) =>
(ua.match(regexp) || [])
.toString()
.replace(/[^0-9|_.]/g, '')
.replace(/_/g, '.');
// 系统
let system = 'unknow';
if (testUa(/windows|win32|win64|wow32|wow64/g)) {
system = 'windows'; // windows系统
} else if (testUa(/macintosh|macintel/g)) {
system = 'macos'; // macos系统
} else if (testUa(/x11/g)) {
system = 'linux'; // linux系统
} else if (testUa(/android|adr/g)) {
system = 'android'; // android系统
} else if (testUa(/ios|iphone|ipad|ipod|iwatch/g)) {
system = 'ios'; // ios系统
}
let platform = 'desktop';
if (system === 'windows' || system === 'macos' || system === 'linux') {
platform = 'desktop';
} else if (system === 'android' || system === 'ios' || testUa(/mobile/g)) {
platform = 'mobile';
}
return platform as 'mobile' | 'desktop';
}
export async function fileExist(file: any) {
return new Promise((resolve) => {
try {
fs.accessSync(file);
resolve(true);
} catch (error) {
resolve(false);
}
});
}
export async function createFile(file: string, data: string = '') {
return new Promise((resolve) => {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, data);
resolve(true);
});
}
export async function concurrentRun(
fnList: Array<() => Promise<any>> = [],
max = 5,
) {
if (!fnList.length) return;
const replyList: any[] = []; // 收集任务执行结果
const startTime = new Date().getTime(); // 记录任务执行开始时间
// 任务执行程序
const schedule = async (index: number) => {
return new Promise(async (resolve) => {
const fn = fnList[index];
if (!fn) return resolve(null);
// 执行当前异步任务
const reply = await fn();
replyList[index] = reply;
// 执行完当前任务后,继续执行任务池的剩余任务
await schedule(index + max);
resolve(null);
});
};
// 任务池执行程序
const scheduleList = new Array(max)
.fill(0)
.map((_, index) => schedule(index));
// 使用 Promise.all 批量执行
const r = await Promise.all(scheduleList);
const cost = (new Date().getTime() - startTime) / 1000;
return replyList;
}
enum FileType {
'directory',
'file',
}
export function readDirs(
dir: string,
baseDir: string = '',
blacklist: string[] = [],
) {
const relativePath = path.relative(baseDir, dir);
const files = fs.readdirSync(dir);
const result: any = files
.filter((x) => !blacklist.includes(x))
.map((file: string) => {
const subPath = path.join(dir, file);
const stats = fs.statSync(subPath);
const key = path.join(relativePath, file);
if (stats.isDirectory()) {
return {
title: file,
key,
type: 'directory',
disabled: true,
parent: relativePath,
children: readDirs(subPath, baseDir).sort(
(a: any, b: any) =>
(FileType as any)[a.type] - (FileType as any)[b.type],
),
};
}
return {
title: file,
type: 'file',
key,
parent: relativePath,
};
});
return result.sort(
(a: any, b: any) => (FileType as any)[a.type] - (FileType as any)[b.type],
);
}
export function readDir(
dir: string,
baseDir: string = '',
blacklist: string[] = [],
) {
const relativePath = path.relative(baseDir, dir);
const files = fs.readdirSync(dir);
const result: any = files
.filter((x) => !blacklist.includes(x))
.map((file: string) => {
const subPath = path.join(dir, file);
const stats = fs.statSync(subPath);
const key = path.join(relativePath, file);
return {
title: file,
type: stats.isDirectory() ? 'directory' : 'file',
key,
parent: relativePath,
};
});
return result;
}
export function promiseExec(command: string): Promise<string> {
return new Promise((resolve, reject) => {
exec(
command,
{ maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' },
(err, stdout, stderr) => {
resolve(stdout || stderr || JSON.stringify(err));
},
);
});
}
+38
View File
@@ -0,0 +1,38 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
export class AuthInfo {
ip?: string;
type: AuthDataType;
info?: any;
id?: number;
constructor(options: AuthInfo) {
this.ip = options.ip;
this.info = options.info;
this.type = options.type;
this.id = options.id;
}
}
export enum LoginStatus {
'success',
'fail',
}
export enum AuthDataType {
'loginLog' = 'loginLog',
'authToken' = 'authToken',
'notification' = 'notification',
'removeLogFrequency' = 'removeLogFrequency',
}
interface AuthInstance extends Model<AuthInfo, AuthInfo>, AuthInfo {}
export const AuthModel = sequelize.define<AuthInstance>('Auth', {
ip: DataTypes.STRING,
type: DataTypes.STRING,
info: {
type: DataTypes.JSON,
allowNull: true,
},
});
-27
View File
@@ -1,27 +0,0 @@
export class Cookie {
value?: string;
timestamp?: string;
created?: number;
_id?: string;
status?: CookieStatus;
position?: number;
constructor(options: Cookie) {
this.value = options.value;
this._id = options._id;
this.created = options.created || new Date().valueOf();
this.status = options.status || CookieStatus.noacquired;
this.timestamp = new Date().toString();
this.position = options.position;
}
}
export enum CookieStatus {
'noacquired',
'normal',
'disabled',
'invalid',
'abnormal',
}
export const initCookiePosition = 9999999999;
+47 -6
View File
@@ -1,28 +1,42 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
export class Crontab {
name?: string;
command: string;
schedule: string;
schedule?: string;
timestamp?: string;
created?: number;
saved?: boolean;
_id?: string;
id?: number;
status?: CrontabStatus;
isSystem?: 1 | 0;
pid?: number;
isDisabled?: 1 | 0;
log_path?: string;
isPinned?: 1 | 0;
labels?: string[];
last_running_time?: number;
last_execution_time?: number;
constructor(options: Crontab) {
this.name = options.name;
this.command = options.command;
this.schedule = options.schedule;
this.saved = options.saved;
this._id = options._id;
this.created = options.created;
this.status = options.status || CrontabStatus.idle;
this.id = options.id;
this.status =
options.status && CrontabStatus[options.status]
? options.status
: CrontabStatus.idle;
this.timestamp = new Date().toString();
this.isSystem = options.isSystem || 0;
this.pid = options.pid;
this.isDisabled = options.isDisabled || 0;
this.log_path = options.log_path || '';
this.isPinned = options.isPinned || 0;
this.labels = options.labels || [];
this.last_running_time = options.last_running_time || 0;
this.last_execution_time = options.last_execution_time || 0;
}
}
@@ -32,3 +46,30 @@ export enum CrontabStatus {
'disabled',
'queued',
}
interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
name: {
unique: 'compositeIndex',
type: DataTypes.STRING,
},
command: {
unique: 'compositeIndex',
type: DataTypes.STRING,
},
schedule: {
unique: 'compositeIndex',
type: DataTypes.STRING,
},
timestamp: DataTypes.STRING,
saved: DataTypes.BOOLEAN,
status: DataTypes.NUMBER,
isSystem: DataTypes.NUMBER,
pid: DataTypes.NUMBER,
isDisabled: DataTypes.NUMBER,
isPinned: DataTypes.NUMBER,
log_path: DataTypes.STRING,
labels: DataTypes.JSON,
last_running_time: DataTypes.NUMBER,
last_execution_time: DataTypes.NUMBER,
});
+47
View File
@@ -0,0 +1,47 @@
import { sequelize } from '.';
import { DataTypes, Model } from 'sequelize';
interface SortType {
type: 'ASC' | 'DESC';
value: string;
}
interface FilterType {
type: 'or' | 'and';
value: string;
}
export class CrontabView {
name?: string;
id?: number;
position?: number;
isDisabled?: 1 | 0;
filters?: FilterType[];
sorts?: SortType[];
constructor(options: CrontabView) {
this.name = options.name;
this.id = options.id;
this.position = options.position;
this.isDisabled = options.isDisabled || 0;
this.filters = options.filters;
this.sorts = options.sorts;
}
}
interface CronViewInstance
extends Model<CrontabView, CrontabView>,
CrontabView {}
export const CrontabViewModel = sequelize.define<CronViewInstance>(
'CrontabView',
{
name: {
unique: 'name',
type: DataTypes.STRING,
},
position: DataTypes.NUMBER,
isDisabled: DataTypes.NUMBER,
filters: DataTypes.JSON,
sorts: DataTypes.JSON,
},
);
+64
View File
@@ -0,0 +1,64 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
export class Dependence {
timestamp?: string;
id?: number;
status?: DependenceStatus;
type?: DependenceTypes;
name?: number;
log?: string[];
remark?: string;
constructor(options: Dependence) {
this.id = options.id;
this.status = options.status || DependenceStatus.installing;
this.type = options.type || DependenceTypes.nodejs;
this.timestamp = new Date().toString();
this.name = options.name;
this.log = options.log || [];
this.remark = options.remark || '';
}
}
export enum DependenceStatus {
'installing',
'installed',
'installFailed',
'removing',
'removed',
'removeFailed',
}
export enum DependenceTypes {
'nodejs',
'python3',
'linux',
}
export enum InstallDependenceCommandTypes {
'pnpm add -g',
'pip3 install',
'apk add',
}
export enum unInstallDependenceCommandTypes {
'pnpm remove -g',
'pip3 uninstall -y',
'apk del',
}
interface DependenceInstance
extends Model<Dependence, Dependence>,
Dependence {}
export const DependenceModel = sequelize.define<DependenceInstance>(
'Dependence',
{
name: DataTypes.STRING,
type: DataTypes.NUMBER,
timestamp: DataTypes.STRING,
status: DataTypes.NUMBER,
log: DataTypes.JSON,
remark: DataTypes.STRING,
},
);
+39
View File
@@ -0,0 +1,39 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
export class Env {
value?: string;
timestamp?: string;
id?: number;
status?: EnvStatus;
position?: number;
name?: string;
remarks?: string;
constructor(options: Env) {
this.value = options.value;
this.id = options.id;
this.status = options.status || EnvStatus.normal;
this.timestamp = new Date().toString();
this.position = options.position;
this.name = options.name;
this.remarks = options.remarks;
}
}
export enum EnvStatus {
'normal',
'disabled',
}
export const initEnvPosition = 9999999999;
interface EnvInstance extends Model<Env, Env>, Env {}
export const EnvModel = sequelize.define<EnvInstance>('Env', {
value: { type: DataTypes.STRING, unique: 'compositeIndex' },
timestamp: DataTypes.STRING,
status: DataTypes.NUMBER,
position: DataTypes.NUMBER,
name: { type: DataTypes.STRING, unique: 'compositeIndex' },
remarks: DataTypes.STRING,
});
+15
View File
@@ -0,0 +1,15 @@
import { Sequelize } from 'sequelize';
import config from '../config/index';
export const sequelize = new Sequelize({
dialect: 'sqlite',
storage: `${config.dbPath}database.sqlite`,
logging: false,
pool: {
max: 6,
min: 0,
idle: 30000,
},
});
export type ResponseType<T> = { code: number; data?: T; message?: string };
+103
View File
@@ -0,0 +1,103 @@
export enum NotificationMode {
'gotify' = 'gotify',
'goCqHttpBot' = 'goCqHttpBot',
'serverChan' = 'serverChan',
'pushDeer' = 'pushDeer',
'bark' = 'bark',
'chat' = 'chat',
'telegramBot' = 'telegramBot',
'dingtalkBot' = 'dingtalkBot',
'weWorkBot' = 'weWorkBot',
'weWorkApp' = 'weWorkApp',
'iGot' = 'iGot',
'pushPlus' = 'pushPlus',
'email' = 'email',
}
abstract class NotificationBaseInfo {
public type!: NotificationMode;
}
export class GotifyNotification extends NotificationBaseInfo {
public gotifyUrl = '';
public gotifyToken = '';
public gotifyPriority = 0;
}
export class GoCqHttpBotNotification extends NotificationBaseInfo {
public goCqHttpBotUrl = '';
public goCqHttpBotToken = '';
public goCqHttpBotQq = '';
}
export class ServerChanNotification extends NotificationBaseInfo {
public serverChanKey = '';
}
export class PushDeerNotification extends NotificationBaseInfo {
public pushDeerKey = '';
}
export class ChatNotification extends NotificationBaseInfo {
public chatUrl = '';
public chatToken = '';
}
export class BarkNotification extends NotificationBaseInfo {
public barkPush = '';
public barkIcon = 'http://qn.whyour.cn/logo.png';
public barkSound = '';
public barkGroup = 'qinglong';
}
export class TelegramBotNotification extends NotificationBaseInfo {
public telegramBotToken = '';
public telegramBotUserId = '';
public telegramBotProxyHost = '';
public telegramBotProxyPort = '';
public telegramBotProxyAuth = '';
public telegramBotApiHost = 'api.telegram.org';
}
export class DingtalkBotNotification extends NotificationBaseInfo {
public dingtalkBotToken = '';
public dingtalkBotSecret = '';
}
export class WeWorkBotNotification extends NotificationBaseInfo {
public weWorkBotKey = '';
}
export class WeWorkAppNotification extends NotificationBaseInfo {
public weWorkAppKey = '';
}
export class IGotNotification extends NotificationBaseInfo {
public iGotPushKey = '';
}
export class PushPlusNotification extends NotificationBaseInfo {
public pushPlusToken = '';
public pushPlusUser = '';
}
export class EmailNotification extends NotificationBaseInfo {
public emailService: string = '';
public emailUser: string = '';
public emailPass: string = '';
}
export interface NotificationInfo
extends GoCqHttpBotNotification,
GotifyNotification,
ServerChanNotification,
PushDeerNotification,
ChatNotification,
BarkNotification,
TelegramBotNotification,
DingtalkBotNotification,
WeWorkBotNotification,
WeWorkAppNotification,
IGotNotification,
PushPlusNotification,
EmailNotification {}
+43
View File
@@ -0,0 +1,43 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
export class App {
name: string;
scopes: AppScope[];
client_id: string;
client_secret: string;
tokens?: AppToken[];
id?: number;
constructor(options: App) {
this.name = options.name;
this.scopes = options.scopes;
this.client_id = options.client_id;
this.client_secret = options.client_secret;
this.id = options.id;
}
}
export interface AppToken {
value: string;
type?: 'Bearer';
expiration: number;
}
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
export enum CrontabStatus {
'running',
'idle',
'disabled',
'queued',
}
interface AppInstance extends Model<App, App>, App {}
export const AppModel = sequelize.define<AppInstance>('App', {
name: { type: DataTypes.STRING, unique: 'name' },
scopes: DataTypes.JSON,
client_id: DataTypes.STRING,
client_secret: DataTypes.STRING,
tokens: DataTypes.JSON,
});
+19
View File
@@ -0,0 +1,19 @@
export class SockMessage {
message?: string;
type?: SockMessageType;
references?: number[];
constructor(options: SockMessage) {
this.type = options.type;
this.message = options.message;
this.references = options.references;
}
}
export type SockMessageType =
| 'ping'
| 'installDependence'
| 'uninstallDependence'
| 'updateSystemVersion'
| 'manuallyRunScript'
| 'runSubscriptionEnd';
+106
View File
@@ -0,0 +1,106 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
import { SimpleIntervalSchedule } from 'toad-scheduler';
type SimpleIntervalScheduleUnit = keyof SimpleIntervalSchedule;
export class Subscription {
id?: number;
name?: string;
type?: 'public-repo' | 'private-repo' | 'file';
schedule_type?: 'crontab' | 'interval';
schedule?: string;
interval_schedule?: { type: SimpleIntervalScheduleUnit; value: number };
url?: string;
whitelist?: string;
blacklist?: string;
dependences?: string;
branch?: string;
status?: SubscriptionStatus;
pull_type?: 'ssh-key' | 'user-pwd';
pull_option?:
| { private_key: string }
| { username: string; password: string };
pid?: number;
is_disabled?: 1 | 0;
log_path?: string;
alias: string;
command?: string;
extensions?: string;
sub_before?: string;
sub_after?: string;
constructor(options: Subscription) {
this.id = options.id;
this.name = options.name || options.alias;
this.type = options.type;
this.schedule = options.schedule;
this.status =
options.status && SubscriptionStatus[options.status]
? options.status
: SubscriptionStatus.idle;
this.url = options.url;
this.whitelist = options.whitelist;
this.blacklist = options.blacklist;
this.dependences = options.dependences;
this.branch = options.branch;
this.pull_type = options.pull_type;
this.pull_option = options.pull_option;
this.pid = options.pid;
this.is_disabled = options.is_disabled;
this.log_path = options.log_path;
this.schedule_type = options.schedule_type;
this.alias = options.alias;
this.interval_schedule = options.interval_schedule;
this.extensions = options.extensions;
this.sub_before = options.sub_before;
this.sub_after = options.sub_after;
}
}
export enum SubscriptionStatus {
'running',
'idle',
'disabled',
'queued',
}
interface SubscriptionInstance
extends Model<Subscription, Subscription>,
Subscription {}
export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
'Subscription',
{
name: {
unique: 'compositeIndex',
type: DataTypes.STRING,
},
url: {
unique: 'compositeIndex',
type: DataTypes.STRING,
},
schedule: {
unique: 'compositeIndex',
type: DataTypes.STRING,
},
interval_schedule: {
unique: 'compositeIndex',
type: DataTypes.JSON,
},
type: DataTypes.STRING,
whitelist: DataTypes.STRING,
blacklist: DataTypes.STRING,
status: DataTypes.NUMBER,
dependences: DataTypes.STRING,
extensions: DataTypes.STRING,
sub_before: DataTypes.STRING,
sub_after: DataTypes.STRING,
branch: DataTypes.STRING,
pull_type: DataTypes.STRING,
pull_option: DataTypes.JSON,
pid: DataTypes.NUMBER,
is_disabled: DataTypes.NUMBER,
log_path: DataTypes.STRING,
schedule_type: DataTypes.STRING,
alias: { type: DataTypes.STRING, unique: 'alias' },
},
);
+5
View File
@@ -0,0 +1,5 @@
declare namespace Express {
interface Request {
platform: 'desktop' | 'mobile';
}
}
+24
View File
@@ -0,0 +1,24 @@
import expressLoader from './express';
import depInjectorLoader from './depInjector';
import Logger from './logger';
import initData from './initData';
import { Application } from 'express';
import linkDeps from './deps';
import initTask from './initTask';
export default async ({ expressApp }: { expressApp: Application }) => {
await depInjectorLoader();
Logger.info('✌️ Dependency Injector loaded');
await expressLoader({ app: expressApp });
Logger.info('✌️ Express loaded');
await initData();
Logger.info('✌️ init data loaded');
await linkDeps();
Logger.info('✌️ link deps loaded');
initTask();
Logger.info('✌️ init task loaded');
};
+108
View File
@@ -0,0 +1,108 @@
import Logger from './logger';
import path from 'path';
import DataStore from 'nedb';
import { EnvModel } from '../data/env';
import { CrontabModel } from '../data/cron';
import { DependenceModel } from '../data/dependence';
import { AppModel } from '../data/open';
import { AuthModel } from '../data/auth';
import { fileExist } from '../config/util';
import { SubscriptionModel } from '../data/subscription';
import { CrontabViewModel } from '../data/cronView';
import config from '../config';
export default async () => {
try {
await CrontabModel.sync();
await DependenceModel.sync();
await AppModel.sync();
await AuthModel.sync();
await EnvModel.sync();
await SubscriptionModel.sync();
await CrontabViewModel.sync();
// try {
// const queryInterface = sequelize.getQueryInterface();
// await queryInterface.addIndex('Crontabs', ['command'], { unique: true });
// await queryInterface.addIndex('Envs', ['name', 'value'], { unique: true });
// await queryInterface.addIndex('Apps', ['name'], { unique: true });
// } catch (error) { }
// 2.10-2.11 升级
const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
const envDbFile = path.join(config.rootPath, 'db/env.db');
const appDbFile = path.join(config.rootPath, 'db/app.db');
const authDbFile = path.join(config.rootPath, 'db/auth.db');
const dependenceDbFile = path.join(config.rootPath, 'db/dependence.db');
const crondbExist = await fileExist(cronDbFile);
const dependenceDbExist = await fileExist(dependenceDbFile);
const envDbExist = await fileExist(envDbFile);
const appDbExist = await fileExist(appDbFile);
const authDbExist = await fileExist(authDbFile);
const cronCount = await CrontabModel.count();
const dependenceCount = await DependenceModel.count();
const envCount = await EnvModel.count();
const appCount = await AppModel.count();
const authCount = await AuthModel.count();
if (crondbExist && cronCount === 0) {
const cronDb = new DataStore({
filename: cronDbFile,
autoload: true,
});
cronDb.persistence.compactDatafile();
cronDb.find({}).exec(async (err, docs) => {
await CrontabModel.bulkCreate(docs, { ignoreDuplicates: true });
});
}
if (dependenceDbExist && dependenceCount === 0) {
const dependenceDb = new DataStore({
filename: dependenceDbFile,
autoload: true,
});
dependenceDb.persistence.compactDatafile();
dependenceDb.find({}).exec(async (err, docs) => {
await DependenceModel.bulkCreate(docs, { ignoreDuplicates: true });
});
}
if (envDbExist && envCount === 0) {
const envDb = new DataStore({
filename: envDbFile,
autoload: true,
});
envDb.persistence.compactDatafile();
envDb.find({}).exec(async (err, docs) => {
await EnvModel.bulkCreate(docs, { ignoreDuplicates: true });
});
}
if (appDbExist && appCount === 0) {
const appDb = new DataStore({
filename: appDbFile,
autoload: true,
});
appDb.persistence.compactDatafile();
appDb.find({}).exec(async (err, docs) => {
await AppModel.bulkCreate(docs, { ignoreDuplicates: true });
});
}
if (authDbExist && authCount === 0) {
const authDb = new DataStore({
filename: authDbFile,
autoload: true,
});
authDb.persistence.compactDatafile();
authDb.find({}).exec(async (err, docs) => {
await AuthModel.bulkCreate(docs, { ignoreDuplicates: true });
});
}
Logger.info('✌️ DB loaded');
} catch (error) {
Logger.info('✌️ DB load failed');
Logger.info(error);
}
};
@@ -1,12 +1,8 @@
import { Container } from 'typedi';
import LoggerInstance from './logger';
export default ({ models }: { models: { name: string; model: any }[] }) => {
export default () => {
try {
models.forEach((m) => {
Container.set(m.name, m.model);
});
Container.set('logger', LoggerInstance);
} catch (e) {
LoggerInstance.error('🔥 Error on dependency injector loader: %o', e);
+57
View File
@@ -0,0 +1,57 @@
import path from 'path';
import fs from 'fs';
import chokidar from 'chokidar';
import config from '../config/index';
import { promiseExec } from '../config/util';
function linkToNodeModule(src: string, dst?: string) {
const target = path.join(config.rootPath, 'node_modules', dst || src);
const source = path.join(config.rootPath, src);
fs.lstat(target, (err, stat) => {
if (!stat) {
fs.symlink(source, target, 'dir', (err) => {
if (err) throw err;
});
}
});
}
async function linkCommand() {
const commandPath = await promiseExec('which node');
const commandDir = path.dirname(commandPath);
const linkShell = [
{
src: 'update.sh',
dest: 'ql',
},
{
src: 'task.sh',
dest: 'task',
},
];
for (const link of linkShell) {
const source = path.join(config.rootPath, 'shell', link.src);
const target = path.join(commandDir, link.dest);
if (fs.existsSync(target)) {
fs.unlinkSync(target);
}
fs.symlink(source, target, (err) => {});
}
}
export default async (src: string = 'deps') => {
await linkCommand();
linkToNodeModule(src);
const source = path.join(config.rootPath, src);
const watcher = chokidar.watch(source, {
ignored: /(^|[\/\\])\../, // ignore dotfiles
persistent: true,
});
watcher
.on('add', (path) => linkToNodeModule(src))
.on('change', (path) => linkToNodeModule(src));
};
+138 -12
View File
@@ -1,38 +1,127 @@
import { Request, Response, NextFunction, Application } from 'express';
import express, { Request, Response, NextFunction, Application } from 'express';
import bodyParser from 'body-parser';
import cors from 'cors';
import routes from '../api';
import config from '../config';
import jwt from 'express-jwt';
import jwt, { UnauthorizedError } from 'express-jwt';
import fs from 'fs';
import { getPlatform, getToken } from '../config/util';
import Container from 'typedi';
import OpenService from '../services/open';
import rewrite from 'express-urlrewrite';
import UserService from '../services/user';
import handler from 'serve-handler';
import * as Sentry from '@sentry/node';
import { EnvModel } from '../data/env';
import { errors } from 'celebrate';
export default ({ app }: { app: Application }) => {
app.enable('trust proxy');
app.use(cors());
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
app.use((req, res, next) => {
if (req.path.startsWith('/api') || req.path.startsWith('/open')) {
next();
} else {
return handler(req, res, {
public: 'static/dist',
rewrites: [{ source: '**', destination: '/index.html' }],
});
}
});
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
app.use(
jwt({ secret: config.secret as string, algorithms: ['HS384'] }).unless({
path: ['/api/login'],
jwt({
secret: config.secret as string,
algorithms: ['HS384'],
}).unless({
path: [...config.apiWhiteList, /^\/open\//],
}),
);
app.use((req, res, next) => {
if (req.url && req.url.includes('/api/login')) {
app.use((req: Request, res, next) => {
if (!req.headers) {
req.platform = 'desktop';
} else {
const platform = getPlatform(req.headers['user-agent'] || '');
req.platform = platform;
}
return next();
});
app.use(async (req, res, next) => {
const headerToken = getToken(req);
if (req.path.startsWith('/open/')) {
const openService = Container.get(OpenService);
const doc = await openService.findTokenByValue(headerToken);
if (doc && doc.tokens && doc.tokens.length > 0) {
const currentToken = doc.tokens.find((x) => x.value === headerToken);
const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/);
const key = keyMatch && keyMatch[1];
if (
doc.scopes.includes(key as any) &&
currentToken &&
currentToken.expiration >= Math.round(Date.now() / 1000)
) {
return next();
}
}
}
const originPath = `${req.baseUrl}${req.path === '/' ? '' : req.path}`;
if (
!headerToken &&
originPath &&
config.apiWhiteList.includes(originPath)
) {
return next();
}
const data = fs.readFileSync(config.authConfigFile, 'utf8');
const authHeader = req.headers.authorization;
if (data) {
const { token } = JSON.parse(data);
if (token && authHeader.includes(token)) {
if (data && headerToken) {
const { token = '', tokens = {} } = JSON.parse(data);
if (headerToken === token || tokens[req.platform] === headerToken) {
return next();
}
}
const err: any = new Error('UnauthorizedError');
err['status'] = 401;
const errorCode = headerToken ? 'invalid_token' : 'credentials_required';
const errorMessage = headerToken
? 'jwt malformed'
: 'No authorization token was found';
const err = new UnauthorizedError(errorCode, { message: errorMessage });
next(err);
});
app.use(async (req, res, next) => {
if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) {
return next();
}
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
const envCount = await EnvModel.count();
let isInitialized = true;
if (
Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' &&
authInfo.password === 'admin' &&
envCount === 0
) {
isInitialized = false;
}
if (isInitialized) {
return res.send({ code: 450, message: '未知错误' });
} else {
return next();
}
});
app.use(rewrite('/open/*', '/api/$1'));
app.use(config.api.prefix, routes());
app.use((req, res, next) => {
@@ -41,6 +130,8 @@ export default ({ app }: { app: Application }) => {
next(err);
});
app.use(errors());
app.use(
(
err: Error & { status: number },
@@ -58,6 +149,39 @@ export default ({ app }: { app: Application }) => {
},
);
app.use(
(
err: Error & { errors: any[] },
req: Request,
res: Response,
next: NextFunction,
) => {
if (err.name.includes('Sequelize')) {
return res
.status(500)
.send({
code: 400,
message: `${err.name} ${err.message}`,
validation: err.errors,
})
.end();
}
return next(err);
},
);
app.use(
Sentry.Handlers.errorHandler({
shouldHandleError(error) {
// 排除 SequelizeUniqueConstraintError / NotFound
return (
!['SequelizeUniqueConstraintError'].includes(error.name) ||
!['Not Found'].includes(error.message)
);
},
}),
);
app.use(
(
err: Error & { status: number },
@@ -65,6 +189,8 @@ export default ({ app }: { app: Application }) => {
res: Response,
next: NextFunction,
) => {
Sentry.captureException(err);
res.status(err.status || 500);
res.json({
code: err.status || 500,
-19
View File
@@ -1,19 +0,0 @@
import expressLoader from './express';
import dependencyInjectorLoader from './dependencyInjector';
import Logger from './logger';
import initData from './initData';
export default async ({ expressApp }: { expressApp: any }) => {
Logger.info('✌️ DB loaded and connected!');
await dependencyInjectorLoader({
models: [],
});
Logger.info('✌️ Dependency Injector loaded');
await expressLoader({ app: expressApp });
Logger.info('✌️ Express loaded');
await initData();
Logger.info('✌️ init data loaded');
};
+99 -99
View File
@@ -1,117 +1,117 @@
import DependenceService from '../services/dependence';
import { exec } from 'child_process';
import { Container } from 'typedi';
import { Crontab, CrontabStatus } from '../data/cron';
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import CronService from '../services/cron';
import CookieService from '../services/cookie';
const initData = [
{
name: '更新面板',
command: `ql update`,
schedule: `${randomSchedule(60, 1)} ${randomSchedule(
6,
1,
).toString()} * * *`,
status: CrontabStatus.disabled,
},
{
name: '删除日志',
command: 'ql rmlog 7',
schedule: '30 7 */7 * *',
status: CrontabStatus.idle,
},
{
name: '互助码',
command: 'ql code',
schedule: '30 7 * * *',
status: CrontabStatus.idle,
},
];
import EnvService from '../services/env';
import _ from 'lodash';
import { DependenceModel } from '../data/dependence';
import { Op } from 'sequelize';
import config from '../config';
export default async () => {
const cronService = Container.get(CronService);
const cookieService = Container.get(CookieService);
const cronDb = cronService.getDb();
const envService = Container.get(EnvService);
const dependenceService = Container.get(DependenceService);
cronDb.count({}, async (err, count) => {
if (count === 0) {
const data = initData.map((x) => {
const tab = new Crontab(x);
tab.created = new Date().valueOf();
tab.saved = false;
if (tab.name === '更新面板') {
tab.isSystem = 1;
} else {
tab.isSystem = 0;
}
return tab;
});
cronDb.insert(data);
await cronService.autosave_crontab();
}
});
// 初始化更新所有任务状态为空闲
await CrontabModel.update(
{ status: CrontabStatus.idle },
{ where: { status: [CrontabStatus.running, CrontabStatus.queued] } },
);
// patch更新面板任务状态
cronDb.find({ name: '更新面板' }).exec((err, docs) => {
const doc = docs[0];
if (doc && doc.status === CrontabStatus.running) {
cronDb.update(
{ name: '更新面板' },
{ $set: { status: CrontabStatus.idle } },
);
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
DependenceModel.findAll({
where: {},
order: [['type', 'DESC']],
raw: true,
}).then(async (docs) => {
const groups = _.groupBy(docs, 'type');
const keys = Object.keys(groups).sort((a, b) => parseInt(b) - parseInt(a));
for (const key of keys) {
const group = groups[key];
const depIds = group.map((x) => x.id);
await dependenceService.reInstall(depIds as number[]);
}
});
// 初始化时执行一次所有的ql repo 任务
cronDb
.find({
command: /ql (repo|raw)/,
})
.exec((err, docs) => {
for (let i = 0; i < docs.length; i++) {
const doc = docs[i];
if (doc && doc.isDisabled !== 1) {
exec(doc.command);
CrontabModel.findAll({
where: {
isDisabled: { [Op.ne]: 1 },
command: {
[Op.or]: [{ [Op.like]: `%ql repo%` }, { [Op.like]: `%ql raw%` }],
},
},
}).then((docs) => {
for (let i = 0; i < docs.length; i++) {
const doc = docs[i];
if (doc) {
exec(doc.command);
}
}
});
// 更新2.11.3以前的脚本路径
CrontabModel.findAll({
where: {
command: {
[Op.or]: [
{ [Op.like]: `%\/${config.rootPath}\/scripts\/%` },
{ [Op.like]: `%\/${config.rootPath}\/config\/%` },
{ [Op.like]: `%\/${config.rootPath}\/log\/%` },
{ [Op.like]: `%\/${config.rootPath}\/db\/%` },
],
},
},
}).then(async (docs) => {
for (let i = 0; i < docs.length; i++) {
const doc = docs[i];
if (doc) {
if (doc.command.includes(`${config.rootPath}/scripts/`)) {
await CrontabModel.update(
{ command: doc.command.replace(`${config.rootPath}/scripts/`, '') },
{ where: { id: doc.id } },
);
}
if (doc.command.includes(`${config.rootPath}/log/`)) {
await CrontabModel.update(
{
command: `${config.rootPath}/data/log/${doc.command.replace(
`${config.rootPath}/log/`,
'',
)}`,
},
{ where: { id: doc.id } },
);
}
if (doc.command.includes(`${config.rootPath}/config/`)) {
await CrontabModel.update(
{
command: `${config.rootPath}/data/config/${doc.command.replace(
`${config.rootPath}/config/`,
'',
)}`,
},
{ where: { id: doc.id } },
);
}
if (doc.command.includes(`${config.rootPath}/db/`)) {
await CrontabModel.update(
{
command: `${config.rootPath}/data/db/${doc.command.replace(
`${config.rootPath}/db/`,
'',
)}`,
},
{ where: { id: doc.id } },
);
}
}
});
// patch 禁用状态字段改变
cronDb
.find({
status: CrontabStatus.disabled,
})
.exec((err, docs) => {
if (docs.length > 0) {
const ids = docs.map((x) => x._id);
cronDb.update(
{ _id: { $in: ids } },
{ $set: { status: CrontabStatus.idle, isDisabled: 1 } },
{ multi: true },
(err) => {
cronService.autosave_crontab();
},
);
}
});
}
});
// 初始化保存一次ck和定时任务数据
await cronService.autosave_crontab();
await cookieService.set_cookies();
await envService.set_envs();
};
function randomSchedule(from: number, to: number) {
const result = [];
const arr = [...Array(from).keys()];
let count = arr.length;
for (let i = 0; i < to; i++) {
const index = ~~(Math.random() * count) + i;
if (result.includes(arr[index])) {
continue;
}
result[i] = arr[index];
arr[index] = arr[i];
count--;
}
return result;
}
+68
View File
@@ -0,0 +1,68 @@
import fs from 'fs';
import path from 'path';
import os from 'os';
import dotenv from 'dotenv';
import Logger from './logger';
import { fileExist } from '../config/util';
const rootPath = process.env.QL_DIR as string;;
const dataPath = path.join(rootPath, 'data/');
const configPath = path.join(dataPath, 'config/');
const scriptPath = path.join(dataPath, 'scripts/');
const logPath = path.join(dataPath, 'log/');
const uploadPath = path.join(dataPath, 'upload/');
const bakPath = path.join(dataPath, 'bak/');
const samplePath = path.join(rootPath, 'sample/');
const confFile = path.join(configPath, 'config.sh');
const authConfigFile = path.join(configPath, 'auth.json');
const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
const sampleAuthFile = path.join(samplePath, 'auth.sample.json');
const homedir = os.homedir();
const sshPath = path.resolve(homedir, '.ssh');
export default async () => {
const authFileExist = await fileExist(authConfigFile);
const confFileExist = await fileExist(confFile);
const scriptDirExist = await fileExist(scriptPath);
const logDirExist = await fileExist(logPath);
const configDirExist = await fileExist(configPath);
const uploadDirExist = await fileExist(uploadPath);
const sshDirExist = await fileExist(sshPath);
const bakDirExist = await fileExist(bakPath);
if (!configDirExist) {
fs.mkdirSync(configPath);
}
if (!authFileExist) {
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
}
if (!confFileExist) {
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
}
if (!scriptDirExist) {
fs.mkdirSync(scriptPath);
}
if (!logDirExist) {
fs.mkdirSync(logPath);
}
if (!uploadDirExist) {
fs.mkdirSync(uploadPath);
}
if (!sshDirExist) {
fs.mkdirSync(sshPath);
}
if (!bakDirExist) {
fs.mkdirSync(bakPath);
}
dotenv.config({ path: confFile });
Logger.info('✌️ Init file down');
};
+54
View File
@@ -0,0 +1,54 @@
import { Container } from 'typedi';
import _ from 'lodash';
import SystemService from '../services/system';
import ScheduleService from '../services/schedule';
import SubscriptionService from '../services/subscription';
import config from '../config';
import { fileExist } from '../config/util';
export default async () => {
const systemService = Container.get(SystemService);
const scheduleService = Container.get(ScheduleService);
const subscriptionService = Container.get(SubscriptionService);
// 生成内置token
let tokenCommand = `ts-node-transpile-only ${config.rootPath}/back/token.ts`;
const tokenFile = `${config.rootPath}/static/build/token.js`;
if (await fileExist(tokenFile)) {
tokenCommand = `node ${tokenFile}`;
}
const cron = {
id: 'token',
name: '生成token',
command: tokenCommand,
};
scheduleService.createIntervalTask(cron as any, {
days: 28,
runImmediately: true,
});
// 运行删除日志任务
const data = await systemService.getLogRemoveFrequency();
if (data && data.info && data.info.frequency) {
const cron = {
id: data.id,
name: '删除日志',
command: `ql rmlog ${data.info.frequency}`,
};
scheduleService.createIntervalTask(cron, {
days: data.info.frequency,
runImmediately: true,
});
}
// 运行所有订阅
const subs = await subscriptionService.list();
for (const sub of subs) {
await subscriptionService.handleTask(
sub,
!sub.is_disabled,
true,
!sub.is_disabled,
);
}
};
+21
View File
@@ -0,0 +1,21 @@
import { Application } from 'express';
import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing';
import Logger from './logger';
export default ({ expressApp }: { expressApp: Application }) => {
Sentry.init({
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
new Tracing.Integrations.Express({ app: expressApp }),
],
tracesSampleRate: 1.0,
});
expressApp.use(Sentry.Handlers.requestHandler());
expressApp.use(Sentry.Handlers.tracingHandler());
Logger.info('✌️ Sentry loaded');
};
+16
View File
@@ -0,0 +1,16 @@
import { Server } from 'http';
import Logger from './logger';
import Sock from './sock';
export default async ({ server }: { server: Server }) => {
await Sock({ server });
Logger.info('✌️ Sock loaded');
process.on('SIGINT', () => {
Logger.info('✌️ Server need close');
server.close(() => {
Logger.info('✌️ Server closed');
process.exit(0);
});
});
};
+46
View File
@@ -0,0 +1,46 @@
import sockJs from 'sockjs';
import { Server } from 'http';
import Logger from './logger';
import { Container } from 'typedi';
import SockService from '../services/sock';
import config from '../config/index';
import fs from 'fs';
import { getPlatform } from '../config/util';
export default async ({ server }: { server: Server }) => {
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
const sockService = Container.get(SockService);
echo.on('connection', (conn) => {
if (!conn.headers || !conn.url || !conn.pathname) {
conn.close('404');
}
const data = fs.readFileSync(config.authConfigFile, 'utf8');
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
if (data) {
const { token = '', tokens = {} } = JSON.parse(data);
if (headerToken === token || tokens[platform] === headerToken) {
conn.write(JSON.stringify({ type: 'ping', message: 'hanhh' }));
sockService.addClient(conn);
conn.on('data', (message) => {
conn.write(message);
});
conn.on('close', function () {
sockService.removeClient(conn);
});
return;
} else {
conn.write(JSON.stringify({ type: 'ping', message: 'whyour' }));
}
}
conn.close('404');
});
echo.installHandlers(server);
};
+30
View File
@@ -0,0 +1,30 @@
import express from 'express';
import { exec } from 'child_process';
import Logger from './loaders/logger';
import config from './config';
const app = express();
app.get('/api/public/panel/log', (req, res) => {
exec(
'pm2 logs panel --lines 500 --nostream --timestamp',
(err, stdout, stderr) => {
if (err || stderr) {
return res.send({ code: 400, message: (err && err.message) || stderr });
}
return res.send({ code: 200, data: stdout });
},
);
});
app
.listen(config.publicPort, async () => {
await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default();
Logger.debug(`✌️ Back server launched on port ${config.publicPort}`);
})
.on('error', (err) => {
Logger.error(err);
process.exit(1);
});
+13 -18
View File
@@ -2,30 +2,18 @@ import schedule from 'node-schedule';
import express from 'express';
import { exec } from 'child_process';
import Logger from './loaders/logger';
import { Container } from 'typedi';
import CronService from './services/cron';
import { CrontabStatus } from './data/cron';
import { CrontabModel, CrontabStatus } from './data/cron';
import config from './config';
const app = express();
const run = async () => {
const cronService = Container.get(CronService);
const cronDb = cronService.getDb();
cronDb
.find({})
.sort({ created: 1 })
.exec((err, docs) => {
if (err) {
Logger.error(err);
process.exit(1);
}
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(' ');
const _schedule = task.schedule && task.schedule.split(/ +/);
if (
_schedule &&
_schedule.length > 5 &&
@@ -42,12 +30,19 @@ const run = async () => {
}
}
}
})
.catch((err) => {
Logger.error(err);
process.exit(1);
});
};
app
.listen(config.cronPort, () => {
run();
.listen(config.cronPort, async () => {
await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default();
await run();
Logger.info(`
################################################
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
-333
View File
@@ -1,333 +0,0 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import fetch from 'node-fetch';
import { getFileContentByName } from '../config/util';
import config from '../config';
import * as fs from 'fs';
import got from 'got';
import DataStore from 'nedb';
import { Cookie, CookieStatus, initCookiePosition } from '../data/cookie';
@Service()
export default class CookieService {
private cronDb = new DataStore({ filename: config.cookieDbFile });
constructor(@Inject('logger') private logger: winston.Logger) {
this.cronDb.loadDatabase((err) => {
if (err) throw err;
});
}
public async getCookies() {
const content = getFileContentByName(config.cookieFile);
return this.formatCookie(content.split('\n').filter((x) => !!x));
}
public async addCookie(cookies: string[]) {
let content = getFileContentByName(config.cookieFile);
const originCookies = content.split('\n').filter((x) => !!x);
const result = originCookies.concat(cookies);
fs.writeFileSync(config.cookieFile, result.join('\n'));
return '';
}
public async updateCookie({ cookie, oldCookie }) {
let content = getFileContentByName(config.cookieFile);
const cookies = content.split('\n');
const index = cookies.findIndex((x) => x === oldCookie);
if (index !== -1) {
cookies[index] = cookie;
fs.writeFileSync(config.cookieFile, cookies.join('\n'));
return '';
} else {
return '未找到要原有Cookie';
}
}
public async deleteCookie(cookie: string) {
let content = getFileContentByName(config.cookieFile);
const cookies = content.split('\n');
const index = cookies.findIndex((x) => x === cookie);
if (index !== -1) {
cookies.splice(index, 1);
fs.writeFileSync(config.cookieFile, cookies.join('\n'));
return '';
} else {
return '未找到要删除的Cookie';
}
}
private async formatCookie(data: any[]) {
const result = [];
for (const x of data) {
const { nickname, status } = await this.getJdInfo(x);
if (/pt_pin=(.+?);/.test(x)) {
result.push({
pin: x.match(/pt_pin=(.+?);/)[1],
cookie: x,
status,
nickname: nickname,
});
} else {
result.push({
pin: 'pin未匹配到',
cookie: x,
status,
nickname: nickname,
});
}
}
return result;
}
public async refreshCookie(_id: string) {
const current = await this.get(_id);
const { status, nickname } = await this.getJdInfo(current.value);
return {
...current,
status,
nickname,
};
}
private getJdInfo(cookie: string) {
return fetch(
`https://me-api.jd.com/user_new/info/GetJDUserInfoUnion?orgFlag=JD_PinGou_New&callSource=mainorder&channel=4&isHomewhite=0&sceneval=2&_=${Date.now()}&sceneval=2&g_login_type=1&g_ty=ls`,
{
method: 'get',
headers: {
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-cn',
Connection: 'keep-alive',
Cookie: cookie,
Referer: 'https://home.m.jd.com/myJd/newhome.action',
'User-Agent':
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1',
Host: 'me-api.jd.com',
},
},
)
.then((x) => x.json())
.then((x) => {
if (x.retcode === '0' && x.data && x.data.userInfo) {
return {
nickname: x.data.userInfo.baseInfo.nickname,
status: CookieStatus.normal,
};
} else if (x.retcode === 13) {
return { status: CookieStatus.invalid, nickname: '-' };
}
return { status: CookieStatus.abnormal, nickname: '-' };
});
}
private async formatCookies(cookies: Cookie[]) {
const result = [];
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i];
if (cookie.status !== CookieStatus.disabled) {
const { status, nickname } = await this.getJdInfo(cookie.value);
result.push({ ...cookie, status, nickname });
} else {
result.push({ ...cookie, nickname: '-' });
}
}
return result;
}
public async create(payload: string[]): Promise<Cookie[]> {
const cookies = await this.cookies();
let position = initCookiePosition;
if (cookies && cookies.length > 0) {
position = cookies[cookies.length - 1].position;
}
const tabs = payload.map((x) => {
const cookie = new Cookie({ value: x, position });
position = position / 2;
cookie.position = position;
return cookie;
});
const docs = await this.insert(tabs);
await this.set_cookies();
return await this.formatCookies(docs);
}
public async insert(payload: Cookie[]): Promise<Cookie[]> {
return new Promise((resolve) => {
this.cronDb.insert(payload, (err, docs) => {
if (err) {
this.logger.error(err);
} else {
resolve(docs);
}
});
});
}
public async update(payload: Cookie): Promise<Cookie> {
const { _id, ...other } = payload;
const doc = await this.get(_id);
const tab = new Cookie({ ...doc, ...other });
const newDoc = await this.updateDb(tab);
await this.set_cookies();
const [newCookie] = await this.formatCookies([newDoc]);
return newCookie;
}
private async updateDb(payload: Cookie): Promise<Cookie> {
return new Promise((resolve) => {
this.cronDb.update(
{ _id: payload._id },
payload,
{ returnUpdatedDocs: true },
(err, num, doc) => {
if (err) {
this.logger.error(err);
} else {
resolve(doc as Cookie);
}
},
);
});
}
public async remove(ids: string[]) {
return new Promise((resolve: any) => {
this.cronDb.remove(
{ _id: { $in: ids } },
{ multi: true },
async (err) => {
await this.set_cookies();
resolve();
},
);
});
}
public async move(
_id: string,
{
fromIndex,
toIndex,
}: {
fromIndex: number;
toIndex: number;
},
) {
let targetPosition: number;
const isUpward = fromIndex > toIndex;
const cookies = await this.cookies();
if (toIndex === 0 || toIndex === cookies.length - 1) {
targetPosition = isUpward
? cookies[0].position * 2
: cookies[toIndex].position / 2;
} else {
targetPosition = isUpward
? (cookies[toIndex].position + cookies[toIndex - 1].position) / 2
: (cookies[toIndex].position + cookies[toIndex + 1].position) / 2;
}
this.update({
_id,
position: targetPosition,
});
await this.set_cookies();
}
public async cookies(
searchText?: string,
sort: any = { position: -1 },
needDetail: boolean = false,
): Promise<Cookie[]> {
let query = {};
if (searchText) {
const reg = new RegExp(searchText);
query = {
$or: [
{
name: reg,
},
{
command: reg,
},
],
};
}
const newDocs = await this.find(query, sort);
if (needDetail) {
return await this.formatCookies(newDocs);
} else {
return newDocs;
}
}
private async find(query: any, sort: any): Promise<Cookie[]> {
return new Promise((resolve) => {
this.cronDb
.find(query)
.sort({ ...sort })
.exec((err, docs) => {
resolve(docs);
});
});
}
public async get(_id: string): Promise<Cookie> {
return new Promise((resolve) => {
this.cronDb.find({ _id }).exec((err, docs) => {
resolve(docs[0]);
});
});
}
public async getBySort(sort: any): Promise<Cookie> {
return new Promise((resolve) => {
this.cronDb
.find({})
.sort({ ...sort })
.limit(1)
.exec((err, docs) => {
resolve(docs[0]);
});
});
}
public async disabled(ids: string[]) {
return new Promise((resolve: any) => {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { status: CookieStatus.disabled } },
{ multi: true },
async (err) => {
await this.set_cookies();
resolve();
},
);
});
}
public async enabled(ids: string[]) {
return new Promise((resolve: any) => {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { status: CookieStatus.noacquired } },
{ multi: true },
async (err, num) => {
await this.set_cookies();
resolve();
},
);
});
}
public async set_cookies() {
const cookies = await this.cookies();
let cookie_string = '';
cookies.forEach((tab) => {
if (tab.status !== CookieStatus.disabled) {
cookie_string += tab.value;
cookie_string += '\n';
}
});
fs.writeFileSync(config.cookieFile, cookie_string);
}
}
+487 -216
View File
@@ -1,35 +1,24 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import DataStore from 'nedb';
import config from '../config';
import { Crontab, CrontabStatus } from '../data/cron';
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import { exec, execSync, spawn } from 'child_process';
import fs from 'fs';
import cron_parser from 'cron-parser';
import { getFileContentByName } from '../config/util';
import PQueue from 'p-queue';
import { getFileContentByName, concurrentRun, fileExist } from '../config/util';
import { promises, existsSync } from 'fs';
import { promisify } from 'util';
import { Op } from 'sequelize';
import path from 'path';
import dayjs from 'dayjs';
@Service()
export default class CronService {
private cronDb = new DataStore({ filename: config.cronDbFile });
private queue = new PQueue({
concurrency: parseInt(process.env.MaxConcurrentNum) || 5,
});
constructor(@Inject('logger') private logger: winston.Logger) {
this.cronDb.loadDatabase((err) => {
if (err) throw err;
});
}
public getDb(): DataStore {
return this.cronDb;
}
constructor(@Inject('logger') private logger: winston.Logger) {}
private isSixCron(cron: Crontab) {
const { schedule } = cron;
if (schedule.split(' ').length === 6) {
if (schedule?.split(/ +/).length === 6) {
return true;
}
return false;
@@ -37,7 +26,6 @@ export default class CronService {
public async create(payload: Crontab): Promise<Crontab> {
const tab = new Crontab(payload);
tab.created = new Date().valueOf();
tab.saved = false;
const doc = await this.insert(tab);
await this.set_crontab(this.isSixCron(doc));
@@ -45,251 +33,540 @@ export default class CronService {
}
public async insert(payload: Crontab): Promise<Crontab> {
return new Promise((resolve) => {
this.cronDb.insert(payload, (err, docs) => {
if (err) {
this.logger.error(err);
} else {
resolve(docs);
}
});
});
return await CrontabModel.create(payload, { returning: true });
}
public async update(payload: Crontab): Promise<Crontab> {
const { _id, ...other } = payload;
const doc = await this.get(_id);
const tab = new Crontab({ ...doc, ...other });
tab.saved = false;
const newDoc = await this.updateDb(tab);
payload.saved = false;
const newDoc = await this.updateDb(payload);
await this.set_crontab(this.isSixCron(newDoc));
return newDoc;
}
public async updateDb(payload: Crontab): Promise<Crontab> {
return new Promise((resolve) => {
this.cronDb.update(
{ _id: payload._id },
payload,
{ returnUpdatedDocs: true },
(err, num, docs: any) => {
if (err) {
this.logger.error(err);
} else {
resolve(docs);
}
},
);
});
await CrontabModel.update(payload, { where: { id: payload.id } });
return await this.getDb({ id: payload.id });
}
public async status({
ids,
status,
pid,
log_path,
last_running_time = 0,
last_execution_time = 0,
}: {
ids: string[];
ids: number[];
status: CrontabStatus;
pid: number;
log_path: string;
last_running_time: number;
last_execution_time: number;
}) {
this.cronDb.update({ _id: { $in: ids } }, { $set: { status } });
const options: any = {
status,
pid,
log_path,
last_execution_time,
};
if (last_running_time > 0) {
options.last_running_time = last_running_time;
}
return await CrontabModel.update({ ...options }, { where: { id: ids } });
}
public async remove(ids: string[]) {
return new Promise((resolve: any) => {
this.cronDb.remove(
{ _id: { $in: ids } },
{ multi: true },
async (err) => {
await this.set_crontab(true);
resolve();
public async remove(ids: number[]) {
await CrontabModel.destroy({ where: { id: ids } });
await this.set_crontab(true);
}
public async pin(ids: number[]) {
await CrontabModel.update({ isPinned: 1 }, { where: { id: ids } });
}
public async unPin(ids: number[]) {
await CrontabModel.update({ isPinned: 0 }, { where: { id: ids } });
}
public async addLabels(ids: string[], labels: string[]) {
const docs = await CrontabModel.findAll({ where: { id: ids } });
for (const doc of docs) {
await CrontabModel.update(
{
labels: Array.from(new Set((doc.labels || []).concat(labels))),
},
{ where: { id: doc.id } },
);
});
}
}
public async crontabs(searchText?: string): Promise<Crontab[]> {
let query = {};
public async removeLabels(ids: string[], labels: string[]) {
const docs = await CrontabModel.findAll({ where: { id: ids } });
for (const doc of docs) {
await CrontabModel.update(
{
labels: (doc.labels || []).filter((label) => !labels.includes(label)),
},
{ where: { id: doc.id } },
);
}
}
private formatViewQuery(query: any, viewQuery: any) {
if (viewQuery.filters && viewQuery.filters.length > 0) {
if (!query[Op.and]) {
query[Op.and] = [];
}
for (const col of viewQuery.filters) {
const { property, value, operation } = col;
let q: any = {};
let operate2 = null;
let operate = null;
switch (operation) {
case 'Reg':
operate = Op.like;
operate2 = Op.or;
break;
case 'NotReg':
operate = Op.notLike;
operate2 = Op.and;
break;
case 'In':
q[Op.or] = [
{
[property]: value,
},
property === 'status' && value.includes(2)
? { isDisabled: 1 }
: {},
];
break;
case 'Nin':
q[Op.and] = [
{
[property]: {
[Op.notIn]: value,
},
},
property === 'status' && value.includes(2)
? { isDisabled: { [Op.ne]: 1 } }
: {},
];
break;
default:
break;
}
if (operate && operate2) {
q[property] = {
[operate2]: [
{ [operate]: `%${value}%` },
{ [operate]: `%${encodeURIComponent(value)}%` },
],
};
}
query[Op.and].push(q);
}
}
}
private formatSearchText(query: any, searchText: string | undefined) {
if (searchText) {
const reg = new RegExp(searchText, 'i');
query = {
$or: [
{
name: reg,
},
{
command: reg,
},
{
schedule: reg,
},
],
};
if (!query[Op.and]) {
query[Op.and] = [];
}
let q: any = {};
const textArray = searchText.split(':');
switch (textArray[0]) {
case 'name':
case 'command':
case 'schedule':
case 'label':
const column = textArray[0] === 'label' ? 'labels' : textArray[0];
q[column] = {
[Op.or]: [
{ [Op.like]: `%${textArray[1]}%` },
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
],
};
break;
default:
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchText}%` },
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
],
};
q[Op.or] = [
{
name: reg,
},
{
command: reg,
},
{
schedule: reg,
},
{
labels: reg,
},
];
break;
}
query[Op.and].push(q);
}
return new Promise((resolve) => {
this.cronDb
.find(query)
.sort({ created: -1 })
.exec((err, docs) => {
resolve(docs);
});
});
}
public async get(_id: string): Promise<Crontab> {
return new Promise((resolve) => {
this.cronDb.find({ _id }).exec((err, docs) => {
resolve(docs[0]);
});
});
private formatFilterQuery(query: any, filterQuery: any) {
if (filterQuery) {
if (!query[Op.and]) {
query[Op.and] = [];
}
const filterKeys: any = Object.keys(filterQuery);
for (const key of filterKeys) {
let q: any = {};
if (filterKeys[key]) {
q[key] = filterKeys[key];
}
query[Op.and].push(q);
}
}
}
public async run(ids: string[]) {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { status: CrontabStatus.queued } },
{ multi: true },
private formatViewSort(order: string[][], viewQuery: any) {
if (viewQuery.sorts && viewQuery.sorts.length > 0) {
for (const { property, type } of viewQuery.sorts) {
order.unshift([property, type]);
}
}
}
public async crontabs(params?: {
searchValue: string;
page: string;
size: string;
sorter: string;
filters: string;
queryString: string;
}): Promise<{ data: Crontab[]; total: number }> {
const searchText = params?.searchValue;
const page = Number(params?.page || '0');
const size = Number(params?.size || '0');
const viewQuery = JSON.parse(params?.queryString || '{}');
const filterQuery = JSON.parse(params?.filters || '{}');
const sorterQuery = JSON.parse(params?.sorter || '{}');
let query: any = {};
let order = [
['isPinned', 'DESC'],
['isDisabled', 'ASC'],
['status', 'ASC'],
['createdAt', 'DESC'],
];
this.formatViewQuery(query, viewQuery);
this.formatSearchText(query, searchText);
this.formatFilterQuery(query, filterQuery);
this.formatViewSort(order, viewQuery);
if (sorterQuery) {
const { field, type } = sorterQuery;
if (field && type) {
order.unshift([field, type]);
}
}
let condition: any = {
where: query,
order: order,
};
if (page && size) {
condition.offset = (page - 1) * size;
condition.limit = size;
}
try {
const result = await CrontabModel.findAll(condition);
const count = await CrontabModel.count({ where: query });
return { data: result, total: count };
} catch (error) {
throw error;
}
}
public async getDb(query: any): Promise<Crontab> {
const doc: any = await CrontabModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Crontab);
}
public async run(ids: number[]) {
await CrontabModel.update(
{ status: CrontabStatus.queued },
{ where: { id: ids } },
);
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
this.queue.add(() => this.runSingle(id));
concurrentRun(
ids.map((id) => async () => await this.runSingle(id)),
10,
);
}
public async stop(ids: number[]) {
const docs = await CrontabModel.findAll({ where: { id: ids } });
for (const doc of docs) {
if (doc.pid) {
try {
process.kill(-doc.pid);
} catch (error) {
this.logger.silly(error);
}
}
const err = await this.killTask(doc.command);
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
const logFileExist = doc.log_path && (await fileExist(absolutePath));
const endTime = dayjs();
const diffTimeStr = doc.last_execution_time
? `,耗时 ${endTime.diff(
dayjs(doc.last_execution_time * 1000),
'second',
)}`
: '';
if (logFileExist) {
const str = err ? `\n${err}` : '';
fs.appendFileSync(
`${absolutePath}`,
`${str}\n## 执行结束... ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)}${diffTimeStr}`,
);
}
}
await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined },
{ where: { id: ids } },
);
}
public async killTask(name: string) {
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
const execAsync = promisify(exec);
try {
let pid = (await execAsync(taskCommand)).stdout;
if (pid) {
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
} else {
return;
}
let pids = pid.match(/\(\d+/g);
const killLogs = [];
if (pids && pids.length > 0) {
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
pids = pids.slice(0, 3);
for (const id of pids) {
const c = `kill -9 ${id.slice(1)}`;
try {
const { stdout, stderr } = await execAsync(c);
if (stderr) {
killLogs.push(stderr);
}
if (stdout) {
killLogs.push(stdout);
}
} catch (error: any) {
killLogs.push(error.message);
}
}
}
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
} catch (e) {
return JSON.stringify(e);
}
}
public async stop(ids: string[]) {
return new Promise((resolve: any) => {
this.cronDb.find({ _id: { $in: ids } }).exec((err, docs: Crontab[]) => {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
);
const pids = docs
.map((x) => x.pid)
.filter((x) => !!x)
.join('\n');
exec(`echo - e "${pids}" | xargs kill - 9`, (err) => {
resolve();
});
});
});
}
private async runSingle(id: string): Promise<number> {
private async runSingle(cronId: number): Promise<number> {
return new Promise(async (resolve: any) => {
const cron = await this.get(id);
const cron = await this.getDb({ id: cronId });
if (cron.status !== CrontabStatus.queued) {
resolve();
return;
}
let { _id, command } = cron;
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('ID: ' + id);
this.logger.silly('Original command: ' + command);
let logFile = `${config.manualLogPath}${_id}.log`;
fs.writeFileSync(
logFile,
`开始执行... ${new Date().toLocaleString()}\n\n`,
);
let cmdStr = command;
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
cmdStr = `task ${cmdStr}`;
}
if (cmdStr.endsWith('.js')) {
if (
cmdStr.endsWith('.js') ||
cmdStr.endsWith('.py') ||
cmdStr.endsWith('.pyc') ||
cmdStr.endsWith('.sh') ||
cmdStr.endsWith('.ts')
) {
cmdStr = `${cmdStr} now`;
}
const cmd = spawn(cmdStr, { shell: true });
this.cronDb.update(
{ _id },
{ $set: { status: CrontabStatus.running, pid: cmd.pid } },
const cp = spawn(cmdStr, { shell: '/bin/bash' });
await CrontabModel.update(
{ status: CrontabStatus.running, pid: cp.pid },
{ where: { id } },
);
cmd.stdout.on('data', (data) => {
this.logger.silly(`stdout: ${data}`);
fs.appendFileSync(logFile, data);
cp.stderr.on('data', (data) => {
if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${data}`);
}
});
cp.on('error', (err) => {
if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
}
});
cmd.stderr.on('data', (data) => {
this.logger.silly(`stderr: ${data}`);
fs.appendFileSync(logFile, data);
});
cmd.on('close', (code) => {
this.logger.silly(`child process exited with code ${code}`);
this.cronDb.update(
{ _id },
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
cp.on('exit', async (code, signal) => {
this.logger.info(
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
);
});
cmd.on('error', (err) => {
this.logger.info(err);
fs.appendFileSync(logFile, err.stack);
});
cmd.on('exit', (code: number, signal: any) => {
this.logger.silly(`cmd exit ${code}`);
this.cronDb.update(
{ _id },
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
cp.on('close', async (code) => {
await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined },
{ where: { id } },
);
fs.appendFileSync(logFile, `\n执行结束...`);
resolve();
});
process.on('SIGINT', function () {
fs.appendFileSync(logFile, `\n执行结束...`);
resolve();
});
});
}
public async disabled(ids: string[]) {
return new Promise((resolve: any) => {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { isDisabled: 1 } },
{ multi: true },
async (err) => {
await this.set_crontab(true);
resolve();
},
);
});
public async disabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
await this.set_crontab(true);
}
public async enabled(ids: string[]) {
return new Promise((resolve: any) => {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { isDisabled: 0 } },
{ multi: true },
async (err) => {
await this.set_crontab(true);
resolve();
},
);
});
public async enabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
await this.set_crontab(true);
}
public async log(_id: string) {
let logFile = `${config.manualLogPath}${_id}.log`;
return getFileContentByName(logFile);
public async log(id: number) {
const doc = await this.getDb({ id });
if (!doc) {
return '';
}
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
const logFileExist = doc.log_path && (await fileExist(absolutePath));
if (logFileExist) {
return getFileContentByName(`${absolutePath}`);
}
const [, commandStr, url] = doc.command.split(/ +/);
let logPath = this.getKey(commandStr);
const isQlCommand = doc.command.startsWith('ql ');
const key =
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
logPath;
if (isQlCommand) {
logPath = 'update';
}
let logDir = `${config.logPath}${logPath}`;
if (existsSync(logDir)) {
let files = await promises.readdir(logDir);
if (isQlCommand) {
files = files.filter((x) => x.includes(key));
}
return getFileContentByName(`${logDir}/${files[files.length - 1]}`);
} else {
return '';
}
}
public async logs(id: number) {
const doc = await this.getDb({ id });
if (!doc) {
return [];
}
if (doc.log_path) {
const relativeDir = path.dirname(`${doc.log_path}`);
const dir = path.resolve(config.logPath, relativeDir);
if (existsSync(dir)) {
let files = await promises.readdir(dir);
return files
.map((x) => ({
filename: x,
directory: relativeDir.replace(config.logPath, ''),
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time);
}
}
const [, commandStr, url] = doc.command.split(/ +/);
let logPath = this.getKey(commandStr);
const isQlCommand = doc.command.startsWith('ql ');
const key =
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
logPath;
if (isQlCommand) {
logPath = 'update';
}
let logDir = `${config.logPath}${logPath}`;
if (existsSync(logDir)) {
let files = await promises.readdir(logDir);
if (isQlCommand) {
files = files.filter((x) => x.includes(key));
}
return files
.map((x) => ({
filename: x,
directory: logPath,
time: fs.statSync(`${logDir}/${x}`).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time);
} else {
return [];
}
}
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) {
const crontab_job_string = `ID=${tab._id} ${tab.command}`;
const crontab_job_string = `ID=${tab.id} ${tab.command}`;
return crontab_job_string;
}
private async set_crontab(needReloadSchedule: boolean = false) {
const tabs = await this.crontabs();
var crontab_string = '';
tabs.forEach((tab) => {
const _schedule = tab.schedule && tab.schedule.split(' ');
if (tab.isDisabled === 1 || _schedule.length !== 5) {
tabs.data.forEach((tab) => {
const _schedule = tab.schedule && tab.schedule.split(/ +/);
if (tab.isDisabled === 1 || _schedule!.length !== 5) {
crontab_string += '# ';
crontab_string += tab.schedule;
crontab_string += ' ';
@@ -310,44 +587,38 @@ export default class CronService {
if (needReloadSchedule) {
exec(`pm2 reload schedule`);
}
this.cronDb.update({}, { $set: { saved: true } }, { multi: true });
}
private reload_db() {
this.cronDb.loadDatabase();
await CrontabModel.update({ saved: true }, { where: {} });
}
public import_crontab() {
exec('crontab -l', (error, stdout, stderr) => {
var lines = stdout.split('\n');
var namePrefix = new Date().getTime();
const lines = stdout.split('\n');
const namePrefix = new Date().getTime();
lines.reverse().forEach((line, index) => {
lines.reverse().forEach(async (line, index) => {
line = line.replace(/\t+/g, ' ');
var regex =
const regex =
/^((\@[a-zA-Z]+\s+)|(([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+))/;
var command = line.replace(regex, '').trim();
var schedule = line.replace(command, '').trim();
const command = line.replace(regex, '').trim();
const schedule = line.replace(command, '').trim();
if (
command &&
schedule &&
cron_parser.parseExpression(schedule).hasNext()
) {
var name = namePrefix + '_' + index;
const name = namePrefix + '_' + index;
this.cronDb.findOne({ command, schedule }, (err, doc) => {
if (err) {
throw err;
}
if (!doc) {
this.create({ name, command, schedule });
} else {
doc.command = command;
doc.schedule = schedule;
this.update(doc);
}
const _crontab = await CrontabModel.findOne({
where: { command, schedule },
});
if (!_crontab) {
await this.create({ name, command, schedule });
} else {
_crontab.command = command;
_crontab.schedule = schedule;
await this.update(_crontab);
}
}
});
});
+92
View File
@@ -0,0 +1,92 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { CrontabView, CrontabViewModel } from '../data/cronView';
import { initEnvPosition } from '../data/env';
@Service()
export default class CronViewService {
constructor(@Inject('logger') private logger: winston.Logger) {}
public async create(payload: CrontabView): Promise<CrontabView> {
let position = initEnvPosition;
const views = await this.list();
if (views && views.length > 0 && views[views.length - 1].position) {
position = views[views.length - 1].position as number;
}
position = position / 2;
const tab = new CrontabView({ ...payload, position });
const doc = await this.insert(tab);
return doc;
}
public async insert(payload: CrontabView): Promise<CrontabView> {
return await CrontabViewModel.create(payload, { returning: true });
}
public async update(payload: CrontabView): Promise<CrontabView> {
const newDoc = await this.updateDb(payload);
return newDoc;
}
public async updateDb(payload: CrontabView): Promise<CrontabView> {
await CrontabViewModel.update(payload, { where: { id: payload.id } });
return await this.getDb({ id: payload.id });
}
public async remove(ids: number[]) {
await CrontabViewModel.destroy({ where: { id: ids } });
}
public async list(): Promise<CrontabView[]> {
try {
const result = await CrontabViewModel.findAll({
where: {},
order: [['position', 'DESC']],
});
return result;
} catch (error) {
throw error;
}
}
public async getDb(query: any): Promise<CrontabView> {
const doc: any = await CrontabViewModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as CrontabView);
}
public async disabled(ids: number[]) {
await CrontabViewModel.update({ isDisabled: 1 }, { where: { id: ids } });
}
public async enabled(ids: number[]) {
await CrontabViewModel.update({ isDisabled: 0 }, { where: { id: ids } });
}
public async move({
id,
fromIndex,
toIndex,
}: {
fromIndex: number;
toIndex: number;
id: number;
}): Promise<CrontabView> {
let targetPosition: number;
const isUpward = fromIndex > toIndex;
const views = await this.list();
if (toIndex === 0 || toIndex === views.length - 1) {
targetPosition = isUpward
? views[0].position * 2
: views[toIndex].position / 2;
} else {
targetPosition = isUpward
? (views[toIndex].position + views[toIndex - 1].position) / 2
: (views[toIndex].position + views[toIndex + 1].position) / 2;
}
const newDoc = await this.update({
id,
position: targetPosition,
});
return newDoc;
}
}
+243
View File
@@ -0,0 +1,243 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import config from '../config';
import {
Dependence,
InstallDependenceCommandTypes,
DependenceStatus,
DependenceTypes,
unInstallDependenceCommandTypes,
DependenceModel,
} from '../data/dependence';
import _ from 'lodash';
import { spawn } from 'child_process';
import SockService from './sock';
import { Op } from 'sequelize';
import { concurrentRun } from '../config/util';
import dayjs from 'dayjs';
@Service()
export default class DependenceService {
constructor(
@Inject('logger') private logger: winston.Logger,
private sockService: SockService,
) {}
public async create(payloads: Dependence[]): Promise<Dependence[]> {
const tabs = payloads.map((x) => {
const tab = new Dependence({ ...x, status: DependenceStatus.installing });
return tab;
});
const docs = await this.insert(tabs);
this.installDependenceOneByOne(docs);
return docs;
}
public async insert(payloads: Dependence[]): Promise<Dependence[]> {
const docs = await DependenceModel.bulkCreate(payloads);
return docs;
}
public async update(
payload: Dependence & { id: string },
): Promise<Dependence> {
const { id, ...other } = payload;
const doc = await this.getDb({ id });
const tab = new Dependence({
...doc,
...other,
status: DependenceStatus.installing,
});
const newDoc = await this.updateDb(tab);
this.installDependenceOneByOne([newDoc]);
return newDoc;
}
private async updateDb(payload: Dependence): Promise<Dependence> {
await DependenceModel.update(payload, { where: { id: payload.id } });
return await this.getDb({ id: payload.id });
}
public async remove(ids: number[], force = false): Promise<Dependence[]> {
await DependenceModel.update(
{ status: DependenceStatus.removing, log: [] },
{ where: { id: ids } },
);
const docs = await DependenceModel.findAll({ where: { id: ids } });
this.installDependenceOneByOne(docs, false, force);
return docs;
}
public async removeDb(ids: number[]) {
await DependenceModel.destroy({ where: { id: ids } });
}
public async dependencies(
{ searchValue, type }: { searchValue: string; type: string },
sort: any = { position: -1 },
query: any = {},
): Promise<Dependence[]> {
let condition = { ...query, type: DependenceTypes[type as any] };
if (searchValue) {
const encodeText = encodeURIComponent(searchValue);
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchValue}%` },
{ [Op.like]: `%${encodeText}%` },
],
};
condition = {
...condition,
name: reg,
};
}
try {
const result = await this.find(condition);
return result as any;
} catch (error) {
throw error;
}
}
private installDependenceOneByOne(
docs: Dependence[],
isInstall: boolean = true,
force: boolean = false,
) {
concurrentRun(
docs.map(
(dep) => async () =>
await this.installOrUninstallDependencies([dep], isInstall, force),
),
1,
);
}
public async reInstall(ids: number[]): Promise<Dependence[]> {
await DependenceModel.update(
{ status: DependenceStatus.installing, log: [] },
{ where: { id: ids } },
);
const docs = await DependenceModel.findAll({ where: { id: ids } });
this.installDependenceOneByOne(docs);
return docs;
}
private async find(query: any, sort?: any): Promise<Dependence[]> {
const docs = await DependenceModel.findAll({ where: { ...query } });
return docs;
}
public async getDb(query: any): Promise<Dependence> {
const doc: any = await DependenceModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Dependence);
}
private async updateLog(ids: number[], log: string): Promise<void> {
const doc = await DependenceModel.findOne({ where: { id: ids } });
const newLog = doc?.log ? [...doc.log, log] : [log];
await DependenceModel.update({ log: newLog }, { where: { id: ids } });
}
public installOrUninstallDependencies(
dependencies: Dependence[],
isInstall: boolean = true,
force: boolean = false,
) {
return new Promise(async (resolve) => {
if (dependencies.length === 0) {
resolve(null);
return;
}
const socketMessageType = !force
? 'installDependence'
: 'uninstallDependence';
const depNames = dependencies.map((x) => x.name).join(' ');
const depRunCommand = (
isInstall
? InstallDependenceCommandTypes
: unInstallDependenceCommandTypes
)[dependencies[0].type as any];
const actionText = isInstall ? '安装' : '删除';
const depIds = dependencies.map((x) => x.id) as number[];
const startTime = dayjs();
const message = `开始${actionText}依赖 ${depNames},开始时间 ${startTime.format(
'YYYY-MM-DD HH:mm:ss',
)}\n\n`;
this.sockService.sendMessage({
type: socketMessageType,
message,
references: depIds,
});
await this.updateLog(depIds, message);
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
cp.stdout.on('data', async (data) => {
this.sockService.sendMessage({
type: socketMessageType,
message: data.toString(),
references: depIds,
});
await this.updateLog(depIds, data.toString());
});
cp.stderr.on('data', async (data) => {
this.sockService.sendMessage({
type: socketMessageType,
message: data.toString(),
references: depIds,
});
await this.updateLog(depIds, data.toString());
});
cp.on('error', async (err) => {
this.sockService.sendMessage({
type: socketMessageType,
message: JSON.stringify(err),
references: depIds,
});
await this.updateLog(depIds, JSON.stringify(err));
resolve(null);
});
cp.on('close', async (code) => {
const endTime = dayjs();
const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败';
const message = `\n依赖${actionText}${resultText},结束时间 ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)},耗时 ${endTime.diff(startTime, 'second')}`;
this.sockService.sendMessage({
type: socketMessageType,
message,
references: depIds,
});
await 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);
});
});
}
}
+181
View File
@@ -0,0 +1,181 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { getFileContentByName } from '../config/util';
import config from '../config';
import * as fs from 'fs';
import { Env, EnvModel, EnvStatus, initEnvPosition } from '../data/env';
import _ from 'lodash';
import { Op } from 'sequelize';
@Service()
export default class EnvService {
constructor(@Inject('logger') private logger: winston.Logger) {}
public async create(payloads: Env[]): Promise<Env[]> {
const envs = await this.envs();
let position = initEnvPosition;
if (envs && envs.length > 0 && envs[envs.length - 1].position) {
position = envs[envs.length - 1].position as number;
}
const tabs = payloads.map((x) => {
position = position / 2;
const tab = new Env({ ...x, position });
return tab;
});
const docs = await this.insert(tabs);
await this.set_envs();
return docs;
}
public async insert(payloads: Env[]): Promise<Env[]> {
const result = [];
for (const env of payloads) {
const doc = await EnvModel.create(env, { returning: true });
result.push(doc);
}
return result;
}
public async update(payload: Env): Promise<Env> {
const newDoc = await this.updateDb(payload);
await this.set_envs();
return newDoc;
}
private async updateDb(payload: Env): Promise<Env> {
await EnvModel.update({ ...payload }, { where: { id: payload.id } });
return await this.getDb({ id: payload.id });
}
public async remove(ids: string[]) {
await EnvModel.destroy({ where: { id: ids } });
await this.set_envs();
}
public async move(
id: number,
{
fromIndex,
toIndex,
}: {
fromIndex: number;
toIndex: number;
},
): Promise<Env> {
let targetPosition: number;
const isUpward = fromIndex > toIndex;
const envs = await this.envs();
if (toIndex === 0 || toIndex === envs.length - 1) {
targetPosition = isUpward
? envs[0].position * 2
: envs[toIndex].position / 2;
} else {
targetPosition = isUpward
? (envs[toIndex].position + envs[toIndex - 1].position) / 2
: (envs[toIndex].position + envs[toIndex + 1].position) / 2;
}
const newDoc = await this.update({
id,
position: targetPosition,
});
return newDoc;
}
public async envs(
searchText: string = '',
sort: any = { position: -1 },
query: any = {},
): Promise<Env[]> {
let condition = { ...query };
if (searchText) {
const encodeText = encodeURIComponent(searchText);
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchText}%` },
{ [Op.like]: `%${encodeText}%` },
],
};
condition = {
...condition,
[Op.or]: [
{
name: reg,
},
{
value: reg,
},
{
remarks: reg,
},
],
};
}
try {
const result = await this.find(condition, [['position', 'DESC']]);
return result as any;
} catch (error) {
throw error;
}
}
private async find(query: any, sort: any = []): Promise<Env[]> {
const docs = await EnvModel.findAll({
where: { ...query },
order: [...sort],
});
return docs;
}
public async getDb(query: any): Promise<Env> {
const doc: any = await EnvModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Env);
}
public async disabled(ids: string[]) {
await EnvModel.update(
{ status: EnvStatus.disabled },
{ where: { id: ids } },
);
await this.set_envs();
}
public async enabled(ids: string[]) {
await EnvModel.update({ status: EnvStatus.normal }, { where: { id: ids } });
await this.set_envs();
}
public async updateNames({ ids, name }: { ids: string[]; name: string }) {
await EnvModel.update({ name }, { where: { id: ids } });
await this.set_envs();
}
public async set_envs() {
const envs = await this.envs(
'',
{ position: -1 },
{ name: { [Op.not]: null } },
);
const groups = _.groupBy(envs, 'name');
let env_string = '';
for (const key in groups) {
if (Object.prototype.hasOwnProperty.call(groups, key)) {
const group = groups[key];
// 忽略不符合bash要求的环境变量名称
if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) {
let value = _(group)
.filter((x) => x.status !== EnvStatus.disabled)
.map('value')
.join('&')
.replace(/(\\)[^n]/g, '\\\\')
.replace(/(\\$)/, '\\\\')
.replace(/"/g, '\\"')
.trim();
env_string += `export ${key}="${value}"\n`;
}
}
}
fs.writeFileSync(config.envFile, env_string);
}
}
+386
View File
@@ -0,0 +1,386 @@
import { NotificationInfo } from '../data/notify';
import { Service, Inject } from 'typedi';
import winston from 'winston';
import UserService from './user';
import got from 'got';
import nodemailer from 'nodemailer';
import crypto from 'crypto';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
@Service()
export default class NotificationService {
@Inject((type) => UserService)
private userService!: UserService;
private modeMap = new Map([
['gotify', this.gotify],
['goCqHttpBot', this.goCqHttpBot],
['serverChan', this.serverChan],
['pushDeer', this.pushDeer],
['chat', this.chat],
['bark', this.bark],
['telegramBot', this.telegramBot],
['dingtalkBot', this.dingtalkBot],
['weWorkBot', this.weWorkBot],
['weWorkApp', this.weWorkApp],
['iGot', this.iGot],
['pushPlus', this.pushPlus],
['email', this.email],
]);
private timeout = 10000;
private title = '';
private content = '';
private params!: Omit<NotificationInfo, 'type'>;
constructor(@Inject('logger') private logger: winston.Logger) {}
public async notify(
title: string,
content: string,
): Promise<boolean | undefined> {
const { type, ...rest } = await this.userService.getNotificationMode();
if (type) {
this.title = title;
this.content = content;
this.params = rest;
const notificationModeAction = this.modeMap.get(type);
try {
return await notificationModeAction?.call(this);
} catch (error: any) {
return false;
}
}
return false;
}
public async testNotify(
info: NotificationInfo,
title: string,
content: string,
) {
const { type, ...rest } = info;
if (type) {
this.title = title;
this.content = content;
this.params = rest;
const notificationModeAction = this.modeMap.get(type);
return await notificationModeAction?.call(this);
}
return true;
}
private async gotify() {
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
const res: any = await got
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
timeout: this.timeout,
retry: 0,
body: `title=${encodeURIComponent(
this.title,
)}&message=${encodeURIComponent(
this.content,
)}&priority=${gotifyPriority}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
})
.json();
return typeof res.id === 'number';
}
private async goCqHttpBot() {
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
const res: any = await got
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
timeout: this.timeout,
retry: 0,
json: { message: `${this.title}\n${this.content}` },
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
})
.json();
return res.retcode === 0;
}
private async serverChan() {
const { serverChanKey } = this.params;
const url = serverChanKey.startsWith('SCT')
? `https://sctapi.ftqq.com/${serverChanKey}.send`
: `https://sc.ftqq.com/${serverChanKey}.send`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
body: `title=${this.title}&desp=${this.content}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return res.errno === 0 || res.data.errno === 0;
}
private async pushDeer() {
const { pushDeerKey } = this.params;
const url = `https://api2.pushdeer.com/message/push`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
this.title,
)}&desp=${encodeURIComponent(this.content)}&type=markdown`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return (
res.content.result.length !== undefined && res.content.result.length > 0
);
}
private async chat() {
const { chatUrl, chatToken } = this.params;
const url = `${chatUrl}${chatToken}`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
body: `payload={"text":"${this.title}\n${this.content}"}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return res.success;
}
private async bark() {
let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
barkPush = `https://api.day.app/${barkPush}`;
}
const url = `${barkPush}/${encodeURIComponent(
this.title,
)}/${encodeURIComponent(
this.content,
)}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`;
const res: any = await got
.get(url, {
timeout: this.timeout,
retry: 0,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return res.code === 200;
}
private async telegramBot() {
const {
telegramBotApiHost,
telegramBotProxyAuth,
telegramBotProxyHost,
telegramBotProxyPort,
telegramBotToken,
telegramBotUserId,
} = this.params;
const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : '';
const url = `https://${
telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org'
}/bot${telegramBotToken}/sendMessage`;
let agent;
if (telegramBotProxyHost && telegramBotProxyPort) {
const options: any = {
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: 256,
maxFreeSockets: 256,
proxy: `http://${authStr}${telegramBotProxyHost}:${telegramBotProxyPort}`,
};
const httpAgent = new HttpProxyAgent(options);
const httpsAgent = new HttpsProxyAgent(options);
agent = {
http: httpAgent,
https: httpsAgent,
};
}
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
agent,
})
.json();
return !!res.ok;
}
private async dingtalkBot() {
const { dingtalkBotSecret, dingtalkBotToken } = this.params;
let secretParam = '';
if (dingtalkBotSecret) {
const dateNow = Date.now();
const hmac = crypto.createHmac('sha256', dingtalkBotSecret);
hmac.update(`${dateNow}\n${dingtalkBotSecret}`);
const result = encodeURIComponent(hmac.digest('base64'));
secretParam = `&timestamp=${dateNow}&sign=${result}`;
}
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
json: {
msgtype: 'text',
text: {
content: ` ${this.title}\n\n${this.content}`,
},
},
})
.json();
return res.errcode === 0;
}
private async weWorkBot() {
const { weWorkBotKey } = this.params;
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
json: {
msgtype: 'text',
text: {
content: ` ${this.title}\n\n${this.content}`,
},
},
})
.json();
return res.errcode === 0;
}
private async weWorkApp() {
const { weWorkAppKey } = this.params;
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
weWorkAppKey.split(',');
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
const tokenRes: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
json: {
corpid,
corpsecret,
},
})
.json();
let options: any = {
msgtype: 'mpnews',
mpnews: {
articles: [
{
title: `${this.title}`,
thumb_media_id,
author: `智能助手`,
content_source_url: ``,
content: `${this.content.replace(/\n/g, '<br/>')}`,
digest: `${this.content}`,
},
],
},
};
switch (thumb_media_id) {
case '0':
options = {
msgtype: 'textcard',
textcard: {
title: `${this.title}`,
description: `${this.content}`,
url: 'https://github.com/whyour/qinglong',
btntxt: '更多',
},
};
break;
case '1':
options = {
msgtype: 'text',
text: {
content: `${this.title}\n\n${this.content}`,
},
};
break;
}
const res: any = await got
.post(
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
{
timeout: this.timeout,
retry: 0,
json: {
touser,
agentid,
safe: '0',
...options,
},
},
)
.json();
return res.errcode === 0;
}
private async iGot() {
const { iGotPushKey } = this.params;
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
body: `title=${this.title}&content=${this.content}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return res.ret === 0;
}
private async pushPlus() {
const { pushPlusToken, pushPlusUser } = this.params;
const url = `https://www.pushplus.plus/send`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
json: {
token: `${pushPlusToken}`,
title: `${this.title}`,
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
topic: `${pushPlusUser || ''}`,
},
})
.json();
return res.code === 200;
}
private async email() {
const { emailPass, emailService, emailUser } = this.params;
const transporter = nodemailer.createTransport({
service: emailService,
auth: {
user: emailUser,
pass: emailPass,
},
});
const info = await transporter.sendMail({
from: `"青龙快讯" <${emailUser}>`,
to: `${emailUser}`,
subject: `${this.title}`,
html: `${this.content.replace(/\n/g, '<br/>')}`,
});
transporter.close();
return !!info.messageId;
}
}
+170
View File
@@ -0,0 +1,170 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { createRandomString } from '../config/util';
import config from '../config';
import { App, AppModel } from '../data/open';
import { v4 as uuidV4 } from 'uuid';
import sequelize, { Op } from 'sequelize';
@Service()
export default class OpenService {
constructor(@Inject('logger') private logger: winston.Logger) {}
public async findTokenByValue(token: string): Promise<App | null> {
const docs = await this.find({});
const doc = docs.filter((x) => x.tokens?.find((y) => y.value === token));
return doc[0];
}
public async create(payload: App): Promise<App> {
const tab = { ...payload };
tab.client_id = createRandomString(12, 12);
tab.client_secret = createRandomString(24, 24);
const doc = await this.insert(tab);
return { ...doc, tokens: [] };
}
public async insert(payload: App): Promise<App> {
const doc = await AppModel.create(payload, { returning: true });
return doc.get({ plain: true }) as App;
}
public async update(payload: App): Promise<App> {
const newDoc = await this.updateDb({
name: payload.name,
scopes: payload.scopes,
id: payload.id,
} as any);
return { ...newDoc, tokens: [] };
}
private async updateDb(payload: App): Promise<App> {
await AppModel.update(payload, { where: { id: payload.id } });
return await this.getDb({ id: payload.id });
}
public async getDb(query: any): Promise<App> {
const doc: any = await AppModel.findOne({ where: query });
return doc && (doc.get({ plain: true }) as App);
}
public async remove(ids: number[]) {
await AppModel.destroy({ where: { id: ids } });
}
public async resetSecret(id: number): Promise<App> {
const tab: any = {
client_secret: createRandomString(24, 24),
tokens: [],
id,
};
// const doc = await this.get(id);
// const tab = new App({ ...doc });
// tab.client_secret = createRandomString(24, 24);
// tab.tokens = [];
// const newDoc = await this.updateDb(tab);
// return newDoc;
const newDoc = await this.updateDb(tab);
return newDoc;
}
public async list(
searchText: string = '',
sort: any = {},
query: any = {},
): Promise<App[]> {
let condition = { ...query };
if (searchText) {
const encodeText = encodeURIComponent(searchText);
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchText}%` },
{ [Op.like]: `%${encodeText}%` },
],
};
condition = {
...condition,
name: reg,
};
}
try {
const result = await this.find(condition);
return result
.filter((x) => x.name !== 'system')
.map((x) => ({ ...x, tokens: [] }));
} catch (error) {
throw error;
}
}
private async find(query: any, sort?: any): Promise<App[]> {
const docs = await AppModel.findAll({ where: { ...query } });
return docs.map((x) => x.get({ plain: true }));
}
public async authToken({
client_id,
client_secret,
}: {
client_id: string;
client_secret: string;
}): Promise<any> {
let token = uuidV4();
const expiration = Math.round(Date.now() / 1000) + 2592000; // 2592000 30天
const doc = await AppModel.findOne({ where: { client_id, client_secret } });
if (doc) {
const timestamp = Math.round(Date.now() / 1000);
const invalidTokens = (doc.tokens || []).filter(
(x) => x.expiration >= timestamp,
);
let tokens = invalidTokens;
if (invalidTokens.length > 5) {
tokens = [
...invalidTokens.slice(0, 4),
{ ...invalidTokens[4], expiration },
];
token = invalidTokens[4].value;
} else {
tokens = [...invalidTokens, { value: token, expiration }];
}
await AppModel.update(
{ tokens },
{ where: { client_id, client_secret } },
);
return {
code: 200,
data: {
token,
token_type: 'Bearer',
expiration,
},
};
} else {
return { code: 400, message: 'client_id或client_seret有误' };
}
}
public async generateSystemToken(): Promise<{
value: string;
expiration: number;
}> {
let systemApp = (await AppModel.findOne({
where: { name: 'system' },
})) as App;
if (!systemApp) {
systemApp = await this.create({
name: 'system',
scopes: ['crons', 'system'],
} as App);
}
const { data } = await this.authToken({
client_id: systemApp.client_id,
client_secret: systemApp.client_secret,
});
return {
...data,
value: data.token,
};
}
}
+192
View File
@@ -0,0 +1,192 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import nodeSchedule from 'node-schedule';
import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process';
import {
ToadScheduler,
LongIntervalJob,
AsyncTask,
SimpleIntervalSchedule,
} from 'toad-scheduler';
import dayjs from 'dayjs';
interface ScheduleTaskType {
id: number;
command: string;
name?: string;
schedule?: string;
}
export interface TaskCallbacks {
onBefore?: (startTime: dayjs.Dayjs) => Promise<void>;
onStart?: (
cp: ChildProcessWithoutNullStreams,
startTime: dayjs.Dayjs,
) => Promise<void>;
onEnd?: (
cp: ChildProcessWithoutNullStreams,
endTime: dayjs.Dayjs,
diff: number,
) => Promise<void>;
onLog?: (message: string) => Promise<void>;
onError?: (message: string) => Promise<void>;
}
@Service()
export default class ScheduleService {
private scheduleStacks = new Map<string, nodeSchedule.Job>();
private intervalSchedule = new ToadScheduler();
private maxBuffer = 200 * 1024 * 1024;
constructor(@Inject('logger') private logger: winston.Logger) {}
async runTask(command: string, callbacks: TaskCallbacks = {}) {
return new Promise(async (resolve, reject) => {
try {
const startTime = dayjs();
await callbacks.onBefore?.(startTime);
const cp = spawn(command, { shell: '/bin/bash' });
// TODO:
callbacks.onStart?.(cp, startTime);
cp.stdout.on('data', async (data) => {
await callbacks.onLog?.(data.toString());
});
cp.stderr.on('data', async (data) => {
this.logger.error(
'执行任务 %s 失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
data.toString(),
);
await callbacks.onError?.(data.toString());
});
cp.on('error', async (err) => {
this.logger.error(
'创建任务 %s 失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
err,
);
await callbacks.onError?.(JSON.stringify(err));
});
cp.on('exit', async (code, signal) => {
this.logger.info(
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
);
});
cp.on('close', async (code) => {
const endTime = dayjs();
await callbacks.onEnd?.(
cp,
endTime,
endTime.diff(startTime, 'seconds'),
);
resolve(null);
});
} catch (error) {
await this.logger.error(
'执行任务%s失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
error,
);
await callbacks.onError?.(JSON.stringify(error));
resolve(null);
}
});
}
async createCronTask(
{ id = 0, command, name, schedule = '' }: ScheduleTaskType,
callbacks?: TaskCallbacks,
runImmediately = false,
) {
const _id = this.formatId(id);
this.logger.info(
'[创建cron任务],任务ID: %scron: %s,任务名: %s,执行命令: %s',
_id,
schedule,
name,
command,
);
this.scheduleStacks.set(
_id,
nodeSchedule.scheduleJob(_id, schedule, async () => {
await this.runTask(command, callbacks);
}),
);
if (runImmediately) {
await this.runTask(command, callbacks);
}
}
async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
const _id = this.formatId(id);
this.logger.info('[取消定时任务],任务名:%s', name);
this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel();
}
async createIntervalTask(
{ id = 0, command, name = '' }: ScheduleTaskType,
schedule: SimpleIntervalSchedule,
runImmediately = true,
callbacks?: TaskCallbacks,
) {
const _id = this.formatId(id);
this.logger.info(
'[创建interval任务],任务ID: %s,任务名: %s,执行命令: %s',
_id,
name,
command,
);
const task = new AsyncTask(
name,
async () => {
return new Promise(async (resolve, reject) => {
await this.runTask(command, callbacks);
});
},
(err) => {
this.logger.error(
'执行任务%s失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
err,
);
},
);
const job = new LongIntervalJob(
{ runImmediately: false, ...schedule },
task,
_id,
);
this.intervalSchedule.addIntervalJob(job);
if (runImmediately) {
await this.runTask(command, callbacks);
}
}
async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) {
const _id = this.formatId(id);
this.logger.info('[取消interval任务],任务ID: %s,任务名:%s', _id, name);
this.intervalSchedule.removeById(_id);
}
private formatId(id: number): string {
return String(id);
}
}
+63
View File
@@ -0,0 +1,63 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import fs from 'fs';
import path from 'path';
import SockService from './sock';
import CronService from './cron';
import ScheduleService, { TaskCallbacks } from './schedule';
import config from '../config';
@Service()
export default class ScriptService {
constructor(
@Inject('logger') private logger: winston.Logger,
private sockService: SockService,
private cronService: CronService,
private scheduleService: ScheduleService,
) {}
private taskCallbacks(filePath: string): TaskCallbacks {
return {
onEnd: async (cp, endTime, diff) => {
try {
fs.unlinkSync(filePath);
} catch (error) {}
},
onError: async (message: string) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
message,
});
},
onLog: async (message: string) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
message,
});
},
};
}
public async runScript(filePath: string) {
const relativePath = path.relative(config.scriptPath, filePath);
const command = `task -l ${relativePath} now`;
this.scheduleService.runTask(command, this.taskCallbacks(filePath));
return { code: 200 };
}
public async stopScript(filePath: string) {
const relativePath = path.relative(config.scriptPath, filePath);
const err = await this.cronService.killTask(`task -l ${relativePath} now`);
const str = err ? `\n${err}` : '';
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: `${str}\n## 执行结束... ${new Date()
.toLocaleString('zh', { hour12: false })
.replace(' 24:', ' 00:')} `,
});
return { code: 200 };
}
}
+34
View File
@@ -0,0 +1,34 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { Connection } from 'sockjs';
import { SockMessage } from '../data/sock';
@Service()
export default class SockService {
private clients: Connection[] = [];
constructor(@Inject('logger') private logger: winston.Logger) {}
public getClients() {
return this.clients;
}
public addClient(conn: Connection) {
if (this.clients.indexOf(conn) === -1) {
this.clients.push(conn);
}
}
public removeClient(conn: Connection) {
const index = this.clients.indexOf(conn);
if (index !== -1) {
this.clients.splice(index, 1);
}
}
public sendMessage(msg: SockMessage) {
this.clients.forEach((x) => {
x.write(JSON.stringify(msg));
});
}
}
+84
View File
@@ -0,0 +1,84 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import fs from 'fs';
import os from 'os';
import path from 'path';
@Service()
export default class SshKeyService {
private homedir = os.homedir();
private sshPath = path.resolve(this.homedir, '.ssh');
private sshConfigFilePath = path.resolve(this.sshPath, 'config');
constructor(@Inject('logger') private logger: winston.Logger) {}
private generatePrivateKeyFile(alias: string, key: string): void {
try {
fs.writeFileSync(`${this.sshPath}/${alias}`, key, {
encoding: 'utf8',
mode: '400',
});
} catch (error) {
this.logger.error('生成私钥文件失败', error);
}
}
private getConfigRegx(alias: string) {
return new RegExp(
`Host ${alias}\n.*[^StrictHostKeyChecking]*.*[\n]*.*StrictHostKeyChecking no`,
'g',
);
}
private removePrivateKeyFile(alias: string): void {
try {
fs.unlinkSync(`${this.sshPath}/${alias}`);
} catch (error) {
this.logger.error('删除私钥文件失败', error);
}
}
private generateSingleSshConfig(alias: string, host: string): string {
return `\nHost ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no`;
}
private generateSshConfig(configs: string[]) {
try {
for (const config of configs) {
fs.appendFileSync(this.sshConfigFilePath, config, {
encoding: 'utf8',
});
}
} catch (error) {
this.logger.error('写入ssh配置文件失败', error);
}
}
private removeSshConfig(alias: string) {
try {
const configRegx = this.getConfigRegx(alias);
const data = fs
.readFileSync(this.sshConfigFilePath, { encoding: 'utf8' })
.replace(configRegx, '')
.replace(/\n[\n]+/g, '\n');
fs.writeFileSync(this.sshConfigFilePath, data, {
encoding: 'utf8',
});
} catch (error) {
this.logger.error(`删除ssh配置文件${alias}失败`, error);
}
}
public addSSHKey(key: string, alias: string, host: string): void {
this.generatePrivateKeyFile(alias, key);
const config = this.generateSingleSshConfig(alias, host);
this.removeSshConfig(alias);
this.generateSshConfig([config]);
}
public removeSSHKey(alias: string, host: string): void {
this.removePrivateKeyFile(alias);
const config = this.generateSingleSshConfig(alias, host);
this.removeSshConfig(config);
}
}
+459
View File
@@ -0,0 +1,459 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import config from '../config';
import {
Subscription,
SubscriptionModel,
SubscriptionStatus,
} from '../data/subscription';
import {
ChildProcessWithoutNullStreams,
exec,
execSync,
spawn,
} from 'child_process';
import fs from 'fs';
import cron_parser from 'cron-parser';
import {
getFileContentByName,
concurrentRun,
fileExist,
createFile,
} from '../config/util';
import { promises, existsSync } from 'fs';
import { promisify } from 'util';
import { Op } from 'sequelize';
import path from 'path';
import ScheduleService, { TaskCallbacks } from './schedule';
import { SimpleIntervalSchedule } from 'toad-scheduler';
import SockService from './sock';
import SshKeyService from './sshKey';
import dayjs from 'dayjs';
@Service()
export default class SubscriptionService {
constructor(
@Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService,
private sockService: SockService,
private sshKeyService: SshKeyService,
) {}
public async list(searchText?: string): Promise<Subscription[]> {
let query = {};
if (searchText) {
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchText}%` },
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
],
};
query = {
[Op.or]: [
{
name: reg,
},
{
url: reg,
},
],
};
}
try {
const result = await SubscriptionModel.findAll({
where: query,
order: [['createdAt', 'DESC']],
});
return result as any;
} catch (error) {
throw error;
}
}
private formatCommand(doc: Subscription, url?: string) {
let command = 'ql ';
let _url = url || this.formatUrl(doc).url;
const { type, whitelist, blacklist, dependences, branch, extensions } = doc;
if (type === 'file') {
command += `raw "${_url}"`;
} else {
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
dependences || ''
}" "${branch || ''}" "${extensions || ''}"`;
}
return command;
}
private formatUrl(doc: Subscription) {
let url = doc.url;
let host = '';
if (doc.type === 'private-repo') {
if (doc.pull_type === 'ssh-key') {
host = doc.url!.replace(/.*\@([^\:]+)\:.*/, '$1');
url = doc.url!.replace(host, doc.alias);
} else {
host = doc.url!.replace(/.*\:\/\/([^\/]+)\/.*/, '$1');
const { username, password } = doc.pull_option as any;
url = doc.url!.replace(host, `${username}:${password}@${host}`);
}
}
return { url, host };
}
public async handleTask(
doc: Subscription,
needCreate = true,
needAddKey = true,
runImmediately = false,
) {
const { url, host } = this.formatUrl(doc);
if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {
if (needAddKey) {
this.sshKeyService.addSSHKey(
(doc.pull_option as any).private_key,
doc.alias,
host,
);
} else {
this.sshKeyService.removeSSHKey(doc.alias, host);
}
}
doc.command = this.formatCommand(doc, url as string);
if (doc.schedule_type === 'crontab') {
this.scheduleService.cancelCronTask(doc as any);
needCreate &&
(await this.scheduleService.createCronTask(
doc as any,
this.taskCallbacks(doc),
runImmediately,
));
} else {
this.scheduleService.cancelIntervalTask(doc as any);
const { type, value } = doc.interval_schedule as any;
needCreate &&
(await this.scheduleService.createIntervalTask(
doc as any,
{ [type]: value } as SimpleIntervalSchedule,
runImmediately,
this.taskCallbacks(doc),
));
}
}
private async promiseExec(command: string): Promise<string> {
return new Promise((resolve, reject) => {
exec(
command,
{ maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' },
(err, stdout, stderr) => {
resolve(stdout || stderr || JSON.stringify(err));
},
);
});
}
private async handleLogPath(
logPath: string,
data: string = '',
): Promise<string> {
const absolutePath = path.resolve(config.logPath, logPath);
const logFileExist = await fileExist(absolutePath);
if (!logFileExist) {
await createFile(absolutePath, data);
}
return absolutePath;
}
private taskCallbacks(doc: Subscription): TaskCallbacks {
return {
onBefore: async (startTime) => {
const logTime = startTime.format('YYYY-MM-DD-HH-mm-ss');
const logPath = `${doc.alias}/${logTime}.log`;
await SubscriptionModel.update(
{
status: SubscriptionStatus.running,
log_path: logPath,
},
{ where: { id: doc.id } },
);
const absolutePath = await this.handleLogPath(
logPath as string,
`## 开始执行... ${startTime.format('YYYY-MM-DD HH:mm:ss')}\n`,
);
// 执行sub_before
let beforeStr = '';
try {
if (doc.sub_before) {
fs.appendFileSync(absolutePath, `\n## 执行before命令...\n\n`);
beforeStr = await this.promiseExec(doc.sub_before);
}
} catch (error: any) {
beforeStr =
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
}
if (beforeStr) {
fs.appendFileSync(absolutePath, `${beforeStr}\n`);
}
},
onStart: async (cp: ChildProcessWithoutNullStreams, startTime) => {
await SubscriptionModel.update(
{
pid: cp.pid,
},
{ where: { id: doc.id } },
);
},
onEnd: async (cp, endTime, diff) => {
const sub = await this.getDb({ id: doc.id });
const absolutePath = await this.handleLogPath(sub.log_path as string);
// 执行 sub_after
let afterStr = '';
try {
if (sub.sub_after) {
fs.appendFileSync(absolutePath, `\n\n## 执行after命令...\n\n`);
afterStr = await this.promiseExec(sub.sub_after);
}
} catch (error: any) {
afterStr =
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
}
if (afterStr) {
fs.appendFileSync(absolutePath, `${afterStr}\n`);
}
fs.appendFileSync(
absolutePath,
`\n## 执行结束... ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)} 耗时 ${diff}`,
);
await SubscriptionModel.update(
{ status: SubscriptionStatus.idle, pid: undefined },
{ where: { id: sub.id } },
);
this.sockService.sendMessage({
type: 'runSubscriptionEnd',
message: '订阅执行完成',
references: [doc.id as number],
});
},
onError: async (message: string) => {
const sub = await this.getDb({ id: doc.id });
const absolutePath = await this.handleLogPath(sub.log_path as string);
fs.appendFileSync(absolutePath, `\n${message}`);
},
onLog: async (message: string) => {
const sub = await this.getDb({ id: doc.id });
const absolutePath = await this.handleLogPath(sub.log_path as string);
fs.appendFileSync(absolutePath, `\n${message}`);
},
};
}
public async create(payload: Subscription): Promise<Subscription> {
const tab = new Subscription(payload);
const doc = await this.insert(tab);
await this.handleTask(doc);
return doc;
}
public async insert(payload: Subscription): Promise<Subscription> {
return await SubscriptionModel.create(payload, { returning: true });
}
public async update(payload: Subscription): Promise<Subscription> {
const newDoc = await this.updateDb(payload);
await this.handleTask(newDoc, !newDoc.is_disabled);
return newDoc;
}
public async updateDb(payload: Subscription): Promise<Subscription> {
await SubscriptionModel.update(payload, { where: { id: payload.id } });
return await this.getDb({ id: payload.id });
}
public async status({
ids,
status,
pid,
log_path,
last_running_time = 0,
last_execution_time = 0,
}: {
ids: number[];
status: SubscriptionStatus;
pid: number;
log_path: string;
last_running_time: number;
last_execution_time: number;
}) {
const options: any = {
status,
pid,
log_path,
last_execution_time,
};
if (last_running_time > 0) {
options.last_running_time = last_running_time;
}
return await SubscriptionModel.update(
{ ...options },
{ where: { id: ids } },
);
}
public async remove(ids: number[]) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) {
await this.handleTask(doc, false, false);
}
await SubscriptionModel.destroy({ where: { id: ids } });
}
public async getDb(query: any): Promise<Subscription> {
const doc: any = await SubscriptionModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Subscription);
}
public async run(ids: number[]) {
await SubscriptionModel.update(
{ status: SubscriptionStatus.queued },
{ where: { id: ids } },
);
concurrentRun(
ids.map((id) => async () => await this.runSingle(id)),
10,
);
}
public async stop(ids: number[]) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) {
if (doc.pid) {
try {
process.kill(-doc.pid);
} catch (error) {
this.logger.silly(error);
}
}
const command = this.formatCommand(doc);
const err = await this.killTask(command);
const absolutePath = await this.handleLogPath(doc.log_path as string);
const str = err ? `\n${err}` : '';
fs.appendFileSync(
`${absolutePath}`,
`${str}\n## 执行结束... ${dayjs().format('YYYY-MM-DD HH:mm:ss')} `,
);
}
await SubscriptionModel.update(
{ status: SubscriptionStatus.idle, pid: undefined },
{ where: { id: ids } },
);
}
public async killTask(name: string) {
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
const execAsync = promisify(exec);
try {
let pid = (await execAsync(taskCommand)).stdout;
if (pid) {
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
} else {
return;
}
let pids = pid.match(/\(\d+/g);
const killLogs = [];
if (pids && pids.length > 0) {
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
for (const id of pids) {
const c = `kill -9 ${id.slice(1)}`;
try {
const { stdout, stderr } = await execAsync(c);
if (stderr) {
killLogs.push(stderr);
}
if (stdout) {
killLogs.push(stdout);
}
} catch (error: any) {
killLogs.push(error.message);
}
}
}
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
} catch (e) {
return JSON.stringify(e);
}
}
private async runSingle(subscriptionId: number) {
const subscription = await this.getDb({ id: subscriptionId });
if (subscription.status !== SubscriptionStatus.queued) {
return;
}
const command = this.formatCommand(subscription);
await this.scheduleService.runTask(
command,
this.taskCallbacks(subscription),
);
}
public async disabled(ids: number[]) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) {
await this.handleTask(doc, false);
}
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
}
public async enabled(ids: number[]) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) {
await this.handleTask(doc);
}
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
}
public async log(id: number) {
const doc = await this.getDb({ id });
if (!doc || !doc.log_path) {
return '';
}
const absolutePath = await this.handleLogPath(doc.log_path as string);
return getFileContentByName(absolutePath);
}
public async logs(id: number) {
const doc = await this.getDb({ id });
if (!doc) {
return [];
}
if (doc.log_path) {
const relativeDir = path.dirname(`${doc.log_path}`);
const dir = path.resolve(config.logPath, relativeDir);
if (existsSync(dir)) {
let files = await promises.readdir(dir);
return files
.map((x) => ({
filename: x,
directory: relativeDir.replace(config.logPath, ''),
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time);
}
}
}
}
+177
View File
@@ -0,0 +1,177 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import config from '../config';
import * as fs from 'fs';
import _ from 'lodash';
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
import { NotificationInfo } from '../data/notify';
import NotificationService from './notify';
import ScheduleService from './schedule';
import { spawn } from 'child_process';
import SockService from './sock';
import got from 'got';
@Service()
export default class SystemService {
@Inject((type) => NotificationService)
private notificationService!: NotificationService;
constructor(
@Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService,
private sockService: SockService,
) {}
public async getLogRemoveFrequency() {
const doc = await this.getDb({ type: AuthDataType.removeLogFrequency });
return doc || {};
}
private async updateAuthDb(payload: AuthInfo): Promise<any> {
await AuthModel.upsert({ ...payload });
const doc = await this.getDb({ type: payload.type });
return doc;
}
public async getDb(query: any): Promise<any> {
const doc: any = await AuthModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as any);
}
public async updateNotificationMode(notificationInfo: NotificationInfo) {
const code = Math.random().toString().slice(-6);
const isSuccess = await this.notificationService.testNotify(
notificationInfo,
'青龙',
`【蛟龙】测试通知 https://t.me/jiao_long`,
);
if (isSuccess) {
const result = await this.updateAuthDb({
type: AuthDataType.notification,
info: { ...notificationInfo },
});
return { code: 200, data: { ...result, code } };
} else {
return { code: 400, message: '通知发送失败,请检查参数' };
}
}
public async updateLogRemoveFrequency(frequency: number) {
const oDoc = await this.getLogRemoveFrequency();
const result = await this.updateAuthDb({
...oDoc,
type: AuthDataType.removeLogFrequency,
info: { frequency },
});
const cron = {
id: result.id,
name: '删除日志',
command: `ql rmlog ${frequency}`,
};
await this.scheduleService.cancelIntervalTask(cron);
if (frequency > 0) {
await this.scheduleService.createIntervalTask(cron, {
days: frequency,
runImmediately: true,
});
}
return { code: 200, data: { ...cron } };
}
public async checkUpdate() {
try {
const versionRegx = /.*export const version = \'(.*)\'\;/;
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
let lastVersion = '';
let lastLog = '';
try {
const result = await got.get(config.lastVersionFile, {
timeout: 6000,
retry: 0,
});
const lastVersionFileContent = result.body;
lastVersion = lastVersionFileContent.match(versionRegx)![1];
lastLog = lastVersionFileContent.match(logRegx)
? lastVersionFileContent.match(logRegx)![1]
: '';
} catch (error) {}
return {
code: 200,
data: {
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion),
lastVersion,
lastLog,
},
};
} catch (error: any) {
return {
code: 400,
message: error.message,
};
}
}
private checkHasNewVersion(curVersion: string, lastVersion: string) {
const curArr = curVersion.split('.').map((x) => parseInt(x, 10));
const lastArr = lastVersion.split('.').map((x) => parseInt(x, 10));
if (curArr[0] < lastArr[0]) {
return true;
}
if (curArr[0] === lastArr[0] && curArr[1] < lastArr[1]) {
return true;
}
if (
curArr[0] === lastArr[0] &&
curArr[1] === lastArr[1] &&
curArr[2] < lastArr[2]
) {
return true;
}
return false;
}
public async updateSystem() {
const cp = spawn('ql -l update', { shell: '/bin/bash' });
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: `开始更新系统`,
});
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: data.toString(),
});
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: data.toString(),
});
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: JSON.stringify(err),
});
});
return { code: 200 };
}
public async notify({ title, content }: { title: string; content: string }) {
const isSuccess = await this.notificationService.notify(title, content);
if (isSuccess) {
return { code: 200, message: '通知发送成功' };
} else {
return { code: 400, message: '通知发送失败,请检查系统设置/通知配置' };
}
}
}
+352
View File
@@ -0,0 +1,352 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { createRandomString, getNetIp, getPlatform } from '../config/util';
import config from '../config';
import * as fs from 'fs';
import _ from 'lodash';
import jwt from 'jsonwebtoken';
import { authenticator } from '@otplib/preset-default';
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
import { NotificationInfo } from '../data/notify';
import NotificationService from './notify';
import { Request } from 'express';
import ScheduleService from './schedule';
import { spawn } from 'child_process';
import SockService from './sock';
import dayjs from 'dayjs';
@Service()
export default class UserService {
@Inject((type) => NotificationService)
private notificationService!: NotificationService;
constructor(
@Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService,
private sockService: SockService,
) {}
public async login(
payloads: {
username: string;
password: string;
},
req: Request,
needTwoFactor = true,
): Promise<any> {
if (!fs.existsSync(config.authConfigFile)) {
return this.initAuthInfo();
}
let { username, password } = payloads;
const content = this.getAuthInfo();
const timestamp = Date.now();
if (content) {
let {
username: cUsername,
password: cPassword,
retries = 0,
lastlogon,
lastip,
lastaddr,
twoFactorActivated,
twoFactorActived,
tokens = {},
platform,
} = content;
// patch old field
twoFactorActivated = twoFactorActivated || twoFactorActived;
if (
(cUsername === 'admin' && cPassword === 'admin') ||
!cUsername ||
!cPassword
) {
return this.initAuthInfo();
}
if (retries > 2 && Date.now() - lastlogon < Math.pow(3, retries) * 1000) {
return {
code: 410,
message: `失败次数过多,请${Math.round(
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
)}秒后重试`,
data: Math.round(
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
),
};
}
const { ip, address } = await getNetIp(req);
if (username === cUsername && password === cPassword) {
if (twoFactorActivated && needTwoFactor) {
this.updateAuthInfo(content, {
isTwoFactorChecking: true,
});
return {
code: 420,
message: '请输入两步验证token',
};
}
const data = createRandomString(50, 100);
const expiration = twoFactorActivated ? 30 : 3;
let token = jwt.sign({ data }, config.secret as any, {
expiresIn: 60 * 60 * 24 * expiration,
algorithm: 'HS384',
});
this.updateAuthInfo(content, {
token,
tokens: {
...tokens,
[req.platform]: token,
},
lastlogon: timestamp,
retries: 0,
lastip: ip,
lastaddr: address,
platform: req.platform,
isTwoFactorChecking: false,
});
await this.notificationService.notify(
'登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform
}端 登录成功,ip地址 ${ip}`,
);
await this.getLoginLog();
await this.insertDb({
type: AuthDataType.loginLog,
info: {
timestamp,
address,
ip,
platform: req.platform,
status: LoginStatus.success,
},
});
return {
code: 200,
data: { token, lastip, lastaddr, lastlogon, retries, platform },
};
} else {
this.updateAuthInfo(content, {
retries: retries + 1,
lastlogon: timestamp,
lastip: ip,
lastaddr: address,
platform: req.platform,
});
await this.notificationService.notify(
'登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform
}端 登录失败,ip地址 ${ip}`,
);
await this.getLoginLog();
await this.insertDb({
type: AuthDataType.loginLog,
info: {
timestamp,
address,
ip,
platform: req.platform,
status: LoginStatus.fail,
},
});
return { code: 400, message: config.authError };
}
} else {
return this.initAuthInfo();
}
}
public async logout(platform: string): Promise<any> {
const authInfo = this.getAuthInfo();
this.updateAuthInfo(authInfo, {
token: '',
tokens: { ...authInfo.tokens, [platform]: '' },
});
}
public async getLoginLog(): Promise<AuthInfo[]> {
const docs = await AuthModel.findAll({
where: { type: AuthDataType.loginLog },
});
if (docs && docs.length > 0) {
const result = docs.sort((a, b) => b.info.timestamp - a.info.timestamp);
if (result.length > 100) {
await AuthModel.destroy({
where: { id: result[result.length - 1].id },
});
}
return result.map((x) => x.info);
}
return [];
}
private async insertDb(payload: AuthInfo): Promise<AuthInfo> {
const doc = await AuthModel.create({ ...payload }, { returning: true });
return doc;
}
private initAuthInfo() {
const newPassword = createRandomString(16, 22);
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({
username: 'admin',
password: newPassword,
}),
);
return {
code: 100,
message: '已初始化密码,请前往auth.json查看并重新登录',
};
}
public async updateUsernameAndPassword({
username,
password,
}: {
username: string;
password: string;
}) {
if (password === 'admin') {
return { code: 400, message: '密码不能设置为admin' };
}
const authInfo = this.getAuthInfo();
this.updateAuthInfo(authInfo, { username, password });
return { code: 200, message: '更新成功' };
}
public async updateAvatar(avatar: string) {
const authInfo = this.getAuthInfo();
this.updateAuthInfo(authInfo, { avatar });
return { code: 200, data: avatar, message: '更新成功' };
}
public getUserInfo(): Promise<any> {
return new Promise((resolve) => {
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
if (err) console.log(err);
resolve(JSON.parse(data));
});
});
}
public initTwoFactor() {
const secret = authenticator.generateSecret();
const authInfo = this.getAuthInfo();
const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);
this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
return { secret, url: otpauth };
}
public activeTwoFactor(code: string) {
const authInfo = this.getAuthInfo();
const isValid = authenticator.verify({
token: code,
secret: authInfo.twoFactorSecret,
});
if (isValid) {
this.updateAuthInfo(authInfo, { twoFactorActivated: true });
}
return isValid;
}
public async twoFactorLogin(
{
username,
password,
code,
}: { username: string; password: string; code: string },
req: any,
) {
const authInfo = this.getAuthInfo();
const { isTwoFactorChecking, twoFactorSecret } = authInfo;
if (!isTwoFactorChecking) {
return { code: 450, message: '未知错误' };
}
const isValid = authenticator.verify({
token: code,
secret: twoFactorSecret,
});
if (isValid) {
return this.login({ username, password }, req, false);
} else {
const { ip, address } = await getNetIp(req);
this.updateAuthInfo(authInfo, {
lastip: ip,
lastaddr: address,
platform: req.platform,
});
return { code: 430, message: '验证失败' };
}
}
public deactiveTwoFactor() {
const authInfo = this.getAuthInfo();
this.updateAuthInfo(authInfo, {
twoFactorActivated: false,
twoFactorActived: false,
twoFactorSecret: '',
});
return true;
}
private getAuthInfo() {
const content = fs.readFileSync(config.authConfigFile, 'utf8');
return JSON.parse(content || '{}');
}
private updateAuthInfo(authInfo: any, info: any) {
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({ ...authInfo, ...info }),
);
}
public async getNotificationMode(): Promise<NotificationInfo> {
const doc = await this.getDb({ type: AuthDataType.notification });
return (doc && doc.info) || {};
}
private async updateAuthDb(payload: AuthInfo): Promise<any> {
let doc = await AuthModel.findOne({ type: payload.type });
if (doc) {
const updateResult = await AuthModel.update(payload, {
where: { id: doc.id },
returning: true,
});
doc = updateResult[1][0];
} else {
doc = await AuthModel.create(payload, { returning: true });
}
return doc;
}
public async getDb(query: any): Promise<any> {
const doc: any = await AuthModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as any);
}
public async updateNotificationMode(notificationInfo: NotificationInfo) {
const code = Math.random().toString().slice(-6);
const isSuccess = await this.notificationService.testNotify(
notificationInfo,
'青龙',
`【蛟龙】测试通知 https://t.me/jiao_long`,
);
if (isSuccess) {
const result = await this.updateAuthDb({
type: AuthDataType.notification,
info: { ...notificationInfo },
});
return { code: 200, data: { ...result, code } };
} else {
return { code: 400, message: '通知发送失败,请检查参数' };
}
}
}
Executable
+34
View File
@@ -0,0 +1,34 @@
import 'reflect-metadata';
import OpenService from './services/open';
import { Container } from 'typedi';
import LoggerInstance from './loaders/logger';
import fs from 'fs';
import config from './config';
import path from 'path';
const tokenFile = path.join(config.configPath, 'token.json');
async function getToken() {
try {
Container.set('logger', LoggerInstance);
const openService = Container.get(OpenService);
const appToken = await openService.generateSystemToken();
console.log(appToken.value);
await writeFile({
value: appToken.value,
expiration: appToken.expiration,
});
} catch (error) {
console.log(error);
}
}
async function writeFile(data: any) {
return new Promise<void>((resolve, reject) => {
fs.writeFile(tokenFile, JSON.stringify(data), { encoding: 'utf8' }, () => {
resolve();
});
});
}
getToken();
+13
View File
@@ -0,0 +1,13 @@
import { CommandModule } from 'yargs';
export const updateCommand: CommandModule = {
command: 'update',
describe: 'Update and restart qinglong',
builder: (yargs) => {
return yargs.option('repositority', {
type: 'string',
alias: 'r',
describe: `Specify the release warehouse address of the package`,
});
},
handler: async (argv) => {},
};
+13
View File
@@ -0,0 +1,13 @@
import * as yargs from 'yargs';
import { green, red } from 'chalk';
import { updateCommand } from './commands/update';
yargs
.usage('Usage: ql [command] <options>')
.command(updateCommand)
.fail((err) => {
console.error(`${red(err)}`);
})
.alias('h', 'help')
.showHelp()
.recommendCommands().argv;
+32 -14
View File
@@ -1,14 +1,22 @@
FROM node:lts-alpine
LABEL maintainer="whyour"
ARG QL_URL=https://github.com/whyour/qinglong.git
ARG QL_BRANCH=master
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
FROM python:alpine
ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}"
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
ARG QL_BRANCH=develop
ENV PNPM_HOME=/root/.local/share/pnpm \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
LANG=zh_CN.UTF-8 \
SHELL=/bin/bash \
PS1="\u@\h:\w \$ " \
QL_DIR=/ql
QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH}
WORKDIR ${QL_DIR}
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
RUN set -x \
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk update -f \
&& apk upgrade \
&& apk --no-cache add -f bash \
@@ -21,22 +29,32 @@ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
perl \
openssl \
nginx \
python3 \
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 \
&& touch ~/.bashrc \
&& mkdir /run/nginx \
&& 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 \
&& npm install -g pm2 \
&& npm install -g pnpm \
&& rm -rf /root/.npm \
&& pnpm install --prod \
&& rm -rf /root/.pnpm-store
&& 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"]
+9
View File
@@ -0,0 +1,9 @@
version: '2'
services:
web:
image: whyour/qinglong:latest
volumes:
- ./data:/ql/data
ports:
- "0.0.0.0:5700:5700"
restart: unless-stopped
+36 -20
View File
@@ -1,42 +1,58 @@
#!/bin/bash
set -e
dir_shell=/ql/shell
. $dir_shell/share.sh
link_shell
echo -e "======================1. 检测配置文件========================\n"
fix_config
cp -fv $dir_root/docker/front.conf /etc/nginx/conf.d/front.conf
make_dir /etc/nginx/conf.d
make_dir /run/nginx
cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
pm2 l &>/dev/null
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
npm config set registry $NpmMirror
fi
echo
echo -e "======================2. 更新源代码========================\n"
ql update "no-restart"
pm2 l >/dev/null 2>&1
echo -e "======================2. 安装依赖========================\n"
update_depend
echo
echo -e "======================3. 启动nginx========================\n"
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "nginx启动成功...\n"
echo -e "======================4. 启动控制面板========================\n"
if [[ $(pm2 info panel 2>/dev/null) ]]; then
pm2 reload panel
else
pm2 start $dir_root/build/app.js -n panel
fi
echo -e "======================4. 启动面板监控========================\n"
pm2 delete public &>/dev/null
pm2 start $dir_static/build/public.js -n public --source-map-support --time
echo -e "监控服务启动成功...\n"
echo -e "======================5. 启动控制面板========================\n"
pm2 delete panel &>/dev/null
pm2 start $dir_static/build/app.js -n panel --source-map-support --time
echo -e "控制面板启动成功...\n"
echo -e "======================5. 启动定时任务========================\n"
if [[ $(pm2 info schedule 2>/dev/null) ]]; then
pm2 reload schedule
else
pm2 start $dir_root/build/schedule.js -n schedule
fi
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
echo -e "======================6. 启动bot========================\n"
ql bot
echo -e "======================7. 启动bot========================\n"
nohup ql bot >>$dir_log/start.log 2>&1 &
echo -e "bot后台启动中...\n"
fi
if [[ $EnableExtraShell == true ]]; then
echo -e "======================8. 执行自定义脚本========================\n"
nohup ql extra >>$dir_log/start.log 2>&1 &
echo -e "自定义脚本后台执行中...\n"
fi
echo -e "############################################################\n"
+44 -11
View File
@@ -1,27 +1,60 @@
upstream api {
server localhost:5600;
upstream baseApi {
server 0.0.0.0:5600;
}
upstream publicApi {
server 0.0.0.0:5400;
}
map $http_upgrade $connection_upgrade {
default keep-alive;
'websocket' upgrade;
}
server {
listen 5700;
root /ql/dist;
root /ql/static/dist;
ssl_session_timeout 5m;
location /api {
proxy_pass http://api;
location QL_BASE_URL/api/public/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://publicApi/api/public/;
}
location QL_BASE_URL/api/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://baseApi/api/;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
location QL_BASE_URL/open/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://baseApi/open/;
}
gzip on;
gzip_static on;
gzip_static on;
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
gzip_proxied any;
gzip_proxied any;
gzip_vary on;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.0;
gzip_http_version 1.0;
location / {
index index.html index.htm;
try_files $uri $uri/ /index.html;
location QL_BASE_URL/ {
index index.html index.htm;
try_files $uri $uri/ QL_BASE_URL/index.html;
}
location ~ .*\.(html)$ {
add_header Cache-Control no-cache;
}
}
+45
View File
@@ -0,0 +1,45 @@
user root;
worker_processes auto;
pcre_jit on;
error_log /var/log/nginx/error.log warn;
include /etc/nginx/modules/*.conf;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server_tokens off;
client_max_body_size 20m;
client_body_buffer_size 20m;
keepalive_timeout 65;
sendfile on;
tcp_nodelay on;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:2m;
gzip on;
gzip_static on;
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
gzip_proxied any;
gzip_vary on;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.0;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
include /etc/nginx/conf.d/*.conf;
}
+91 -43
View File
@@ -1,14 +1,17 @@
{
"private": true,
"scripts": {
"start": "umi dev",
"build": "umi build",
"build-back": "tsc -p tsconfig.back.json",
"start-back": "nodemon",
"panel": "npm run build-back && node build/app.js",
"schedule": "npm run build-back && node build/schedule.js",
"prepare": "umi generate tmp",
"start": "concurrently -n w: npm:start:*",
"start:front": "umi dev",
"start:back": "nodemon",
"start:public": "ts-node back/public.ts",
"build:front": "umi build",
"build:back": "tsc -p tsconfig.back.json",
"panel": "npm run build:back && node static/build/app.js",
"schedule": "npm run build:back && node static/build/schedule.js",
"public": "npm run build:back && node static/build/public.js",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"postinstall": "umi generate tmp 2>/dev/null || true",
"test": "umi-test",
"test:coverage": "umi-test --coverage"
},
@@ -23,58 +26,103 @@
"prettier --parser=typescript --write"
]
},
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
"react": "17",
"react-dom": "17"
}
}
},
"dependencies": {
"body-parser": "^1.19.0",
"celebrate": "^13.0.3",
"@otplib/preset-default": "^12.0.1",
"@sentry/node": "^7.12.1",
"@sentry/tracing": "^7.12.1",
"body-parser": "^1.19.2",
"celebrate": "^15.0.1",
"chokidar": "^3.5.3",
"cors": "^2.8.5",
"cron-parser": "^3.5.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-jwt": "^6.0.0",
"cron-parser": "^4.2.1",
"dayjs": "^1.11.2",
"dotenv": "^16.0.0",
"express": "^4.17.3",
"express-jwt": "^6.1.1",
"express-urlrewrite": "^1.4.0",
"got": "^11.8.2",
"hpagent": "^0.1.2",
"iconv-lite": "^0.6.3",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21",
"multer": "^1.4.4",
"nedb": "^1.8.0",
"node-fetch": "^2.6.1",
"node-schedule": "^2.0.0",
"p-queue": "6.6.2",
"node-schedule": "^2.1.0",
"nodemailer": "^6.7.2",
"p-queue": "7.2.0",
"reflect-metadata": "^0.1.13",
"typedi": "^0.8.0",
"winston": "^3.3.3"
"sequelize": "^6.20.1",
"serve-handler": "^6.1.3",
"sockjs": "^0.3.24",
"sqlite3": "npm:@louislam/sqlite3@^15.0.6",
"toad-scheduler": "^1.6.0",
"typedi": "^0.10.0",
"uuid": "^8.3.2",
"winston": "^3.6.0",
"yargs": "^17.3.1"
},
"devDependencies": {
"@ant-design/pro-layout": "^6.5.0",
"@ant-design/icons": "^4.6.2",
"@types/cors": "^2.8.10",
"@types/express": "^4.17.8",
"@types/express-jwt": "^6.0.1",
"@types/jsonwebtoken": "^8.5.0",
"@types/nedb": "^1.8.11",
"@types/node": "^14.11.2",
"@types/node-fetch": "^2.5.8",
"@types/qrcode.react": "^1.0.1",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@umijs/plugin-antd": "^0.9.1",
"@umijs/test": "^3.3.9",
"codemirror": "^5.59.4",
"compression-webpack-plugin": "6.1.1",
"darkreader": "^4.9.27",
"lint-staged": "^10.0.7",
"nodemon": "^2.0.4",
"prettier": "^2.2.0",
"@ant-design/icons": "^4.7.0",
"@ant-design/pro-layout": "^6.33.1",
"@monaco-editor/react": "^4.3.1",
"@sentry/react": "^7.12.1",
"@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
"@types/express-jwt": "^6.0.4",
"@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.179",
"@types/multer": "^1.4.7",
"@types/nedb": "^1.8.12",
"@types/node": "^17.0.21",
"@types/node-schedule": "^1.3.2",
"@types/nodemailer": "^6.4.4",
"@types/qrcode.react": "^1.0.2",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.13",
"@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1",
"@types/uuid": "^8.3.4",
"@umijs/plugin-antd": "^0.15.0",
"@umijs/plugin-esbuild": "^1.4.1",
"@umijs/ssr-darkreader": "^4.9.45",
"@umijs/test": "^3.5.21",
"ansi-to-react": "^6.1.6",
"antd": "^4.20.5",
"antd-img-crop": "^4.2.3",
"codemirror": "^5.65.2",
"compression-webpack-plugin": "9.2.0",
"concurrently": "^7.0.0",
"lint-staged": "^12.3.4",
"nodemon": "^2.0.15",
"prettier": "^2.5.1",
"qiniu": "^7.4.0",
"qrcode.react": "^1.0.1",
"rc-tween-one": "^3.0.6",
"react": "17.x",
"react-codemirror2": "^7.2.1",
"react-diff-viewer": "^3.1.1",
"react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0",
"react-dom": "17.x",
"ts-node": "^9.0.0",
"typescript": "^4.1.2",
"umi": "^3.3.9",
"umi-request": "^1.3.5",
"react-split-pane": "^0.1.92",
"sockjs-client": "^1.6.0",
"ts-node": "^10.6.0",
"tslib": "^2.4.0",
"typescript": "^4.6.2",
"umi": "^3.5.21",
"umi-request": "^1.4.0",
"vh-check": "^2.0.5",
"webpack": "^5.28.0",
"webpack": "^5.70.0",
"yorkie": "^2.0.0"
}
}
-9826
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.8 KiB

+1 -1
View File
@@ -1 +1 @@
{ "username": "admin", "password": "adminadmin" }
{ "username": "admin", "password": "admin" }
+48 -109
View File
@@ -1,6 +1,6 @@
## Version: v2.2.0-066
## Date: 2021-06-17
## Update Content: \n1. 修复版本号样式\n2. 修复查看日志或者编辑任务后列表异常
## Version: v2.8.0
## Date: 2021-06-20
## Update Content: 可持续发展纲要\n1. session管理破坏性修改\n2. 配置管理可编辑config下文件\n3. 自定义脚本改为查看脚本\n4. 移除互助相关
## 上面版本号中,如果第2位数字有变化,那么代表增加了新的参数,如果只有第3位数字有变化,仅代表更新了注释,没有增加新的参数,可更新可不更新
@@ -10,11 +10,14 @@ AutoDelCron="true"
## 在运行 ql repo 命令时,是否自动增加新的本地定时任务
AutoAddCron="true"
## 拉取脚本时默认的定时规则,当匹配不到定时规则时使用,例如: 0 9 * * *
DefaultCronRule=""
## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可
RepoFileExtensions="js py"
## 由于github仓库拉取较慢,所以会默认添加代理前缀,如不需要请移除
GithubProxyUrl="https://ghproxy.com/"
## 代理地址,支持http/https/socks,例如 http://127.0.0.1:7890
ProxyUrl=""
## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
CommandTimeoutTime="1h"
@@ -26,24 +29,28 @@ MaxConcurrentNum="5"
## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
RandomDelay="300"
## 需要随机延迟运行任务的文件后缀,直接写后缀名即可,多个后缀用空格分开,例如: js py ts
## 默认仅给javascript任务加随机延迟,其它任务按定时规则准点运行。全部任务随机延迟赋值为空
RandomDelayFileExtensions="js"
## 每小时的第几分钟准点运行任务,当在这些时间运行任务时将忽略 RandomDelay 配置,不会被随机延迟
## 默认是第0分钟和第30分钟,例如21:00或21:30分的任务将会准点运行。不需要准点运行赋值为空
RandomDelayIgnoredMinutes="0 30"
## 如果你自己会写shell脚本,并且希望在每次运行 ql update 命令时,额外运行你的 shell 脚本,请赋值为 "true",默认为true
EnableExtraShell="true"
## 自动按顺序进行账号间互助(选填) 设置为 true 时,将直接导入code最新日志来进行互助
AutoHelpOther=""
## 定义 jcode 脚本导出的互助码模板样式(选填)
## 不填 使用“按编号顺序助力模板”,Cookie编号在前的优先助力
## 填 0 使用“全部一致助力模板”,所有账户要助力的码全部一致
## 填 1 使用“均等机会助力模板”,所有账户获得助力次数一致
## 填 2 使用“随机顺序助力模板”,本套脚本内账号间随机顺序助力,每次生成的顺序都不一致。
HelpType=""
## 是否自动启动bot,默认不启动,设置为true时自动启动,目前需要自行克隆bot仓库所需代码,存到ql/repo目录下,文件夹命名为dockerbot
AutoStartBot=""
## 安装bot依赖时指定pip源,默认使用清华源,如不需要源,设置此参数为空
PipMirror="https://pypi.tuna.tsinghua.edu.cn/simple"
## 是否使用第三方bot,默认使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
BotRepoUrl=""
## 安装python依赖时指定pip源,默认使用清华源
PipMirror="https://pypi.doubanio.com/simple/"
## 安装node依赖时指定pip源,默认使用清华源
NpmMirror="https://registry.npmmirror.com"
## 通知环境变量
## 1. Server酱
@@ -54,10 +61,14 @@ export PUSH_KEY=""
## 2. BARK
## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123
export BARK_PUSH=""
## 下方填写推送图标设置,自定义推送图标(需iOS15或以上)
export BARK_ICON="http://qn.whyour.cn/logo.png"
## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声
export BARK_SOUND=""
## 下方填写推送消息分组,默认为"QingLong"
export BARK_GROUP="QingLong"
## 3. Telegram
## 3. Telegram
## 下方填写自己申请@BotFather的Token,如10xxx4:AAFcqxxxxgER5uw
export TG_BOT_TOKEN=""
## 下方填写 @getuseridbot 中获取到的纯数字ID
@@ -78,7 +89,7 @@ export TG_PROXY_AUTH=""
## 如需使用,请赋值代理地址链接,并自行解除下一行的注释
export TG_API_HOST=""
## 4. 钉钉
## 4. 钉钉
## 官方文档:https://developers.dingtalk.com/document/app/custom-robot-access
## 下方填写token后面的内容,只需 https://oapi.dingtalk.com/robot/send?access_token=XXX 等于=符号后面的XXX即可
export DD_BOT_TOKEN=""
@@ -108,103 +119,31 @@ export PUSH_PLUS_TOKEN=""
## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送
export PUSH_PLUS_USER=""
## 8. go-cqhttp
## gobot_url 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
## 9. go-cqhttp
## gobot_url 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
## gobot_token 填写在go-cqhttp文件设置的访问密钥
## gobot_qq 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
## gobot_qq 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
## go-cqhttp相关API https://docs.go-cqhttp.org/api
export GOBOT_URL=""
export GOBOT_TOKEN=""
export GOBOT_QQ=""
## 如果只是想要屏蔽某个ck不执行某个脚本,可以参考下面 case 这个命令的例子来控制,脚本名称包含后缀
## case $1 in
## test.js)
## TempBlockCookie="5"
## ;;
## esac
## 10. gotify
## gotify_url 填写gotify地址,如https://push.example.de:8080
## gotify_token 填写gotify的消息应用token
## gotify_priority 填写推送消息优先级,默认为0
export GOTIFY_URL=""
export GOTIFY_TOKEN=""
export GOTIFY_PRIORITY=0
## 需组合的环境变量列表,env_name需要和var_name一一对应,如何有新活动按照格式添加(不懂勿动)
env_name=(
JD_COOKIE
FRUITSHARECODES
PETSHARECODES
PLANT_BEAN_SHARECODES
DREAM_FACTORY_SHARE_CODES
DDFACTORY_SHARECODES
JDZZ_SHARECODES
JDJOY_SHARECODES
JXNC_SHARECODES
BOOKSHOP_SHARECODES
JD_CASH_SHARECODES
JDSGMH_SHARECODES
JDCFD_SHARECODES
JDHEALTH_SHARECODES
)
var_name=(
Cookie
ForOtherFruit
ForOtherPet
ForOtherBean
ForOtherDreamFactory
ForOtherJdFactory
ForOtherJdzz
ForOtherJoy
ForOtherJxnc
ForOtherBookShop
ForOtherCash
ForOtherSgmh
ForOtherCfd
ForOtherHealth
)
## 11. PushDeer
## deer_key 填写PushDeer的key
export DEER_KEY=""
## name_js为脚本文件名,如果使用ql repo命令拉取,文件名含有作者名
## 所有有互助码的活动,把脚本名称列在 name_js 中,对应 config.sh 中互助码后缀列在 name_config 中,中文名称列在 name_chinese 中。
## name_js、name_config 和 name_chinese 中的三个名称必须一一对应。
name_js=(
jd_fruit
jd_pet
jd_plantBean
jd_dreamFactory
jd_jdfactory
jd_jdzz
jd_crazy_joy
jd_jxnc
jd_bookshop
jd_cash
jd_sgmh
jd_cfd
jd_health
)
name_config=(
Fruit
Pet
Bean
DreamFactory
JdFactory
Jdzz
Joy
Jxnc
BookShop
Cash
Sgmh
Cfd
Health
)
name_chinese=(
东东农场
东东萌宠
京东种豆得豆
京喜工厂
东东工厂
京东赚赚
crazyJoy任务
京喜农场
口袋书店
签到领现金
闪购盲盒
京喜财富岛
东东健康社区
)
## 12. Chat
## chat_url 填写synology chat地址,http://IP:PORT/webapi/***token=
## chat_token 填写后面的token
export CHAT_URL=""
export CHAT_TOKEN=""
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+2
View File
@@ -1,3 +1,5 @@
#!/usr/bin/env bash
## 添加你需要重启自动执行的任意命令,比如 ql repo
## 安装node依赖使用 pnpm add -g xxx xxx
## 安装python依赖使用 pip3 install xxx
+199 -11
View File
@@ -13,25 +13,47 @@
const querystring = require('querystring');
const $ = new Env();
const timeout = 15000; //超时时间(单位毫秒)
// =======================================gotify通知设置区域==============================================
//gotify_url 填写gotify地址,如https://push.example.de:8080
//gotify_token 填写gotify的消息应用token
//gotify_priority 填写推送消息优先级,默认为0
let GOTIFY_URL = '';
let GOTIFY_TOKEN = '';
let GOTIFY_PRIORITY = 0;
// =======================================go-cqhttp通知设置区域===========================================
//gobot_url 填写请求地址http://127.0.0.1/send_private_msg
//gobot_token 填写在go-cqhttp文件设置的访问密钥
//gobot_qq 填写推送到个人QQ或者QQ群号
//go-cqhttp相关API https://docs.go-cqhttp.org/api
let GOBOT_URL = ''; // 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
let GOBOT_URL = ''; // 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
let GOBOT_TOKEN = ''; //访问密钥
let GOBOT_QQ = ''; // 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
let GOBOT_QQ = ''; // 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
// =======================================微信server酱通知设置区域===========================================
//此处填你申请的SCKEY.
//(环境变量名 PUSH_KEY)
let SCKEY = '';
// =======================================PushDeer通知设置区域===========================================
//此处填你申请的PushDeer KEY.
//(环境变量名 DEER_KEY)
let PUSHDEER_KEY = '';
// =======================================Synology Chat通知设置区域===========================================
//此处填你申请的CHAT_URL与CHAT_TOKEN
//(环境变量名 CHAT_URL CHAT_TOKEN)
let CHAT_URL = '';
let CHAT_TOKEN = '';
// =======================================Bark App通知设置区域===========================================
//此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX)
let BARK_PUSH = '';
//BARK app推送图标,自定义推送图标(需iOS15或以上)
let BARK_ICON = 'http://qn.whyour.cn/logo.png';
//BARK app推送铃声,铃声列表去APP查看复制填写
let BARK_SOUND = '';
//BARK app推送消息的分组, 默认为"QingLong"
let BARK_GROUP = 'QingLong';
// =======================================telegram机器人通知设置区域===========================================
//此处填你telegram bot 的Tokentelegram机器人通知推送必填项.例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw
@@ -81,7 +103,24 @@ let IGOT_PUSH_KEY = '';
let PUSH_PLUS_TOKEN = '';
let PUSH_PLUS_USER = '';
// =======================================Cool Push设置区域=======================================
//官方文档:https://cp.xuthus.cc/docs
//QQ_SKEY: Cool Push登录授权后推送消息的调用代码Skey
//QQ_MODE: 推送模式详情请登录获取QQ_SKEY后见https://cp.xuthus.cc/feat
let QQ_SKEY = '';
let QQ_MODE = '';
//==========================云端环境变量的判断与接收=========================
if (process.env.GOTIFY_URL) {
GOTIFY_URL = process.env.GOTIFY_URL;
}
if (process.env.GOTIFY_TOKEN) {
GOTIFY_TOKEN = process.env.GOTIFY_TOKEN;
}
if (process.env.GOTIFY_PRIORITY) {
GOTIFY_PRIORITY = process.env.GOTIFY_PRIORITY;
}
if (process.env.GOBOT_URL) {
GOBOT_URL = process.env.GOBOT_URL;
}
@@ -96,6 +135,18 @@ if (process.env.PUSH_KEY) {
SCKEY = process.env.PUSH_KEY;
}
if (process.env.DEER_KEY) {
PUSHDEER_KEY = process.env.DEER_KEY;
}
if (process.env.CHAT_URL) {
CHAT_URL = process.env.CHAT_URL;
}
if (process.env.CHAT_TOKEN) {
CHAT_TOKEN = process.env.CHAT_TOKEN;
}
if (process.env.QQ_SKEY) {
QQ_SKEY = process.env.QQ_SKEY;
}
@@ -114,9 +165,15 @@ if (process.env.BARK_PUSH) {
} else {
BARK_PUSH = `https://api.day.app/${process.env.BARK_PUSH}`;
}
if (process.env.BARK_ICON) {
BARK_ICON = process.env.BARK_ICON;
}
if (process.env.BARK_SOUND) {
BARK_SOUND = process.env.BARK_SOUND;
}
if (process.env.BARK_GROUP) {
BARK_GROUP = process.env.BARK_GROUP;
}
} else {
if (
BARK_PUSH &&
@@ -194,18 +251,58 @@ async function sendNotify(
qywxBotNotify(text, desp), //企业微信机器人
qywxamNotify(text, desp), //企业微信应用消息推送
iGotNotify(text, desp, params), //iGot
gobotNotify(text, desp),//go-cqhttp
gobotNotify(text, desp), //go-cqhttp
gotifyNotify(text, desp), //gotify
ChatNotify(text, desp), //synolog chat
PushDeerNotify(text, desp), //PushDeer
]);
}
function gotifyNotify(text, desp) {
return new Promise((resolve) => {
if (GOTIFY_URL && GOTIFY_TOKEN) {
const options = {
url: `${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}`,
body: `title=${encodeURIComponent(text)}&message=${encodeURIComponent(
desp,
)}&priority=${GOTIFY_PRIORITY}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('gotify发送通知调用API失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
if (data.id) {
console.log('gotify发送通知消息成功🎉\n');
} else {
console.log(`${data.message}\n`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve();
}
});
} else {
resolve();
}
});
}
function gobotNotify(text, desp, time = 2100) {
return new Promise((resolve) => {
if (GOBOT_URL) {
const options = {
url: `${GOBOT_URL}?access_token=${GOBOT_TOKEN}&${GOBOT_QQ}`,
body: `message=${text}\n${desp}`,
json: { message: `${text}\n${desp}` },
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Type': 'application/json',
},
timeout,
};
@@ -288,6 +385,91 @@ function serverNotify(text, desp, time = 2100) {
});
}
function PushDeerNotify(text, desp) {
return new Promise((resolve) => {
if (PUSHDEER_KEY) {
// PushDeer 建议对消息内容进行 urlencode
desp = encodeURI(desp);
const options = {
url: `https://api2.pushdeer.com/message/push`,
body: `pushkey=${PUSHDEER_KEY}&text=${text}&desp=${desp}&type=markdown`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
timeout,
};
$.post(
options,
(err, resp, data) => {
try {
if (err) {
console.log('发送通知调用API失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
// 通过返回的result的长度来判断是否成功
if (
data.content.result.length !== undefined &&
data.content.result.length > 0
) {
console.log('PushDeer发送通知消息成功🎉\n');
} else {
console.log(
`PushDeer发送通知消息异常\n${JSON.stringify(data)}`,
);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
},
time,
);
} else {
resolve();
}
});
}
function ChatNotify(text, desp) {
return new Promise((resolve) => {
if (CHAT_URL && CHAT_TOKEN) {
// 对消息内容进行 urlencode
desp = encodeURI(desp);
const options = {
url: `${CHAT_URL}${CHAT_TOKEN}`,
body: `payload={"text":"${text}\n${desp}"}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('发送通知调用API失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
if (data.success) {
console.log('Chat发送通知消息成功🎉\n');
} else {
console.log(`Chat发送通知消息异常\n${JSON.stringify(data)}`);
}
}
} catch (e) {
$.logErr(e);
} finally {
resolve(data);
}
});
} else {
resolve();
}
});
}
function CoolPush(text, desp) {
return new Promise((resolve) => {
if (QQ_SKEY) {
@@ -371,7 +553,9 @@ function BarkNotify(text, desp, params = {}) {
const options = {
url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent(
desp,
)}?sound=${BARK_SOUND}&${querystring.stringify(params)}`,
)}?icon=${BARK_ICON}?sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(
params,
)}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
@@ -407,9 +591,13 @@ function tgBotNotify(text, desp) {
if (TG_BOT_TOKEN && TG_USER_ID) {
const options = {
url: `https://${TG_API_HOST}/bot${TG_BOT_TOKEN}/sendMessage`,
body: `chat_id=${TG_USER_ID}&text=${text}\n\n${desp}&disable_web_page_preview=true`,
json: {
chat_id: `${TG_USER_ID}`,
text: `${text}\n\n${desp}`,
disable_web_page_preview: true,
},
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Type': 'application/json',
},
timeout,
};
@@ -597,9 +785,9 @@ function qywxamNotify(text, desp) {
timeout,
};
$.post(options_accesstoken, (err, resp, data) => {
html = desp.replace(/\n/g, '<br/>');
var json = JSON.parse(data);
accesstoken = json.access_token;
let html = desp.replace(/\n/g, '<br/>');
let json = JSON.parse(data);
let accesstoken = json.access_token;
let options;
switch (QYWX_AM_AY[4]) {
+552 -240
View File
@@ -1,277 +1,589 @@
#!/usr/bin/env python3
# _*_ coding:utf-8 _*_
import sys
import os
cur_path = os.path.abspath(os.path.dirname(__file__))
root_path = os.path.split(cur_path)[0]
sys.path.append(root_path)
import requests
import json
import traceback
import time
import hmac
import hashlib
import base64
import urllib.parse
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
import hashlib
import hmac
import json
import os
import re
import threading
import time
import urllib.parse
import requests
# 原先的 print 函数和主线程的锁
_print = print
mutex = threading.Lock()
# 定义新的 print 函数
def print(text, *args, **kw):
"""
使输出有序进行,不出现多线程同一时间输出导致错乱的问题。
"""
with mutex:
_print(text, *args, **kw)
# 通知服务
BARK = '' # bark服务,此参数如果以http或者https开头则判定为自建bark服务; secrets可填;
SCKEY = '' # Server酱的SCKEY; secrets可填
TG_BOT_TOKEN = '' # tg机器人的TG_BOT_TOKEN; secrets可填
TG_USER_ID = '' # tg机器人的TG_USER_ID; secrets可填
TG_PROXY_IP = '' # tg机器人的TG_PROXY_IP; secrets可填
TG_PROXY_PORT = '' # tg机器人的TG_PROXY_PORT; secrets可填
DD_BOT_ACCESS_TOKEN = '' # 钉钉机器人的DD_BOT_ACCESS_TOKEN; secrets可填
DD_BOT_SECRET = '' # 钉钉机器人的DD_BOT_SECRET; secrets可填
QYWX_APP = '' # 企业微信应用的QYWX_APP; secrets可填 参考http://note.youdao.com/s/HMiudGkb
# fmt: off
push_config = {
'HITOKOTO': False, # 启用一言(随机句子)
notify_mode = []
'BARK_PUSH': '', # bark IP 或设备码,例:https://api.day.app/DxHcxxxxxRxxxxxxcm/
'BARK_ARCHIVE': '', # bark 推送是否存档
'BARK_GROUP': '', # bark 推送分组
'BARK_SOUND': '', # bark 推送声音
'BARK_ICON': '', # bark 推送图标
# GitHub action运行需要填写对应的secrets
if "BARK" in os.environ and os.environ["BARK"]:
BARK = os.environ["BARK"]
if "SCKEY" in os.environ and os.environ["SCKEY"]:
SCKEY = os.environ["SCKEY"]
if "TG_BOT_TOKEN" in os.environ and os.environ["TG_BOT_TOKEN"] and "TG_USER_ID" in os.environ and os.environ["TG_USER_ID"]:
TG_BOT_TOKEN = os.environ["TG_BOT_TOKEN"]
TG_USER_ID = os.environ["TG_USER_ID"]
if "DD_BOT_ACCESS_TOKEN" in os.environ and os.environ["DD_BOT_ACCESS_TOKEN"] and "DD_BOT_SECRET" in os.environ and os.environ["DD_BOT_SECRET"]:
DD_BOT_ACCESS_TOKEN = os.environ["DD_BOT_ACCESS_TOKEN"]
DD_BOT_SECRET = os.environ["DD_BOT_SECRET"]
if "QYWX_APP" in os.environ and os.environ["QYWX_APP"]:
QYWX_APP = os.environ["QYWX_APP"]
'CONSOLE': True, # 控制台输出
if BARK:
notify_mode.append('bark')
print("BARK 推送打开")
if SCKEY:
notify_mode.append('sc_key')
print("Server酱 推送打开")
if TG_BOT_TOKEN and TG_USER_ID:
notify_mode.append('telegram_bot')
print("Telegram 推送打开")
if DD_BOT_ACCESS_TOKEN and DD_BOT_SECRET:
notify_mode.append('dingding_bot')
print("钉钉机器人 推送打开")
if QYWX_APP:
notify_mode.append('qywxapp_bot')
print("企业微信应用 推送打开")
'DD_BOT_SECRET': '', # 钉钉机器人的 DD_BOT_SECRET
'DD_BOT_TOKEN': '', # 钉钉机器人的 DD_BOT_TOKEN
def bark(title, content):
print("\n")
if not BARK:
print("bark服务的bark_token未设置!!\n取消推送")
'FSKEY': '', # 飞书机器人的 FSKEY
'GOBOT_URL': '', # go-cqhttp
# 推送到个人QQhttp://127.0.0.1/send_private_msg
# 群:http://127.0.0.1/send_group_msg
'GOBOT_QQ': '', # go-cqhttp 的推送群或用户
# GOBOT_URL 设置 /send_private_msg 时填入 user_id=个人QQ
# /send_group_msg 时填入 group_id=QQ群
'GOBOT_TOKEN': '', # go-cqhttp 的 access_token
'GOTIFY_URL': '', # gotify地址,如https://push.example.de:8080
'GOTIFY_TOKEN': '', # gotify的消息应用token
'GOTIFY_PRIORITY': 0, # 推送消息优先级,默认为0
'IGOT_PUSH_KEY': '', # iGot 聚合推送的 IGOT_PUSH_KEY
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY
'CHAT_URL': '', # synology chat url
'CHAT_TOKEN': '', # synology chat token
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
'QMSG_KEY': '', # qmsg 酱的 QMSG_KEY
'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE
'QYWX_AM': '', # 企业微信应用
'QYWX_KEY': '', # 企业微信机器人
'TG_BOT_TOKEN': '', # tg 机器人的 TG_BOT_TOKEN,例:1407203283:AAG9rt-6RDaaX0HBLZQq0laNOh898iFYaRQ
'TG_USER_ID': '', # tg 机器人的 TG_USER_ID,例:1434078534
'TG_API_HOST': '', # tg 代理 api
'TG_PROXY_AUTH': '', # tg 代理认证参数
'TG_PROXY_HOST': '', # tg 机器人的 TG_PROXY_HOST
'TG_PROXY_PORT': '', # tg 机器人的 TG_PROXY_PORT
}
notify_function = []
# fmt: on
# 首先读取 面板变量 或者 github action 运行变量
for k in push_config:
if os.getenv(k):
v = os.getenv(k)
push_config[k] = v
def bark(title: str, content: str) -> None:
"""
使用 bark 推送消息。
"""
if not push_config.get("BARK_PUSH"):
print("bark 服务的 BARK_PUSH 未设置!!\n取消推送")
return
print("bark服务启动")
url = None
if BARK.startswith('http'):
url = f"""{BARK}/{title}/{content}"""
print("bark 服务启动")
if push_config.get("BARK_PUSH").startswith("http"):
url = f'{push_config.get("BARK_PUSH")}/{urllib.parse.quote_plus(title)}/{urllib.parse.quote_plus(content)}'
else:
url = f"""https://api.day.app/{BARK}/{title}/{content}"""
url = f'https://api.day.app/{push_config.get("BARK_PUSH")}/{urllib.parse.quote_plus(title)}/{urllib.parse.quote_plus(content)}'
bark_params = {
"BARK_ARCHIVE": "isArchive",
"BARK_GROUP": "group",
"BARK_SOUND": "sound",
"BARK_ICON": "icon",
}
params = ""
for pair in filter(
lambda pairs: pairs[0].startswith("BARK_")
and pairs[0] != "BARK_PUSH"
and pairs[1]
and bark_params.get(pairs[0]),
push_config.items(),
):
params += f"{bark_params.get(pair[0])}={pair[1]}&"
if params:
url = url + "?" + params.rstrip("&")
response = requests.get(url).json()
if response['code'] == 200:
print('推送成功!')
else:
print('推送失败!')
def serverJ(title, content):
print("\n")
if not SCKEY:
print("server酱服务的SCKEY未设置!!\n取消推送")
if response["code"] == 200:
print("bark 推送成功!")
else:
print("bark 推送失败!")
def console(title: str, content: str) -> None:
"""
使用 控制台 推送消息。
"""
print(f"{title}\n\n{content}")
def dingding_bot(title: str, content: str) -> None:
"""
使用 钉钉机器人 推送消息。
"""
if not push_config.get("DD_BOT_SECRET") or not push_config.get("DD_BOT_TOKEN"):
print("钉钉机器人 服务的 DD_BOT_SECRET 或者 DD_BOT_TOKEN 未设置!!\n取消推送")
return
print("serverJ服务启动")
print("钉钉机器人 服务启动")
timestamp = str(round(time.time() * 1000))
secret_enc = push_config.get("DD_BOT_SECRET").encode("utf-8")
string_to_sign = "{}\n{}".format(timestamp, push_config.get("DD_BOT_SECRET"))
string_to_sign_enc = string_to_sign.encode("utf-8")
hmac_code = hmac.new(
secret_enc, string_to_sign_enc, digestmod=hashlib.sha256
).digest()
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
url = f'https://oapi.dingtalk.com/robot/send?access_token={push_config.get("DD_BOT_TOKEN")}&timestamp={timestamp}&sign={sign}'
headers = {"Content-Type": "application/json;charset=utf-8"}
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
response = requests.post(
url=url, data=json.dumps(data), headers=headers, timeout=15
).json()
if not response["errcode"]:
print("钉钉机器人 推送成功!")
else:
print("钉钉机器人 推送失败!")
def feishu_bot(title: str, content: str) -> None:
"""
使用 飞书机器人 推送消息。
"""
if not push_config.get("FSKEY"):
print("飞书 服务的 FSKEY 未设置!!\n取消推送")
return
print("飞书 服务启动")
url = f'https://open.feishu.cn/open-apis/bot/v2/hook/{push_config.get("FSKEY")}'
data = {"msg_type": "text", "content": {"text": f"{title}\n\n{content}"}}
response = requests.post(url, data=json.dumps(data)).json()
if response.get("StatusCode") == 0:
print("飞书 推送成功!")
else:
print("飞书 推送失败!错误信息如下:\n", response)
def go_cqhttp(title: str, content: str) -> None:
"""
使用 go_cqhttp 推送消息。
"""
if not push_config.get("GOBOT_URL") or not push_config.get("GOBOT_QQ"):
print("go-cqhttp 服务的 GOBOT_URL 或 GOBOT_QQ 未设置!!\n取消推送")
return
print("go-cqhttp 服务启动")
url = f'{push_config.get("GOBOT_URL")}?access_token={push_config.get("GOBOT_TOKEN")}&{push_config.get("GOBOT_QQ")}&message=标题:{title}\n内容:{content}'
response = requests.get(url).json()
if response["status"] == "ok":
print("go-cqhttp 推送成功!")
else:
print("go-cqhttp 推送失败!")
def gotify(title:str,content:str) -> None:
"""
使用 gotify 推送消息。
"""
if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"):
print("gotify 服务的 GOTIFY_URL 或 GOTIFY_TOKEN 未设置!!\n取消推送")
return
print("gotify 服务启动")
url = f'{push_config.get("GOTIFY_URL")}/message?token={push_config.get("GOTIFY_TOKEN")}'
data = {"title": title,"message": content,"priority": push_config.get("GOTIFY_PRIORITY")}
response = requests.post(url,data=data).json()
if response.get("id"):
print("gotify 推送成功!")
else:
print("gotify 推送失败!")
def iGot(title: str, content: str) -> None:
"""
使用 iGot 推送消息。
"""
if not push_config.get("IGOT_PUSH_KEY"):
print("iGot 服务的 IGOT_PUSH_KEY 未设置!!\n取消推送")
return
print("iGot 服务启动")
url = f'https://push.hellyw.com/{push_config.get("IGOT_PUSH_KEY")}'
data = {"title": title, "content": content}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
response = requests.post(url, data=data, headers=headers).json()
if response["ret"] == 0:
print("iGot 推送成功!")
else:
print(f'iGot 推送失败!{response["errMsg"]}')
def serverJ(title: str, content: str) -> None:
"""
通过 serverJ 推送消息。
"""
if not push_config.get("PUSH_KEY"):
print("serverJ 服务的 PUSH_KEY 未设置!!\n取消推送")
return
print("serverJ 服务启动")
data = {"text": title, "desp": content.replace("\n", "\n\n")}
if push_config.get("PUSH_KEY").index("SCT") != -1:
url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send'
else:
url = f'https://sc.ftqq.com/${push_config.get("PUSH_KEY")}.send'
response = requests.post(url, data=data).json()
if response.get("errno") == 0 or response.get("code") == 0:
print("serverJ 推送成功!")
else:
print(f'serverJ 推送失败!错误码:{response["message"]}')
def pushdeer(title: str, content: str) -> None:
"""
通过PushDeer 推送消息
"""
if not push_config.get("DEER_KEY"):
print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送")
return
print("PushDeer 服务启动")
data = {"text": title, "desp": content, "type": "markdown", "pushkey": push_config.get("DEER_KEY")}
url = 'https://api2.pushdeer.com/message/push'
response = requests.post(url, data=data).json()
if len(response.get("content").get("result")) > 0:
print("PushDeer 推送成功!")
else:
print("PushDeer 推送失败!错误信息:", response)
def chat(title: str, content: str) -> None:
"""
通过Chat 推送消息
"""
if not push_config.get("CHAT_URL") or not push_config.get("CHAT_TOKEN"):
print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送")
return
print("chat 服务启动")
data = 'payload=' + json.dumps({'text': title + '\n' + content})
url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN")
response = requests.post(url, data=data)
if response.status_code == 200:
print("Chat 推送成功!")
else:
print("Chat 推送失败!错误信息:", response)
def pushplus_bot(title: str, content: str) -> None:
"""
通过 push+ 推送消息。
"""
if not push_config.get("PUSH_PLUS_TOKEN"):
print("PUSHPLUS 服务的 PUSH_PLUS_TOKEN 未设置!!\n取消推送")
return
print("PUSHPLUS 服务启动")
url = "http://www.pushplus.plus/send"
data = {
"text": title,
"desp": content.replace("\n", "\n\n")
"token": push_config.get("PUSH_PLUS_TOKEN"),
"title": title,
"content": content,
"topic": push_config.get("PUSH_PLUS_USER"),
}
response = requests.post(f"https://sc.ftqq.com/{SCKEY}.send", data=data).json()
if response['errno'] == 0:
print('推送成功!')
else:
print('推送失败!')
body = json.dumps(data).encode(encoding="utf-8")
headers = {"Content-Type": "application/json"}
response = requests.post(url=url, data=body, headers=headers).json()
def telegram_bot(title, content):
print("\n")
bot_token = TG_BOT_TOKEN
user_id = TG_USER_ID
if not bot_token or not user_id:
print("tg服务的bot_token或者user_id未设置!!\n取消推送")
if response["code"] == 200:
print("PUSHPLUS 推送成功!")
else:
url_old = "http://pushplus.hxtrip.com/send"
headers["Accept"] = "application/json"
response = requests.post(url=url_old, data=body, headers=headers).json()
if response["code"] == 200:
print("PUSHPLUS(hxtrip) 推送成功!")
else:
print("PUSHPLUS 推送失败!")
def qmsg_bot(title: str, content: str) -> None:
"""
使用 qmsg 推送消息。
"""
if not push_config.get("QMSG_KEY") or not push_config.get("QMSG_TYPE"):
print("qmsg 的 QMSG_KEY 或者 QMSG_TYPE 未设置!!\n取消推送")
return
print("tg服务启动")
url=f"https://api.telegram.org/bot{TG_BOT_TOKEN}/sendMessage"
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
payload = {'chat_id': str(TG_USER_ID), 'text': f'{title}\n\n{content}', 'disable_web_page_preview': 'true'}
proxies = None
if TG_PROXY_IP and TG_PROXY_PORT:
proxyStr = "http://{}:{}".format(TG_PROXY_IP, TG_PROXY_PORT)
proxies = {"http": proxyStr, "https": proxyStr}
response = requests.post(url=url, headers=headers, params=payload, proxies=proxies).json()
if response['ok']:
print('推送成功!')
else:
print('推送失败!')
print("qmsg 服务启动")
def dingding_bot(title, content):
timestamp = str(round(time.time() * 1000)) # 时间戳
secret_enc = DD_BOT_SECRET.encode('utf-8')
string_to_sign = '{}\n{}'.format(timestamp, DD_BOT_SECRET)
string_to_sign_enc = string_to_sign.encode('utf-8')
hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code)) # 签名
print('开始使用 钉钉机器人 推送消息...', end='')
url = f'https://oapi.dingtalk.com/robot/send?access_token={DD_BOT_ACCESS_TOKEN}&timestamp={timestamp}&sign={sign}'
headers = {'Content-Type': 'application/json;charset=utf-8'}
data = {
'msgtype': 'text',
'text': {'content': f'{title}\n\n{content}'}
}
response = requests.post(url=url, data=json.dumps(data), headers=headers, timeout=15).json()
if not response['errcode']:
print('推送成功!')
else:
print('推送失败!')
url = f'https://qmsg.zendee.cn/{push_config.get("QMSG_TYPE")}/{push_config.get("QMSG_KEY")}'
payload = {"msg": f'{title}\n\n{content.replace("----", "-")}'.encode("utf-8")}
response = requests.post(url=url, params=payload).json()
def qywxapp_bot(title, content):
print("\n")
if not QYWX_APP:
print("企业微信应用的QYWX_APP未设置!!\n取消推送")
if response["code"] == 0:
print("qmsg 推送成功!")
else:
print(f'qmsg 推送失败!{response["reason"]}')
def wecom_app(title: str, content: str) -> None:
"""
通过 企业微信 APP 推送消息。
"""
if not push_config.get("QYWX_AM"):
print("QYWX_AM 未设置!!\n取消推送")
return
print("企业微信应用启动")
qywx_app_params = QYWX_APP.split(',')
url='https://qyapi.weixin.qq.com/cgi-bin/gettoken'
headers= {
'Content-Type': 'application/json',
}
payload = {
'corpid': qywx_app_params[0],
'corpsecret': qywx_app_params[1],
}
response = requests.post(url=url, headers=headers, data=json.dumps(payload), timeout=15).json()
accesstoken = response["access_token"]
html = content.replace("\n", "<br/>")
QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM"))
if 4 < len(QYWX_AM_AY) > 5:
print("QYWX_AM 设置错误!!\n取消推送")
return
print("企业微信 APP 服务启动")
options = None
if not qywx_app_params[4]:
options = {
'msgtype': 'text',
'text': {
content: f'{title}\n\n${content}'
}
}
elif qywx_app_params[4] == '0':
options = {
'msgtype': 'textcard',
'textcard': {
title: f'{title}',
description: f'{content}',
btntxt: '更多'
}
}
elif qywx_app_params[4] == '1':
options = {
'msgtype': 'text',
'text': {
content: f'{title}\n\n${content}'
}
}
corpid = QYWX_AM_AY[0]
corpsecret = QYWX_AM_AY[1]
touser = QYWX_AM_AY[2]
agentid = QYWX_AM_AY[3]
try:
media_id = QYWX_AM_AY[4]
except IndexError:
media_id = ""
wx = WeCom(corpid, corpsecret, agentid)
# 如果没有配置 media_id 默认就以 text 方式发送
if not media_id:
message = title + "\n\n" + content
response = wx.send_text(message, touser)
else:
options = {
'msgtype': 'mpnews',
'mpnews': {
'articles': [
response = wx.send_mpnews(title, content, media_id, touser)
if response == "ok":
print("企业微信推送成功!")
else:
print("企业微信推送失败!错误信息如下:\n", response)
class WeCom:
def __init__(self, corpid, corpsecret, agentid):
self.CORPID = corpid
self.CORPSECRET = corpsecret
self.AGENTID = agentid
def get_access_token(self):
url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
values = {
"corpid": self.CORPID,
"corpsecret": self.CORPSECRET,
}
req = requests.post(url, params=values)
data = json.loads(req.text)
return data["access_token"]
def send_text(self, message, touser="@all"):
send_url = (
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
+ self.get_access_token()
)
send_values = {
"touser": touser,
"msgtype": "text",
"agentid": self.AGENTID,
"text": {"content": message},
"safe": "0",
}
send_msges = bytes(json.dumps(send_values), "utf-8")
respone = requests.post(send_url, send_msges)
respone = respone.json()
return respone["errmsg"]
def send_mpnews(self, title, message, media_id, touser="@all"):
send_url = (
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
+ self.get_access_token()
)
send_values = {
"touser": touser,
"msgtype": "mpnews",
"agentid": self.AGENTID,
"mpnews": {
"articles": [
{
'title': f'{title}',
'thumb_media_id': f'{qywx_app_params[4]}',
'author': '智能助手',
'content_source_url': '',
'content': f'{html}',
'digest': f'{content}'
"title": title,
"thumb_media_id": media_id,
"author": "Author",
"content_source_url": "",
"content": message.replace("\n", "<br/>"),
"digest": message,
}
]
}
},
}
send_msges = bytes(json.dumps(send_values), "utf-8")
respone = requests.post(send_url, send_msges)
respone = respone.json()
return respone["errmsg"]
url=f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={accesstoken}"
data = {
'touser': f'{change_user_id(content)}',
'agentid': f'{qywx_app_params[3]}',
'safe': '0'
}
data.update(options)
headers = {
'Content-Type': 'application/json',
}
response = requests.post(url=url, headers=headers, data=json.dumps(data)).json()
if response['errcode'] == 0:
print('推送成功!')
else:
print('推送失败!')
def change_user_id(desp):
qywx_app_params = QYWX_APP.split(',')
if qywx_app_params[2]:
userIdTmp = qywx_app_params[2].split("|")
userId = ""
for i in range(len(userIdTmp)):
count1 = f"账号{i + 1}"
count2 = f"签到号{i + 1}"
if re.search(count1, desp) or re.search(count2, desp):
userId = userIdTmp[i]
if not userId:
userId = qywx_app_params[2]
return userId
else:
return "@all"
def send(title, content):
def wecom_bot(title: str, content: str) -> None:
"""
使用 bark, telegram bot, dingding bot, serverJ 发送手机推送
:param title:
:param content:
通过 企业微信机器人 推送消息。
"""
if not push_config.get("QYWX_KEY"):
print("企业微信机器人 服务的 QYWX_KEY 未设置!!\n取消推送")
return
print("企业微信机器人服务启动")
url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={push_config.get('QYWX_KEY')}"
headers = {"Content-Type": "application/json;charset=utf-8"}
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
response = requests.post(
url=url, data=json.dumps(data), headers=headers, timeout=15
).json()
if response["errcode"] == 0:
print("企业微信机器人推送成功!")
else:
print("企业微信机器人推送失败!")
def telegram_bot(title: str, content: str) -> None:
"""
使用 telegram 机器人 推送消息。
"""
if not push_config.get("TG_BOT_TOKEN") or not push_config.get("TG_USER_ID"):
print("tg 服务的 bot_token 或者 user_id 未设置!!\n取消推送")
return
print("tg 服务启动")
if push_config.get("TG_API_HOST"):
url = f"https://{push_config.get('TG_API_HOST')}/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
else:
url = (
f"https://api.telegram.org/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
)
headers = {"Content-Type": "application/x-www-form-urlencoded"}
payload = {
"chat_id": str(push_config.get("TG_USER_ID")),
"text": f"{title}\n\n{content}",
"disable_web_page_preview": "true",
}
proxies = None
if push_config.get("TG_PROXY_HOST") and push_config.get("TG_PROXY_PORT"):
if push_config.get("TG_PROXY_AUTH") is not None and "@" not in push_config.get(
"TG_PROXY_HOST"
):
push_config["TG_PROXY_HOST"] = (
push_config.get("TG_PROXY_AUTH")
+ "@"
+ push_config.get("TG_PROXY_HOST")
)
proxyStr = "http://{}:{}".format(
push_config.get("TG_PROXY_HOST"), push_config.get("TG_PROXY_PORT")
)
proxies = {"http": proxyStr, "https": proxyStr}
response = requests.post(
url=url, headers=headers, params=payload, proxies=proxies
).json()
if response["ok"]:
print("tg 推送成功!")
else:
print("tg 推送失败!")
def one() -> str:
"""
获取一条一言。
:return:
"""
for i in notify_mode:
if i == 'bark':
if BARK:
bark(title=title, content=content)
else:
print('未启用 bark')
continue
if i == 'sc_key':
if SCKEY:
serverJ(title=title, content=content)
else:
print('未启用 Server酱')
continue
elif i == 'dingding_bot':
if DD_BOT_ACCESS_TOKEN and DD_BOT_SECRET:
dingding_bot(title=title, content=content)
else:
print('未启用 钉钉机器人')
continue
elif i == 'telegram_bot':
if TG_BOT_TOKEN and TG_USER_ID:
telegram_bot(title=title, content=content)
else:
print('未启用 telegram机器人')
continue
elif i == 'qywxapp_bot':
if QYWX_APP:
qywxapp_bot(title=title, content=content)
else:
print('未启用 企业微信应用推送')
continue
else:
print('此类推送方式不存在')
url = "https://v1.hitokoto.cn/"
res = requests.get(url).json()
return res["hitokoto"] + " ----" + res["from"]
if push_config.get("BARK_PUSH"):
notify_function.append(bark)
if push_config.get("CONSOLE"):
notify_function.append(console)
if push_config.get("DD_BOT_TOKEN") and push_config.get("DD_BOT_SECRET"):
notify_function.append(dingding_bot)
if push_config.get("FSKEY"):
notify_function.append(feishu_bot)
if push_config.get("GOBOT_URL") and push_config.get("GOBOT_QQ"):
notify_function.append(go_cqhttp)
if push_config.get("GOTIFY_URL") and push_config.get("GOTIFY_TOKEN"):
notify_function.append(gotify)
if push_config.get("IGOT_PUSH_KEY"):
notify_function.append(iGot)
if push_config.get("PUSH_KEY"):
notify_function.append(serverJ)
if push_config.get("DEER_KEY"):
notify_function.append(pushdeer)
if push_config.get("CHAT_URL") and push_config.get("CHAT_TOKEN"):
notify_function.append(chat)
if push_config.get("PUSH_PLUS_TOKEN"):
notify_function.append(pushplus_bot)
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
notify_function.append(qmsg_bot)
if push_config.get("QYWX_AM"):
notify_function.append(wecom_app)
if push_config.get("QYWX_KEY"):
notify_function.append(wecom_bot)
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
notify_function.append(telegram_bot)
def send(title: str, content: str) -> None:
if not content:
print(f"{title} 推送内容为空!")
return
hitokoto = push_config.get("HITOKOTO")
text = one() if hitokoto else ""
content += "\n\n" + text
ts = [
threading.Thread(target=mode, args=(title, content), name=mode.__name__)
for mode in notify_function
]
[t.start() for t in ts]
[t.join() for t in ts]
def main():
send('title', 'content')
send("title", "content")
if __name__ == '__main__':
main()
if __name__ == "__main__":
main()
+1 -1
View File
@@ -5,8 +5,8 @@
"dependencies": {
"crypto-js": "^4.0.0",
"download": "^8.0.0",
"got": "^11.5.1",
"http-server": "^0.12.3",
"got": "^11.5.1",
"qrcode-terminal": "^0.12.0",
"request": "^2.88.2",
"tough-cookie": "^4.0.0",
-1
View File
@@ -1 +0,0 @@
requests
+1
View File
@@ -0,0 +1 @@
#!/usr/bin/env bash
+40
View File
@@ -0,0 +1,40 @@
import * as qiniu from 'qiniu';
import dotenv from 'dotenv';
const envFound = dotenv.config();
const accessKey = process.env.QINIU_AK;
const secretKey = process.env.QINIU_SK;
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
const key = 'version.ts';
const options = {
scope: `${process.env.QINIU_SCOPE}:${key}`,
};
const putPolicy = new qiniu.rs.PutPolicy(options);
const uploadToken = putPolicy.uploadToken(mac);
const localFile = 'src/version.ts';
const config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z1 });
const formUploader = new qiniu.form_up.FormUploader(config);
const putExtra = new qiniu.form_up.PutExtra(
'',
{},
'text/plain; charset=utf-8',
);
// 文件上传
formUploader.putFile(
uploadToken,
key,
localFile,
putExtra,
function (respErr, respBody, respInfo) {
if (respErr) {
throw respErr;
}
if (respInfo.statusCode == 200) {
console.log(respBody);
} else {
console.log(respInfo.statusCode);
console.log(respBody);
}
},
);
+56 -46
View File
@@ -1,12 +1,12 @@
#!/usr/bin/env bash
get_token() {
token=$(cat $file_auth_user | jq -r .token)
token=$(cat $file_auth_token | jq -r .value)
}
add_cron_api() {
local currentTimeStamp=$(date +%s)
if [ $# -eq 1 ]; then
if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}')
@@ -17,13 +17,13 @@ add_cron_api() {
fi
local api=$(
curl -s "http://localhost:5600/api/crons?t=$currentTimeStamp" \
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://localhost:5700" \
-H "Referer: http://localhost:5700/crontab" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \
--compressed
@@ -39,7 +39,7 @@ add_cron_api() {
update_cron_api() {
local currentTimeStamp=$(date +%s)
if [ $# -eq 1 ]; then
if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}')
@@ -52,16 +52,16 @@ update_cron_api() {
fi
local api=$(
curl -s "http://localhost:5600/api/crons?t=$currentTimeStamp" \
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://localhost:5700" \
-H "Referer: http://localhost:5700/crontab" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"_id\":\"$id\"}" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
@@ -75,7 +75,7 @@ update_cron_api() {
update_cron_command_api() {
local currentTimeStamp=$(date +%s)
if [ $# -eq 1 ]; then
if [[ $# -eq 1 ]]; then
local command=$(echo "$1" | awk -F ":" '{print $1}')
local id=$(echo "$1" | awk -F ":" '{print $2}')
else
@@ -84,16 +84,16 @@ update_cron_command_api() {
fi
local api=$(
curl -s "http://localhost:5600/api/crons?t=$currentTimeStamp" \
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://localhost:5700" \
-H "Referer: http://localhost:5700/crontab" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"command\":\"$command\",\"_id\":\"$id\"}" \
--data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
@@ -109,14 +109,14 @@ del_cron_api() {
local ids=$1
local currentTimeStamp=$(date +%s)
local api=$(
curl -s "http://localhost:5600/api/crons?t=$currentTimeStamp" \
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'DELETE' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://localhost:5700" \
-H "Referer: http://localhost:5700/crontab" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "[$ids]" \
--compressed
@@ -130,48 +130,58 @@ del_cron_api() {
fi
}
get_user_info() {
update_cron() {
local ids="$1"
local status="$2"
local pid="${3:-''}"
local logPath="$4"
local lastExecutingTime="${5:-0}"
local runningTime="${6:-0}"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s "http://localhost:5600/api/user?t=$currentTimeStamp" \
-H 'Accept: */*' \
-H "Authorization: Bearer $token" \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \
-H 'Referer: http://localhost:5700/crontab' \
-H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \
--compressed
)
code=$(echo $api | jq -r .code)
if [[ $code != 200 ]]; then
echo -e "请先登录!"
exit 0
fi
}
update_cron_status() {
local ids=$1
local status=$2
local currentTimeStamp=$(date +%s)
local api=$(
curl -s "http://localhost:5600/api/crons/status?t=$currentTimeStamp" \
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/status?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://localhost:5700" \
-H "Referer: http://localhost:5700/crontab" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"ids\":[$ids],\"status\":\"$status\"}" \
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code != 200 ]]; then
echo -e "\n## 更新任务状态失败(${message})\n" >> $dir_log/$log_path
fi
}
notify_api() {
local title=$1
local content=$2
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "更新任务状态成功"
echo -e "通知发送成功"
else
echo -e "更新任务状态失败(${message})"
echo -e "通知失败(${message})"
fi
}
get_token
get_token
+24 -14
View File
@@ -1,26 +1,30 @@
#!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=/ql/shell
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
url="${github_proxy_url}https://github.com/SuMaiKaDe/bot.git"
repo_path="${dir_repo}/dockerbot"
if [[ -z ${BotRepoUrl} ]]; then
url="https://github.com/SuMaiKaDe/bot.git"
repo_path="${dir_repo}/dockerbot"
else
url=${BotRepoUrl}
repo_path="${dir_repo}/diybot"
fi
echo -e "\n1、安装bot依赖...\n"
apk --no-cache add -f zlib-dev gcc jpeg-dev python3-dev musl-dev freetype-dev
echo -e "\nbot依赖安装成功...\n"
echo -e "2、下载bot所需文件...\n"
if [ -d ${repo_path}/.git ]; then
git_pull_scripts ${repo_path}
else
if [[ ! -d ${repo_path}/.git ]]; then
rm -rf ${repo_path}
git_clone_scripts ${url} ${repo_path} "main"
fi
cp -rf "$repo_path/jbot" $dir_root
if [[ ! -f "$dir_root/config/bot.json" ]]; then
cp -f "$repo_path/config/bot.json" "$dir_root/config"
cp -rf "$repo_path/jbot" $dir_data
if [[ ! -f "$dir_config/bot.json" ]]; then
cp -f "$repo_path/config/bot.json" "$dir_config"
fi
echo -e "\nbot文件下载成功...\n"
@@ -28,15 +32,21 @@ echo -e "3、安装python3依赖...\n"
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
cp -f "$repo_path/jbot/requirements.txt" "$dir_root"
pip3 --default-timeout=100 install -r requirements.txt --no-cache-dir
cp -f "$repo_path/jbot/requirements.txt" "$dir_data"
cd $dir_data
cat requirements.txt | while read LREAD
do
if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
pip3 --default-timeout=100 install ${LREAD}
fi
done
echo -e "\npython3依赖安装成功...\n"
echo -e "4、启动bot程序...\n"
make_dir $dir_log/bot
cd $dir_root
cd $dir_data
ps -ef | grep "python3 -m jbot" | grep -v grep | awk '{print $1}' | xargs kill -9 2>/dev/null
nohup python3 -m jbot >$dir_log/bot/nohup.log 2>&1 &
echo -e "bot启动成功...\n"
exit 0
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
. $dir_shell/api.sh
reset_env() {
echo -e "---> 1. 开始检测配置文件\n"
fix_config
echo -e "---> 配置文件检测完成\n"
echo -e "---> 2. 开始安装青龙依赖\n"
npm_install_2 $dir_root
echo -e "---> 青龙依赖安装完成\n"
echo -e "---> 3. 开始安装脚本依赖\n"
cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts
echo -e "---> 脚本依赖安装完成\n"
}
copy_dep() {
echo -e "---> 1. 复制通知文件\n"
echo -e "---> 复制一份 $file_notify_py_sample$file_notify_py\n"
cp -fv $file_notify_py_sample $file_notify_py
echo
echo -e "---> 复制一份 $file_notify_js_sample$file_notify_js\n"
cp -fv $file_notify_js_sample $file_notify_js
echo -e "---> 通知文件复制完成\n"
echo -e "---> 2. 复制nginx配置文件\n"
cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
echo -e "---> 配置文件复制完成\n"
}
pm2_log() {
echo -e "---> pm2日志"
local panelOut="/root/.pm2/logs/panel-out.log"
local panelError="/root/.pm2/logs/panel-error.log"
tail -n 100 "$panelOut"
tail -n 100 "$panelError"
}
check_nginx() {
local nginxPid=$(ps -ef | grep nginx | grep -v grep)
echo -e "=====> 检测nginx服务\n$nginxPid"
if [[ $nginxPid ]]; then
echo -e "\n=====> nginx服务正常\n"
nginx -s reload
else
echo -e "\n=====> nginx服务异常,重新启动nginx\n"
nginx -c /etc/nginx/nginx.conf
fi
}
check_ql() {
local api=$(curl -s --noproxy "*" "http://0.0.0.0:5700")
echo -e "\n=====> 检测面板\n\n$api\n"
if [[ $api =~ "<div id=\"root\"></div>" ]]; then
echo -e "=====> 面板服务启动正常\n"
else
echo -e "=====> 面板服务异常,重置基础环境\n"
reset_env
fi
}
check_pm2() {
pm2_log
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/api/user?t=$currentTimeStamp" \
-H 'Accept: */*' \
-H "Authorization: Bearer $token" \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \
-H 'Referer: http://0.0.0.0:5700/crontab' \
-H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \
--compressed
)
echo -e "\n=====> 检测后台\n\n$api\n"
if [[ $api =~ "{\"code\"" ]]; then
echo -e "=====> 后台服务启动正常\n"
else
echo -e "=====> 后台服务异常,重置基础环境并重启后台\n"
reset_env
fi
}
main() {
echo -e "=====> 开始检测"
npm i -g pnpm
pnpm add -g pm2
copy_dep
check_ql
check_nginx
check_pm2
reload_pm2
echo -e "\n=====> 检测结束\n"
}
main
-151
View File
@@ -1,151 +0,0 @@
#!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=/ql/shell
. $dir_shell/share.sh
## 生成pt_pin清单
gen_pt_pin_array() {
local tmp1 tmp2 i pt_pin_temp
for ((user_num = 1; user_num <= $user_sum; user_num++)); do
tmp1=Cookie$user_num
tmp2=${!tmp1}
i=$(($user_num - 1))
pt_pin_temp=$(echo $tmp2 | perl -pe "{s|.*pt_pin=([^; ]+)(?=;?).*|\1|; s|%|\\\x|g}")
[[ $pt_pin_temp == *\\x* ]] && pt_pin[i]=$(printf $pt_pin_temp) || pt_pin[i]=$pt_pin_temp
done
}
## 导出互助码的通用程序,$1:去掉后缀的脚本名称,$2:config.sh中的后缀,$3:活动中文名称
export_codes_sub() {
local task_name=$1
local config_name=$2
local chinese_name=$3
local config_name_my=My$config_name
local config_name_for_other=ForOther$config_name
local i j k m n pt_pin_in_log code tmp_grep tmp_my_code tmp_for_other user_num random_num_list
if cd $dir_log/$task_name &>/dev/null && [[ $(ls) ]]; then
## 寻找所有互助码以及对应的pt_pin
i=0
pt_pin_in_log=()
code=()
pt_pin_and_code=$(ls -r *.log | xargs awk -v var="$chinese_name好友互助码" 'BEGIN{FS="[ )】]+"; OFS="&"} $3~var {print $2,$4}')
for line in $pt_pin_and_code; do
pt_pin_in_log[i]=$(echo $line | awk -F "&" '{print $1}')
code[i]=$(echo $line | awk -F "&" '{print $2}')
let i++
done
## 输出My系列变量
if [[ ${#code[*]} -gt 0 ]]; then
for ((m = 0; m < ${#pt_pin[*]}; m++)); do
tmp_my_code=""
j=$((m + 1))
for ((n = 0; n < ${#code[*]}; n++)); do
if [[ ${pt_pin[m]} == ${pt_pin_in_log[n]} ]]; then
tmp_my_code=${code[n]}
break
fi
done
echo "$config_name_my$j='$tmp_my_code'"
done
else
echo "## 从日志中未找到任何互助码"
fi
## 输出ForOther系列变量
if [[ ${#code[*]} -gt 0 ]]; then
echo
case $HelpType in
0) ## 全部一致
tmp_for_other=""
for ((m = 0; m < ${#pt_pin[*]}; m++)); do
j=$((m + 1))
tmp_for_other="$tmp_for_other@\${$config_name_my$j}"
done
echo "${config_name_for_other}1=\"$tmp_for_other\"" | perl -pe "s|($config_name_for_other\d+=\")@|\1|"
for ((m = 1; m < ${#pt_pin[*]}; m++)); do
j=$((m + 1))
echo "$config_name_for_other$j=\"\${${config_name_for_other}1}\""
done
;;
1) ## 均等助力
for ((m = 0; m < ${#pt_pin[*]}; m++)); do
tmp_for_other=""
j=$((m + 1))
for ((n = $m; n < $(($user_sum + $m)); n++)); do
[[ $m -eq $n ]] && continue
if [[ $((n + 1)) -le $user_sum ]]; then
k=$((n + 1))
else
k=$((n + 1 - $user_sum))
fi
tmp_for_other="$tmp_for_other@\${$config_name_my$k}"
done
echo "$config_name_for_other$j=\"$tmp_for_other\"" | perl -pe "s|($config_name_for_other\d+=\")@|\1|"
done
;;
2) ## 本套脚本内账号间随机顺序助力
for ((m = 0; m < ${#pt_pin[*]}; m++)); do
tmp_for_other=""
random_num_list=$(seq $user_sum | sort -R)
j=$((m + 1))
for n in $random_num_list; do
[[ $j -eq $n ]] && continue
tmp_for_other="$tmp_for_other@\${$config_name_my$n}"
done
echo "$config_name_for_other$j=\"$tmp_for_other\"" | perl -pe "s|($config_name_for_other\d+=\")@|\1|"
done
;;
*) ## 按编号优先
for ((m = 0; m < ${#pt_pin[*]}; m++)); do
tmp_for_other=""
j=$((m + 1))
for ((n = 0; n < ${#pt_pin[*]}; n++)); do
[[ $m -eq $n ]] && continue
k=$((n + 1))
tmp_for_other="$tmp_for_other@\${$config_name_my$k}"
done
echo "$config_name_for_other$j=\"$tmp_for_other\"" | perl -pe "s|($config_name_for_other\d+=\")@|\1|"
done
;;
esac
fi
else
echo "## 未运行过 $task_name.js 脚本,未产生日志"
fi
}
## 汇总输出
export_all_codes() {
gen_pt_pin_array
echo -e "\n# 从日志提取互助码,编号和配置文件中Cookie编号完全对应,如果为空就是所有日志中都没有。\n\n# 即使某个MyXxx变量未赋值,也可以将其变量名填在ForOtherXxx中,jtask脚本会自动过滤空值。\n"
echo -n "# 你选择的互助码模板为:"
case $HelpType in
0)
echo "所有账号助力码全部一致。"
;;
1)
echo "所有账号机会均等助力。"
;;
2)
echo "本套脚本内账号间随机顺序助力。"
;;
*)
echo "按账号编号优先。"
;;
esac
for ((i = 0; i < ${#name_js[*]}; i++)); do
echo -e "\n## ${name_chinese[i]}"
export_codes_sub "${name_js[i]}" "${name_config[i]}" "${name_chinese[i]}"
done
}
## 执行并写入日志
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
log_path="$dir_code/$log_time.log"
make_dir "$dir_code"
export_all_codes | perl -pe "{s|京东种豆|种豆|; s|crazyJoy任务|疯狂的JOY|}" | tee $log_path
-5
View File
@@ -1,5 +0,0 @@
const notify = require('/ql/scripts/sendNotify.js');
const title = process.argv[2];
const content = process.argv[3];
notify.sendNotify(`${title}`, `${content}`);
-8
View File
@@ -1,8 +0,0 @@
#!/bin/bash
#author:spark thanks to: https://github.com/sparkssssssss/scripts
. /ql/config/config.sh
title=$(echo $1|sed 's/-/_/g')
msg=$(echo -e $2)
node /ql/shell/notify.js "$title" "$msg"
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
echo -e "开始发布"
echo -e "切换master分支"
git checkout master
echo -e "合并develop代码"
git merge origin/develop
echo -e "提交master代码"
git push
echo -e "更新cdn文件"
ts-node sample/tool.ts
string=$(cat src/version.ts | grep "version" | egrep "[^\']*" -o | egrep "\d\.*")
version="v$string"
echo -e "当前版本$version"
echo -e "删除已经存在的本地tag"
git tag -d "$version" &>/dev/null
echo -e "删除已经存在的远程tag"
git push origin :refs/tags/$version &>/dev/null
echo -e "创建新tag"
git tag -a "$version" -m "release $version"
echo -e "提交tag"
git push --tags
echo -e "完成发布"
-26
View File
@@ -1,26 +0,0 @@
#!/usr/bin/env bash
# 导入通用变量与函数
dir_shell=/ql/shell
. $dir_shell/share.sh
echo -e "1. 开始安装青龙依赖\n"
npm_install_2 $dir_root
echo -e "青龙依赖安装完成\n"
echo -e "2. 开始安装脚本依赖\n"
cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts
echo -e "脚本依赖安装完成\n"
echo -e "3. 复制通知文件\n"
echo -e "复制一份 $file_notify_py_sample$file_notify_py\n"
cp -fv $file_notify_py_sample $file_notify_py
echo
echo -e "复制一份 $file_notify_js_sample$file_notify_js\n"
cp -fv $file_notify_js_sample $file_notify_js
echo -e "通知文件复制完成\n"
exit 0
+2 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=/ql/shell
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
days=$1
@@ -27,7 +27,7 @@ remove_js_log() {
remove_empty_dir() {
cd $dir_log
for dir in $(ls); do
if [ -d $dir ] && [[ -z $(ls $dir) ]]; then
if [[ -d $dir ]] && [[ -z $(ls $dir) ]]; then
rm -rf $dir
fi
done
+207 -76
View File
@@ -1,34 +1,44 @@
#!/usr/bin/env bash
## 目录
dir_root=/ql
dir_root=$QL_DIR
dir_data=$dir_root/data
dir_shell=$dir_root/shell
dir_sample=$dir_root/sample
dir_config=$dir_root/config
dir_scripts=$dir_root/scripts
dir_repo=$dir_root/repo
dir_raw=$dir_root/raw
dir_log=$dir_root/log
dir_db=$dir_root/db
dir_manual_log=$dir_root/manual_log
dir_static=$dir_root/static
dir_config=$dir_data/config
dir_scripts=$dir_data/scripts
dir_repo=$dir_data/repo
dir_raw=$dir_data/raw
dir_log=$dir_data/log
dir_db=$dir_data/db
dir_dep=$dir_data/deps
dir_list_tmp=$dir_log/.tmp
dir_code=$dir_log/code
dir_update_log=$dir_log/update
ql_static_repo=$dir_repo/static
## 文件
file_config_sample=$dir_sample/config.sample.sh
file_cookie=$dir_config/cookie.sh
file_env=$dir_config/env.sh
file_sharecode=$dir_config/sharecode.sh
file_config_user=$dir_config/config.sh
file_auth_sample=$dir_sample/auth.sample.json
file_auth_user=$dir_config/auth.json
file_auth_token=$dir_config/token.json
file_extra_shell=$dir_config/extra.sh
file_task_before=$dir_config/task_before.sh
file_task_after=$dir_config/task_after.sh
file_task_sample=$dir_sample/task.sample.sh
file_extra_sample=$dir_sample/extra.sample.sh
file_notify_js_sample=$dir_sample/notify.js
file_notify_py_sample=$dir_sample/notify.py
file_notify_py=$dir_scripts/notify.py
file_notify_js=$dir_scripts/sendNotify.js
task_error_log_path=$dir_log/task_error.log
nginx_app_conf=$dir_root/docker/front.conf
nginx_conf=$dir_root/docker/nginx.conf
dep_notify_py=$dir_dep/notify.py
dep_notify_js=$dir_dep/sendNotify.js
## 清单文件
list_crontab_user=$dir_config/crontab.list
@@ -42,30 +52,46 @@ list_own_drop=$dir_list_tmp/own_drop.list
link_name=(
task
ql
notify
)
original_name=(
task.sh
update.sh
notify.sh
)
## 导入配置文件
init_env() {
export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules
export PYTHONUNBUFFERED=1
}
import_config() {
[ -f $file_config_user ] && . $file_config_user
user_sum=0
for line in $(cat $file_cookie); do
let user_sum+=1
eval Cookie${user_sum}="\"${line}\""
done
[[ -f $file_config_user ]] && . $file_config_user
[[ -f $file_env ]] && . $file_env
ql_base_url=${QlBaseUrl:-""}
command_timeout_time=${CommandTimeoutTime:-"1h"}
github_proxy_url=${GithubProxyUrl:-""}
block_cookie=${TempBlockCookie:-""}
proxy_url=${ProxyUrl:-""}
file_extensions=${RepoFileExtensions:-"js py"}
current_branch=${QL_BRANCH}
if [[ -n "${DefaultCronRule}" ]]; then
default_cron="${DefaultCronRule}"
else
default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
fi
}
set_proxy() {
if [[ $proxy_url ]]; then
export http_proxy="${proxy_url}"
export https_proxy="${proxy_url}"
fi
}
unset_proxy() {
unset http_proxy
unset https_proxy
}
## 创建目录,$1:目录的绝对路径
make_dir() {
local dir=$1
if [[ ! -d $dir ]]; then
@@ -73,41 +99,36 @@ make_dir() {
fi
}
## 检测termux
detect_termux() {
if [[ ${ANDROID_RUNTIME_ROOT}${ANDROID_ROOT} ]] || [[ $PATH == *com.termux* ]]; then
if [[ $PATH == *com.termux* ]]; then
is_termux=1
else
is_termux=0
fi
}
## 检测macos
detect_macos() {
[[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0
}
## 生成随机数,$1:用来求余的数字
gen_random_num() {
local divi=$1
echo $((${RANDOM} % $divi))
}
## 创建软连接的子函数,$1:软连接文件路径,$2:要连接的对象
link_shell_sub() {
local link_path="$1"
local original_path="$2"
if [ ! -L $link_path ] || [[ $(readlink -f $link_path) != $original_path ]]; then
if [[ ! -L $link_path ]] || [[ $(readlink -f $link_path) != $original_path ]]; then
rm -f $link_path 2>/dev/null
ln -sf $original_path $link_path
fi
}
## 创建软连接
link_shell() {
if [[ $is_termux -eq 1 ]]; then
local path="/data/data/com.termux/files/usr/bin/"
elif [[ $PATH == */usr/local/bin* ]] && [ -d /usr/local/bin ]; then
elif [[ $PATH == */usr/local/bin* ]] && [[ -d /usr/local/bin ]]; then
local path="/usr/local/bin/"
else
local path=""
@@ -120,19 +141,18 @@ link_shell() {
fi
}
## 定义各命令
define_cmd() {
local cmd_prefix cmd_suffix
if type task >/dev/null 2>&1; then
if type task &>/dev/null; then
cmd_suffix=""
if [ -x "$dir_shell/task.sh" ]; then
if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix=""
else
cmd_prefix="bash "
fi
else
cmd_suffix=".sh"
if [ -x "$dir_shell/task.sh" ]; then
if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix="$dir_shell/"
else
cmd_prefix="bash $dir_shell/"
@@ -143,74 +163,97 @@ define_cmd() {
done
}
## 修复配置文件
fix_config() {
make_dir $dir_static
make_dir $dir_data
make_dir $dir_config
make_dir $dir_log
make_dir $dir_db
make_dir $dir_manual_log
make_dir $dir_scripts
make_dir $dir_list_tmp
make_dir $dir_repo
make_dir $dir_raw
make_dir $dir_update_log
make_dir $dir_dep
if [ ! -s $file_config_user ]; then
if [[ ! -s $file_config_user ]]; then
echo -e "复制一份 $file_config_sample$file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
cp -fv $file_config_sample $file_config_user
echo
fi
if [ ! -f $file_cookie ]; then
echo -e "检测到config配置目录下不存在cookie.sh,创建一个空文件用于初始化...\n"
touch $file_cookie
if [[ ! -f $file_env ]]; then
echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n"
touch $file_env
echo
fi
if [ ! -f $file_extra_shell ]; then
if [[ ! -f $file_task_before ]]; then
echo -e "复制一份 $file_task_sample$file_task_before\n"
cp -fv $file_task_sample $file_task_before
echo
fi
if [[ ! -f $file_task_after ]]; then
echo -e "复制一份 $file_task_sample$file_task_after\n"
cp -fv $file_task_sample $file_task_after
echo
fi
if [[ ! -f $file_extra_shell ]]; then
echo -e "复制一份 $file_extra_sample$file_extra_shell\n"
cp -fv $file_extra_sample $file_extra_shell
echo
fi
if [ ! -s $file_auth_user ]; then
if [[ ! -s $file_auth_user ]]; then
echo -e "复制一份 $file_auth_sample$file_auth_user\n"
cp -fv $file_auth_sample $file_auth_user
echo
fi
if [ ! -s $file_notify_py ]; then
if [[ ! -s $file_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$file_notify_py\n"
cp -fv $file_notify_py_sample $file_notify_py
echo
fi
if [ ! -s $file_notify_js ]; then
if [[ ! -s $file_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$file_notify_js\n"
cp -fv $file_notify_js_sample $file_notify_js
echo
fi
if [ -s /etc/nginx/conf.d/default.conf ]; then
echo -e "检测到默认nginx配置文件,删除...\n"
rm -f /etc/nginx/conf.d/default.conf
if [[ -s /etc/nginx/conf.d/default.conf ]]; then
echo -e "检测到默认nginx配置文件,清空...\n"
cat /dev/null >/etc/nginx/conf.d/default.conf
echo
fi
if [[ ! -s $dep_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$dep_notify_js\n"
cp -fv $file_notify_js_sample $dep_notify_js
echo
fi
if [[ ! -s $dep_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$dep_notify_py\n"
cp -fv $file_notify_py_sample $dep_notify_py
echo
fi
}
## npm install 子程序,判断是否为安卓,判断是否安装有pnpm
npm_install_sub() {
if [ $is_termux -eq 1 ]; then
npm install --production --no-save --no-bin-links --registry=https://registry.npm.taobao.org || npm install --production --no-bin-links --no-save
elif ! type pnpm >/dev/null 2>&1; then
npm install --production --no-save --registry=https://registry.npm.taobao.org || npm install --production --no-save
npm install --production --no-bin-links
elif ! type pnpm &>/dev/null; then
npm install --production
else
echo -e "检测到本机安装了 pnpm,使用 pnpm 替代 ...\n"
pnpm install --prod
pnpm install --loglevel error --production
fi
}
## npm install$1package.json文件所在路径
npm_install_1() {
local dir_current=$(pwd)
local dir_work=$1
@@ -236,66 +279,154 @@ npm_install_2() {
cd $dir_current
}
## 比对两个文件,$1比$2新时,将$1复制为$2
diff_and_copy() {
local copy_source=$1
local copy_to=$2
if [ ! -s $copy_to ] || [[ $(diff $copy_source $copy_to) ]]; then
if [[ ! -s $copy_to ]] || [[ $(diff $copy_source $copy_to) ]]; then
cp -f $copy_source $copy_to
fi
}
## 更新依赖
update_depend() {
local dir_current=$(pwd)
if [ ! -s $dir_scripts/package.json ] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts
fi
if [ ! -s $dir_scripts/requirements.txt ] || [[ $(diff $dir_sample/requirements.txt $dir_scripts/requirements.txt) ]]; then
cp -f $dir_sample/requirements.txt $dir_scripts/requirements.txt
cd $dir_scripts
pip3 install -r $dir_scripts/requirements.txt
fi
cd $dir_current
}
## 克隆脚本,$1:仓库地址,$2:仓库保存路径,$3:分支(可省略)
git_clone_scripts() {
local url=$1
local dir=$2
local branch=$3
[[ $branch ]] && local cmd="-b $branch "
[[ $branch ]] && local part_cmd="-b $branch "
echo -e "开始克隆仓库 $url$dir\n"
git clone $cmd $url $dir
set_proxy
git clone $part_cmd $url $dir
exit_status=$?
unset_proxy
}
## 更新脚本,$1:仓库保存路径
git_pull_scripts() {
local dir_current=$(pwd)
local dir_work="$1"
local branch="$2"
[[ $branch ]] && local cmd="origin/${branch}"
cd $dir_work
echo -e "开始更新仓库:$dir_work\n"
set_proxy
git fetch --all
exit_status=$?
git reset --hard $cmd
git pull
git pull &>/dev/null
unset_proxy
reset_branch "$branch"
cd $dir_current
}
init_env() {
TempBlockCookie=""
reset_romote_url() {
local dir_current=$(pwd)
local dir_work=$1
local url=$2
local branch="$3"
if [[ -d "$dir_work/.git" ]]; then
cd $dir_work
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
git remote set-url origin $url &>/dev/null
local part_cmd=""
reset_branch "$branch"
cd $dir_current
fi
}
## 导入配置文件,检测平台,创建软连接,识别命令,修复配置文件
reset_branch() {
local branch="$1"
if [[ $branch ]]; then
part_cmd="origin/${branch}"
git checkout -B "$branch"
git branch --set-upstream-to=$part_cmd $branch
fi
git reset --hard $part_cmd &>/dev/null
}
random_range() {
local beg=$1
local end=$2
echo $((RANDOM % ($end - $beg) + $beg))
}
reload_pm2() {
pm2 l &>/dev/null
echo -e "启动面板服务\n"
pm2 delete panel --source-map-support --time &>/dev/null
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
echo -e "启动定时任务服务\n"
pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
echo -e "启动公开服务\n"
pm2 delete public --source-map-support --time &>/dev/null
pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null
}
diff_time() {
local format="$1"
local begin_time="$2"
local end_time="$3"
if [[ $is_macos -eq 1 ]]; then
diff_time=$(($(date -j -f "$format" "$end_time" +%s) - $(date -j -f "$format" "$begin_time" +%s)))
else
diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
fi
echo "$diff_time"
}
format_time() {
local format="$1"
local time="$2"
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d %H:%M:%S")
else
echo $(date -d "$time" "+%Y-%m-%d %H:%M:%S")
fi
}
format_log_time() {
local format="$1"
local time="$2"
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S")
else
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S")
fi
}
format_timestamp() {
local format="$1"
local time="$2"
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%s")
else
echo $(date -d "$time" "+%s")
fi
}
init_env
detect_termux
detect_macos
define_cmd
init_env
import_config $1
fix_config
import_config $1 >$task_error_log_path 2>&1

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