Compare commits

..
125 Commits
Author SHA1 Message Date
whyour 0caf53b147 修复删除环境变量无效 2022-02-17 11:14:43 +08:00
whyour 4d1354d5f4 更新版本 v2.11.3 2022-02-16 23:09:33 +08:00
whyour fc4562eca2 修改check命令,安装依赖使用代理
修改apk源地址
2022-02-16 23:06:12 +08:00
whyour 5b048a5510 修改logo地址 2022-02-16 22:56:54 +08:00
whyour d23049b2fe 移除esbuild 2022-02-11 13:52:12 +08:00
whyour caac5607b9 环境变量增加更新时间排序 2022-02-11 13:41:48 +08:00
Ghost LeeandGitHub fb2e69b12d 修复goCqHttpBotToken中存在&字符导致认证失败的问题 (#993)
修改goCqHttpBot鉴权方式,调整为使用Header模式
2022-02-08 18:45:24 +08:00
UkennandGitHub fc1838ff39 bark支持自定义推送图标 (#1146)
* bark添加自定义推送图标

* 脚本bark通知添加自定义推送图标
2022-02-08 18:21:37 +08:00
whyour 25689cf40f 修复定时任务排序 2022-02-01 22:33:18 +08:00
whyour c746a3d215 修复.env 2022-01-30 20:22:00 +08:00
whyour 548d924fa0 更新版本 v2.11.2 2022-01-30 18:25:18 +08:00
whyour 18a6be3ecd 修改placeholder 2022-01-30 17:39:27 +08:00
whyour afef0cb72f 更新dockerfile 2022-01-30 14:59:41 +08:00
whyour 1221b47eca 修改dockerfile 2022-01-30 14:03:57 +08:00
whyour a356eeb5da 修改base context 2022-01-30 12:37:20 +08:00
whyour 25bb0972b9 修改dockerfile 2022-01-30 12:31:49 +08:00
whyour 0914075cf6 修改定时任务唯一键 2022-01-30 10:11:47 +08:00
whyour 74b258e1b6 修改定时任务排序 2022-01-30 09:59:33 +08:00
whyour 27eb0a21e4 修改dockerfile 2022-01-29 14:23:19 +08:00
whyour d77c6d50d8 修改dockerfile 2022-01-29 12:53:21 +08:00
whyour 6a64c18a7b 修改dockerfile 2022-01-29 12:15:25 +08:00
whyour b97ff714ae 修复dockerfile 2022-01-29 11:45:06 +08:00
whyour 1f23eea805 更新dockerfile 2022-01-29 11:20:43 +08:00
whyour cb0cb7df21 修改dockerfile 2022-01-29 00:09:37 +08:00
whyour f3e8628664 移除prepare scripts 2022-01-28 23:46:17 +08:00
whyour 4758471f9f 修改sentry依赖 2022-01-28 23:19:39 +08:00
whyour c10bd5a549 修改sentry cli版本 2022-01-28 23:14:12 +08:00
whyour d53faf96eb 修改sentry依赖 2022-01-28 22:47:46 +08:00
whyour 2307b8ed1a 增加esbuild 2022-01-28 21:53:05 +08:00
whyour 711af8d4a5 修改dockfile 2022-01-28 20:54:00 +08:00
whyour 458c9c456b 修改dockerfile 2022-01-28 20:27:38 +08:00
whyour 29069df48c 修复dockerfile 2022-01-28 19:33:01 +08:00
whyour 300e84ae2c 修改dockerfile 2022-01-28 19:23:57 +08:00
whyour bf11768b92 修改base镜像 2022-01-27 23:09:29 +08:00
whyour 91bd1d9263 测试青龙基础镜像 2022-01-27 23:06:35 +08:00
whyour ac40f9ff12 测试最新alpine 2022-01-27 22:21:34 +08:00
whyour f930837220 修复获取克隆仓库状态 2022-01-27 22:15:45 +08:00
whyour 1d5e3acc93 去除sql执行日志 2022-01-27 00:56:37 +08:00
whyour 2aec1f9846 修改表唯一约束 2022-01-27 00:49:44 +08:00
whyour b56a681b98 python默认实时输出 2022-01-25 23:03:56 +08:00
whyour 2713419b32 修复修改任务状态本地免认证 2022-01-25 22:52:27 +08:00
whyour 764a3492cc 添加关于页 2022-01-24 23:17:12 +08:00
whyour 29fc471ac4 修复日志加载状态 2022-01-24 22:50:01 +08:00
whyour 54a597aad3 修改logo地址 2022-01-23 16:55:11 +08:00
whyour 778961d0b6 修改node基础镜像版本 2022-01-22 22:09:16 +08:00
whyour 14b01ac761 测试基础镜像node版本 2022-01-22 21:57:02 +08:00
whyour f87848c05b 修改darkreader版本 2022-01-22 21:09:01 +08:00
whyour 153e5a806d 增加sqlite连接池 2022-01-22 20:53:22 +08:00
whyour ee1b8a906f 移除cdn依赖 2022-01-22 20:25:22 +08:00
whyour 734685d45a 定时任务默认改为创建时间倒序 2022-01-21 22:38:13 +08:00
whyour e3b540c212 修复更新系统通知 2022-01-21 22:27:50 +08:00
whyour 201f578bbc 修复sqlite加载顺序 2022-01-20 22:19:39 +08:00
whyour ce9aa369d2 更新版本 v2.11.1 2022-01-20 19:11:41 +08:00
whyour c5c41c0859 修改cdn地址,增加重启服务延迟 2022-01-20 19:02:29 +08:00
whyour d3009f82dd 移除默认github代理,增加http或者socks代理参数 2022-01-19 23:29:55 +08:00
whyour 0eb9a1fdb0 定时任务默认倒序 2022-01-18 23:50:18 +08:00
whyour 83f6d9926a 修改迁移数据库逻辑 2022-01-18 22:56:12 +08:00
whyour 14134842b0 修复环境变量更新时间显示 2022-01-17 22:47:23 +08:00
whyour 4c094c4710 修复重置应用秘钥 2022-01-16 18:55:18 +08:00
whyour 3ac41075fd 修复停止任务后任务状态修改 2022-01-14 23:44:22 +08:00
whyour fdc97773d1 修改数据迁移逻辑 2022-01-14 23:17:16 +08:00
whyour 917a739595 修复环境变量搜索 2022-01-13 22:38:20 +08:00
ChiupamandGitHub 21475f81ae 支持运行pyc格式文件 (#1072) 2022-01-13 14:39:49 +08:00
whyour a2a9a0560c 修复更新定时任务 2022-01-13 14:37:56 +08:00
whyour b879846cdb 修复环境变量搜索 2022-01-13 13:15:00 +08:00
whyour a24011cd72 修复openapi token 查询 2022-01-13 12:02:36 +08:00
whyour 410dd2ecdc 修复openapi认证,环境变量排序 2022-01-13 11:13:04 +08:00
whyour 96b111b894 更新版本 v2.11.0 2022-01-12 23:43:32 +08:00
whyour 630572c8f9 更新task命令获取时间戳 2022-01-12 23:39:40 +08:00
whyour 6a381a6366 更新dockerfile 2022-01-12 00:55:14 +08:00
whyour fd04f12677 修复dockerfile 2022-01-11 00:44:22 +08:00
whyour 86b8cd17a4 修复dockerfile 2022-01-11 00:35:30 +08:00
whyour bb8ee01693 修复dockerfile 2022-01-10 23:35:47 +08:00
whyour 5fb0bca3af 修复dockerfile 2022-01-10 23:17:33 +08:00
whyour 1e4d1f373d 测试编译sqlite 2022-01-10 23:10:59 +08:00
whyour 87c78d0fcc 测试nodejs14 sqlite 2022-01-09 23:17:50 +08:00
whyour 041d2b7947 测试打包镜像时间 2022-01-09 12:38:20 +08:00
whyour 5e7b90d995 更新dockerfile 2022-01-08 23:56:45 +08:00
whyour fadc3dcb55 测试arm/v7 2022-01-08 23:29:20 +08:00
whyour 4eea091944 修改dockerfile 2022-01-08 22:49:17 +08:00
whyour 9f8d46f395 更新node gyp 2022-01-08 22:19:21 +08:00
whyour 2c54a057c9 修改node gyp版本 2022-01-08 21:48:32 +08:00
whyour e1e5cacc6d 添加node-pre-gyp依赖 2022-01-08 21:12:38 +08:00
whyour edf9066967 修复方法名称 2022-01-08 01:26:34 +08:00
whyour 7097d12b87 更新dockerfile 2022-01-08 01:10:10 +08:00
whyour 08aaeb6372 修复sqlite数据操作类型 2022-01-08 01:09:38 +08:00
kilo5hzandGitHub 4310f80ec0 添加标签功能 (#1026)
* 添加标签功能
2022-01-07 22:01:13 +08:00
whyour 01577f6494 dockerfile使用yarn install 2022-01-07 00:18:12 +08:00
whyour 9aa570b2c6 修改sqlite版本 2022-01-06 23:25:48 +08:00
whyour d611d7d698 修复param类型 2022-01-06 22:59:29 +08:00
whyour f05549b06c 使用sqlite替换nedb 2022-01-06 22:51:12 +08:00
ChiupamandGitHub 8c4db8dd40 支持自定义BOT仓库 (#1051) 2022-01-04 13:26:16 +08:00
whyour ddee69d291 更新版本 v2.10.13 2021-12-25 21:31:52 +08:00
whyour 28960c9fab 添加sentry-cli cdn 2021-12-25 20:55:13 +08:00
whyour 449301e519 修改热更新目录 2021-12-25 20:13:49 +08:00
whyour 28dab3dc2f 修改两步验证界面逻辑 2021-12-25 20:09:02 +08:00
whyour 7995d98db5 修改批量运行任务逻辑 2021-12-24 23:31:11 +08:00
whyour 1f319f3533 调试脚本增加停止,修复调试交互 2021-12-23 23:25:39 +08:00
whyour 7d2dbaa62d 修改文本 2021-12-23 13:39:16 +08:00
whyour ed5999526b 优化初始化通知设置交互 2021-12-21 23:49:52 +08:00
whyour 2409f75491 修复登录退出接口路径 2021-12-21 23:42:05 +08:00
whyour 0c840095d1 增加系统openapi 2021-12-21 23:22:34 +08:00
whyour 3eaf0b4856 system接口增加版本号,应用权限增加依赖管理 2021-12-21 22:53:19 +08:00
琚涛涛andGitHub 394735a5d9 优化定时任务名称展示效果,提升用户体验 (#1017) 2021-12-21 22:39:17 +08:00
whyour 5bfa10bfe7 修复任务跳转脚本参数,task运行脚本支持更多参数 2021-12-21 00:23:20 +08:00
whyour 3f12d9cbd5 更新版本号 v2.10.12 2021-12-09 23:24:10 +08:00
whyour 54d89754df 修复darkreader最新版bug 2021-12-09 23:22:17 +08:00
whyour 9470524615 更新readme 2021-12-06 23:42:14 +08:00
whyour b5faa6eaaa 修复logo样式 2021-12-06 11:18:19 +08:00
whyour eab476cfe8 更新版本号 v2.10.11 2021-12-05 23:15:49 +08:00
whyour b773def012 修复搜索环境变量urlencode 2021-12-05 23:04:26 +08:00
whyour a9f8b23a7a 修复内容区高度和logo样式 2021-11-29 23:02:42 +08:00
whyour 61bb6535fb 修复最后运行时长被覆盖 2021-11-29 21:49:32 +08:00
雪狐andGitHub eb0bdffe06 避免环境变量中出现"字符的时候提前截断字符串 (#966) 2021-11-29 21:43:29 +08:00
whyour e129485285 更新版本 v2.10.10 2021-11-27 20:43:18 +08:00
colinxuandGitHub aac7de172e 修复使用旧pushplus推送后结果解析失败的问题 (#958) 2021-11-27 20:33:49 +08:00
whyour e19d2412fc 检测更新增加强制更新 2021-11-27 20:19:49 +08:00
whyour 7cba2cf76c 依赖目录通知文件没有时,拷贝示例的通知文件 2021-11-27 17:41:26 +08:00
whyour 55d7a49dbc 修复deps映射目录 2021-11-27 17:19:50 +08:00
whyour f695864fd8 修复build args 2021-11-27 17:12:45 +08:00
whyour ae4883fbe7 测试yml 2021-11-27 17:05:55 +08:00
whyour 836dfd2861 添加deps目录软链 2021-11-27 16:20:24 +08:00
whyour 6e0523c6d7 修复删除JavaScript进程,修改依赖重新安装逻辑 2021-11-27 13:41:38 +08:00
whyour cd9ba084ae 修复调试运行时文件路径 2021-11-23 23:36:43 +08:00
phoenixandGitHub c8a0c0de67 fix nginx can not run without /run/nginx (#944)
alpine3.12 和 alpine3.14 兼容性问题
2021-11-23 18:32:22 +08:00
88 changed files with 4243 additions and 3066 deletions
+2 -1
View File
@@ -2,7 +2,8 @@ MONGODB_URI='mongodb://'
YIYAN_MONGODB_URI=''
CRON_PORT=5500
PORT=5600
BACK_PORT=5600
PORT=5700
LOG_LEVEL='debug'
+59
View File
@@ -0,0 +1,59 @@
name: Build QL BASE
on:
schedule:
- cron: '00 18 * * *' # GMT 14:00 => 北京时间 2:00
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '14'
- name: Set time zone
uses: szenius/set-timezone@v1.0
with:
timezoneLinux: "Asia/Shanghai"
timezoneMacos: "Asia/Shanghai"
timezoneWindows: "China Standard Time"
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
build-args: |
MAINTAINER=${{ github.repository_owner }}
QL_BRANCH=${{ github.ref_name }}
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x
context: .
file: docker/base/Dockerfile
push: true
tags: whyour/ql:base
+8 -3
View File
@@ -28,8 +28,8 @@ jobs:
- name: build front and back
run: |
yarn install
yarn build
yarn build-back
yarn build:front
yarn build:back
- name: copy to static repo
env:
@@ -56,6 +56,9 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '14'
- name: Set time zone
uses: szenius/set-timezone@v1.0
@@ -105,7 +108,9 @@ jobs:
id: docker_build
uses: docker/build-push-action@v2
with:
build-args: MAINTAINER=${{ github.repository_owner }}, QL_BRANCH=${{ github.ref_name }}
build-args: |
MAINTAINER=${{ github.repository_owner }}
QL_BRANCH=${{ github.ref_name }}
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x
context: docker/
push: true
+1
View File
@@ -0,0 +1 @@
sentrycli_cdnurl=https://cdn.npm.taobao.org/dist/sentry-cli
-9
View File
@@ -32,18 +32,9 @@ export default defineConfig({
externals: {
react: 'window.React',
'react-dom': 'window.ReactDOM',
darkreader: 'window.DarkReader',
codemirror: 'window.CodeMirror',
'sockjs-client': 'window.SockJS',
},
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/darkreader@4/darkreader.min.js',
'https://cdn.jsdelivr.net/npm/codemirror@5/lib/codemirror.min.js',
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/shell/shell.js',
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/python/python.js',
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/javascript/javascript.js',
'https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js',
],
});
+1 -2
View File
@@ -153,11 +153,10 @@ $ git clone git@github.com:whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
$ yarn install
$ yarn start-back
$ yarn start
```
打开你的浏览器,访问 http://127.0.0.1:5601
打开你的浏览器,访问 http://127.0.0.1:5700
## 交流
+4 -4
View File
@@ -8,10 +8,10 @@ import { celebrate, Joi } from 'celebrate';
const route = Router();
export default (app: Router) => {
app.use('/', route);
app.use('/configs', route);
route.get(
'/configs/files',
'/files',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -32,7 +32,7 @@ export default (app: Router) => {
);
route.get(
'/configs/:file',
'/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -58,7 +58,7 @@ export default (app: Router) => {
);
route.post(
'/configs/save',
'/save',
celebrate({
body: Joi.object({
name: Joi.string().required(),
+88 -45
View File
@@ -7,31 +7,28 @@ import cron_parser from 'cron-parser';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.get(
'/crons',
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,
);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
app.use('/crons', route);
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.searchValue as string);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.post(
'/crons',
'/',
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) => {
@@ -52,9 +49,9 @@ export default (app: Router) => {
);
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');
@@ -70,9 +67,9 @@ export default (app: Router) => {
);
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');
@@ -87,10 +84,55 @@ export default (app: Router) => {
},
);
route.put(
'/crons/disable',
route.delete(
'/labels',
celebrate({
body: Joi.array().items(Joi.string().required()),
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) {
logger.error('🔥 error: %o', 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) {
logger.error('🔥 error: %o', 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');
@@ -106,9 +148,9 @@ export default (app: Router) => {
);
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');
@@ -124,13 +166,13 @@ export default (app: Router) => {
);
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);
@@ -144,13 +186,14 @@ export default (app: Router) => {
);
route.put(
'/crons',
'/',
celebrate({
body: Joi.object({
labels: Joi.array().optional().allow(null),
command: Joi.string().optional(),
schedule: Joi.string().optional(),
name: Joi.string().optional(),
_id: Joi.string().required(),
id: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
@@ -174,9 +217,9 @@ export default (app: Router) => {
);
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');
@@ -192,9 +235,9 @@ export default (app: Router) => {
);
route.put(
'/crons/pin',
'/pin',
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');
@@ -210,9 +253,9 @@ export default (app: Router) => {
);
route.put(
'/crons/unpin',
'/unpin',
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');
@@ -228,7 +271,7 @@ export default (app: Router) => {
);
route.get(
'/crons/import',
'/import',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -243,17 +286,17 @@ export default (app: Router) => {
);
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);
@@ -263,10 +306,10 @@ export default (app: Router) => {
);
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(),
log_path: Joi.string().optional(),
+28 -30
View File
@@ -6,30 +6,28 @@ import { celebrate, Joi } from 'celebrate';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.get(
'/dependencies',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const dependenceService = Container.get(DependenceService);
const data = await dependenceService.dependencies(req.query as any);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
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(
'/dependencies',
'/',
celebrate({
body: Joi.array().items(
Joi.object({
name: Joi.string().required(),
type: Joi.number().required(),
remark: Joi.number().optional().allow(''),
remark: Joi.string().optional().allow(''),
}),
),
}),
@@ -47,13 +45,13 @@ export default (app: Router) => {
);
route.put(
'/dependencies',
'/',
celebrate({
body: Joi.object({
name: Joi.string().required(),
_id: Joi.string().required(),
id: Joi.number().required(),
type: Joi.number().required(),
remark: Joi.number().optional().allow(''),
remark: Joi.string().optional().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
@@ -70,9 +68,9 @@ export default (app: Router) => {
);
route.delete(
'/dependencies',
'/',
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');
@@ -88,9 +86,9 @@ export default (app: Router) => {
);
route.delete(
'/dependencies/force',
'/force',
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');
@@ -106,17 +104,17 @@ export default (app: Router) => {
);
route.get(
'/dependencies/: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 dependenceService = Container.get(DependenceService);
const data = await dependenceService.get(req.params.id);
const data = await dependenceService.getDb({ id: req.params.id });
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
@@ -126,9 +124,9 @@ export default (app: Router) => {
);
route.put(
'/dependencies/reinstall',
'/reinstall',
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');
+32 -34
View File
@@ -6,24 +6,22 @@ import { celebrate, Joi } from 'celebrate';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.get(
'/envs',
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);
}
},
);
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(
'/envs',
'/',
celebrate({
body: Joi.array().items(
Joi.object({
@@ -47,13 +45,13 @@ export default (app: Router) => {
);
route.put(
'/envs',
'/',
celebrate({
body: Joi.object({
value: Joi.string().required(),
name: Joi.string().required(),
remarks: Joi.string().optional().allow(''),
_id: Joi.string().required(),
remarks: Joi.string().optional().allow('').allow(null),
id: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
@@ -70,9 +68,9 @@ export default (app: Router) => {
);
route.delete(
'/envs',
'/',
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');
@@ -88,17 +86,17 @@ export default (app: Router) => {
);
route.put(
'/envs/: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 envService = Container.get(EnvService);
@@ -112,9 +110,9 @@ export default (app: Router) => {
);
route.put(
'/envs/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');
@@ -130,9 +128,9 @@ export default (app: Router) => {
);
route.put(
'/envs/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');
@@ -148,10 +146,10 @@ export default (app: Router) => {
);
route.put(
'/envs/name',
'/name',
celebrate({
body: Joi.object({
ids: Joi.array().items(Joi.string().required()),
ids: Joi.array().items(Joi.number().required()),
name: Joi.string().required(),
}),
}),
@@ -169,17 +167,17 @@ export default (app: Router) => {
);
route.get(
'/envs/: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 envService = Container.get(EnvService);
const data = await envService.get(req.params.id);
const data = await envService.getDb(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
+2
View File
@@ -7,6 +7,7 @@ import cron from './cron';
import script from './script';
import open from './open';
import dependence from './dependence';
import system from './system';
export default () => {
const app = Router();
@@ -18,6 +19,7 @@ export default () => {
script(app);
open(app);
dependence(app);
system(app);
return app;
};
+33 -35
View File
@@ -7,44 +7,42 @@ import { getFileContentByName } from '../config/util';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.get(
'/logs',
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: [],
});
}
app.use('/logs', route);
route.get('/', 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: [],
});
}
res.send({ code: 200, dirs });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
res.send({ code: 200, dirs });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.get(
'/logs/:dir/:file',
'/:dir/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -61,7 +59,7 @@ export default (app: Router) => {
);
route.get(
'/logs/:file',
'/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
+4 -4
View File
@@ -49,7 +49,7 @@ export default (app: Router) => {
body: Joi.object({
name: Joi.string().optional().allow(''),
scopes: Joi.array().items(Joi.string()),
_id: Joi.string().required(),
id: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
@@ -68,7 +68,7 @@ export default (app: Router) => {
route.delete(
'/apps',
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');
@@ -87,10 +87,10 @@ export default (app: Router) => {
'/apps/:id/reset-secret',
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 openService = Container.get(OpenService);
+46 -18
View File
@@ -9,15 +9,15 @@ import { Logger } from 'winston';
import config from '../config';
import * as fs from 'fs';
import { celebrate, Joi } from 'celebrate';
import path from 'path';
import path, { join } from 'path';
import ScriptService from '../services/script';
const route = Router();
export default (app: Router) => {
app.use('/', route);
app.use('/scripts', route);
route.get(
'/scripts/files',
'/files',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -44,7 +44,7 @@ export default (app: Router) => {
children.push({
title: childFile,
value: childFile,
key: `${fileOrDir}-${childFile}`,
key: `${fileOrDir}/${childFile}`,
mtime: statObj.mtimeMs,
parent: fileOrDir,
});
@@ -80,14 +80,16 @@ export default (app: Router) => {
);
route.get(
'/scripts/:file',
'/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const path = req.query.path ? `${req.query.path}/` : '';
const content = getFileContentByName(
`${config.scriptPath}${path}${req.params.file}`,
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) {
logger.error('🔥 error: %o', e);
@@ -97,11 +99,11 @@ export default (app: Router) => {
);
route.post(
'/scripts',
'/',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
path: Joi.string().optional().allow(''),
content: Joi.string().allow(''),
originFilename: Joi.string().allow(''),
}),
@@ -157,11 +159,11 @@ export default (app: Router) => {
);
route.put(
'/scripts',
'/',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
path: Joi.string().optional().allow(''),
content: Joi.string().required(),
}),
}),
@@ -173,7 +175,7 @@ export default (app: Router) => {
content: string;
path: string;
};
const filePath = `${config.scriptPath}${path}/${filename}`;
const filePath = join(config.scriptPath, path, filename);
fs.writeFileSync(filePath, content);
return res.send({ code: 200 });
} catch (e) {
@@ -184,7 +186,7 @@ export default (app: Router) => {
);
route.delete(
'/scripts',
'/',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
@@ -198,7 +200,7 @@ export default (app: Router) => {
filename: string;
path: string;
};
const filePath = `${config.scriptPath}${path}/${filename}`;
const filePath = join(config.scriptPath, path, filename);
fs.unlinkSync(filePath);
res.send({ code: 200 });
} catch (e) {
@@ -209,7 +211,7 @@ export default (app: Router) => {
);
route.post(
'/scripts/download',
'/download',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
@@ -240,7 +242,7 @@ export default (app: Router) => {
);
route.put(
'/scripts/run',
'/run',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
@@ -254,7 +256,7 @@ export default (app: Router) => {
filename: string;
path: string;
};
const filePath = `${path}/${filename}`;
const filePath = join(path, filename);
const scriptService = Container.get(ScriptService);
const result = await scriptService.runScript(filePath);
res.send(result);
@@ -264,4 +266,30 @@ export default (app: Router) => {
}
},
);
route.put(
'/stop',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().optional().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, path } = req.body as {
filename: string;
path: string;
};
const filePath = join(path, filename);
const scriptService = Container.get(ScriptService);
const result = await scriptService.stopScript(filePath);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+114
View File
@@ -0,0 +1,114 @@
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 { getFileContentByName } from '../config/util';
import UserService from '../services/user';
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 envDbContent = getFileContentByName(config.envDbFile);
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' &&
envDbContent.length === 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) {
logger.error('🔥 error: %o', 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) {
logger.error('🔥 error: %o', 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) {
logger.error('🔥 error: %o', 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) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+29 -130
View File
@@ -9,7 +9,8 @@ import { getFileContentByName } from '../config/util';
const route = Router();
export default (app: Router) => {
app.use('/', route);
app.use('/user', route);
route.post(
'/login',
celebrate({
@@ -47,7 +48,7 @@ export default (app: Router) => {
);
route.put(
'/user',
'/',
celebrate({
body: Joi.object({
username: Joi.string().required(),
@@ -67,29 +68,26 @@ export default (app: Router) => {
},
);
route.get(
'/user',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
res.send({
code: 200,
data: {
username: authInfo.username,
twoFactorActivated: authInfo.twoFactorActivated,
},
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
res.send({
code: 200,
data: {
username: authInfo.username,
twoFactorActivated: authInfo.twoFactorActivated,
},
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.get(
'/user/two-factor/init',
'/two-factor/init',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -104,7 +102,7 @@ export default (app: Router) => {
);
route.put(
'/user/two-factor/active',
'/two-factor/active',
celebrate({
body: Joi.object({
code: Joi.string().required(),
@@ -124,7 +122,7 @@ export default (app: Router) => {
);
route.put(
'/user/two-factor/deactive',
'/two-factor/deactive',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -139,7 +137,7 @@ export default (app: Router) => {
);
route.put(
'/user/two-factor/login',
'/two-factor/login',
celebrate({
body: Joi.object({
code: Joi.string().required(),
@@ -161,7 +159,7 @@ export default (app: Router) => {
);
route.get(
'/user/login-log',
'/login-log',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -176,7 +174,7 @@ export default (app: Router) => {
);
route.get(
'/user/notification',
'/notification',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -191,7 +189,7 @@ export default (app: Router) => {
);
route.put(
'/user/notification',
'/notification',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -205,40 +203,8 @@ export default (app: Router) => {
},
);
route.get(
'/system',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
const envDbContent = getFileContentByName(config.envDbFile);
let isInitialized = true;
if (
Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' &&
authInfo.password === 'admin' &&
envDbContent.length === 0
) {
isInitialized = false;
}
res.send({
code: 200,
data: {
isInitialized,
},
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
// 初始化api
route.put(
'/init/user',
'/init',
celebrate({
body: Joi.object({
username: Joi.string().required(),
@@ -259,7 +225,7 @@ export default (app: Router) => {
);
route.put(
'/init/notification',
'/notification/init',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -272,71 +238,4 @@ export default (app: Router) => {
}
},
);
route.get(
'/system/log/remove',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const data = await userService.getLogRemoveFrequency();
res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/system/log/remove',
celebrate({
body: Joi.object({
frequency: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const result = await userService.updateLogRemoveFrequency(
req.body.frequency,
);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/system/update-check',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const result = await userService.checkUpdate();
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/system/update',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const result = await userService.updateSystem();
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+5 -5
View File
@@ -5,7 +5,7 @@ import { createRandomString } from './util';
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
const lastVersionFile =
'https://ghproxy.com/https://raw.githubusercontent.com/whyour/qinglong/master/src/version.ts';
'https://raw.githubusercontent.com/whyour/qinglong/master/src/version.ts';
const envFound = dotenv.config();
const rootPath = process.cwd();
@@ -37,7 +37,7 @@ if (envFound.error) {
}
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 || createRandomString(16, 32),
logs: {
@@ -77,12 +77,12 @@ export default {
writePathList: [configPath, scriptPath],
bakPath,
apiWhiteList: [
'/api/login',
'/api/user/login',
'/open/auth/token',
'/api/user/two-factor/login',
'/api/system',
'/api/init/user',
'/api/init/notification',
'/api/user/init',
'/api/user/notification/init',
],
versionFile,
lastVersionFile,
+37
View File
@@ -231,3 +231,40 @@ export async function fileExist(file: any) {
}
});
}
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;
}
+15 -2
View File
@@ -1,14 +1,17 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
export class AuthInfo {
ip?: string;
type: AuthDataType;
info?: any;
_id?: string;
id?: number;
constructor(options: AuthInfo) {
this.ip = options.ip;
this.info = options.info;
this.type = options.type;
this._id = options._id;
this.id = options.id;
}
}
@@ -23,3 +26,13 @@ export enum AuthDataType {
'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,
},
});
+42 -7
View File
@@ -1,34 +1,42 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
export class Crontab {
name?: string;
command: 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 = CrontabStatus[options.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;
}
}
@@ -38,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,
});
+20 -4
View File
@@ -1,7 +1,9 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
export class Dependence {
timestamp?: string;
created?: number;
_id?: string;
id?: number;
status?: DependenceStatus;
type?: DependenceTypes;
name?: number;
@@ -9,8 +11,7 @@ export class Dependence {
remark?: string;
constructor(options: Dependence) {
this._id = options._id;
this.created = options.created || new Date().valueOf();
this.id = options.id;
this.status = options.status || DependenceStatus.installing;
this.type = options.type || DependenceTypes.nodejs;
this.timestamp = new Date().toString();
@@ -46,3 +47,18 @@ export enum unInstallDependenceCommandTypes {
'pip3 uninstall -y',
'apk del -f',
}
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,
},
);
+17 -6
View File
@@ -1,17 +1,18 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
export class Env {
value?: string;
timestamp?: string;
created?: number;
_id?: string;
id?: number;
status?: EnvStatus;
position?: number;
name?: number;
remarks?: number;
name?: string;
remarks?: string;
constructor(options: Env) {
this.value = options.value;
this._id = options._id;
this.created = options.created || new Date().valueOf();
this.id = options.id;
this.status = options.status || EnvStatus.normal;
this.timestamp = new Date().toString();
this.position = options.position;
@@ -26,3 +27,13 @@ export enum EnvStatus {
}
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 };
+2
View File
@@ -34,6 +34,8 @@ export class ServerChanNotification extends NotificationBaseInfo {
export class BarkNotification extends NotificationBaseInfo {
public barkPush = '';
public barkIcon =
'https://img.gejiba.com/images/a3f551e09ac19add4c49ec16228729c5.png';
public barkSound = '';
public barkGroup = 'qinglong';
}
+15 -3
View File
@@ -1,23 +1,26 @@
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?: string;
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;
this.id = options.id;
}
}
export interface AppToken {
value: string;
type: 'Bearer';
type?: 'Bearer';
expiration: number;
}
@@ -29,3 +32,12 @@ export enum CrontabStatus {
'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,
});
+1 -1
View File
@@ -1,7 +1,7 @@
export class SockMessage {
message?: string;
type?: SockMessageType;
references?: string[];
references?: number[];
constructor(options: SockMessage) {
this.type = options.type;
+4
View File
@@ -3,6 +3,7 @@ import dependencyInjectorLoader from './dependencyInjector';
import Logger from './logger';
import initData from './initData';
import { Application } from 'express';
import linkDeps from './deps';
export default async ({ expressApp }: { expressApp: Application }) => {
await dependencyInjectorLoader({
@@ -15,4 +16,7 @@ export default async ({ expressApp }: { expressApp: Application }) => {
await initData();
Logger.info('✌️ init data loaded');
await linkDeps();
Logger.info('✌️ link deps');
};
+81 -47
View File
@@ -1,57 +1,93 @@
import DataStore from 'nedb';
import config from '../config';
import Logger from './logger';
import fs from 'fs';
import { fileExist } from '../config/util';
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 { sequelize } from '../data';
interface Dbs {
cronDb: DataStore;
dependenceDb: DataStore;
envDb: DataStore;
appDb: DataStore;
authDb: DataStore;
}
const db: Dbs = {} as any;
async function truncateDb() {
return new Promise(async (resolve) => {
const files = [
config.cronDbFile,
config.dependenceDbFile,
config.envDbFile,
config.appDbFile,
config.authDbFile,
];
for (const file of files) {
const _fileExist = await fileExist(file);
if (_fileExist && fs.statSync(file).size >= 1024 * 1024 * 500) {
fs.truncateSync(file, 1024 * 1024 * 500);
}
}
resolve(null);
});
}
export default async () => {
try {
await truncateDb();
await sequelize.sync();
await new Promise((resolve) => setTimeout(() => resolve(null), 5000));
db.cronDb = new DataStore({ filename: config.cronDbFile, autoload: true });
db.dependenceDb = new DataStore({
filename: config.dependenceDbFile,
autoload: true,
});
db.envDb = new DataStore({ filename: config.envDbFile, autoload: true });
db.appDb = new DataStore({ filename: config.appDbFile, autoload: true });
db.authDb = new DataStore({ filename: config.authDbFile, autoload: true });
// 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) {
// compaction data file
db.cronDb.persistence.compactDatafile();
db.envDb.persistence.compactDatafile();
db.dependenceDb.persistence.compactDatafile();
db.appDb.persistence.compactDatafile();
db.authDb.persistence.compactDatafile();
// }
const crondbExist = await fileExist(config.cronDbFile);
const dependenceDbExist = await fileExist(config.dependenceDbFile);
const envDbExist = await fileExist(config.envDbFile);
const appDbExist = await fileExist(config.appDbFile);
const authDbExist = await fileExist(config.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: config.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: config.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: config.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: config.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: config.authDbFile,
autoload: true,
});
authDb.persistence.compactDatafile();
authDb.find({}).exec(async (err, docs) => {
await AuthModel.bulkCreate(docs, { ignoreDuplicates: true });
});
}
Logger.info('✌️ DB loaded');
} catch (error) {
@@ -59,5 +95,3 @@ export default async () => {
Logger.info(error);
}
};
export const dbs: Dbs = db;
+31
View File
@@ -0,0 +1,31 @@
import path from 'path';
import fs from 'fs';
import chokidar from 'chokidar';
import config from '../config/index';
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;
});
}
});
}
export default async (src: string = 'deps') => {
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));
};
+6 -5
View File
@@ -68,18 +68,19 @@ export default ({ app }: { app: Application }) => {
}
}
const originPath = `${req.baseUrl}${req.path === '/' ? '' : req.path}`;
if (
!headerToken &&
req.path &&
config.apiWhiteList.includes(req.path) &&
req.path !== '/api/crons/status'
originPath &&
config.apiWhiteList.includes(originPath) &&
originPath !== '/api/crons/status'
) {
return next();
}
const remoteAddress = req.socket.remoteAddress;
if (
remoteAddress === '::ffff:127.0.0.1' &&
req.path === '/api/crons/status'
originPath === '/api/crons/status'
) {
return next();
}
@@ -98,7 +99,7 @@ export default ({ app }: { app: Application }) => {
});
app.use(async (req, res, next) => {
if (!['/api/init/user', '/api/init/notification'].includes(req.path)) {
if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) {
return next();
}
const userService = Container.get(UserService);
+23 -42
View File
@@ -1,73 +1,54 @@
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 EnvService from '../services/env';
import _ from 'lodash';
import { dbs } from '../loaders/db';
import { DependenceModel } from '../data/dependence';
import { Op } from 'sequelize';
export default async () => {
const cronService = Container.get(CronService);
const envService = Container.get(EnvService);
const dependenceService = Container.get(DependenceService);
const cronDb = dbs.cronDb;
const dependenceDb = dbs.dependenceDb;
// 初始化更新所有任务状态为空闲
cronDb.update(
{ status: { $in: [CrontabStatus.running, CrontabStatus.queued] } },
{ $set: { status: CrontabStatus.idle } },
{ multi: true },
await CrontabModel.update(
{ status: CrontabStatus.idle },
{ where: { status: [CrontabStatus.running, CrontabStatus.queued] } },
);
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
dependenceDb.find({ status: { $in: [0, 1, 2] } }).exec((err, docs) => {
DependenceModel.findAll({ where: {} }).then(async (docs) => {
const groups = _.groupBy(docs, 'type');
for (const key in groups) {
if (Object.prototype.hasOwnProperty.call(groups, key)) {
const group = groups[key];
const depIds = group.map((x) => x._id);
const depIds = group.map((x) => x.id);
for (const dep of depIds) {
dependenceService.reInstall([dep]);
await dependenceService.reInstall([dep]);
}
}
}
});
// 初始化时执行一次所有的ql repo 任务
cronDb
.find({
command: /ql (repo|raw)/,
isDisabled: { $ne: 1 },
})
.exec((err, docs) => {
for (let i = 0; i < docs.length; i++) {
const doc = docs[i];
if (doc) {
exec(doc.command);
}
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);
}
});
// 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();
+1 -1
View File
@@ -5,7 +5,7 @@ import Logger from './logger';
export default ({ expressApp }: { expressApp: Application }) => {
Sentry.init({
dsn: 'https://e14681bce55f4849b11024a7d424b711@o1051273.ingest.sentry.io/6047906',
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
new Tracing.Integrations.Express({ app: expressApp }),
+7 -13
View File
@@ -2,24 +2,14 @@ import schedule from 'node-schedule';
import express from 'express';
import { exec } from 'child_process';
import Logger from './loaders/logger';
import { CrontabStatus } from './data/cron';
import { CrontabModel, CrontabStatus } from './data/cron';
import config from './config';
import { dbs } from './loaders/db';
const app = express();
const run = async () => {
const cronDb = dbs.cronDb;
cronDb
.find({})
.sort({ created: 1 })
.exec((err, docs) => {
if (err) {
Logger.error(err);
process.exit(1);
}
CrontabModel.findAll({ where: {} })
.then((docs) => {
if (docs && docs.length > 0) {
for (let i = 0; i < docs.length; i++) {
const task = docs[i];
@@ -40,6 +30,10 @@ const run = async () => {
}
}
}
})
.catch((err) => {
Logger.error(err);
process.exit(1);
});
};
+191 -229
View File
@@ -1,25 +1,17 @@
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 { dbs } from '../loaders/db';
import { Op } from 'sequelize';
@Service()
export default class CronService {
private cronDb = dbs.cronDb;
private queue = new PQueue({
concurrency: parseInt(process.env.MaxConcurrentNum as string) || 5,
});
constructor(@Inject('logger') private logger: winston.Logger) {}
private isSixCron(cron: Crontab) {
@@ -32,7 +24,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));
@@ -40,42 +31,19 @@ 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({
@@ -86,156 +54,165 @@ export default class CronService {
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;
}) {
return new Promise((resolve) => {
this.cronDb.update(
{ _id: { $in: ids } },
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: 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(
{
$set: {
status,
pid,
log_path,
last_running_time,
last_execution_time,
},
},
{ multi: true, returnUpdatedDocs: true },
(err) => {
resolve(null);
labels: Array.from(new Set((doc.labels || []).concat(labels))),
},
{ where: { id: doc.id } },
);
});
}
}
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 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 } },
);
});
}
public async pin(ids: string[]) {
return new Promise((resolve: any) => {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { isPinned: 1 } },
{ multi: true },
async (err) => {
resolve();
},
);
});
}
public async unPin(ids: string[]) {
return new Promise((resolve: any) => {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { isPinned: 0 } },
{ multi: true },
async (err) => {
resolve();
},
);
});
}
}
public async crontabs(searchText?: string): Promise<Crontab[]> {
let query = {};
if (searchText) {
const reg = new RegExp(searchText, 'i');
query = {
$or: [
{
name: reg,
},
{
command: reg,
},
{
schedule: reg,
},
],
};
const textArray = searchText.split(':');
switch (textArray[0]) {
case 'name':
case 'command':
case 'schedule':
case 'label':
const column = textArray[0] === 'label' ? 'labels' : textArray[0];
query = {
[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)}%` },
],
};
query = {
[Op.or]: [
{
name: reg,
},
{
command: reg,
},
{
schedule: reg,
},
{
labels: reg,
},
],
};
break;
}
}
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]);
try {
const result = await CrontabModel.findAll({
where: query,
order: [['createdAt', 'DESC']],
});
});
}
public async run(ids: string[]) {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { status: CrontabStatus.queued } },
{ multi: true },
);
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
this.queue.add(() => this.runSingle(id));
return result as any;
} catch (error) {
throw error;
}
}
public async stop(ids: string[]) {
return new Promise((resolve: any) => {
this.cronDb
.find({ _id: { $in: ids } })
.exec(async (err, docs: Crontab[]) => {
for (const doc of docs) {
if (doc.pid) {
try {
process.kill(-doc.pid);
} catch (error) {
this.logger.silly(error);
}
}
const err = await this.killTask(doc.command);
if (doc.log_path) {
const str = err ? `\n${err}` : '';
fs.appendFileSync(
`${doc.log_path}`,
`${str}\n## 执行结束... ${new Date()
.toLocaleString('zh', { hour12: false })
.replace(' 24:', ' 00:')} `,
);
}
}
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
{ multi: true },
);
this.queue.clear();
resolve();
});
});
public async getDb(query: any): Promise<Crontab> {
const doc: any = await CrontabModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Crontab);
}
private async killTask(name: string) {
public async run(ids: number[]) {
await CrontabModel.update(
{ status: CrontabStatus.queued },
{ where: { id: ids } },
);
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 logFileExist = await fileExist(doc.log_path);
if (doc.log_path && logFileExist) {
const str = err ? `\n${err}` : '';
fs.appendFileSync(
`${doc.log_path}`,
`${str}\n## 执行结束... ${new Date()
.toLocaleString('zh', { hour12: false })
.replace(' 24:', ' 00:')} `,
);
}
}
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 {
@@ -245,16 +222,20 @@ export default class CronService {
} else {
return;
}
const pids = pid.match(/\(\d+/g);
let pids = pid.match(/\(\d+/g);
const killLogs = [];
for (const id of pids) {
const c = `kill -9 ${id.slice(1)}`;
const { stdout, stderr } = await execAsync(c);
if (stderr) {
killLogs.push(stderr);
}
if (stdout) {
killLogs.push(stdout);
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)}`;
const { stdout, stderr } = await execAsync(c);
if (stderr) {
killLogs.push(stderr);
}
if (stdout) {
killLogs.push(stdout);
}
}
}
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
@@ -263,18 +244,18 @@ export default class CronService {
}
}
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, log_path } = cron;
let { id, command, log_path } = cron;
this.logger.silly('Running job');
this.logger.silly('ID: ' + _id);
this.logger.silly('ID: ' + id);
this.logger.silly('Original command: ' + command);
let cmdStr = command;
@@ -286,9 +267,10 @@ export default class CronService {
}
const cp = spawn(cmdStr, { shell: '/bin/bash' });
this.cronDb.update(
{ _id },
{ $set: { status: CrontabStatus.running, pid: cp.pid } },
await CrontabModel.update(
{ status: CrontabStatus.running, pid: cp.pid },
{ where: { id } },
);
cp.stderr.on('data', (data) => {
if (log_path) {
@@ -301,57 +283,39 @@ export default class CronService {
}
});
cp.on('exit', (code, signal) => {
cp.on('exit', async (code, signal) => {
this.logger.info(
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
);
this.cronDb.update(
{ _id },
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined },
{ where: { id } },
);
resolve();
});
cp.on('close', (code) => {
cp.on('close', async (code) => {
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
this.cronDb.update(
{ _id },
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined },
{ where: { id } },
);
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) {
const doc = await this.get(_id);
public async log(id: number) {
const doc = await this.getDb({ id });
if (!doc) {
return '';
}
@@ -391,7 +355,7 @@ export default class CronService {
}
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;
}
@@ -421,7 +385,7 @@ export default class CronService {
if (needReloadSchedule) {
exec(`pm2 reload schedule`);
}
this.cronDb.update({}, { $set: { saved: true } }, { multi: true });
await CrontabModel.update({ saved: true }, { where: {} });
}
public import_crontab() {
@@ -429,7 +393,7 @@ export default class CronService {
var lines = stdout.split('\n');
var namePrefix = new Date().getTime();
lines.reverse().forEach((line, index) => {
lines.reverse().forEach(async (line, index) => {
line = line.replace(/\t+/g, ' ');
var regex =
/^((\@[a-zA-Z]+\s+)|(([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+))/;
@@ -443,18 +407,16 @@ export default class CronService {
) {
var 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);
}
}
});
});
+142 -192
View File
@@ -1,23 +1,21 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import config from '../config';
import DataStore from 'nedb';
import {
Dependence,
InstallDependenceCommandTypes,
DependenceStatus,
DependenceTypes,
unInstallDependenceCommandTypes,
DependenceModel,
} from '../data/dependence';
import _ from 'lodash';
import { spawn } from 'child_process';
import SockService from './sock';
import { dbs } from '../loaders/db';
import { Op } from 'sequelize';
@Service()
export default class DependenceService {
private dependenceDb = dbs.dependenceDb;
constructor(
@Inject('logger') private logger: winston.Logger,
private sockService: SockService,
@@ -34,22 +32,15 @@ export default class DependenceService {
}
public async insert(payloads: Dependence[]): Promise<Dependence[]> {
return new Promise((resolve) => {
this.dependenceDb.insert(payloads, (err, docs) => {
if (err) {
this.logger.error(err);
} else {
resolve(docs);
}
});
});
const docs = await DependenceModel.bulkCreate(payloads);
return docs;
}
public async update(
payload: Dependence & { _id: string },
payload: Dependence & { id: string },
): Promise<Dependence> {
const { _id, ...other } = payload;
const doc = await this.get(_id);
const { id, ...other } = payload;
const doc = await this.getDb({ id });
const tab = new Dependence({
...doc,
...other,
@@ -61,46 +52,22 @@ export default class DependenceService {
}
private async updateDb(payload: Dependence): Promise<Dependence> {
return new Promise((resolve) => {
this.dependenceDb.update(
{ _id: payload._id },
payload,
{ returnUpdatedDocs: true },
(err, num, doc) => {
if (err) {
this.logger.error(err);
} else {
resolve(doc as Dependence);
}
},
);
});
await DependenceModel.update(payload, { where: { id: payload.id } });
return await this.getDb({ id: payload.id });
}
public async remove(ids: string[]) {
return new Promise((resolve: any) => {
this.dependenceDb.update(
{ _id: { $in: ids } },
{ $set: { status: DependenceStatus.removing, log: [] } },
{ multi: true, returnUpdatedDocs: true },
async (err, num, docs: Dependence[]) => {
this.installOrUninstallDependencies(docs, false);
resolve(docs);
},
);
});
public async remove(ids: number[]) {
await DependenceModel.update(
{ status: DependenceStatus.removing, log: [] },
{ where: { id: ids } },
);
const docs = await DependenceModel.findAll({ where: { id: ids } });
this.installOrUninstallDependencies(docs, false);
return docs;
}
public async removeDb(ids: string[]) {
return new Promise((resolve: any) => {
this.dependenceDb.remove(
{ _id: { $in: ids } },
{ multi: true },
async (err) => {
resolve();
},
);
});
public async removeDb(ids: number[]) {
await DependenceModel.destroy({ where: { id: ids } });
}
public async dependencies(
@@ -110,171 +77,154 @@ export default class DependenceService {
): Promise<Dependence[]> {
let condition = { ...query, type: DependenceTypes[type as any] };
if (searchValue) {
const reg = new RegExp(searchValue);
condition = {
...condition,
$or: [
{
name: reg,
},
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;
}
const newDocs = await this.find(condition, sort);
return newDocs;
}
public async reInstall(ids: string[]): Promise<Dependence[]> {
return new Promise((resolve: any) => {
this.dependenceDb.update(
{ _id: { $in: ids } },
{ $set: { status: DependenceStatus.installing, log: [] } },
{ multi: true, returnUpdatedDocs: true },
async (err, num, docs: Dependence[]) => {
this.installOrUninstallDependencies(docs);
resolve(docs);
},
);
});
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 } });
await this.installOrUninstallDependencies(docs);
return docs;
}
private async find(query: any, sort: any): Promise<Dependence[]> {
return new Promise((resolve) => {
this.dependenceDb
.find(query)
.sort({ ...sort })
.exec((err, docs) => {
resolve(docs);
});
});
private async find(query: any, sort?: any): Promise<Dependence[]> {
const docs = await DependenceModel.findAll({ where: { ...query } });
return docs;
}
public async get(_id: string): Promise<Dependence> {
return new Promise((resolve) => {
this.dependenceDb.find({ _id }).exec((err, docs) => {
resolve(docs[0]);
});
});
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: string[], log: string): Promise<void> {
return new Promise((resolve) => {
this.dependenceDb.update(
{ _id: { $in: ids } },
{ $push: { log } },
{ multi: true },
(err, num, doc) => {
if (err) {
this.logger.error(err);
} else {
resolve();
}
},
);
});
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,
) {
if (dependencies.length === 0) {
return;
}
const depNames = dependencies.map((x) => x.name).join(' ');
const depRunCommand = (
isInstall
? InstallDependenceCommandTypes
: unInstallDependenceCommandTypes
)[dependencies[0].type as any];
const actionText = isInstall ? '安装' : '删除';
const depIds = dependencies.map((x) => x._id) as string[];
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
const startTime = Date.now();
this.sockService.sendMessage({
type: 'installDependence',
message: `开始${actionText}依赖 ${depNames},开始时间 ${new Date(
startTime,
).toLocaleString()}`,
references: depIds,
});
this.updateLog(
depIds,
`开始${actionText}依赖 ${depNames},开始时间 ${new Date(
startTime,
).toLocaleString()}\n`,
);
cp.stdout.on('data', (data) => {
return new Promise(async (resolve) => {
if (dependencies.length === 0) {
resolve(null);
return;
}
const depNames = dependencies.map((x) => x.name).join(' ');
const depRunCommand = (
isInstall
? InstallDependenceCommandTypes
: unInstallDependenceCommandTypes
)[dependencies[0].type as any];
const actionText = isInstall ? '安装' : '删除';
const depIds = dependencies.map((x) => x.id) as number[];
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
const startTime = Date.now();
this.sockService.sendMessage({
type: 'installDependence',
message: data.toString(),
message: `开始${actionText}依赖 ${depNames},开始时间 ${new Date(
startTime,
).toLocaleString()}`,
references: depIds,
});
this.updateLog(depIds, data.toString());
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'installDependence',
message: data.toString(),
references: depIds,
});
this.updateLog(depIds, data.toString());
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'installDependence',
message: JSON.stringify(err),
references: depIds,
});
this.updateLog(depIds, JSON.stringify(err));
});
cp.on('close', (code) => {
const endTime = Date.now();
const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败';
this.sockService.sendMessage({
type: 'installDependence',
message: `依赖${actionText}${resultText},结束时间 ${new Date(
endTime,
).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`,
references: depIds,
});
this.updateLog(
await this.updateLog(
depIds,
`依赖${actionText}${resultText}结束时间 ${new Date(
endTime,
).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`,
`开始${actionText}依赖 ${depNames}开始时间 ${new Date(
startTime,
).toLocaleString()}\n`,
);
cp.stdout.on('data', async (data) => {
this.sockService.sendMessage({
type: 'installDependence',
message: data.toString(),
references: depIds,
});
await this.updateLog(depIds, data.toString());
});
let status = null;
if (isSucceed) {
status = isInstall
? DependenceStatus.installed
: DependenceStatus.removed;
} else {
status = isInstall
? DependenceStatus.installFailed
: DependenceStatus.removeFailed;
}
this.dependenceDb.update(
{ _id: { $in: depIds } },
{
$set: { status },
$unset: { pid: true },
},
{ multi: true },
);
cp.stderr.on('data', async (data) => {
this.sockService.sendMessage({
type: 'installDependence',
message: data.toString(),
references: depIds,
});
await this.updateLog(depIds, data.toString());
});
// 如果删除依赖成功,3秒后删除数据库记录
if (isSucceed && !isInstall) {
setTimeout(() => {
this.removeDb(depIds);
}, 5000);
}
cp.on('error', async (err) => {
this.sockService.sendMessage({
type: 'installDependence',
message: JSON.stringify(err),
references: depIds,
});
await this.updateLog(depIds, JSON.stringify(err));
resolve(null);
});
cp.on('close', async (code) => {
const endTime = Date.now();
const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败';
this.sockService.sendMessage({
type: 'installDependence',
message: `依赖${actionText}${resultText},结束时间 ${new Date(
endTime,
).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`,
references: depIds,
});
await this.updateLog(
depIds,
`依赖${actionText}${resultText},结束时间 ${new Date(
endTime,
).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`,
);
let status = null;
if (isSucceed) {
status = isInstall
? DependenceStatus.installed
: DependenceStatus.removed;
} else {
status = isInstall
? DependenceStatus.installFailed
: DependenceStatus.removeFailed;
}
await DependenceModel.update({ status }, { where: { id: depIds } });
// 如果删除依赖成功,3秒后删除数据库记录
if (isSucceed && !isInstall) {
setTimeout(() => {
this.removeDb(depIds);
}, 5000);
}
resolve(null);
});
});
}
}
+60 -109
View File
@@ -3,15 +3,12 @@ import winston from 'winston';
import { getFileContentByName } from '../config/util';
import config from '../config';
import * as fs from 'fs';
import DataStore from 'nedb';
import { Env, EnvStatus, initEnvPosition } from '../data/env';
import { Env, EnvModel, EnvStatus, initEnvPosition } from '../data/env';
import _ from 'lodash';
import { dbs } from '../loaders/db';
import { Op } from 'sequelize';
@Service()
export default class EnvService {
private envDb = dbs.envDb;
constructor(@Inject('logger') private logger: winston.Logger) {}
public async create(payloads: Env[]): Promise<Env[]> {
@@ -31,54 +28,32 @@ export default class EnvService {
}
public async insert(payloads: Env[]): Promise<Env[]> {
return new Promise((resolve) => {
this.envDb.insert(payloads, (err, docs) => {
if (err) {
this.logger.error(err);
} else {
resolve(docs);
}
});
});
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 { _id, ...other } = payload;
const doc = await this.get(_id);
const tab = new Env({ ...doc, ...other });
const newDoc = await this.updateDb(tab);
const newDoc = await this.updateDb(payload);
await this.set_envs();
return newDoc;
}
private async updateDb(payload: Env): Promise<Env> {
return new Promise((resolve) => {
this.envDb.update(
{ _id: payload._id },
payload,
{ returnUpdatedDocs: true },
(err, num, doc) => {
if (err) {
this.logger.error(err);
} else {
resolve(doc as Env);
}
},
);
});
await EnvModel.update({ ...payload }, { where: { id: payload.id } });
return await this.getDb({ id: payload.id });
}
public async remove(ids: string[]) {
return new Promise((resolve: any) => {
this.envDb.remove({ _id: { $in: ids } }, { multi: true }, async (err) => {
await this.set_envs();
resolve();
});
});
await EnvModel.destroy({ where: { id: ids } });
await this.set_envs();
}
public async move(
_id: string,
id: number,
{
fromIndex,
toIndex,
@@ -100,7 +75,7 @@ export default class EnvService {
: (envs[toIndex].position + envs[toIndex + 1].position) / 2;
}
const newDoc = await this.update({
_id,
id,
position: targetPosition,
});
return newDoc;
@@ -113,103 +88,73 @@ export default class EnvService {
): Promise<Env[]> {
let condition = { ...query };
if (searchText) {
const reg = new RegExp(searchText);
const encodeText = encodeURIComponent(searchText);
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchText}%` },
{ [Op.like]: `%${encodeText}%` },
],
};
condition = {
$or: [
{
value: reg,
},
...condition,
[Op.or]: [
{
name: reg,
},
{
value: reg,
},
{
remarks: reg,
},
],
};
}
const newDocs = await this.find(condition, sort);
return newDocs;
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[]> {
return new Promise((resolve) => {
this.envDb
.find(query)
.sort({ ...sort })
.exec((err, docs) => {
resolve(docs);
});
private async find(query: any, sort: any = []): Promise<Env[]> {
const docs = await EnvModel.findAll({
where: { ...query },
order: [...sort],
});
return docs;
}
public async get(_id: string): Promise<Env> {
return new Promise((resolve) => {
this.envDb.find({ _id }).exec((err, docs) => {
resolve(docs[0]);
});
});
}
public async getBySort(sort: any): Promise<Env> {
return new Promise((resolve) => {
this.envDb
.find({})
.sort({ ...sort })
.limit(1)
.exec((err, docs) => {
resolve(docs[0]);
});
});
public async 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[]) {
return new Promise((resolve: any) => {
this.envDb.update(
{ _id: { $in: ids } },
{ $set: { status: EnvStatus.disabled } },
{ multi: true },
async (err) => {
await this.set_envs();
resolve();
},
);
});
await EnvModel.update(
{ status: EnvStatus.disabled },
{ where: { id: ids } },
);
await this.set_envs();
}
public async enabled(ids: string[]) {
return new Promise((resolve: any) => {
this.envDb.update(
{ _id: { $in: ids } },
{ $set: { status: EnvStatus.normal } },
{ multi: true },
async (err, num) => {
await this.set_envs();
resolve();
},
);
});
await EnvModel.update({ status: EnvStatus.normal }, { where: { id: ids } });
await this.set_envs();
}
public async updateNames({ ids, name }: { ids: string[]; name: string }) {
return new Promise((resolve: any) => {
this.envDb.update(
{ _id: { $in: ids } },
{ $set: { name } },
{ multi: true },
async (err, num) => {
await this.set_envs();
resolve();
},
);
});
await EnvModel.update({ name }, { where: { id: ids } });
await this.set_envs();
}
public async set_envs() {
const envs = await this.envs(
'',
{ position: -1 },
{ name: { $exists: true } },
{ name: { [Op.not]: null } },
);
const groups = _.groupBy(envs, 'name');
let env_string = '';
@@ -219,11 +164,17 @@ export default class EnvService {
// 忽略不符合bash要求的环境变量名称
if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) {
env_string += `export ${key}="${_(group)
let value = _(group)
.filter((x) => x.status !== EnvStatus.disabled)
.map('value')
.join('&')
.replace(/ /g, '')}"\n`;
.replace(/ /g, '');
if (/"/.test(value)) {
value = `'${value}'`;
} else {
value = `"${value}"`;
}
env_string += `export ${key}=${value}\n`;
}
}
}
+4 -3
View File
@@ -91,11 +91,12 @@ export default class NotificationService {
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
const res: any = await got
.post(
`${goCqHttpBotUrl}?access_token=${goCqHttpBotToken}&${goCqHttpBotQq}`,
`${goCqHttpBotUrl}?${goCqHttpBotQq}`,
{
timeout: this.timeout,
retry: 0,
json: { message: `${this.title}\n${this.content}` },
headers: { 'Authorization': 'Bearer '+goCqHttpBotToken },
},
)
.json();
@@ -119,7 +120,7 @@ export default class NotificationService {
}
private async bark() {
let { barkPush, barkSound, barkGroup } = this.params;
let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
barkPush = `https://api.day.app/${barkPush}`;
}
@@ -127,7 +128,7 @@ export default class NotificationService {
this.title,
)}/${encodeURIComponent(
this.content,
)}?sound=${barkSound}&group=${barkGroup}`;
)}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`;
const res: any = await got
.get(url, {
timeout: this.timeout,
+74 -114
View File
@@ -2,90 +2,68 @@ import { Service, Inject } from 'typedi';
import winston from 'winston';
import { createRandomString } from '../config/util';
import config from '../config';
import DataStore from 'nedb';
import { App } from '../data/open';
import { App, AppModel } from '../data/open';
import { v4 as uuidV4 } from 'uuid';
import { dbs } from '../loaders/db';
import sequelize, { Op } from 'sequelize';
@Service()
export default class OpenService {
private appDb = dbs.appDb;
constructor(@Inject('logger') private logger: winston.Logger) {}
public async findTokenByValue(token: string): Promise<App> {
return new Promise((resolve) => {
this.appDb.find(
{ tokens: { $elemMatch: { value: token } } },
(err, docs) => {
if (err) {
this.logger.error(err);
} else {
resolve(docs[0]);
}
},
);
});
public async 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 = new App({ ...payload });
const tab = { ...payload };
tab.client_id = createRandomString(12, 12);
tab.client_secret = createRandomString(24, 24);
const docs = await this.insert([tab]);
return { ...docs[0], tokens: [] };
const doc = await this.insert(tab);
return { ...doc, tokens: [] };
}
public async insert(payloads: App[]): Promise<App[]> {
return new Promise((resolve) => {
this.appDb.insert(payloads, (err, docs) => {
if (err) {
this.logger.error(err);
} else {
resolve(docs);
}
});
});
public async 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 { _id, client_id, client_secret, tokens, ...other } = payload;
const doc = await this.get(_id);
const tab = new App({ ...doc, ...other });
const newDoc = await this.updateDb(tab);
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> {
return new Promise((resolve) => {
this.appDb.update(
{ _id: payload._id },
payload,
{ returnUpdatedDocs: true },
(err, num, doc, up) => {
if (err) {
this.logger.error(err);
} else {
resolve(doc);
}
},
);
});
await AppModel.update(payload, { where: { id: payload.id } });
return await this.getDb({ id: payload.id });
}
public async remove(ids: string[]) {
return new Promise((resolve: any) => {
this.appDb.remove({ _id: { $in: ids } }, { multi: true }, async (err) => {
resolve();
});
});
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 resetSecret(_id: string): Promise<App> {
const doc = await this.get(_id);
const tab = new App({ ...doc });
tab.client_secret = createRandomString(24, 24);
tab.tokens = [];
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;
}
@@ -97,42 +75,30 @@ export default class OpenService {
): Promise<App[]> {
let condition = { ...query };
if (searchText) {
const reg = new RegExp(searchText);
condition = {
$or: [
{
value: reg,
},
{
name: reg,
},
{
remarks: reg,
},
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.map((x) => ({ ...x, tokens: [] }));
} catch (error) {
throw error;
}
const newDocs = await this.find(condition, sort);
return newDocs.map((x) => ({ ...x, tokens: [] }));
}
private async find(query: any, sort: any): Promise<App[]> {
return new Promise((resolve) => {
this.appDb
.find(query)
.sort({ ...sort })
.exec((err, docs) => {
resolve(docs);
});
});
}
public async get(_id: string): Promise<App> {
return new Promise((resolve) => {
this.appDb.find({ _id }).exec((err, docs) => {
resolve(docs[0]);
});
});
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({
@@ -144,28 +110,22 @@ export default class OpenService {
}): Promise<any> {
const token = uuidV4();
const expiration = Math.round(Date.now() / 1000) + 2592000; // 2592000 30天
return new Promise((resolve) => {
this.appDb.find({ client_id, client_secret }).exec((err, docs) => {
if (docs && docs[0]) {
this.appDb.update(
{ client_id, client_secret },
{ $push: { tokens: { value: token, expiration } } },
{},
(err, num, doc) => {
resolve({
code: 200,
data: {
token,
token_type: 'Bearer',
expiration,
},
});
},
);
} else {
resolve({ code: 400, message: 'client_id或client_seret有误' });
}
});
});
const doc = await AppModel.findOne({ where: { client_id, client_secret } });
if (doc) {
await AppModel.update(
{ tokens: [...(doc.tokens || []), { value: token, expiration }] },
{ where: { client_id, client_secret } },
);
return {
code: 200,
data: {
token,
token_type: 'Bearer',
expiration,
},
};
} else {
return { code: 400, message: 'client_id或client_seret有误' };
}
}
}
+7 -7
View File
@@ -6,22 +6,22 @@ import { exec } from 'child_process';
@Service()
export default class ScheduleService {
private scheduleStacks = new Map<string, nodeSchedule.Job>();
private scheduleStacks = new Map<number, nodeSchedule.Job>();
constructor(@Inject('logger') private logger: winston.Logger) {}
async generateSchedule({ _id = '', command, name, schedule }: Crontab) {
async generateSchedule({ id = 0, command, name, schedule }: Crontab) {
this.logger.info(
'[创建定时任务],任务ID: %scron: %s,任务名: %s,执行命令: %s',
_id,
id,
schedule,
name,
command,
);
this.scheduleStacks.set(
_id,
nodeSchedule.scheduleJob(_id, schedule, async () => {
id,
nodeSchedule.scheduleJob(id + '', schedule, async () => {
try {
exec(command, async (error, stdout, stderr) => {
if (error) {
@@ -55,8 +55,8 @@ export default class ScheduleService {
);
}
async cancelSchedule({ _id = '', name }: Crontab) {
async cancelSchedule({ id = 0, name }: Crontab) {
this.logger.info('[取消定时任务],任务名:%s', name);
this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel();
this.scheduleStacks.has(id) && this.scheduleStacks.get(id)?.cancel();
}
}
+22 -4
View File
@@ -2,21 +2,19 @@ import { Service, Inject } from 'typedi';
import winston from 'winston';
import { spawn } from 'child_process';
import SockService from './sock';
import CronService from './cron';
@Service()
export default class ScriptService {
constructor(
@Inject('logger') private logger: winston.Logger,
private sockService: SockService,
private cronService: CronService,
) {}
public async runScript(path: string) {
const cp = spawn(`task -l ${path} now`, { shell: '/bin/bash' });
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: `开始执行脚本`,
});
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
@@ -38,6 +36,26 @@ export default class ScriptService {
});
});
cp.on('close', (err) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: `执行结束`,
});
});
return { code: 200 };
}
public async stopScript(path: string) {
const err = await this.cronService.killTask(`task -l ${path} 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 };
}
}
+167
View File
@@ -0,0 +1,167 @@
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 && doc.info) || {};
}
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, data: '通知发送失败,请检查参数' };
}
}
public async updateLogRemoveFrequency(frequency: number) {
const result = await this.updateAuthDb({
type: AuthDataType.removeLogFrequency,
info: { frequency },
});
const cron = {
id: result.id,
name: '删除日志',
command: `ql rmlog ${frequency}`,
schedule: `5 23 */${frequency} * *`,
};
await this.scheduleService.cancelSchedule(cron);
if (frequency > 0) {
await this.scheduleService.generateSchedule(cron);
}
return { code: 200, data: { ...cron } };
}
public async checkUpdate() {
try {
const versionRegx = /.*export const version = \'(.*)\'\;/;
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
let lastVersion = '';
let lastLog = '';
try {
const result = await Promise.race([
got.get(config.lastVersionFile, { timeout: 6000, retry: 0 }),
got.get(`https://ghproxy.com/${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,
data: error.message,
};
}
}
private checkHasNewVersion(curVersion: string, lastVersion: string) {
const curArr = curVersion.split('.').map((x) => parseInt(x, 10));
const lastArr = lastVersion.split('.').map((x) => parseInt(x, 10));
if (curArr[0] < lastArr[0]) {
return true;
}
if (curArr[0] === lastArr[0] && curArr[1] < lastArr[1]) {
return true;
}
if (
curArr[0] === lastArr[0] &&
curArr[1] === lastArr[1] &&
curArr[2] < lastArr[2]
) {
return true;
}
return false;
}
public async updateSystem() {
const cp = spawn('ql -l update', { shell: '/bin/bash' });
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: `开始更新系统`,
});
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: data.toString(),
});
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: data.toString(),
});
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: JSON.stringify(err),
});
});
return { code: 200 };
}
}
+52 -71
View File
@@ -6,8 +6,7 @@ import * as fs from 'fs';
import _ from 'lodash';
import jwt from 'jsonwebtoken';
import { authenticator } from '@otplib/preset-default';
import DataStore from 'nedb';
import { AuthDataType, AuthInfo, LoginStatus } from '../data/auth';
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
import { NotificationInfo } from '../data/notify';
import NotificationService from './notify';
import { Request } from 'express';
@@ -15,13 +14,11 @@ import ScheduleService from './schedule';
import { spawn } from 'child_process';
import SockService from './sock';
import got from 'got';
import { dbs } from '../loaders/db';
@Service()
export default class UserService {
@Inject((type) => NotificationService)
private notificationService!: NotificationService;
private authDb = dbs.authDb;
constructor(
@Inject('logger') private logger: winston.Logger,
@@ -174,33 +171,24 @@ export default class UserService {
}
public async getLoginLog(): Promise<AuthInfo[]> {
return new Promise((resolve) => {
this.authDb.find({ type: AuthDataType.loginLog }).exec((err, docs) => {
if (err || docs.length === 0) {
resolve([]);
} else {
const result = docs.sort(
(a, b) => b.info.timestamp - a.info.timestamp,
);
if (result.length > 100) {
this.authDb.remove({ _id: result[result.length - 1]._id });
}
resolve(result.map((x) => x.info));
}
});
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> {
return new Promise((resolve) => {
this.authDb.insert(payload, (err, doc) => {
if (err) {
this.logger.error(err);
} else {
resolve(doc);
}
});
});
const doc = await AuthModel.create({ ...payload }, { returning: true });
return doc;
}
private initAuthInfo() {
@@ -315,48 +303,32 @@ export default class UserService {
}
public async getNotificationMode(): Promise<NotificationInfo> {
return new Promise((resolve) => {
this.authDb
.find({ type: AuthDataType.notification })
.exec((err, docs) => {
if (err || docs.length === 0) {
resolve({} as NotificationInfo);
} else {
resolve(docs[0].info);
}
});
});
const doc = await this.getDb({ type: AuthDataType.notification });
return (doc && doc.info) || {};
}
public async getLogRemoveFrequency() {
return new Promise((resolve) => {
this.authDb
.find({ type: AuthDataType.removeLogFrequency })
.exec((err, docs) => {
if (err || docs.length === 0) {
resolve({});
} else {
resolve(docs[0].info);
}
});
});
const doc = await this.getDb({ type: AuthDataType.removeLogFrequency });
return (doc && doc.info) || {};
}
private async updateAuthDb(payload: AuthInfo): Promise<any> {
return new Promise((resolve) => {
this.authDb.update(
{ type: payload.type },
{ ...payload },
{ upsert: true, returnUpdatedDocs: true },
(err, num, doc: any) => {
if (err) {
resolve({} as NotificationInfo);
} else {
resolve({ ...doc.info, _id: doc._id });
}
},
);
});
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) {
@@ -383,7 +355,7 @@ export default class UserService {
info: { frequency },
});
const cron = {
_id: result._id,
id: result.id,
name: '删除日志',
command: `ql rmlog ${frequency}`,
schedule: `5 23 */${frequency} * *`,
@@ -403,13 +375,22 @@ export default class UserService {
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
const lastVersionFileContent = await (
await got.get(config.lastVersionFile)
).body;
const lastVersion = lastVersionFileContent.match(versionRegx)![1];
const lastLog = lastVersionFileContent.match(logRegx)
? lastVersionFileContent.match(logRegx)![1]
: '';
let lastVersion = '';
let lastLog = '';
try {
const result = await Promise.race([
got.get(config.lastVersionFile, { timeout: 1000, retry: 0 }),
got.get(`https://ghproxy.com/${config.lastVersionFile}`, {
timeout: 5000,
retry: 0,
}),
]);
const lastVersionFileContent = result.body;
lastVersion = lastVersionFileContent.match(versionRegx)![1];
lastLog = lastVersionFileContent.match(logRegx)
? lastVersionFileContent.match(logRegx)![1]
: '';
} catch (error) {}
return {
code: 200,
+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;
+8 -30
View File
@@ -1,50 +1,28 @@
FROM node:lts-alpine3.12
FROM whyour/ql:base
ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}"
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
ARG QL_BRANCH=master
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
LANG=zh_CN.UTF-8 \
SHELL=/bin/bash \
PS1="\u@\h:\w \$ " \
QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH}
WORKDIR ${QL_DIR}
RUN set -x \
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \
&& apk update -f \
&& apk upgrade \
&& apk --no-cache add -f bash \
coreutils \
moreutils \
git \
curl \
wget \
tzdata \
perl \
openssl \
nginx \
python3 \
jq \
openssh \
py3-pip \
&& rm -rf /var/cache/apk/* \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& touch ~/.bashrc \
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
&& git config --global user.email "qinglong@@users.noreply.github.com" \
&& git config --global user.name "qinglong" \
RUN 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 pnpm \
&& pnpm install -g pm2 \
&& pnpm install -g ts-node typescript tslib \
&& rm -rf /root/.npm \
&& cp -rf /node_modules ./ \
&& rm -rf /node_modules \
&& pnpm install --prod \
&& rm -rf /root/.pnpm-store \
&& rm -rf /root/.cache \
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
&& cp -rf /static/* ${QL_DIR} \
&& rm -rf /static
+49
View File
@@ -0,0 +1,49 @@
FROM node:alpine
ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}"
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
LANG=zh_CN.UTF-8 \
SHELL=/bin/bash \
PS1="\u@\h:\w \$ "
COPY package.json /
RUN set -x \
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk update -f \
&& apk upgrade \
&& apk --no-cache add -f bash \
coreutils \
moreutils \
git \
curl \
wget \
tzdata \
perl \
openssl \
nginx \
python3 \
jq \
openssh \
py3-pip \
python2 \
g++ \
make \
&& rm -rf /var/cache/apk/* \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& touch ~/.bashrc \
&& git config --global user.email "qinglong@@users.noreply.github.com" \
&& git config --global user.name "qinglong" \
&& npm install -g pnpm \
&& pnpm install -g pm2 \
&& pnpm install -g ts-node typescript tslib \
&& cd / && pnpm install --prod \
&& apk --purge del python2 g++ make \
&& rm -rf /root/.npm \
&& rm -rf /root/.pnpm-store \
&& rm -rf /root/.cache \
&& rm -f /package.json
CMD [ "node" ]
+1
View File
@@ -6,6 +6,7 @@ link_shell
echo -e "======================1. 检测配置文件========================\n"
make_dir /etc/nginx/conf.d
make_dir /run/nginx
cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
pm2 l &>/dev/null
+4
View File
@@ -42,4 +42,8 @@ server {
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location ~ .*\.(html)$ {
add_header Cache-Control no-cache;
}
}
+19 -13
View File
@@ -1,14 +1,15 @@
{
"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",
"build:front": "umi build",
"build:back": "tsc -p tsconfig.back.json",
"panel": "npm run build:back && node build/app.js",
"schedule": "npm run build:back && node build/schedule.js",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"prepare": "umi generate tmp",
"test": "umi-test",
"test:coverage": "umi-test --coverage"
},
@@ -25,10 +26,11 @@
},
"dependencies": {
"@otplib/preset-default": "^12.0.1",
"@sentry/node": "^6.14.0",
"@sentry/tracing": "^6.14.0",
"@sentry/node": "^6.17.2",
"@sentry/tracing": "^6.17.2",
"body-parser": "^1.19.0",
"celebrate": "^13.0.3",
"chokidar": "^3.5.2",
"cors": "^2.8.5",
"cron-parser": "^3.5.0",
"dotenv": "^8.2.0",
@@ -46,18 +48,20 @@
"nodemailer": "^6.7.0",
"p-queue": "6.6.2",
"reflect-metadata": "^0.1.13",
"sequelize": "^7.0.0-alpha.3",
"serve-handler": "^6.1.3",
"sockjs": "^0.3.21",
"sqlite3": "^5.0.2",
"typedi": "^0.8.0",
"uuid": "^8.3.2",
"winston": "^3.3.3"
"winston": "^3.3.3",
"yargs": "^17.2.1"
},
"devDependencies": {
"@ant-design/icons": "^4.6.2",
"@ant-design/pro-layout": "^6.26.0",
"@monaco-editor/react": "^4.3.1",
"@sentry/react": "^6.14.0",
"@sentry/webpack-plugin": "^1.18.3",
"@sentry/react": "^6.17.2",
"@types/cors": "^2.8.10",
"@types/express": "^4.17.8",
"@types/express-jwt": "^6.0.1",
@@ -74,12 +78,14 @@
"@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1",
"@types/uuid": "^8.3.3",
"@umijs/plugin-antd": "^0.11.0",
"@umijs/test": "^3.3.9",
"antd": "^4.17.0-alpha.6",
"codemirror": "^5.62.2",
"compression-webpack-plugin": "6.1.1",
"darkreader": "^4.9.27",
"concurrently": "^7.0.0",
"darkreader": "4.9.40",
"lint-staged": "^10.0.7",
"nodemon": "^2.0.4",
"prettier": "^2.2.0",
+14 -9
View File
@@ -16,8 +16,8 @@ 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"
@@ -35,6 +35,9 @@ EnableExtraShell="true"
## 是否自动启动bot,默认不启动,设置为true时自动启动,目前需要自行克隆bot仓库所需代码,存到ql/repo目录下,文件夹命名为dockerbot
AutoStartBot=""
## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
BotRepoUrl=""
## 安装bot依赖时指定pip源,默认使用清华源,如不需要源,设置此参数为空
PipMirror="https://pypi.tuna.tsinghua.edu.cn/simple"
@@ -47,12 +50,14 @@ export PUSH_KEY=""
## 2. BARK
## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123
export BARK_PUSH=""
## 下方填写推送图标设置,自定义推送图标(需iOS15或以上)
export BARK_ICON="https://img.gejiba.com/images/a3f551e09ac19add4c49ec16228729c5.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
@@ -73,7 +78,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=""
@@ -104,9 +109,9 @@ export PUSH_PLUS_TOKEN=""
export PUSH_PLUS_USER=""
## 9. go-cqhttp
## gobot_url 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
## gobot_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=""
@@ -116,8 +121,8 @@ export GOBOT_QQ=""
## 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;
export GOTIFY_URL=""
export GOTIFY_TOKEN=""
export GOTIFY_PRIORITY=0
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+22 -12
View File
@@ -25,9 +25,9 @@ let GOTIFY_PRIORITY = 0;
//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.
@@ -37,6 +37,9 @@ let SCKEY = '';
// =======================================Bark App通知设置区域===========================================
//此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX)
let BARK_PUSH = '';
//BARK app推送图标,自定义推送图标(需iOS15或以上)
let BARK_ICON =
'https://img.gejiba.com/images/a3f551e09ac19add4c49ec16228729c5.png';
//BARK app推送铃声,铃声列表去APP查看复制填写
let BARK_SOUND = '';
//BARK app推送消息的分组, 默认为"QingLong"
@@ -133,6 +136,9 @@ 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;
}
@@ -216,8 +222,8 @@ async function sendNotify(
qywxBotNotify(text, desp), //企业微信机器人
qywxamNotify(text, desp), //企业微信应用消息推送
iGotNotify(text, desp, params), //iGot
gobotNotify(text, desp),//go-cqhttp
gotifyNotify(text, desp),//gotify
gobotNotify(text, desp), //go-cqhttp
gotifyNotify(text, desp), //gotify
]);
}
@@ -226,10 +232,12 @@ function gotifyNotify(text, desp) {
if (GOTIFY_URL && GOTIFY_TOKEN) {
const options = {
url: `${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}`,
body: `title=${encodeURIComponent(text)}&message=${encodeURIComponent(desp)}&priority=${GOTIFY_PRIORITY}`,
body: `title=${encodeURIComponent(text)}&message=${encodeURIComponent(
desp,
)}&priority=${GOTIFY_PRIORITY}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
}
},
};
$.post(options, (err, resp, data) => {
try {
@@ -261,7 +269,7 @@ function gobotNotify(text, desp, time = 2100) {
if (GOBOT_URL) {
const options = {
url: `${GOBOT_URL}?access_token=${GOBOT_TOKEN}&${GOBOT_QQ}`,
json: {message:`${text}\n${desp}`},
json: { message: `${text}\n${desp}` },
headers: {
'Content-Type': 'application/json',
},
@@ -429,7 +437,9 @@ function BarkNotify(text, desp, params = {}) {
const options = {
url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent(
desp,
)}?sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(params)}`,
)}?icon=${BARK_ICON}?sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(
params,
)}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
@@ -726,8 +736,8 @@ function qywxamNotify(text, desp) {
if (err) {
console.log(
'成员ID:' +
ChangeUserId(desp) +
'企业微信应用消息发送通知消息失败!!\n',
ChangeUserId(desp) +
'企业微信应用消息发送通知消息失败!!\n',
);
console.log(err);
} else {
@@ -735,8 +745,8 @@ function qywxamNotify(text, desp) {
if (data.errcode === 0) {
console.log(
'成员ID:' +
ChangeUserId(desp) +
'企业微信应用消息发送通知消息成功🎉。\n',
ChangeUserId(desp) +
'企业微信应用消息发送通知消息成功🎉。\n',
);
} else {
console.log(`${data.errmsg}\n`);
+3
View File
@@ -35,6 +35,7 @@ push_config = {
'BARK_ARCHIVE': '', # bark 推送是否存档
'BARK_GROUP': '', # bark 推送分组
'BARK_SOUND': '', # bark 推送声音
'BARK_ICON': '', # bark 推送图标
'CONSOLE': True, # 控制台输出
@@ -104,6 +105,7 @@ def bark(title: str, content: str) -> None:
"BARK_ARCHIVE": "isArchive",
"BARK_GROUP": "group",
"BARK_SOUND": "sound",
"BARK_ICON": "icon",
}
params = ""
for pair in filter(
@@ -285,6 +287,7 @@ def pushplus_bot(title: str, content: str) -> None:
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:
+2 -2
View File
@@ -61,7 +61,7 @@ update_cron_api() {
-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)
@@ -93,7 +93,7 @@ update_cron_command_api() {
-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)
+8 -2
View File
@@ -3,8 +3,14 @@
## 导入通用变量与函数
dir_shell=/ql/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
+2 -4
View File
@@ -10,13 +10,11 @@ reset_env() {
echo -e "---> 配置文件检测完成\n"
echo -e "---> 2. 开始安装青龙依赖\n"
rm -rf $dir_root/node_modules
npm_install_2 $dir_root
echo -e "---> 青龙依赖安装完成\n"
echo -e "---> 3. 开始安装脚本依赖\n"
cp -f $dir_sample/package.json $dir_scripts/package.json
rm -rf $dir_scripts/node_modules
npm_install_2 $dir_scripts
echo -e "---> 脚本依赖安装完成\n"
}
@@ -50,8 +48,8 @@ pm2_log() {
echo -e "---> pm2日志"
local panelOut="/root/.pm2/logs/panel-out.log"
local panelError="/root/.pm2/logs/panel-error.log"
tail -n 20 "$panelOut"
tail -n 20 "$panelError"
tail -n 100 "$panelOut"
tail -n 100 "$panelError"
}
check_nginx() {
+39 -1
View File
@@ -35,6 +35,8 @@ 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
@@ -58,6 +60,7 @@ original_name=(
init_env() {
export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules
export PYTHONUNBUFFERED=1
}
import_config() {
@@ -65,8 +68,9 @@ import_config() {
[[ -f $file_env ]] && . $file_env
command_timeout_time=${CommandTimeoutTime:-"1h"}
github_proxy_url=${GithubProxyUrl:-""}
proxy_url=${ProxyUrl:-""}
file_extensions=${RepoFileExtensions:-"js py"}
current_branch=${QL_BRANCH}
if [[ -n "${DefaultCronRule}" ]]; then
default_cron="${DefaultCronRule}"
@@ -75,6 +79,18 @@ import_config() {
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
}
make_dir() {
local dir=$1
if [[ ! -d $dir ]]; then
@@ -210,9 +226,23 @@ fix_config() {
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_sub() {
set_proxy
if [ $is_termux -eq 1 ]; then
npm install --production --no-bin-links --registry=https://registry.npm.taobao.org || npm install --production --no-bin-links
elif ! type pnpm &>/dev/null; then
@@ -220,6 +250,7 @@ npm_install_sub() {
else
pnpm install --production --registry=https://registry.npm.taobao.org || pnpm install --production
fi
unset_proxy
}
npm_install_1() {
@@ -272,8 +303,11 @@ git_clone_scripts() {
local branch=$3
[[ $branch ]] && local part_cmd="-b $branch "
echo -e "开始克隆仓库 $url$dir\n"
set_proxy
git clone $part_cmd $url $dir
exit_status=$?
unset_proxy
}
git_pull_scripts() {
@@ -283,10 +317,14 @@ git_pull_scripts() {
[[ $branch ]] && local part_cmd="origin/${branch}"
cd $dir_work
echo -e "开始更新仓库:$dir_work\n"
set_proxy
git fetch --all
exit_status=$?
git reset --hard $part_cmd
git pull
unset_proxy
cd $dir_current
}
+32 -19
View File
@@ -10,7 +10,7 @@ define_program() {
local first_param=$1
if [[ $first_param == *.js ]]; then
which_program="node"
elif [[ $first_param == *.py ]]; then
elif [[ $first_param == *.py ]] || [[ $first_param == *.pyc ]]; then
which_program="python3"
elif [[ $first_param == *.sh ]]; then
which_program="bash"
@@ -83,7 +83,7 @@ run_normal() {
random_delay
fi
fi
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
log_dir_tmp="${first_param##*/}"
log_dir_tmp_path="${first_param%%/*}"
@@ -96,7 +96,7 @@ run_normal() {
make_dir "$log_dir"
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
local begin_timestamp=$(date "+%s" -d "$begin_time")
local begin_timestamp=$(date "+%s")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
@@ -114,7 +114,7 @@ run_normal() {
eval . $file_task_after "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s" -d "$end_time")
local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
@@ -141,7 +141,7 @@ run_concurrent() {
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
define_program "$first_param"
@@ -157,7 +157,7 @@ run_concurrent() {
make_dir $log_dir
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
local begin_timestamp=$(date "+%s" -d "$begin_time")
local begin_timestamp=$(date "+%s")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
@@ -193,7 +193,7 @@ run_concurrent() {
eval . $file_task_after "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s" -d "$end_time")
local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
@@ -221,7 +221,7 @@ run_designated() {
make_dir $log_dir
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
local begin_timestamp=$(date "+%s" -d "$begin_time")
local begin_timestamp=$(date "+%s")
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
@@ -234,7 +234,7 @@ run_designated() {
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
@@ -256,7 +256,7 @@ run_designated() {
eval . $file_task_after "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s" -d "$end_time")
local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
@@ -264,29 +264,42 @@ run_designated() {
## 运行其他命令
run_else() {
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
local log_dir_tmp="${1##*/}"
local log_dir="$dir_log/${log_dir_tmp%%.*}"
local file_param="$1"
define_program "$file_param"
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
log_dir_tmp="${file_param##*/}"
log_dir_tmp_path="${file_param%%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="$dir_log/${log_dir_tmp%%.*}"
log_path="$log_dir/$log_time.log"
cmd="&>> $log_path"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$log_dir"
make_dir $log_dir
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
local begin_timestamp=$(date "+%s" -d "$begin_time")
local begin_timestamp=$(date "+%s")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $first_param" | perl -pe "s|.*ID=(.*) $cmd_task $first_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
local id=$(cat $list_crontab_user | grep -E "$cmd_task $@" | perl -pe "s|.*ID=(.*) $cmd_task $@\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
eval timeout -k 10s $command_timeout_time "$@" $cmd
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
eval . $file_task_after "$@" $cmd
shift
eval timeout -k 10s $command_timeout_time $which_program "$file_param" "$@" $cmd
eval . $file_task_after "$file_param" "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s" -d "$end_time")
local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
+12 -24
View File
@@ -151,11 +151,6 @@ update_repo() {
make_dir "${dir_scripts}/${uniq_path}"
local formatUrl="$url"
if [[ $github_proxy_url ]]; then
if [[ $url =~ "github.com" ]] || [[ $url =~ "githubusercontent.com" ]]; then
[[ ! $url =~ "https://ghproxy.com" ]] && formatUrl="${github_proxy_url}${url}"
fi
fi
if [[ -d ${repo_path}/.git ]]; then
reset_romote_url ${repo_path} "${formatUrl}" "${branch}"
git_pull_scripts ${repo_path} "${branch}"
@@ -166,15 +161,7 @@ update_repo() {
echo -e "\n更新${repo_path}成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence"
else
echo -e "\n更新${repo_path}失败,重新下载全新仓库...\n"
rm -rf ${repo_path}
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}"
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新${repo_path}成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence"
else
echo -e "\n更新${repo_path}失败,请检查网络...\n"
fi
echo -e "\n更新${repo_path}失败,请检查网络...\n"
fi
}
@@ -183,15 +170,14 @@ update_raw() {
echo -e "--------------------------------------------------------------\n"
local url="$1"
local raw_url="$url"
if [[ $github_proxy_url ]]; then
if [[ $url =~ "github.com" ]] || [[ $url =~ "githubusercontent.com" ]]; then
[[ ! $url =~ "https://ghproxy.com" ]] && raw_url="${github_proxy_url}${url}"
fi
fi
local suffix="${raw_url##*.}"
local raw_file_name="${uniq_path}.${suffix}"
echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
set_proxy
wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
unset_proxy
if [[ $? -eq 0 ]]; then
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
echo -e "下载 ${raw_file_name} 成功...\n"
@@ -259,8 +245,8 @@ update_qinglong() {
local no_restart="$1"
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
reset_romote_url ${dir_root} "${github_proxy_url}https://github.com/whyour/qinglong.git" "master"
git_pull_scripts $dir_root "master"
reset_romote_url ${dir_root} "https://github.com/whyour/qinglong.git" ${current_branch}
git_pull_scripts $dir_root ${current_branch}
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新$dir_root成功...\n"
@@ -274,10 +260,10 @@ update_qinglong() {
echo -e "\n更新$dir_root失败,请检查原因...\n"
fi
local url="${github_proxy_url}https://github.com/whyour/qinglong-static.git"
local url="https://github.com/whyour/qinglong-static.git"
if [[ -d ${ql_static_repo}/.git ]]; then
reset_romote_url ${ql_static_repo} ${url} "master"
git_pull_scripts ${ql_static_repo} "master"
reset_romote_url ${ql_static_repo} ${url} ${current_branch}
git_pull_scripts ${ql_static_repo} ${current_branch}
else
git_clone_scripts ${url} ${ql_static_repo}
fi
@@ -313,6 +299,8 @@ patch_version() {
fi
git config --global pull.rebase false
cp -f /ql/.env.example /ql/.env
}
reload_pm2() {
-1
View File
@@ -79,6 +79,5 @@ export default {
fixSiderbar: true,
contentWidth: 'Fixed',
splitMenus: false,
logo: 'https://z3.ax1x.com/2021/11/18/I7MpAe.png',
siderWidth: 180,
} as any;
+20 -2
View File
@@ -152,8 +152,8 @@
.ant-layout-content.ant-pro-basicLayout-content.ant-pro-basicLayout-has-header {
margin-bottom: 0 !important;
min-height: calc(100vh - 66px);
min-height: calc(100vh - var(--vh-offset, 0px) - 66px);
min-height: calc(100vh - 72px);
min-height: calc(100vh - var(--vh-offset, 0px) - 72px);
}
.Resizer {
@@ -293,3 +293,21 @@
color: #1890ff;
}
}
.ant-pro-sider-logo {
padding: 16px 8px !important;
h1 {
margin-left: 5px !important;
}
img {
width: 32px !important;
border-radius: 52% !important;
}
}
.ant-pro-global-header-logo {
a img {
// 移动端logo被拉伸
width: auto !important;
}
}
+19 -11
View File
@@ -20,14 +20,14 @@ import './index.less';
import vhCheck from 'vh-check';
import { version, changeLogLink, changeLog } from '../version';
import { useCtx, useTheme } from '@/utils/hooks';
import { message, Badge, Modal, Avatar, Dropdown, Menu, Popover } from 'antd';
import { message, Badge, Modal, Avatar, Dropdown, Menu, Image } from 'antd';
// @ts-ignore
import SockJS from 'sockjs-client';
import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing';
Sentry.init({
dsn: 'https://ea2fede373244db99c536210b910d9da@o1051273.ingest.sentry.io/6047851',
dsn: 'https://3406424fb1dc4813a62d39e844a9d0ac@o1098464.ingest.sentry.io/6122818',
integrations: [new Integrations.BrowserTracing()],
release: version,
tracesSampleRate: 1.0,
@@ -44,7 +44,7 @@ export default function (props: any) {
const [collapsed, setCollapsed] = useState(false);
const logout = () => {
request.post(`${config.apiPrefix}logout`).then(() => {
request.post(`${config.apiPrefix}user/logout`).then(() => {
localStorage.removeItem(config.authKey);
history.push('/login');
});
@@ -232,6 +232,12 @@ export default function (props: any) {
selectedKeys={[props.location.pathname]}
loading={loading}
ErrorBoundary={Sentry.ErrorBoundary}
logo={
<Image
preview={false}
src="https://img.gejiba.com/images/a3f551e09ac19add4c49ec16228729c5.png"
/>
}
title={
<>
<span style={{ fontSize: 16 }}></span>
@@ -275,14 +281,16 @@ export default function (props: any) {
}}
onCollapse={setCollapsed}
collapsed={collapsed}
rightContentRender={() => (
<Dropdown overlay={menu} trigger={['click']}>
<span className="side-menu-user-wrapper">
<Avatar shape="square" size="small" icon={<UserOutlined />} />
<span style={{ marginLeft: 5 }}>admin</span>
</span>
</Dropdown>
)}
rightContentRender={() =>
ctx.isPhone && (
<Dropdown overlay={menu} trigger={['click']}>
<span className="side-menu-user-wrapper">
<Avatar shape="square" size="small" icon={<UserOutlined />} />
<span style={{ marginLeft: 5 }}>admin</span>
</span>
</Dropdown>
)
}
collapsedButtonRender={(collapsed) => (
<span
className="side-menu-container"
-8
View File
@@ -1,11 +1,3 @@
.ant-table-pagination.ant-pagination {
margin-bottom: 0 !important;
}
tr {
td:nth-child(2) {
span {
cursor: pointer;
}
}
}
+97 -41
View File
@@ -11,6 +11,7 @@ import {
Menu,
Typography,
Input,
Popover,
} from 'antd';
import {
ClockCircleOutlined,
@@ -30,7 +31,7 @@ import {
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http';
import CronModal from './modal';
import CronModal, { CronLabelModal } from './modal';
import CronLogModal from './logModal';
import cron_parser from 'cron-parser';
import { diffTime } from '@/utils/date';
@@ -48,7 +49,7 @@ enum CrontabStatus {
'queued',
}
const CrontabSort: any = { 0: 0, 3: 1, 1: 2, 4: 3 };
const CrontabSort: any = { 0: 0, 5: 1, 3: 2, 1: 3, 4: 4 };
enum OperationName {
'启用',
@@ -77,20 +78,48 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
width: 150,
align: 'center' as const,
render: (text: string, record: any) => (
<span
onClick={() => {
goToScriptManager(record);
}}
>
{record.name || record._id}{' '}
{record.isPinned ? (
<span>
<PushpinOutlined />
</span>
) : (
''
)}
</span>
<>
<a
onClick={() => {
goToScriptManager(record);
}}
>
{record.name || record._id}{' '}
{record.isPinned ? (
<span>
<PushpinOutlined />
</span>
) : (
''
)}
</a>
<span>
{record.labels?.length > 0 && record.labels[0] !== '' ? (
<Popover
placement="right"
trigger={isPhone ? 'click' : 'hover'}
content={
<div>
{record.labels?.map((label: string, i: number) => (
<Tag
color="blue"
onClick={() => {
onSearch(`label:${label}`);
}}
>
{label}
</Tag>
))}
</div>
}
>
<Tag color="blue">{record.labels[0]}</Tag>
</Popover>
) : (
''
)}
</span>
</>
),
sorter: {
compare: (a: any, b: any) => a.name.localeCompare(b.name),
@@ -325,6 +354,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
const [value, setValue] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [isModalVisible, setIsModalVisible] = useState(false);
const [isLabelModalVisible, setisLabelModalVisible] = useState(false);
const [editedCron, setEditedCron] = useState();
const [searchText, setSearchText] = useState('');
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
@@ -335,8 +365,12 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const goToScriptManager = (record: any) => {
const cmd = record.command.split(' ');
const cmd = record.command.split(' ') as string[];
if (cmd[0] === 'task') {
if (cmd[1].startsWith('/ql/scripts')) {
cmd[1] = cmd[1].replace('/ql/scripts/', '');
}
let [p, s] = cmd[1].split('/');
if (!s) {
s = p;
@@ -356,14 +390,19 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
setValue(
data.data
.sort((a: any, b: any) => {
const sortA = a.isDisabled ? 4 : a.status;
const sortB = b.isDisabled ? 4 : b.status;
a.isPinned = a.isPinned ? a.isPinned : 0;
b.isPinned = b.isPinned ? b.isPinned : 0;
if (a.isPinned === b.isPinned) {
return CrontabSort[sortA] - CrontabSort[sortB];
}
return b.isPinned - a.isPinned;
const sortA =
a.isPinned && a.status !== 0
? 5
: a.isDisabled && a.status !== 0
? 4
: a.status;
const sortB =
b.isPinned && b.status !== 0
? 5
: b.isDisabled && b.status !== 0
? 4
: b.status;
return CrontabSort[sortA] - CrontabSort[sortB];
})
.map((x) => {
return {
@@ -404,12 +443,12 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
),
onOk() {
request
.delete(`${config.apiPrefix}crons`, { data: [record._id] })
.delete(`${config.apiPrefix}crons`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
message.success('删除成功');
const result = [...value];
const i = result.findIndex((x) => x._id === record._id);
const i = result.findIndex((x) => x.id === record.id);
result.splice(i, 1);
setValue(result);
} else {
@@ -437,11 +476,11 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
),
onOk() {
request
.put(`${config.apiPrefix}crons/run`, { data: [record._id] })
.put(`${config.apiPrefix}crons/run`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
const result = [...value];
const i = result.findIndex((x) => x._id === record._id);
const i = result.findIndex((x) => x.id === record.id);
result.splice(i, 1, {
...record,
status: CrontabStatus.running,
@@ -472,11 +511,11 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
),
onOk() {
request
.put(`${config.apiPrefix}crons/stop`, { data: [record._id] })
.put(`${config.apiPrefix}crons/stop`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
const result = [...value];
const i = result.findIndex((x) => x._id === record._id);
const i = result.findIndex((x) => x.id === record.id);
result.splice(i, 1, {
...record,
pid: null,
@@ -514,14 +553,14 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
record.isDisabled === 1 ? 'enable' : 'disable'
}`,
{
data: [record._id],
data: [record.id],
},
)
.then((data: any) => {
if (data.code === 200) {
const newStatus = record.isDisabled === 1 ? 0 : 1;
const result = [...value];
const i = result.findIndex((x) => x._id === record._id);
const i = result.findIndex((x) => x.id === record.id);
result.splice(i, 1, {
...record,
isDisabled: newStatus,
@@ -558,14 +597,14 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
record.isPinned === 1 ? 'unpin' : 'pin'
}`,
{
data: [record._id],
data: [record.id],
},
)
.then((data: any) => {
if (data.code === 200) {
const newStatus = record.isPinned === 1 ? 0 : 1;
const result = [...value];
const i = result.findIndex((x) => x._id === record._id);
const i = result.findIndex((x) => x.id === record.id);
result.splice(i, 1, {
...record,
isPinned: newStatus,
@@ -657,7 +696,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
};
const handleCrons = (cron: any) => {
const index = value.findIndex((x) => x._id === cron._id);
const index = value.findIndex((x) => x.id === cron.id);
const result = [...value];
cron.nextRunTime = cron_parser
.parseExpression(cron.schedule)
@@ -675,9 +714,9 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
const getCronDetail = (cron: any) => {
request
.get(`${config.apiPrefix}crons/${cron._id}`)
.get(`${config.apiPrefix}crons/${cron.id}`)
.then((data: any) => {
const index = value.findIndex((x) => x._id === cron._id);
const index = value.findIndex((x) => x.id === cron.id);
const result = [...value];
data.data.nextRunTime = cron_parser
.parseExpression(data.data.schedule)
@@ -770,7 +809,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
useEffect(() => {
if (logCron) {
localStorage.setItem('logCron', logCron._id);
localStorage.setItem('logCron', logCron.id);
setIsLogModalVisible(true);
}
}, [logCron]);
@@ -799,7 +838,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
onSearch={onSearch}
/>,
<Button key="2" type="primary" onClick={() => addCron()}>
</Button>,
]}
header={{
@@ -849,6 +888,13 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
>
</Button>
<Button
type="primary"
onClick={() => setisLabelModalVisible(true)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
@@ -869,7 +915,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
pageSizeOptions: [10, 20, 50, 100, 200, 500, 1000],
}}
dataSource={value}
rowKey="_id"
rowKey="id"
size="middle"
scroll={{ x: 1000, y: tableScrollHeight }}
loading={loading}
@@ -889,6 +935,16 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
handleCancel={handleCancel}
cron={editedCron}
/>
<CronLabelModal
visible={isLabelModalVisible}
handleCancel={(needUpdate?: boolean) => {
setisLabelModalVisible(false);
if (needUpdate) {
getCrons();
}
}}
ids={selectedRowIds}
/>
</PageContainer>
);
};
+5 -5
View File
@@ -36,9 +36,9 @@ const CronLogModal = ({
setLoading(true);
}
request
.get(`${config.apiPrefix}crons/${cron._id}/log`)
.get(`${config.apiPrefix}crons/${cron.id}/log`)
.then((data: any) => {
if (localStorage.getItem('logCron') === cron._id) {
if (localStorage.getItem('logCron') === String(cron.id)) {
const log = data.data as string;
setValue(log || '暂无日志');
setExecuting(
@@ -61,7 +61,7 @@ const CronLogModal = ({
<Countdown
className="inline-countdown"
format="ss"
value={Date.now() + 1000 * 10}
value={Date.now() + 1000 * 30}
/>
</span>
@@ -70,7 +70,7 @@ const CronLogModal = ({
});
setTimeout(() => {
window.location.reload();
}, 10000);
}, 30000);
}
}
})
@@ -90,7 +90,7 @@ const CronLogModal = ({
return (
<>
{(executing || loading) && <Loading3QuartersOutlined spin />}
{!executing && <CheckCircleOutlined />}
{!executing && !loading && <CheckCircleOutlined />}
<span style={{ marginLeft: 5 }}>-{cron && cron.name}</span>{' '}
</>
);
+99 -13
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form } from 'antd';
import { Modal, message, Input, Form, Button } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import cronParse from 'cron-parser';
@@ -20,19 +20,28 @@ const CronModal = ({
setLoading(true);
const method = cron ? 'put' : 'post';
const payload = { ...values };
if (typeof payload.labels === 'string') {
payload.labels = values.labels.split(/,|/);
} else if (!payload.labels) {
payload.labels = [];
}
if (cron) {
payload._id = cron._id;
payload.id = cron.id;
}
const { code, data } = await request[method](`${config.apiPrefix}crons`, {
data: payload,
});
if (code === 200) {
message.success(cron ? '更新Cron成功' : '添加Cron成功');
} else {
message.error(data);
try {
const { code, data } = await request[method](`${config.apiPrefix}crons`, {
data: payload,
});
if (code === 200) {
message.success(cron ? '更新Cron成功' : '新建Cron成功');
} else {
message.error(data);
}
setLoading(false);
handleCancel(data);
} catch (error: any) {
setLoading(false);
}
setLoading(false);
handleCancel(data);
};
useEffect(() => {
@@ -41,7 +50,7 @@ const CronModal = ({
return (
<Modal
title={cron ? '编辑定时' : '新建定时'}
title={cron ? '编辑任务' : '新建任务'}
visible={visible}
forceRender
onOk={() => {
@@ -95,9 +104,86 @@ const CronModal = ({
>
<Input placeholder="秒(可选) 分 时 天 月 周" />
</Form.Item>
<Form.Item name="labels" label="标签">
<Input placeholder="请输入任务标签" />
</Form.Item>
</Form>
</Modal>
);
};
export default CronModal;
const CronLabelModal = ({
ids,
handleCancel,
visible,
}: {
ids: Array<string>;
visible: boolean;
handleCancel: (needUpdate?: boolean) => void;
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const update = async (action: 'delete' | 'post') => {
form
.validateFields()
.then(async (values) => {
if (typeof values.labels === 'string') {
values.labels = values.labels.split(/,|/);
}
setLoading(true);
const payload = { ids, labels: values.labels };
const { code, data } = await request[action](
`${config.apiPrefix}crons/labels`,
{
data: payload,
},
);
if (code === 200) {
message.success(
action === 'post' ? '添加Labels成功' : '删除Labels成功',
);
} else {
message.error(data);
}
setLoading(false);
handleCancel(true);
})
.catch((info) => {
console.log('Validate Failed:', info);
});
};
useEffect(() => {
form.resetFields();
}, [ids, visible]);
const buttons = [
<Button onClick={() => handleCancel(false)}></Button>,
<Button type="primary" danger onClick={() => update('delete')}>
</Button>,
<Button type="primary" onClick={() => update('post')}>
</Button>,
];
return (
<Modal
title="批量修改标签"
visible={visible}
footer={buttons}
forceRender
onCancel={() => handleCancel(false)}
confirmLoading={loading}
>
<Form form={form} layout="vertical" name="form_in_label_modal">
<Form.Item name="labels" label="标签">
<Input placeholder="请输入任务标签" />
</Form.Item>
</Form>
</Modal>
);
};
export { CronModal as default, CronLabelModal };
+36 -15
View File
@@ -82,13 +82,34 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
);
},
},
{
title: '备注',
dataIndex: 'remark',
key: 'remark',
align: 'center' as const,
},
{
title: '创建时间',
key: 'created',
dataIndex: 'created',
key: 'timestamp',
dataIndex: 'timestamp',
align: 'center' as const,
render: (text: string, record: any) => {
return <span>{new Date(record.created).toLocaleString()}</span>;
const language = navigator.language || navigator.languages[0];
const time = record.createdAt || record.timestamp;
const date = new Date(time)
.toLocaleString(language, {
hour12: false,
})
.replace(' 24:', ' 00:');
return (
<Tooltip
placement="topLeft"
title={date}
trigger={['hover', 'click']}
>
<span>{date}</span>
</Tooltip>
);
},
},
{
@@ -175,7 +196,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
),
onOk() {
request
.delete(`${config.apiPrefix}dependencies`, { data: [record._id] })
.delete(`${config.apiPrefix}dependencies`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
handleDependence(data.data[0]);
@@ -205,7 +226,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
onOk() {
request
.put(`${config.apiPrefix}dependencies/reinstall`, {
data: [record._id],
data: [record.id],
})
.then((data: any) => {
if (data.code === 200) {
@@ -231,7 +252,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
if (Array.isArray(dependence)) {
result.push(...dependence);
} else {
const index = value.findIndex((x) => x._id === dependence._id);
const index = value.findIndex((x) => x.id === dependence.id);
result.splice(index, 1, {
...dependence,
});
@@ -278,9 +299,9 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
const getDependenceDetail = (dependence: any) => {
request
.get(`${config.apiPrefix}dependencies/${dependence._id}`)
.get(`${config.apiPrefix}dependencies/${dependence.id}`)
.then((data: any) => {
const index = value.findIndex((x) => x._id === dependence._id);
const index = value.findIndex((x) => x.id === dependence.id);
const result = [...value];
result.splice(index, 1, {
...dependence,
@@ -307,7 +328,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
useEffect(() => {
if (logDependence) {
localStorage.setItem('logDependence', logDependence._id);
localStorage.setItem('logDependence', logDependence.id);
setIsLogModalVisible(true);
}
}, [logDependence]);
@@ -328,7 +349,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
}
const result = [...value];
for (let i = 0; i < references.length; i++) {
const index = value.findIndex((x) => x._id === references[i]);
const index = value.findIndex((x) => x.id === references[i]);
result.splice(index, 1, {
...result[index],
status,
@@ -340,7 +361,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
setTimeout(() => {
const _result = [...value];
for (let i = 0; i < references.length; i++) {
const index = value.findIndex((x) => x._id === references[i]);
const index = value.findIndex((x) => x.id === references[i]);
_result.splice(index, 1);
}
setValue(_result);
@@ -372,7 +393,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
rowSelection={rowSelection}
pagination={false}
dataSource={value}
rowKey="_id"
rowKey="id"
size="middle"
scroll={{ x: 768, y: tableScrollHeight }}
loading={loading}
@@ -398,7 +419,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
onSearch={onSearch}
/>,
<Button key="2" type="primary" onClick={() => addDependence()}>
</Button>,
]}
header={{
@@ -432,11 +453,11 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
handleCancel={(needRemove?: boolean) => {
setIsLogModalVisible(false);
if (needRemove) {
const index = value.findIndex((x) => x._id === logDependence._id);
const index = value.findIndex((x) => x.id === logDependence.id);
const result = [...value];
result.splice(index, 1);
setValue(result);
} else if ([...value].map((x) => x._id).includes(logDependence._id)) {
} else if ([...value].map((x) => x.id).includes(logDependence.id)) {
getDependenceDetail(logDependence);
}
}}
+5 -5
View File
@@ -46,10 +46,10 @@ const DependenceLogModal = ({
const getDependenceLog = () => {
setLoading(true);
request
.get(`${config.apiPrefix}dependencies/${dependence._id}`)
.get(`${config.apiPrefix}dependencies/${dependence.id}`)
.then((data: any) => {
if (localStorage.getItem('logDependence') === dependence._id) {
const log = (data.data.log || []).join('\n') as string;
if (localStorage.getItem('logDependence') === String(dependence.id)) {
const log = (data.data.log || []).join('') as string;
setValue(log);
setExecuting(!log.includes('结束时间'));
setIsRemoveFailed(log.includes('删除失败'));
@@ -64,7 +64,7 @@ const DependenceLogModal = ({
setRemoveLoading(true);
request
.delete(`${config.apiPrefix}dependencies/force`, {
data: [dependence._id],
data: [dependence.id],
})
.then((data: any) => {
cancel(true);
@@ -99,7 +99,7 @@ const DependenceLogModal = ({
setExecuting(false);
setIsRemoveFailed(message.includes('删除失败'));
}
setValue(`${value} \n ${message}`);
setValue(`${value}${message}`);
}, [socketMessage]);
useEffect(() => {
+4 -3
View File
@@ -26,7 +26,7 @@ const DependenceModal = ({
const handleOk = async (values: any) => {
setLoading(true);
const { name, split, type } = values;
const { name, split, type, remark } = values;
const method = dependence ? 'put' : 'post';
let payload;
if (!dependence) {
@@ -36,13 +36,14 @@ const DependenceModal = ({
return {
name: x,
type,
remark,
};
});
} else {
payload = [{ name, type }];
payload = [{ name, type, remark }];
}
} else {
payload = { ...values, _id: dependence._id };
payload = { ...values, id: dependence.id };
}
try {
const { code, data } = await request[method](
+1
View File
@@ -13,6 +13,7 @@
}
.diff-switch-file {
min-width: 768px;
.ant-form-item {
margin-bottom: 8px;
}
+17 -9
View File
@@ -101,7 +101,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
{
title: '序号',
align: 'center' as const,
width: 50,
width: 60,
render: (text: string, record: any, index: number) => {
return <span style={{ cursor: 'text' }}>{index + 1} </span>;
},
@@ -149,9 +149,17 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
ellipsis: {
showTitle: false,
},
sorter: {
compare: (a: any, b: any) => {
const updatedAtA = new Date(a.updatedAt || a.timestamp).getTime();
const updatedAtB = new Date(b.updatedAt || b.timestamp).getTime();
return updatedAtA - updatedAtB;
},
},
render: (text: string, record: any) => {
const language = navigator.language || navigator.languages[0];
const date = new Date(text)
const time = record.updatedAt || record.timestamp;
const date = new Date(time)
.toLocaleString(language, {
hour12: false,
})
@@ -197,7 +205,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
{
title: '操作',
key: 'action',
width: 100,
width: 120,
align: 'center' as const,
render: (text: string, record: any, index: number) => {
const isPc = !isPhone;
@@ -270,7 +278,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
record.status === Status. ? 'enable' : 'disable'
}`,
{
data: [record._id],
data: [record.id],
},
)
.then((data: any) => {
@@ -321,7 +329,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
),
onOk() {
request
.delete(`${config.apiPrefix}envs`, { data: [record._id] })
.delete(`${config.apiPrefix}envs`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
message.success('删除成功');
@@ -351,7 +359,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
const handleEnv = (env: any) => {
const result = [...value];
const index = value.findIndex((x) => x._id === env._id);
const index = value.findIndex((x) => x.id === env.id);
if (index === -1) {
env = Array.isArray(env) ? env : [env];
result.push(...env);
@@ -376,7 +384,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
}
const dragRow = value[dragIndex];
request
.put(`${config.apiPrefix}envs/${dragRow._id}/move`, {
.put(`${config.apiPrefix}envs/${dragRow.id}/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex },
})
.then((data: any) => {
@@ -485,7 +493,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
onSearch={onSearch}
/>,
<Button key="2" type="primary" onClick={() => addEnv()}>
</Button>,
]}
header={{
@@ -534,7 +542,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
rowSelection={rowSelection}
pagination={false}
dataSource={value}
rowKey="_id"
rowKey="id"
size="middle"
scroll={{ x: 1000, y: tableScrollHeight }}
components={components}
+15 -11
View File
@@ -34,18 +34,22 @@ const EnvModal = ({
payload = [{ value, name, remarks }];
}
} else {
payload = { ...values, _id: env._id };
payload = { ...values, id: env.id };
}
const { code, data } = await request[method](`${config.apiPrefix}envs`, {
data: payload,
});
if (code === 200) {
message.success(env ? '更新变量成功' : '添加变量成功');
} else {
message.error(data);
try {
const { code, data } = await request[method](`${config.apiPrefix}envs`, {
data: payload,
});
if (code === 200) {
message.success(env ? '更新变量成功' : '新建变量成功');
} else {
message.error(data);
}
setLoading(false);
handleCancel(data);
} catch (error: any) {
setLoading(false);
}
setLoading(false);
handleCancel(data);
};
useEffect(() => {
@@ -77,7 +81,7 @@ const EnvModal = ({
rules={[
{ required: true, message: '请输入环境变量名称', whitespace: true },
{
pattern: /^[a-zA-Z_][0-9a-zA-Z_]+$/,
pattern: /^[a-zA-Z_][0-9a-zA-Z_]*$/,
message: '只能输入字母数字下划线,且不能以数字开头',
},
]}
+8 -3
View File
@@ -35,7 +35,7 @@ const Initialization = () => {
const submitAccountSetting = (values: any) => {
setLoading(true);
request
.put(`${config.apiPrefix}init/user`, {
.put(`${config.apiPrefix}user/init`, {
data: {
username: values.username,
password: values.password,
@@ -52,8 +52,9 @@ const Initialization = () => {
};
const submitNotification = (values: any) => {
setLoading(true);
request
.put(`${config.apiPrefix}init/notification`, {
.put(`${config.apiPrefix}user/notification/init`, {
data: {
...values,
},
@@ -73,6 +74,10 @@ const Initialization = () => {
setFields(_fields || []);
};
useEffect(() => {
localStorage.removeItem(config.authKey);
}, []);
const steps = [
{
title: '欢迎使用',
@@ -228,7 +233,7 @@ const Initialization = () => {
<img
alt="logo"
className={styles.logo}
src="https://z3.ax1x.com/2021/11/18/I7MpAe.png"
src="https://img.gejiba.com/images/a3f551e09ac19add4c49ec16228729c5.png"
/>
<span className={styles.title}></span>
</div>
+37 -18
View File
@@ -1,12 +1,11 @@
import { useState, useEffect, useCallback, Key, useRef } from 'react';
import { TreeSelect, Tree, Input } from 'antd';
import { TreeSelect, Tree, Input, Empty } from 'antd';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
import Editor from '@monaco-editor/react';
import { request } from '@/utils/http';
import styles from './index.module.less';
import { Controlled as CodeMirror } from 'react-codemirror2';
import { useCtx, useTheme } from '@/utils/hooks';
import SplitPane from 'react-split-pane';
function getFilterData(keyword: string, data: any) {
@@ -129,7 +128,7 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={data}
placeholder="请选择日志文件"
placeholder="请选择日志"
showSearch
onSelect={onSelect}
/>,
@@ -143,21 +142,41 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
{!isPhone && (
<SplitPane split="vertical" size={200} maxSize={-100}>
<div className={styles['left-tree-container']}>
<Input.Search
className={styles['left-tree-search']}
onChange={onSearch}
></Input.Search>
<div className={styles['left-tree-scroller']} ref={treeDom}>
<Tree
className={styles['left-tree']}
treeData={filterData}
showIcon={true}
height={height}
selectedKeys={[select]}
showLine={{ showLeafIcon: true }}
onSelect={onTreeSelect}
></Tree>
</div>
{data.length > 0 ? (
<>
<Input.Search
className={styles['left-tree-search']}
onChange={onSearch}
placeholder="请输入日志名"
allowClear
></Input.Search>
<div className={styles['left-tree-scroller']} ref={treeDom}>
<Tree
className={styles['left-tree']}
treeData={filterData}
showIcon={true}
height={height}
selectedKeys={[select]}
showLine={{ showLeafIcon: true }}
onSelect={onTreeSelect}
></Tree>
</div>
</>
) : (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
}}
>
<Empty
description="暂无日志"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
</div>
)}
</div>
<Editor
language="shell"
+1
View File
@@ -32,6 +32,7 @@
.logo {
width: 48px;
height: 48px;
display: block;
margin-bottom: 24px;
}
+16 -5
View File
@@ -31,7 +31,7 @@ const Login = () => {
setTwoFactor(false);
setWaitTime(null);
request
.post(`${config.apiPrefix}login`, {
.post(`${config.apiPrefix}user/login`, {
data: {
username: values.username,
password: values.password,
@@ -113,6 +113,14 @@ const Login = () => {
}
};
const codeInputChange = (e: React.ChangeEvent) => {
const { value } = e.target as any;
const regx = /^[0-9]{6}$/;
if (regx.test(value)) {
completeTowFactor({ code: value });
}
};
useEffect(() => {
const isAuth = localStorage.getItem(config.authKey);
if (isAuth) {
@@ -127,7 +135,7 @@ const Login = () => {
<img
alt="logo"
className={styles.logo}
src="https://z3.ax1x.com/2021/11/18/I7MpAe.png"
src="https://img.gejiba.com/images/a3f551e09ac19add4c49ec16228729c5.png"
/>
<span className={styles.title}>
{twoFactor ? '两步验证' : config.siteName}
@@ -144,12 +152,15 @@ const Login = () => {
{
pattern: /^[0-9]{6}$/,
message: '验证码为6位数字',
validateTrigger: 'onBlur',
},
]}
hasFeedback
>
<Input placeholder="6位数字" autoFocus autoComplete="off" />
<Input
placeholder="6位数字"
onChange={codeInputChange}
autoFocus
autoComplete="off"
/>
</FormItem>
<Button
type="primary"
+71 -25
View File
@@ -24,22 +24,22 @@ const prefixMap: any = {
const EditModal = ({
treeData,
currentFile,
currentNode,
content,
handleCancel,
visible,
socketMessage,
}: {
treeData?: any;
currentFile?: string;
content?: string;
visible: boolean;
socketMessage: any;
currentNode: any;
handleCancel: () => void;
}) => {
const [value, setValue] = useState('');
const [language, setLanguage] = useState<string>('javascript');
const [fileName, setFileName] = useState<string>('');
const [cNode, setCNode] = useState<any>();
const [selectedKey, setSelectedKey] = useState<string>('');
const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false);
const [settingModalVisible, setSettingModalVisible] =
@@ -47,37 +47,59 @@ const EditModal = ({
const [log, setLog] = useState<string>('');
const { theme } = useTheme();
const editorRef = useRef<any>(null);
const [isRunning, setIsRunning] = useState(false);
const cancel = () => {
handleCancel();
};
const onSelect = (value: any, node: any) => {
if (node.value === fileName || !value) {
if (node.key === selectedKey || !value) {
return;
}
const newMode = LangMap[value.slice(-3)] || '';
setFileName(value);
setCNode(node);
setLanguage(newMode);
getDetail(node);
setSelectedKey(node.key);
};
const getDetail = (node: any) => {
request.get(`${config.apiPrefix}scripts/${node.value}`).then((data) => {
setValue(data.data);
});
request
.get(`${config.apiPrefix}scripts/${node.value}?path=${node.parent || ''}`)
.then((data) => {
setValue(data.data);
});
};
const run = () => {
setLog('');
request
.put(`${config.apiPrefix}scripts/run`, {
data: {
filename: fileName,
path: '',
filename: cNode.value,
path: cNode.parent || '',
},
})
.then((data) => {});
.then((data) => {
setIsRunning(true);
});
};
const stop = () => {
if (!cNode || !cNode.value) {
return;
}
request
.put(`${config.apiPrefix}scripts/stop`, {
data: {
filename: cNode.value,
path: cNode.parent || '',
},
})
.then((data) => {
setIsRunning(false);
});
};
useEffect(() => {
@@ -91,6 +113,13 @@ const EditModal = ({
return;
}
if (_message === '执行结束') {
_message = '';
setTimeout(() => {
setIsRunning(false);
}, 300);
}
if (log) {
_message = `\n${_message}`;
}
@@ -98,21 +127,21 @@ const EditModal = ({
}, [socketMessage]);
useEffect(() => {
if (currentFile) {
setFileName(currentFile);
if (currentNode) {
setCNode(currentNode);
setValue(content as string);
setSelectedKey(currentFile);
setSelectedKey(currentNode.key);
}
}, [currentFile, content]);
}, [content, currentNode]);
return (
<Drawer
className="edit-modal"
closable={false}
title={
<>
<span style={{ marginRight: 8 }}>{fileName}</span>
<TreeSelect
style={{ marginRight: 8, width: 120 }}
style={{ marginRight: 8, width: 150 }}
value={selectedKey}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={treeData}
@@ -122,7 +151,7 @@ const EditModal = ({
/>
<Select
value={language}
style={{ width: 120, marginRight: 8 }}
style={{ width: 110, marginRight: 8 }}
onChange={(e) => {
setLanguage(e);
}}
@@ -132,8 +161,12 @@ const EditModal = ({
<Option value="shell">shell</Option>
<Option value="python">python</Option>
</Select>
<Button type="primary" style={{ marginRight: 8 }} onClick={run}>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={isRunning ? stop : run}
>
{isRunning ? '停止' : '运行'}
</Button>
<Button
type="primary"
@@ -162,6 +195,16 @@ const EditModal = ({
>
</Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={() => {
stop();
handleCancel();
}}
>
退
</Button>
</>
}
width={'100%'}
@@ -169,7 +212,12 @@ const EditModal = ({
onClose={cancel}
visible={visible}
>
<SplitPane split="vertical" minSize={200} defaultSize="50%">
<SplitPane
split="vertical"
minSize={200}
defaultSize="50%"
style={{ height: 'calc(100vh - 55px)' }}
>
<Editor
language={language}
value={value}
@@ -184,9 +232,7 @@ const EditModal = ({
editorRef.current = editor;
}}
/>
<div>
<pre>{log}</pre>
</div>
<pre style={{ height: '100%', whiteSpace: 'break-spaces' }}>{log}</pre>
</SplitPane>
<SaveModal
visible={saveModalVisible}
@@ -197,7 +243,7 @@ const EditModal = ({
content:
editorRef.current &&
editorRef.current.getValue().replace(/\r\n/g, '\n'),
filename: fileName,
filename: cNode?.value,
}}
/>
<SettingModal
+44 -22
View File
@@ -10,6 +10,7 @@ import {
Tooltip,
Dropdown,
Menu,
Empty,
} from 'antd';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
@@ -112,16 +113,17 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
const initGetScript = () => {
const { p, s } = history.location.query as any;
if (s) {
const vkey = `${p}/${s}`;
const obj = {
node: {
title: s,
value: s,
key: p ? `${p}-${s}` : s,
key: p ? vkey : s,
parent: p,
},
};
setExpandedKeys([p]);
onTreeSelect([`${p}-${s}`], obj);
onTreeSelect([vkey], obj);
}
};
@@ -377,7 +379,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
) : (
<Menu>
<Menu.Item key="add" icon={<PlusOutlined />} onClick={addFile}>
</Menu.Item>
<Menu.Item
key="edit"
@@ -411,7 +413,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={data}
placeholder="请选择脚本文件"
placeholder="请选择脚本"
showSearch
onSelect={onSelect}
/>,
@@ -470,23 +472,43 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
{!isPhone && (
<SplitPane split="vertical" size={200} maxSize={-100}>
<div className={styles['left-tree-container']}>
<Input.Search
className={styles['left-tree-search']}
onChange={onSearch}
></Input.Search>
<div className={styles['left-tree-scroller']} ref={treeDom}>
<Tree
className={styles['left-tree']}
treeData={filterData}
showIcon={true}
height={height}
selectedKeys={[select]}
expandedKeys={expandedKeys}
onExpand={onExpand}
showLine={{ showLeafIcon: true }}
onSelect={onTreeSelect}
></Tree>
</div>
{data.length > 0 ? (
<>
<Input.Search
className={styles['left-tree-search']}
onChange={onSearch}
placeholder="请输入脚本名"
allowClear
></Input.Search>
<div className={styles['left-tree-scroller']} ref={treeDom}>
<Tree
className={styles['left-tree']}
treeData={filterData}
showIcon={true}
height={height}
selectedKeys={[select]}
expandedKeys={expandedKeys}
onExpand={onExpand}
showLine={{ showLeafIcon: true }}
onSelect={onTreeSelect}
></Tree>
</div>
</>
) : (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
}}
>
<Empty
description="暂无脚本"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
</div>
)}
</div>
<Editor
language={mode}
@@ -525,7 +547,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
<EditModal
visible={isLogModalVisible}
treeData={data}
currentFile={select}
currentNode={currentNode}
content={value}
socketMessage={socketMessage}
handleCancel={() => {
+49
View File
@@ -0,0 +1,49 @@
import React, { useEffect, useState } from 'react';
import { Typography, Input, Form, Button, message } from 'antd';
import styles from './index.less';
const { Link } = Typography;
const About = () => {
return (
<div className={styles.container}>
<img
alt="logo"
style={{ width: 140, marginRight: 20 }}
src="https://img.gejiba.com/images/a3f551e09ac19add4c49ec16228729c5.png"
/>
<div className={styles.right}>
<span className={styles.title}></span>
<span className={styles.desc}>
python3javaScriptshelltypescript A timed
task management panel that supports typescript, javaScript, python3,
and shell.
</span>
<div>
<Link
href="https://github.com/whyour/qinglong"
target="_blank"
style={{ marginRight: 15 }}
>
Github
</Link>
<Link
href="https://t.me/jiao_long"
target="_blank"
style={{ marginRight: 15 }}
>
Telegram频道
</Link>
<Link
href="https://github.com/whyour/qinglong/issues"
target="_blank"
>
BUG
</Link>
</div>
</div>
</div>
);
};
export default About;
+2 -2
View File
@@ -20,13 +20,13 @@ const AppModal = ({
const method = app ? 'put' : 'post';
const payload = { ...values };
if (app) {
payload._id = app._id;
payload.id = app.id;
}
const { code, data } = await request[method](`${config.apiPrefix}apps`, {
data: payload,
});
if (code === 200) {
message.success(app ? '更新应用成功' : '添加应用成功');
message.success(app ? '更新应用成功' : '新建应用成功');
} else {
message.error(data);
}
+30 -4
View File
@@ -24,7 +24,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
if (data.hasNewVersion) {
showConfirmUpdateModal(data);
} else {
message.success('已经是最新版了!');
showForceUpdateModal();
}
} else {
message.error(data);
@@ -39,6 +39,32 @@ const CheckUpdate = ({ socketMessage }: any) => {
});
};
const showForceUpdateModal = () => {
Modal.confirm({
width: 500,
title: '更新',
content: (
<>
<div></div>
<div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>
{version}
</div>
</>
),
okText: '确认',
cancelText: '强制更新',
onCancel() {
showUpdatingModal();
request
.put(`${config.apiPrefix}system/update`)
.then((_data: any) => {})
.catch((error: any) => {
console.log(error);
});
},
});
};
const showConfirmUpdateModal = (data: any) => {
const { lastVersion, lastLog } = data;
Modal.confirm({
@@ -113,7 +139,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
return;
}
const newMessage = `${value}\n${_message}`;
const newMessage = `${value}${_message}`;
modalRef.current.update({
content: (
<div style={{ height: '60vh', overflowY: 'auto' }}>
@@ -146,7 +172,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
<Countdown
className="inline-countdown"
format="ss"
value={Date.now() + 1000 * 10}
value={Date.now() + 1000 * 30}
/>
</span>
@@ -155,7 +181,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
});
setTimeout(() => {
window.location.reload();
}, 10000);
}, 30000);
}
}, [socketMessage]);
+20
View File
@@ -0,0 +1,20 @@
.container {
display: flex;
justify-content: center;
align-items: center;
padding: 50px 130px;
.right {
display: flex;
justify-content: center;
flex-direction: column;
.title {
font-size: 25px;
margin-bottom: 10px;
}
.desc {
margin-bottom: 10px;
}
}
}
+10 -7
View File
@@ -32,7 +32,7 @@ import SecuritySettings from './security';
import LoginLog from './loginLog';
import NotificationSetting from './notification';
import CheckUpdate from './checkUpdate';
import { useForm } from 'antd/lib/form/Form';
import About from './about';
const { Text } = Typography;
const optionsWithDisabled = [
@@ -124,7 +124,7 @@ const Setting = ({
const [loginLogData, setLoginLogData] = useState<any[]>([]);
const [notificationInfo, setNotificationInfo] = useState<any>();
const [logRemoveFrequency, setLogRemoveFrequency] = useState<number>();
const [form] = useForm();
const [form] = Form.useForm();
const themeChange = (e: any) => {
setTheme(e.target.value);
@@ -164,7 +164,7 @@ const Setting = ({
),
onOk() {
request
.delete(`${config.apiPrefix}apps`, { data: [record._id] })
.delete(`${config.apiPrefix}apps`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
message.success('删除成功');
@@ -198,7 +198,7 @@ const Setting = ({
),
onOk() {
request
.put(`${config.apiPrefix}apps/${record._id}/reset-secret`)
.put(`${config.apiPrefix}apps/${record.id}/reset-secret`)
.then((data: any) => {
if (data.code === 200) {
message.success('重置成功');
@@ -222,7 +222,7 @@ const Setting = ({
};
const handleApp = (app: any) => {
const index = dataSource.findIndex((x) => x._id === app._id);
const index = dataSource.findIndex((x) => x.id === app.id);
const result = [...dataSource];
if (index === -1) {
result.push(app);
@@ -319,7 +319,7 @@ const Setting = ({
tabActiveKey === 'app'
? [
<Button key="2" type="primary" onClick={() => addApp()}>
</Button>,
]
: []
@@ -339,7 +339,7 @@ const Setting = ({
columns={columns}
pagination={false}
dataSource={dataSource}
rowKey="_id"
rowKey="id"
size="middle"
scroll={{ x: 768 }}
loading={loading}
@@ -381,6 +381,9 @@ const Setting = ({
</Form.Item>
</Form>
</Tabs.TabPane>
<Tabs.TabPane tab="关于" key="about">
<About />
</Tabs.TabPane>
</Tabs>
<AppModal
visible={isModalVisible}
+1 -1
View File
@@ -73,7 +73,7 @@ const LoginLog = ({ data }: any) => {
columns={columns}
pagination={false}
dataSource={data}
rowKey="_id"
rowKey="id"
size="middle"
scroll={{ x: 768 }}
/>
+11
View File
@@ -55,6 +55,14 @@ export default {
name: '任务日志',
value: 'logs',
},
{
name: '依赖管理',
value: 'dependencies',
},
{
name: '系统信息',
value: 'system',
},
],
scopesMap: {
crons: '定时任务',
@@ -62,6 +70,8 @@ export default {
configs: '配置文件',
scripts: '脚本管理',
logs: '任务日志',
dependencies: '依赖管理',
system: '系统信息',
},
notificationModes: [
{ value: 'gotify', label: 'Gotify' },
@@ -109,6 +119,7 @@ export default {
tip: 'Bark的信息IP/设备码,例如:https://api.day.app/XXXXXXXX',
required: true,
},
{ label: 'barkIcon', tip: 'BARK推送图标,自定义推送图标 (需iOS15或以上才能显示)' },
{ label: 'barkSound', tip: 'BARK推送铃声,铃声列表去APP查看复制填写' },
{ label: 'barkGroup', tip: 'BARK推送消息的分组, 默认为qinglong' },
],
+3 -3
View File
@@ -34,12 +34,12 @@ const errorHandler = function (error: any) {
const _request = extend({ timeout: 60000, params: { t: time }, errorHandler });
const apiWhiteList = [
'/api/login',
'/api/user/login',
'/open/auth/token',
'/api/user/two-factor/login',
'/api/system',
'/api/init/user',
'/api/init/notification',
'/api/user/init',
'/api/user/notification/init',
];
_request.interceptors.request.use((url, options) => {
+10 -9
View File
@@ -1,10 +1,11 @@
export const version = '2.10.9';
export const changeLogLink = 'https://t.me/jiao_long/228';
export const changeLog = `2.10.9 版本说明
1. https://github.com/kilo5hz PR
2. gotify https://github.com/kilo5hz PR
3. pageSize增加200/500/1000 https://github.com/fzls PR
4. deps目录依赖文件拷贝
5. alpine基础镜像版本arm32位系统无法启动容器 https://github.com/lx200916
6.
export const version = '2.11.3';
export const changeLogLink = 'https://t.me/jiao_long/262';
export const changeLog = `2.11.3 版本说明
1.
2.
3. logo地址
4. check命令
5. 使Proxy_url代理地址
6. apk源地址
7. bark支持自定义图标 https://github.com/Ukenn2112 PR
`;
+1 -7
View File
@@ -27,13 +27,7 @@
"allowJs": true,
"noEmit": false
},
"include": [
"src/**/*",
"config/**/*",
".umirc.ts",
"typings.d.ts",
"back/**/*"
],
"include": ["src/**/*", ".umirc.ts", "typings.d.ts", "back/**/*"],
"exclude": [
"node_modules",
"lib",
+1878 -1540
View File
File diff suppressed because it is too large Load Diff