mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-11 19:05:20 +08:00
Compare commits
306
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f12d9cbd5 | ||
|
|
54d89754df | ||
|
|
9470524615 | ||
|
|
b5faa6eaaa | ||
|
|
eab476cfe8 | ||
|
|
b773def012 | ||
|
|
a9f8b23a7a | ||
|
|
61bb6535fb | ||
|
|
eb0bdffe06 | ||
|
|
e129485285 | ||
|
|
aac7de172e | ||
|
|
e19d2412fc | ||
|
|
7cba2cf76c | ||
|
|
55d7a49dbc | ||
|
|
f695864fd8 | ||
|
|
ae4883fbe7 | ||
|
|
836dfd2861 | ||
|
|
6e0523c6d7 | ||
|
|
cd9ba084ae | ||
|
|
c8a0c0de67 | ||
|
|
5dd8d9df91 | ||
|
|
2dbee94e85 | ||
|
|
9001797037 | ||
|
|
5e7104d5a3 | ||
|
|
7c33b22d63 | ||
|
|
b7cb1e379c | ||
|
|
274a6ce52e | ||
|
|
8079e4e7b8 | ||
|
|
920efb8fab | ||
|
|
92aeb2b9da | ||
|
|
ced76cf343 | ||
|
|
3750d6ecd5 | ||
|
|
7444f30384 | ||
|
|
b0fd5ba47f | ||
|
|
cc098bc2e5 | ||
|
|
1cfcf8d194 | ||
|
|
d1e9f741d0 | ||
|
|
0cde9d07ee | ||
|
|
a266425348 | ||
|
|
6dbe02e321 | ||
|
|
39979879c9 | ||
|
|
ba3f604a5c | ||
|
|
9572582401 | ||
|
|
2bcef0ba75 | ||
|
|
31cd7c4007 | ||
|
|
1c4c1799b0 | ||
|
|
2d558df15e | ||
|
|
7b769da4e2 | ||
|
|
4e95d62933 | ||
|
|
83255280b9 | ||
|
|
2546cebdea | ||
|
|
f14a3fe0f1 | ||
|
|
c3eb845be4 | ||
|
|
1eef77dca5 | ||
|
|
d7d9f47fe5 | ||
|
|
fce6a996d9 | ||
|
|
48a484d7de | ||
|
|
4b81d3c612 | ||
|
|
21b6edd7eb | ||
|
|
9a43e022e9 | ||
|
|
62bef3f13d | ||
|
|
c87f5fa004 | ||
|
|
8d99f3ffbb | ||
|
|
100ca7b54b | ||
|
|
4a2624f12a | ||
|
|
21b01a85a8 | ||
|
|
e039ae436e | ||
|
|
b13a1a45f8 | ||
|
|
667fe2bbba | ||
|
|
f083871ca0 | ||
|
|
782e1ba235 | ||
|
|
b92c6edc4e | ||
|
|
bdf4657c73 | ||
|
|
8559438611 | ||
|
|
04045fd410 | ||
|
|
1c71a9d8a8 | ||
|
|
9a9ae9782c | ||
|
|
da2569850b | ||
|
|
b475035415 | ||
|
|
7baee27585 | ||
|
|
2cb2f1ce0a | ||
|
|
8c1407e2a9 | ||
|
|
7bcb113085 | ||
|
|
42dc27addf | ||
|
|
aac30398c4 | ||
|
|
ceae888372 | ||
|
|
61df7050db | ||
|
|
af5561cbbf | ||
|
|
bce30f922d | ||
|
|
d6ed394557 | ||
|
|
7b512a1681 | ||
|
|
7af0597cd5 | ||
|
|
2f5cf6d93c | ||
|
|
d672d2d104 | ||
|
|
c0d1fb1ef4 | ||
|
|
3c882ad0ce | ||
|
|
58acd2eb68 | ||
|
|
590473e7d2 | ||
|
|
21139157c8 | ||
|
|
8f8296ab64 | ||
|
|
40bf07d2a4 | ||
|
|
fd2b4fb86d | ||
|
|
783e867ef6 | ||
|
|
3873d9f978 | ||
|
|
7dece938c8 | ||
|
|
0842d66c79 | ||
|
|
d071e50d62 | ||
|
|
337f01aff1 | ||
|
|
5614e41eb0 | ||
|
|
4f0fdbe453 | ||
|
|
49c068db78 | ||
|
|
fbba44d482 | ||
|
|
10c07b0a09 | ||
|
|
20893698ba | ||
|
|
31de659ed3 | ||
|
|
6c08a16c66 | ||
|
|
21e7178f48 | ||
|
|
aefd267eb2 | ||
|
|
fb1a3980d4 | ||
|
|
cfc467f8a5 | ||
|
|
48120f012d | ||
|
|
c907a78437 | ||
|
|
915c0d09a9 | ||
|
|
80638862cc | ||
|
|
a7350948cd | ||
|
|
1e1be013b5 | ||
|
|
4062584eae | ||
|
|
abedddffa6 | ||
|
|
38858ccdb2 | ||
|
|
d56e6359d0 | ||
|
|
5886991464 | ||
|
|
838326616b | ||
|
|
fcf80fe946 | ||
|
|
2e63a7056c | ||
|
|
7c807e3ec8 | ||
|
|
b558aa0934 | ||
|
|
38757758cb | ||
|
|
84dd3d8fd1 | ||
|
|
19423c2d30 | ||
|
|
98bba7d7fa | ||
|
|
dcaa9ce000 | ||
|
|
3cae44a136 | ||
|
|
df8a3d2692 | ||
|
|
56e43f1808 | ||
|
|
2f6b95cccf | ||
|
|
ad9f6b2b64 | ||
|
|
165c3dc976 | ||
|
|
cbe6390b41 | ||
|
|
026eaf6606 | ||
|
|
ee184f8e62 | ||
|
|
5745231dc9 | ||
|
|
cbdc3a9861 | ||
|
|
ba5c42ad6d | ||
|
|
b056a731a1 | ||
|
|
b4cd94f2d9 | ||
|
|
3b8cb43f02 | ||
|
|
82cb11c3c4 | ||
|
|
eb7522a90b | ||
|
|
3898201b1e | ||
|
|
bb6ab0c17b | ||
|
|
652c40ab1f | ||
|
|
ca0d6215a9 | ||
|
|
61d155257c | ||
|
|
91399f1a1f | ||
|
|
2e738c5977 | ||
|
|
c0bd51cf03 | ||
|
|
ca5afe04f8 | ||
|
|
47db0b9f4e | ||
|
|
27f17a377a | ||
|
|
ecaf731ecf | ||
|
|
9fba2a85f6 | ||
|
|
93f75be182 | ||
|
|
d537901f59 | ||
|
|
260c1ed4a4 | ||
|
|
ac2bd52463 | ||
|
|
16c6f8fee1 | ||
|
|
8a0db21f17 | ||
|
|
9ab08a71c0 | ||
|
|
381c32a3b4 | ||
|
|
350c0f10c4 | ||
|
|
d5b982f813 | ||
|
|
6587375ff2 | ||
|
|
f5ecb81bad | ||
|
|
0141cd81c4 | ||
|
|
ff412c7497 | ||
|
|
623f239f91 | ||
|
|
c7ba304c6a | ||
|
|
c8393604d1 | ||
|
|
c5d47a67aa | ||
|
|
3a1423cd59 | ||
|
|
d57f4cf6cd | ||
|
|
af420942c4 | ||
|
|
eac4e5e66f | ||
|
|
b8822a98f7 | ||
|
|
95322b42c0 | ||
|
|
33f5a06d32 | ||
|
|
a7e5126c92 | ||
|
|
c62f3e5b8e | ||
|
|
5d3f413a05 | ||
|
|
644154ca38 | ||
|
|
cccce5ff74 | ||
|
|
aa1fe98188 | ||
|
|
b6a78550d6 | ||
|
|
70b7f2c493 | ||
|
|
ea44e5f209 | ||
|
|
41ef87d9f8 | ||
|
|
c0b4a70aa0 | ||
|
|
955875cb7a | ||
|
|
827e8571aa | ||
|
|
aed08b0f59 | ||
|
|
3ddaf41e27 | ||
|
|
38a9cce6bf | ||
|
|
ae255ef381 | ||
|
|
4316bef01e | ||
|
|
8491c5706d | ||
|
|
7b7fab1f32 | ||
|
|
5a6fccdc2f | ||
|
|
23668be46d | ||
|
|
f92f2c1666 | ||
|
|
895740a3cb | ||
|
|
4c76c3231f | ||
|
|
ca9d3f9559 | ||
|
|
3ca3c5e75c | ||
|
|
c2c0b1dfdc | ||
|
|
c633774487 | ||
|
|
c2d86fc517 | ||
|
|
d7c8d150ca | ||
|
|
6b8a4e65f2 | ||
|
|
482e43dd1f | ||
|
|
1437babc6c | ||
|
|
5ed882943a | ||
|
|
e678b3fb3a | ||
|
|
a5770bc4e4 | ||
|
|
59821bb939 | ||
|
|
b58040be44 | ||
|
|
be3b8ad52e | ||
|
|
0fdaf97461 | ||
|
|
f5f036372f | ||
|
|
3fceb983e6 | ||
|
|
ae6a39196d | ||
|
|
2810ff2753 | ||
|
|
205c0cf8d3 | ||
|
|
d9c801a091 | ||
|
|
0833aa3b30 | ||
|
|
9c90bbc146 | ||
|
|
231b387076 | ||
|
|
3de432652f | ||
|
|
cad714493e | ||
|
|
6a690a6108 | ||
|
|
1b85c52557 | ||
|
|
b37be92ea9 | ||
|
|
f95979f445 | ||
|
|
09e07218ae | ||
|
|
085f5a9451 | ||
|
|
eb4e935554 | ||
|
|
57986cf425 | ||
|
|
7bf14b9acc | ||
|
|
3ba0be34d9 | ||
|
|
0f828a3c75 | ||
|
|
16a768a142 | ||
|
|
f380d20279 | ||
|
|
be908ab36c | ||
|
|
b545454537 | ||
|
|
a5ff8dd1d6 | ||
|
|
59f23e518b | ||
|
|
858b3f0176 | ||
|
|
89b8494ad0 | ||
|
|
64ecd75bd5 | ||
|
|
138eb3393d | ||
|
|
1ad51d2925 | ||
|
|
317e175bca | ||
|
|
0e287aa7d1 | ||
|
|
28018b705b | ||
|
|
46ec72d6be | ||
|
|
82494a87af | ||
|
|
95c19b447a | ||
|
|
9c6eae936a | ||
|
|
77605a9360 | ||
|
|
82880c3f06 | ||
|
|
11143c771b | ||
|
|
0968eaa5a8 | ||
|
|
0687f9d472 | ||
|
|
25f0f5951a | ||
|
|
eec0ce65ad | ||
|
|
be4e100168 | ||
|
|
59e4f1063c | ||
|
|
359e21f02b | ||
|
|
6894bd9c0e | ||
|
|
7040f02823 | ||
|
|
41408de337 | ||
|
|
72e243dce8 | ||
|
|
77d4840e2a | ||
|
|
0d5961821c | ||
|
|
f34980b173 | ||
|
|
5b21a6e529 | ||
|
|
6dbb737d4b | ||
|
|
76ecfbaaa2 | ||
|
|
d9aacbcb92 | ||
|
|
e088325d95 | ||
|
|
b29dd2c580 | ||
|
|
89e82fd5b0 | ||
|
|
9f0b671d42 | ||
|
|
7de7c724dc | ||
|
|
3525b1d92a | ||
|
|
30cea84886 | ||
|
|
0c2cc7f575 |
@@ -1,34 +0,0 @@
|
|||||||
name: BuildStatic
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: '14'
|
|
||||||
|
|
||||||
- name: build front and back
|
|
||||||
run: |
|
|
||||||
yarn install
|
|
||||||
yarn build
|
|
||||||
yarn build-back
|
|
||||||
|
|
||||||
- name: copy to static repo
|
|
||||||
env:
|
|
||||||
# GITHUB_REPO: gitee.com/whyour/qinglong-static
|
|
||||||
GITHUB_REPO: github.com/whyour/qinglong-static
|
|
||||||
run: |
|
|
||||||
mkdir -p static
|
|
||||||
cd ./static
|
|
||||||
cp -rf ../dist ./ && cp -rf ../build ./
|
|
||||||
git init && git add .
|
|
||||||
git config user.name "whyour"
|
|
||||||
git config user.email "imwhyour@gmail.com"
|
|
||||||
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
|
|
||||||
git push --force --quiet "https://whyour:${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" master:master
|
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
name: Build And Push Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
- 'develop'
|
||||||
|
# Sequence of patterns matched against refs/tags
|
||||||
|
tags:
|
||||||
|
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||||
|
schedule:
|
||||||
|
# 参考 https://jasonet.co/posts/scheduled-actions/
|
||||||
|
# note: 这里是GMT时间,北京时间减去八小时即可。如北京时间 22:30 => GMT 14:30
|
||||||
|
# minute hour day month dayOfWeek
|
||||||
|
- cron: '00 14 * * *' # GMT 14:00 => 北京时间 22:00
|
||||||
|
#- cron: '30 16 * * *' # GMT 16:30(前一天) => 北京时间 00:30
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-static:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: '14'
|
||||||
|
|
||||||
|
- name: build front and back
|
||||||
|
run: |
|
||||||
|
yarn install
|
||||||
|
yarn build
|
||||||
|
yarn build-back
|
||||||
|
|
||||||
|
- name: copy to static repo
|
||||||
|
env:
|
||||||
|
GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static
|
||||||
|
GITHUB_BRANCH: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
mkdir -p static
|
||||||
|
cd ./static
|
||||||
|
cp -rf ../dist ./ && cp -rf ../build ./
|
||||||
|
git init -b ${GITHUB_BRANCH} && git add .
|
||||||
|
git config --local user.name 'github-actions[bot]'
|
||||||
|
git config --local user.email 'github-actions[bot]@users.noreply.github.com'
|
||||||
|
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
|
||||||
|
git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH}
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: build-static
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set time zone
|
||||||
|
uses: szenius/set-timezone@v1.0
|
||||||
|
with:
|
||||||
|
timezoneLinux: "Asia/Shanghai"
|
||||||
|
timezoneMacos: "Asia/Shanghai"
|
||||||
|
timezoneWindows: "China Standard Time"
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Login to GHCR
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v3
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
${{ github.repository }}
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
|
# generate Docker tags based on the following events/attributes
|
||||||
|
# nightly, master, pr-2, 1.2.3, 1.2, 1
|
||||||
|
tags: |
|
||||||
|
type=schedule,pattern=nightly
|
||||||
|
type=edge
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
build-args: |
|
||||||
|
MAINTAINER=${{ github.repository_owner }}
|
||||||
|
QL_BRANCH=${{ github.ref_name }}
|
||||||
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x
|
||||||
|
context: docker/
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
@@ -1,13 +1,6 @@
|
|||||||
name: PublishDockerImage
|
name: PublishDockerImage
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
# workflow_run:
|
|
||||||
# workflows:
|
|
||||||
# - BuildStatic
|
|
||||||
# branches:
|
|
||||||
# - master
|
|
||||||
# types:
|
|
||||||
# - completed
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
+4
-2
@@ -4,7 +4,7 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
/npm-debug.log*
|
/npm-debug.log*
|
||||||
/yarn-error.log
|
/yarn-error.log
|
||||||
/yarn.lock
|
/pnpm-lock.yaml
|
||||||
/package-lock.json
|
/package-lock.json
|
||||||
|
|
||||||
# production
|
# production
|
||||||
@@ -21,9 +21,11 @@
|
|||||||
/.env.local
|
/.env.local
|
||||||
.env
|
.env
|
||||||
.history
|
.history
|
||||||
|
.version.ts
|
||||||
|
|
||||||
/config
|
/config
|
||||||
/log
|
/log
|
||||||
/db
|
/db
|
||||||
/manual_log
|
/manual_log
|
||||||
/scripts
|
/scripts
|
||||||
|
/bak
|
||||||
@@ -8,11 +8,15 @@ export default defineConfig({
|
|||||||
type: 'none',
|
type: 'none',
|
||||||
},
|
},
|
||||||
fastRefresh: {},
|
fastRefresh: {},
|
||||||
favicon: 'https://qinglong.whyour.cn/g5.ico',
|
dynamicImport: {
|
||||||
|
loading: '@/components/pageLoading',
|
||||||
|
},
|
||||||
|
favicon: '/images/g5.ico',
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://127.0.0.1:5678/',
|
target: 'http://127.0.0.1:5600/',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
ws: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
chainWebpack: (config) => {
|
chainWebpack: (config) => {
|
||||||
@@ -30,14 +34,16 @@ export default defineConfig({
|
|||||||
'react-dom': 'window.ReactDOM',
|
'react-dom': 'window.ReactDOM',
|
||||||
darkreader: 'window.DarkReader',
|
darkreader: 'window.DarkReader',
|
||||||
codemirror: 'window.CodeMirror',
|
codemirror: 'window.CodeMirror',
|
||||||
|
'sockjs-client': 'window.SockJS',
|
||||||
},
|
},
|
||||||
scripts: [
|
scripts: [
|
||||||
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
|
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
|
||||||
'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
|
'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
|
||||||
'https://cdn.jsdelivr.net/npm/darkreader@4.9.34/darkreader.min.js',
|
'https://cdn.jsdelivr.net/npm/darkreader@4.9.40/darkreader.min.js',
|
||||||
'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/lib/codemirror.min.js',
|
'https://cdn.jsdelivr.net/npm/codemirror@5/lib/codemirror.min.js',
|
||||||
'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/mode/shell/shell.js',
|
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/shell/shell.js',
|
||||||
'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/mode/python/python.js',
|
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/python/python.js',
|
||||||
'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/mode/javascript/javascript.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,6 +1,6 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/whyour/qinglong">
|
<a href="https://github.com/whyour/qinglong">
|
||||||
<img width="150" src="https://qinglong.whyour.cn/qinglong.png">
|
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -8,12 +8,10 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
Python/JavaScript/Shell/Typescript 定时任务管理
|
支持python3、javaScript、shell、typescript 的定时任务管理面板(A timed task management panel that supports typescript, javaScript, python3, and shell.)
|
||||||
|
|
||||||
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url] [![donate][donate-image]][donate-url]
|
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
|
||||||
|
|
||||||
[donate-image]: https://img.shields.io/badge/donate-wechat-green?style=flat
|
|
||||||
[donate-url]: https://qinglong.whyour.cn/nice.png
|
|
||||||
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
|
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
|
||||||
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
|
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
|
||||||
@@ -24,27 +22,151 @@ Python/JavaScript/Shell/Typescript 定时任务管理
|
|||||||
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p align="center">
|
[](https://whyour.cn)
|
||||||
<img width="49%" src="https://qinglong.whyour.cn/login.png">
|
|
||||||
<img width="49%" src="https://qinglong.whyour.cn/home.png">
|
简体中文 | [English](./README-en.md)
|
||||||
</p>
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
- 支持多种脚本语言(python3、javaScript、shell、typescript)
|
||||||
|
- 支持在线管理脚本、环境变量、配置文件
|
||||||
|
- 支持在线查看任务日志
|
||||||
|
- 支持秒级任务设置
|
||||||
|
- 支持系统级通知
|
||||||
|
- 支持暗黑模式
|
||||||
|
- 支持手机端操作
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
### 本机部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 待完善
|
||||||
|
```
|
||||||
|
|
||||||
|
### docker 部署
|
||||||
|
|
||||||
|
1. docker 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo curl -sSL get.docker.com | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 启动容器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -dit \
|
||||||
|
-v $PWD/ql/config:/ql/config \
|
||||||
|
-v $PWD/ql/log:/ql/log \
|
||||||
|
-v $PWD/ql/db:/ql/db \
|
||||||
|
-v $PWD/ql/repo:/ql/repo \
|
||||||
|
-v $PWD/ql/raw:/ql/raw \
|
||||||
|
-v $PWD/ql/scripts:/ql/scripts \
|
||||||
|
-p 5700:5700 \
|
||||||
|
--name qinglong \
|
||||||
|
--hostname qinglong \
|
||||||
|
--restart unless-stopped \
|
||||||
|
whyour/qinglong:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### docker-compose 部署
|
||||||
|
|
||||||
|
1. docker-compose 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 启动容器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir qinglong
|
||||||
|
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker-compose.yml
|
||||||
|
|
||||||
|
# 启动
|
||||||
|
docker-compose up -d
|
||||||
|
# 停止
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
1. 内置命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 更新并重启青龙
|
||||||
|
ql update
|
||||||
|
# 运行自定义脚本extra.sh
|
||||||
|
ql extra
|
||||||
|
# 添加单个脚本文件
|
||||||
|
ql raw <file_url>
|
||||||
|
# 添加单个仓库的指定脚本
|
||||||
|
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch>
|
||||||
|
# 删除旧日志
|
||||||
|
ql rmlog <days>
|
||||||
|
# 启动tg-bot
|
||||||
|
ql bot
|
||||||
|
# 检测青龙环境并修复
|
||||||
|
ql check
|
||||||
|
# 重置登录错误次数
|
||||||
|
ql resetlet
|
||||||
|
# 禁用两步登录
|
||||||
|
ql resettfa
|
||||||
|
|
||||||
|
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
|
||||||
|
task <file_path>
|
||||||
|
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
|
||||||
|
task <file_path> now
|
||||||
|
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
|
||||||
|
task <file_path> conc <env_name> <account_number>(可选的)
|
||||||
|
# 指定账号执行,无论是否设置了随机延迟,均立即运行
|
||||||
|
task <file_path> desi <env_name> <account_number>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 参数说明
|
||||||
|
|
||||||
|
* file_url: 脚本地址
|
||||||
|
* repo_url: 仓库地址
|
||||||
|
* whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串
|
||||||
|
* blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串
|
||||||
|
* dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响
|
||||||
|
* branch: 拉取仓库的分支
|
||||||
|
* days: 需要保留的日志的天数
|
||||||
|
* file_path: 任务执行时的文件路径
|
||||||
|
* env_name: 任务执行时需要并发或者指定时的环境变量名称
|
||||||
|
* account_number: 任务执行时指定某个环境变量需要执行的账号序号
|
||||||
|
|
||||||
|
## 链接
|
||||||
|
|
||||||
|
- [nevinee](https://gitee.com/evine)
|
||||||
|
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
|
||||||
|
- [Ant Design](https://ant.design)
|
||||||
|
- [Ant Design Pro](https://pro.ant.design/)
|
||||||
|
- [Umijs3.0](https://umijs.org)
|
||||||
|
- [darkreader](https://github.com/darkreader/darkreader)
|
||||||
|
- [admin-server](https://github.com/sunpu007/admin-server)
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ git clone git@github.com:whyour/qinglong.git
|
||||||
|
$ cd qinglong
|
||||||
|
$ cp .env.example .env
|
||||||
|
$ yarn install
|
||||||
|
$ yarn start-back
|
||||||
|
$ yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
打开你的浏览器,访问 http://127.0.0.1:5601
|
||||||
|
|
||||||
|
## 交流
|
||||||
|
|
||||||
|
[telegram频道](https://t.me/jiao_long)
|
||||||
|
|
||||||
|
## 名称来源
|
||||||
|
|
||||||
青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
|
青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
|
||||||
|
|
||||||
《后汉书·律历志下》记载:日周于天,一寒一暑,四时备成,万物毕改,摄提迁次,青龙移辰,谓之岁。
|
《后汉书·律历志下》记载:日周于天,一寒一暑,四时备成,万物毕改,摄提迁次,青龙移辰,谓之岁。
|
||||||
|
|
||||||
在中国[二十八宿](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF)中,青龙是东方七宿(角、亢、氐、房、心、尾、箕)的总称。 在早期星宿信仰中,祂是最尊贵的天神。 但被道教信仰吸纳入其神系后,神格大跌,道教将其称为“孟章”,在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼,与白虎监兵神君一起,是道教的护卫天神。
|
在中国[二十八宿](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF)中,青龙是东方七宿(角、亢、氐、房、心、尾、箕)的总称。 在早期星宿信仰中,祂是最尊贵的天神。 但被道教信仰吸纳入其神系后,神格大跌,道教将其称为“孟章”,在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼,与白虎监兵神君一起,是道教的护卫天神。
|
||||||
|
|
||||||
## 多谢
|
|
||||||
|
|
||||||
* [nevinee](https://gitee.com/evine)
|
|
||||||
|
|
||||||
* [crontab-ui](https://github.com/alseambusher/crontab-ui)
|
|
||||||
|
|
||||||
* [Ant Design](https://ant.design)
|
|
||||||
|
|
||||||
* [Ant Design Pro](https://pro.ant.design/)
|
|
||||||
|
|
||||||
* [Umijs3.0](https://umijs.org)
|
|
||||||
|
|
||||||
* [darkreader](https://github.com/darkreader/darkreader)
|
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
import { Router, Request, Response, NextFunction } from 'express';
|
|
||||||
import { Container } from 'typedi';
|
|
||||||
import { Logger } from 'winston';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import config from '../config';
|
|
||||||
import jwt from 'jsonwebtoken';
|
|
||||||
import { createRandomString } from '../config/util';
|
|
||||||
import crypto from 'crypto';
|
|
||||||
const route = Router();
|
|
||||||
|
|
||||||
export default (app: Router) => {
|
|
||||||
app.use('/', route);
|
|
||||||
route.post(
|
|
||||||
'/login',
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
let username = req.body.username;
|
|
||||||
let password = req.body.password;
|
|
||||||
fs.readFile(config.authConfigFile, 'utf8', function (err, data) {
|
|
||||||
if (err) console.log(err);
|
|
||||||
const authInfo = JSON.parse(data);
|
|
||||||
if (username && password) {
|
|
||||||
if (
|
|
||||||
authInfo.username === 'admin' &&
|
|
||||||
authInfo.password === 'adminadmin'
|
|
||||||
) {
|
|
||||||
const newPassword = createRandomString(16, 22);
|
|
||||||
fs.writeFileSync(
|
|
||||||
config.authConfigFile,
|
|
||||||
JSON.stringify({
|
|
||||||
username: authInfo.username,
|
|
||||||
password: newPassword,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return res.send({
|
|
||||||
code: 100,
|
|
||||||
message: '已初始化密码,请前往auth.json查看并重新登录',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
username == authInfo.username &&
|
|
||||||
password == authInfo.password
|
|
||||||
) {
|
|
||||||
const data = createRandomString(50, 100);
|
|
||||||
let token = jwt.sign({ data }, config.secret as any, {
|
|
||||||
expiresIn: 60 * 60 * 24 * 3,
|
|
||||||
algorithm: 'HS384',
|
|
||||||
});
|
|
||||||
fs.writeFileSync(
|
|
||||||
config.authConfigFile,
|
|
||||||
JSON.stringify({
|
|
||||||
username: authInfo.username,
|
|
||||||
password: authInfo.password,
|
|
||||||
token,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
res.send({ code: 200, token });
|
|
||||||
} else {
|
|
||||||
res.send({ code: 400, message: config.authError });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res.send({ err: 400, message: '请输入用户名密码!' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.post(
|
|
||||||
'/logout',
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
fs.readFile(config.authConfigFile, 'utf8', function (err, data) {
|
|
||||||
if (err) console.log(err);
|
|
||||||
const authInfo = JSON.parse(data);
|
|
||||||
fs.writeFileSync(
|
|
||||||
config.authConfigFile,
|
|
||||||
JSON.stringify({
|
|
||||||
username: authInfo.username,
|
|
||||||
password: authInfo.password,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
res.send({ code: 200 });
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.post(
|
|
||||||
'/user',
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
fs.writeFile(config.authConfigFile, JSON.stringify(req.body), (err) => {
|
|
||||||
if (err) console.log(err);
|
|
||||||
res.send({ code: 200, message: '更新成功' });
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.get(
|
|
||||||
'/user',
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
|
|
||||||
if (err) console.log(err);
|
|
||||||
const authInfo = JSON.parse(data);
|
|
||||||
res.send({ code: 200, data: { username: authInfo.username } });
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -37,6 +37,9 @@ export default (app: Router) => {
|
|||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
let content = '';
|
let content = '';
|
||||||
|
if (config.blackFileList.includes(req.params.file)) {
|
||||||
|
res.send({ code: 403, message: '文件无法访问' });
|
||||||
|
}
|
||||||
if (req.params.file.includes('sample')) {
|
if (req.params.file.includes('sample')) {
|
||||||
content = getFileContentByName(
|
content = getFileContentByName(
|
||||||
`${config.samplePath}${req.params.file}`,
|
`${config.samplePath}${req.params.file}`,
|
||||||
@@ -66,6 +69,9 @@ export default (app: Router) => {
|
|||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const { name, content } = req.body;
|
const { name, content } = req.body;
|
||||||
|
if (config.blackFileList.includes(name)) {
|
||||||
|
res.send({ code: 403, message: '文件无法访问' });
|
||||||
|
}
|
||||||
const path = `${config.configPath}${name}`;
|
const path = `${config.configPath}${name}`;
|
||||||
fs.writeFileSync(path, content);
|
fs.writeFileSync(path, content);
|
||||||
res.send({ code: 200, message: '保存成功' });
|
res.send({ code: 200, message: '保存成功' });
|
||||||
|
|||||||
@@ -191,6 +191,42 @@ export default (app: Router) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/crons/pin',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.string().required()),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const cronService = Container.get(CronService);
|
||||||
|
const data = await cronService.pin(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/crons/unpin',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.string().required()),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const cronService = Container.get(CronService);
|
||||||
|
const data = await cronService.unPin(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/crons/import',
|
'/crons/import',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -234,6 +270,8 @@ export default (app: Router) => {
|
|||||||
status: Joi.string().required(),
|
status: Joi.string().required(),
|
||||||
pid: Joi.string().optional(),
|
pid: Joi.string().optional(),
|
||||||
log_path: Joi.string().optional(),
|
log_path: Joi.string().optional(),
|
||||||
|
last_running_time: Joi.number().optional(),
|
||||||
|
last_execution_time: Joi.number().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
import DependenceService from '../services/dependence';
|
||||||
|
import { Logger } from 'winston';
|
||||||
|
import { celebrate, Joi } from 'celebrate';
|
||||||
|
const route = Router();
|
||||||
|
|
||||||
|
export default (app: Router) => {
|
||||||
|
app.use('/', route);
|
||||||
|
route.get(
|
||||||
|
'/dependencies',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const dependenceService = Container.get(DependenceService);
|
||||||
|
const data = await dependenceService.dependencies(req.query as any);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
'/dependencies',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(
|
||||||
|
Joi.object({
|
||||||
|
name: Joi.string().required(),
|
||||||
|
type: Joi.number().required(),
|
||||||
|
remark: Joi.number().optional().allow(''),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const dependenceService = Container.get(DependenceService);
|
||||||
|
const data = await dependenceService.create(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/dependencies',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
name: Joi.string().required(),
|
||||||
|
_id: Joi.string().required(),
|
||||||
|
type: Joi.number().required(),
|
||||||
|
remark: Joi.number().optional().allow(''),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const dependenceService = Container.get(DependenceService);
|
||||||
|
const data = await dependenceService.update(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.delete(
|
||||||
|
'/dependencies',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.string().required()),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const dependenceService = Container.get(DependenceService);
|
||||||
|
const data = await dependenceService.remove(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.delete(
|
||||||
|
'/dependencies/force',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.string().required()),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const dependenceService = Container.get(DependenceService);
|
||||||
|
const data = await dependenceService.removeDb(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/dependencies/:id',
|
||||||
|
celebrate({
|
||||||
|
params: Joi.object({
|
||||||
|
id: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const dependenceService = Container.get(DependenceService);
|
||||||
|
const data = await dependenceService.get(req.params.id);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/dependencies/reinstall',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.string().required()),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const dependenceService = Container.get(DependenceService);
|
||||||
|
const data = await dependenceService.reInstall(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
+8
-6
@@ -25,11 +25,13 @@ export default (app: Router) => {
|
|||||||
route.post(
|
route.post(
|
||||||
'/envs',
|
'/envs',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.array().items(
|
||||||
value: Joi.string().required(),
|
Joi.object({
|
||||||
name: Joi.string().required(),
|
value: Joi.string().required(),
|
||||||
remarks: Joi.string().optional(),
|
name: Joi.string().required(),
|
||||||
}),
|
remarks: Joi.string().optional().allow(''),
|
||||||
|
}),
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
@@ -50,7 +52,7 @@ export default (app: Router) => {
|
|||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
value: Joi.string().required(),
|
value: Joi.string().required(),
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
remarks: Joi.string().optional(),
|
remarks: Joi.string().optional().allow(''),
|
||||||
_id: Joi.string().required(),
|
_id: Joi.string().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
+6
-2
@@ -1,19 +1,23 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import auth from './auth';
|
import user from './user';
|
||||||
import env from './env';
|
import env from './env';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
import log from './log';
|
import log from './log';
|
||||||
import cron from './cron';
|
import cron from './cron';
|
||||||
import script from './script';
|
import script from './script';
|
||||||
|
import open from './open';
|
||||||
|
import dependence from './dependence';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const app = Router();
|
const app = Router();
|
||||||
auth(app);
|
user(app);
|
||||||
env(app);
|
env(app);
|
||||||
config(app);
|
config(app);
|
||||||
log(app);
|
log(app);
|
||||||
cron(app);
|
cron(app);
|
||||||
script(app);
|
script(app);
|
||||||
|
open(app);
|
||||||
|
dependence(app);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
import OpenService from '../services/open';
|
||||||
|
import { Logger } from 'winston';
|
||||||
|
import { celebrate, Joi } from 'celebrate';
|
||||||
|
const route = Router();
|
||||||
|
|
||||||
|
export default (app: Router) => {
|
||||||
|
app.use('/', route);
|
||||||
|
route.get(
|
||||||
|
'/apps',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const openService = Container.get(OpenService);
|
||||||
|
const data = await openService.list();
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
'/apps',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
name: Joi.string().optional().allow(''),
|
||||||
|
scopes: Joi.array().items(Joi.string().required()),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const openService = Container.get(OpenService);
|
||||||
|
const data = await openService.create(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/apps',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
name: Joi.string().optional().allow(''),
|
||||||
|
scopes: Joi.array().items(Joi.string()),
|
||||||
|
_id: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const openService = Container.get(OpenService);
|
||||||
|
const data = await openService.update(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.delete(
|
||||||
|
'/apps',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.array().items(Joi.string().required()),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const openService = Container.get(OpenService);
|
||||||
|
const data = await openService.remove(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/apps/:id/reset-secret',
|
||||||
|
celebrate({
|
||||||
|
params: Joi.object({
|
||||||
|
id: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const openService = Container.get(OpenService);
|
||||||
|
const data = await openService.resetSecret(req.params.id);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/auth/token',
|
||||||
|
celebrate({
|
||||||
|
query: {
|
||||||
|
client_id: Joi.string().required(),
|
||||||
|
client_secret: Joi.string().required(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const openService = Container.get(OpenService);
|
||||||
|
const result = await openService.authToken(req.query as any);
|
||||||
|
return res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
+198
-20
@@ -1,10 +1,16 @@
|
|||||||
import { getFileContentByName, getLastModifyFilePath } from '../config/util';
|
import {
|
||||||
|
fileExist,
|
||||||
|
getFileContentByName,
|
||||||
|
getLastModifyFilePath,
|
||||||
|
} from '../config/util';
|
||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
|
import path, { join } from 'path';
|
||||||
|
import ScriptService from '../services/script';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
@@ -16,15 +22,55 @@ export default (app: Router) => {
|
|||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const fileList = fs.readdirSync(config.scriptPath, 'utf-8');
|
const fileList = fs.readdirSync(config.scriptPath, 'utf-8');
|
||||||
|
|
||||||
|
let result = [];
|
||||||
|
for (let i = 0; i < fileList.length; i++) {
|
||||||
|
const fileOrDir = fileList[i];
|
||||||
|
const fPath = path.join(config.scriptPath, fileOrDir);
|
||||||
|
const dirStat = fs.statSync(fPath);
|
||||||
|
if (['node_modules'].includes(fileOrDir)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dirStat.isDirectory()) {
|
||||||
|
const childFileList = fs.readdirSync(fPath, 'utf-8');
|
||||||
|
let children = [];
|
||||||
|
for (let j = 0; j < childFileList.length; j++) {
|
||||||
|
const childFile = childFileList[j];
|
||||||
|
const sPath = path.join(config.scriptPath, fileOrDir, childFile);
|
||||||
|
const _fileExist = await fileExist(sPath);
|
||||||
|
if (_fileExist && fs.statSync(sPath).isFile()) {
|
||||||
|
const statObj = fs.statSync(sPath);
|
||||||
|
children.push({
|
||||||
|
title: childFile,
|
||||||
|
value: childFile,
|
||||||
|
key: `${fileOrDir}/${childFile}`,
|
||||||
|
mtime: statObj.mtimeMs,
|
||||||
|
parent: fileOrDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push({
|
||||||
|
title: fileOrDir,
|
||||||
|
value: fileOrDir,
|
||||||
|
key: fileOrDir,
|
||||||
|
mtime: dirStat.mtimeMs,
|
||||||
|
disabled: true,
|
||||||
|
children: children.sort((a, b) => b.mtime - a.mtime),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result.push({
|
||||||
|
title: fileOrDir,
|
||||||
|
value: fileOrDir,
|
||||||
|
key: fileOrDir,
|
||||||
|
mtime: dirStat.mtimeMs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
code: 200,
|
code: 200,
|
||||||
data: fileList
|
data: result,
|
||||||
.filter((x) => {
|
|
||||||
return !fs.lstatSync(config.scriptPath + x).isDirectory();
|
|
||||||
})
|
|
||||||
.map((x) => {
|
|
||||||
return { title: x, value: x, key: x };
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
logger.error('🔥 error: %o', e);
|
||||||
@@ -38,9 +84,12 @@ export default (app: Router) => {
|
|||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const content = getFileContentByName(
|
const filePath = join(
|
||||||
`${config.scriptPath}${req.params.file}`,
|
config.scriptPath,
|
||||||
|
req.query.path as string,
|
||||||
|
req.params.file,
|
||||||
);
|
);
|
||||||
|
const content = getFileContentByName(filePath);
|
||||||
res.send({ code: 200, data: content });
|
res.send({ code: 200, data: content });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
logger.error('🔥 error: %o', e);
|
||||||
@@ -54,31 +103,51 @@ export default (app: Router) => {
|
|||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
filename: Joi.string().required(),
|
filename: Joi.string().required(),
|
||||||
path: Joi.string().required(),
|
path: Joi.string().optional().allow(''),
|
||||||
content: Joi.string().required(),
|
content: Joi.string().allow(''),
|
||||||
|
originFilename: Joi.string().allow(''),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
let { filename, path, content } = req.body as {
|
let { filename, path, content, originFilename } = req.body as {
|
||||||
filename: string;
|
filename: string;
|
||||||
path: string;
|
path: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
originFilename: string;
|
||||||
};
|
};
|
||||||
|
if (!path) {
|
||||||
|
path = config.scriptPath;
|
||||||
|
}
|
||||||
if (!path.endsWith('/')) {
|
if (!path.endsWith('/')) {
|
||||||
path += '/';
|
path += '/';
|
||||||
}
|
}
|
||||||
if (config.writePathList.every((x) => !path.startsWith(x))) {
|
if (!path.startsWith('/')) {
|
||||||
return res.send({ code: 400, data: '文件路径错误,可保存目录/ql/scripts、/ql/config、/ql/jbot、/ql/bak' });
|
path = `${config.scriptPath}${path}`;
|
||||||
}
|
}
|
||||||
|
if (config.writePathList.every((x) => !path.startsWith(x))) {
|
||||||
|
return res.send({
|
||||||
|
code: 430,
|
||||||
|
data: '文件路径禁止访问',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!originFilename) {
|
||||||
|
originFilename = filename;
|
||||||
|
}
|
||||||
|
const originFilePath = `${path}${originFilename.replace(/\//g, '')}`;
|
||||||
const filePath = `${path}${filename.replace(/\//g, '')}`;
|
const filePath = `${path}${filename.replace(/\//g, '')}`;
|
||||||
const bakPath = '/ql/bak';
|
if (fs.existsSync(originFilePath)) {
|
||||||
if (fs.existsSync(filePath)) {
|
if (!fs.existsSync(config.bakPath)) {
|
||||||
if (!fs.existsSync(bakPath)) {
|
fs.mkdirSync(config.bakPath);
|
||||||
fs.mkdirSync(bakPath);
|
}
|
||||||
|
fs.copyFileSync(
|
||||||
|
originFilePath,
|
||||||
|
`${config.bakPath}${originFilename.replace(/\//g, '')}`,
|
||||||
|
);
|
||||||
|
if (filename !== originFilename) {
|
||||||
|
fs.unlinkSync(originFilePath);
|
||||||
}
|
}
|
||||||
fs.copyFileSync(filePath, bakPath);
|
|
||||||
}
|
}
|
||||||
fs.writeFileSync(filePath, content);
|
fs.writeFileSync(filePath, content);
|
||||||
return res.send({ code: 200 });
|
return res.send({ code: 200 });
|
||||||
@@ -88,4 +157,113 @@ export default (app: Router) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/scripts',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
filename: Joi.string().required(),
|
||||||
|
path: Joi.string().optional().allow(''),
|
||||||
|
content: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
let { filename, content, path } = req.body as {
|
||||||
|
filename: string;
|
||||||
|
content: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
const filePath = join(config.scriptPath, path, filename);
|
||||||
|
fs.writeFileSync(filePath, content);
|
||||||
|
return res.send({ code: 200 });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.delete(
|
||||||
|
'/scripts',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
filename: Joi.string().required(),
|
||||||
|
path: Joi.string().allow(''),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
let { filename, path } = req.body as {
|
||||||
|
filename: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
const filePath = join(config.scriptPath, path, filename);
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
res.send({ code: 200 });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
'/scripts/download',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
filename: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
let { filename } = req.body as {
|
||||||
|
filename: string;
|
||||||
|
};
|
||||||
|
const filePath = `${config.scriptPath}${filename}`;
|
||||||
|
// const stats = fs.statSync(filePath);
|
||||||
|
// res.set({
|
||||||
|
// 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件
|
||||||
|
// 'Content-Disposition': 'attachment; filename=' + filename, //告诉浏览器这是一个需要下载的文件
|
||||||
|
// 'Content-Length': stats.size //文件大小
|
||||||
|
// });
|
||||||
|
// fs.createReadStream(filePath).pipe(res);
|
||||||
|
return res.download(filePath, filename, (err) => {
|
||||||
|
return next(err);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/scripts/run',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
filename: Joi.string().required(),
|
||||||
|
path: Joi.string().optional().allow(''),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
let { filename, path } = req.body as {
|
||||||
|
filename: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
const filePath = join(path, filename);
|
||||||
|
const scriptService = Container.get(ScriptService);
|
||||||
|
const result = await scriptService.runScript(filePath);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,342 @@
|
|||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
import { Logger } from 'winston';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import config from '../config';
|
||||||
|
import UserService from '../services/user';
|
||||||
|
import { celebrate, Joi } from 'celebrate';
|
||||||
|
import { getFileContentByName } from '../config/util';
|
||||||
|
const route = Router();
|
||||||
|
|
||||||
|
export default (app: Router) => {
|
||||||
|
app.use('/', route);
|
||||||
|
route.post(
|
||||||
|
'/login',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
username: Joi.string().required(),
|
||||||
|
password: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const data = await userService.login({ ...req.body }, req);
|
||||||
|
return res.send(data);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
'/logout',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
await userService.logout(req.platform);
|
||||||
|
res.send({ code: 200 });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/user',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
username: Joi.string().required(),
|
||||||
|
password: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
await userService.updateUsernameAndPassword(req.body);
|
||||||
|
res.send({ code: 200, message: '更新成功' });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/user',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const authInfo = await userService.getUserInfo();
|
||||||
|
res.send({
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
username: authInfo.username,
|
||||||
|
twoFactorActivated: authInfo.twoFactorActivated,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/user/two-factor/init',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const data = await userService.initTwoFactor();
|
||||||
|
res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/user/two-factor/active',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
code: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const data = await userService.activeTwoFactor(req.body.code);
|
||||||
|
res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/user/two-factor/deactive',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const data = await userService.deactiveTwoFactor();
|
||||||
|
res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/user/two-factor/login',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
code: Joi.string().required(),
|
||||||
|
username: Joi.string().required(),
|
||||||
|
password: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const data = await userService.twoFactorLogin(req.body, req);
|
||||||
|
res.send(data);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/user/login-log',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const data = await userService.getLoginLog();
|
||||||
|
res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/user/notification',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const data = await userService.getNotificationMode();
|
||||||
|
res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/user/notification',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const result = await userService.updateNotificationMode(req.body);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/system',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const authInfo = await userService.getUserInfo();
|
||||||
|
const envDbContent = getFileContentByName(config.envDbFile);
|
||||||
|
|
||||||
|
let isInitialized = true;
|
||||||
|
if (
|
||||||
|
Object.keys(authInfo).length === 2 &&
|
||||||
|
authInfo.username === 'admin' &&
|
||||||
|
authInfo.password === 'admin' &&
|
||||||
|
envDbContent.length === 0
|
||||||
|
) {
|
||||||
|
isInitialized = false;
|
||||||
|
}
|
||||||
|
res.send({
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
isInitialized,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 初始化api
|
||||||
|
route.put(
|
||||||
|
'/init/user',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
username: Joi.string().required(),
|
||||||
|
password: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
await userService.updateUsernameAndPassword(req.body);
|
||||||
|
res.send({ code: 200, message: '更新成功' });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/init/notification',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const result = await userService.updateNotificationMode(req.body);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/system/log/remove',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const data = await userService.getLogRemoveFrequency();
|
||||||
|
res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/system/log/remove',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
frequency: Joi.number().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const result = await userService.updateLogRemoveFrequency(
|
||||||
|
req.body.frequency,
|
||||||
|
);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/system/update-check',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const result = await userService.checkUpdate();
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/system/update',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const result = await userService.updateSystem();
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
+10
-2
@@ -9,9 +9,15 @@ import Logger from './loaders/logger';
|
|||||||
async function startServer() {
|
async function startServer() {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
await require('./loaders').default({ expressApp: app });
|
await require('./loaders/initFile').default();
|
||||||
|
|
||||||
app
|
await require('./loaders/sentry').default({ expressApp: app });
|
||||||
|
|
||||||
|
await require('./loaders/db').default();
|
||||||
|
|
||||||
|
await require('./loaders/app').default({ expressApp: app });
|
||||||
|
|
||||||
|
const server = app
|
||||||
.listen(config.port, () => {
|
.listen(config.port, () => {
|
||||||
Logger.info(`
|
Logger.info(`
|
||||||
################################################
|
################################################
|
||||||
@@ -23,6 +29,8 @@ async function startServer() {
|
|||||||
Logger.error(err);
|
Logger.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await require('./loaders/server').default({ server });
|
||||||
}
|
}
|
||||||
|
|
||||||
startServer();
|
startServer();
|
||||||
|
|||||||
+24
-11
@@ -4,8 +4,11 @@ import { createRandomString } from './util';
|
|||||||
|
|
||||||
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
||||||
|
|
||||||
|
const lastVersionFile =
|
||||||
|
'https://raw.githubusercontent.com/whyour/qinglong/master/src/version.ts';
|
||||||
|
|
||||||
const envFound = dotenv.config();
|
const envFound = dotenv.config();
|
||||||
const rootPath = path.resolve(__dirname, '../../');
|
const rootPath = process.cwd();
|
||||||
const envFile = path.join(rootPath, 'config/env.sh');
|
const envFile = path.join(rootPath, 'config/env.sh');
|
||||||
const confFile = path.join(rootPath, 'config/config.sh');
|
const confFile = path.join(rootPath, 'config/config.sh');
|
||||||
const sampleFile = path.join(rootPath, 'sample/config.sample.sh');
|
const sampleFile = path.join(rootPath, 'sample/config.sample.sh');
|
||||||
@@ -15,6 +18,7 @@ const authConfigFile = path.join(rootPath, 'config/auth.json');
|
|||||||
const extraFile = path.join(rootPath, 'config/extra.sh');
|
const extraFile = path.join(rootPath, 'config/extra.sh');
|
||||||
const configPath = path.join(rootPath, 'config/');
|
const configPath = path.join(rootPath, 'config/');
|
||||||
const scriptPath = path.join(rootPath, 'scripts/');
|
const scriptPath = path.join(rootPath, 'scripts/');
|
||||||
|
const bakPath = path.join(rootPath, 'bak/');
|
||||||
const samplePath = path.join(rootPath, 'sample/');
|
const samplePath = path.join(rootPath, 'sample/');
|
||||||
const logPath = path.join(rootPath, 'log/');
|
const logPath = path.join(rootPath, 'log/');
|
||||||
const authError = '错误的用户名密码,请重试';
|
const authError = '错误的用户名密码,请重试';
|
||||||
@@ -23,16 +27,15 @@ const configString = 'config sample crontab shareCode diy';
|
|||||||
const dbPath = path.join(rootPath, 'db/');
|
const dbPath = path.join(rootPath, 'db/');
|
||||||
const cronDbFile = path.join(rootPath, 'db/crontab.db');
|
const cronDbFile = path.join(rootPath, 'db/crontab.db');
|
||||||
const envDbFile = path.join(rootPath, 'db/env.db');
|
const envDbFile = path.join(rootPath, 'db/env.db');
|
||||||
const configFound = dotenv.config({ path: confFile });
|
const appDbFile = path.join(rootPath, 'db/app.db');
|
||||||
|
const authDbFile = path.join(rootPath, 'db/auth.db');
|
||||||
|
const dependenceDbFile = path.join(rootPath, 'db/dependence.db');
|
||||||
|
const versionFile = path.join(rootPath, 'src/version.ts');
|
||||||
|
|
||||||
if (envFound.error) {
|
if (envFound.error) {
|
||||||
throw new Error("⚠️ Couldn't find .env file ⚠️");
|
throw new Error("⚠️ Couldn't find .env file ⚠️");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configFound.error) {
|
|
||||||
throw new Error("⚠️ Couldn't find config.sh file ⚠️");
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
port: parseInt(process.env.PORT as string, 10),
|
port: parseInt(process.env.PORT as string, 10),
|
||||||
cronPort: parseInt(process.env.CRON_PORT as string, 10),
|
cronPort: parseInt(process.env.CRON_PORT as string, 10),
|
||||||
@@ -43,6 +46,7 @@ export default {
|
|||||||
api: {
|
api: {
|
||||||
prefix: '/api',
|
prefix: '/api',
|
||||||
},
|
},
|
||||||
|
rootPath,
|
||||||
configString,
|
configString,
|
||||||
loginFaild,
|
loginFaild,
|
||||||
authError,
|
authError,
|
||||||
@@ -57,6 +61,9 @@ export default {
|
|||||||
dbPath,
|
dbPath,
|
||||||
cronDbFile,
|
cronDbFile,
|
||||||
envDbFile,
|
envDbFile,
|
||||||
|
appDbFile,
|
||||||
|
authDbFile,
|
||||||
|
dependenceDbFile,
|
||||||
configPath,
|
configPath,
|
||||||
scriptPath,
|
scriptPath,
|
||||||
samplePath,
|
samplePath,
|
||||||
@@ -67,10 +74,16 @@ export default {
|
|||||||
'crontab.list',
|
'crontab.list',
|
||||||
'env.sh',
|
'env.sh',
|
||||||
],
|
],
|
||||||
writePathList: [
|
writePathList: [configPath, scriptPath],
|
||||||
'/ql/scripts/',
|
bakPath,
|
||||||
'/ql/config/',
|
apiWhiteList: [
|
||||||
'/ql/jbot/',
|
'/api/login',
|
||||||
'/ql/bak/',
|
'/open/auth/token',
|
||||||
|
'/api/user/two-factor/login',
|
||||||
|
'/api/system',
|
||||||
|
'/api/init/user',
|
||||||
|
'/api/init/notification',
|
||||||
],
|
],
|
||||||
|
versionFile,
|
||||||
|
lastVersionFile,
|
||||||
};
|
};
|
||||||
|
|||||||
+115
-3
@@ -1,5 +1,7 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import got from 'got';
|
||||||
|
import iconv from 'iconv-lite';
|
||||||
|
|
||||||
export function getFileContentByName(fileName: string) {
|
export function getFileContentByName(fileName: string) {
|
||||||
if (fs.existsSync(fileName)) {
|
if (fs.existsSync(fileName)) {
|
||||||
@@ -85,7 +87,7 @@ export function createRandomString(min: number, max: number): string {
|
|||||||
'Y',
|
'Y',
|
||||||
'Z',
|
'Z',
|
||||||
];
|
];
|
||||||
const special = ['-', '_', '#'];
|
const special = ['-', '_'];
|
||||||
const config = num.concat(english).concat(ENGLISH).concat(special);
|
const config = num.concat(english).concat(ENGLISH).concat(special);
|
||||||
|
|
||||||
const arr = [];
|
const arr = [];
|
||||||
@@ -113,9 +115,119 @@ export function createRandomString(min: number, max: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getToken(req: any) {
|
export function getToken(req: any) {
|
||||||
const { authorization } = req.headers;
|
const { authorization = '' } = req.headers;
|
||||||
if (authorization && authorization.split(' ')[0] === 'Bearer') {
|
if (authorization && authorization.split(' ')[0] === 'Bearer') {
|
||||||
return authorization.split(' ')[1];
|
return (authorization as string)
|
||||||
|
.replace('Bearer ', '')
|
||||||
|
.replace('mobile-', '')
|
||||||
|
.replace('desktop-', '');
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getNetIp(req: any) {
|
||||||
|
const ipArray = [
|
||||||
|
...new Set([
|
||||||
|
...(req.headers['x-real-ip'] || '').split(','),
|
||||||
|
...(req.headers['x-forwarded-for'] || '').split(','),
|
||||||
|
req.ip,
|
||||||
|
...req.ips,
|
||||||
|
req.socket.remoteAddress,
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
let ip = ipArray[0];
|
||||||
|
|
||||||
|
if (ipArray.length > 1) {
|
||||||
|
for (let i = 0; i < ipArray.length; i++) {
|
||||||
|
const ipNumArray = ipArray[i].split('.');
|
||||||
|
const tmp = ipNumArray[0] + '.' + ipNumArray[1];
|
||||||
|
if (
|
||||||
|
tmp === '192.168' ||
|
||||||
|
(ipNumArray[0] === '172' &&
|
||||||
|
ipNumArray[1] >= 16 &&
|
||||||
|
ipNumArray[1] <= 32) ||
|
||||||
|
tmp === '10.7' ||
|
||||||
|
tmp === '127.0'
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ip = ipArray[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ip = ip.substr(ip.lastIndexOf(':') + 1, ip.length);
|
||||||
|
if (ip.includes('127.0') || ip.includes('192.168') || ip.includes('10.7')) {
|
||||||
|
ip = '';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const baiduApi = got
|
||||||
|
.get(`https://www.cip.cc/${ip}`, { timeout: 10000, retry: 0 })
|
||||||
|
.text();
|
||||||
|
const ipApi = got
|
||||||
|
.get(`https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`, {
|
||||||
|
timeout: 10000,
|
||||||
|
retry: 0,
|
||||||
|
})
|
||||||
|
.buffer();
|
||||||
|
const [data, ipApiBody] = await await Promise.all<any>([baiduApi, ipApi]);
|
||||||
|
|
||||||
|
const ipRegx = /.*IP :(.*)\n/;
|
||||||
|
const addrRegx = /.*数据二 :(.*)\n/;
|
||||||
|
if (data && ipRegx.test(data) && addrRegx.test(data)) {
|
||||||
|
const ip = data.match(ipRegx)[1];
|
||||||
|
const addr = data.match(addrRegx)[1];
|
||||||
|
return { address: addr, ip };
|
||||||
|
} else if (ipApiBody) {
|
||||||
|
const { addr, ip } = JSON.parse(iconv.decode(ipApiBody, 'GBK'));
|
||||||
|
return { address: `${addr}`, ip };
|
||||||
|
} else {
|
||||||
|
return { address: `获取失败`, ip };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return { address: `获取失败`, ip };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
|
||||||
|
const ua = userAgent.toLowerCase();
|
||||||
|
const testUa = (regexp: RegExp) => regexp.test(ua);
|
||||||
|
const testVs = (regexp: RegExp) =>
|
||||||
|
(ua.match(regexp) || [])
|
||||||
|
.toString()
|
||||||
|
.replace(/[^0-9|_.]/g, '')
|
||||||
|
.replace(/_/g, '.');
|
||||||
|
|
||||||
|
// 系统
|
||||||
|
let system = 'unknow';
|
||||||
|
if (testUa(/windows|win32|win64|wow32|wow64/g)) {
|
||||||
|
system = 'windows'; // windows系统
|
||||||
|
} else if (testUa(/macintosh|macintel/g)) {
|
||||||
|
system = 'macos'; // macos系统
|
||||||
|
} else if (testUa(/x11/g)) {
|
||||||
|
system = 'linux'; // linux系统
|
||||||
|
} else if (testUa(/android|adr/g)) {
|
||||||
|
system = 'android'; // android系统
|
||||||
|
} else if (testUa(/ios|iphone|ipad|ipod|iwatch/g)) {
|
||||||
|
system = 'ios'; // ios系统
|
||||||
|
}
|
||||||
|
|
||||||
|
let platform = 'desktop';
|
||||||
|
if (system === 'windows' || system === 'macos' || system === 'linux') {
|
||||||
|
platform = 'desktop';
|
||||||
|
} else if (system === 'android' || system === 'ios' || testUa(/mobile/g)) {
|
||||||
|
platform = 'mobile';
|
||||||
|
}
|
||||||
|
|
||||||
|
return platform as 'mobile' | 'desktop';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fileExist(file: any) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
try {
|
||||||
|
fs.accessSync(file);
|
||||||
|
resolve(true);
|
||||||
|
} catch (error) {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export class AuthInfo {
|
||||||
|
ip?: string;
|
||||||
|
type: AuthDataType;
|
||||||
|
info?: any;
|
||||||
|
_id?: string;
|
||||||
|
|
||||||
|
constructor(options: AuthInfo) {
|
||||||
|
this.ip = options.ip;
|
||||||
|
this.info = options.info;
|
||||||
|
this.type = options.type;
|
||||||
|
this._id = options._id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LoginStatus {
|
||||||
|
'success',
|
||||||
|
'fail',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AuthDataType {
|
||||||
|
'loginLog' = 'loginLog',
|
||||||
|
'authToken' = 'authToken',
|
||||||
|
'notification' = 'notification',
|
||||||
|
'removeLogFrequency' = 'removeLogFrequency',
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ export class Crontab {
|
|||||||
pid?: number;
|
pid?: number;
|
||||||
isDisabled?: 1 | 0;
|
isDisabled?: 1 | 0;
|
||||||
log_path?: string;
|
log_path?: string;
|
||||||
|
isPinned?: 1 | 0;
|
||||||
|
|
||||||
constructor(options: Crontab) {
|
constructor(options: Crontab) {
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
@@ -27,6 +28,7 @@ export class Crontab {
|
|||||||
this.pid = options.pid;
|
this.pid = options.pid;
|
||||||
this.isDisabled = options.isDisabled || 0;
|
this.isDisabled = options.isDisabled || 0;
|
||||||
this.log_path = options.log_path || '';
|
this.log_path = options.log_path || '';
|
||||||
|
this.isPinned = options.isPinned || 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
export class Dependence {
|
||||||
|
timestamp?: string;
|
||||||
|
created?: number;
|
||||||
|
_id?: string;
|
||||||
|
status?: DependenceStatus;
|
||||||
|
type?: DependenceTypes;
|
||||||
|
name?: number;
|
||||||
|
log?: string[];
|
||||||
|
remark?: string;
|
||||||
|
|
||||||
|
constructor(options: Dependence) {
|
||||||
|
this._id = options._id;
|
||||||
|
this.created = options.created || new Date().valueOf();
|
||||||
|
this.status = options.status || DependenceStatus.installing;
|
||||||
|
this.type = options.type || DependenceTypes.nodejs;
|
||||||
|
this.timestamp = new Date().toString();
|
||||||
|
this.name = options.name;
|
||||||
|
this.log = options.log || [];
|
||||||
|
this.remark = options.remark || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DependenceStatus {
|
||||||
|
'installing',
|
||||||
|
'installed',
|
||||||
|
'installFailed',
|
||||||
|
'removing',
|
||||||
|
'removed',
|
||||||
|
'removeFailed',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DependenceTypes {
|
||||||
|
'nodejs',
|
||||||
|
'python3',
|
||||||
|
'linux',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum InstallDependenceCommandTypes {
|
||||||
|
'npm i -g --force',
|
||||||
|
'pip3 install',
|
||||||
|
'apk add --no-cache -f',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum unInstallDependenceCommandTypes {
|
||||||
|
'npm uninstall -g --force',
|
||||||
|
'pip3 uninstall -y',
|
||||||
|
'apk del -f',
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
export enum NotificationMode {
|
||||||
|
'gotify' = 'gotify',
|
||||||
|
'goCqHttpBot' = 'goCqHttpBot',
|
||||||
|
'serverChan' = 'serverChan',
|
||||||
|
'bark' = 'bark',
|
||||||
|
'telegramBot' = 'telegramBot',
|
||||||
|
'dingtalkBot' = 'dingtalkBot',
|
||||||
|
'weWorkBot' = 'weWorkBot',
|
||||||
|
'weWorkApp' = 'weWorkApp',
|
||||||
|
'iGot' = 'iGot',
|
||||||
|
'pushPlus' = 'pushPlus',
|
||||||
|
'email' = 'email',
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class NotificationBaseInfo {
|
||||||
|
public type!: NotificationMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GotifyNotification extends NotificationBaseInfo {
|
||||||
|
public gotifyUrl = '';
|
||||||
|
public gotifyToken = '';
|
||||||
|
public gotifyPriority = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GoCqHttpBotNotification extends NotificationBaseInfo {
|
||||||
|
public goCqHttpBotUrl = '';
|
||||||
|
public goCqHttpBotToken = '';
|
||||||
|
public goCqHttpBotQq = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ServerChanNotification extends NotificationBaseInfo {
|
||||||
|
public serverChanKey = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BarkNotification extends NotificationBaseInfo {
|
||||||
|
public barkPush = '';
|
||||||
|
public barkSound = '';
|
||||||
|
public barkGroup = 'qinglong';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TelegramBotNotification extends NotificationBaseInfo {
|
||||||
|
public telegramBotToken = '';
|
||||||
|
public telegramBotUserId = '';
|
||||||
|
public telegramBotProxyHost = '';
|
||||||
|
public telegramBotProxyPort = '';
|
||||||
|
public telegramBotProxyAuth = '';
|
||||||
|
public telegramBotApiHost = 'api.telegram.org';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DingtalkBotNotification extends NotificationBaseInfo {
|
||||||
|
public dingtalkBotToken = '';
|
||||||
|
public dingtalkBotSecret = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WeWorkBotNotification extends NotificationBaseInfo {
|
||||||
|
public weWorkBotKey = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WeWorkAppNotification extends NotificationBaseInfo {
|
||||||
|
public weWorkAppKey = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IGotNotification extends NotificationBaseInfo {
|
||||||
|
public iGotPushKey = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PushPlusNotification extends NotificationBaseInfo {
|
||||||
|
public pushPlusToken = '';
|
||||||
|
public pushPlusUser = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EmailNotification extends NotificationBaseInfo {
|
||||||
|
public emailService: string = '';
|
||||||
|
public emailUser: string = '';
|
||||||
|
public emailPass: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationInfo
|
||||||
|
extends GoCqHttpBotNotification,
|
||||||
|
GotifyNotification,
|
||||||
|
ServerChanNotification,
|
||||||
|
BarkNotification,
|
||||||
|
TelegramBotNotification,
|
||||||
|
DingtalkBotNotification,
|
||||||
|
WeWorkBotNotification,
|
||||||
|
WeWorkAppNotification,
|
||||||
|
IGotNotification,
|
||||||
|
PushPlusNotification,
|
||||||
|
EmailNotification {}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
export class App {
|
||||||
|
name: string;
|
||||||
|
scopes: AppScope[];
|
||||||
|
client_id: string;
|
||||||
|
client_secret: string;
|
||||||
|
tokens?: AppToken[];
|
||||||
|
_id?: string;
|
||||||
|
|
||||||
|
constructor(options: App) {
|
||||||
|
this.name = options.name;
|
||||||
|
this.scopes = options.scopes;
|
||||||
|
this.client_id = options.client_id;
|
||||||
|
this.client_secret = options.client_secret;
|
||||||
|
this._id = options._id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppToken {
|
||||||
|
value: string;
|
||||||
|
type: 'Bearer';
|
||||||
|
expiration: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
|
||||||
|
|
||||||
|
export enum CrontabStatus {
|
||||||
|
'running',
|
||||||
|
'idle',
|
||||||
|
'disabled',
|
||||||
|
'queued',
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export class SockMessage {
|
||||||
|
message?: string;
|
||||||
|
type?: SockMessageType;
|
||||||
|
references?: string[];
|
||||||
|
|
||||||
|
constructor(options: SockMessage) {
|
||||||
|
this.type = options.type;
|
||||||
|
this.message = options.message;
|
||||||
|
this.references = options.references;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SockMessageType =
|
||||||
|
| 'ping'
|
||||||
|
| 'installDependence'
|
||||||
|
| 'updateSystemVersion'
|
||||||
|
| 'manuallyRunScript';
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
declare namespace Express {
|
||||||
|
interface Request {
|
||||||
|
platform: 'desktop' | 'mobile';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,10 @@ import expressLoader from './express';
|
|||||||
import dependencyInjectorLoader from './dependencyInjector';
|
import dependencyInjectorLoader from './dependencyInjector';
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
import initData from './initData';
|
import initData from './initData';
|
||||||
|
import { Application } from 'express';
|
||||||
|
import linkDeps from './deps';
|
||||||
|
|
||||||
export default async ({ expressApp }: { expressApp: any }) => {
|
export default async ({ expressApp }: { expressApp: Application }) => {
|
||||||
Logger.info('✌️ DB loaded and connected!');
|
|
||||||
|
|
||||||
await dependencyInjectorLoader({
|
await dependencyInjectorLoader({
|
||||||
models: [],
|
models: [],
|
||||||
});
|
});
|
||||||
@@ -16,4 +16,7 @@ export default async ({ expressApp }: { expressApp: any }) => {
|
|||||||
|
|
||||||
await initData();
|
await initData();
|
||||||
Logger.info('✌️ init data loaded');
|
Logger.info('✌️ init data loaded');
|
||||||
|
|
||||||
|
await linkDeps();
|
||||||
|
Logger.info('✌️ link deps');
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import DataStore from 'nedb';
|
||||||
|
import config from '../config';
|
||||||
|
import Logger from './logger';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { fileExist } from '../config/util';
|
||||||
|
|
||||||
|
interface Dbs {
|
||||||
|
cronDb: DataStore;
|
||||||
|
dependenceDb: DataStore;
|
||||||
|
envDb: DataStore;
|
||||||
|
appDb: DataStore;
|
||||||
|
authDb: DataStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db: Dbs = {} as any;
|
||||||
|
|
||||||
|
async function truncateDb() {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
const files = [
|
||||||
|
config.cronDbFile,
|
||||||
|
config.dependenceDbFile,
|
||||||
|
config.envDbFile,
|
||||||
|
config.appDbFile,
|
||||||
|
config.authDbFile,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const _fileExist = await fileExist(file);
|
||||||
|
if (_fileExist && fs.statSync(file).size >= 1024 * 1024 * 500) {
|
||||||
|
fs.truncateSync(file, 1024 * 1024 * 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export default async () => {
|
||||||
|
try {
|
||||||
|
await truncateDb();
|
||||||
|
|
||||||
|
db.cronDb = new DataStore({ filename: config.cronDbFile, autoload: true });
|
||||||
|
db.dependenceDb = new DataStore({
|
||||||
|
filename: config.dependenceDbFile,
|
||||||
|
autoload: true,
|
||||||
|
});
|
||||||
|
db.envDb = new DataStore({ filename: config.envDbFile, autoload: true });
|
||||||
|
db.appDb = new DataStore({ filename: config.appDbFile, autoload: true });
|
||||||
|
db.authDb = new DataStore({ filename: config.authDbFile, autoload: true });
|
||||||
|
|
||||||
|
// compaction data file
|
||||||
|
db.cronDb.persistence.compactDatafile();
|
||||||
|
db.envDb.persistence.compactDatafile();
|
||||||
|
db.dependenceDb.persistence.compactDatafile();
|
||||||
|
db.appDb.persistence.compactDatafile();
|
||||||
|
db.authDb.persistence.compactDatafile();
|
||||||
|
|
||||||
|
Logger.info('✌️ DB loaded');
|
||||||
|
} catch (error) {
|
||||||
|
Logger.info('✌️ DB load failed');
|
||||||
|
Logger.info(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dbs: Dbs = db;
|
||||||
@@ -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));
|
||||||
|
};
|
||||||
+97
-11
@@ -5,29 +5,75 @@ import routes from '../api';
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import jwt from 'express-jwt';
|
import jwt from 'express-jwt';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { getToken } from '../config/util';
|
import { getFileContentByName, getPlatform, getToken } from '../config/util';
|
||||||
|
import Container from 'typedi';
|
||||||
|
import OpenService from '../services/open';
|
||||||
|
import rewrite from 'express-urlrewrite';
|
||||||
|
import UserService from '../services/user';
|
||||||
|
import handler from 'serve-handler';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
|
||||||
export default ({ app }: { app: Application }) => {
|
export default ({ app }: { app: Application }) => {
|
||||||
app.enable('trust proxy');
|
app.enable('trust proxy');
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (req.path.startsWith('/api') || req.path.startsWith('/open')) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
return handler(req, res, {
|
||||||
|
public: 'dist',
|
||||||
|
rewrites: [{ source: '**', destination: '/index.html' }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
app.use(bodyParser.json({ limit: '50mb' }));
|
app.use(bodyParser.json({ limit: '50mb' }));
|
||||||
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
jwt({ secret: config.secret as string, algorithms: ['HS384'] }).unless({
|
jwt({
|
||||||
path: ['/api/login', '/api/crons/status'],
|
secret: config.secret as string,
|
||||||
|
algorithms: ['HS384'],
|
||||||
|
}).unless({
|
||||||
|
path: [...config.apiWhiteList, /^\/open\//],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
app.use((req, res, next) => {
|
|
||||||
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
app.use((req: Request, res, next) => {
|
||||||
|
if (!req.headers) {
|
||||||
|
req.platform = 'desktop';
|
||||||
|
} else {
|
||||||
|
const platform = getPlatform(req.headers['user-agent'] || '');
|
||||||
|
req.platform = platform;
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(async (req, res, next) => {
|
||||||
const headerToken = getToken(req);
|
const headerToken = getToken(req);
|
||||||
if (data) {
|
if (req.path.startsWith('/open/')) {
|
||||||
const { token } = JSON.parse(data);
|
const openService = Container.get(OpenService);
|
||||||
if (token && headerToken === token) {
|
const doc = await openService.findTokenByValue(headerToken);
|
||||||
return next();
|
if (doc && doc.tokens && doc.tokens.length > 0) {
|
||||||
|
const currentToken = doc.tokens.find((x) => x.value === headerToken);
|
||||||
|
const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/);
|
||||||
|
const key = keyMatch && keyMatch[1];
|
||||||
|
if (
|
||||||
|
doc.scopes.includes(key as any) &&
|
||||||
|
currentToken &&
|
||||||
|
currentToken.expiration >= Math.round(Date.now() / 1000)
|
||||||
|
) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!headerToken && req.path && req.path === '/api/login') {
|
|
||||||
|
if (
|
||||||
|
!headerToken &&
|
||||||
|
req.path &&
|
||||||
|
config.apiWhiteList.includes(req.path) &&
|
||||||
|
req.path !== '/api/crons/status'
|
||||||
|
) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
const remoteAddress = req.socket.remoteAddress;
|
const remoteAddress = req.socket.remoteAddress;
|
||||||
@@ -37,10 +83,46 @@ export default ({ app }: { app: Application }) => {
|
|||||||
) {
|
) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||||
|
if (data) {
|
||||||
|
const { token = '', tokens = {} } = JSON.parse(data);
|
||||||
|
if (headerToken === token || tokens[req.platform] === headerToken) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const err: any = new Error('UnauthorizedError');
|
const err: any = new Error('UnauthorizedError');
|
||||||
err['status'] = 401;
|
err.status = 401;
|
||||||
next(err);
|
next(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.use(async (req, res, next) => {
|
||||||
|
if (!['/api/init/user', '/api/init/notification'].includes(req.path)) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const authInfo = await userService.getUserInfo();
|
||||||
|
const envDbContent = getFileContentByName(config.envDbFile);
|
||||||
|
|
||||||
|
let isInitialized = true;
|
||||||
|
if (
|
||||||
|
Object.keys(authInfo).length === 2 &&
|
||||||
|
authInfo.username === 'admin' &&
|
||||||
|
authInfo.password === 'admin' &&
|
||||||
|
envDbContent.length === 0
|
||||||
|
) {
|
||||||
|
isInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInitialized) {
|
||||||
|
return res.send({ code: 450, message: '未知错误' });
|
||||||
|
} else {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(rewrite('/open/*', '/api/$1'));
|
||||||
app.use(config.api.prefix, routes());
|
app.use(config.api.prefix, routes());
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
@@ -49,6 +131,8 @@ export default ({ app }: { app: Application }) => {
|
|||||||
next(err);
|
next(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.use(Sentry.Handlers.errorHandler());
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
(
|
(
|
||||||
err: Error & { status: number },
|
err: Error & { status: number },
|
||||||
@@ -73,6 +157,8 @@ export default ({ app }: { app: Application }) => {
|
|||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction,
|
next: NextFunction,
|
||||||
) => {
|
) => {
|
||||||
|
Sentry.captureException(err);
|
||||||
|
|
||||||
res.status(err.status || 500);
|
res.status(err.status || 500);
|
||||||
res.json({
|
res.json({
|
||||||
code: err.status || 500,
|
code: err.status || 500,
|
||||||
|
|||||||
+21
-38
@@ -1,49 +1,18 @@
|
|||||||
|
import DependenceService from '../services/dependence';
|
||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { Crontab, CrontabStatus } from '../data/cron';
|
import { Crontab, CrontabStatus } from '../data/cron';
|
||||||
import CronService from '../services/cron';
|
import CronService from '../services/cron';
|
||||||
import EnvService from '../services/env';
|
import EnvService from '../services/env';
|
||||||
|
import _ from 'lodash';
|
||||||
const initData = [
|
import { dbs } from '../loaders/db';
|
||||||
{
|
|
||||||
name: '更新面板',
|
|
||||||
command: `ql update`,
|
|
||||||
schedule: `${randomSchedule(60, 1)} ${randomSchedule(
|
|
||||||
6,
|
|
||||||
1,
|
|
||||||
).toString()} * * *`,
|
|
||||||
isDisabled: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '删除日志',
|
|
||||||
command: 'ql rmlog 7',
|
|
||||||
schedule: '30 7 */7 * *',
|
|
||||||
isDisabled: 1,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
const cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const envService = Container.get(EnvService);
|
const envService = Container.get(EnvService);
|
||||||
const cronDb = cronService.getDb();
|
const dependenceService = Container.get(DependenceService);
|
||||||
|
const cronDb = dbs.cronDb;
|
||||||
cronDb.count({}, async (err, count) => {
|
const dependenceDb = dbs.dependenceDb;
|
||||||
if (count === 0) {
|
|
||||||
const data = initData.map((x: any) => {
|
|
||||||
const tab = new Crontab(x);
|
|
||||||
tab.created = new Date().valueOf();
|
|
||||||
tab.saved = false;
|
|
||||||
if (tab.name === '更新面板') {
|
|
||||||
tab.isSystem = 1;
|
|
||||||
} else {
|
|
||||||
tab.isSystem = 0;
|
|
||||||
}
|
|
||||||
return tab;
|
|
||||||
});
|
|
||||||
cronDb.insert(data);
|
|
||||||
await cronService.autosave_crontab();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化更新所有任务状态为空闲
|
// 初始化更新所有任务状态为空闲
|
||||||
cronDb.update(
|
cronDb.update(
|
||||||
@@ -52,6 +21,20 @@ export default async () => {
|
|||||||
{ multi: true },
|
{ multi: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
|
||||||
|
dependenceDb.find({ status: { $in: [0, 1, 2] } }).exec(async (err, 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);
|
||||||
|
for (const dep of depIds) {
|
||||||
|
await dependenceService.reInstall([dep]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 初始化时执行一次所有的ql repo 任务
|
// 初始化时执行一次所有的ql repo 任务
|
||||||
cronDb
|
cronDb
|
||||||
.find({
|
.find({
|
||||||
@@ -92,7 +75,7 @@ export default async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function randomSchedule(from: number, to: number) {
|
function randomSchedule(from: number, to: number) {
|
||||||
const result = [];
|
const result: any[] = [];
|
||||||
const arr = [...Array(from).keys()];
|
const arr = [...Array(from).keys()];
|
||||||
let count = arr.length;
|
let count = arr.length;
|
||||||
for (let i = 0; i < to; i++) {
|
for (let i = 0; i < to; i++) {
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import Logger from './logger';
|
||||||
|
import { fileExist } from '../config/util';
|
||||||
|
|
||||||
|
const rootPath = process.cwd();
|
||||||
|
const confFile = path.join(rootPath, 'config/config.sh');
|
||||||
|
const sampleConfigFile = path.join(rootPath, 'sample/config.sample.sh');
|
||||||
|
const sampleAuthFile = path.join(rootPath, 'sample/auth.sample.json');
|
||||||
|
const authConfigFile = path.join(rootPath, 'config/auth.json');
|
||||||
|
const configPath = path.join(rootPath, 'config/');
|
||||||
|
const scriptPath = path.join(rootPath, 'scripts/');
|
||||||
|
const logPath = path.join(rootPath, 'log/');
|
||||||
|
|
||||||
|
export default async () => {
|
||||||
|
const authFileExist = await fileExist(authConfigFile);
|
||||||
|
const confFileExist = await fileExist(confFile);
|
||||||
|
const scriptDirExist = await fileExist(scriptPath);
|
||||||
|
const logDirExist = await fileExist(logPath);
|
||||||
|
const configDirExist = await fileExist(configPath);
|
||||||
|
|
||||||
|
if (!configDirExist) {
|
||||||
|
fs.mkdirSync(configPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authFileExist) {
|
||||||
|
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confFileExist) {
|
||||||
|
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!scriptDirExist) {
|
||||||
|
fs.mkdirSync(scriptPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!logDirExist) {
|
||||||
|
fs.mkdirSync(logPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
dotenv.config({ path: confFile });
|
||||||
|
|
||||||
|
Logger.info('✌️ Init file down');
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Application } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import * as Tracing from '@sentry/tracing';
|
||||||
|
import Logger from './logger';
|
||||||
|
|
||||||
|
export default ({ expressApp }: { expressApp: Application }) => {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: 'https://e14681bce55f4849b11024a7d424b711@o1051273.ingest.sentry.io/6047906',
|
||||||
|
integrations: [
|
||||||
|
new Sentry.Integrations.Http({ tracing: true }),
|
||||||
|
new Tracing.Integrations.Express({ app: expressApp }),
|
||||||
|
],
|
||||||
|
|
||||||
|
tracesSampleRate: 1.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expressApp.use(Sentry.Handlers.requestHandler());
|
||||||
|
expressApp.use(Sentry.Handlers.tracingHandler());
|
||||||
|
|
||||||
|
Logger.info('✌️ Sentry loaded');
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Server } from 'http';
|
||||||
|
import Logger from './logger';
|
||||||
|
import Sock from './sock';
|
||||||
|
|
||||||
|
export default async ({ server }: { server: Server }) => {
|
||||||
|
await Sock({ server });
|
||||||
|
Logger.info('✌️ Sock loaded');
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
Logger.info('✌️ Server need close');
|
||||||
|
server.close(() => {
|
||||||
|
Logger.info('✌️ Server closed');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import sockJs from 'sockjs';
|
||||||
|
import { Server } from 'http';
|
||||||
|
import Logger from './logger';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
import SockService from '../services/sock';
|
||||||
|
import config from '../config/index';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { getPlatform } from '../config/util';
|
||||||
|
|
||||||
|
export default async ({ server }: { server: Server }) => {
|
||||||
|
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
|
||||||
|
const sockService = Container.get(SockService);
|
||||||
|
|
||||||
|
echo.on('connection', (conn) => {
|
||||||
|
if (!conn.headers || !conn.url || !conn.pathname) {
|
||||||
|
conn.close('404');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||||
|
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
|
||||||
|
const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
|
||||||
|
if (data) {
|
||||||
|
const { token = '', tokens = {} } = JSON.parse(data);
|
||||||
|
if (headerToken === token || tokens[platform] === headerToken) {
|
||||||
|
conn.write(JSON.stringify({ type: 'ping', message: 'hanhh' }));
|
||||||
|
sockService.addClient(conn);
|
||||||
|
|
||||||
|
conn.on('data', (message) => {
|
||||||
|
conn.write(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
conn.on('close', function () {
|
||||||
|
sockService.removeClient(conn);
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
conn.write(JSON.stringify({ type: 'ping', message: 'whyour' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.close('404');
|
||||||
|
});
|
||||||
|
|
||||||
|
echo.installHandlers(server);
|
||||||
|
};
|
||||||
+8
-7
@@ -2,16 +2,14 @@ import schedule from 'node-schedule';
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import Logger from './loaders/logger';
|
import Logger from './loaders/logger';
|
||||||
import { Container } from 'typedi';
|
|
||||||
import CronService from './services/cron';
|
|
||||||
import { CrontabStatus } from './data/cron';
|
import { CrontabStatus } from './data/cron';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
|
import { dbs } from './loaders/db';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
const cronService = Container.get(CronService);
|
const cronDb = dbs.cronDb;
|
||||||
const cronDb = cronService.getDb();
|
|
||||||
|
|
||||||
cronDb
|
cronDb
|
||||||
.find({})
|
.find({})
|
||||||
@@ -25,7 +23,7 @@ const run = async () => {
|
|||||||
if (docs && docs.length > 0) {
|
if (docs && docs.length > 0) {
|
||||||
for (let i = 0; i < docs.length; i++) {
|
for (let i = 0; i < docs.length; i++) {
|
||||||
const task = docs[i];
|
const task = docs[i];
|
||||||
const _schedule = task.schedule && task.schedule.split(' ');
|
const _schedule = task.schedule && task.schedule.split(/ +/);
|
||||||
if (
|
if (
|
||||||
_schedule &&
|
_schedule &&
|
||||||
_schedule.length > 5 &&
|
_schedule.length > 5 &&
|
||||||
@@ -46,8 +44,11 @@ const run = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
app
|
app
|
||||||
.listen(config.cronPort, () => {
|
.listen(config.cronPort, async () => {
|
||||||
run();
|
await require('./loaders/sentry').default({ expressApp: app });
|
||||||
|
await require('./loaders/db').default();
|
||||||
|
|
||||||
|
await run();
|
||||||
Logger.info(`
|
Logger.info(`
|
||||||
################################################
|
################################################
|
||||||
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
|
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
|
||||||
|
|||||||
+108
-37
@@ -10,28 +10,21 @@ import { getFileContentByName } from '../config/util';
|
|||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
import { promises, existsSync } from 'fs';
|
import { promises, existsSync } from 'fs';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
import { dbs } from '../loaders/db';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class CronService {
|
export default class CronService {
|
||||||
private cronDb = new DataStore({ filename: config.cronDbFile });
|
private cronDb = dbs.cronDb;
|
||||||
|
|
||||||
private queue = new PQueue({
|
private queue = new PQueue({
|
||||||
concurrency: parseInt(process.env.MaxConcurrentNum) || 5,
|
concurrency: parseInt(process.env.MaxConcurrentNum as string) || 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
this.cronDb.loadDatabase((err) => {
|
|
||||||
if (err) throw err;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public getDb(): DataStore {
|
|
||||||
return this.cronDb;
|
|
||||||
}
|
|
||||||
|
|
||||||
private isSixCron(cron: Crontab) {
|
private isSixCron(cron: Crontab) {
|
||||||
const { schedule } = cron;
|
const { schedule } = cron;
|
||||||
if (schedule.split(' ').length === 6) {
|
if (schedule.split(/ +/).length === 6) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -90,16 +83,38 @@ export default class CronService {
|
|||||||
status,
|
status,
|
||||||
pid,
|
pid,
|
||||||
log_path,
|
log_path,
|
||||||
|
last_running_time = 0,
|
||||||
|
last_execution_time = 0,
|
||||||
}: {
|
}: {
|
||||||
ids: string[];
|
ids: string[];
|
||||||
status: CrontabStatus;
|
status: CrontabStatus;
|
||||||
pid: number;
|
pid: number;
|
||||||
log_path: string;
|
log_path: string;
|
||||||
|
last_running_time: number;
|
||||||
|
last_execution_time: number;
|
||||||
}) {
|
}) {
|
||||||
this.cronDb.update(
|
const options: any = {
|
||||||
{ _id: { $in: ids } },
|
status,
|
||||||
{ $set: { status, pid, log_path } },
|
pid,
|
||||||
);
|
log_path,
|
||||||
|
last_execution_time,
|
||||||
|
};
|
||||||
|
if (last_running_time > 0) {
|
||||||
|
options.last_running_time = last_running_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.cronDb.update(
|
||||||
|
{ _id: { $in: ids } },
|
||||||
|
{
|
||||||
|
$set: options,
|
||||||
|
},
|
||||||
|
{ multi: true, returnUpdatedDocs: true },
|
||||||
|
(err) => {
|
||||||
|
resolve(null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async remove(ids: string[]) {
|
public async remove(ids: string[]) {
|
||||||
@@ -115,10 +130,38 @@ export default class CronService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async pin(ids: string[]) {
|
||||||
|
return new Promise((resolve: any) => {
|
||||||
|
this.cronDb.update(
|
||||||
|
{ _id: { $in: ids } },
|
||||||
|
{ $set: { isPinned: 1 } },
|
||||||
|
{ multi: true },
|
||||||
|
async (err) => {
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async unPin(ids: string[]) {
|
||||||
|
return new Promise((resolve: any) => {
|
||||||
|
this.cronDb.update(
|
||||||
|
{ _id: { $in: ids } },
|
||||||
|
{ $set: { isPinned: 0 } },
|
||||||
|
{ multi: true },
|
||||||
|
async (err) => {
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async crontabs(searchText?: string): Promise<Crontab[]> {
|
public async crontabs(searchText?: string): Promise<Crontab[]> {
|
||||||
let query = {};
|
let query = {};
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
const reg = new RegExp(searchText, 'i');
|
const encodeText = encodeURIComponent(searchText);
|
||||||
|
const reg = new RegExp(`${searchText}|${encodeText}`, 'i');
|
||||||
|
|
||||||
query = {
|
query = {
|
||||||
$or: [
|
$or: [
|
||||||
{
|
{
|
||||||
@@ -181,7 +224,9 @@ export default class CronService {
|
|||||||
const str = err ? `\n${err}` : '';
|
const str = err ? `\n${err}` : '';
|
||||||
fs.appendFileSync(
|
fs.appendFileSync(
|
||||||
`${doc.log_path}`,
|
`${doc.log_path}`,
|
||||||
`${str}\n## 执行结束... ${new Date().toLocaleString()} `,
|
`${str}\n## 执行结束... ${new Date()
|
||||||
|
.toLocaleString('zh', { hour12: false })
|
||||||
|
.replace(' 24:', ' 00:')} `,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,25 +242,29 @@ export default class CronService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async killTask(name: string) {
|
private async killTask(name: string) {
|
||||||
let taskCommond = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
|
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
try {
|
try {
|
||||||
let pid = (await execAsync(taskCommond)).stdout;
|
let pid = (await execAsync(taskCommand)).stdout;
|
||||||
if (pid) {
|
if (pid) {
|
||||||
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
|
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pids = pid.match(/\d+/g);
|
let pids = pid.match(/\(\d+/g);
|
||||||
const killLogs = [];
|
const killLogs = [];
|
||||||
for (const id of pids) {
|
if (pids && pids.length > 0) {
|
||||||
const c = `kill -9 ${id}`;
|
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
|
||||||
const { stdout, stderr } = await execAsync(c);
|
pids = pids.slice(0, 3);
|
||||||
if (stderr) {
|
for (const id of pids) {
|
||||||
killLogs.push(stderr);
|
const c = `kill -9 ${id.slice(1)}`;
|
||||||
}
|
const { stdout, stderr } = await execAsync(c);
|
||||||
if (stdout) {
|
if (stderr) {
|
||||||
killLogs.push(stdout);
|
killLogs.push(stderr);
|
||||||
|
}
|
||||||
|
if (stdout) {
|
||||||
|
killLogs.push(stdout);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
|
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
|
||||||
@@ -232,7 +281,7 @@ export default class CronService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { _id, command } = cron;
|
let { _id, command, log_path } = cron;
|
||||||
|
|
||||||
this.logger.silly('Running job');
|
this.logger.silly('Running job');
|
||||||
this.logger.silly('ID: ' + _id);
|
this.logger.silly('ID: ' + _id);
|
||||||
@@ -246,12 +295,34 @@ export default class CronService {
|
|||||||
cmdStr = `${cmdStr} now`;
|
cmdStr = `${cmdStr} now`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cp = spawn(cmdStr, { shell: '/bin/bash', detached: true });
|
const cp = spawn(cmdStr, { shell: '/bin/bash' });
|
||||||
this.cronDb.update(
|
this.cronDb.update(
|
||||||
{ _id },
|
{ _id },
|
||||||
{ $set: { status: CrontabStatus.running, pid: cp.pid } },
|
{ $set: { status: CrontabStatus.running, pid: cp.pid } },
|
||||||
);
|
);
|
||||||
|
cp.stderr.on('data', (data) => {
|
||||||
|
if (log_path) {
|
||||||
|
fs.appendFileSync(`${log_path}`, `${data}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cp.on('error', (err) => {
|
||||||
|
if (log_path) {
|
||||||
|
fs.appendFileSync(`${log_path}`, `${JSON.stringify(err)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('exit', (code, signal) => {
|
||||||
|
this.logger.info(
|
||||||
|
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
|
||||||
|
);
|
||||||
|
this.cronDb.update(
|
||||||
|
{ _id },
|
||||||
|
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
|
||||||
|
);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
cp.on('close', (code) => {
|
cp.on('close', (code) => {
|
||||||
|
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
|
||||||
this.cronDb.update(
|
this.cronDb.update(
|
||||||
{ _id },
|
{ _id },
|
||||||
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
|
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
|
||||||
@@ -291,10 +362,14 @@ export default class CronService {
|
|||||||
|
|
||||||
public async log(_id: string) {
|
public async log(_id: string) {
|
||||||
const doc = await this.get(_id);
|
const doc = await this.get(_id);
|
||||||
|
if (!doc) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
if (doc.log_path) {
|
if (doc.log_path) {
|
||||||
return getFileContentByName(`${doc.log_path}`);
|
return getFileContentByName(`${doc.log_path}`);
|
||||||
}
|
}
|
||||||
const [, commandStr, url] = doc.command.split(' ');
|
const [, commandStr, url] = doc.command.split(/ +/);
|
||||||
let logPath = this.getKey(commandStr);
|
let logPath = this.getKey(commandStr);
|
||||||
const isQlCommand = doc.command.startsWith('ql ');
|
const isQlCommand = doc.command.startsWith('ql ');
|
||||||
const key =
|
const key =
|
||||||
@@ -334,7 +409,7 @@ export default class CronService {
|
|||||||
const tabs = await this.crontabs();
|
const tabs = await this.crontabs();
|
||||||
var crontab_string = '';
|
var crontab_string = '';
|
||||||
tabs.forEach((tab) => {
|
tabs.forEach((tab) => {
|
||||||
const _schedule = tab.schedule && tab.schedule.split(' ');
|
const _schedule = tab.schedule && tab.schedule.split(/ +/);
|
||||||
if (tab.isDisabled === 1 || _schedule.length !== 5) {
|
if (tab.isDisabled === 1 || _schedule.length !== 5) {
|
||||||
crontab_string += '# ';
|
crontab_string += '# ';
|
||||||
crontab_string += tab.schedule;
|
crontab_string += tab.schedule;
|
||||||
@@ -359,10 +434,6 @@ export default class CronService {
|
|||||||
this.cronDb.update({}, { $set: { saved: true } }, { multi: true });
|
this.cronDb.update({}, { $set: { saved: true } }, { multi: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
private reload_db() {
|
|
||||||
this.cronDb.loadDatabase();
|
|
||||||
}
|
|
||||||
|
|
||||||
public import_crontab() {
|
public import_crontab() {
|
||||||
exec('crontab -l', (error, stdout, stderr) => {
|
exec('crontab -l', (error, stdout, stderr) => {
|
||||||
var lines = stdout.split('\n');
|
var lines = stdout.split('\n');
|
||||||
|
|||||||
@@ -0,0 +1,286 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import config from '../config';
|
||||||
|
import DataStore from 'nedb';
|
||||||
|
import {
|
||||||
|
Dependence,
|
||||||
|
InstallDependenceCommandTypes,
|
||||||
|
DependenceStatus,
|
||||||
|
DependenceTypes,
|
||||||
|
unInstallDependenceCommandTypes,
|
||||||
|
} from '../data/dependence';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import SockService from './sock';
|
||||||
|
import { dbs } from '../loaders/db';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class DependenceService {
|
||||||
|
private dependenceDb = dbs.dependenceDb;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject('logger') private logger: winston.Logger,
|
||||||
|
private sockService: SockService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async create(payloads: Dependence[]): Promise<Dependence[]> {
|
||||||
|
const tabs = payloads.map((x) => {
|
||||||
|
const tab = new Dependence({ ...x, status: DependenceStatus.installing });
|
||||||
|
return tab;
|
||||||
|
});
|
||||||
|
const docs = await this.insert(tabs);
|
||||||
|
this.installOrUninstallDependencies(docs);
|
||||||
|
return docs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async insert(payloads: Dependence[]): Promise<Dependence[]> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.dependenceDb.insert(payloads, (err, docs) => {
|
||||||
|
if (err) {
|
||||||
|
this.logger.error(err);
|
||||||
|
} else {
|
||||||
|
resolve(docs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update(
|
||||||
|
payload: Dependence & { _id: string },
|
||||||
|
): Promise<Dependence> {
|
||||||
|
const { _id, ...other } = payload;
|
||||||
|
const doc = await this.get(_id);
|
||||||
|
const tab = new Dependence({
|
||||||
|
...doc,
|
||||||
|
...other,
|
||||||
|
status: DependenceStatus.installing,
|
||||||
|
});
|
||||||
|
const newDoc = await this.updateDb(tab);
|
||||||
|
this.installOrUninstallDependencies([newDoc]);
|
||||||
|
return newDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateDb(payload: Dependence): Promise<Dependence> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.dependenceDb.update(
|
||||||
|
{ _id: payload._id },
|
||||||
|
payload,
|
||||||
|
{ returnUpdatedDocs: true },
|
||||||
|
(err, num, doc) => {
|
||||||
|
if (err) {
|
||||||
|
this.logger.error(err);
|
||||||
|
} else {
|
||||||
|
resolve(doc as Dependence);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async remove(ids: string[]) {
|
||||||
|
return new Promise((resolve: any) => {
|
||||||
|
this.dependenceDb.update(
|
||||||
|
{ _id: { $in: ids } },
|
||||||
|
{ $set: { status: DependenceStatus.removing, log: [] } },
|
||||||
|
{ multi: true, returnUpdatedDocs: true },
|
||||||
|
async (err, num, docs: Dependence[]) => {
|
||||||
|
this.installOrUninstallDependencies(docs, false);
|
||||||
|
resolve(docs);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeDb(ids: string[]) {
|
||||||
|
return new Promise((resolve: any) => {
|
||||||
|
this.dependenceDb.remove(
|
||||||
|
{ _id: { $in: ids } },
|
||||||
|
{ multi: true },
|
||||||
|
async (err) => {
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async dependencies(
|
||||||
|
{ searchValue, type }: { searchValue: string; type: string },
|
||||||
|
sort: any = { position: -1 },
|
||||||
|
query: any = {},
|
||||||
|
): Promise<Dependence[]> {
|
||||||
|
let condition = { ...query, type: DependenceTypes[type as any] };
|
||||||
|
if (searchValue) {
|
||||||
|
const reg = new RegExp(searchValue);
|
||||||
|
condition = {
|
||||||
|
...condition,
|
||||||
|
$or: [
|
||||||
|
{
|
||||||
|
name: reg,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const newDocs = await this.find(condition, sort);
|
||||||
|
return newDocs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async reInstall(ids: string[]): Promise<Dependence[]> {
|
||||||
|
return new Promise((resolve: any) => {
|
||||||
|
this.dependenceDb.update(
|
||||||
|
{ _id: { $in: ids } },
|
||||||
|
{ $set: { status: DependenceStatus.installing, log: [] } },
|
||||||
|
{ multi: true, returnUpdatedDocs: true },
|
||||||
|
async (err, num, docs: Dependence[]) => {
|
||||||
|
await this.installOrUninstallDependencies(docs);
|
||||||
|
resolve(docs);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async find(query: any, sort: any): Promise<Dependence[]> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.dependenceDb
|
||||||
|
.find(query)
|
||||||
|
.sort({ ...sort })
|
||||||
|
.exec((err, docs) => {
|
||||||
|
resolve(docs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get(_id: string): Promise<Dependence> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.dependenceDb.find({ _id }).exec((err, docs) => {
|
||||||
|
resolve(docs[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateLog(ids: string[], log: string): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.dependenceDb.update(
|
||||||
|
{ _id: { $in: ids } },
|
||||||
|
{ $push: { log } },
|
||||||
|
{ multi: true },
|
||||||
|
(err, num, doc) => {
|
||||||
|
if (err) {
|
||||||
|
this.logger.error(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public installOrUninstallDependencies(
|
||||||
|
dependencies: Dependence[],
|
||||||
|
isInstall: boolean = true,
|
||||||
|
) {
|
||||||
|
return new Promise((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 string[];
|
||||||
|
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
|
||||||
|
const startTime = Date.now();
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: 'installDependence',
|
||||||
|
message: `开始${actionText}依赖 ${depNames},开始时间 ${new Date(
|
||||||
|
startTime,
|
||||||
|
).toLocaleString()}`,
|
||||||
|
references: depIds,
|
||||||
|
});
|
||||||
|
this.updateLog(
|
||||||
|
depIds,
|
||||||
|
`开始${actionText}依赖 ${depNames},开始时间 ${new Date(
|
||||||
|
startTime,
|
||||||
|
).toLocaleString()}\n`,
|
||||||
|
);
|
||||||
|
cp.stdout.on('data', (data) => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: 'installDependence',
|
||||||
|
message: data.toString(),
|
||||||
|
references: depIds,
|
||||||
|
});
|
||||||
|
this.updateLog(depIds, data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.stderr.on('data', (data) => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: 'installDependence',
|
||||||
|
message: data.toString(),
|
||||||
|
references: depIds,
|
||||||
|
});
|
||||||
|
this.updateLog(depIds, data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('error', (err) => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: 'installDependence',
|
||||||
|
message: JSON.stringify(err),
|
||||||
|
references: depIds,
|
||||||
|
});
|
||||||
|
this.updateLog(depIds, JSON.stringify(err));
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('close', (code) => {
|
||||||
|
const endTime = Date.now();
|
||||||
|
const isSucceed = code === 0;
|
||||||
|
const resultText = isSucceed ? '成功' : '失败';
|
||||||
|
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: 'installDependence',
|
||||||
|
message: `依赖${actionText}${resultText},结束时间 ${new Date(
|
||||||
|
endTime,
|
||||||
|
).toLocaleString()},耗时 ${(endTime - startTime) / 1000} 秒`,
|
||||||
|
references: depIds,
|
||||||
|
});
|
||||||
|
this.updateLog(
|
||||||
|
depIds,
|
||||||
|
`依赖${actionText}${resultText},结束时间 ${new Date(
|
||||||
|
endTime,
|
||||||
|
).toLocaleString()},耗时 ${(endTime - startTime) / 1000} 秒`,
|
||||||
|
);
|
||||||
|
|
||||||
|
let status = null;
|
||||||
|
if (isSucceed) {
|
||||||
|
status = isInstall
|
||||||
|
? DependenceStatus.installed
|
||||||
|
: DependenceStatus.removed;
|
||||||
|
} else {
|
||||||
|
status = isInstall
|
||||||
|
? DependenceStatus.installFailed
|
||||||
|
: DependenceStatus.removeFailed;
|
||||||
|
}
|
||||||
|
this.dependenceDb.update(
|
||||||
|
{ _id: { $in: depIds } },
|
||||||
|
{
|
||||||
|
$set: { status },
|
||||||
|
$unset: { pid: true },
|
||||||
|
},
|
||||||
|
{ multi: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// 如果删除依赖成功,3秒后删除数据库记录
|
||||||
|
if (isSucceed && !isInstall) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.removeDb(depIds);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
-38
@@ -6,35 +6,37 @@ import * as fs from 'fs';
|
|||||||
import DataStore from 'nedb';
|
import DataStore from 'nedb';
|
||||||
import { Env, EnvStatus, initEnvPosition } from '../data/env';
|
import { Env, EnvStatus, initEnvPosition } from '../data/env';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { dbs } from '../loaders/db';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class EnvService {
|
export default class EnvService {
|
||||||
private cronDb = new DataStore({ filename: config.envDbFile });
|
private envDb = dbs.envDb;
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {
|
|
||||||
this.cronDb.loadDatabase((err) => {
|
|
||||||
if (err) throw err;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async create(payload: Env): Promise<Env> {
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
|
public async create(payloads: Env[]): Promise<Env[]> {
|
||||||
const envs = await this.envs();
|
const envs = await this.envs();
|
||||||
let position = initEnvPosition;
|
let position = initEnvPosition;
|
||||||
if (envs && envs.length > 0 && envs[envs.length - 1].position) {
|
if (envs && envs.length > 0 && envs[envs.length - 1].position) {
|
||||||
position = envs[envs.length - 1].position;
|
position = envs[envs.length - 1].position as number;
|
||||||
}
|
}
|
||||||
const tab = new Env({ ...payload, position: position / 2 });
|
const tabs = payloads.map((x) => {
|
||||||
const doc = await this.insert(tab);
|
position = position / 2;
|
||||||
|
const tab = new Env({ ...x, position });
|
||||||
|
return tab;
|
||||||
|
});
|
||||||
|
const docs = await this.insert(tabs);
|
||||||
await this.set_envs();
|
await this.set_envs();
|
||||||
return doc;
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async insert(payload: Env): Promise<Env> {
|
public async insert(payloads: Env[]): Promise<Env[]> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.cronDb.insert(payload, (err, doc) => {
|
this.envDb.insert(payloads, (err, docs) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
} else {
|
} else {
|
||||||
resolve(doc);
|
resolve(docs);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -51,7 +53,7 @@ export default class EnvService {
|
|||||||
|
|
||||||
private async updateDb(payload: Env): Promise<Env> {
|
private async updateDb(payload: Env): Promise<Env> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.cronDb.update(
|
this.envDb.update(
|
||||||
{ _id: payload._id },
|
{ _id: payload._id },
|
||||||
payload,
|
payload,
|
||||||
{ returnUpdatedDocs: true },
|
{ returnUpdatedDocs: true },
|
||||||
@@ -68,14 +70,10 @@ export default class EnvService {
|
|||||||
|
|
||||||
public async remove(ids: string[]) {
|
public async remove(ids: string[]) {
|
||||||
return new Promise((resolve: any) => {
|
return new Promise((resolve: any) => {
|
||||||
this.cronDb.remove(
|
this.envDb.remove({ _id: { $in: ids } }, { multi: true }, async (err) => {
|
||||||
{ _id: { $in: ids } },
|
await this.set_envs();
|
||||||
{ multi: true },
|
resolve();
|
||||||
async (err) => {
|
});
|
||||||
await this.set_envs();
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +86,7 @@ export default class EnvService {
|
|||||||
fromIndex: number;
|
fromIndex: number;
|
||||||
toIndex: number;
|
toIndex: number;
|
||||||
},
|
},
|
||||||
) {
|
): Promise<Env> {
|
||||||
let targetPosition: number;
|
let targetPosition: number;
|
||||||
const isUpward = fromIndex > toIndex;
|
const isUpward = fromIndex > toIndex;
|
||||||
const envs = await this.envs();
|
const envs = await this.envs();
|
||||||
@@ -101,11 +99,11 @@ export default class EnvService {
|
|||||||
? (envs[toIndex].position + envs[toIndex - 1].position) / 2
|
? (envs[toIndex].position + envs[toIndex - 1].position) / 2
|
||||||
: (envs[toIndex].position + envs[toIndex + 1].position) / 2;
|
: (envs[toIndex].position + envs[toIndex + 1].position) / 2;
|
||||||
}
|
}
|
||||||
this.update({
|
const newDoc = await this.update({
|
||||||
_id,
|
_id,
|
||||||
position: targetPosition,
|
position: targetPosition,
|
||||||
});
|
});
|
||||||
await this.set_envs();
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async envs(
|
public async envs(
|
||||||
@@ -115,7 +113,9 @@ export default class EnvService {
|
|||||||
): Promise<Env[]> {
|
): Promise<Env[]> {
|
||||||
let condition = { ...query };
|
let condition = { ...query };
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
const reg = new RegExp(searchText);
|
const encodeText = encodeURIComponent(searchText);
|
||||||
|
const reg = new RegExp(`${searchText}|${encodeText}`, 'i');
|
||||||
|
|
||||||
condition = {
|
condition = {
|
||||||
$or: [
|
$or: [
|
||||||
{
|
{
|
||||||
@@ -136,7 +136,7 @@ export default class EnvService {
|
|||||||
|
|
||||||
private async find(query: any, sort: any): Promise<Env[]> {
|
private async find(query: any, sort: any): Promise<Env[]> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.cronDb
|
this.envDb
|
||||||
.find(query)
|
.find(query)
|
||||||
.sort({ ...sort })
|
.sort({ ...sort })
|
||||||
.exec((err, docs) => {
|
.exec((err, docs) => {
|
||||||
@@ -147,7 +147,7 @@ export default class EnvService {
|
|||||||
|
|
||||||
public async get(_id: string): Promise<Env> {
|
public async get(_id: string): Promise<Env> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.cronDb.find({ _id }).exec((err, docs) => {
|
this.envDb.find({ _id }).exec((err, docs) => {
|
||||||
resolve(docs[0]);
|
resolve(docs[0]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -155,7 +155,7 @@ export default class EnvService {
|
|||||||
|
|
||||||
public async getBySort(sort: any): Promise<Env> {
|
public async getBySort(sort: any): Promise<Env> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.cronDb
|
this.envDb
|
||||||
.find({})
|
.find({})
|
||||||
.sort({ ...sort })
|
.sort({ ...sort })
|
||||||
.limit(1)
|
.limit(1)
|
||||||
@@ -167,7 +167,7 @@ export default class EnvService {
|
|||||||
|
|
||||||
public async disabled(ids: string[]) {
|
public async disabled(ids: string[]) {
|
||||||
return new Promise((resolve: any) => {
|
return new Promise((resolve: any) => {
|
||||||
this.cronDb.update(
|
this.envDb.update(
|
||||||
{ _id: { $in: ids } },
|
{ _id: { $in: ids } },
|
||||||
{ $set: { status: EnvStatus.disabled } },
|
{ $set: { status: EnvStatus.disabled } },
|
||||||
{ multi: true },
|
{ multi: true },
|
||||||
@@ -181,7 +181,7 @@ export default class EnvService {
|
|||||||
|
|
||||||
public async enabled(ids: string[]) {
|
public async enabled(ids: string[]) {
|
||||||
return new Promise((resolve: any) => {
|
return new Promise((resolve: any) => {
|
||||||
this.cronDb.update(
|
this.envDb.update(
|
||||||
{ _id: { $in: ids } },
|
{ _id: { $in: ids } },
|
||||||
{ $set: { status: EnvStatus.normal } },
|
{ $set: { status: EnvStatus.normal } },
|
||||||
{ multi: true },
|
{ multi: true },
|
||||||
@@ -195,7 +195,7 @@ export default class EnvService {
|
|||||||
|
|
||||||
public async updateNames({ ids, name }: { ids: string[]; name: string }) {
|
public async updateNames({ ids, name }: { ids: string[]; name: string }) {
|
||||||
return new Promise((resolve: any) => {
|
return new Promise((resolve: any) => {
|
||||||
this.cronDb.update(
|
this.envDb.update(
|
||||||
{ _id: { $in: ids } },
|
{ _id: { $in: ids } },
|
||||||
{ $set: { name } },
|
{ $set: { name } },
|
||||||
{ multi: true },
|
{ multi: true },
|
||||||
@@ -218,11 +218,21 @@ export default class EnvService {
|
|||||||
for (const key in groups) {
|
for (const key in groups) {
|
||||||
if (Object.prototype.hasOwnProperty.call(groups, key)) {
|
if (Object.prototype.hasOwnProperty.call(groups, key)) {
|
||||||
const group = groups[key];
|
const group = groups[key];
|
||||||
env_string += `export ${key}="${_(group)
|
|
||||||
.filter((x) => x.status !== EnvStatus.disabled)
|
// 忽略不符合bash要求的环境变量名称
|
||||||
.map('value')
|
if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) {
|
||||||
.join('&')
|
let value = _(group)
|
||||||
.replace(/ /g, '')}"\n`;
|
.filter((x) => x.status !== EnvStatus.disabled)
|
||||||
|
.map('value')
|
||||||
|
.join('&')
|
||||||
|
.replace(/ /g, '');
|
||||||
|
if (/"/.test(value)) {
|
||||||
|
value = `'${value}'`;
|
||||||
|
} else {
|
||||||
|
value = `"${value}"`;
|
||||||
|
}
|
||||||
|
env_string += `export ${key}=${value}\n`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fs.writeFileSync(config.envFile, env_string);
|
fs.writeFileSync(config.envFile, env_string);
|
||||||
|
|||||||
@@ -0,0 +1,354 @@
|
|||||||
|
import { NotificationInfo } from '../data/notify';
|
||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import UserService from './user';
|
||||||
|
import got from 'got';
|
||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class NotificationService {
|
||||||
|
@Inject((type) => UserService)
|
||||||
|
private userService!: UserService;
|
||||||
|
|
||||||
|
private modeMap = new Map([
|
||||||
|
['gotify', this.gotify],
|
||||||
|
['goCqHttpBot', this.goCqHttpBot],
|
||||||
|
['serverChan', this.serverChan],
|
||||||
|
['bark', this.bark],
|
||||||
|
['telegramBot', this.telegramBot],
|
||||||
|
['dingtalkBot', this.dingtalkBot],
|
||||||
|
['weWorkBot', this.weWorkBot],
|
||||||
|
['weWorkApp', this.weWorkApp],
|
||||||
|
['iGot', this.iGot],
|
||||||
|
['pushPlus', this.pushPlus],
|
||||||
|
['email', this.email],
|
||||||
|
]);
|
||||||
|
|
||||||
|
private timeout = 10000;
|
||||||
|
private title = '';
|
||||||
|
private content = '';
|
||||||
|
private params!: Omit<NotificationInfo, 'type'>;
|
||||||
|
|
||||||
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
|
public async notify(
|
||||||
|
title: string,
|
||||||
|
content: string,
|
||||||
|
): Promise<boolean | undefined> {
|
||||||
|
const { type, ...rest } = await this.userService.getNotificationMode();
|
||||||
|
if (type) {
|
||||||
|
this.title = title;
|
||||||
|
this.content = content;
|
||||||
|
this.params = rest;
|
||||||
|
const notificationModeAction = this.modeMap.get(type);
|
||||||
|
try {
|
||||||
|
return await notificationModeAction?.call(this);
|
||||||
|
} catch (error: any) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async testNotify(
|
||||||
|
info: NotificationInfo,
|
||||||
|
title: string,
|
||||||
|
content: string,
|
||||||
|
) {
|
||||||
|
const { type, ...rest } = info;
|
||||||
|
if (type) {
|
||||||
|
this.title = title;
|
||||||
|
this.content = content;
|
||||||
|
this.params = rest;
|
||||||
|
const notificationModeAction = this.modeMap.get(type);
|
||||||
|
return await notificationModeAction?.call(this);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async gotify() {
|
||||||
|
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
|
||||||
|
const res: any = await got
|
||||||
|
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
body: `title=${encodeURIComponent(
|
||||||
|
this.title,
|
||||||
|
)}&message=${encodeURIComponent(
|
||||||
|
this.content,
|
||||||
|
)}&priority=${gotifyPriority}`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
return typeof res.id === 'number';
|
||||||
|
}
|
||||||
|
|
||||||
|
private async goCqHttpBot() {
|
||||||
|
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
|
||||||
|
const res: any = await got
|
||||||
|
.post(
|
||||||
|
`${goCqHttpBotUrl}?access_token=${goCqHttpBotToken}&${goCqHttpBotQq}`,
|
||||||
|
{
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
json: { message: `${this.title}\n${this.content}` },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.json();
|
||||||
|
return res.retcode === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async serverChan() {
|
||||||
|
const { serverChanKey } = this.params;
|
||||||
|
const url = serverChanKey.startsWith('SCT')
|
||||||
|
? `https://sctapi.ftqq.com/${serverChanKey}.send`
|
||||||
|
: `https://sc.ftqq.com/${serverChanKey}.send`;
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
body: `title=${this.title}&desp=${this.content}`,
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
return res.errno === 0 || res.data.errno === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async bark() {
|
||||||
|
let { barkPush, barkSound, barkGroup } = this.params;
|
||||||
|
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
|
||||||
|
barkPush = `https://api.day.app/${barkPush}`;
|
||||||
|
}
|
||||||
|
const url = `${barkPush}/${encodeURIComponent(
|
||||||
|
this.title,
|
||||||
|
)}/${encodeURIComponent(
|
||||||
|
this.content,
|
||||||
|
)}?sound=${barkSound}&group=${barkGroup}`;
|
||||||
|
const res: any = await got
|
||||||
|
.get(url, {
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
return res.code === 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async telegramBot() {
|
||||||
|
const {
|
||||||
|
telegramBotApiHost,
|
||||||
|
telegramBotProxyAuth,
|
||||||
|
telegramBotProxyHost,
|
||||||
|
telegramBotProxyPort,
|
||||||
|
telegramBotToken,
|
||||||
|
telegramBotUserId,
|
||||||
|
} = this.params;
|
||||||
|
const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : '';
|
||||||
|
const url = `https://${
|
||||||
|
telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org'
|
||||||
|
}/bot${telegramBotToken}/sendMessage`;
|
||||||
|
let agent;
|
||||||
|
if (telegramBotProxyHost && telegramBotProxyPort) {
|
||||||
|
const options: any = {
|
||||||
|
keepAlive: true,
|
||||||
|
keepAliveMsecs: 1000,
|
||||||
|
maxSockets: 256,
|
||||||
|
maxFreeSockets: 256,
|
||||||
|
proxy: `http://${authStr}${telegramBotProxyHost}:${telegramBotProxyPort}`,
|
||||||
|
};
|
||||||
|
const httpAgent = new HttpProxyAgent(options);
|
||||||
|
const httpsAgent = new HttpsProxyAgent(options);
|
||||||
|
agent = {
|
||||||
|
http: httpAgent,
|
||||||
|
https: httpsAgent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
agent,
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
return !!res.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async dingtalkBot() {
|
||||||
|
const { dingtalkBotSecret, dingtalkBotToken } = this.params;
|
||||||
|
let secretParam = '';
|
||||||
|
if (dingtalkBotSecret) {
|
||||||
|
const dateNow = Date.now();
|
||||||
|
const hmac = crypto.createHmac('sha256', dingtalkBotSecret);
|
||||||
|
hmac.update(`${dateNow}\n${dingtalkBotSecret}`);
|
||||||
|
const result = encodeURIComponent(hmac.digest('base64'));
|
||||||
|
secretParam = `×tamp=${dateNow}&sign=${result}`;
|
||||||
|
}
|
||||||
|
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
json: {
|
||||||
|
msgtype: 'text',
|
||||||
|
text: {
|
||||||
|
content: ` ${this.title}\n\n${this.content}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
return res.errcode === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async weWorkBot() {
|
||||||
|
const { weWorkBotKey } = this.params;
|
||||||
|
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`;
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
json: {
|
||||||
|
msgtype: 'text',
|
||||||
|
text: {
|
||||||
|
content: ` ${this.title}\n\n${this.content}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
return res.errcode === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async weWorkApp() {
|
||||||
|
const { weWorkAppKey } = this.params;
|
||||||
|
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
|
||||||
|
weWorkAppKey.split(',');
|
||||||
|
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
|
||||||
|
const { access_token } = await got
|
||||||
|
.post(url, {
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
json: {
|
||||||
|
corpid,
|
||||||
|
corpsecret,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
|
||||||
|
let options: any = {
|
||||||
|
msgtype: 'mpnews',
|
||||||
|
mpnews: {
|
||||||
|
articles: [
|
||||||
|
{
|
||||||
|
title: `${this.title}`,
|
||||||
|
thumb_media_id,
|
||||||
|
author: `智能助手`,
|
||||||
|
content_source_url: ``,
|
||||||
|
content: `${this.content.replace(/\n/g, '<br/>')}`,
|
||||||
|
digest: `${this.content}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
switch (thumb_media_id) {
|
||||||
|
case '0':
|
||||||
|
options = {
|
||||||
|
msgtype: 'textcard',
|
||||||
|
textcard: {
|
||||||
|
title: `${this.title}`,
|
||||||
|
description: `${this.content}`,
|
||||||
|
url: 'https://github.com/whyour/qinglong',
|
||||||
|
btntxt: '更多',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '1':
|
||||||
|
options = {
|
||||||
|
msgtype: 'text',
|
||||||
|
text: {
|
||||||
|
content: `${this.title}\n\n${this.content}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: any = await got
|
||||||
|
.post(
|
||||||
|
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${access_token}`,
|
||||||
|
{
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
json: {
|
||||||
|
touser,
|
||||||
|
agentid,
|
||||||
|
safe: '0',
|
||||||
|
...options,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.json();
|
||||||
|
|
||||||
|
return res.errcode === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async iGot() {
|
||||||
|
const { iGotPushKey } = this.params;
|
||||||
|
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
body: `title=${this.title}&content=${this.content}`,
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
|
||||||
|
return res.ret === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async pushPlus() {
|
||||||
|
const { pushPlusToken, pushPlusUser } = this.params;
|
||||||
|
const url = `https://www.pushplus.plus/send`;
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, {
|
||||||
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
|
json: {
|
||||||
|
token: `${pushPlusToken}`,
|
||||||
|
title: `${this.title}`,
|
||||||
|
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
|
||||||
|
topic: `${pushPlusUser || ''}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
|
||||||
|
return res.code === 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async email() {
|
||||||
|
const { emailPass, emailService, emailUser } = this.params;
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
service: emailService,
|
||||||
|
auth: {
|
||||||
|
user: emailUser,
|
||||||
|
pass: emailPass,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const info = await transporter.sendMail({
|
||||||
|
from: `"青龙快讯" <${emailUser}>`,
|
||||||
|
to: `${emailUser}`,
|
||||||
|
subject: `${this.title}`,
|
||||||
|
html: `${this.content.replace(/\n/g, '<br/>')}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
transporter.close();
|
||||||
|
|
||||||
|
return !!info.messageId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import { createRandomString } from '../config/util';
|
||||||
|
import config from '../config';
|
||||||
|
import DataStore from 'nedb';
|
||||||
|
import { App } from '../data/open';
|
||||||
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
|
import { dbs } from '../loaders/db';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class OpenService {
|
||||||
|
private appDb = dbs.appDb;
|
||||||
|
|
||||||
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
|
public async findTokenByValue(token: string): Promise<App> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.appDb.find(
|
||||||
|
{ tokens: { $elemMatch: { value: token } } },
|
||||||
|
(err, docs) => {
|
||||||
|
if (err) {
|
||||||
|
this.logger.error(err);
|
||||||
|
} else {
|
||||||
|
resolve(docs[0]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async create(payload: App): Promise<App> {
|
||||||
|
const tab = new App({ ...payload });
|
||||||
|
tab.client_id = createRandomString(12, 12);
|
||||||
|
tab.client_secret = createRandomString(24, 24);
|
||||||
|
const docs = await this.insert([tab]);
|
||||||
|
return { ...docs[0], tokens: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async insert(payloads: App[]): Promise<App[]> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.appDb.insert(payloads, (err, docs) => {
|
||||||
|
if (err) {
|
||||||
|
this.logger.error(err);
|
||||||
|
} else {
|
||||||
|
resolve(docs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update(payload: App): Promise<App> {
|
||||||
|
const { _id, client_id, client_secret, tokens, ...other } = payload;
|
||||||
|
const doc = await this.get(_id);
|
||||||
|
const tab = new App({ ...doc, ...other });
|
||||||
|
const newDoc = await this.updateDb(tab);
|
||||||
|
return { ...newDoc, tokens: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateDb(payload: App): Promise<App> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.appDb.update(
|
||||||
|
{ _id: payload._id },
|
||||||
|
payload,
|
||||||
|
{ returnUpdatedDocs: true },
|
||||||
|
(err, num, doc, up) => {
|
||||||
|
if (err) {
|
||||||
|
this.logger.error(err);
|
||||||
|
} else {
|
||||||
|
resolve(doc);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async remove(ids: string[]) {
|
||||||
|
return new Promise((resolve: any) => {
|
||||||
|
this.appDb.remove({ _id: { $in: ids } }, { multi: true }, async (err) => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async resetSecret(_id: string): Promise<App> {
|
||||||
|
const doc = await this.get(_id);
|
||||||
|
const tab = new App({ ...doc });
|
||||||
|
tab.client_secret = createRandomString(24, 24);
|
||||||
|
tab.tokens = [];
|
||||||
|
const newDoc = await this.updateDb(tab);
|
||||||
|
return newDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async list(
|
||||||
|
searchText: string = '',
|
||||||
|
sort: any = {},
|
||||||
|
query: any = {},
|
||||||
|
): Promise<App[]> {
|
||||||
|
let condition = { ...query };
|
||||||
|
if (searchText) {
|
||||||
|
const encodeText = encodeURIComponent(searchText);
|
||||||
|
const reg = new RegExp(`${searchText}|${encodeText}`, 'i');
|
||||||
|
|
||||||
|
condition = {
|
||||||
|
$or: [
|
||||||
|
{
|
||||||
|
value: reg,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: reg,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
remarks: reg,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const newDocs = await this.find(condition, sort);
|
||||||
|
return newDocs.map((x) => ({ ...x, tokens: [] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async find(query: any, sort: any): Promise<App[]> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.appDb
|
||||||
|
.find(query)
|
||||||
|
.sort({ ...sort })
|
||||||
|
.exec((err, docs) => {
|
||||||
|
resolve(docs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get(_id: string): Promise<App> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.appDb.find({ _id }).exec((err, docs) => {
|
||||||
|
resolve(docs[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async authToken({
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
}: {
|
||||||
|
client_id: string;
|
||||||
|
client_secret: string;
|
||||||
|
}): Promise<any> {
|
||||||
|
const token = uuidV4();
|
||||||
|
const expiration = Math.round(Date.now() / 1000) + 2592000; // 2592000 30天
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.appDb.find({ client_id, client_secret }).exec((err, docs) => {
|
||||||
|
if (docs && docs[0]) {
|
||||||
|
this.appDb.update(
|
||||||
|
{ client_id, client_secret },
|
||||||
|
{ $push: { tokens: { value: token, expiration } } },
|
||||||
|
{},
|
||||||
|
(err, num, doc) => {
|
||||||
|
resolve({
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
token,
|
||||||
|
token_type: 'Bearer',
|
||||||
|
expiration,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
resolve({ code: 400, message: 'client_id或client_seret有误' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import nodeSchedule from 'node-schedule';
|
||||||
|
import { Crontab } from '../data/cron';
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class ScheduleService {
|
||||||
|
private scheduleStacks = new Map<string, nodeSchedule.Job>();
|
||||||
|
|
||||||
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
|
async generateSchedule({ _id = '', command, name, schedule }: Crontab) {
|
||||||
|
this.logger.info(
|
||||||
|
'[创建定时任务],任务ID: %s,cron: %s,任务名: %s,执行命令: %s',
|
||||||
|
_id,
|
||||||
|
schedule,
|
||||||
|
name,
|
||||||
|
command,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.scheduleStacks.set(
|
||||||
|
_id,
|
||||||
|
nodeSchedule.scheduleJob(_id, schedule, async () => {
|
||||||
|
try {
|
||||||
|
exec(command, async (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
await this.logger.info(
|
||||||
|
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||||
|
command,
|
||||||
|
new Date().toLocaleString(),
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
await this.logger.info(
|
||||||
|
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||||
|
command,
|
||||||
|
new Date().toLocaleString(),
|
||||||
|
stderr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
await this.logger.info(
|
||||||
|
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||||
|
command,
|
||||||
|
new Date().toLocaleString(),
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelSchedule({ _id = '', name }: Crontab) {
|
||||||
|
this.logger.info('[取消定时任务],任务名:%s', name);
|
||||||
|
this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import SockService from './sock';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class ScriptService {
|
||||||
|
constructor(
|
||||||
|
@Inject('logger') private logger: winston.Logger,
|
||||||
|
private sockService: SockService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async runScript(path: string) {
|
||||||
|
const cp = spawn(`task -l ${path} now`, { shell: '/bin/bash' });
|
||||||
|
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: 'manuallyRunScript',
|
||||||
|
message: `开始执行脚本`,
|
||||||
|
});
|
||||||
|
cp.stdout.on('data', (data) => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: 'manuallyRunScript',
|
||||||
|
message: data.toString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.stderr.on('data', (data) => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: 'manuallyRunScript',
|
||||||
|
message: data.toString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('error', (err) => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: 'manuallyRunScript',
|
||||||
|
message: JSON.stringify(err),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { code: 200 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import { Connection } from 'sockjs';
|
||||||
|
import { SockMessage } from '../data/sock';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class SockService {
|
||||||
|
private clients: Connection[] = [];
|
||||||
|
|
||||||
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
|
public getClients() {
|
||||||
|
return this.clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addClient(conn: Connection) {
|
||||||
|
if (this.clients.indexOf(conn) === -1) {
|
||||||
|
this.clients.push(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeClient(conn: Connection) {
|
||||||
|
const index = this.clients.indexOf(conn);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.clients.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendMessage(msg: SockMessage) {
|
||||||
|
this.clients.forEach((x) => {
|
||||||
|
x.write(JSON.stringify(msg));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,488 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import { createRandomString, getNetIp, getPlatform } from '../config/util';
|
||||||
|
import config from '../config';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { authenticator } from '@otplib/preset-default';
|
||||||
|
import DataStore from 'nedb';
|
||||||
|
import { AuthDataType, AuthInfo, LoginStatus } from '../data/auth';
|
||||||
|
import { NotificationInfo } from '../data/notify';
|
||||||
|
import NotificationService from './notify';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import ScheduleService from './schedule';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import SockService from './sock';
|
||||||
|
import got from 'got';
|
||||||
|
import { dbs } from '../loaders/db';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class UserService {
|
||||||
|
@Inject((type) => NotificationService)
|
||||||
|
private notificationService!: NotificationService;
|
||||||
|
private authDb = dbs.authDb;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject('logger') private logger: winston.Logger,
|
||||||
|
private scheduleService: ScheduleService,
|
||||||
|
private sockService: SockService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async login(
|
||||||
|
payloads: {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
},
|
||||||
|
req: Request,
|
||||||
|
needTwoFactor = true,
|
||||||
|
): Promise<any> {
|
||||||
|
if (!fs.existsSync(config.authConfigFile)) {
|
||||||
|
return this.initAuthInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
let { username, password } = payloads;
|
||||||
|
const content = this.getAuthInfo();
|
||||||
|
const timestamp = Date.now();
|
||||||
|
if (content) {
|
||||||
|
let {
|
||||||
|
username: cUsername,
|
||||||
|
password: cPassword,
|
||||||
|
retries = 0,
|
||||||
|
lastlogon,
|
||||||
|
lastip,
|
||||||
|
lastaddr,
|
||||||
|
twoFactorActivated,
|
||||||
|
twoFactorActived,
|
||||||
|
tokens = {},
|
||||||
|
platform,
|
||||||
|
} = content;
|
||||||
|
// patch old field
|
||||||
|
twoFactorActivated = twoFactorActivated || twoFactorActived;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(cUsername === 'admin' && cPassword === 'admin') ||
|
||||||
|
!cUsername ||
|
||||||
|
!cPassword
|
||||||
|
) {
|
||||||
|
return this.initAuthInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retries > 2 && Date.now() - lastlogon < Math.pow(3, retries) * 1000) {
|
||||||
|
return {
|
||||||
|
code: 410,
|
||||||
|
message: `失败次数过多,请${Math.round(
|
||||||
|
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
|
||||||
|
)}秒后重试`,
|
||||||
|
data: Math.round(
|
||||||
|
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ip, address } = await getNetIp(req);
|
||||||
|
if (username === cUsername && password === cPassword) {
|
||||||
|
if (twoFactorActivated && needTwoFactor) {
|
||||||
|
this.updateAuthInfo(content, {
|
||||||
|
isTwoFactorChecking: true,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
code: 420,
|
||||||
|
message: '请输入两步验证token',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = createRandomString(50, 100);
|
||||||
|
const expiration = twoFactorActivated ? 30 : 3;
|
||||||
|
let token = jwt.sign({ data }, config.secret as any, {
|
||||||
|
expiresIn: 60 * 60 * 24 * expiration,
|
||||||
|
algorithm: 'HS384',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateAuthInfo(content, {
|
||||||
|
token,
|
||||||
|
tokens: {
|
||||||
|
...tokens,
|
||||||
|
[req.platform]: token,
|
||||||
|
},
|
||||||
|
lastlogon: timestamp,
|
||||||
|
retries: 0,
|
||||||
|
lastip: ip,
|
||||||
|
lastaddr: address,
|
||||||
|
platform: req.platform,
|
||||||
|
isTwoFactorChecking: false,
|
||||||
|
});
|
||||||
|
await this.notificationService.notify(
|
||||||
|
'登录通知',
|
||||||
|
`你于${new Date(timestamp).toLocaleString()}在 ${address} ${
|
||||||
|
req.platform
|
||||||
|
}端 登录成功,ip地址 ${ip}`,
|
||||||
|
);
|
||||||
|
await this.getLoginLog();
|
||||||
|
await this.insertDb({
|
||||||
|
type: AuthDataType.loginLog,
|
||||||
|
info: {
|
||||||
|
timestamp,
|
||||||
|
address,
|
||||||
|
ip,
|
||||||
|
platform: req.platform,
|
||||||
|
status: LoginStatus.success,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
data: { token, lastip, lastaddr, lastlogon, retries, platform },
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.updateAuthInfo(content, {
|
||||||
|
retries: retries + 1,
|
||||||
|
lastlogon: timestamp,
|
||||||
|
lastip: ip,
|
||||||
|
lastaddr: address,
|
||||||
|
platform: req.platform,
|
||||||
|
});
|
||||||
|
await this.notificationService.notify(
|
||||||
|
'登录通知',
|
||||||
|
`你于${new Date(timestamp).toLocaleString()}在 ${address} ${
|
||||||
|
req.platform
|
||||||
|
}端 登录失败,ip地址 ${ip}`,
|
||||||
|
);
|
||||||
|
await this.getLoginLog();
|
||||||
|
await this.insertDb({
|
||||||
|
type: AuthDataType.loginLog,
|
||||||
|
info: {
|
||||||
|
timestamp,
|
||||||
|
address,
|
||||||
|
ip,
|
||||||
|
platform: req.platform,
|
||||||
|
status: LoginStatus.fail,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { code: 400, message: config.authError };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return this.initAuthInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async logout(platform: string): Promise<any> {
|
||||||
|
const authInfo = this.getAuthInfo();
|
||||||
|
this.updateAuthInfo(authInfo, {
|
||||||
|
token: '',
|
||||||
|
tokens: { ...authInfo.tokens, [platform]: '' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLoginLog(): Promise<AuthInfo[]> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.authDb.find({ type: AuthDataType.loginLog }).exec((err, docs) => {
|
||||||
|
if (err || docs.length === 0) {
|
||||||
|
resolve([]);
|
||||||
|
} else {
|
||||||
|
const result = docs.sort(
|
||||||
|
(a, b) => b.info.timestamp - a.info.timestamp,
|
||||||
|
);
|
||||||
|
if (result.length > 100) {
|
||||||
|
this.authDb.remove({ _id: result[result.length - 1]._id });
|
||||||
|
}
|
||||||
|
resolve(result.map((x) => x.info));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async insertDb(payload: AuthInfo): Promise<AuthInfo> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.authDb.insert(payload, (err, doc) => {
|
||||||
|
if (err) {
|
||||||
|
this.logger.error(err);
|
||||||
|
} else {
|
||||||
|
resolve(doc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private initAuthInfo() {
|
||||||
|
const newPassword = createRandomString(16, 22);
|
||||||
|
fs.writeFileSync(
|
||||||
|
config.authConfigFile,
|
||||||
|
JSON.stringify({
|
||||||
|
username: 'admin',
|
||||||
|
password: newPassword,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
code: 100,
|
||||||
|
message: '已初始化密码,请前往auth.json查看并重新登录',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateUsernameAndPassword({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}: {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}) {
|
||||||
|
if (password === 'admin') {
|
||||||
|
return { code: 400, message: '密码不能设置为admin' };
|
||||||
|
}
|
||||||
|
const authInfo = this.getAuthInfo();
|
||||||
|
this.updateAuthInfo(authInfo, { username, password });
|
||||||
|
return { code: 200, message: '更新成功' };
|
||||||
|
}
|
||||||
|
|
||||||
|
public getUserInfo(): Promise<any> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
|
||||||
|
if (err) console.log(err);
|
||||||
|
resolve(JSON.parse(data));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public initTwoFactor() {
|
||||||
|
const secret = authenticator.generateSecret();
|
||||||
|
const authInfo = this.getAuthInfo();
|
||||||
|
const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);
|
||||||
|
this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
|
||||||
|
return { secret, url: otpauth };
|
||||||
|
}
|
||||||
|
|
||||||
|
public activeTwoFactor(code: string) {
|
||||||
|
const authInfo = this.getAuthInfo();
|
||||||
|
const isValid = authenticator.verify({
|
||||||
|
token: code,
|
||||||
|
secret: authInfo.twoFactorSecret,
|
||||||
|
});
|
||||||
|
if (isValid) {
|
||||||
|
this.updateAuthInfo(authInfo, { twoFactorActivated: true });
|
||||||
|
}
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async twoFactorLogin(
|
||||||
|
{
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
code,
|
||||||
|
}: { username: string; password: string; code: string },
|
||||||
|
req: any,
|
||||||
|
) {
|
||||||
|
const authInfo = this.getAuthInfo();
|
||||||
|
const { isTwoFactorChecking, twoFactorSecret } = authInfo;
|
||||||
|
if (!isTwoFactorChecking) {
|
||||||
|
return { code: 450, message: '未知错误' };
|
||||||
|
}
|
||||||
|
const isValid = authenticator.verify({
|
||||||
|
token: code,
|
||||||
|
secret: twoFactorSecret,
|
||||||
|
});
|
||||||
|
if (isValid) {
|
||||||
|
return this.login({ username, password }, req, false);
|
||||||
|
} else {
|
||||||
|
const { ip, address } = await getNetIp(req);
|
||||||
|
this.updateAuthInfo(authInfo, {
|
||||||
|
lastip: ip,
|
||||||
|
lastaddr: address,
|
||||||
|
platform: req.platform,
|
||||||
|
});
|
||||||
|
return { code: 430, message: '验证失败' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public deactiveTwoFactor() {
|
||||||
|
const authInfo = this.getAuthInfo();
|
||||||
|
this.updateAuthInfo(authInfo, {
|
||||||
|
twoFactorActivated: false,
|
||||||
|
twoFactorActived: false,
|
||||||
|
twoFactorSecret: '',
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAuthInfo() {
|
||||||
|
const content = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||||
|
return JSON.parse(content || '{}');
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateAuthInfo(authInfo: any, info: any) {
|
||||||
|
fs.writeFileSync(
|
||||||
|
config.authConfigFile,
|
||||||
|
JSON.stringify({ ...authInfo, ...info }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getNotificationMode(): Promise<NotificationInfo> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.authDb
|
||||||
|
.find({ type: AuthDataType.notification })
|
||||||
|
.exec((err, docs) => {
|
||||||
|
if (err || docs.length === 0) {
|
||||||
|
resolve({} as NotificationInfo);
|
||||||
|
} else {
|
||||||
|
resolve(docs[0].info);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLogRemoveFrequency() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.authDb
|
||||||
|
.find({ type: AuthDataType.removeLogFrequency })
|
||||||
|
.exec((err, docs) => {
|
||||||
|
if (err || docs.length === 0) {
|
||||||
|
resolve({});
|
||||||
|
} else {
|
||||||
|
resolve(docs[0].info);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateAuthDb(payload: AuthInfo): Promise<any> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.authDb.update(
|
||||||
|
{ type: payload.type },
|
||||||
|
{ ...payload },
|
||||||
|
{ upsert: true, returnUpdatedDocs: true },
|
||||||
|
(err, num, doc: any) => {
|
||||||
|
if (err) {
|
||||||
|
resolve({} as NotificationInfo);
|
||||||
|
} else {
|
||||||
|
resolve({ ...doc.info, _id: doc._id });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateNotificationMode(notificationInfo: NotificationInfo) {
|
||||||
|
const code = Math.random().toString().slice(-6);
|
||||||
|
const isSuccess = await this.notificationService.testNotify(
|
||||||
|
notificationInfo,
|
||||||
|
'青龙',
|
||||||
|
`【蛟龙】测试通知 https://t.me/jiao_long`,
|
||||||
|
);
|
||||||
|
if (isSuccess) {
|
||||||
|
const result = await this.updateAuthDb({
|
||||||
|
type: AuthDataType.notification,
|
||||||
|
info: { ...notificationInfo },
|
||||||
|
});
|
||||||
|
return { code: 200, data: { ...result, code } };
|
||||||
|
} else {
|
||||||
|
return { code: 400, data: '通知发送失败,请检查参数' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateLogRemoveFrequency(frequency: number) {
|
||||||
|
const result = await this.updateAuthDb({
|
||||||
|
type: AuthDataType.removeLogFrequency,
|
||||||
|
info: { frequency },
|
||||||
|
});
|
||||||
|
const cron = {
|
||||||
|
_id: result._id,
|
||||||
|
name: '删除日志',
|
||||||
|
command: `ql rmlog ${frequency}`,
|
||||||
|
schedule: `5 23 */${frequency} * *`,
|
||||||
|
};
|
||||||
|
await this.scheduleService.cancelSchedule(cron);
|
||||||
|
if (frequency > 0) {
|
||||||
|
await this.scheduleService.generateSchedule(cron);
|
||||||
|
}
|
||||||
|
return { code: 200, data: { ...cron } };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async checkUpdate() {
|
||||||
|
try {
|
||||||
|
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
||||||
|
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
|
||||||
|
|
||||||
|
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
||||||
|
const currentVersion = currentVersionFile.match(versionRegx)![1];
|
||||||
|
|
||||||
|
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,
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import * as yargs from 'yargs';
|
||||||
|
|
||||||
|
yargs.help('h').alias('h', 'help').help().argv;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: whyour/qinglong:latest
|
||||||
|
volumes:
|
||||||
|
- ./data/config:/ql/config
|
||||||
|
- ./data/log:/ql/log
|
||||||
|
- ./data/db:/ql/db
|
||||||
|
- ./data/scripts:/ql/scripts
|
||||||
|
- ./data/repo:/ql/repo
|
||||||
|
- ./data/raw:/ql/raw
|
||||||
|
ports:
|
||||||
|
- "0.0.0.0:5700:5700"
|
||||||
|
restart: unless-stopped
|
||||||
+11
-9
@@ -1,14 +1,17 @@
|
|||||||
FROM node:lts-alpine
|
FROM node:lts-alpine3.12
|
||||||
LABEL maintainer="whyour"
|
ARG QL_MAINTAINER="whyour"
|
||||||
ARG QL_URL=https://github.com/whyour/qinglong.git
|
LABEL maintainer="${QL_MAINTAINER}"
|
||||||
|
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
|
||||||
ARG QL_BRANCH=master
|
ARG QL_BRANCH=master
|
||||||
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
||||||
LANG=zh_CN.UTF-8 \
|
LANG=zh_CN.UTF-8 \
|
||||||
SHELL=/bin/bash \
|
SHELL=/bin/bash \
|
||||||
PS1="\u@\h:\w \$ " \
|
PS1="\u@\h:\w \$ " \
|
||||||
QL_DIR=/ql
|
QL_DIR=/ql \
|
||||||
|
QL_BRANCH=${QL_BRANCH}
|
||||||
WORKDIR ${QL_DIR}
|
WORKDIR ${QL_DIR}
|
||||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
RUN set -x \
|
||||||
|
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \
|
||||||
&& apk update -f \
|
&& apk update -f \
|
||||||
&& apk upgrade \
|
&& apk upgrade \
|
||||||
&& apk --no-cache add -f bash \
|
&& apk --no-cache add -f bash \
|
||||||
@@ -24,15 +27,14 @@ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
|||||||
python3 \
|
python3 \
|
||||||
jq \
|
jq \
|
||||||
openssh \
|
openssh \
|
||||||
|
py3-pip \
|
||||||
&& rm -rf /var/cache/apk/* \
|
&& rm -rf /var/cache/apk/* \
|
||||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||||
&& echo "Asia/Shanghai" > /etc/timezone \
|
&& echo "Asia/Shanghai" > /etc/timezone \
|
||||||
&& touch ~/.bashrc \
|
&& touch ~/.bashrc \
|
||||||
&& mkdir /run/nginx \
|
|
||||||
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
||||||
&& git config --global user.email "qinglong@@users.noreply.github.com" \
|
&& git config --global user.email "qinglong@@users.noreply.github.com" \
|
||||||
&& git config --global user.name "qinglong" \
|
&& git config --global user.name "qinglong" \
|
||||||
&& git config --global pull.rebase true \
|
|
||||||
&& cd ${QL_DIR} \
|
&& cd ${QL_DIR} \
|
||||||
&& cp -f .env.example .env \
|
&& cp -f .env.example .env \
|
||||||
&& chmod 777 ${QL_DIR}/shell/*.sh \
|
&& chmod 777 ${QL_DIR}/shell/*.sh \
|
||||||
@@ -43,7 +45,7 @@ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
|||||||
&& rm -rf /root/.npm \
|
&& rm -rf /root/.npm \
|
||||||
&& pnpm install --prod \
|
&& pnpm install --prod \
|
||||||
&& rm -rf /root/.pnpm-store \
|
&& rm -rf /root/.pnpm-store \
|
||||||
&& git clone -b ${QL_BRANCH} https://github.com/whyour/qinglong-static.git /static \
|
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
|
||||||
&& cp -rf /static/* ${QL_DIR} \
|
&& cp -rf /static/* ${QL_DIR} \
|
||||||
&& rm -rf /static
|
&& rm -rf /static
|
||||||
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
||||||
|
|||||||
+10
-14
@@ -1,13 +1,15 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
|
||||||
|
|
||||||
dir_shell=/ql/shell
|
dir_shell=/ql/shell
|
||||||
. $dir_shell/share.sh
|
. $dir_shell/share.sh
|
||||||
link_shell
|
link_shell
|
||||||
|
|
||||||
echo -e "======================1. 检测配置文件========================\n"
|
echo -e "======================1. 检测配置文件========================\n"
|
||||||
fix_config
|
make_dir /etc/nginx/conf.d
|
||||||
cp -fv $dir_root/docker/front.conf /etc/nginx/conf.d/front.conf
|
make_dir /run/nginx
|
||||||
pm2 l >/dev/null 2>&1
|
cp -fv $nginx_conf /etc/nginx/nginx.conf
|
||||||
|
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
|
||||||
|
pm2 l &>/dev/null
|
||||||
echo
|
echo
|
||||||
|
|
||||||
echo -e "======================2. 安装依赖========================\n"
|
echo -e "======================2. 安装依赖========================\n"
|
||||||
@@ -19,19 +21,13 @@ nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
|||||||
echo -e "nginx启动成功...\n"
|
echo -e "nginx启动成功...\n"
|
||||||
|
|
||||||
echo -e "======================4. 启动控制面板========================\n"
|
echo -e "======================4. 启动控制面板========================\n"
|
||||||
if [[ $(pm2 info panel 2>/dev/null) ]]; then
|
pm2 delete panel &>/dev/null
|
||||||
pm2 reload panel --source-map-support --time
|
pm2 start $dir_root/build/app.js -n panel --source-map-support --time
|
||||||
else
|
|
||||||
pm2 start $dir_root/build/app.js -n panel --source-map-support --time
|
|
||||||
fi
|
|
||||||
echo -e "控制面板启动成功...\n"
|
echo -e "控制面板启动成功...\n"
|
||||||
|
|
||||||
echo -e "======================5. 启动定时任务========================\n"
|
echo -e "======================5. 启动定时任务========================\n"
|
||||||
if [[ $(pm2 info schedule 2>/dev/null) ]]; then
|
pm2 delete schedule &>/dev/null
|
||||||
pm2 reload schedule --source-map-support --time
|
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time
|
||||||
else
|
|
||||||
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time
|
|
||||||
fi
|
|
||||||
echo -e "定时任务启动成功...\n"
|
echo -e "定时任务启动成功...\n"
|
||||||
|
|
||||||
if [[ $AutoStartBot == true ]]; then
|
if [[ $AutoStartBot == true ]]; then
|
||||||
|
|||||||
@@ -2,12 +2,30 @@ upstream api {
|
|||||||
server 0.0.0.0:5600;
|
server 0.0.0.0:5600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default keep-alive;
|
||||||
|
'websocket' upgrade;
|
||||||
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 5700;
|
listen 5700;
|
||||||
root /ql/dist;
|
root /ql/dist;
|
||||||
ssl_session_timeout 5m;
|
ssl_session_timeout 5m;
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_pass http://api;
|
||||||
|
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /open {
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_pass http://api;
|
proxy_pass http://api;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
user root;
|
||||||
|
worker_processes auto;
|
||||||
|
pcre_jit on;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
include /etc/nginx/modules/*.conf;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
client_max_body_size 20m;
|
||||||
|
client_body_buffer_size 20m;
|
||||||
|
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
|
||||||
|
tcp_nodelay on;
|
||||||
|
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
|
||||||
|
ssl_session_cache shared:SSL:2m;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_static on;
|
||||||
|
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_buffers 16 8k;
|
||||||
|
gzip_http_version 1.0;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
}
|
||||||
+28
-6
@@ -24,27 +24,42 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@otplib/preset-default": "^12.0.1",
|
||||||
|
"@sentry/node": "^6.14.0",
|
||||||
|
"@sentry/tracing": "^6.14.0",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"celebrate": "^13.0.3",
|
"celebrate": "^13.0.3",
|
||||||
|
"chokidar": "^3.5.2",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cron-parser": "^3.5.0",
|
"cron-parser": "^3.5.0",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-jwt": "^6.0.0",
|
"express-jwt": "^6.0.0",
|
||||||
|
"express-urlrewrite": "^1.4.0",
|
||||||
|
"got": "^11.8.2",
|
||||||
|
"hpagent": "^0.1.2",
|
||||||
|
"iconv-lite": "^0.6.3",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"nedb": "^1.8.0",
|
"nedb": "^1.8.0",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"node-schedule": "^2.0.0",
|
"node-schedule": "^2.0.0",
|
||||||
|
"nodemailer": "^6.7.0",
|
||||||
"p-queue": "6.6.2",
|
"p-queue": "6.6.2",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"serve-handler": "^6.1.3",
|
||||||
|
"sockjs": "^0.3.21",
|
||||||
"typedi": "^0.8.0",
|
"typedi": "^0.8.0",
|
||||||
"winston": "^3.3.3"
|
"uuid": "^8.3.2",
|
||||||
|
"winston": "^3.3.3",
|
||||||
|
"yargs": "^17.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/icons": "^4.6.2",
|
"@ant-design/icons": "^4.6.2",
|
||||||
"@ant-design/pro-layout": "^6.5.0",
|
"@ant-design/pro-layout": "^6.26.0",
|
||||||
"@monaco-editor/react": "^4.2.1",
|
"@monaco-editor/react": "^4.3.1",
|
||||||
|
"@sentry/react": "^6.14.0",
|
||||||
|
"@sentry/webpack-plugin": "^1.18.3",
|
||||||
"@types/cors": "^2.8.10",
|
"@types/cors": "^2.8.10",
|
||||||
"@types/express": "^4.17.8",
|
"@types/express": "^4.17.8",
|
||||||
"@types/express-jwt": "^6.0.1",
|
"@types/express-jwt": "^6.0.1",
|
||||||
@@ -53,11 +68,17 @@
|
|||||||
"@types/nedb": "^1.8.11",
|
"@types/nedb": "^1.8.11",
|
||||||
"@types/node": "^14.11.2",
|
"@types/node": "^14.11.2",
|
||||||
"@types/node-fetch": "^2.5.8",
|
"@types/node-fetch": "^2.5.8",
|
||||||
|
"@types/node-schedule": "^1.3.2",
|
||||||
|
"@types/nodemailer": "^6.4.4",
|
||||||
"@types/qrcode.react": "^1.0.1",
|
"@types/qrcode.react": "^1.0.1",
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.0",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^17.0.0",
|
||||||
"@umijs/plugin-antd": "^0.9.1",
|
"@types/serve-handler": "^6.1.1",
|
||||||
|
"@types/sockjs": "^0.3.33",
|
||||||
|
"@types/sockjs-client": "^1.5.1",
|
||||||
|
"@umijs/plugin-antd": "^0.11.0",
|
||||||
"@umijs/test": "^3.3.9",
|
"@umijs/test": "^3.3.9",
|
||||||
|
"antd": "^4.17.0-alpha.6",
|
||||||
"codemirror": "^5.62.2",
|
"codemirror": "^5.62.2",
|
||||||
"compression-webpack-plugin": "6.1.1",
|
"compression-webpack-plugin": "6.1.1",
|
||||||
"darkreader": "^4.9.27",
|
"darkreader": "^4.9.27",
|
||||||
@@ -68,13 +89,14 @@
|
|||||||
"react": "17.x",
|
"react": "17.x",
|
||||||
"react-codemirror2": "^7.2.1",
|
"react-codemirror2": "^7.2.1",
|
||||||
"react-diff-viewer": "^3.1.1",
|
"react-diff-viewer": "^3.1.1",
|
||||||
"react-split-pane": "^0.1.92",
|
|
||||||
"react-dnd": "^14.0.2",
|
"react-dnd": "^14.0.2",
|
||||||
"react-dnd-html5-backend": "^14.0.0",
|
"react-dnd-html5-backend": "^14.0.0",
|
||||||
"react-dom": "17.x",
|
"react-dom": "17.x",
|
||||||
|
"react-split-pane": "^0.1.92",
|
||||||
|
"sockjs-client": "^1.5.2",
|
||||||
"ts-node": "^9.0.0",
|
"ts-node": "^9.0.0",
|
||||||
"typescript": "^4.1.2",
|
"typescript": "^4.1.2",
|
||||||
"umi": "^3.3.9",
|
"umi": "^3.5.0",
|
||||||
"umi-request": "^1.3.5",
|
"umi-request": "^1.3.5",
|
||||||
"vh-check": "^2.0.5",
|
"vh-check": "^2.0.5",
|
||||||
"webpack": "^5.28.0",
|
"webpack": "^5.28.0",
|
||||||
|
|||||||
Generated
-9765
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
@@ -1 +1 @@
|
|||||||
{ "username": "admin", "password": "adminadmin" }
|
{ "username": "admin", "password": "admin" }
|
||||||
|
|||||||
+12
-1
@@ -10,6 +10,9 @@ AutoDelCron="true"
|
|||||||
## 在运行 ql repo 命令时,是否自动增加新的本地定时任务
|
## 在运行 ql repo 命令时,是否自动增加新的本地定时任务
|
||||||
AutoAddCron="true"
|
AutoAddCron="true"
|
||||||
|
|
||||||
|
## 拉取脚本时默认的定时规则,当匹配不到定时规则时使用,例如: 0 9 * * *
|
||||||
|
DefaultCronRule=""
|
||||||
|
|
||||||
## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可
|
## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可
|
||||||
RepoFileExtensions="js py"
|
RepoFileExtensions="js py"
|
||||||
|
|
||||||
@@ -100,7 +103,7 @@ export PUSH_PLUS_TOKEN=""
|
|||||||
## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送
|
## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送
|
||||||
export PUSH_PLUS_USER=""
|
export PUSH_PLUS_USER=""
|
||||||
|
|
||||||
## 8. go-cqhttp
|
## 9. go-cqhttp
|
||||||
## gobot_url 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
|
## gobot_url 推送到个人QQ: http://127.0.0.1/send_private_msg 群:http://127.0.0.1/send_group_msg
|
||||||
## gobot_token 填写在go-cqhttp文件设置的访问密钥
|
## gobot_token 填写在go-cqhttp文件设置的访问密钥
|
||||||
## gobot_qq 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
|
## gobot_qq 如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群
|
||||||
@@ -109,4 +112,12 @@ export GOBOT_URL=""
|
|||||||
export GOBOT_TOKEN=""
|
export GOBOT_TOKEN=""
|
||||||
export GOBOT_QQ=""
|
export GOBOT_QQ=""
|
||||||
|
|
||||||
|
## 10. gotify
|
||||||
|
## gotify_url 填写gotify地址,如https://push.example.de:8080
|
||||||
|
## gotify_token 填写gotify的消息应用token
|
||||||
|
## gotify_priority 填写推送消息优先级,默认为0
|
||||||
|
export GOTIFY_URL="";
|
||||||
|
export GOTIFY_TOKEN="";
|
||||||
|
export GOTIFY_PRIORITY=0;
|
||||||
|
|
||||||
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
|
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
|
||||||
|
|||||||
+57
-4
@@ -13,6 +13,13 @@
|
|||||||
const querystring = require('querystring');
|
const querystring = require('querystring');
|
||||||
const $ = new Env();
|
const $ = new Env();
|
||||||
const timeout = 15000; //超时时间(单位毫秒)
|
const timeout = 15000; //超时时间(单位毫秒)
|
||||||
|
// =======================================gotify通知设置区域==============================================
|
||||||
|
//gotify_url 填写gotify地址,如https://push.example.de:8080
|
||||||
|
//gotify_token 填写gotify的消息应用token
|
||||||
|
//gotify_priority 填写推送消息优先级,默认为0
|
||||||
|
let GOTIFY_URL = '';
|
||||||
|
let GOTIFY_TOKEN = '';
|
||||||
|
let GOTIFY_PRIORITY = 0;
|
||||||
// =======================================go-cqhttp通知设置区域===========================================
|
// =======================================go-cqhttp通知设置区域===========================================
|
||||||
//gobot_url 填写请求地址http://127.0.0.1/send_private_msg
|
//gobot_url 填写请求地址http://127.0.0.1/send_private_msg
|
||||||
//gobot_token 填写在go-cqhttp文件设置的访问密钥
|
//gobot_token 填写在go-cqhttp文件设置的访问密钥
|
||||||
@@ -84,6 +91,16 @@ let PUSH_PLUS_TOKEN = '';
|
|||||||
let PUSH_PLUS_USER = '';
|
let PUSH_PLUS_USER = '';
|
||||||
|
|
||||||
//==========================云端环境变量的判断与接收=========================
|
//==========================云端环境变量的判断与接收=========================
|
||||||
|
if (process.env.GOTIFY_URL) {
|
||||||
|
GOTIFY_URL = process.env.GOTIFY_URL;
|
||||||
|
}
|
||||||
|
if (process.env.GOTIFY_TOKEN) {
|
||||||
|
GOTIFY_TOKEN = process.env.GOTIFY_TOKEN;
|
||||||
|
}
|
||||||
|
if (process.env.GOTIFY_PRIORITY) {
|
||||||
|
GOTIFY_PRIORITY = process.env.GOTIFY_PRIORITY;
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.GOBOT_URL) {
|
if (process.env.GOBOT_URL) {
|
||||||
GOBOT_URL = process.env.GOBOT_URL;
|
GOBOT_URL = process.env.GOBOT_URL;
|
||||||
}
|
}
|
||||||
@@ -200,9 +217,45 @@ async function sendNotify(
|
|||||||
qywxamNotify(text, desp), //企业微信应用消息推送
|
qywxamNotify(text, desp), //企业微信应用消息推送
|
||||||
iGotNotify(text, desp, params), //iGot
|
iGotNotify(text, desp, params), //iGot
|
||||||
gobotNotify(text, desp),//go-cqhttp
|
gobotNotify(text, desp),//go-cqhttp
|
||||||
|
gotifyNotify(text, desp),//gotify
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function gotifyNotify(text, desp) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (GOTIFY_URL && GOTIFY_TOKEN) {
|
||||||
|
const options = {
|
||||||
|
url: `${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}`,
|
||||||
|
body: `title=${encodeURIComponent(text)}&message=${encodeURIComponent(desp)}&priority=${GOTIFY_PRIORITY}`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$.post(options, (err, resp, data) => {
|
||||||
|
try {
|
||||||
|
if (err) {
|
||||||
|
console.log('gotify发送通知调用API失败!!\n');
|
||||||
|
console.log(err);
|
||||||
|
} else {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
if (data.id) {
|
||||||
|
console.log('gotify发送通知消息成功🎉\n');
|
||||||
|
} else {
|
||||||
|
console.log(`${data.message}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.logErr(e, resp);
|
||||||
|
} finally {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function gobotNotify(text, desp, time = 2100) {
|
function gobotNotify(text, desp, time = 2100) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (GOBOT_URL) {
|
if (GOBOT_URL) {
|
||||||
@@ -673,8 +726,8 @@ function qywxamNotify(text, desp) {
|
|||||||
if (err) {
|
if (err) {
|
||||||
console.log(
|
console.log(
|
||||||
'成员ID:' +
|
'成员ID:' +
|
||||||
ChangeUserId(desp) +
|
ChangeUserId(desp) +
|
||||||
'企业微信应用消息发送通知消息失败!!\n',
|
'企业微信应用消息发送通知消息失败!!\n',
|
||||||
);
|
);
|
||||||
console.log(err);
|
console.log(err);
|
||||||
} else {
|
} else {
|
||||||
@@ -682,8 +735,8 @@ function qywxamNotify(text, desp) {
|
|||||||
if (data.errcode === 0) {
|
if (data.errcode === 0) {
|
||||||
console.log(
|
console.log(
|
||||||
'成员ID:' +
|
'成员ID:' +
|
||||||
ChangeUserId(desp) +
|
ChangeUserId(desp) +
|
||||||
'企业微信应用消息发送通知消息成功🎉。\n',
|
'企业微信应用消息发送通知消息成功🎉。\n',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log(`${data.errmsg}\n`);
|
console.log(`${data.errmsg}\n`);
|
||||||
|
|||||||
+504
-240
@@ -1,277 +1,541 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# _*_ coding:utf-8 _*_
|
# _*_ coding:utf-8 _*_
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
cur_path = os.path.abspath(os.path.dirname(__file__))
|
|
||||||
root_path = os.path.split(cur_path)[0]
|
|
||||||
sys.path.append(root_path)
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
import traceback
|
|
||||||
import time
|
|
||||||
import hmac
|
|
||||||
import hashlib
|
|
||||||
import base64
|
import base64
|
||||||
import urllib.parse
|
import hashlib
|
||||||
from requests.adapters import HTTPAdapter
|
import hmac
|
||||||
from urllib3.util import Retry
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# 原先的 print 函数和主线程的锁
|
||||||
|
_print = print
|
||||||
|
mutex = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
# 定义新的 print 函数
|
||||||
|
def print(text, *args, **kw):
|
||||||
|
"""
|
||||||
|
使输出有序进行,不出现多线程同一时间输出导致错乱的问题。
|
||||||
|
"""
|
||||||
|
with mutex:
|
||||||
|
_print(text, *args, **kw)
|
||||||
|
|
||||||
|
|
||||||
# 通知服务
|
# 通知服务
|
||||||
BARK = '' # bark服务,此参数如果以http或者https开头则判定为自建bark服务; secrets可填;
|
# fmt: off
|
||||||
SCKEY = '' # Server酱的SCKEY; secrets可填
|
push_config = {
|
||||||
TG_BOT_TOKEN = '' # tg机器人的TG_BOT_TOKEN; secrets可填
|
'HITOKOTO': False, # 启用一言(随机句子)
|
||||||
TG_USER_ID = '' # tg机器人的TG_USER_ID; secrets可填
|
|
||||||
TG_PROXY_IP = '' # tg机器人的TG_PROXY_IP; secrets可填
|
|
||||||
TG_PROXY_PORT = '' # tg机器人的TG_PROXY_PORT; secrets可填
|
|
||||||
DD_BOT_ACCESS_TOKEN = '' # 钉钉机器人的DD_BOT_ACCESS_TOKEN; secrets可填
|
|
||||||
DD_BOT_SECRET = '' # 钉钉机器人的DD_BOT_SECRET; secrets可填
|
|
||||||
QYWX_APP = '' # 企业微信应用的QYWX_APP; secrets可填 参考http://note.youdao.com/s/HMiudGkb
|
|
||||||
|
|
||||||
notify_mode = []
|
'BARK_PUSH': '', # bark IP 或设备码,例:https://api.day.app/DxHcxxxxxRxxxxxxcm/
|
||||||
|
'BARK_ARCHIVE': '', # bark 推送是否存档
|
||||||
|
'BARK_GROUP': '', # bark 推送分组
|
||||||
|
'BARK_SOUND': '', # bark 推送声音
|
||||||
|
|
||||||
# GitHub action运行需要填写对应的secrets
|
'CONSOLE': True, # 控制台输出
|
||||||
if "BARK" in os.environ and os.environ["BARK"]:
|
|
||||||
BARK = os.environ["BARK"]
|
|
||||||
if "SCKEY" in os.environ and os.environ["SCKEY"]:
|
|
||||||
SCKEY = os.environ["SCKEY"]
|
|
||||||
if "TG_BOT_TOKEN" in os.environ and os.environ["TG_BOT_TOKEN"] and "TG_USER_ID" in os.environ and os.environ["TG_USER_ID"]:
|
|
||||||
TG_BOT_TOKEN = os.environ["TG_BOT_TOKEN"]
|
|
||||||
TG_USER_ID = os.environ["TG_USER_ID"]
|
|
||||||
if "DD_BOT_ACCESS_TOKEN" in os.environ and os.environ["DD_BOT_ACCESS_TOKEN"] and "DD_BOT_SECRET" in os.environ and os.environ["DD_BOT_SECRET"]:
|
|
||||||
DD_BOT_ACCESS_TOKEN = os.environ["DD_BOT_ACCESS_TOKEN"]
|
|
||||||
DD_BOT_SECRET = os.environ["DD_BOT_SECRET"]
|
|
||||||
if "QYWX_APP" in os.environ and os.environ["QYWX_APP"]:
|
|
||||||
QYWX_APP = os.environ["QYWX_APP"]
|
|
||||||
|
|
||||||
if BARK:
|
'DD_BOT_SECRET': '', # 钉钉机器人的 DD_BOT_SECRET
|
||||||
notify_mode.append('bark')
|
'DD_BOT_TOKEN': '', # 钉钉机器人的 DD_BOT_TOKEN
|
||||||
print("BARK 推送打开")
|
|
||||||
if SCKEY:
|
|
||||||
notify_mode.append('sc_key')
|
|
||||||
print("Server酱 推送打开")
|
|
||||||
if TG_BOT_TOKEN and TG_USER_ID:
|
|
||||||
notify_mode.append('telegram_bot')
|
|
||||||
print("Telegram 推送打开")
|
|
||||||
if DD_BOT_ACCESS_TOKEN and DD_BOT_SECRET:
|
|
||||||
notify_mode.append('dingding_bot')
|
|
||||||
print("钉钉机器人 推送打开")
|
|
||||||
if QYWX_APP:
|
|
||||||
notify_mode.append('qywxapp_bot')
|
|
||||||
print("企业微信应用 推送打开")
|
|
||||||
|
|
||||||
def bark(title, content):
|
'FSKEY': '', # 飞书机器人的 FSKEY
|
||||||
print("\n")
|
|
||||||
if not BARK:
|
'GOBOT_URL': '', # go-cqhttp
|
||||||
print("bark服务的bark_token未设置!!\n取消推送")
|
# 推送到个人QQ:http://127.0.0.1/send_private_msg
|
||||||
|
# 群:http://127.0.0.1/send_group_msg
|
||||||
|
'GOBOT_QQ': '', # go-cqhttp 的推送群或用户
|
||||||
|
# GOBOT_URL 设置 /send_private_msg 时填入 user_id=个人QQ
|
||||||
|
# /send_group_msg 时填入 group_id=QQ群
|
||||||
|
'GOBOT_TOKEN': '', # go-cqhttp 的 access_token
|
||||||
|
|
||||||
|
'GOTIFY_URL': '', # gotify地址,如https://push.example.de:8080
|
||||||
|
'GOTIFY_TOKEN': '', # gotify的消息应用token
|
||||||
|
'GOTIFY_PRIORITY': 0, # 推送消息优先级,默认为0
|
||||||
|
|
||||||
|
'IGOT_PUSH_KEY': '', # iGot 聚合推送的 IGOT_PUSH_KEY
|
||||||
|
|
||||||
|
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
|
||||||
|
|
||||||
|
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
|
||||||
|
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
|
||||||
|
|
||||||
|
'QMSG_KEY': '', # qmsg 酱的 QMSG_KEY
|
||||||
|
'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE
|
||||||
|
|
||||||
|
'QYWX_AM': '', # 企业微信应用
|
||||||
|
|
||||||
|
'QYWX_KEY': '', # 企业微信机器人
|
||||||
|
|
||||||
|
'TG_BOT_TOKEN': '', # tg 机器人的 TG_BOT_TOKEN,例:1407203283:AAG9rt-6RDaaX0HBLZQq0laNOh898iFYaRQ
|
||||||
|
'TG_USER_ID': '', # tg 机器人的 TG_USER_ID,例:1434078534
|
||||||
|
'TG_API_HOST': '', # tg 代理 api
|
||||||
|
'TG_PROXY_AUTH': '', # tg 代理认证参数
|
||||||
|
'TG_PROXY_HOST': '', # tg 机器人的 TG_PROXY_HOST
|
||||||
|
'TG_PROXY_PORT': '', # tg 机器人的 TG_PROXY_PORT
|
||||||
|
}
|
||||||
|
notify_function = []
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
# 首先读取 面板变量 或者 github action 运行变量
|
||||||
|
for k in push_config:
|
||||||
|
if os.getenv(k):
|
||||||
|
v = os.getenv(k)
|
||||||
|
push_config[k] = v
|
||||||
|
|
||||||
|
|
||||||
|
def bark(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 bark 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("BARK_PUSH"):
|
||||||
|
print("bark 服务的 BARK_PUSH 未设置!!\n取消推送")
|
||||||
return
|
return
|
||||||
print("bark服务启动")
|
print("bark 服务启动")
|
||||||
url = None
|
|
||||||
if BARK.startswith('http'):
|
if push_config.get("BARK_PUSH").startswith("http"):
|
||||||
url = f"""{BARK}/{title}/{content}"""
|
url = f'{push_config.get("BARK_PUSH")}/{urllib.parse.quote_plus(title)}/{urllib.parse.quote_plus(content)}'
|
||||||
else:
|
else:
|
||||||
url = f"""https://api.day.app/{BARK}/{title}/{content}"""
|
url = f'https://api.day.app/{push_config.get("BARK_PUSH")}/{urllib.parse.quote_plus(title)}/{urllib.parse.quote_plus(content)}'
|
||||||
|
|
||||||
|
bark_params = {
|
||||||
|
"BARK_ARCHIVE": "isArchive",
|
||||||
|
"BARK_GROUP": "group",
|
||||||
|
"BARK_SOUND": "sound",
|
||||||
|
}
|
||||||
|
params = ""
|
||||||
|
for pair in filter(
|
||||||
|
lambda pairs: pairs[0].startswith("BARK_")
|
||||||
|
and pairs[0] != "BARK_PUSH"
|
||||||
|
and pairs[1]
|
||||||
|
and bark_params.get(pairs[0]),
|
||||||
|
push_config.items(),
|
||||||
|
):
|
||||||
|
params += f"{bark_params.get(pair[0])}={pair[1]}&"
|
||||||
|
if params:
|
||||||
|
url = url + "?" + params.rstrip("&")
|
||||||
response = requests.get(url).json()
|
response = requests.get(url).json()
|
||||||
if response['code'] == 200:
|
|
||||||
print('推送成功!')
|
|
||||||
else:
|
|
||||||
print('推送失败!')
|
|
||||||
|
|
||||||
def serverJ(title, content):
|
if response["code"] == 200:
|
||||||
print("\n")
|
print("bark 推送成功!")
|
||||||
if not SCKEY:
|
else:
|
||||||
print("server酱服务的SCKEY未设置!!\n取消推送")
|
print("bark 推送失败!")
|
||||||
|
|
||||||
|
|
||||||
|
def console(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 控制台 推送消息。
|
||||||
|
"""
|
||||||
|
print(f"{title}\n\n{content}")
|
||||||
|
|
||||||
|
|
||||||
|
def dingding_bot(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 钉钉机器人 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("DD_BOT_SECRET") or not push_config.get("DD_BOT_TOKEN"):
|
||||||
|
print("钉钉机器人 服务的 DD_BOT_SECRET 或者 DD_BOT_TOKEN 未设置!!\n取消推送")
|
||||||
return
|
return
|
||||||
print("serverJ服务启动")
|
print("钉钉机器人 服务启动")
|
||||||
|
|
||||||
|
timestamp = str(round(time.time() * 1000))
|
||||||
|
secret_enc = push_config.get("DD_BOT_SECRET").encode("utf-8")
|
||||||
|
string_to_sign = "{}\n{}".format(timestamp, push_config.get("DD_BOT_SECRET"))
|
||||||
|
string_to_sign_enc = string_to_sign.encode("utf-8")
|
||||||
|
hmac_code = hmac.new(
|
||||||
|
secret_enc, string_to_sign_enc, digestmod=hashlib.sha256
|
||||||
|
).digest()
|
||||||
|
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
|
||||||
|
url = f'https://oapi.dingtalk.com/robot/send?access_token={push_config.get("DD_BOT_TOKEN")}×tamp={timestamp}&sign={sign}'
|
||||||
|
headers = {"Content-Type": "application/json;charset=utf-8"}
|
||||||
|
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
|
||||||
|
response = requests.post(
|
||||||
|
url=url, data=json.dumps(data), headers=headers, timeout=15
|
||||||
|
).json()
|
||||||
|
|
||||||
|
if not response["errcode"]:
|
||||||
|
print("钉钉机器人 推送成功!")
|
||||||
|
else:
|
||||||
|
print("钉钉机器人 推送失败!")
|
||||||
|
|
||||||
|
|
||||||
|
def feishu_bot(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 飞书机器人 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("FSKEY"):
|
||||||
|
print("飞书 服务的 FSKEY 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("飞书 服务启动")
|
||||||
|
|
||||||
|
url = f'https://open.feishu.cn/open-apis/bot/v2/hook/{push_config.get("FSKEY")}'
|
||||||
|
data = {"msg_type": "text", "content": {"text": f"{title}\n\n{content}"}}
|
||||||
|
response = requests.post(url, data=json.dumps(data)).json()
|
||||||
|
|
||||||
|
if response.get("StatusCode") == 0:
|
||||||
|
print("飞书 推送成功!")
|
||||||
|
else:
|
||||||
|
print("飞书 推送失败!错误信息如下:\n", response)
|
||||||
|
|
||||||
|
|
||||||
|
def go_cqhttp(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 go_cqhttp 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("GOBOT_URL") or not push_config.get("GOBOT_QQ"):
|
||||||
|
print("go-cqhttp 服务的 GOBOT_URL 或 GOBOT_QQ 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("go-cqhttp 服务启动")
|
||||||
|
|
||||||
|
url = f'{push_config.get("GOBOT_URL")}?access_token={push_config.get("GOBOT_TOKEN")}&{push_config.get("GOBOT_QQ")}&message=标题:{title}\n内容:{content}'
|
||||||
|
response = requests.get(url).json()
|
||||||
|
|
||||||
|
if response["status"] == "ok":
|
||||||
|
print("go-cqhttp 推送成功!")
|
||||||
|
else:
|
||||||
|
print("go-cqhttp 推送失败!")
|
||||||
|
|
||||||
|
|
||||||
|
def gotify(title:str,content:str) -> None:
|
||||||
|
"""
|
||||||
|
使用 gotify 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"):
|
||||||
|
print("gotify 服务的 GOTIFY_URL 或 GOTIFY_TOKEN 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("gotify 服务启动")
|
||||||
|
|
||||||
|
url = f'{push_config.get("GOTIFY_URL")}/message?token={push_config.get("GOTIFY_TOKEN")}'
|
||||||
|
data = {"title": title,"message": content,"priority": push_config.get("GOTIFY_PRIORITY")}
|
||||||
|
response = requests.post(url,data=data).json()
|
||||||
|
|
||||||
|
if response.get("id"):
|
||||||
|
print("gotify 推送成功!")
|
||||||
|
else:
|
||||||
|
print("gotify 推送失败!")
|
||||||
|
|
||||||
|
|
||||||
|
def iGot(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 iGot 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("IGOT_PUSH_KEY"):
|
||||||
|
print("iGot 服务的 IGOT_PUSH_KEY 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("iGot 服务启动")
|
||||||
|
|
||||||
|
url = f'https://push.hellyw.com/{push_config.get("IGOT_PUSH_KEY")}'
|
||||||
|
data = {"title": title, "content": content}
|
||||||
|
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||||
|
response = requests.post(url, data=data, headers=headers).json()
|
||||||
|
|
||||||
|
if response["ret"] == 0:
|
||||||
|
print("iGot 推送成功!")
|
||||||
|
else:
|
||||||
|
print(f'iGot 推送失败!{response["errMsg"]}')
|
||||||
|
|
||||||
|
|
||||||
|
def serverJ(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
通过 serverJ 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("PUSH_KEY"):
|
||||||
|
print("serverJ 服务的 PUSH_KEY 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("serverJ 服务启动")
|
||||||
|
|
||||||
|
data = {"text": title, "desp": content.replace("\n", "\n\n")}
|
||||||
|
if push_config.get("PUSH_KEY").index("SCT") != -1:
|
||||||
|
url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send'
|
||||||
|
else:
|
||||||
|
url = f'https://sc.ftqq.com/${push_config.get("PUSH_KEY")}.send'
|
||||||
|
response = requests.post(url, data=data).json()
|
||||||
|
|
||||||
|
if response.get("errno") == 0 or response.get("code") == 0:
|
||||||
|
print("serverJ 推送成功!")
|
||||||
|
else:
|
||||||
|
print(f'serverJ 推送失败!错误码:{response["message"]}')
|
||||||
|
|
||||||
|
|
||||||
|
def pushplus_bot(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
通过 push+ 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("PUSH_PLUS_TOKEN"):
|
||||||
|
print("PUSHPLUS 服务的 PUSH_PLUS_TOKEN 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("PUSHPLUS 服务启动")
|
||||||
|
|
||||||
|
url = "http://www.pushplus.plus/send"
|
||||||
data = {
|
data = {
|
||||||
"text": title,
|
"token": push_config.get("PUSH_PLUS_TOKEN"),
|
||||||
"desp": content.replace("\n", "\n\n")
|
"title": title,
|
||||||
|
"content": content,
|
||||||
|
"topic": push_config.get("PUSH_PLUS_USER"),
|
||||||
}
|
}
|
||||||
response = requests.post(f"https://sc.ftqq.com/{SCKEY}.send", data=data).json()
|
body = json.dumps(data).encode(encoding="utf-8")
|
||||||
if response['errno'] == 0:
|
headers = {"Content-Type": "application/json"}
|
||||||
print('推送成功!')
|
response = requests.post(url=url, data=body, headers=headers).json()
|
||||||
else:
|
|
||||||
print('推送失败!')
|
|
||||||
|
|
||||||
def telegram_bot(title, content):
|
if response["code"] == 200:
|
||||||
print("\n")
|
print("PUSHPLUS 推送成功!")
|
||||||
bot_token = TG_BOT_TOKEN
|
|
||||||
user_id = TG_USER_ID
|
else:
|
||||||
if not bot_token or not user_id:
|
|
||||||
print("tg服务的bot_token或者user_id未设置!!\n取消推送")
|
url_old = "http://pushplus.hxtrip.com/send"
|
||||||
|
headers["Accept"] = "application/json"
|
||||||
|
response = requests.post(url=url_old, data=body, headers=headers).json()
|
||||||
|
|
||||||
|
if response["code"] == 200:
|
||||||
|
print("PUSHPLUS(hxtrip) 推送成功!")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("PUSHPLUS 推送失败!")
|
||||||
|
|
||||||
|
|
||||||
|
def qmsg_bot(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 qmsg 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("QMSG_KEY") or not push_config.get("QMSG_TYPE"):
|
||||||
|
print("qmsg 的 QMSG_KEY 或者 QMSG_TYPE 未设置!!\n取消推送")
|
||||||
return
|
return
|
||||||
print("tg服务启动")
|
print("qmsg 服务启动")
|
||||||
url=f"https://api.telegram.org/bot{TG_BOT_TOKEN}/sendMessage"
|
|
||||||
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
|
||||||
payload = {'chat_id': str(TG_USER_ID), 'text': f'{title}\n\n{content}', 'disable_web_page_preview': 'true'}
|
|
||||||
proxies = None
|
|
||||||
if TG_PROXY_IP and TG_PROXY_PORT:
|
|
||||||
proxyStr = "http://{}:{}".format(TG_PROXY_IP, TG_PROXY_PORT)
|
|
||||||
proxies = {"http": proxyStr, "https": proxyStr}
|
|
||||||
response = requests.post(url=url, headers=headers, params=payload, proxies=proxies).json()
|
|
||||||
if response['ok']:
|
|
||||||
print('推送成功!')
|
|
||||||
else:
|
|
||||||
print('推送失败!')
|
|
||||||
|
|
||||||
def dingding_bot(title, content):
|
url = f'https://qmsg.zendee.cn/{push_config.get("QMSG_TYPE")}/{push_config.get("QMSG_KEY")}'
|
||||||
timestamp = str(round(time.time() * 1000)) # 时间戳
|
payload = {"msg": f'{title}\n\n{content.replace("----", "-")}'.encode("utf-8")}
|
||||||
secret_enc = DD_BOT_SECRET.encode('utf-8')
|
response = requests.post(url=url, params=payload).json()
|
||||||
string_to_sign = '{}\n{}'.format(timestamp, DD_BOT_SECRET)
|
|
||||||
string_to_sign_enc = string_to_sign.encode('utf-8')
|
|
||||||
hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
|
|
||||||
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code)) # 签名
|
|
||||||
print('开始使用 钉钉机器人 推送消息...', end='')
|
|
||||||
url = f'https://oapi.dingtalk.com/robot/send?access_token={DD_BOT_ACCESS_TOKEN}×tamp={timestamp}&sign={sign}'
|
|
||||||
headers = {'Content-Type': 'application/json;charset=utf-8'}
|
|
||||||
data = {
|
|
||||||
'msgtype': 'text',
|
|
||||||
'text': {'content': f'{title}\n\n{content}'}
|
|
||||||
}
|
|
||||||
response = requests.post(url=url, data=json.dumps(data), headers=headers, timeout=15).json()
|
|
||||||
if not response['errcode']:
|
|
||||||
print('推送成功!')
|
|
||||||
else:
|
|
||||||
print('推送失败!')
|
|
||||||
|
|
||||||
def qywxapp_bot(title, content):
|
if response["code"] == 0:
|
||||||
print("\n")
|
print("qmsg 推送成功!")
|
||||||
if not QYWX_APP:
|
else:
|
||||||
print("企业微信应用的QYWX_APP未设置!!\n取消推送")
|
print(f'qmsg 推送失败!{response["reason"]}')
|
||||||
|
|
||||||
|
|
||||||
|
def wecom_app(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
通过 企业微信 APP 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("QYWX_AM"):
|
||||||
|
print("QYWX_AM 未设置!!\n取消推送")
|
||||||
return
|
return
|
||||||
print("企业微信应用启动")
|
QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM"))
|
||||||
qywx_app_params = QYWX_APP.split(',')
|
if 4 < len(QYWX_AM_AY) > 5:
|
||||||
url='https://qyapi.weixin.qq.com/cgi-bin/gettoken'
|
print("QYWX_AM 设置错误!!\n取消推送")
|
||||||
headers= {
|
return
|
||||||
'Content-Type': 'application/json',
|
print("企业微信 APP 服务启动")
|
||||||
}
|
|
||||||
payload = {
|
|
||||||
'corpid': qywx_app_params[0],
|
|
||||||
'corpsecret': qywx_app_params[1],
|
|
||||||
}
|
|
||||||
response = requests.post(url=url, headers=headers, data=json.dumps(payload), timeout=15).json()
|
|
||||||
accesstoken = response["access_token"]
|
|
||||||
html = content.replace("\n", "<br/>")
|
|
||||||
|
|
||||||
options = None
|
corpid = QYWX_AM_AY[0]
|
||||||
if not qywx_app_params[4]:
|
corpsecret = QYWX_AM_AY[1]
|
||||||
options = {
|
touser = QYWX_AM_AY[2]
|
||||||
'msgtype': 'text',
|
agentid = QYWX_AM_AY[3]
|
||||||
'text': {
|
try:
|
||||||
content: f'{title}\n\n${content}'
|
media_id = QYWX_AM_AY[4]
|
||||||
}
|
except IndexError:
|
||||||
}
|
media_id = ""
|
||||||
elif qywx_app_params[4] == '0':
|
wx = WeCom(corpid, corpsecret, agentid)
|
||||||
options = {
|
# 如果没有配置 media_id 默认就以 text 方式发送
|
||||||
'msgtype': 'textcard',
|
if not media_id:
|
||||||
'textcard': {
|
message = title + "\n\n" + content
|
||||||
title: f'{title}',
|
response = wx.send_text(message, touser)
|
||||||
description: f'{content}',
|
|
||||||
btntxt: '更多'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elif qywx_app_params[4] == '1':
|
|
||||||
options = {
|
|
||||||
'msgtype': 'text',
|
|
||||||
'text': {
|
|
||||||
content: f'{title}\n\n${content}'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
options = {
|
response = wx.send_mpnews(title, content, media_id, touser)
|
||||||
'msgtype': 'mpnews',
|
|
||||||
'mpnews': {
|
if response == "ok":
|
||||||
'articles': [
|
print("企业微信推送成功!")
|
||||||
|
else:
|
||||||
|
print("企业微信推送失败!错误信息如下:\n", response)
|
||||||
|
|
||||||
|
|
||||||
|
class WeCom:
|
||||||
|
def __init__(self, corpid, corpsecret, agentid):
|
||||||
|
self.CORPID = corpid
|
||||||
|
self.CORPSECRET = corpsecret
|
||||||
|
self.AGENTID = agentid
|
||||||
|
|
||||||
|
def get_access_token(self):
|
||||||
|
url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
|
||||||
|
values = {
|
||||||
|
"corpid": self.CORPID,
|
||||||
|
"corpsecret": self.CORPSECRET,
|
||||||
|
}
|
||||||
|
req = requests.post(url, params=values)
|
||||||
|
data = json.loads(req.text)
|
||||||
|
return data["access_token"]
|
||||||
|
|
||||||
|
def send_text(self, message, touser="@all"):
|
||||||
|
send_url = (
|
||||||
|
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
|
||||||
|
+ self.get_access_token()
|
||||||
|
)
|
||||||
|
send_values = {
|
||||||
|
"touser": touser,
|
||||||
|
"msgtype": "text",
|
||||||
|
"agentid": self.AGENTID,
|
||||||
|
"text": {"content": message},
|
||||||
|
"safe": "0",
|
||||||
|
}
|
||||||
|
send_msges = bytes(json.dumps(send_values), "utf-8")
|
||||||
|
respone = requests.post(send_url, send_msges)
|
||||||
|
respone = respone.json()
|
||||||
|
return respone["errmsg"]
|
||||||
|
|
||||||
|
def send_mpnews(self, title, message, media_id, touser="@all"):
|
||||||
|
send_url = (
|
||||||
|
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
|
||||||
|
+ self.get_access_token()
|
||||||
|
)
|
||||||
|
send_values = {
|
||||||
|
"touser": touser,
|
||||||
|
"msgtype": "mpnews",
|
||||||
|
"agentid": self.AGENTID,
|
||||||
|
"mpnews": {
|
||||||
|
"articles": [
|
||||||
{
|
{
|
||||||
'title': f'{title}',
|
"title": title,
|
||||||
'thumb_media_id': f'{qywx_app_params[4]}',
|
"thumb_media_id": media_id,
|
||||||
'author': '智能助手',
|
"author": "Author",
|
||||||
'content_source_url': '',
|
"content_source_url": "",
|
||||||
'content': f'{html}',
|
"content": message.replace("\n", "<br/>"),
|
||||||
'digest': f'{content}'
|
"digest": message,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
send_msges = bytes(json.dumps(send_values), "utf-8")
|
||||||
|
respone = requests.post(send_url, send_msges)
|
||||||
|
respone = respone.json()
|
||||||
|
return respone["errmsg"]
|
||||||
|
|
||||||
url=f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={accesstoken}"
|
|
||||||
data = {
|
|
||||||
'touser': f'{change_user_id(content)}',
|
|
||||||
'agentid': f'{qywx_app_params[3]}',
|
|
||||||
'safe': '0'
|
|
||||||
}
|
|
||||||
data.update(options)
|
|
||||||
headers = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
response = requests.post(url=url, headers=headers, data=json.dumps(data)).json()
|
|
||||||
|
|
||||||
if response['errcode'] == 0:
|
def wecom_bot(title: str, content: str) -> None:
|
||||||
print('推送成功!')
|
|
||||||
else:
|
|
||||||
print('推送失败!')
|
|
||||||
|
|
||||||
def change_user_id(desp):
|
|
||||||
qywx_app_params = QYWX_APP.split(',')
|
|
||||||
if qywx_app_params[2]:
|
|
||||||
userIdTmp = qywx_app_params[2].split("|")
|
|
||||||
userId = ""
|
|
||||||
for i in range(len(userIdTmp)):
|
|
||||||
count1 = f"账号{i + 1}"
|
|
||||||
count2 = f"签到号{i + 1}"
|
|
||||||
if re.search(count1, desp) or re.search(count2, desp):
|
|
||||||
userId = userIdTmp[i]
|
|
||||||
if not userId:
|
|
||||||
userId = qywx_app_params[2]
|
|
||||||
return userId
|
|
||||||
else:
|
|
||||||
return "@all"
|
|
||||||
|
|
||||||
def send(title, content):
|
|
||||||
"""
|
"""
|
||||||
使用 bark, telegram bot, dingding bot, serverJ 发送手机推送
|
通过 企业微信机器人 推送消息。
|
||||||
:param title:
|
"""
|
||||||
:param content:
|
if not push_config.get("QYWX_KEY"):
|
||||||
|
print("企业微信机器人 服务的 QYWX_KEY 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("企业微信机器人服务启动")
|
||||||
|
|
||||||
|
url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={push_config.get('QYWX_KEY')}"
|
||||||
|
headers = {"Content-Type": "application/json;charset=utf-8"}
|
||||||
|
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
|
||||||
|
response = requests.post(
|
||||||
|
url=url, data=json.dumps(data), headers=headers, timeout=15
|
||||||
|
).json()
|
||||||
|
|
||||||
|
if response["errcode"] == 0:
|
||||||
|
print("企业微信机器人推送成功!")
|
||||||
|
else:
|
||||||
|
print("企业微信机器人推送失败!")
|
||||||
|
|
||||||
|
|
||||||
|
def telegram_bot(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 telegram 机器人 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("TG_BOT_TOKEN") or not push_config.get("TG_USER_ID"):
|
||||||
|
print("tg 服务的 bot_token 或者 user_id 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
print("tg 服务启动")
|
||||||
|
|
||||||
|
if push_config.get("TG_API_HOST"):
|
||||||
|
url = f"https://{push_config.get('TG_API_HOST')}/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
|
||||||
|
else:
|
||||||
|
url = (
|
||||||
|
f"https://api.telegram.org/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
|
||||||
|
)
|
||||||
|
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||||
|
payload = {
|
||||||
|
"chat_id": str(push_config.get("TG_USER_ID")),
|
||||||
|
"text": f"{title}\n\n{content}",
|
||||||
|
"disable_web_page_preview": "true",
|
||||||
|
}
|
||||||
|
proxies = None
|
||||||
|
if push_config.get("TG_PROXY_HOST") and push_config.get("TG_PROXY_PORT"):
|
||||||
|
if push_config.get("TG_PROXY_AUTH") is not None and "@" not in push_config.get(
|
||||||
|
"TG_PROXY_HOST"
|
||||||
|
):
|
||||||
|
push_config["TG_PROXY_HOST"] = (
|
||||||
|
push_config.get("TG_PROXY_AUTH")
|
||||||
|
+ "@"
|
||||||
|
+ push_config.get("TG_PROXY_HOST")
|
||||||
|
)
|
||||||
|
proxyStr = "http://{}:{}".format(
|
||||||
|
push_config.get("TG_PROXY_HOST"), push_config.get("TG_PROXY_PORT")
|
||||||
|
)
|
||||||
|
proxies = {"http": proxyStr, "https": proxyStr}
|
||||||
|
response = requests.post(
|
||||||
|
url=url, headers=headers, params=payload, proxies=proxies
|
||||||
|
).json()
|
||||||
|
|
||||||
|
if response["ok"]:
|
||||||
|
print("tg 推送成功!")
|
||||||
|
else:
|
||||||
|
print("tg 推送失败!")
|
||||||
|
|
||||||
|
|
||||||
|
def one() -> str:
|
||||||
|
"""
|
||||||
|
获取一条一言。
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
for i in notify_mode:
|
url = "https://v1.hitokoto.cn/"
|
||||||
if i == 'bark':
|
res = requests.get(url).json()
|
||||||
if BARK:
|
return res["hitokoto"] + " ----" + res["from"]
|
||||||
bark(title=title, content=content)
|
|
||||||
else:
|
|
||||||
print('未启用 bark')
|
if push_config.get("BARK_PUSH"):
|
||||||
continue
|
notify_function.append(bark)
|
||||||
if i == 'sc_key':
|
if push_config.get("CONSOLE"):
|
||||||
if SCKEY:
|
notify_function.append(console)
|
||||||
serverJ(title=title, content=content)
|
if push_config.get("DD_BOT_TOKEN") and push_config.get("DD_BOT_SECRET"):
|
||||||
else:
|
notify_function.append(dingding_bot)
|
||||||
print('未启用 Server酱')
|
if push_config.get("FSKEY"):
|
||||||
continue
|
notify_function.append(feishu_bot)
|
||||||
elif i == 'dingding_bot':
|
if push_config.get("GOBOT_URL") and push_config.get("GOBOT_QQ"):
|
||||||
if DD_BOT_ACCESS_TOKEN and DD_BOT_SECRET:
|
notify_function.append(go_cqhttp)
|
||||||
dingding_bot(title=title, content=content)
|
if push_config.get("GOTIFY_URL") and push_config.get("GOTIFY_TOKEN"):
|
||||||
else:
|
notify_function.append(gotify)
|
||||||
print('未启用 钉钉机器人')
|
if push_config.get("IGOT_PUSH_KEY"):
|
||||||
continue
|
notify_function.append(iGot)
|
||||||
elif i == 'telegram_bot':
|
if push_config.get("PUSH_KEY"):
|
||||||
if TG_BOT_TOKEN and TG_USER_ID:
|
notify_function.append(serverJ)
|
||||||
telegram_bot(title=title, content=content)
|
if push_config.get("PUSH_PLUS_TOKEN"):
|
||||||
else:
|
notify_function.append(pushplus_bot)
|
||||||
print('未启用 telegram机器人')
|
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
|
||||||
continue
|
notify_function.append(qmsg_bot)
|
||||||
elif i == 'qywxapp_bot':
|
if push_config.get("QYWX_AM"):
|
||||||
if QYWX_APP:
|
notify_function.append(wecom_app)
|
||||||
qywxapp_bot(title=title, content=content)
|
if push_config.get("QYWX_KEY"):
|
||||||
else:
|
notify_function.append(wecom_bot)
|
||||||
print('未启用 企业微信应用推送')
|
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
|
||||||
continue
|
notify_function.append(telegram_bot)
|
||||||
else:
|
|
||||||
print('此类推送方式不存在')
|
|
||||||
|
def send(title: str, content: str) -> None:
|
||||||
|
if not content:
|
||||||
|
print(f"{title} 推送内容为空!")
|
||||||
|
return
|
||||||
|
|
||||||
|
hitokoto = push_config.get("HITOKOTO")
|
||||||
|
|
||||||
|
text = one() if hitokoto else ""
|
||||||
|
content += "\n\n" + text
|
||||||
|
|
||||||
|
ts = [
|
||||||
|
threading.Thread(target=mode, args=(title, content), name=mode.__name__)
|
||||||
|
for mode in notify_function
|
||||||
|
]
|
||||||
|
[t.start() for t in ts]
|
||||||
|
[t.join() for t in ts]
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
send('title', 'content')
|
send("title", "content")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
requests
|
|
||||||
+6
-4
@@ -6,7 +6,7 @@ get_token() {
|
|||||||
|
|
||||||
add_cron_api() {
|
add_cron_api() {
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
if [ $# -eq 1 ]; then
|
if [[ $# -eq 1 ]]; then
|
||||||
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
|
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
|
||||||
local command=$(echo "$1" | awk -F ":" '{print $2}')
|
local command=$(echo "$1" | awk -F ":" '{print $2}')
|
||||||
local name=$(echo "$1" | awk -F ":" '{print $3}')
|
local name=$(echo "$1" | awk -F ":" '{print $3}')
|
||||||
@@ -39,7 +39,7 @@ add_cron_api() {
|
|||||||
|
|
||||||
update_cron_api() {
|
update_cron_api() {
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
if [ $# -eq 1 ]; then
|
if [[ $# -eq 1 ]]; then
|
||||||
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
|
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
|
||||||
local command=$(echo "$1" | awk -F ":" '{print $2}')
|
local command=$(echo "$1" | awk -F ":" '{print $2}')
|
||||||
local name=$(echo "$1" | awk -F ":" '{print $3}')
|
local name=$(echo "$1" | awk -F ":" '{print $3}')
|
||||||
@@ -75,7 +75,7 @@ update_cron_api() {
|
|||||||
|
|
||||||
update_cron_command_api() {
|
update_cron_command_api() {
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
if [ $# -eq 1 ]; then
|
if [[ $# -eq 1 ]]; then
|
||||||
local command=$(echo "$1" | awk -F ":" '{print $1}')
|
local command=$(echo "$1" | awk -F ":" '{print $1}')
|
||||||
local id=$(echo "$1" | awk -F ":" '{print $2}')
|
local id=$(echo "$1" | awk -F ":" '{print $2}')
|
||||||
else
|
else
|
||||||
@@ -153,6 +153,8 @@ update_cron() {
|
|||||||
local status="$2"
|
local status="$2"
|
||||||
local pid="${3:-''}"
|
local pid="${3:-''}"
|
||||||
local logPath="$4"
|
local logPath="$4"
|
||||||
|
local lastExecutingTime="${5:-0}"
|
||||||
|
local runningTime="${6:-0}"
|
||||||
local currentTimeStamp=$(date +%s)
|
local currentTimeStamp=$(date +%s)
|
||||||
local api=$(
|
local api=$(
|
||||||
curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons/status?t=$currentTimeStamp" \
|
curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons/status?t=$currentTimeStamp" \
|
||||||
@@ -164,7 +166,7 @@ update_cron() {
|
|||||||
-H "Origin: http://0.0.0.0:5700" \
|
-H "Origin: http://0.0.0.0:5700" \
|
||||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
-H "Referer: http://0.0.0.0:5700/crontab" \
|
||||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||||
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\"}" \
|
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
|
||||||
--compressed
|
--compressed
|
||||||
)
|
)
|
||||||
code=$(echo $api | jq -r .code)
|
code=$(echo $api | jq -r .code)
|
||||||
|
|||||||
+9
-5
@@ -11,10 +11,7 @@ apk --no-cache add -f zlib-dev gcc jpeg-dev python3-dev musl-dev freetype-dev
|
|||||||
echo -e "\nbot依赖安装成功...\n"
|
echo -e "\nbot依赖安装成功...\n"
|
||||||
|
|
||||||
echo -e "2、下载bot所需文件...\n"
|
echo -e "2、下载bot所需文件...\n"
|
||||||
if [ -d ${repo_path}/.git ]; then
|
if [[ ! -d ${repo_path}/.git ]]; then
|
||||||
reset_romote_url ${repo_path} "${url}"
|
|
||||||
git_pull_scripts ${repo_path}
|
|
||||||
else
|
|
||||||
rm -rf ${repo_path}
|
rm -rf ${repo_path}
|
||||||
git_clone_scripts ${url} ${repo_path} "main"
|
git_clone_scripts ${url} ${repo_path} "main"
|
||||||
fi
|
fi
|
||||||
@@ -30,8 +27,15 @@ if [[ $PipMirror ]]; then
|
|||||||
pip3 config set global.index-url $PipMirror
|
pip3 config set global.index-url $PipMirror
|
||||||
fi
|
fi
|
||||||
cp -f "$repo_path/jbot/requirements.txt" "$dir_root"
|
cp -f "$repo_path/jbot/requirements.txt" "$dir_root"
|
||||||
|
|
||||||
cd $dir_root
|
cd $dir_root
|
||||||
pip3 --default-timeout=100 install -r requirements.txt --no-cache-dir
|
cat requirements.txt | while read LREAD
|
||||||
|
do
|
||||||
|
if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
|
||||||
|
pip3 --default-timeout=100 install ${LREAD}
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
echo -e "\npython3依赖安装成功...\n"
|
echo -e "\npython3依赖安装成功...\n"
|
||||||
|
|
||||||
echo -e "4、启动bot程序...\n"
|
echo -e "4、启动bot程序...\n"
|
||||||
|
|||||||
+17
-25
@@ -3,7 +3,6 @@
|
|||||||
dir_shell=/ql/shell
|
dir_shell=/ql/shell
|
||||||
. $dir_shell/share.sh
|
. $dir_shell/share.sh
|
||||||
. $dir_shell/api.sh
|
. $dir_shell/api.sh
|
||||||
get_token
|
|
||||||
|
|
||||||
reset_env() {
|
reset_env() {
|
||||||
echo -e "---> 1. 开始检测配置文件\n"
|
echo -e "---> 1. 开始检测配置文件\n"
|
||||||
@@ -11,37 +10,40 @@ reset_env() {
|
|||||||
echo -e "---> 配置文件检测完成\n"
|
echo -e "---> 配置文件检测完成\n"
|
||||||
|
|
||||||
echo -e "---> 2. 开始安装青龙依赖\n"
|
echo -e "---> 2. 开始安装青龙依赖\n"
|
||||||
|
rm -rf $dir_root/node_modules
|
||||||
npm_install_2 $dir_root
|
npm_install_2 $dir_root
|
||||||
echo -e "---> 青龙依赖安装完成\n"
|
echo -e "---> 青龙依赖安装完成\n"
|
||||||
|
|
||||||
echo -e "---> 3. 开始安装脚本依赖\n"
|
echo -e "---> 3. 开始安装脚本依赖\n"
|
||||||
cp -f $dir_sample/package.json $dir_scripts/package.json
|
cp -f $dir_sample/package.json $dir_scripts/package.json
|
||||||
|
rm -rf $dir_scripts/node_modules
|
||||||
npm_install_2 $dir_scripts
|
npm_install_2 $dir_scripts
|
||||||
echo -e "---> 脚本依赖安装完成\n"
|
echo -e "---> 脚本依赖安装完成\n"
|
||||||
|
}
|
||||||
|
|
||||||
echo -e "---> 4. 复制通知文件\n"
|
copy_dep() {
|
||||||
|
echo -e "---> 1. 复制通知文件\n"
|
||||||
echo -e "---> 复制一份 $file_notify_py_sample 为 $file_notify_py\n"
|
echo -e "---> 复制一份 $file_notify_py_sample 为 $file_notify_py\n"
|
||||||
cp -fv $file_notify_py_sample $file_notify_py
|
cp -fv $file_notify_py_sample $file_notify_py
|
||||||
echo
|
echo
|
||||||
echo -e "---> 复制一份 $file_notify_js_sample 为 $file_notify_js\n"
|
echo -e "---> 复制一份 $file_notify_js_sample 为 $file_notify_js\n"
|
||||||
cp -fv $file_notify_js_sample $file_notify_js
|
cp -fv $file_notify_js_sample $file_notify_js
|
||||||
echo -e "---> 通知文件复制完成\n"
|
echo -e "---> 通知文件复制完成\n"
|
||||||
|
|
||||||
|
echo -e "---> 2. 复制nginx配置文件\n"
|
||||||
|
cp -fv $nginx_conf /etc/nginx/nginx.conf
|
||||||
|
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
|
||||||
|
echo -e "---> 配置文件复制完成\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
reload_pm2() {
|
reload_pm2() {
|
||||||
pm2 l >/dev/null 2>&1
|
pm2 l &>/dev/null
|
||||||
|
|
||||||
if [[ $(pm2 info panel 2>/dev/null) ]]; then
|
pm2 delete panel --source-map-support --time &>/dev/null
|
||||||
pm2 reload panel --source-map-support --time >/dev/null 2>&1
|
pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null
|
||||||
else
|
|
||||||
pm2 start $dir_root/build/app.js -n panel --source-map-support --time >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $(pm2 info schedule 2>/dev/null) ]]; then
|
pm2 delete schedule --source-map-support --time &>/dev/null
|
||||||
pm2 reload schedule --source-map-support --time >/dev/null 2>&1
|
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null
|
||||||
else
|
|
||||||
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pm2_log() {
|
pm2_log() {
|
||||||
@@ -57,6 +59,7 @@ check_nginx() {
|
|||||||
echo -e "=====> 检测nginx服务\n$nginxPid"
|
echo -e "=====> 检测nginx服务\n$nginxPid"
|
||||||
if [[ $nginxPid ]]; then
|
if [[ $nginxPid ]]; then
|
||||||
echo -e "\n=====> nginx服务正常\n"
|
echo -e "\n=====> nginx服务正常\n"
|
||||||
|
nginx -s reload
|
||||||
else
|
else
|
||||||
echo -e "\n=====> nginx服务异常,重新启动nginx\n"
|
echo -e "\n=====> nginx服务异常,重新启动nginx\n"
|
||||||
nginx -c /etc/nginx/nginx.conf
|
nginx -c /etc/nginx/nginx.conf
|
||||||
@@ -95,20 +98,9 @@ check_pm2() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
init_git() {
|
|
||||||
local dir_current=$(pwd)
|
|
||||||
|
|
||||||
cd $dir_root
|
|
||||||
git config --global user.email "qinglong@@users.noreply.github.com"
|
|
||||||
git config --global user.name "qinglong"
|
|
||||||
git config --global pull.rebase true
|
|
||||||
|
|
||||||
cd $dir_current
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
echo -e "=====> 开始检测"
|
echo -e "=====> 开始检测"
|
||||||
init_git
|
copy_dep
|
||||||
check_ql
|
check_ql
|
||||||
check_nginx
|
check_nginx
|
||||||
check_pm2
|
check_pm2
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ remove_js_log() {
|
|||||||
remove_empty_dir() {
|
remove_empty_dir() {
|
||||||
cd $dir_log
|
cd $dir_log
|
||||||
for dir in $(ls); do
|
for dir in $(ls); do
|
||||||
if [ -d $dir ] && [[ -z $(ls $dir) ]]; then
|
if [[ -d $dir ]] && [[ -z $(ls $dir) ]]; then
|
||||||
rm -rf $dir
|
rm -rf $dir
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
+64
-38
@@ -10,6 +10,7 @@ dir_repo=$dir_root/repo
|
|||||||
dir_raw=$dir_root/raw
|
dir_raw=$dir_root/raw
|
||||||
dir_log=$dir_root/log
|
dir_log=$dir_root/log
|
||||||
dir_db=$dir_root/db
|
dir_db=$dir_root/db
|
||||||
|
dir_dep=$dir_root/deps
|
||||||
dir_list_tmp=$dir_log/.tmp
|
dir_list_tmp=$dir_log/.tmp
|
||||||
dir_code=$dir_log/code
|
dir_code=$dir_log/code
|
||||||
dir_update_log=$dir_log/update
|
dir_update_log=$dir_log/update
|
||||||
@@ -32,6 +33,10 @@ file_notify_py_sample=$dir_sample/notify.py
|
|||||||
file_notify_py=$dir_scripts/notify.py
|
file_notify_py=$dir_scripts/notify.py
|
||||||
file_notify_js=$dir_scripts/sendNotify.js
|
file_notify_js=$dir_scripts/sendNotify.js
|
||||||
task_error_log_path=$dir_log/task_error.log
|
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
|
list_crontab_user=$dir_config/crontab.list
|
||||||
@@ -58,12 +63,18 @@ init_env() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
import_config() {
|
import_config() {
|
||||||
[ -f $file_config_user ] && . $file_config_user
|
[[ -f $file_config_user ]] && . $file_config_user
|
||||||
[ -f $file_env ] && . $file_env
|
[[ -f $file_env ]] && . $file_env
|
||||||
|
|
||||||
command_timeout_time=${CommandTimeoutTime:-"1h"}
|
command_timeout_time=${CommandTimeoutTime:-"1h"}
|
||||||
github_proxy_url=${GithubProxyUrl:-""}
|
github_proxy_url=${GithubProxyUrl:-""}
|
||||||
file_extensions=${RepoFileExtensions:-"js py"}
|
file_extensions=${RepoFileExtensions:-"js py"}
|
||||||
|
|
||||||
|
if [[ -n "${DefaultCronRule}" ]]; then
|
||||||
|
default_cron="${DefaultCronRule}"
|
||||||
|
else
|
||||||
|
default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
make_dir() {
|
make_dir() {
|
||||||
@@ -93,7 +104,7 @@ gen_random_num() {
|
|||||||
link_shell_sub() {
|
link_shell_sub() {
|
||||||
local link_path="$1"
|
local link_path="$1"
|
||||||
local original_path="$2"
|
local original_path="$2"
|
||||||
if [ ! -L $link_path ] || [[ $(readlink -f $link_path) != $original_path ]]; then
|
if [[ ! -L $link_path ]] || [[ $(readlink -f $link_path) != $original_path ]]; then
|
||||||
rm -f $link_path 2>/dev/null
|
rm -f $link_path 2>/dev/null
|
||||||
ln -sf $original_path $link_path
|
ln -sf $original_path $link_path
|
||||||
fi
|
fi
|
||||||
@@ -102,7 +113,7 @@ link_shell_sub() {
|
|||||||
link_shell() {
|
link_shell() {
|
||||||
if [[ $is_termux -eq 1 ]]; then
|
if [[ $is_termux -eq 1 ]]; then
|
||||||
local path="/data/data/com.termux/files/usr/bin/"
|
local path="/data/data/com.termux/files/usr/bin/"
|
||||||
elif [[ $PATH == */usr/local/bin* ]] && [ -d /usr/local/bin ]; then
|
elif [[ $PATH == */usr/local/bin* ]] && [[ -d /usr/local/bin ]]; then
|
||||||
local path="/usr/local/bin/"
|
local path="/usr/local/bin/"
|
||||||
else
|
else
|
||||||
local path=""
|
local path=""
|
||||||
@@ -117,16 +128,16 @@ link_shell() {
|
|||||||
|
|
||||||
define_cmd() {
|
define_cmd() {
|
||||||
local cmd_prefix cmd_suffix
|
local cmd_prefix cmd_suffix
|
||||||
if type task >/dev/null 2>&1; then
|
if type task &>/dev/null; then
|
||||||
cmd_suffix=""
|
cmd_suffix=""
|
||||||
if [ -x "$dir_shell/task.sh" ]; then
|
if [[ -f "$dir_shell/task.sh" ]]; then
|
||||||
cmd_prefix=""
|
cmd_prefix=""
|
||||||
else
|
else
|
||||||
cmd_prefix="bash "
|
cmd_prefix="bash "
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
cmd_suffix=".sh"
|
cmd_suffix=".sh"
|
||||||
if [ -x "$dir_shell/task.sh" ]; then
|
if [[ -f "$dir_shell/task.sh" ]]; then
|
||||||
cmd_prefix="$dir_shell/"
|
cmd_prefix="$dir_shell/"
|
||||||
else
|
else
|
||||||
cmd_prefix="bash $dir_shell/"
|
cmd_prefix="bash $dir_shell/"
|
||||||
@@ -146,69 +157,83 @@ fix_config() {
|
|||||||
make_dir $dir_repo
|
make_dir $dir_repo
|
||||||
make_dir $dir_raw
|
make_dir $dir_raw
|
||||||
make_dir $dir_update_log
|
make_dir $dir_update_log
|
||||||
|
make_dir $dir_dep
|
||||||
|
|
||||||
if [ ! -s $file_config_user ]; then
|
if [[ ! -s $file_config_user ]]; then
|
||||||
echo -e "复制一份 $file_config_sample 为 $file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
|
echo -e "复制一份 $file_config_sample 为 $file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
|
||||||
cp -fv $file_config_sample $file_config_user
|
cp -fv $file_config_sample $file_config_user
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f $file_env ]; then
|
if [[ ! -f $file_env ]]; then
|
||||||
echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n"
|
echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n"
|
||||||
touch $file_env
|
touch $file_env
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f $file_task_before ]; then
|
if [[ ! -f $file_task_before ]]; then
|
||||||
echo -e "复制一份 $file_task_sample 为 $file_task_before\n"
|
echo -e "复制一份 $file_task_sample 为 $file_task_before\n"
|
||||||
cp -fv $file_task_sample $file_task_before
|
cp -fv $file_task_sample $file_task_before
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f $file_task_after ]; then
|
if [[ ! -f $file_task_after ]]; then
|
||||||
echo -e "复制一份 $file_task_sample 为 $file_task_after\n"
|
echo -e "复制一份 $file_task_sample 为 $file_task_after\n"
|
||||||
cp -fv $file_task_sample $file_task_after
|
cp -fv $file_task_sample $file_task_after
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f $file_extra_shell ]; then
|
if [[ ! -f $file_extra_shell ]]; then
|
||||||
echo -e "复制一份 $file_extra_sample 为 $file_extra_shell\n"
|
echo -e "复制一份 $file_extra_sample 为 $file_extra_shell\n"
|
||||||
cp -fv $file_extra_sample $file_extra_shell
|
cp -fv $file_extra_sample $file_extra_shell
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -s $file_auth_user ]; then
|
if [[ ! -s $file_auth_user ]]; then
|
||||||
echo -e "复制一份 $file_auth_sample 为 $file_auth_user\n"
|
echo -e "复制一份 $file_auth_sample 为 $file_auth_user\n"
|
||||||
cp -fv $file_auth_sample $file_auth_user
|
cp -fv $file_auth_sample $file_auth_user
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -s $file_notify_py ]; then
|
if [[ ! -s $file_notify_py ]]; then
|
||||||
echo -e "复制一份 $file_notify_py_sample 为 $file_notify_py\n"
|
echo -e "复制一份 $file_notify_py_sample 为 $file_notify_py\n"
|
||||||
cp -fv $file_notify_py_sample $file_notify_py
|
cp -fv $file_notify_py_sample $file_notify_py
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -s $file_notify_js ]; then
|
if [[ ! -s $file_notify_js ]]; then
|
||||||
echo -e "复制一份 $file_notify_js_sample 为 $file_notify_js\n"
|
echo -e "复制一份 $file_notify_js_sample 为 $file_notify_js\n"
|
||||||
cp -fv $file_notify_js_sample $file_notify_js
|
cp -fv $file_notify_js_sample $file_notify_js
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -s /etc/nginx/conf.d/default.conf ]; then
|
if [[ -s /etc/nginx/conf.d/default.conf ]]; then
|
||||||
echo -e "检测到默认nginx配置文件,清空...\n"
|
echo -e "检测到默认nginx配置文件,清空...\n"
|
||||||
echo '' > /etc/nginx/conf.d/default.conf
|
cat /dev/null > /etc/nginx/conf.d/default.conf
|
||||||
echo
|
echo
|
||||||
fi
|
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() {
|
npm_install_sub() {
|
||||||
if [ $is_termux -eq 1 ]; then
|
if [ $is_termux -eq 1 ]; then
|
||||||
npm install --production --no-save --no-bin-links --registry=https://registry.npm.taobao.org || npm install --production --no-bin-links --no-save
|
npm install --production --no-bin-links --registry=https://registry.npm.taobao.org || npm install --production --no-bin-links
|
||||||
elif ! type pnpm >/dev/null 2>&1; then
|
elif ! type pnpm &>/dev/null; then
|
||||||
npm install --production --no-save --registry=https://registry.npm.taobao.org || npm install --production --no-save
|
npm install --production --registry=https://registry.npm.taobao.org || npm install --production
|
||||||
else
|
else
|
||||||
pnpm install --prod
|
pnpm install --production --registry=https://registry.npm.taobao.org || pnpm install --production
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +265,7 @@ npm_install_2() {
|
|||||||
diff_and_copy() {
|
diff_and_copy() {
|
||||||
local copy_source=$1
|
local copy_source=$1
|
||||||
local copy_to=$2
|
local copy_to=$2
|
||||||
if [ ! -s $copy_to ] || [[ $(diff $copy_source $copy_to) ]]; then
|
if [[ ! -s $copy_to ]] || [[ $(diff $copy_source $copy_to) ]]; then
|
||||||
cp -f $copy_source $copy_to
|
cp -f $copy_source $copy_to
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -248,17 +273,11 @@ diff_and_copy() {
|
|||||||
update_depend() {
|
update_depend() {
|
||||||
local dir_current=$(pwd)
|
local dir_current=$(pwd)
|
||||||
|
|
||||||
if [ ! -s $dir_scripts/package.json ] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
|
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
|
||||||
cp -f $dir_sample/package.json $dir_scripts/package.json
|
cp -f $dir_sample/package.json $dir_scripts/package.json
|
||||||
npm_install_2 $dir_scripts
|
npm_install_2 $dir_scripts
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -s $dir_scripts/requirements.txt ] || [[ $(diff $dir_sample/requirements.txt $dir_scripts/requirements.txt) ]]; then
|
|
||||||
cp -f $dir_sample/requirements.txt $dir_scripts/requirements.txt
|
|
||||||
cd $dir_scripts
|
|
||||||
pip3 install -r $dir_scripts/requirements.txt
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd $dir_current
|
cd $dir_current
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,9 +285,9 @@ git_clone_scripts() {
|
|||||||
local url=$1
|
local url=$1
|
||||||
local dir=$2
|
local dir=$2
|
||||||
local branch=$3
|
local branch=$3
|
||||||
[[ $branch ]] && local cmd="-b $branch "
|
[[ $branch ]] && local part_cmd="-b $branch "
|
||||||
echo -e "开始克隆仓库 $url 到 $dir\n"
|
echo -e "开始克隆仓库 $url 到 $dir\n"
|
||||||
git clone $cmd $url $dir
|
git clone $part_cmd $url $dir
|
||||||
exit_status=$?
|
exit_status=$?
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,12 +295,12 @@ git_pull_scripts() {
|
|||||||
local dir_current=$(pwd)
|
local dir_current=$(pwd)
|
||||||
local dir_work="$1"
|
local dir_work="$1"
|
||||||
local branch="$2"
|
local branch="$2"
|
||||||
[[ $branch ]] && local cmd="origin/${branch}"
|
[[ $branch ]] && local part_cmd="origin/${branch}"
|
||||||
cd $dir_work
|
cd $dir_work
|
||||||
echo -e "开始更新仓库:$dir_work\n"
|
echo -e "开始更新仓库:$dir_work\n"
|
||||||
git fetch --all
|
git fetch --all
|
||||||
exit_status=$?
|
exit_status=$?
|
||||||
git reset --hard $cmd
|
git reset --hard $part_cmd
|
||||||
git pull
|
git pull
|
||||||
cd $dir_current
|
cd $dir_current
|
||||||
}
|
}
|
||||||
@@ -292,20 +311,27 @@ reset_romote_url() {
|
|||||||
local url=$2
|
local url=$2
|
||||||
local branch="$3"
|
local branch="$3"
|
||||||
|
|
||||||
[[ $branch ]] && local cmd="origin/${branch}"
|
[[ $branch ]] && local part_cmd="origin/${branch}"
|
||||||
|
|
||||||
if [ -d "$dir_work/.git" ]; then
|
if [[ -d "$dir_work/.git" ]]; then
|
||||||
cd $dir_work
|
cd $dir_work
|
||||||
|
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
|
||||||
git remote set-url origin $url >/dev/null
|
git remote set-url origin $url >/dev/null
|
||||||
git reset --hard $cmd >/dev/null
|
git reset --hard $part_cmd >/dev/null
|
||||||
cd $dir_current
|
cd $dir_current
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
random_range() {
|
||||||
|
local beg=$1
|
||||||
|
local end=$2
|
||||||
|
echo $((RANDOM % ($end - $beg) + $beg))
|
||||||
|
}
|
||||||
|
|
||||||
init_env
|
init_env
|
||||||
detect_termux
|
detect_termux
|
||||||
detect_macos
|
detect_macos
|
||||||
define_cmd
|
define_cmd
|
||||||
|
fix_config
|
||||||
|
|
||||||
[ -f $task_error_log_path ] && rm $task_error_log_path
|
import_config $1 > $task_error_log_path 2>&1
|
||||||
import_config $1 >> $task_error_log_path 2>&1
|
|
||||||
|
|||||||
+187
-62
@@ -7,14 +7,14 @@ dir_shell=/ql/shell
|
|||||||
|
|
||||||
## 选择python3还是node
|
## 选择python3还是node
|
||||||
define_program() {
|
define_program() {
|
||||||
local p1=$1
|
local first_param=$1
|
||||||
if [[ $p1 == *.js ]]; then
|
if [[ $first_param == *.js ]]; then
|
||||||
which_program="node"
|
which_program="node"
|
||||||
elif [[ $p1 == *.py ]]; then
|
elif [[ $first_param == *.py ]]; then
|
||||||
which_program="python3"
|
which_program="python3"
|
||||||
elif [[ $p1 == *.sh ]]; then
|
elif [[ $first_param == *.sh ]]; then
|
||||||
which_program="bash"
|
which_program="bash"
|
||||||
elif [[ $p1 == *.ts ]]; then
|
elif [[ $first_param == *.ts ]]; then
|
||||||
which_program="ts-node-transpile-only"
|
which_program="ts-node-transpile-only"
|
||||||
else
|
else
|
||||||
which_program=""
|
which_program=""
|
||||||
@@ -39,7 +39,7 @@ gen_array_scripts() {
|
|||||||
local i="-1"
|
local i="-1"
|
||||||
cd $dir_scripts
|
cd $dir_scripts
|
||||||
for file in $(ls); do
|
for file in $(ls); do
|
||||||
if [ -f $file ] && [[ $file == *.js && $file != sendNotify.js ]]; then
|
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
|
||||||
let i++
|
let i++
|
||||||
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
|
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
|
||||||
array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1)
|
array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1)
|
||||||
@@ -54,9 +54,10 @@ usage() {
|
|||||||
define_cmd
|
define_cmd
|
||||||
gen_array_scripts
|
gen_array_scripts
|
||||||
echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:"
|
echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:"
|
||||||
echo -e "1.$cmd_task <file_name> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数"
|
echo -e "1.$cmd_task <file_name> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数"
|
||||||
echo -e "2.$cmd_task <file_name> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中"
|
echo -e "2.$cmd_task <file_name> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中"
|
||||||
echo -e "3.$cmd_task <file_name> conc <环境变量名> # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中"
|
echo -e "3.$cmd_task <file_name> conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行"
|
||||||
|
echo -e "4.$cmd_task <file_name> desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行"
|
||||||
if [[ ${#array_scripts[*]} -gt 0 ]]; then
|
if [[ ${#array_scripts[*]} -gt 0 ]]; then
|
||||||
echo -e "\n当前有以下脚本可以运行:"
|
echo -e "\n当前有以下脚本可以运行:"
|
||||||
for ((i = 0; i < ${#array_scripts[*]}; i++)); do
|
for ((i = 0; i < ${#array_scripts[*]}; i++)); do
|
||||||
@@ -75,82 +76,190 @@ run_nohup() {
|
|||||||
|
|
||||||
## 正常运行单个脚本,$1:传入参数
|
## 正常运行单个脚本,$1:传入参数
|
||||||
run_normal() {
|
run_normal() {
|
||||||
local p1=$1
|
local first_param=$1
|
||||||
cd $dir_scripts
|
define_program "$first_param"
|
||||||
define_program "$p1"
|
if [[ $first_param == *.js ]]; then
|
||||||
if [[ $p1 == *.js ]]; then
|
|
||||||
if [[ $# -eq 1 ]]; then
|
if [[ $# -eq 1 ]]; then
|
||||||
random_delay
|
random_delay
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
||||||
log_dir_tmp="${p1##*/}"
|
log_dir_tmp="${first_param##*/}"
|
||||||
|
log_dir_tmp_path="${first_param%%/*}"
|
||||||
|
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||||
|
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||||
log_dir="$dir_log/${log_dir_tmp%%.*}"
|
log_dir="$dir_log/${log_dir_tmp%%.*}"
|
||||||
log_path="$log_dir/$log_time.log"
|
log_path="$log_dir/$log_time.log"
|
||||||
|
cmd="&>> $log_path"
|
||||||
|
[[ "$show_log" == "true" ]] && cmd=""
|
||||||
make_dir "$log_dir"
|
make_dir "$log_dir"
|
||||||
|
|
||||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $p1" | perl -pe "s|.*ID=(.*) $cmd_task $p1\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
|
||||||
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
echo -e "## 开始执行... $begin_time\n" >> $log_path
|
local begin_timestamp=$(date "+%s" -d "$begin_time")
|
||||||
cat $task_error_log_path >> $log_path
|
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||||
|
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||||
|
|
||||||
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path"
|
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}')
|
||||||
. $file_task_before >> $log_path 2>&1
|
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||||
|
eval . $file_task_before "$@" $cmd
|
||||||
|
|
||||||
timeout -k 10s $command_timeout_time $which_program $p1 >> $log_path 2>&1
|
cd $dir_scripts
|
||||||
|
local relative_path="${first_param%/*}"
|
||||||
|
if [[ ! -z ${relative_path} ]] && [[ ${first_param} =~ "/" ]]; then
|
||||||
|
cd ${relative_path}
|
||||||
|
first_param=${first_param/$relative_path\//}
|
||||||
|
fi
|
||||||
|
eval timeout -k 10s $command_timeout_time $which_program $first_param $cmd
|
||||||
|
|
||||||
. $file_task_after >> $log_path 2>&1
|
eval . $file_task_after "$@" $cmd
|
||||||
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path"
|
|
||||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
local diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
|
local end_timestamp=$(date "+%s" -d "$end_time")
|
||||||
echo -e "\n## 执行结束... $end_time 耗时 $diff_time 秒" >> $log_path
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
|
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
|
||||||
run_concurrent() {
|
run_concurrent() {
|
||||||
local p1=$1
|
local first_param="$1"
|
||||||
local p3=$3
|
local env_param="$2"
|
||||||
if [[ ! $p3 ]]; then
|
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
|
||||||
|
if [[ ! $env_param ]]; then
|
||||||
echo -e "\n 缺少并发运行的环境变量参数"
|
echo -e "\n 缺少并发运行的环境变量参数"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd $dir_scripts
|
local envs=$(eval echo "\$${env_param}")
|
||||||
define_program "$p1"
|
local array=($(echo $envs | sed 's/&/ /g'))
|
||||||
|
local tempArr=$(echo $num_param | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
|
||||||
|
local runArr=($(eval echo $tempArr))
|
||||||
|
runArr=($(awk -v RS=' ' '!a[$1]++' <<< ${runArr[@]}))
|
||||||
|
|
||||||
|
local n=0
|
||||||
|
for i in ${runArr[@]}; do
|
||||||
|
array_run[n]=${array[$i - 1]}
|
||||||
|
let n++
|
||||||
|
done
|
||||||
|
|
||||||
|
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
|
||||||
|
|
||||||
|
define_program "$first_param"
|
||||||
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
||||||
log_dir_tmp="${p1##*/}"
|
log_dir_tmp="${first_param##*/}"
|
||||||
|
log_dir_tmp_path="${first_param%%/*}"
|
||||||
|
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||||
|
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||||
log_dir="$dir_log/${log_dir_tmp%%.*}"
|
log_dir="$dir_log/${log_dir_tmp%%.*}"
|
||||||
log_path="$log_dir/$log_time.log"
|
log_path="$log_dir/$log_time.log"
|
||||||
|
cmd="&>> $log_path"
|
||||||
|
[[ "$show_log" == "true" ]] && cmd=""
|
||||||
make_dir $log_dir
|
make_dir $log_dir
|
||||||
|
|
||||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $p1" | perl -pe "s|.*ID=(.*) $cmd_task $p1\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
|
||||||
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
echo -e "## 开始执行... $begin_time\n" >> $log_path
|
local begin_timestamp=$(date "+%s" -d "$begin_time")
|
||||||
cat $task_error_log_path >> $log_path
|
|
||||||
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path"
|
|
||||||
. $file_task_before >> $log_path 2>&1
|
|
||||||
|
|
||||||
local envs=$(eval echo "\$${p3}")
|
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||||
|
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||||
|
|
||||||
|
local id=$(cat $list_crontab_user | grep -E "$cmd_task $first_param" | perl -pe "s|.*ID=(.*) $cmd_task $first_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||||
|
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||||
|
eval . $file_task_before "$@" $cmd
|
||||||
|
|
||||||
|
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
|
||||||
|
|
||||||
|
local envs=$(eval echo "\$${env_param}")
|
||||||
local array=($(echo $envs | sed 's/&/ /g'))
|
local array=($(echo $envs | sed 's/&/ /g'))
|
||||||
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
|
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
|
||||||
|
|
||||||
|
cd $dir_scripts
|
||||||
|
local relative_path="${first_param%/*}"
|
||||||
|
if [[ ! -z ${relative_path} ]] && [[ ${first_param} =~ "/" ]]; then
|
||||||
|
cd ${relative_path}
|
||||||
|
first_param=${first_param/$relative_path\//}
|
||||||
|
fi
|
||||||
for i in "${!array[@]}"; do
|
for i in "${!array[@]}"; do
|
||||||
export ${p3}=${array[i]}
|
export ${env_param}=${array[i]}
|
||||||
single_log_path="$log_dir/${single_log_time}_$((i + 1)).log"
|
single_log_path="$log_dir/${single_log_time}_$((i + 1)).log"
|
||||||
timeout -k 10s $command_timeout_time $which_program $p1 &>$single_log_path 2>&1 &
|
timeout -k 10s $command_timeout_time $which_program $first_param &>$single_log_path &
|
||||||
done
|
done
|
||||||
|
|
||||||
wait
|
wait
|
||||||
for i in "${!array[@]}"; do
|
for i in "${!array[@]}"; do
|
||||||
single_log_path="$log_dir/${single_log_time}_$((i + 1)).log"
|
single_log_path="$log_dir/${single_log_time}_$((i + 1)).log"
|
||||||
cat $single_log_path >> $log_path
|
eval cat $single_log_path $cmd
|
||||||
[ -f $single_log_path ] && rm -f $single_log_path
|
[[ -f $single_log_path ]] && rm -f $single_log_path
|
||||||
done
|
done
|
||||||
|
|
||||||
. $file_task_after >> $log_path 2>&1
|
eval . $file_task_after "$@" $cmd
|
||||||
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path"
|
|
||||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
local diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
|
local end_timestamp=$(date "+%s" -d "$end_time")
|
||||||
echo -e "\n## 执行结束... $end_time 耗时 $diff_time 秒" >> $log_path
|
local diff_time=$(( $end_timestamp - $begin_timestamp ))
|
||||||
|
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||||
|
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
run_designated() {
|
||||||
|
local file_param="$1"
|
||||||
|
local env_param="$2"
|
||||||
|
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
|
||||||
|
if [[ ! $env_param ]] || [[ ! $num_param ]]; then
|
||||||
|
echo -e "\n 缺少单独运行的参数 task xxx.js desi Test 1 3"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
define_program "$file_param"
|
||||||
|
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
||||||
|
log_dir_tmp="${file_param##*/}"
|
||||||
|
log_dir_tmp_path="${file_param%%/*}"
|
||||||
|
log_dir_tmp_path="${log_dir_tmp_path##*/}"
|
||||||
|
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
|
||||||
|
log_dir="$dir_log/${log_dir_tmp%%.*}"
|
||||||
|
log_path="$log_dir/$log_time.log"
|
||||||
|
cmd="&>> $log_path"
|
||||||
|
[[ "$show_log" == "true" ]] && cmd=""
|
||||||
|
make_dir $log_dir
|
||||||
|
|
||||||
|
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
local begin_timestamp=$(date "+%s" -d "$begin_time")
|
||||||
|
|
||||||
|
local envs=$(eval echo "\$${env_param}")
|
||||||
|
local array=($(echo $envs | sed 's/&/ /g'))
|
||||||
|
local tempArr=$(echo $num_param | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
|
||||||
|
local runArr=($(eval echo $tempArr))
|
||||||
|
runArr=($(awk -v RS=' ' '!a[$1]++' <<< ${runArr[@]}))
|
||||||
|
|
||||||
|
local n=0
|
||||||
|
for i in ${runArr[@]}; do
|
||||||
|
array_run[n]=${array[$i - 1]}
|
||||||
|
let n++
|
||||||
|
done
|
||||||
|
|
||||||
|
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
|
||||||
|
|
||||||
|
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||||
|
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||||
|
|
||||||
|
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||||
|
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||||
|
eval . $file_task_before "$@" $cmd
|
||||||
|
|
||||||
|
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
|
||||||
|
|
||||||
|
cd $dir_scripts
|
||||||
|
local relative_path="${file_param%/*}"
|
||||||
|
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||||
|
cd ${relative_path}
|
||||||
|
file_param=${file_param/$relative_path\//}
|
||||||
|
fi
|
||||||
|
eval timeout -k 10s $command_timeout_time $which_program $file_param $cmd
|
||||||
|
|
||||||
|
eval . $file_task_after "$@" $cmd
|
||||||
|
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
local end_timestamp=$(date "+%s" -d "$end_time")
|
||||||
|
local diff_time=$(( $end_timestamp - $begin_timestamp ))
|
||||||
|
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||||
|
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
## 运行其他命令
|
## 运行其他命令
|
||||||
@@ -159,50 +268,66 @@ run_else() {
|
|||||||
local log_dir_tmp="${1##*/}"
|
local log_dir_tmp="${1##*/}"
|
||||||
local log_dir="$dir_log/${log_dir_tmp%%.*}"
|
local log_dir="$dir_log/${log_dir_tmp%%.*}"
|
||||||
log_path="$log_dir/$log_time.log"
|
log_path="$log_dir/$log_time.log"
|
||||||
|
cmd="&>> $log_path"
|
||||||
|
[[ "$show_log" == "true" ]] && cmd=""
|
||||||
make_dir "$log_dir"
|
make_dir "$log_dir"
|
||||||
|
|
||||||
local id=$(cat $list_crontab_user | grep -E "$cmd_task $p1" | perl -pe "s|.*ID=(.*) $cmd_task $p1\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
|
||||||
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
echo -e "## 开始执行... $begin_time\n" >> $log_path
|
local begin_timestamp=$(date "+%s" -d "$begin_time")
|
||||||
cat $task_error_log_path >> $log_path
|
|
||||||
|
|
||||||
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path"
|
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||||
. $file_task_before >> $log_path 2>&1
|
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||||
|
|
||||||
timeout -k 10s $command_timeout_time "$@" >> $log_path 2>&1
|
local id=$(cat $list_crontab_user | grep -E "$cmd_task $first_param" | perl -pe "s|.*ID=(.*) $cmd_task $first_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||||
|
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||||
|
eval . $file_task_before "$@" $cmd
|
||||||
|
|
||||||
. $file_task_after >> $log_path 2>&1
|
eval timeout -k 10s $command_timeout_time "$@" $cmd
|
||||||
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path"
|
|
||||||
|
eval . $file_task_after "$@" $cmd
|
||||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
local diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
|
local end_timestamp=$(date "+%s" -d "$end_time")
|
||||||
echo -e "\n## 执行结束... $end_time 耗时 $diff_time 秒" >> $log_path
|
local diff_time=$(( $end_timestamp - $begin_timestamp ))
|
||||||
|
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||||
|
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒" $cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
## 命令检测
|
## 命令检测
|
||||||
main() {
|
main() {
|
||||||
|
show_log="false"
|
||||||
|
while getopts ":l" opt
|
||||||
|
do
|
||||||
|
case $opt in
|
||||||
|
l)
|
||||||
|
show_log="true"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
|
||||||
|
|
||||||
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
|
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
|
||||||
case $# in
|
case $# in
|
||||||
1)
|
1)
|
||||||
run_normal $1
|
run_normal "$1"
|
||||||
;;
|
;;
|
||||||
2 | 3)
|
*)
|
||||||
case $2 in
|
case $2 in
|
||||||
now)
|
now)
|
||||||
run_normal $1 $2
|
run_normal "$1" "$2"
|
||||||
;;
|
;;
|
||||||
conc)
|
conc)
|
||||||
run_concurrent $1 $2 $3
|
run_concurrent "$1" "$3" "$*"
|
||||||
|
;;
|
||||||
|
desi)
|
||||||
|
run_designated "$1" "$3" "$*"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
run_else "$@"
|
run_else "$@"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
*)
|
|
||||||
run_else "$@"
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
cat $log_path
|
[[ -f $log_path ]] && cat $log_path
|
||||||
elif [[ $# -eq 0 ]]; then
|
elif [[ $# -eq 0 ]]; then
|
||||||
echo
|
echo
|
||||||
usage
|
usage
|
||||||
|
|||||||
+118
-82
@@ -12,12 +12,12 @@ diff_cron() {
|
|||||||
local list_task="$2"
|
local list_task="$2"
|
||||||
local list_add="$3"
|
local list_add="$3"
|
||||||
local list_drop="$4"
|
local list_drop="$4"
|
||||||
if [ -s $list_task ]; then
|
if [[ -s $list_task ]]; then
|
||||||
grep -vwf $list_task $list_scripts >$list_add
|
grep -vwf $list_task $list_scripts >$list_add
|
||||||
elif [ ! -s $list_task ] && [ -s $list_scripts ]; then
|
elif [[ ! -s $list_task ]] && [[ -s $list_scripts ]]; then
|
||||||
cp -f $list_scripts $list_add
|
cp -f $list_scripts $list_add
|
||||||
fi
|
fi
|
||||||
if [ -s $list_scripts ]; then
|
if [[ -s $list_scripts ]]; then
|
||||||
grep -vwf $list_scripts $list_task >$list_drop
|
grep -vwf $list_scripts $list_task >$list_drop
|
||||||
else
|
else
|
||||||
cp -f $list_task $list_drop
|
cp -f $list_task $list_drop
|
||||||
@@ -28,18 +28,18 @@ diff_cron() {
|
|||||||
detect_config_version() {
|
detect_config_version() {
|
||||||
## 识别出两个文件的版本号
|
## 识别出两个文件的版本号
|
||||||
ver_config_sample=$(grep " Version: " $file_config_sample | perl -pe "s|.+v((\d+\.?){3})|\1|")
|
ver_config_sample=$(grep " Version: " $file_config_sample | perl -pe "s|.+v((\d+\.?){3})|\1|")
|
||||||
[ -f $file_config_user ] && ver_config_user=$(grep " Version: " $file_config_user | perl -pe "s|.+v((\d+\.?){3})|\1|")
|
[[ -f $file_config_user ]] && ver_config_user=$(grep " Version: " $file_config_user | perl -pe "s|.+v((\d+\.?){3})|\1|")
|
||||||
|
|
||||||
## 删除旧的发送记录文件
|
## 删除旧的发送记录文件
|
||||||
[ -f $send_mark ] && [[ $(cat $send_mark) != $ver_config_sample ]] && rm -f $send_mark
|
[[ -f $send_mark ]] && [[ $(cat $send_mark) != $ver_config_sample ]] && rm -f $send_mark
|
||||||
|
|
||||||
## 识别出更新日期和更新内容
|
## 识别出更新日期和更新内容
|
||||||
update_date=$(grep " Date: " $file_config_sample | awk -F ": " '{print $2}')
|
update_date=$(grep " Date: " $file_config_sample | awk -F ": " '{print $2}')
|
||||||
update_content=$(grep " Update Content: " $file_config_sample | awk -F ": " '{print $2}')
|
update_content=$(grep " Update Content: " $file_config_sample | awk -F ": " '{print $2}')
|
||||||
|
|
||||||
## 如果是今天,并且版本号不一致,则发送通知
|
## 如果是今天,并且版本号不一致,则发送通知
|
||||||
if [ -f $file_config_user ] && [[ $ver_config_user != $ver_config_sample ]] && [[ $update_date == $(date "+%Y-%m-%d") ]]; then
|
if [[ -f $file_config_user ]] && [[ $ver_config_user != $ver_config_sample ]] && [[ $update_date == $(date "+%Y-%m-%d") ]]; then
|
||||||
if [ ! -f $send_mark ]; then
|
if [[ ! -f $send_mark ]]; then
|
||||||
local notify_title="配置文件更新通知"
|
local notify_title="配置文件更新通知"
|
||||||
local notify_content="更新日期: $update_date\n用户版本: $ver_config_user\n新的版本: $ver_config_sample\n更新内容: $update_content\n更新说明: 如需使用新功能请对照config.sample.sh,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。\n"
|
local notify_content="更新日期: $update_date\n用户版本: $ver_config_user\n新的版本: $ver_config_sample\n更新内容: $update_content\n更新说明: 如需使用新功能请对照config.sample.sh,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。\n"
|
||||||
echo -e $notify_content
|
echo -e $notify_content
|
||||||
@@ -47,7 +47,7 @@ detect_config_version() {
|
|||||||
[[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark
|
[[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
[ -f $send_mark ] && rm -f $send_mark
|
[[ -f $send_mark ]] && rm -f $send_mark
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ detect_config_version() {
|
|||||||
output_list_add_drop() {
|
output_list_add_drop() {
|
||||||
local list=$1
|
local list=$1
|
||||||
local type=$2
|
local type=$2
|
||||||
if [ -s $list ]; then
|
if [[ -s $list ]]; then
|
||||||
echo -e "检测到有$type的定时任务:\n"
|
echo -e "检测到有$type的定时任务:\n"
|
||||||
cat $list
|
cat $list
|
||||||
echo
|
echo
|
||||||
@@ -104,8 +104,9 @@ add_cron() {
|
|||||||
local detail=""
|
local detail=""
|
||||||
cd $dir_scripts
|
cd $dir_scripts
|
||||||
for file in $(cat $list_add); do
|
for file in $(cat $list_add); do
|
||||||
local file_name=${file/${path}\_/}
|
local file_name=${file/${path}\//}
|
||||||
if [ -f $file ]; then
|
file_name=${file_name/${path}\_/}
|
||||||
|
if [[ -f $file ]]; then
|
||||||
cron_line=$(
|
cron_line=$(
|
||||||
perl -ne "{
|
perl -ne "{
|
||||||
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/
|
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/
|
||||||
@@ -120,7 +121,7 @@ add_cron() {
|
|||||||
[[ -z $cron_name ]] && cron_name="$file_name"
|
[[ -z $cron_name ]] && cron_name="$file_name"
|
||||||
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
|
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
|
||||||
[[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
|
[[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
|
||||||
[[ -z $cron_line ]] && cron_line="0 6 * * *"
|
[[ -z $cron_line ]] && cron_line="$default_cron"
|
||||||
result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name")
|
result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name")
|
||||||
echo -e "$result"
|
echo -e "$result"
|
||||||
if [[ $detail ]]; then
|
if [[ $detail ]]; then
|
||||||
@@ -147,13 +148,15 @@ update_repo() {
|
|||||||
|
|
||||||
local repo_path="${dir_repo}/${uniq_path}"
|
local repo_path="${dir_repo}/${uniq_path}"
|
||||||
|
|
||||||
|
make_dir "${dir_scripts}/${uniq_path}"
|
||||||
|
|
||||||
local formatUrl="$url"
|
local formatUrl="$url"
|
||||||
if [[ $github_proxy_url ]]; then
|
if [[ $github_proxy_url ]]; then
|
||||||
if [[ $url =~ "github.com" ]] || [[ $url =~ "githubusercontent.com" ]]; then
|
if [[ $url =~ "github.com" ]] || [[ $url =~ "githubusercontent.com" ]]; then
|
||||||
[[ ! $url =~ "https://ghproxy.com" ]] && formatUrl="${github_proxy_url}${url}"
|
[[ ! $url =~ "https://ghproxy.com" ]] && formatUrl="${github_proxy_url}${url}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if [ -d ${repo_path}/.git ]; then
|
if [[ -d ${repo_path}/.git ]]; then
|
||||||
reset_romote_url ${repo_path} "${formatUrl}" "${branch}"
|
reset_romote_url ${repo_path} "${formatUrl}" "${branch}"
|
||||||
git_pull_scripts ${repo_path} "${branch}"
|
git_pull_scripts ${repo_path} "${branch}"
|
||||||
else
|
else
|
||||||
@@ -210,7 +213,7 @@ update_raw() {
|
|||||||
[[ -z $cron_name ]] && cron_name="$raw_file_name"
|
[[ -z $cron_name ]] && cron_name="$raw_file_name"
|
||||||
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
|
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
|
||||||
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
|
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
|
||||||
[[ -z $cron_line ]] && cron_line="0 6 * * *"
|
[[ -z $cron_line ]] && cron_line="$default_cron"
|
||||||
if [[ -z $cron_id ]]; then
|
if [[ -z $cron_id ]]; then
|
||||||
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name")
|
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name")
|
||||||
echo -e "$result\n"
|
echo -e "$result\n"
|
||||||
@@ -219,7 +222,7 @@ update_raw() {
|
|||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo -e "下载 ${raw_file_name} 失败,保留之前正常下载的版本...\n"
|
echo -e "下载 ${raw_file_name} 失败,保留之前正常下载的版本...\n"
|
||||||
[ -f "$dir_raw/${raw_file_name}.new" ] && rm -f "$dir_raw/${raw_file_name}.new"
|
[[ -f "$dir_raw/${raw_file_name}.new" ]] && rm -f "$dir_raw/${raw_file_name}.new"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -227,7 +230,7 @@ update_raw() {
|
|||||||
## 调用用户自定义的extra.sh
|
## 调用用户自定义的extra.sh
|
||||||
run_extra_shell() {
|
run_extra_shell() {
|
||||||
if [[ ${EnableExtraShell} == true ]]; then
|
if [[ ${EnableExtraShell} == true ]]; then
|
||||||
if [ -f $file_extra_shell ]; then
|
if [[ -f $file_extra_shell ]]; then
|
||||||
echo -e "--------------------------------------------------------------\n"
|
echo -e "--------------------------------------------------------------\n"
|
||||||
. $file_extra_shell
|
. $file_extra_shell
|
||||||
else
|
else
|
||||||
@@ -240,12 +243,14 @@ run_extra_shell() {
|
|||||||
usage() {
|
usage() {
|
||||||
echo -e "本脚本用法:"
|
echo -e "本脚本用法:"
|
||||||
echo -e "1. $cmd_update update # 更新并重启青龙"
|
echo -e "1. $cmd_update update # 更新并重启青龙"
|
||||||
echo -e "1. $cmd_update extra # 运行自定义脚本"
|
echo -e "2. $cmd_update extra # 运行自定义脚本"
|
||||||
echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
|
echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
|
||||||
echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> # 更新单个仓库的脚本"
|
echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> # 更新单个仓库的脚本"
|
||||||
echo -e "5. $cmd_update rmlog <days> # 删除旧日志"
|
echo -e "5. $cmd_update rmlog <days> # 删除旧日志"
|
||||||
echo -e "6. $cmd_update bot # 启动tg-bot"
|
echo -e "6. $cmd_update bot # 启动tg-bot"
|
||||||
echo -e "7. $cmd_update check # 检测青龙环境并修复"
|
echo -e "7. $cmd_update check # 检测青龙环境并修复"
|
||||||
|
echo -e "8. $cmd_update resetlet # 重置登录错误次数"
|
||||||
|
echo -e "9. $cmd_update resettfa # 禁用两步登录"
|
||||||
}
|
}
|
||||||
|
|
||||||
## 更新qinglong
|
## 更新qinglong
|
||||||
@@ -253,7 +258,7 @@ update_qinglong() {
|
|||||||
patch_version
|
patch_version
|
||||||
|
|
||||||
local no_restart="$1"
|
local no_restart="$1"
|
||||||
[ -f $dir_root/package.json ] && ql_depend_old=$(cat $dir_root/package.json)
|
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
|
||||||
reset_romote_url ${dir_root} "${github_proxy_url}https://github.com/whyour/qinglong.git" "master"
|
reset_romote_url ${dir_root} "${github_proxy_url}https://github.com/whyour/qinglong.git" "master"
|
||||||
git_pull_scripts $dir_root "master"
|
git_pull_scripts $dir_root "master"
|
||||||
|
|
||||||
@@ -263,14 +268,14 @@ update_qinglong() {
|
|||||||
detect_config_version
|
detect_config_version
|
||||||
update_depend
|
update_depend
|
||||||
|
|
||||||
[ -f $dir_root/package.json ] && ql_depend_new=$(cat $dir_root/package.json)
|
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
|
||||||
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
|
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
|
||||||
else
|
else
|
||||||
echo -e "\n更新$dir_root失败,请检查原因...\n"
|
echo -e "\n更新$dir_root失败,请检查原因...\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local url="${github_proxy_url}https://github.com/whyour/qinglong-static.git"
|
local url="${github_proxy_url}https://github.com/whyour/qinglong-static.git"
|
||||||
if [ -d ${ql_static_repo}/.git ]; then
|
if [[ -d ${ql_static_repo}/.git ]]; then
|
||||||
reset_romote_url ${ql_static_repo} ${url} "master"
|
reset_romote_url ${ql_static_repo} ${url} "master"
|
||||||
git_pull_scripts ${ql_static_repo} "master"
|
git_pull_scripts ${ql_static_repo} "master"
|
||||||
else
|
else
|
||||||
@@ -303,25 +308,21 @@ patch_version() {
|
|||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! type ts-node >/dev/null 2>&1; then
|
if ! type ts-node &>/dev/null; then
|
||||||
pnpm i -g ts-node typescript tslib
|
pnpm i -g ts-node typescript tslib
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
git config --global pull.rebase false
|
||||||
}
|
}
|
||||||
|
|
||||||
reload_pm2() {
|
reload_pm2() {
|
||||||
pm2 l >/dev/null 2>&1
|
pm2 l &>/dev/null
|
||||||
|
|
||||||
if [[ $(pm2 info panel 2>/dev/null) ]]; then
|
pm2 delete panel --source-map-support --time &>/dev/null
|
||||||
pm2 reload panel --source-map-support --time >/dev/null 2>&1
|
pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null
|
||||||
else
|
|
||||||
pm2 start $dir_root/build/app.js -n panel --source-map-support --time >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $(pm2 info schedule 2>/dev/null) ]]; then
|
pm2 delete schedule --source-map-support --time &>/dev/null
|
||||||
pm2 reload schedule --source-map-support --time >/dev/null 2>&1
|
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null
|
||||||
else
|
|
||||||
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
## 对比脚本
|
## 对比脚本
|
||||||
@@ -335,21 +336,20 @@ diff_scripts() {
|
|||||||
|
|
||||||
gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence"
|
gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence"
|
||||||
|
|
||||||
local repo="${repo_path##*/}"
|
local list_add="$dir_list_tmp/${uniq_path}_add.list"
|
||||||
local list_add="$dir_list_tmp/${repo}_add.list"
|
local list_drop="$dir_list_tmp/${uniq_path}_drop.list"
|
||||||
local list_drop="$dir_list_tmp/${repo}_drop.list"
|
diff_cron "$dir_list_tmp/${uniq_path}_scripts.list" "$dir_list_tmp/${uniq_path}_user.list" $list_add $list_drop
|
||||||
diff_cron "$dir_list_tmp/${repo}_scripts.list" "$dir_list_tmp/${repo}_user.list" $list_add $list_drop
|
|
||||||
|
|
||||||
if [ -s $list_drop ]; then
|
if [[ -s $list_drop ]]; then
|
||||||
output_list_add_drop $list_drop "失效"
|
output_list_add_drop $list_drop "失效"
|
||||||
if [[ ${AutoDelCron} == true ]]; then
|
if [[ ${AutoDelCron} == true ]]; then
|
||||||
del_cron $list_drop $repo
|
del_cron $list_drop $uniq_path
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if [ -s $list_add ]; then
|
if [[ -s $list_add ]]; then
|
||||||
output_list_add_drop $list_add "新"
|
output_list_add_drop $list_add "新"
|
||||||
if [[ ${AutoAddCron} == true ]]; then
|
if [[ ${AutoAddCron} == true ]]; then
|
||||||
add_cron $list_add $repo
|
add_cron $list_add $uniq_path
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
cd $dir_current
|
cd $dir_current
|
||||||
@@ -364,8 +364,7 @@ gen_list_repo() {
|
|||||||
local blackword="$4"
|
local blackword="$4"
|
||||||
local dependence="$5"
|
local dependence="$5"
|
||||||
|
|
||||||
local repo="${repo_path##*/}"
|
rm -f $dir_list_tmp/${uniq_path}*.list &>/dev/null
|
||||||
rm -f $dir_list_tmp/${repo}*.list >/dev/null 2>&1
|
|
||||||
|
|
||||||
cd ${repo_path}
|
cd ${repo_path}
|
||||||
|
|
||||||
@@ -386,43 +385,65 @@ gen_list_repo() {
|
|||||||
if [[ $blackword ]]; then
|
if [[ $blackword ]]; then
|
||||||
files=$(echo "$files" | egrep -v $blackword)
|
files=$(echo "$files" | egrep -v $blackword)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
|
||||||
|
cp -f $file_notify_py "${dir_scripts}/${uniq_path}"
|
||||||
|
|
||||||
if [[ $dependence ]]; then
|
if [[ $dependence ]]; then
|
||||||
|
cd ${repo_path}
|
||||||
results=$(eval $cmd | sed 's/^..//' | egrep $dependence)
|
results=$(eval $cmd | sed 's/^..//' | egrep $dependence)
|
||||||
for _file in ${results}; do
|
for _file in ${results}; do
|
||||||
file_path=$(dirname $_file)
|
file_path=$(dirname $_file)
|
||||||
make_dir "${dir_scripts}/${file_path}"
|
make_dir "${dir_scripts}/${uniq_path}/${file_path}"
|
||||||
cp -f $_file "${dir_scripts}/${file_path}"
|
cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -d $dir_dep ]]; then
|
||||||
|
cp -rf $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
for file in ${files}; do
|
for file in ${files}; do
|
||||||
filename=$(basename $file)
|
filename=$(basename $file)
|
||||||
cp -f $file $dir_scripts/${repo}_${filename}
|
cp -f $file "$dir_scripts/${uniq_path}/${filename}"
|
||||||
echo ${repo}_${filename} >>"$dir_list_tmp/${repo}_scripts.list"
|
echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
|
||||||
cron_id=$(cat $list_crontab_user | grep -E "$cmd_task ${author}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task ${author}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
cron_id=$(cat $list_crontab_user | grep -E "$cmd_task ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||||
if [[ $cron_id ]]; then
|
if [[ $cron_id ]]; then
|
||||||
result=$(update_cron_command_api "$cmd_task ${repo}_${filename}:$cron_id")
|
result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
grep -E "$cmd_task $repo" $list_crontab_user | perl -pe "s|.*ID=(.*) $cmd_task ($repo_.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${repo}_user.list"
|
grep -E "${cmd_task} ${uniq_path}" ${list_crontab_user} | perl -pe "s|.*ID=(.*) ${cmd_task} (${uniq_path}.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${uniq_path}_user.list"
|
||||||
cd $dir_current
|
cd $dir_current
|
||||||
}
|
}
|
||||||
|
|
||||||
get_uniq_path() {
|
get_uniq_path() {
|
||||||
local url="$1"
|
local url="$1"
|
||||||
local branch="$2"
|
local branch="$2"
|
||||||
local urlTmp="${url%*/}"
|
local urlTmp="${url%*/}"
|
||||||
local repoTmp="${urlTmp##*/}"
|
local repoTmp="${urlTmp##*/}"
|
||||||
local repo="${repoTmp%.*}"
|
local repo="${repoTmp%.*}"
|
||||||
local tmp="${url%/*}"
|
local tmp="${url%/*}"
|
||||||
local authorTmp1="${tmp##*/}"
|
local authorTmp1="${tmp##*/}"
|
||||||
local authorTmp2="${authorTmp1##*:}"
|
local authorTmp2="${authorTmp1##*:}"
|
||||||
local author="${authorTmp2##*.}"
|
local author="${authorTmp2##*.}"
|
||||||
|
|
||||||
uniq_path="${author}_${repo}"
|
uniq_path="${author}_${repo}"
|
||||||
[[ $branch ]] && uniq_path="${uniq_path}_${branch}"
|
[[ $branch ]] && uniq_path="${uniq_path}_${branch}"
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
|
## for ql update
|
||||||
|
show_log="false"
|
||||||
|
while getopts ":l" opt
|
||||||
|
do
|
||||||
|
case $opt in
|
||||||
|
l)
|
||||||
|
show_log="true"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
|
||||||
|
|
||||||
local p1=$1
|
local p1=$1
|
||||||
local p2=$2
|
local p2=$2
|
||||||
local p3=$3
|
local p3=$3
|
||||||
@@ -432,25 +453,28 @@ main() {
|
|||||||
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
|
||||||
local log_path="$dir_log/update/${log_time}_$p1.log"
|
local log_path="$dir_log/update/${log_time}_$p1.log"
|
||||||
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
case $p1 in
|
case $p1 in
|
||||||
update)
|
update)
|
||||||
echo -e "## 开始执行... $begin_time\n" >> $log_path
|
cmd=">> $log_path 2>&1"
|
||||||
cat $task_error_log_path >> $log_path
|
[[ "$show_log" == "true" ]] && cmd=""
|
||||||
update_qinglong "$2" >> $log_path
|
eval echo -e "## 开始执行... $begin_time\n" $cmd
|
||||||
|
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
|
||||||
|
eval update_qinglong "$2" $cmd
|
||||||
;;
|
;;
|
||||||
extra)
|
extra)
|
||||||
echo -e "## 开始执行... $begin_time\n" >> $log_path
|
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||||
cat $task_error_log_path >> $log_path
|
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
||||||
run_extra_shell >> $log_path
|
run_extra_shell >>$log_path
|
||||||
;;
|
;;
|
||||||
repo)
|
repo)
|
||||||
get_user_info
|
get_user_info
|
||||||
get_uniq_path "$p2" "$p6"
|
get_uniq_path "$p2" "$p6"
|
||||||
log_path="$dir_log/update/${log_time}_${uniq_path}.log"
|
log_path="$dir_log/update/${log_time}_${uniq_path}.log"
|
||||||
echo -e "## 开始执行... $begin_time\n" >> $log_path
|
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||||
cat $task_error_log_path >> $log_path
|
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
||||||
if [[ -n $p2 ]]; then
|
if [[ -n $p2 ]]; then
|
||||||
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" >> $log_path
|
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" >>$log_path
|
||||||
else
|
else
|
||||||
echo -e "命令输入错误...\n"
|
echo -e "命令输入错误...\n"
|
||||||
usage
|
usage
|
||||||
@@ -460,29 +484,41 @@ main() {
|
|||||||
get_user_info
|
get_user_info
|
||||||
get_uniq_path "$p2"
|
get_uniq_path "$p2"
|
||||||
log_path="$dir_log/update/${log_time}_${uniq_path}.log"
|
log_path="$dir_log/update/${log_time}_${uniq_path}.log"
|
||||||
echo -e "## 开始执行... $begin_time\n" >> $log_path
|
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||||
cat $task_error_log_path >> $log_path
|
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
||||||
if [[ -n $p2 ]]; then
|
if [[ -n $p2 ]]; then
|
||||||
update_raw "$p2" >> $log_path
|
update_raw "$p2" >>$log_path
|
||||||
else
|
else
|
||||||
echo -e "命令输入错误...\n"
|
echo -e "命令输入错误...\n"
|
||||||
usage
|
usage
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
rmlog)
|
rmlog)
|
||||||
echo -e "## 开始执行... $begin_time\n" >> $log_path
|
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||||
cat $task_error_log_path >> $log_path
|
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
||||||
. $dir_shell/rmlog.sh "$p2" >> $log_path
|
. $dir_shell/rmlog.sh "$p2" >>$log_path
|
||||||
;;
|
;;
|
||||||
bot)
|
bot)
|
||||||
echo -e "## 开始执行... $begin_time\n" >> $log_path
|
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||||
cat $task_error_log_path >> $log_path
|
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
||||||
. $dir_shell/bot.sh >> $log_path
|
. $dir_shell/bot.sh >>$log_path
|
||||||
;;
|
;;
|
||||||
check)
|
check)
|
||||||
echo -e "## 开始执行... $begin_time\n" >> $log_path
|
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||||
cat $task_error_log_path >> $log_path
|
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
|
||||||
. $dir_shell/check.sh >> $log_path
|
. $dir_shell/check.sh >>$log_path
|
||||||
|
;;
|
||||||
|
resetlet)
|
||||||
|
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||||
|
auth_value=$(cat $file_auth_user | jq '.retries =0' -c)
|
||||||
|
echo -e "重置登录错误次数成功 \n $auth_value" >>$log_path
|
||||||
|
echo "$auth_value" >$file_auth_user
|
||||||
|
;;
|
||||||
|
resettfa)
|
||||||
|
echo -e "## 开始执行... $begin_time\n" >>$log_path
|
||||||
|
auth_value=$(cat $file_auth_user | jq '.twoFactorActivated =false' | jq '.twoFactorActived =false' -c)
|
||||||
|
echo -e "禁用两步验证成功 \n $auth_value" >>$log_path
|
||||||
|
echo "$auth_value" >$file_auth_user
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo -e "命令输入错误...\n"
|
echo -e "命令输入错误...\n"
|
||||||
@@ -491,7 +527,7 @@ main() {
|
|||||||
esac
|
esac
|
||||||
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
local diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
|
local diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
|
||||||
echo -e "\n## 执行结束... $end_time 耗时 $diff_time 秒" >> $log_path
|
echo -e "\n## 执行结束... $end_time 耗时 $diff_time 秒" >>$log_path
|
||||||
cat $log_path
|
cat $log_path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-42
@@ -1,42 +0,0 @@
|
|||||||
import { history } from 'umi';
|
|
||||||
import { request } from '@/utils/http';
|
|
||||||
import config from '@/utils/config';
|
|
||||||
|
|
||||||
const titleMap: any = {
|
|
||||||
'/': '控制面板',
|
|
||||||
'/login': '登录',
|
|
||||||
'/crontab': '定时任务',
|
|
||||||
'/env': '环境变量',
|
|
||||||
'/config': '配置文件',
|
|
||||||
'/script': '脚本管理',
|
|
||||||
'/diff': '对比工具',
|
|
||||||
'/log': '任务日志',
|
|
||||||
'/setting': '系统设置',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function render(oldRender: any) {
|
|
||||||
request
|
|
||||||
.get(`${config.apiPrefix}user`)
|
|
||||||
.then((data) => {
|
|
||||||
if (data.data && data.data.username) {
|
|
||||||
return oldRender();
|
|
||||||
}
|
|
||||||
localStorage.removeItem(config.authKey);
|
|
||||||
history.push('/login');
|
|
||||||
oldRender();
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
if (e.response && e.response.status === 401) {
|
|
||||||
localStorage.removeItem(config.authKey);
|
|
||||||
history.push('/login');
|
|
||||||
oldRender();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onRouteChange({ matchedRoutes }: any) {
|
|
||||||
if (matchedRoutes.length) {
|
|
||||||
const path: string = matchedRoutes[matchedRoutes.length - 1].route.path;
|
|
||||||
document.title = `${titleMap[path]} - 控制面板`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { PageLoading } from '@ant-design/pro-layout';
|
||||||
|
|
||||||
|
const NewPageLoading = () => {
|
||||||
|
return <PageLoading delay={1}></PageLoading>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewPageLoading;
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
FolderOutlined,
|
FolderOutlined,
|
||||||
RadiusSettingOutlined,
|
RadiusSettingOutlined,
|
||||||
ControlOutlined,
|
ControlOutlined,
|
||||||
|
ContainerOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -18,6 +19,12 @@ export default {
|
|||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
component: '@/pages/login/index',
|
component: '@/pages/login/index',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '初始化',
|
||||||
|
path: '/initialization',
|
||||||
|
hideInMenu: true,
|
||||||
|
component: '@/pages/initialization/index',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/crontab',
|
path: '/crontab',
|
||||||
name: '定时任务',
|
name: '定时任务',
|
||||||
@@ -42,6 +49,12 @@ export default {
|
|||||||
icon: <FormOutlined />,
|
icon: <FormOutlined />,
|
||||||
component: '@/pages/script/index',
|
component: '@/pages/script/index',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/dependence',
|
||||||
|
name: '依赖管理',
|
||||||
|
icon: <ContainerOutlined />,
|
||||||
|
component: '@/pages/dependence/index',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/diff',
|
path: '/diff',
|
||||||
name: '对比工具',
|
name: '对比工具',
|
||||||
@@ -62,12 +75,9 @@ export default {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
location: {
|
|
||||||
pathname: '/',
|
|
||||||
},
|
|
||||||
navTheme: 'light',
|
navTheme: 'light',
|
||||||
fixSiderbar: true,
|
fixSiderbar: true,
|
||||||
contentWidth: 'Fixed',
|
contentWidth: 'Fixed',
|
||||||
splitMenus: false,
|
splitMenus: false,
|
||||||
logo: 'https://qinglong.whyour.cn/qinglong.png',
|
siderWidth: 180,
|
||||||
} as any;
|
} as any;
|
||||||
|
|||||||
+111
-53
@@ -1,3 +1,4 @@
|
|||||||
|
@import '~antd/lib/style/themes/default.less';
|
||||||
@import '~@/styles/variable.less';
|
@import '~@/styles/variable.less';
|
||||||
@import '~codemirror/lib/codemirror.css';
|
@import '~codemirror/lib/codemirror.css';
|
||||||
|
|
||||||
@@ -8,13 +9,6 @@
|
|||||||
url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype');
|
url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
|
||||||
height: 100%;
|
|
||||||
overflow-y: hidden;
|
|
||||||
background-color: rgb(248, 248, 248);
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-modal {
|
.log-modal {
|
||||||
.ant-modal {
|
.ant-modal {
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
@@ -51,8 +45,7 @@ body {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
.ant-pro-page-container-children-content {
|
.ant-pro-page-container-children-content {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
height: calc(100vh - 96px);
|
height: 100%;
|
||||||
height: calc(100vh - var(--vh-offset, 0px) - 96px);
|
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
@@ -77,7 +70,7 @@ body {
|
|||||||
|
|
||||||
.log-wrapper {
|
.log-wrapper {
|
||||||
.log-select {
|
.log-select {
|
||||||
width: 300px;
|
width: 250px;
|
||||||
}
|
}
|
||||||
.ant-page-header-heading-left {
|
.ant-page-header-heading-left {
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
@@ -121,16 +114,6 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input:-webkit-autofill,
|
|
||||||
input:-webkit-autofill:hover,
|
|
||||||
input:-webkit-autofill:focus,
|
|
||||||
input:-webkit-autofill:active {
|
|
||||||
transition-delay: 99999s;
|
|
||||||
transition: color 99999s ease-out, background-color 99999s ease-out;
|
|
||||||
-webkit-transition-delay: 99999s;
|
|
||||||
-webkit-transition: color 99999s ease-out, background-color 99999s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-pro-page-container.ql-container-wrapper {
|
.ant-pro-page-container.ql-container-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -139,8 +122,7 @@ input:-webkit-autofill:active {
|
|||||||
.ant-pro-grid-content.wide {
|
.ant-pro-grid-content.wide {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
.ant-pro-grid-content-children {
|
.ant-pro-grid-content-children {
|
||||||
height: calc(100% - 48px);
|
height: calc(100% - 36px);
|
||||||
height: calc(100% - var(--vh-offset, 0px) - 48px);
|
|
||||||
> div,
|
> div,
|
||||||
.log-container,
|
.log-container,
|
||||||
.react-codemirror2,
|
.react-codemirror2,
|
||||||
@@ -152,22 +134,11 @@ input:-webkit-autofill:active {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.ant-pro-grid-content.wide {
|
|
||||||
.ant-pro-page-container-children-content {
|
|
||||||
height: calc(100vh - 144px);
|
|
||||||
height: calc(100vh - var(--vh-offset, 0px) - 144px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ql-container-wrapper {
|
.ql-container-wrapper {
|
||||||
&.crontab-wrapper,
|
&.crontab-wrapper,
|
||||||
&.log-wrapper,
|
&.log-wrapper,
|
||||||
&.env-wrapper,
|
&.env-wrapper,
|
||||||
&.config-wrapper {
|
&.config-wrapper {
|
||||||
.ant-pro-grid-content.wide .ant-pro-page-container-children-content {
|
|
||||||
height: calc(100vh - 184px);
|
|
||||||
height: calc(100vh - var(--vh-offset, 0px) - 184px);
|
|
||||||
}
|
|
||||||
.CodeMirror {
|
.CodeMirror {
|
||||||
width: calc(100vw - 80px);
|
width: calc(100vw - 80px);
|
||||||
}
|
}
|
||||||
@@ -181,17 +152,12 @@ input:-webkit-autofill:active {
|
|||||||
|
|
||||||
.ant-layout-content.ant-pro-basicLayout-content.ant-pro-basicLayout-has-header {
|
.ant-layout-content.ant-pro-basicLayout-content.ant-pro-basicLayout-has-header {
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
}
|
min-height: calc(100vh - 72px);
|
||||||
|
min-height: calc(100vh - var(--vh-offset, 0px) - 72px);
|
||||||
@media (min-width: 768px) {
|
|
||||||
.ant-layout-content.ant-pro-basicLayout-content.ant-pro-basicLayout-has-header {
|
|
||||||
min-height: calc(100vh - 72px);
|
|
||||||
min-height: calc(100vh - var(--vh-offset, 0px) - 72px);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.Resizer {
|
.Resizer {
|
||||||
background: #000;
|
background: #fff;
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
-moz-box-sizing: border-box;
|
-moz-box-sizing: border-box;
|
||||||
@@ -200,6 +166,8 @@ input:-webkit-autofill:active {
|
|||||||
-moz-background-clip: padding;
|
-moz-background-clip: padding;
|
||||||
-webkit-background-clip: padding;
|
-webkit-background-clip: padding;
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
|
border-left: 5px solid rgba(42, 161, 255, 0);
|
||||||
|
border-right: 5px solid rgba(42, 161, 255, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Resizer:hover {
|
.Resizer:hover {
|
||||||
@@ -210,29 +178,22 @@ input:-webkit-autofill:active {
|
|||||||
.Resizer.horizontal {
|
.Resizer.horizontal {
|
||||||
height: 11px;
|
height: 11px;
|
||||||
margin: -5px 0;
|
margin: -5px 0;
|
||||||
border-top: 5px solid rgba(255, 255, 255, 0);
|
|
||||||
border-bottom: 5px solid rgba(255, 255, 255, 0);
|
|
||||||
cursor: row-resize;
|
cursor: row-resize;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Resizer.horizontal:hover {
|
|
||||||
border-top: 5px solid rgba(0, 0, 0, 0.5);
|
|
||||||
border-bottom: 5px solid rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.Resizer.vertical {
|
.Resizer.vertical {
|
||||||
width: 11px;
|
width: 11px;
|
||||||
margin: 0 -5px;
|
margin: 0 -5px;
|
||||||
border-left: 5px solid rgba(255, 255, 255, 0);
|
|
||||||
border-right: 5px solid rgba(255, 255, 255, 0);
|
|
||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Resizer.horizontal:hover,
|
||||||
.Resizer.vertical:hover {
|
.Resizer.vertical:hover {
|
||||||
border-left: 5px solid rgba(0, 0, 0, 0.5);
|
border-left-color: rgba(42, 161, 255, 0.5);
|
||||||
border-right: 5px solid rgba(0, 0, 0, 0.5);
|
border-right-color: rgba(42, 161, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Resizer.disabled {
|
.Resizer.disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
@@ -246,10 +207,107 @@ input:-webkit-autofill:active {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-message .ant-statistic {
|
.inline-countdown.ant-statistic {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
.ant-statistic-content {
|
.ant-statistic-content {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 0 3px;
|
padding: 0 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-form-item-extra {
|
||||||
|
word-break: break-all;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-dark='true'] {
|
||||||
|
/* Change autocomplete styles in WebKit */
|
||||||
|
input:-webkit-autofill,
|
||||||
|
input:-webkit-autofill:hover,
|
||||||
|
input:-webkit-autofill:focus,
|
||||||
|
textarea:-webkit-autofill,
|
||||||
|
textarea:-webkit-autofill:hover,
|
||||||
|
textarea:-webkit-autofill:focus,
|
||||||
|
select:-webkit-autofill,
|
||||||
|
select:-webkit-autofill:hover,
|
||||||
|
select:-webkit-autofill:focus {
|
||||||
|
border: 1px solid @border-color-base;
|
||||||
|
box-shadow: none;
|
||||||
|
transition: background-color 5000s ease-in-out 0s;
|
||||||
|
-webkit-text-fill-color: @text-color;
|
||||||
|
caret-color: #e8e6f3 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::placeholder {
|
||||||
|
opacity: 0.5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-selection-placeholder {
|
||||||
|
opacity: 0.5 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-pro-page-container-children-content {
|
||||||
|
margin: 18px 18px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-pro-basicLayout-content {
|
||||||
|
margin: 18px;
|
||||||
|
.ant-pro-page-container {
|
||||||
|
margin: -18px -18px -18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu-item.ant-pro-sider-collapsed-button {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-menu-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
|
||||||
|
.side-menu-collapse-button:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-menu-user-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-menu-user-drop-menu {
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
padding: 4px 0;
|
||||||
|
text-align: left;
|
||||||
|
background-clip: padding-box;
|
||||||
|
outline: none;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 6px 16px -8px rgb(0 0 0 / 8%), 0 9px 28px 0 rgb(0 0 0 / 5%),
|
||||||
|
0 12px 48px 16px rgb(0 0 0 / 3%);
|
||||||
|
.ant-dropdown-menu-item:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+256
-45
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import ProLayout from '@ant-design/pro-layout';
|
import ProLayout, { PageLoading } from '@ant-design/pro-layout';
|
||||||
import {
|
import {
|
||||||
enable as enableDarkMode,
|
enable as enableDarkMode,
|
||||||
disable as disableDarkMode,
|
disable as disableDarkMode,
|
||||||
@@ -8,14 +8,41 @@ import {
|
|||||||
} from 'darkreader';
|
} from 'darkreader';
|
||||||
import defaultProps from './defaultProps';
|
import defaultProps from './defaultProps';
|
||||||
import { Link, history } from 'umi';
|
import { Link, history } from 'umi';
|
||||||
import { LogoutOutlined } from '@ant-design/icons';
|
import {
|
||||||
|
LogoutOutlined,
|
||||||
|
MenuFoldOutlined,
|
||||||
|
MenuUnfoldOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
import vhCheck from 'vh-check';
|
import vhCheck from 'vh-check';
|
||||||
import { version, changeLog } from '../version';
|
import { version, changeLogLink, changeLog } from '../version';
|
||||||
|
import { useCtx, useTheme } from '@/utils/hooks';
|
||||||
|
import { message, Badge, Modal, Avatar, Dropdown, Menu, 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',
|
||||||
|
integrations: [new Integrations.BrowserTracing()],
|
||||||
|
release: version,
|
||||||
|
tracesSampleRate: 1.0,
|
||||||
|
});
|
||||||
|
|
||||||
export default function (props: any) {
|
export default function (props: any) {
|
||||||
|
const ctx = useCtx();
|
||||||
|
const theme = useTheme();
|
||||||
|
const [user, setUser] = useState<any>();
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
const [systemInfo, setSystemInfo] = useState<{ isInitialized: boolean }>();
|
||||||
|
const ws = useRef<any>(null);
|
||||||
|
const [socketMessage, setSocketMessage] = useState<any>();
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
request.post(`${config.apiPrefix}logout`).then(() => {
|
request.post(`${config.apiPrefix}logout`).then(() => {
|
||||||
localStorage.removeItem(config.authKey);
|
localStorage.removeItem(config.authKey);
|
||||||
@@ -23,45 +50,166 @@ export default function (props: any) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const getSystemInfo = () => {
|
||||||
const isAuth = localStorage.getItem(config.authKey);
|
request
|
||||||
if (!isAuth) {
|
.get(`${config.apiPrefix}system`)
|
||||||
history.push('/login');
|
.then(({ code, data }) => {
|
||||||
}
|
if (code === 200) {
|
||||||
vhCheck();
|
setSystemInfo(data);
|
||||||
|
if (!data.isInitialized) {
|
||||||
|
history.push('/initialization');
|
||||||
|
setLoading(false);
|
||||||
|
} else {
|
||||||
|
getUser();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// patch custome layout title as react node [object, object]
|
const getUser = (needLoading = true) => {
|
||||||
document.title = '控制面板';
|
needLoading && setLoading(true);
|
||||||
|
request
|
||||||
|
.get(`${config.apiPrefix}user`)
|
||||||
|
.then(({ code, data }) => {
|
||||||
|
if (code === 200 && data.username) {
|
||||||
|
setUser(data);
|
||||||
|
if (props.location.pathname === '/') {
|
||||||
|
history.push('/crontab');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error(data);
|
||||||
|
}
|
||||||
|
needLoading && setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const reloadUser = () => {
|
||||||
|
getUser(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setTheme = () => {
|
||||||
|
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
const storageTheme = localStorage.getItem('qinglong_dark_theme');
|
||||||
|
const isDark =
|
||||||
|
(media.matches && storageTheme !== 'light') || storageTheme === 'dark';
|
||||||
|
if (isDark) {
|
||||||
|
document.body.setAttribute('data-dark', 'true');
|
||||||
|
} else {
|
||||||
|
document.body.setAttribute('data-dark', 'false');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
vhCheck();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.location.pathname === '/') {
|
if (systemInfo && systemInfo.isInitialized && !user) {
|
||||||
history.push('/crontab');
|
getUser();
|
||||||
}
|
}
|
||||||
}, [props.location.pathname]);
|
}, [props.location.pathname]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
|
if (!systemInfo) {
|
||||||
|
getSystemInfo();
|
||||||
|
}
|
||||||
|
}, [systemInfo]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTheme();
|
||||||
|
}, [theme.theme]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
|
||||||
setFetchMethod(window.fetch);
|
setFetchMethod(window.fetch);
|
||||||
if (theme === 'dark') {
|
if (_theme === 'dark') {
|
||||||
enableDarkMode({
|
enableDarkMode({});
|
||||||
brightness: 100,
|
} else if (_theme === 'light') {
|
||||||
contrast: 90,
|
|
||||||
sepia: 10,
|
|
||||||
});
|
|
||||||
} else if (theme === 'light') {
|
|
||||||
disableDarkMode();
|
disableDarkMode();
|
||||||
} else {
|
} else {
|
||||||
followSystemColorScheme({
|
followSystemColorScheme({});
|
||||||
brightness: 100,
|
|
||||||
contrast: 90,
|
|
||||||
sepia: 10,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (props.location.pathname === '/login') {
|
useEffect(() => {
|
||||||
return props.children;
|
if (!user) return;
|
||||||
|
ws.current = new SockJS(
|
||||||
|
`${location.origin}/api/ws?token=${localStorage.getItem(config.authKey)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
ws.current.onmessage = (e: any) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.data);
|
||||||
|
if (data.type === 'ping') {
|
||||||
|
if (data && data.message === 'hanhh') {
|
||||||
|
console.log('websocket连接成功', e);
|
||||||
|
} else {
|
||||||
|
console.log('websocket连接失败', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setSocketMessage(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('websocket连接失败', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const wsCurrent = ws.current;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
wsCurrent.close();
|
||||||
|
};
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.onload = () => {
|
||||||
|
const timing = performance.timing;
|
||||||
|
console.log(`白屏时间: ${timing.responseStart - timing.navigationStart}`);
|
||||||
|
console.log(
|
||||||
|
`请求完毕至DOM加载: ${timing.domInteractive - timing.responseEnd}`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`解释dom树耗时: ${timing.domComplete - timing.domInteractive}`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`从开始至load总耗时: ${timing.loadEventEnd - timing.navigationStart}`,
|
||||||
|
);
|
||||||
|
Sentry.captureMessage(
|
||||||
|
`白屏时间 ${timing.responseStart - timing.navigationStart}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (['/login', '/initialization'].includes(props.location.pathname)) {
|
||||||
|
document.title = `${
|
||||||
|
(config.documentTitleMap as any)[props.location.pathname]
|
||||||
|
} - 控制面板`;
|
||||||
|
if (
|
||||||
|
systemInfo?.isInitialized &&
|
||||||
|
props.location.pathname === '/initialization'
|
||||||
|
) {
|
||||||
|
history.push('/crontab');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (systemInfo) {
|
||||||
|
return React.Children.map(props.children, (child) => {
|
||||||
|
return React.cloneElement(child, {
|
||||||
|
...ctx,
|
||||||
|
...theme,
|
||||||
|
user,
|
||||||
|
reloadUser,
|
||||||
|
reloadTheme: setTheme,
|
||||||
|
ws: ws.current,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFirefox = navigator.userAgent.includes('Firefox');
|
const isFirefox = navigator.userAgent.includes('Firefox');
|
||||||
@@ -70,13 +218,37 @@ export default function (props: any) {
|
|||||||
!navigator.userAgent.includes('Chrome');
|
!navigator.userAgent.includes('Chrome');
|
||||||
const isQQBrowser = navigator.userAgent.includes('QQBrowser');
|
const isQQBrowser = navigator.userAgent.includes('QQBrowser');
|
||||||
|
|
||||||
return (
|
const menu = (
|
||||||
|
<Menu className="side-menu-user-drop-menu">
|
||||||
|
<Menu.Item key="1" icon={<LogoutOutlined />} onClick={logout}>
|
||||||
|
退出登录
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
return loading ? (
|
||||||
|
<PageLoading />
|
||||||
|
) : (
|
||||||
<ProLayout
|
<ProLayout
|
||||||
selectedKeys={[props.location.pathname]}
|
selectedKeys={[props.location.pathname]}
|
||||||
|
loading={loading}
|
||||||
|
ErrorBoundary={Sentry.ErrorBoundary}
|
||||||
|
logo={
|
||||||
|
<Image
|
||||||
|
preview={false}
|
||||||
|
src="https://pic.imgdb.cn/item/61acd0dd2ab3f51d912b1986.png"
|
||||||
|
/>
|
||||||
|
}
|
||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
控制面板
|
<span style={{ fontSize: 16 }}>控制面板</span>
|
||||||
<a href={changeLog} target="_blank" rel="noopener noreferrer">
|
<a
|
||||||
|
href={changeLogLink}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
fontSize: isFirefox ? 9 : 12,
|
fontSize: isFirefox ? 9 : 12,
|
||||||
@@ -86,7 +258,7 @@ export default function (props: any) {
|
|||||||
letterSpacing: isQQBrowser ? -2 : 0,
|
letterSpacing: isQQBrowser ? -2 : 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{version}
|
v{version}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
@@ -101,21 +273,60 @@ export default function (props: any) {
|
|||||||
}
|
}
|
||||||
return <Link to={menuItemProps.path}>{defaultDom}</Link>;
|
return <Link to={menuItemProps.path}>{defaultDom}</Link>;
|
||||||
}}
|
}}
|
||||||
postMenuData={(menuData) => {
|
pageTitleRender={(props, pageName, info) => {
|
||||||
return [
|
if (info && typeof info.pageName === 'string') {
|
||||||
...(menuData || []),
|
return `${info.pageName} - 控制面板`;
|
||||||
{
|
}
|
||||||
icon: <LogoutOutlined />,
|
return '控制面板';
|
||||||
name: '退出登录',
|
|
||||||
path: 'logout',
|
|
||||||
onTitleClick: () => logout(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}}
|
}}
|
||||||
pageTitleRender={() => '控制面板'}
|
onCollapse={setCollapsed}
|
||||||
|
collapsed={collapsed}
|
||||||
|
rightContentRender={() =>
|
||||||
|
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"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!collapsed && !ctx.isPhone && (
|
||||||
|
<Dropdown overlay={menu} trigger={['hover']}>
|
||||||
|
<span className="side-menu-user-wrapper">
|
||||||
|
<Avatar shape="square" size="small" icon={<UserOutlined />} />
|
||||||
|
<span style={{ marginLeft: 5 }}>admin</span>
|
||||||
|
</span>
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className="side-menu-collapse-button"
|
||||||
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
>
|
||||||
|
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{...defaultProps}
|
{...defaultProps}
|
||||||
>
|
>
|
||||||
{props.children}
|
{React.Children.map(props.children, (child) => {
|
||||||
|
return React.cloneElement(child, {
|
||||||
|
...ctx,
|
||||||
|
...theme,
|
||||||
|
user,
|
||||||
|
reloadUser,
|
||||||
|
reloadTheme: setTheme,
|
||||||
|
socketMessage,
|
||||||
|
});
|
||||||
|
})}
|
||||||
</ProLayout>
|
</ProLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,17 +11,15 @@ import { PageContainer } from '@ant-design/pro-layout';
|
|||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import Editor from '@monaco-editor/react';
|
import Editor from '@monaco-editor/react';
|
||||||
import { Controlled as CodeMirror } from 'react-codemirror2';
|
import { Controlled as CodeMirror } from 'react-codemirror2';
|
||||||
import { useCtx, useTheme } from '@/utils/hooks';
|
|
||||||
|
|
||||||
const Config = () => {
|
const Config = ({ headerStyle, isPhone, theme }: any) => {
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [title, setTitle] = useState('config.sh');
|
const [title, setTitle] = useState('config.sh');
|
||||||
const [select, setSelect] = useState('config.sh');
|
const [select, setSelect] = useState('config.sh');
|
||||||
const [data, setData] = useState<any[]>([]);
|
const [data, setData] = useState<any[]>([]);
|
||||||
const { headerStyle, isPhone } = useCtx();
|
|
||||||
const { theme } = useTheme();
|
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
|
|
||||||
const getConfig = (name: string) => {
|
const getConfig = (name: string) => {
|
||||||
request.get(`${config.apiPrefix}configs/${name}`).then((data: any) => {
|
request.get(`${config.apiPrefix}configs/${name}`).then((data: any) => {
|
||||||
@@ -40,6 +38,7 @@ const Config = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateConfig = () => {
|
const updateConfig = () => {
|
||||||
|
setConfirmLoading(true);
|
||||||
const content = editorRef.current
|
const content = editorRef.current
|
||||||
? editorRef.current.getValue().replace(/\r\n/g, '\n')
|
? editorRef.current.getValue().replace(/\r\n/g, '\n')
|
||||||
: value;
|
: value;
|
||||||
@@ -50,6 +49,7 @@ const Config = () => {
|
|||||||
})
|
})
|
||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
message.success(data.message);
|
message.success(data.message);
|
||||||
|
setConfirmLoading(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,7 +79,12 @@ const Config = () => {
|
|||||||
defaultValue="config.sh"
|
defaultValue="config.sh"
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
/>,
|
/>,
|
||||||
<Button key="1" type="primary" onClick={updateConfig}>
|
<Button
|
||||||
|
key="1"
|
||||||
|
loading={confirmLoading}
|
||||||
|
type="primary"
|
||||||
|
onClick={updateConfig}
|
||||||
|
>
|
||||||
保存
|
保存
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.ant-table-pagination.ant-pagination {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
td:nth-child(2) {
|
||||||
|
span {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+291
-26
@@ -25,13 +25,18 @@ import {
|
|||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
PauseCircleOutlined,
|
PauseCircleOutlined,
|
||||||
FieldTimeOutlined,
|
FieldTimeOutlined,
|
||||||
|
PushpinOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
import { PageContainer } from '@ant-design/pro-layout';
|
import { PageContainer } from '@ant-design/pro-layout';
|
||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import CronModal from './modal';
|
import CronModal from './modal';
|
||||||
import CronLogModal from './logModal';
|
import CronLogModal from './logModal';
|
||||||
import { useCtx, useTheme } from '@/utils/hooks';
|
import cron_parser from 'cron-parser';
|
||||||
|
import { diffTime } from '@/utils/date';
|
||||||
|
import { getTableScroll } from '@/utils/index';
|
||||||
|
import { history } from 'umi';
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
@@ -50,6 +55,8 @@ enum OperationName {
|
|||||||
'禁用',
|
'禁用',
|
||||||
'运行',
|
'运行',
|
||||||
'停止',
|
'停止',
|
||||||
|
'置顶',
|
||||||
|
'取消置顶',
|
||||||
}
|
}
|
||||||
|
|
||||||
enum OperationPath {
|
enum OperationPath {
|
||||||
@@ -57,24 +64,44 @@ enum OperationPath {
|
|||||||
'disable',
|
'disable',
|
||||||
'run',
|
'run',
|
||||||
'stop',
|
'stop',
|
||||||
|
'pin',
|
||||||
|
'unpin',
|
||||||
}
|
}
|
||||||
|
|
||||||
const Crontab = () => {
|
const Crontab = ({ headerStyle, isPhone }: any) => {
|
||||||
const columns = [
|
const columns: any = [
|
||||||
{
|
{
|
||||||
title: '任务名',
|
title: '任务名',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
|
width: 150,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
render: (text: string, record: any) => (
|
render: (text: string, record: any) => (
|
||||||
<span>{record.name || record._id}</span>
|
<span
|
||||||
|
onClick={() => {
|
||||||
|
goToScriptManager(record);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{record.name || record._id}{' '}
|
||||||
|
{record.isPinned ? (
|
||||||
|
<span>
|
||||||
|
<PushpinOutlined />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
),
|
),
|
||||||
|
sorter: {
|
||||||
|
compare: (a: any, b: any) => a.name.localeCompare(b.name),
|
||||||
|
multiple: 2,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '任务',
|
title: '任务',
|
||||||
dataIndex: 'command',
|
dataIndex: 'command',
|
||||||
key: 'command',
|
key: 'command',
|
||||||
width: '40%',
|
width: 250,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
render: (text: string, record: any) => {
|
render: (text: string, record: any) => {
|
||||||
return (
|
return (
|
||||||
@@ -90,19 +117,132 @@ const Crontab = () => {
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
sorter: {
|
||||||
|
compare: (a: any, b: any) => a.command.localeCompare(b.command),
|
||||||
|
multiple: 3,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '任务定时',
|
title: '任务定时',
|
||||||
dataIndex: 'schedule',
|
dataIndex: 'schedule',
|
||||||
key: 'schedule',
|
key: 'schedule',
|
||||||
|
width: 110,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
|
sorter: {
|
||||||
|
compare: (a: any, b: any) => a.schedule.localeCompare(b.schedule),
|
||||||
|
multiple: 1,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '最后运行时间',
|
||||||
|
align: 'center' as const,
|
||||||
|
width: 150,
|
||||||
|
sorter: {
|
||||||
|
compare: (a: any, b: any) => {
|
||||||
|
return a.last_execution_time - b.last_execution_time;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
render: (text: string, record: any) => {
|
||||||
|
const language = navigator.language || navigator.languages[0];
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{record.last_execution_time
|
||||||
|
? new Date(record.last_execution_time * 1000)
|
||||||
|
.toLocaleString(language, {
|
||||||
|
hour12: false,
|
||||||
|
})
|
||||||
|
.replace(' 24:', ' 00:')
|
||||||
|
: '-'}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '最后运行时长',
|
||||||
|
align: 'center' as const,
|
||||||
|
width: 120,
|
||||||
|
sorter: {
|
||||||
|
compare: (a: any, b: any) => {
|
||||||
|
return a.last_running_time - b.last_running_time;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
render: (text: string, record: any) => {
|
||||||
|
const language = navigator.language || navigator.languages[0];
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{record.last_running_time
|
||||||
|
? diffTime(record.last_running_time)
|
||||||
|
: '-'}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '下次运行时间',
|
||||||
|
align: 'center' as const,
|
||||||
|
width: 150,
|
||||||
|
sorter: {
|
||||||
|
compare: (a: any, b: any) => {
|
||||||
|
return a.nextRunTime - b.nextRunTime;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
render: (text: string, record: any) => {
|
||||||
|
const language = navigator.language || navigator.languages[0];
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{record.nextRunTime
|
||||||
|
.toLocaleString(language, {
|
||||||
|
hour12: false,
|
||||||
|
})
|
||||||
|
.replace(' 24:', ' 00:')}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
key: 'status',
|
key: 'status',
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
width: 60,
|
width: 85,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
text: '运行中',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '空闲中',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '已禁用',
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '队列中',
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onFilter: (value: number, record: any) => {
|
||||||
|
if (record.isDisabled && record.status !== 0) {
|
||||||
|
return value === 2;
|
||||||
|
} else {
|
||||||
|
return record.status === value;
|
||||||
|
}
|
||||||
|
},
|
||||||
render: (text: string, record: any) => (
|
render: (text: string, record: any) => (
|
||||||
<>
|
<>
|
||||||
{(!record.isDisabled || record.status !== CrontabStatus.idle) && (
|
{(!record.isDisabled || record.status !== CrontabStatus.idle) && (
|
||||||
@@ -139,6 +279,7 @@ const Crontab = () => {
|
|||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
|
width: 100,
|
||||||
render: (text: string, record: any, index: number) => {
|
render: (text: string, record: any, index: number) => {
|
||||||
const isPc = !isPhone;
|
const isPc = !isPhone;
|
||||||
return (
|
return (
|
||||||
@@ -191,7 +332,21 @@ const Crontab = () => {
|
|||||||
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
|
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(20);
|
const [pageSize, setPageSize] = useState(20);
|
||||||
const { headerStyle, isPhone } = useCtx();
|
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
|
||||||
|
|
||||||
|
const goToScriptManager = (record: any) => {
|
||||||
|
const cmd = record.command.split(' ');
|
||||||
|
if (cmd[0] === 'task') {
|
||||||
|
let [p, s] = cmd[1].split('/');
|
||||||
|
if (!s) {
|
||||||
|
s = p;
|
||||||
|
p = '';
|
||||||
|
}
|
||||||
|
history.push(`/script?p=${p}&s=${s}`);
|
||||||
|
} else if (cmd[1] === 'repo') {
|
||||||
|
location.href = cmd[2];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getCrons = () => {
|
const getCrons = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -199,12 +354,28 @@ const Crontab = () => {
|
|||||||
.get(`${config.apiPrefix}crons?searchValue=${searchText}`)
|
.get(`${config.apiPrefix}crons?searchValue=${searchText}`)
|
||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
setValue(
|
setValue(
|
||||||
data.data.sort((a: any, b: any) => {
|
data.data
|
||||||
const sortA = a.isDisabled ? 4 : a.status;
|
.sort((a: any, b: any) => {
|
||||||
const sortB = b.isDisabled ? 4 : b.status;
|
const sortA = a.isDisabled ? 4 : a.status;
|
||||||
return CrontabSort[sortA] - CrontabSort[sortB];
|
const sortB = b.isDisabled ? 4 : b.status;
|
||||||
}),
|
a.isPinned = a.isPinned ? a.isPinned : 0;
|
||||||
|
b.isPinned = b.isPinned ? b.isPinned : 0;
|
||||||
|
if (a.isPinned === b.isPinned) {
|
||||||
|
return CrontabSort[sortA] - CrontabSort[sortB];
|
||||||
|
}
|
||||||
|
return b.isPinned - a.isPinned;
|
||||||
|
})
|
||||||
|
.map((x) => {
|
||||||
|
return {
|
||||||
|
...x,
|
||||||
|
nextRunTime: cron_parser
|
||||||
|
.parseExpression(x.schedule)
|
||||||
|
.next()
|
||||||
|
.toDate(),
|
||||||
|
};
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
setCurrentPage(1);
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
@@ -238,7 +409,8 @@ const Crontab = () => {
|
|||||||
if (data.code === 200) {
|
if (data.code === 200) {
|
||||||
message.success('删除成功');
|
message.success('删除成功');
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
result.splice(index + pageSize * (currentPage - 1), 1);
|
const i = result.findIndex((x) => x._id === record._id);
|
||||||
|
result.splice(i, 1);
|
||||||
setValue(result);
|
setValue(result);
|
||||||
} else {
|
} else {
|
||||||
message.error(data);
|
message.error(data);
|
||||||
@@ -269,7 +441,8 @@ const Crontab = () => {
|
|||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
if (data.code === 200) {
|
if (data.code === 200) {
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
result.splice(index + pageSize * (currentPage - 1), 1, {
|
const i = result.findIndex((x) => x._id === record._id);
|
||||||
|
result.splice(i, 1, {
|
||||||
...record,
|
...record,
|
||||||
status: CrontabStatus.running,
|
status: CrontabStatus.running,
|
||||||
});
|
});
|
||||||
@@ -303,7 +476,8 @@ const Crontab = () => {
|
|||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
if (data.code === 200) {
|
if (data.code === 200) {
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
result.splice(index + pageSize * (currentPage - 1), 1, {
|
const i = result.findIndex((x) => x._id === record._id);
|
||||||
|
result.splice(i, 1, {
|
||||||
...record,
|
...record,
|
||||||
pid: null,
|
pid: null,
|
||||||
status: CrontabStatus.idle,
|
status: CrontabStatus.idle,
|
||||||
@@ -347,7 +521,8 @@ const Crontab = () => {
|
|||||||
if (data.code === 200) {
|
if (data.code === 200) {
|
||||||
const newStatus = record.isDisabled === 1 ? 0 : 1;
|
const newStatus = record.isDisabled === 1 ? 0 : 1;
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
result.splice(index + pageSize * (currentPage - 1), 1, {
|
const i = result.findIndex((x) => x._id === record._id);
|
||||||
|
result.splice(i, 1, {
|
||||||
...record,
|
...record,
|
||||||
isDisabled: newStatus,
|
isDisabled: newStatus,
|
||||||
});
|
});
|
||||||
@@ -363,6 +538,50 @@ const Crontab = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pinOrUnPinCron = (record: any, index: number) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: `确认${record.isPinned === 1 ? '取消置顶' : '置顶'}`,
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
确认{record.isPinned === 1 ? '取消置顶' : '置顶'}
|
||||||
|
定时任务{' '}
|
||||||
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||||
|
{record.name}
|
||||||
|
</Text>{' '}
|
||||||
|
吗
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onOk() {
|
||||||
|
request
|
||||||
|
.put(
|
||||||
|
`${config.apiPrefix}crons/${
|
||||||
|
record.isPinned === 1 ? 'unpin' : 'pin'
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
data: [record._id],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then((data: any) => {
|
||||||
|
if (data.code === 200) {
|
||||||
|
const newStatus = record.isPinned === 1 ? 0 : 1;
|
||||||
|
const result = [...value];
|
||||||
|
const i = result.findIndex((x) => x._id === record._id);
|
||||||
|
result.splice(i, 1, {
|
||||||
|
...record,
|
||||||
|
isPinned: newStatus,
|
||||||
|
});
|
||||||
|
setValue(result);
|
||||||
|
} else {
|
||||||
|
message.error(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const MoreBtn: React.FC<{
|
const MoreBtn: React.FC<{
|
||||||
record: any;
|
record: any;
|
||||||
index: number;
|
index: number;
|
||||||
@@ -376,7 +595,7 @@ const Crontab = () => {
|
|||||||
编辑
|
编辑
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
key="enableordisable"
|
key="enableOrDisable"
|
||||||
icon={
|
icon={
|
||||||
record.isDisabled === 1 ? (
|
record.isDisabled === 1 ? (
|
||||||
<CheckCircleOutlined />
|
<CheckCircleOutlined />
|
||||||
@@ -387,11 +606,17 @@ const Crontab = () => {
|
|||||||
>
|
>
|
||||||
{record.isDisabled === 1 ? '启用' : '禁用'}
|
{record.isDisabled === 1 ? '启用' : '禁用'}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
{record.isSystem !== 1 && (
|
<Menu.Item key="delete" icon={<DeleteOutlined />}>
|
||||||
<Menu.Item key="delete" icon={<DeleteOutlined />}>
|
删除
|
||||||
删除
|
</Menu.Item>
|
||||||
</Menu.Item>
|
<Menu.Item
|
||||||
)}
|
key="pinOrUnPin"
|
||||||
|
icon={
|
||||||
|
record.isPinned === 1 ? <StopOutlined /> : <PushpinOutlined />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{record.isPinned === 1 ? '取消置顶' : '置顶'}
|
||||||
|
</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -406,12 +631,15 @@ const Crontab = () => {
|
|||||||
case 'edit':
|
case 'edit':
|
||||||
editCron(record, index);
|
editCron(record, index);
|
||||||
break;
|
break;
|
||||||
case 'enableordisable':
|
case 'enableOrDisable':
|
||||||
enabledOrDisabledCron(record, index);
|
enabledOrDisabledCron(record, index);
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
delCron(record, index);
|
delCron(record, index);
|
||||||
break;
|
break;
|
||||||
|
case 'pinOrUnPin':
|
||||||
|
pinOrUnPinCron(record, index);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -425,12 +653,16 @@ const Crontab = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSearch = (value: string) => {
|
const onSearch = (value: string) => {
|
||||||
setSearchText(value);
|
setSearchText(value.trim());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCrons = (cron: any) => {
|
const handleCrons = (cron: any) => {
|
||||||
const index = value.findIndex((x) => x._id === cron._id);
|
const index = value.findIndex((x) => x._id === cron._id);
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
|
cron.nextRunTime = cron_parser
|
||||||
|
.parseExpression(cron.schedule)
|
||||||
|
.next()
|
||||||
|
.toDate();
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
result.unshift(cron);
|
result.unshift(cron);
|
||||||
} else {
|
} else {
|
||||||
@@ -447,6 +679,10 @@ const Crontab = () => {
|
|||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
const index = value.findIndex((x) => x._id === cron._id);
|
const index = value.findIndex((x) => x._id === cron._id);
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
|
data.data.nextRunTime = cron_parser
|
||||||
|
.parseExpression(data.data.schedule)
|
||||||
|
.next()
|
||||||
|
.toDate();
|
||||||
result.splice(index, 1, {
|
result.splice(index, 1, {
|
||||||
...cron,
|
...cron,
|
||||||
...data.data,
|
...data.data,
|
||||||
@@ -458,6 +694,12 @@ const Crontab = () => {
|
|||||||
|
|
||||||
const onSelectChange = (selectedIds: any[]) => {
|
const onSelectChange = (selectedIds: any[]) => {
|
||||||
setSelectedRowIds(selectedIds);
|
setSelectedRowIds(selectedIds);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
|
||||||
|
setTableScrollHeight(getTableScroll());
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const rowSelection = {
|
const rowSelection = {
|
||||||
@@ -522,6 +764,10 @@ const Crontab = () => {
|
|||||||
localStorage.setItem('pageSize', pageSize + '');
|
localStorage.setItem('pageSize', pageSize + '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getRowClassName = (record: any, index: number) => {
|
||||||
|
return record.isPinned ? 'pinned-cron' : '';
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (logCron) {
|
if (logCron) {
|
||||||
localStorage.setItem('logCron', logCron._id);
|
localStorage.setItem('logCron', logCron._id);
|
||||||
@@ -535,6 +781,9 @@ const Crontab = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPageSize(parseInt(localStorage.getItem('pageSize') || '20'));
|
setPageSize(parseInt(localStorage.getItem('pageSize') || '20'));
|
||||||
|
setTimeout(() => {
|
||||||
|
setTableScrollHeight(getTableScroll());
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -586,6 +835,20 @@ const Crontab = () => {
|
|||||||
<Button type="primary" onClick={() => operateCrons(3)}>
|
<Button type="primary" onClick={() => operateCrons(3)}>
|
||||||
批量停止
|
批量停止
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => operateCrons(4)}
|
||||||
|
style={{ marginLeft: 8, marginRight: 8 }}
|
||||||
|
>
|
||||||
|
批量置顶
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => operateCrons(5)}
|
||||||
|
style={{ marginLeft: 8, marginRight: 8 }}
|
||||||
|
>
|
||||||
|
批量取消置顶
|
||||||
|
</Button>
|
||||||
<span style={{ marginLeft: 8 }}>
|
<span style={{ marginLeft: 8 }}>
|
||||||
已选择
|
已选择
|
||||||
<a>{selectedRowIds?.length}</a>项
|
<a>{selectedRowIds?.length}</a>项
|
||||||
@@ -595,21 +858,23 @@ const Crontab = () => {
|
|||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
pagination={{
|
pagination={{
|
||||||
hideOnSinglePage: true,
|
|
||||||
current: currentPage,
|
current: currentPage,
|
||||||
onChange: onPageChange,
|
onChange: onPageChange,
|
||||||
pageSize: pageSize,
|
pageSize: pageSize,
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
|
simple: isPhone,
|
||||||
defaultPageSize: 20,
|
defaultPageSize: 20,
|
||||||
showTotal: (total: number, range: number[]) =>
|
showTotal: (total: number, range: number[]) =>
|
||||||
`第 ${range[0]}-${range[1]} 条/总共 ${total} 条`,
|
`第 ${range[0]}-${range[1]} 条/总共 ${total} 条`,
|
||||||
|
pageSizeOptions: [10, 20, 50, 100, 200, 500, 1000],
|
||||||
}}
|
}}
|
||||||
dataSource={value}
|
dataSource={value}
|
||||||
rowKey="_id"
|
rowKey="_id"
|
||||||
size="middle"
|
size="middle"
|
||||||
scroll={{ x: 768 }}
|
scroll={{ x: 1000, y: tableScrollHeight }}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
|
rowClassName={getRowClassName}
|
||||||
/>
|
/>
|
||||||
<CronLogModal
|
<CronLogModal
|
||||||
visible={isLogModalVisible}
|
visible={isLogModalVisible}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Modal, message, Input, Form, Statistic } from 'antd';
|
import { Modal, message, Input, Form, Statistic, Button } from 'antd';
|
||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
import {
|
import {
|
||||||
Loading3QuartersOutlined,
|
Loading3QuartersOutlined,
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
import { PageLoading } from '@ant-design/pro-layout';
|
||||||
|
|
||||||
enum CrontabStatus {
|
enum CrontabStatus {
|
||||||
'running',
|
'running',
|
||||||
@@ -26,7 +27,7 @@ const CronLogModal = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [value, setValue] = useState<string>('启动中...');
|
const [value, setValue] = useState<string>('启动中...');
|
||||||
const [loading, setLoading] = useState<any>(true);
|
const [loading, setLoading] = useState<any>(true);
|
||||||
const [excuting, setExcuting] = useState<any>(true);
|
const [executing, setExecuting] = useState<any>(true);
|
||||||
const [isPhone, setIsPhone] = useState(false);
|
const [isPhone, setIsPhone] = useState(false);
|
||||||
const [theme, setTheme] = useState<string>('');
|
const [theme, setTheme] = useState<string>('');
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ const CronLogModal = ({
|
|||||||
if (localStorage.getItem('logCron') === cron._id) {
|
if (localStorage.getItem('logCron') === cron._id) {
|
||||||
const log = data.data as string;
|
const log = data.data as string;
|
||||||
setValue(log || '暂无日志');
|
setValue(log || '暂无日志');
|
||||||
setExcuting(
|
setExecuting(
|
||||||
log && !log.includes('执行结束') && !log.includes('重启面板'),
|
log && !log.includes('执行结束') && !log.includes('重启面板'),
|
||||||
);
|
);
|
||||||
if (log && !log.includes('执行结束') && !log.includes('重启面板')) {
|
if (log && !log.includes('执行结束') && !log.includes('重启面板')) {
|
||||||
@@ -57,7 +58,11 @@ const CronLogModal = ({
|
|||||||
content: (
|
content: (
|
||||||
<span>
|
<span>
|
||||||
系统将在
|
系统将在
|
||||||
<Countdown format="ss" value={Date.now() + 1000 * 10} />
|
<Countdown
|
||||||
|
className="inline-countdown"
|
||||||
|
format="ss"
|
||||||
|
value={Date.now() + 1000 * 10}
|
||||||
|
/>
|
||||||
秒后自动刷新
|
秒后自动刷新
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@@ -84,8 +89,8 @@ const CronLogModal = ({
|
|||||||
const titleElement = () => {
|
const titleElement = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{excuting && <Loading3QuartersOutlined spin />}
|
{(executing || loading) && <Loading3QuartersOutlined spin />}
|
||||||
{!excuting && <CheckCircleOutlined />}
|
{!executing && <CheckCircleOutlined />}
|
||||||
<span style={{ marginLeft: 5 }}>日志-{cron && cron.name}</span>{' '}
|
<span style={{ marginLeft: 5 }}>日志-{cron && cron.name}</span>{' '}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -110,12 +115,20 @@ const CronLogModal = ({
|
|||||||
bodyStyle={{
|
bodyStyle={{
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
maxHeight: 'calc(80vh - var(--vh-offset, 0px))',
|
maxHeight: 'calc(80vh - var(--vh-offset, 0px))',
|
||||||
|
minHeight: '300px',
|
||||||
}}
|
}}
|
||||||
forceRender
|
forceRender
|
||||||
onOk={() => cancel()}
|
onOk={() => cancel()}
|
||||||
onCancel={() => cancel()}
|
onCancel={() => cancel()}
|
||||||
|
footer={[
|
||||||
|
<Button type="primary" onClick={() => cancel()}>
|
||||||
|
知道了
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
{!loading && value && (
|
{loading ? (
|
||||||
|
<PageLoading />
|
||||||
|
) : (
|
||||||
<pre
|
<pre
|
||||||
style={
|
style={
|
||||||
isPhone
|
isPhone
|
||||||
|
|||||||
@@ -66,8 +66,16 @@ const CronModal = ({
|
|||||||
<Form.Item name="name" label="名称">
|
<Form.Item name="name" label="名称">
|
||||||
<Input placeholder="请输入任务名称" />
|
<Input placeholder="请输入任务名称" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="command" label="命令" rules={[{ required: true }]}>
|
<Form.Item
|
||||||
<Input placeholder="请输入要执行的命令" />
|
name="command"
|
||||||
|
label="命令"
|
||||||
|
rules={[{ required: true, whitespace: true }]}
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
rows={4}
|
||||||
|
autoSize={true}
|
||||||
|
placeholder="请输入要执行的命令"
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="schedule"
|
name="schedule"
|
||||||
|
|||||||
@@ -0,0 +1,450 @@
|
|||||||
|
import React, { useCallback, useRef, useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
message,
|
||||||
|
Modal,
|
||||||
|
Table,
|
||||||
|
Tag,
|
||||||
|
Space,
|
||||||
|
Typography,
|
||||||
|
Tooltip,
|
||||||
|
Input,
|
||||||
|
Tabs,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
StopOutlined,
|
||||||
|
BugOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import config from '@/utils/config';
|
||||||
|
import { PageContainer } from '@ant-design/pro-layout';
|
||||||
|
import { request } from '@/utils/http';
|
||||||
|
import DependenceModal from './modal';
|
||||||
|
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||||
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
|
import './index.less';
|
||||||
|
import { getTableScroll } from '@/utils/index';
|
||||||
|
import DependenceLogModal from './logModal';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
const { Search } = Input;
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
'安装中',
|
||||||
|
'已安装',
|
||||||
|
'安装失败',
|
||||||
|
'删除中',
|
||||||
|
'已删除',
|
||||||
|
'删除失败',
|
||||||
|
}
|
||||||
|
|
||||||
|
enum StatusColor {
|
||||||
|
'processing',
|
||||||
|
'success',
|
||||||
|
'error',
|
||||||
|
}
|
||||||
|
|
||||||
|
const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
|
||||||
|
const columns: any = [
|
||||||
|
{
|
||||||
|
title: '序号',
|
||||||
|
align: 'center' as const,
|
||||||
|
width: 50,
|
||||||
|
render: (text: string, record: any, index: number) => {
|
||||||
|
return <span style={{ cursor: 'text' }}>{index + 1} </span>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
key: 'status',
|
||||||
|
dataIndex: 'status',
|
||||||
|
align: 'center' as const,
|
||||||
|
render: (text: string, record: any, index: number) => {
|
||||||
|
return (
|
||||||
|
<Space size="middle" style={{ cursor: 'text' }}>
|
||||||
|
<Tag
|
||||||
|
color={StatusColor[record.status % 3]}
|
||||||
|
style={{ marginRight: 0 }}
|
||||||
|
>
|
||||||
|
{Status[record.status]}
|
||||||
|
</Tag>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
key: 'created',
|
||||||
|
dataIndex: 'created',
|
||||||
|
align: 'center' as const,
|
||||||
|
render: (text: string, record: any) => {
|
||||||
|
return <span>{new Date(record.created).toLocaleString()}</span>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
align: 'center' as const,
|
||||||
|
render: (text: string, record: any, index: number) => {
|
||||||
|
const isPc = !isPhone;
|
||||||
|
return (
|
||||||
|
<Space size="middle">
|
||||||
|
{record.status !== Status.安装中 &&
|
||||||
|
record.status !== Status.删除中 && (
|
||||||
|
<>
|
||||||
|
<Tooltip title={isPc ? '重新安装' : ''}>
|
||||||
|
<a onClick={() => reInstallDependence(record, index)}>
|
||||||
|
<BugOutlined />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title={isPc ? '删除' : ''}>
|
||||||
|
<a onClick={() => deleteDependence(record, index)}>
|
||||||
|
<DeleteOutlined />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Tooltip title={isPc ? '日志' : ''}>
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
setLogDependence({ ...record, timestamp: Date.now() });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FileTextOutlined />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const [value, setValue] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
|
const [editedDependence, setEditedDependence] = useState();
|
||||||
|
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
|
||||||
|
const [logDependence, setLogDependence] = useState<any>();
|
||||||
|
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
|
||||||
|
const [type, setType] = useState('nodejs');
|
||||||
|
|
||||||
|
const getDependencies = () => {
|
||||||
|
setLoading(true);
|
||||||
|
request
|
||||||
|
.get(
|
||||||
|
`${config.apiPrefix}dependencies?searchValue=${searchText}&type=${type}`,
|
||||||
|
)
|
||||||
|
.then((data: any) => {
|
||||||
|
setValue(data.data);
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addDependence = () => {
|
||||||
|
setEditedDependence(null as any);
|
||||||
|
setIsModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const editDependence = (record: any, index: number) => {
|
||||||
|
setEditedDependence(record);
|
||||||
|
setIsModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteDependence = (record: any, index: number) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
确认删除依赖{' '}
|
||||||
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||||
|
{record.name}
|
||||||
|
</Text>{' '}
|
||||||
|
吗
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onOk() {
|
||||||
|
request
|
||||||
|
.delete(`${config.apiPrefix}dependencies`, { data: [record._id] })
|
||||||
|
.then((data: any) => {
|
||||||
|
if (data.code === 200) {
|
||||||
|
handleDependence(data.data[0]);
|
||||||
|
} else {
|
||||||
|
message.error(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const reInstallDependence = (record: any, index: number) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认重新安装',
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
确认重新安装{' '}
|
||||||
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||||
|
{record.name}
|
||||||
|
</Text>{' '}
|
||||||
|
吗
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onOk() {
|
||||||
|
request
|
||||||
|
.put(`${config.apiPrefix}dependencies/reinstall`, {
|
||||||
|
data: [record._id],
|
||||||
|
})
|
||||||
|
.then((data: any) => {
|
||||||
|
if (data.code === 200) {
|
||||||
|
handleDependence(data.data[0]);
|
||||||
|
} else {
|
||||||
|
message.error(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = (dependence?: any[]) => {
|
||||||
|
setIsModalVisible(false);
|
||||||
|
dependence && handleDependence(dependence);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDependence = (dependence: any) => {
|
||||||
|
const result = [...value];
|
||||||
|
if (Array.isArray(dependence)) {
|
||||||
|
result.push(...dependence);
|
||||||
|
} else {
|
||||||
|
const index = value.findIndex((x) => x._id === dependence._id);
|
||||||
|
result.splice(index, 1, {
|
||||||
|
...dependence,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setValue(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelectChange = (selectedIds: any[]) => {
|
||||||
|
setSelectedRowIds(selectedIds);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
|
||||||
|
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const rowSelection = {
|
||||||
|
selectedRowIds,
|
||||||
|
onChange: onSelectChange,
|
||||||
|
};
|
||||||
|
|
||||||
|
const delDependencies = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: <>确认删除选中的依赖吗</>,
|
||||||
|
onOk() {
|
||||||
|
request
|
||||||
|
.delete(`${config.apiPrefix}dependencies`, { data: selectedRowIds })
|
||||||
|
.then((data: any) => {
|
||||||
|
if (data.code === 200) {
|
||||||
|
setSelectedRowIds([]);
|
||||||
|
getDependencies();
|
||||||
|
} else {
|
||||||
|
message.error(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDependenceDetail = (dependence: any) => {
|
||||||
|
request
|
||||||
|
.get(`${config.apiPrefix}dependencies/${dependence._id}`)
|
||||||
|
.then((data: any) => {
|
||||||
|
const index = value.findIndex((x) => x._id === dependence._id);
|
||||||
|
const result = [...value];
|
||||||
|
result.splice(index, 1, {
|
||||||
|
...dependence,
|
||||||
|
...data.data,
|
||||||
|
});
|
||||||
|
setValue(result);
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSearch = (value: string) => {
|
||||||
|
setSearchText(value.trim());
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getDependencies();
|
||||||
|
}, [searchText, type]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (logDependence) {
|
||||||
|
localStorage.setItem('logDependence', logDependence._id);
|
||||||
|
setIsLogModalVisible(true);
|
||||||
|
}
|
||||||
|
}, [logDependence]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!socketMessage) return;
|
||||||
|
const { type, message, references } = socketMessage;
|
||||||
|
if (
|
||||||
|
type === 'installDependence' &&
|
||||||
|
message.includes('结束时间') &&
|
||||||
|
references.length > 0
|
||||||
|
) {
|
||||||
|
let status;
|
||||||
|
if (message.includes('安装')) {
|
||||||
|
status = message.includes('成功') ? Status.已安装 : Status.安装失败;
|
||||||
|
} else {
|
||||||
|
status = message.includes('成功') ? Status.已删除 : Status.删除失败;
|
||||||
|
}
|
||||||
|
const result = [...value];
|
||||||
|
for (let i = 0; i < references.length; i++) {
|
||||||
|
const index = value.findIndex((x) => x._id === references[i]);
|
||||||
|
result.splice(index, 1, {
|
||||||
|
...result[index],
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setValue(result);
|
||||||
|
|
||||||
|
if (status === Status.已删除) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const _result = [...value];
|
||||||
|
for (let i = 0; i < references.length; i++) {
|
||||||
|
const index = value.findIndex((x) => x._id === references[i]);
|
||||||
|
_result.splice(index, 1);
|
||||||
|
}
|
||||||
|
setValue(_result);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [socketMessage]);
|
||||||
|
|
||||||
|
const panelContent = () => (
|
||||||
|
<>
|
||||||
|
{selectedRowIds.length > 0 && (
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
style={{ marginBottom: 5, marginLeft: 8 }}
|
||||||
|
onClick={delDependencies}
|
||||||
|
>
|
||||||
|
批量删除
|
||||||
|
</Button>
|
||||||
|
<span style={{ marginLeft: 8 }}>
|
||||||
|
已选择
|
||||||
|
<a>{selectedRowIds?.length}</a>项
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<DndProvider backend={HTML5Backend}>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
pagination={false}
|
||||||
|
dataSource={value}
|
||||||
|
rowKey="_id"
|
||||||
|
size="middle"
|
||||||
|
scroll={{ x: 768, y: tableScrollHeight }}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</DndProvider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const onTabChange = (activeKey: string) => {
|
||||||
|
setType(activeKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
className="ql-container-wrapper dependence-wrapper"
|
||||||
|
title="依赖管理"
|
||||||
|
extra={[
|
||||||
|
<Search
|
||||||
|
placeholder="请输入名称"
|
||||||
|
style={{ width: 'auto' }}
|
||||||
|
enterButton
|
||||||
|
loading={loading}
|
||||||
|
onSearch={onSearch}
|
||||||
|
/>,
|
||||||
|
<Button key="2" type="primary" onClick={() => addDependence()}>
|
||||||
|
添加依赖
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
header={{
|
||||||
|
style: headerStyle,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
defaultActiveKey="nodejs"
|
||||||
|
size="small"
|
||||||
|
tabPosition="top"
|
||||||
|
onChange={onTabChange}
|
||||||
|
>
|
||||||
|
<Tabs.TabPane tab="NodeJs" key="nodejs">
|
||||||
|
{panelContent()}
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane tab="Python3" key="python3">
|
||||||
|
{panelContent()}
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane tab="Linux" key="linux">
|
||||||
|
{panelContent()}
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
<DependenceModal
|
||||||
|
visible={isModalVisible}
|
||||||
|
handleCancel={handleCancel}
|
||||||
|
dependence={editedDependence}
|
||||||
|
defaultType={type}
|
||||||
|
/>
|
||||||
|
<DependenceLogModal
|
||||||
|
visible={isLogModalVisible}
|
||||||
|
handleCancel={(needRemove?: boolean) => {
|
||||||
|
setIsLogModalVisible(false);
|
||||||
|
if (needRemove) {
|
||||||
|
const index = value.findIndex((x) => x._id === logDependence._id);
|
||||||
|
const result = [...value];
|
||||||
|
result.splice(index, 1);
|
||||||
|
setValue(result);
|
||||||
|
} else if ([...value].map((x) => x._id).includes(logDependence._id)) {
|
||||||
|
getDependenceDetail(logDependence);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
socketMessage={socketMessage}
|
||||||
|
dependence={logDependence}
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dependence;
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Modal, message, Input, Form, Statistic, Button } from 'antd';
|
||||||
|
import { request } from '@/utils/http';
|
||||||
|
import config from '@/utils/config';
|
||||||
|
import {
|
||||||
|
Loading3QuartersOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { PageLoading } from '@ant-design/pro-layout';
|
||||||
|
|
||||||
|
const DependenceLogModal = ({
|
||||||
|
dependence,
|
||||||
|
handleCancel,
|
||||||
|
visible,
|
||||||
|
socketMessage,
|
||||||
|
}: {
|
||||||
|
dependence?: any;
|
||||||
|
visible: boolean;
|
||||||
|
handleCancel: (needRemove?: boolean) => void;
|
||||||
|
socketMessage: any;
|
||||||
|
}) => {
|
||||||
|
const [value, setValue] = useState<string>('');
|
||||||
|
const [executing, setExecuting] = useState<any>(true);
|
||||||
|
const [isPhone, setIsPhone] = useState(false);
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
const [isRemoveFailed, setIsRemoveFailed] = useState(false);
|
||||||
|
const [removeLoading, setRemoveLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const cancel = (needRemove: boolean = false) => {
|
||||||
|
localStorage.removeItem('logDependence');
|
||||||
|
handleCancel(needRemove);
|
||||||
|
};
|
||||||
|
|
||||||
|
const titleElement = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{executing && <Loading3QuartersOutlined spin />}
|
||||||
|
{!executing && <CheckCircleOutlined />}
|
||||||
|
<span style={{ marginLeft: 5 }}>
|
||||||
|
日志 - {dependence && dependence.name}
|
||||||
|
</span>{' '}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDependenceLog = () => {
|
||||||
|
setLoading(true);
|
||||||
|
request
|
||||||
|
.get(`${config.apiPrefix}dependencies/${dependence._id}`)
|
||||||
|
.then((data: any) => {
|
||||||
|
if (localStorage.getItem('logDependence') === dependence._id) {
|
||||||
|
const log = (data.data.log || []).join('\n') as string;
|
||||||
|
setValue(log);
|
||||||
|
setExecuting(!log.includes('结束时间'));
|
||||||
|
setIsRemoveFailed(log.includes('删除失败'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const forceRemoveDependence = () => {
|
||||||
|
setRemoveLoading(true);
|
||||||
|
request
|
||||||
|
.delete(`${config.apiPrefix}dependencies/force`, {
|
||||||
|
data: [dependence._id],
|
||||||
|
})
|
||||||
|
.then((data: any) => {
|
||||||
|
cancel(true);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setRemoveLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const footerClick = () => {
|
||||||
|
if (isRemoveFailed) {
|
||||||
|
forceRemoveDependence();
|
||||||
|
} else {
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (dependence) {
|
||||||
|
getDependenceLog();
|
||||||
|
}
|
||||||
|
}, [dependence]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!socketMessage) return;
|
||||||
|
const { type, message, references } = socketMessage;
|
||||||
|
if (
|
||||||
|
type === 'installDependence' &&
|
||||||
|
message.includes('结束时间') &&
|
||||||
|
references.length > 0
|
||||||
|
) {
|
||||||
|
setExecuting(false);
|
||||||
|
setIsRemoveFailed(message.includes('删除失败'));
|
||||||
|
}
|
||||||
|
setValue(`${value} \n ${message}`);
|
||||||
|
}, [socketMessage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsPhone(document.body.clientWidth < 768);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={titleElement()}
|
||||||
|
visible={visible}
|
||||||
|
centered
|
||||||
|
className="log-modal"
|
||||||
|
bodyStyle={{
|
||||||
|
overflowY: 'auto',
|
||||||
|
maxHeight: 'calc(70vh - var(--vh-offset, 0px))',
|
||||||
|
minHeight: '300px',
|
||||||
|
}}
|
||||||
|
forceRender
|
||||||
|
onOk={() => cancel()}
|
||||||
|
onCancel={() => cancel()}
|
||||||
|
footer={[
|
||||||
|
<Button type="primary" onClick={footerClick} loading={removeLoading}>
|
||||||
|
{isRemoveFailed ? '强制删除' : '知道了'}
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<PageLoading />
|
||||||
|
) : (
|
||||||
|
<pre
|
||||||
|
style={
|
||||||
|
isPhone
|
||||||
|
? {
|
||||||
|
fontFamily: 'Source Code Pro',
|
||||||
|
width: 375,
|
||||||
|
zoom: 0.83,
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DependenceLogModal;
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Modal, message, Input, Form, Radio, Select } from 'antd';
|
||||||
|
import { request } from '@/utils/http';
|
||||||
|
import config from '@/utils/config';
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
enum DependenceTypes {
|
||||||
|
'nodejs',
|
||||||
|
'python3',
|
||||||
|
'linux',
|
||||||
|
}
|
||||||
|
|
||||||
|
const DependenceModal = ({
|
||||||
|
dependence,
|
||||||
|
handleCancel,
|
||||||
|
visible,
|
||||||
|
defaultType,
|
||||||
|
}: {
|
||||||
|
dependence?: any;
|
||||||
|
visible: boolean;
|
||||||
|
handleCancel: (cks?: any[]) => void;
|
||||||
|
defaultType: string;
|
||||||
|
}) => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleOk = async (values: any) => {
|
||||||
|
setLoading(true);
|
||||||
|
const { name, split, type } = values;
|
||||||
|
const method = dependence ? 'put' : 'post';
|
||||||
|
let payload;
|
||||||
|
if (!dependence) {
|
||||||
|
if (split === '1') {
|
||||||
|
const symbol = name.includes('&') ? '&' : '\n';
|
||||||
|
payload = name.split(symbol).map((x: any) => {
|
||||||
|
return {
|
||||||
|
name: x,
|
||||||
|
type,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
payload = [{ name, type }];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
payload = { ...values, _id: dependence._id };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { code, data } = await request[method](
|
||||||
|
`${config.apiPrefix}dependencies`,
|
||||||
|
{
|
||||||
|
data: payload,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (code !== 200) {
|
||||||
|
message.error(data);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
handleCancel(data);
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.resetFields();
|
||||||
|
}, [dependence, visible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={dependence ? '编辑依赖' : '新建依赖'}
|
||||||
|
visible={visible}
|
||||||
|
forceRender
|
||||||
|
onOk={() => {
|
||||||
|
form
|
||||||
|
.validateFields()
|
||||||
|
.then((values) => {
|
||||||
|
handleOk(values);
|
||||||
|
})
|
||||||
|
.catch((info) => {
|
||||||
|
console.log('Validate Failed:', info);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onCancel={() => handleCancel()}
|
||||||
|
confirmLoading={loading}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
name="dependence_modal"
|
||||||
|
initialValues={dependence}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="type"
|
||||||
|
label="依赖类型"
|
||||||
|
initialValue={DependenceTypes[defaultType as any]}
|
||||||
|
>
|
||||||
|
<Select>
|
||||||
|
{config.dependenceTypes.map((x, i) => (
|
||||||
|
<Option value={i}>{x}</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
{!dependence && (
|
||||||
|
<Form.Item
|
||||||
|
name="split"
|
||||||
|
label="自动拆分"
|
||||||
|
initialValue="0"
|
||||||
|
tooltip="多个依赖是否换行分割"
|
||||||
|
>
|
||||||
|
<Radio.Group>
|
||||||
|
<Radio value="1">是</Radio>
|
||||||
|
<Radio value="0">否</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
<Form.Item
|
||||||
|
name="name"
|
||||||
|
label="名称"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入依赖名称', whitespace: true },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
rows={4}
|
||||||
|
autoSize={true}
|
||||||
|
placeholder="请输入依赖名称"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="remark" label="备注">
|
||||||
|
<Input placeholder="请输入备注" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DependenceModal;
|
||||||
@@ -11,3 +11,10 @@
|
|||||||
.d2h-code-side-line {
|
.d2h-code-side-line {
|
||||||
padding: 0 0.5em;
|
padding: 0 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.diff-switch-file {
|
||||||
|
min-width: 768px;
|
||||||
|
.ant-form-item {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+68
-25
@@ -1,55 +1,78 @@
|
|||||||
import React, { PureComponent, useRef, useState, useEffect } from 'react';
|
import React, { PureComponent, useRef, useState, useEffect } from 'react';
|
||||||
import { Button, message, Modal } from 'antd';
|
import { Button, message, Select, Form, Row, Col } from 'antd';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
import { PageContainer } from '@ant-design/pro-layout';
|
import { PageContainer } from '@ant-design/pro-layout';
|
||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
import { DiffEditor } from '@monaco-editor/react';
|
import { DiffEditor } from '@monaco-editor/react';
|
||||||
import ReactDiffViewer from 'react-diff-viewer';
|
import ReactDiffViewer from 'react-diff-viewer';
|
||||||
import { useCtx, useTheme } from '@/utils/hooks';
|
|
||||||
|
|
||||||
const Crontab = () => {
|
const { Option } = Select;
|
||||||
const [value, setValue] = useState('');
|
|
||||||
const [sample, setSample] = useState('');
|
const Diff = ({ headerStyle, isPhone, theme }: any) => {
|
||||||
|
const [origin, setOrigin] = useState('config.sample.sh');
|
||||||
|
const [current, setCurrent] = useState('config.sh');
|
||||||
|
const [originValue, setOriginValue] = useState('');
|
||||||
|
const [currentValue, setCurrentValue] = useState('');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const { headerStyle, isPhone } = useCtx();
|
const [files, setFiles] = useState<any[]>([]);
|
||||||
const { theme } = useTheme();
|
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
|
|
||||||
const getConfig = () => {
|
const getConfig = () => {
|
||||||
request.get(`${config.apiPrefix}configs/config.sh`).then((data) => {
|
request.get(`${config.apiPrefix}configs/${current}`).then((data) => {
|
||||||
setValue(data.data);
|
setCurrentValue(data.data);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSample = () => {
|
const getSample = () => {
|
||||||
setLoading(true);
|
request.get(`${config.apiPrefix}configs/${origin}`).then((data) => {
|
||||||
request
|
setOriginValue(data.data);
|
||||||
.get(`${config.apiPrefix}configs/config.sample.sh`)
|
});
|
||||||
.then((data) => {
|
|
||||||
setSample(data.data);
|
|
||||||
})
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateConfig = () => {
|
const updateConfig = () => {
|
||||||
const content = editorRef.current
|
const content = editorRef.current
|
||||||
? editorRef.current.getModel().modified.getValue().replace(/\r\n/g, '\n')
|
? editorRef.current.getModel().modified.getValue().replace(/\r\n/g, '\n')
|
||||||
: value;
|
: currentValue;
|
||||||
|
|
||||||
request
|
request
|
||||||
.post(`${config.apiPrefix}configs/save`, {
|
.post(`${config.apiPrefix}configs/save`, {
|
||||||
data: { content, name: 'config.sh' },
|
data: { content, name: current },
|
||||||
})
|
})
|
||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
message.success(data.message);
|
message.success(data.message);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getFiles = () => {
|
||||||
|
setLoading(true);
|
||||||
|
request
|
||||||
|
.get(`${config.apiPrefix}configs/files`)
|
||||||
|
.then((data: any) => {
|
||||||
|
setFiles(data.data);
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const originFileChange = (value: string) => {
|
||||||
|
setOrigin(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentFileChange = (value: string) => {
|
||||||
|
setCurrent(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getFiles();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getSample();
|
||||||
|
}, [origin]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getConfig();
|
getConfig();
|
||||||
getSample();
|
}, [current]);
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
@@ -67,6 +90,26 @@ const Crontab = () => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<Row gutter={24} className="diff-switch-file">
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item label="源文件">
|
||||||
|
<Select value={origin} onChange={originFileChange}>
|
||||||
|
{files.map((x) => (
|
||||||
|
<Option value={x.value}>{x.title}</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item label="当前文件">
|
||||||
|
<Select value={current} onChange={currentFileChange}>
|
||||||
|
{files.map((x) => (
|
||||||
|
<Option value={x.value}>{x.title}</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
{isPhone ? (
|
{isPhone ? (
|
||||||
<ReactDiffViewer
|
<ReactDiffViewer
|
||||||
styles={{
|
styles={{
|
||||||
@@ -86,8 +129,8 @@ const Crontab = () => {
|
|||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
oldValue={value}
|
oldValue={originValue}
|
||||||
newValue={sample}
|
newValue={currentValue}
|
||||||
splitView={true}
|
splitView={true}
|
||||||
leftTitle="config.sh"
|
leftTitle="config.sh"
|
||||||
rightTitle="config.sample.sh"
|
rightTitle="config.sample.sh"
|
||||||
@@ -96,8 +139,8 @@ const Crontab = () => {
|
|||||||
) : (
|
) : (
|
||||||
<DiffEditor
|
<DiffEditor
|
||||||
language={'shell'}
|
language={'shell'}
|
||||||
original={sample}
|
original={originValue}
|
||||||
modified={value}
|
modified={currentValue}
|
||||||
options={{
|
options={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
lineNumbersMinChars: 3,
|
lineNumbersMinChars: 3,
|
||||||
@@ -115,4 +158,4 @@ const Crontab = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Crontab;
|
export default Diff;
|
||||||
|
|||||||
Vendored
+99
-49
@@ -25,7 +25,7 @@ import EditNameModal from './editNameModal';
|
|||||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
import { useCtx } from '@/utils/hooks';
|
import { getTableScroll } from '@/utils/index';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
@@ -60,36 +60,30 @@ const DragableBodyRow = ({
|
|||||||
...restProps
|
...restProps
|
||||||
}: any) => {
|
}: any) => {
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
const [{ isOver, dropClassName }, drop] = useDrop(
|
const [{ isOver, dropClassName }, drop] = useDrop({
|
||||||
() => ({
|
accept: type,
|
||||||
accept: type,
|
collect: (monitor) => {
|
||||||
collect: (monitor) => {
|
const { index: dragIndex } = (monitor.getItem() as any) || {};
|
||||||
const { index: dragIndex } = monitor.getItem() || ({} as any);
|
if (dragIndex === index) {
|
||||||
if (dragIndex === index) {
|
return {};
|
||||||
return {};
|
}
|
||||||
}
|
return {
|
||||||
return {
|
isOver: monitor.isOver(),
|
||||||
isOver: monitor.isOver(),
|
dropClassName:
|
||||||
dropClassName:
|
dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
|
||||||
dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
|
};
|
||||||
};
|
},
|
||||||
},
|
drop: (item: any) => {
|
||||||
drop: (item: any) => {
|
moveRow(item.index, index);
|
||||||
moveRow(item.index, index);
|
},
|
||||||
},
|
});
|
||||||
|
const [, drag] = useDrag({
|
||||||
|
type,
|
||||||
|
item: { index },
|
||||||
|
collect: (monitor) => ({
|
||||||
|
isDragging: monitor.isDragging(),
|
||||||
}),
|
}),
|
||||||
[index],
|
});
|
||||||
);
|
|
||||||
const [, drag, preview] = useDrag(
|
|
||||||
() => ({
|
|
||||||
type,
|
|
||||||
item: { index },
|
|
||||||
collect: (monitor) => ({
|
|
||||||
isDragging: monitor.isDragging(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
[index],
|
|
||||||
);
|
|
||||||
drop(drag(ref));
|
drop(drag(ref));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -98,14 +92,12 @@ const DragableBodyRow = ({
|
|||||||
className={`${className}${isOver ? dropClassName : ''}`}
|
className={`${className}${isOver ? dropClassName : ''}`}
|
||||||
style={{ cursor: 'move', ...style }}
|
style={{ cursor: 'move', ...style }}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
/>
|
||||||
{restProps.children}
|
|
||||||
</tr>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Env = () => {
|
const Env = ({ headerStyle, isPhone, theme }: any) => {
|
||||||
const columns = [
|
const columns: any = [
|
||||||
{
|
{
|
||||||
title: '序号',
|
title: '序号',
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
@@ -119,19 +111,24 @@ const Env = () => {
|
|||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
|
sorter: (a: any, b: any) => a.name.localeCompare(b.name),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '值',
|
title: '值',
|
||||||
dataIndex: 'value',
|
dataIndex: 'value',
|
||||||
key: 'value',
|
key: 'value',
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
width: '44%',
|
width: '35%',
|
||||||
ellipsis: {
|
ellipsis: {
|
||||||
showTitle: false,
|
showTitle: false,
|
||||||
},
|
},
|
||||||
render: (text: string, record: any) => {
|
render: (text: string, record: any) => {
|
||||||
return (
|
return (
|
||||||
<Tooltip placement="topLeft" title={text}>
|
<Tooltip
|
||||||
|
placement="topLeft"
|
||||||
|
title={text}
|
||||||
|
trigger={['hover', 'click']}
|
||||||
|
>
|
||||||
<span>{text}</span>
|
<span>{text}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
@@ -143,12 +140,50 @@ const Env = () => {
|
|||||||
key: 'remarks',
|
key: 'remarks',
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '更新时间',
|
||||||
|
dataIndex: 'timestamp',
|
||||||
|
key: 'timestamp',
|
||||||
|
align: 'center' as const,
|
||||||
|
width: 165,
|
||||||
|
ellipsis: {
|
||||||
|
showTitle: false,
|
||||||
|
},
|
||||||
|
render: (text: string, record: any) => {
|
||||||
|
const language = navigator.language || navigator.languages[0];
|
||||||
|
const date = new Date(text)
|
||||||
|
.toLocaleString(language, {
|
||||||
|
hour12: false,
|
||||||
|
})
|
||||||
|
.replace(' 24:', ' 00:');
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
placement="topLeft"
|
||||||
|
title={date}
|
||||||
|
trigger={['hover', 'click']}
|
||||||
|
>
|
||||||
|
<span>{date}</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
key: 'status',
|
key: 'status',
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
width: 60,
|
width: 70,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
text: '已启用',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '已禁用',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onFilter: (value: number, record: any) => record.status === value,
|
||||||
render: (text: string, record: any, index: number) => {
|
render: (text: string, record: any, index: number) => {
|
||||||
return (
|
return (
|
||||||
<Space size="middle" style={{ cursor: 'text' }}>
|
<Space size="middle" style={{ cursor: 'text' }}>
|
||||||
@@ -162,11 +197,12 @@ const Env = () => {
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
|
width: 100,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
render: (text: string, record: any, index: number) => {
|
render: (text: string, record: any, index: number) => {
|
||||||
const isPc = !isPhone;
|
const isPc = !isPhone;
|
||||||
return (
|
return (
|
||||||
<Space size="middle" style={{ paddingLeft: 8 }}>
|
<Space size="middle">
|
||||||
<Tooltip title={isPc ? '编辑' : ''}>
|
<Tooltip title={isPc ? '编辑' : ''}>
|
||||||
<a onClick={() => editEnv(record, index)}>
|
<a onClick={() => editEnv(record, index)}>
|
||||||
<EditOutlined />
|
<EditOutlined />
|
||||||
@@ -202,7 +238,7 @@ const Env = () => {
|
|||||||
const [editedEnv, setEditedEnv] = useState();
|
const [editedEnv, setEditedEnv] = useState();
|
||||||
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
|
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const { headerStyle, isPhone } = useCtx();
|
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
|
||||||
|
|
||||||
const getEnvs = () => {
|
const getEnvs = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -278,7 +314,7 @@ const Env = () => {
|
|||||||
<>
|
<>
|
||||||
确认删除变量{' '}
|
确认删除变量{' '}
|
||||||
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||||
{record.value}
|
{record.name}: {record.value}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
吗
|
吗
|
||||||
</>
|
</>
|
||||||
@@ -317,7 +353,8 @@ const Env = () => {
|
|||||||
const result = [...value];
|
const result = [...value];
|
||||||
const index = value.findIndex((x) => x._id === env._id);
|
const index = value.findIndex((x) => x._id === env._id);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
result.push(env);
|
env = Array.isArray(env) ? env : [env];
|
||||||
|
result.push(...env);
|
||||||
} else {
|
} else {
|
||||||
result.splice(index, 1, {
|
result.splice(index, 1, {
|
||||||
...env,
|
...env,
|
||||||
@@ -338,16 +375,17 @@ const Env = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const dragRow = value[dragIndex];
|
const dragRow = value[dragIndex];
|
||||||
const newData = [...value];
|
|
||||||
newData.splice(dragIndex, 1);
|
|
||||||
newData.splice(hoverIndex, 0, dragRow);
|
|
||||||
setValue([...newData]);
|
|
||||||
request
|
request
|
||||||
.put(`${config.apiPrefix}envs/${dragRow._id}/move`, {
|
.put(`${config.apiPrefix}envs/${dragRow._id}/move`, {
|
||||||
data: { fromIndex: dragIndex, toIndex: hoverIndex },
|
data: { fromIndex: dragIndex, toIndex: hoverIndex },
|
||||||
})
|
})
|
||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
if (data.code !== 200) {
|
if (data.code === 200) {
|
||||||
|
const newData = [...value];
|
||||||
|
newData.splice(dragIndex, 1);
|
||||||
|
newData.splice(hoverIndex, 0, { ...dragRow, ...data.data });
|
||||||
|
setValue([...newData]);
|
||||||
|
} else {
|
||||||
message.error(data);
|
message.error(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -357,6 +395,12 @@ const Env = () => {
|
|||||||
|
|
||||||
const onSelectChange = (selectedIds: any[]) => {
|
const onSelectChange = (selectedIds: any[]) => {
|
||||||
setSelectedRowIds(selectedIds);
|
setSelectedRowIds(selectedIds);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
|
||||||
|
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const rowSelection = {
|
const rowSelection = {
|
||||||
@@ -415,13 +459,19 @@ const Env = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSearch = (value: string) => {
|
const onSearch = (value: string) => {
|
||||||
setSearchText(value);
|
setSearchText(value.trim());
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getEnvs();
|
getEnvs();
|
||||||
}, [searchText]);
|
}, [searchText]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
className="ql-container-wrapper env-wrapper"
|
className="ql-container-wrapper env-wrapper"
|
||||||
@@ -486,7 +536,7 @@ const Env = () => {
|
|||||||
dataSource={value}
|
dataSource={value}
|
||||||
rowKey="_id"
|
rowKey="_id"
|
||||||
size="middle"
|
size="middle"
|
||||||
scroll={{ x: 768 }}
|
scroll={{ x: 1000, y: tableScrollHeight }}
|
||||||
components={components}
|
components={components}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onRow={(record: any, index: number) => {
|
onRow={(record: any, index: number) => {
|
||||||
|
|||||||
Vendored
+43
-5
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Modal, message, Input, Form } from 'antd';
|
import { Modal, message, Input, Form, Radio } from 'antd';
|
||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
|
|
||||||
@@ -17,8 +17,25 @@ const EnvModal = ({
|
|||||||
|
|
||||||
const handleOk = async (values: any) => {
|
const handleOk = async (values: any) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
const { value, split, name, remarks } = values;
|
||||||
const method = env ? 'put' : 'post';
|
const method = env ? 'put' : 'post';
|
||||||
const payload = env ? { ...values, _id: env._id } : values;
|
let payload;
|
||||||
|
if (!env) {
|
||||||
|
if (split === '1') {
|
||||||
|
const symbol = value.includes('&') ? '&' : '\n';
|
||||||
|
payload = value.split(symbol).map((x: any) => {
|
||||||
|
return {
|
||||||
|
name: name,
|
||||||
|
value: x,
|
||||||
|
remarks: remarks,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
payload = [{ value, name, remarks }];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
payload = { ...values, _id: env._id };
|
||||||
|
}
|
||||||
const { code, data } = await request[method](`${config.apiPrefix}envs`, {
|
const { code, data } = await request[method](`${config.apiPrefix}envs`, {
|
||||||
data: payload,
|
data: payload,
|
||||||
});
|
});
|
||||||
@@ -57,14 +74,35 @@ const EnvModal = ({
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
name="name"
|
name="name"
|
||||||
label="名称"
|
label="名称"
|
||||||
rules={[{ required: true, message: '请输入环境变量名称' }]}
|
rules={[
|
||||||
|
{ required: true, message: '请输入环境变量名称', whitespace: true },
|
||||||
|
{
|
||||||
|
pattern: /^[a-zA-Z_][0-9a-zA-Z_]*$/,
|
||||||
|
message: '只能输入字母数字下划线,且不能以数字开头',
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder="请输入环境变量名称" />
|
<Input placeholder="请输入环境变量名称" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
{!env && (
|
||||||
|
<Form.Item
|
||||||
|
name="split"
|
||||||
|
label="自动拆分"
|
||||||
|
initialValue="0"
|
||||||
|
tooltip="多个依赖是否换行分割"
|
||||||
|
>
|
||||||
|
<Radio.Group>
|
||||||
|
<Radio value="1">是</Radio>
|
||||||
|
<Radio value="0">否</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="value"
|
name="value"
|
||||||
label="值"
|
label="值"
|
||||||
rules={[{ required: true, message: '请输入环境变量值' }]}
|
rules={[
|
||||||
|
{ required: true, message: '请输入环境变量值', whitespace: true },
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
rows={4}
|
rows={4}
|
||||||
@@ -73,7 +111,7 @@ const EnvModal = ({
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="remarks" label="备注">
|
<Form.Item name="remarks" label="备注">
|
||||||
<Input />
|
<Input placeholder="请输入备注" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
@import '~antd/es/style/themes/default.less';
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
height: calc(100vh - var(--vh-offset, 0px));
|
||||||
|
overflow: auto;
|
||||||
|
background: @layout-body-background;
|
||||||
|
padding-top: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: @screen-md-min) {
|
||||||
|
.container {
|
||||||
|
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center 110px;
|
||||||
|
background-size: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.top {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 48px;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
color: @text-color-secondary;
|
||||||
|
font-size: @font-size-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
border: 1px solid #ebedef;
|
||||||
|
display: flex;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90%;
|
||||||
|
height: 350px;
|
||||||
|
|
||||||
|
.ant-steps {
|
||||||
|
width: 35%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
top: 6%;
|
||||||
|
}
|
||||||
|
.steps-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.extra {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
import React, { Fragment, useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Row,
|
||||||
|
Input,
|
||||||
|
Form,
|
||||||
|
message,
|
||||||
|
Typography,
|
||||||
|
Steps,
|
||||||
|
Select,
|
||||||
|
} from 'antd';
|
||||||
|
import config from '@/utils/config';
|
||||||
|
import { history } from 'umi';
|
||||||
|
import styles from './index.less';
|
||||||
|
import { request } from '@/utils/http';
|
||||||
|
|
||||||
|
const FormItem = Form.Item;
|
||||||
|
const { Step } = Steps;
|
||||||
|
const { Option } = Select;
|
||||||
|
const { Link } = Typography;
|
||||||
|
|
||||||
|
const Initialization = () => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [current, setCurrent] = React.useState(0);
|
||||||
|
const [fields, setFields] = useState<any[]>([]);
|
||||||
|
|
||||||
|
const next = () => {
|
||||||
|
setCurrent(current + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const prev = () => {
|
||||||
|
setCurrent(current - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitAccountSetting = (values: any) => {
|
||||||
|
setLoading(true);
|
||||||
|
request
|
||||||
|
.put(`${config.apiPrefix}init/user`, {
|
||||||
|
data: {
|
||||||
|
username: values.username,
|
||||||
|
password: values.password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
if (data.code === 200) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
message.error(data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitNotification = (values: any) => {
|
||||||
|
request
|
||||||
|
.put(`${config.apiPrefix}init/notification`, {
|
||||||
|
data: {
|
||||||
|
...values,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((_data: any) => {
|
||||||
|
if (_data && _data.code === 200) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
message.error(_data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const notificationModeChange = (value: string) => {
|
||||||
|
const _fields = (config.notificationModeMap as any)[value];
|
||||||
|
setFields(_fields || []);
|
||||||
|
};
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
title: '欢迎使用',
|
||||||
|
content: (
|
||||||
|
<div className={styles.top} style={{ marginTop: 100 }}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<span className={styles.title}>欢迎使用青龙控制面板</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.action}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
next();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
开始安装
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '通知设置',
|
||||||
|
content: (
|
||||||
|
<Form onFinish={submitNotification} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
label="通知方式"
|
||||||
|
name="type"
|
||||||
|
rules={[{ required: true, message: '请选择通知方式' }]}
|
||||||
|
style={{ maxWidth: 350 }}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
onChange={notificationModeChange}
|
||||||
|
placeholder="请选择通知方式"
|
||||||
|
>
|
||||||
|
{config.notificationModes
|
||||||
|
.filter((x) => x.value !== 'closed')
|
||||||
|
.map((x) => (
|
||||||
|
<Option value={x.value}>{x.label}</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
{fields.map((x) => (
|
||||||
|
<Form.Item
|
||||||
|
label={x.label}
|
||||||
|
name={x.label}
|
||||||
|
extra={x.tip}
|
||||||
|
rules={[{ required: x.required }]}
|
||||||
|
style={{ maxWidth: 400 }}
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
autoSize={true}
|
||||||
|
placeholder={`请输入${x.label}`}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
<Button type="primary" htmlType="submit" loading={loading}>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
<Button type="link" htmlType="button" onClick={() => next()}>
|
||||||
|
跳过
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '账户设置',
|
||||||
|
content: (
|
||||||
|
<Form onFinish={submitAccountSetting} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
label="用户名"
|
||||||
|
name="username"
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
style={{ maxWidth: 350 }}
|
||||||
|
>
|
||||||
|
<Input placeholder="用户名" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="密码"
|
||||||
|
name="password"
|
||||||
|
rules={[
|
||||||
|
{ required: true },
|
||||||
|
{
|
||||||
|
pattern: /^(?!admin$).*$/,
|
||||||
|
message: '密码不能为admin',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
hasFeedback
|
||||||
|
style={{ maxWidth: 350 }}
|
||||||
|
>
|
||||||
|
<Input type="password" placeholder="密码" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="confirm"
|
||||||
|
label="确认密码"
|
||||||
|
dependencies={['password']}
|
||||||
|
hasFeedback
|
||||||
|
style={{ maxWidth: 350 }}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
({ getFieldValue }) => ({
|
||||||
|
validator(_, value) {
|
||||||
|
if (!value || getFieldValue('password') === value) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error('您输入的两个密码不匹配!'));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="确认密码" />
|
||||||
|
</Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit" loading={loading}>
|
||||||
|
提交
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '完成安装',
|
||||||
|
content: (
|
||||||
|
<div className={styles.top} style={{ marginTop: 80 }}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<span className={styles.title}>恭喜安装完成!</span>
|
||||||
|
<Link href="https://github.com/whyour/qinglong" target="_blank">
|
||||||
|
Github
|
||||||
|
</Link>
|
||||||
|
<Link href="https://t.me/jiao_long" target="_blank">
|
||||||
|
Telegram频道
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
history.push('/login');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
去登录
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.top}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<img
|
||||||
|
alt="logo"
|
||||||
|
className={styles.logo}
|
||||||
|
src="https://pic.imgdb.cn/item/61acd0dd2ab3f51d912b1986.png"
|
||||||
|
/>
|
||||||
|
<span className={styles.title}>初始化配置</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.main}>
|
||||||
|
<Steps
|
||||||
|
current={current}
|
||||||
|
direction="vertical"
|
||||||
|
size="small"
|
||||||
|
className={styles['ant-steps']}
|
||||||
|
>
|
||||||
|
{steps.map((item) => (
|
||||||
|
<Step key={item.title} title={item.title} />
|
||||||
|
))}
|
||||||
|
</Steps>
|
||||||
|
<div className={styles['steps-container']}>
|
||||||
|
{steps[current].content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Initialization;
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
height: calc(100vh - 128px);
|
height: calc(100vh - 128px);
|
||||||
height: calc(100vh - var(--vh-offset, 0px) - 128px);
|
height: calc(100vh - var(--vh-offset, 0px) - 128px);
|
||||||
width: @tree-width;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -15,9 +14,11 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border-right: 1px dashed #f0f0f0;
|
border-right: 1px dashed #f0f0f0;
|
||||||
|
padding-top: 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-container {
|
.log-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-40
@@ -7,6 +7,7 @@ import { request } from '@/utils/http';
|
|||||||
import styles from './index.module.less';
|
import styles from './index.module.less';
|
||||||
import { Controlled as CodeMirror } from 'react-codemirror2';
|
import { Controlled as CodeMirror } from 'react-codemirror2';
|
||||||
import { useCtx, useTheme } from '@/utils/hooks';
|
import { useCtx, useTheme } from '@/utils/hooks';
|
||||||
|
import SplitPane from 'react-split-pane';
|
||||||
|
|
||||||
function getFilterData(keyword: string, data: any) {
|
function getFilterData(keyword: string, data: any) {
|
||||||
const expandedKeys: string[] = [];
|
const expandedKeys: string[] = [];
|
||||||
@@ -15,7 +16,6 @@ function getFilterData(keyword: string, data: any) {
|
|||||||
data.forEach((item: any) => {
|
data.forEach((item: any) => {
|
||||||
if (item.title.toLocaleLowerCase().includes(keyword)) {
|
if (item.title.toLocaleLowerCase().includes(keyword)) {
|
||||||
tree.push(item);
|
tree.push(item);
|
||||||
expandedKeys.push(...item.children.map((x: any) => x.key));
|
|
||||||
} else {
|
} else {
|
||||||
const children: any[] = [];
|
const children: any[] = [];
|
||||||
(item.children || []).forEach((subItem: any) => {
|
(item.children || []).forEach((subItem: any) => {
|
||||||
@@ -28,7 +28,7 @@ function getFilterData(keyword: string, data: any) {
|
|||||||
...item,
|
...item,
|
||||||
children,
|
children,
|
||||||
});
|
});
|
||||||
expandedKeys.push(...children.map((x) => x.key));
|
expandedKeys.push(item.key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -37,17 +37,16 @@ function getFilterData(keyword: string, data: any) {
|
|||||||
return { tree: data, expandedKeys };
|
return { tree: data, expandedKeys };
|
||||||
}
|
}
|
||||||
|
|
||||||
const Log = () => {
|
const Log = ({ headerStyle, isPhone, theme }: any) => {
|
||||||
const [title, setTitle] = useState('请选择日志文件');
|
const [title, setTitle] = useState('请选择日志文件');
|
||||||
const [value, setValue] = useState('请选择日志文件');
|
const [value, setValue] = useState('请选择日志文件');
|
||||||
const [select, setSelect] = useState();
|
const [select, setSelect] = useState<any>();
|
||||||
const [data, setData] = useState<any[]>([]);
|
const [data, setData] = useState<any[]>([]);
|
||||||
const [filterData, setFilterData] = useState<any[]>([]);
|
const [filterData, setFilterData] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [height, setHeight] = useState<number>();
|
const [height, setHeight] = useState<number>();
|
||||||
const treeDom = useRef<any>();
|
const treeDom = useRef<any>();
|
||||||
const { headerStyle, isPhone } = useCtx();
|
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
||||||
const { theme } = useTheme();
|
|
||||||
|
|
||||||
const getLogs = () => {
|
const getLogs = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -85,6 +84,10 @@ const Log = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSelect = (value: any, node: any) => {
|
const onSelect = (value: any, node: any) => {
|
||||||
|
if (node.key === select || !value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setValue('加载中...');
|
||||||
setSelect(value);
|
setSelect(value);
|
||||||
setTitle(node.parent || node.value);
|
setTitle(node.parent || node.value);
|
||||||
getLog(node);
|
getLog(node);
|
||||||
@@ -97,15 +100,21 @@ const Log = () => {
|
|||||||
const onSearch = useCallback(
|
const onSearch = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
const keyword = e.target.value;
|
const keyword = e.target.value;
|
||||||
const { tree } = getFilterData(keyword.toLocaleLowerCase(), data);
|
const { tree, expandedKeys } = getFilterData(
|
||||||
|
keyword.toLocaleLowerCase(),
|
||||||
|
data,
|
||||||
|
);
|
||||||
setFilterData(tree);
|
setFilterData(tree);
|
||||||
|
setExpandedKeys(expandedKeys);
|
||||||
},
|
},
|
||||||
[data, setFilterData],
|
[data, setFilterData],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getLogs();
|
getLogs();
|
||||||
setHeight(treeDom.current.clientHeight);
|
if (treeDom && treeDom.current) {
|
||||||
|
setHeight(treeDom.current.clientHeight);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -122,7 +131,6 @@ const Log = () => {
|
|||||||
treeData={data}
|
treeData={data}
|
||||||
placeholder="请选择日志文件"
|
placeholder="请选择日志文件"
|
||||||
showSearch
|
showSearch
|
||||||
key="value"
|
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
/>,
|
/>,
|
||||||
]
|
]
|
||||||
@@ -133,24 +141,41 @@ const Log = () => {
|
|||||||
>
|
>
|
||||||
<div className={`${styles['log-container']} log-container`}>
|
<div className={`${styles['log-container']} log-container`}>
|
||||||
{!isPhone && (
|
{!isPhone && (
|
||||||
<div className={styles['left-tree-container']}>
|
<SplitPane split="vertical" size={200} maxSize={-100}>
|
||||||
<Input.Search
|
<div className={styles['left-tree-container']}>
|
||||||
className={styles['left-tree-search']}
|
<Input.Search
|
||||||
onChange={onSearch}
|
className={styles['left-tree-search']}
|
||||||
></Input.Search>
|
onChange={onSearch}
|
||||||
<div className={styles['left-tree-scroller']} ref={treeDom}>
|
></Input.Search>
|
||||||
<Tree
|
<div className={styles['left-tree-scroller']} ref={treeDom}>
|
||||||
className={styles['left-tree']}
|
<Tree
|
||||||
treeData={filterData}
|
className={styles['left-tree']}
|
||||||
showIcon={true}
|
treeData={filterData}
|
||||||
height={height}
|
showIcon={true}
|
||||||
showLine={{ showLeafIcon: true }}
|
height={height}
|
||||||
onSelect={onTreeSelect}
|
selectedKeys={[select]}
|
||||||
></Tree>
|
showLine={{ showLeafIcon: true }}
|
||||||
|
onSelect={onTreeSelect}
|
||||||
|
></Tree>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<Editor
|
||||||
|
language="shell"
|
||||||
|
theme={theme}
|
||||||
|
value={value}
|
||||||
|
options={{
|
||||||
|
readOnly: true,
|
||||||
|
fontSize: 12,
|
||||||
|
lineNumbersMinChars: 3,
|
||||||
|
fontFamily: 'Source Code Pro',
|
||||||
|
folding: false,
|
||||||
|
glyphMargin: false,
|
||||||
|
wordWrap: 'on',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SplitPane>
|
||||||
)}
|
)}
|
||||||
{isPhone ? (
|
{isPhone && (
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
value={value}
|
value={value}
|
||||||
options={{
|
options={{
|
||||||
@@ -165,21 +190,6 @@ const Log = () => {
|
|||||||
}}
|
}}
|
||||||
onChange={(editor, data, value) => {}}
|
onChange={(editor, data, value) => {}}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<Editor
|
|
||||||
language="shell"
|
|
||||||
theme={theme}
|
|
||||||
value={value}
|
|
||||||
options={{
|
|
||||||
readOnly: true,
|
|
||||||
fontSize: 12,
|
|
||||||
lineNumbersMinChars: 3,
|
|
||||||
fontFamily: 'Source Code Pro',
|
|
||||||
folding: false,
|
|
||||||
glyphMargin: false,
|
|
||||||
wordWrap: 'on',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
|
|||||||
+23
-27
@@ -3,31 +3,12 @@
|
|||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
height: calc(100vh - var(--vh-offset, 0px));
|
height: calc(100vh - var(--vh-offset, 0px));
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background: @layout-body-background;
|
background: @layout-body-background;
|
||||||
}
|
padding-top: 70px;
|
||||||
|
|
||||||
.lang {
|
|
||||||
width: 100%;
|
|
||||||
height: 40px;
|
|
||||||
line-height: 44px;
|
|
||||||
text-align: right;
|
|
||||||
:global(.ant-dropdown-trigger) {
|
|
||||||
margin-right: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
position: absolute;
|
|
||||||
top: 45%;
|
|
||||||
left: 50%;
|
|
||||||
margin: -160px 0 0 -160px;
|
|
||||||
width: 320px;
|
|
||||||
height: 320px;
|
|
||||||
padding: 36px;
|
|
||||||
box-shadow: 0 0 100px rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: @screen-md-min) {
|
@media (min-width: @screen-md-min) {
|
||||||
@@ -44,16 +25,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
height: 44px;
|
|
||||||
line-height: 44px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
width: 30px;
|
width: 48px;
|
||||||
margin-right: 8px;
|
height: 48px;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.desc {
|
.desc {
|
||||||
@@ -64,7 +50,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
margin: 35px auto 0;
|
padding: 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
border: 1px solid #ebedef;
|
||||||
|
width: 340px;
|
||||||
|
|
||||||
@media screen and (max-width: @screen-sm) {
|
@media screen and (max-width: @screen-sm) {
|
||||||
width: 95%;
|
width: 95%;
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
@@ -104,3 +95,8 @@
|
|||||||
font-size: @font-size-base;
|
font-size: @font-size-base;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.extra {
|
||||||
|
margin-top: 20px;
|
||||||
|
width: 340px;
|
||||||
|
}
|
||||||
|
|||||||
+166
-35
@@ -1,14 +1,35 @@
|
|||||||
import React, { Fragment, useEffect } from 'react';
|
import React, { Fragment, useEffect, useState } from 'react';
|
||||||
import { Button, Row, Input, Form, message } from 'antd';
|
import {
|
||||||
|
Button,
|
||||||
|
Row,
|
||||||
|
Input,
|
||||||
|
Form,
|
||||||
|
message,
|
||||||
|
notification,
|
||||||
|
Statistic,
|
||||||
|
} from 'antd';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
import { history, Link } from 'umi';
|
import { history, Link } from 'umi';
|
||||||
import styles from './index.less';
|
import styles from './index.less';
|
||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
|
import { useTheme } from '@/utils/hooks';
|
||||||
|
import { MobileOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
const FormItem = Form.Item;
|
const FormItem = Form.Item;
|
||||||
|
const { Countdown } = Statistic;
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [waitTime, setWaitTime] = useState<any>();
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const [twoFactor, setTwoFactor] = useState(false);
|
||||||
|
const [verifying, setVerifying] = useState(false);
|
||||||
|
const [loginInfo, setLoginInfo] = useState<any>();
|
||||||
|
|
||||||
const handleOk = (values: any) => {
|
const handleOk = (values: any) => {
|
||||||
|
setLoading(true);
|
||||||
|
setTwoFactor(false);
|
||||||
|
setWaitTime(null);
|
||||||
request
|
request
|
||||||
.post(`${config.apiPrefix}login`, {
|
.post(`${config.apiPrefix}login`, {
|
||||||
data: {
|
data: {
|
||||||
@@ -17,20 +38,81 @@ const Login = () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.code === 200) {
|
if (data.code === 420) {
|
||||||
localStorage.setItem(config.authKey, data.token);
|
setLoginInfo({
|
||||||
history.push('/crontab');
|
username: values.username,
|
||||||
} else if (data.code === 100) {
|
password: values.password,
|
||||||
message.warn(data.message);
|
});
|
||||||
|
setTwoFactor(true);
|
||||||
} else {
|
} else {
|
||||||
message.error(data.message);
|
checkResponse(data);
|
||||||
}
|
}
|
||||||
|
setLoading(false);
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
setLoading(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const completeTowFactor = (values: any) => {
|
||||||
|
setVerifying(true);
|
||||||
|
request
|
||||||
|
.put(`${config.apiPrefix}user/two-factor/login`, {
|
||||||
|
data: { ...loginInfo, code: values.code },
|
||||||
|
})
|
||||||
|
.then((data: any) => {
|
||||||
|
if (data.code === 430) {
|
||||||
|
message.error(data.message);
|
||||||
|
} else {
|
||||||
|
checkResponse(data);
|
||||||
|
}
|
||||||
|
setVerifying(false);
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
console.log(error);
|
||||||
|
setVerifying(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkResponse = (data: any) => {
|
||||||
|
if (data.code === 200) {
|
||||||
|
const {
|
||||||
|
token,
|
||||||
|
lastip,
|
||||||
|
lastaddr,
|
||||||
|
lastlogon,
|
||||||
|
retries = 0,
|
||||||
|
platform,
|
||||||
|
} = data.data;
|
||||||
|
localStorage.setItem(config.authKey, token);
|
||||||
|
notification.success({
|
||||||
|
message: '登录成功!',
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
上次登录时间:
|
||||||
|
{lastlogon ? new Date(lastlogon).toLocaleString() : '-'}
|
||||||
|
</div>
|
||||||
|
<div>上次登录地点:{lastaddr || '-'}</div>
|
||||||
|
<div>上次登录IP:{lastip || '-'}</div>
|
||||||
|
<div>上次登录设备:{platform || '-'}</div>
|
||||||
|
<div>上次登录状态:{retries > 0 ? `失败${retries}次` : '成功'}</div>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
duration: 5,
|
||||||
|
});
|
||||||
|
history.push('/crontab');
|
||||||
|
} else if (data.code === 100) {
|
||||||
|
message.warn(data.message);
|
||||||
|
} else if (data.code === 410) {
|
||||||
|
message.error(data.message);
|
||||||
|
setWaitTime(data.data);
|
||||||
|
} else {
|
||||||
|
message.error(data.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isAuth = localStorage.getItem(config.authKey);
|
const isAuth = localStorage.getItem(config.authKey);
|
||||||
if (isAuth) {
|
if (isAuth) {
|
||||||
@@ -40,44 +122,93 @@ const Login = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.content}>
|
<div className={styles.top}>
|
||||||
<div className={styles.top}>
|
<div className={styles.header}>
|
||||||
<div className={styles.header}>
|
<img
|
||||||
<img
|
alt="logo"
|
||||||
alt="logo"
|
className={styles.logo}
|
||||||
className={styles.logo}
|
src="https://pic.imgdb.cn/item/61acd0dd2ab3f51d912b1986.png"
|
||||||
src="https://qinglong.whyour.cn/qinglong.png"
|
/>
|
||||||
/>
|
<span className={styles.title}>
|
||||||
<span className={styles.title}>{config.siteName}</span>
|
{twoFactor ? '两步验证' : config.siteName}
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.main}>
|
</div>
|
||||||
<Form onFinish={handleOk}>
|
<div className={styles.main}>
|
||||||
|
{twoFactor ? (
|
||||||
|
<Form layout="vertical" onFinish={completeTowFactor}>
|
||||||
<FormItem
|
<FormItem
|
||||||
name="username"
|
name="code"
|
||||||
rules={[{ required: true, message: '请输入用户名' }]}
|
label="验证码"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
pattern: /^[0-9]{6}$/,
|
||||||
|
message: '验证码为6位数字',
|
||||||
|
validateTrigger: 'onBlur',
|
||||||
|
},
|
||||||
|
]}
|
||||||
hasFeedback
|
hasFeedback
|
||||||
>
|
>
|
||||||
|
<Input placeholder="6位数字" autoFocus autoComplete="off" />
|
||||||
|
</FormItem>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
loading={verifying}
|
||||||
|
>
|
||||||
|
验证
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
) : (
|
||||||
|
<Form layout="vertical" onFinish={handleOk}>
|
||||||
|
<FormItem name="username" label="用户名" hasFeedback>
|
||||||
<Input placeholder="用户名" autoFocus />
|
<Input placeholder="用户名" autoFocus />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem
|
<FormItem name="password" label="密码" hasFeedback>
|
||||||
name="password"
|
|
||||||
rules={[{ required: true, message: '请输入密码' }]}
|
|
||||||
hasFeedback
|
|
||||||
>
|
|
||||||
<Input type="password" placeholder="密码" />
|
<Input type="password" placeholder="密码" />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<Row>
|
<Row>
|
||||||
<Button
|
{waitTime ? (
|
||||||
type="primary"
|
<Button type="primary" style={{ width: '100%' }} disabled>
|
||||||
htmlType="submit"
|
请
|
||||||
style={{ width: '100%' }}
|
<Countdown
|
||||||
>
|
valueStyle={{
|
||||||
登录
|
color:
|
||||||
</Button>
|
theme === 'vs'
|
||||||
|
? 'rgba(0,0,0,.25)'
|
||||||
|
: 'rgba(232, 230, 227, 0.25)',
|
||||||
|
}}
|
||||||
|
className="inline-countdown"
|
||||||
|
onFinish={() => setWaitTime(null)}
|
||||||
|
format="ss"
|
||||||
|
value={Date.now() + 1000 * waitTime}
|
||||||
|
/>
|
||||||
|
秒后重试
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.extra}>
|
||||||
|
{twoFactor ? (
|
||||||
|
<div style={{ paddingLeft: 20, position: 'relative' }}>
|
||||||
|
<MobileOutlined style={{ position: 'absolute', left: 0, top: 4 }} />
|
||||||
|
在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,24 +24,26 @@ const prefixMap: any = {
|
|||||||
|
|
||||||
const EditModal = ({
|
const EditModal = ({
|
||||||
treeData,
|
treeData,
|
||||||
currentFile,
|
currentNode,
|
||||||
content,
|
content,
|
||||||
handleCancel,
|
handleCancel,
|
||||||
visible,
|
visible,
|
||||||
|
socketMessage,
|
||||||
}: {
|
}: {
|
||||||
treeData?: any;
|
treeData?: any;
|
||||||
currentFile?: string;
|
|
||||||
content?: string;
|
content?: string;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
socketMessage: any;
|
||||||
|
currentNode: any;
|
||||||
handleCancel: () => void;
|
handleCancel: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
const [language, setLanguage] = useState<string>('javascript');
|
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 [saveModalVisible, setSaveModalVisible] = useState<boolean>(false);
|
||||||
const [settingModalVisible, setSettingModalVisible] =
|
const [settingModalVisible, setSettingModalVisible] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
const [isNewFile, setIsNewFile] = useState<boolean>(false);
|
|
||||||
const [log, setLog] = useState<string>('');
|
const [log, setLog] = useState<string>('');
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
@@ -51,56 +53,79 @@ const EditModal = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSelect = (value: any, node: any) => {
|
const onSelect = (value: any, node: any) => {
|
||||||
|
if (node.key === selectedKey || !value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const newMode = LangMap[value.slice(-3)] || '';
|
const newMode = LangMap[value.slice(-3)] || '';
|
||||||
setFileName(value);
|
setCNode(node);
|
||||||
setLanguage(newMode);
|
setLanguage(newMode);
|
||||||
setIsNewFile(false);
|
|
||||||
getDetail(node);
|
getDetail(node);
|
||||||
|
setSelectedKey(node.key);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDetail = (node: any) => {
|
const getDetail = (node: any) => {
|
||||||
request.get(`${config.apiPrefix}scripts/${node.value}`).then((data) => {
|
request
|
||||||
setValue(data.data);
|
.get(`${config.apiPrefix}scripts/${node.value}?path=${node.parent || ''}`)
|
||||||
});
|
.then((data) => {
|
||||||
|
setValue(data.data);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const createFile = () => {
|
const run = () => {
|
||||||
setFileName(`未命名${prefixMap[language]}`);
|
setLog('');
|
||||||
setIsNewFile(true);
|
request
|
||||||
setValue('');
|
.put(`${config.apiPrefix}scripts/run`, {
|
||||||
|
data: {
|
||||||
|
filename: cNode.value,
|
||||||
|
path: cNode.parent || '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((data) => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
const run = () => {};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentFile) {
|
if (!socketMessage) {
|
||||||
createFile();
|
return;
|
||||||
} else {
|
|
||||||
setFileName(currentFile);
|
|
||||||
setValue(content as string);
|
|
||||||
}
|
}
|
||||||
setIsNewFile(!currentFile);
|
|
||||||
}, []);
|
let { type, message: _message, references } = socketMessage;
|
||||||
|
|
||||||
|
if (type !== 'manuallyRunScript') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log) {
|
||||||
|
_message = `\n${_message}`;
|
||||||
|
}
|
||||||
|
setLog(`${log}${_message}`);
|
||||||
|
}, [socketMessage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentNode) {
|
||||||
|
setCNode(currentNode);
|
||||||
|
setValue(content as string);
|
||||||
|
setSelectedKey(currentNode.key);
|
||||||
|
}
|
||||||
|
}, [content, currentNode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
className="edit-modal"
|
className="edit-modal"
|
||||||
|
closable={false}
|
||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
<span style={{ marginRight: 8 }}>{fileName}</span>
|
|
||||||
<TreeSelect
|
<TreeSelect
|
||||||
style={{ marginRight: 8, width: 120 }}
|
style={{ marginRight: 8, width: 150 }}
|
||||||
value={currentFile}
|
value={selectedKey}
|
||||||
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
||||||
treeData={treeData}
|
treeData={treeData}
|
||||||
placeholder="请选择脚本文件"
|
placeholder="请选择脚本文件"
|
||||||
showSearch
|
showSearch
|
||||||
key="value"
|
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={language}
|
value={language}
|
||||||
style={{ width: 120, marginRight: 8 }}
|
style={{ width: 110, marginRight: 8 }}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setLanguage(e);
|
setLanguage(e);
|
||||||
}}
|
}}
|
||||||
@@ -131,13 +156,6 @@ const EditModal = ({
|
|||||||
>
|
>
|
||||||
设置
|
设置
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
style={{ marginRight: 8 }}
|
|
||||||
onClick={createFile}
|
|
||||||
>
|
|
||||||
新建文件
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
style={{ marginRight: 8 }}
|
style={{ marginRight: 8 }}
|
||||||
@@ -147,6 +165,15 @@ const EditModal = ({
|
|||||||
>
|
>
|
||||||
保存
|
保存
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
style={{ marginRight: 8 }}
|
||||||
|
onClick={() => {
|
||||||
|
handleCancel();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
退出
|
||||||
|
</Button>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
width={'100%'}
|
width={'100%'}
|
||||||
@@ -178,8 +205,12 @@ const EditModal = ({
|
|||||||
handleCancel={() => {
|
handleCancel={() => {
|
||||||
setSaveModalVisible(false);
|
setSaveModalVisible(false);
|
||||||
}}
|
}}
|
||||||
isNewFile={isNewFile}
|
file={{
|
||||||
file={{ content: editorRef.current && editorRef.current.getValue().replace(/\r\n/g, '\n'), filename: fileName }}
|
content:
|
||||||
|
editorRef.current &&
|
||||||
|
editorRef.current.getValue().replace(/\r\n/g, '\n'),
|
||||||
|
filename: cNode?.value,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<SettingModal
|
<SettingModal
|
||||||
visible={settingModalVisible}
|
visible={settingModalVisible}
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Modal, message, Input, Form, Select } from 'antd';
|
||||||
|
import { request } from '@/utils/http';
|
||||||
|
import config from '@/utils/config';
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
const EditScriptNameModal = ({
|
||||||
|
handleCancel,
|
||||||
|
treeData,
|
||||||
|
visible,
|
||||||
|
}: {
|
||||||
|
visible: boolean;
|
||||||
|
treeData: any[];
|
||||||
|
handleCancel: (file?: {
|
||||||
|
filename: string;
|
||||||
|
path: string;
|
||||||
|
key: string;
|
||||||
|
}) => void;
|
||||||
|
}) => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [dirs, setDirs] = useState<any[]>([]);
|
||||||
|
|
||||||
|
const handleOk = async (values: any) => {
|
||||||
|
setLoading(true);
|
||||||
|
values.path = values.path || '';
|
||||||
|
request
|
||||||
|
.post(`${config.apiPrefix}scripts`, {
|
||||||
|
data: { filename: values.filename, path: values.path, content: '' },
|
||||||
|
})
|
||||||
|
.then(({ code, data }) => {
|
||||||
|
if (code === 200) {
|
||||||
|
message.success('保存文件成功');
|
||||||
|
handleCancel({
|
||||||
|
filename: values.filename,
|
||||||
|
path: values.path,
|
||||||
|
key: `${values.path}-${values.filename}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
message.error(data);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.resetFields();
|
||||||
|
const originDirs = treeData.filter((x) => x.disabled);
|
||||||
|
setDirs([{ key: '' }, ...originDirs]);
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="新建文件"
|
||||||
|
visible={visible}
|
||||||
|
forceRender
|
||||||
|
onOk={() => {
|
||||||
|
form
|
||||||
|
.validateFields()
|
||||||
|
.then((values) => {
|
||||||
|
handleOk(values);
|
||||||
|
})
|
||||||
|
.catch((info) => {
|
||||||
|
console.log('Validate Failed:', info);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onCancel={() => handleCancel()}
|
||||||
|
confirmLoading={loading}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical" name="edit_name_modal">
|
||||||
|
<Form.Item
|
||||||
|
name="filename"
|
||||||
|
label="文件名"
|
||||||
|
rules={[{ required: true, message: '请输入文件名' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入文件名" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="父目录"
|
||||||
|
name="path"
|
||||||
|
initialValue={dirs && dirs.length > 0 ? dirs[0].key : ''}
|
||||||
|
>
|
||||||
|
<Select placeholder="请选择父目录">
|
||||||
|
{dirs.map((x) => (
|
||||||
|
<Option value={x.key}>{x.key || '根'}</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditScriptNameModal;
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
height: calc(100vh - 128px);
|
height: calc(100vh - 128px);
|
||||||
height: calc(100vh - var(--vh-offset, 0px) - 128px);
|
height: calc(100vh - var(--vh-offset, 0px) - 128px);
|
||||||
width: @tree-width;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -15,9 +14,11 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border-right: 1px dashed #f0f0f0;
|
border-right: 1px dashed #f0f0f0;
|
||||||
|
padding-top: 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-container {
|
.log-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|||||||
+410
-51
@@ -1,5 +1,16 @@
|
|||||||
import { useState, useEffect, useCallback, Key, useRef } from 'react';
|
import { useState, useEffect, useCallback, Key, useRef } from 'react';
|
||||||
import { TreeSelect, Tree, Input, Button } from 'antd';
|
import {
|
||||||
|
TreeSelect,
|
||||||
|
Tree,
|
||||||
|
Input,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
message,
|
||||||
|
Typography,
|
||||||
|
Tooltip,
|
||||||
|
Dropdown,
|
||||||
|
Menu,
|
||||||
|
} from 'antd';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
import { PageContainer } from '@ant-design/pro-layout';
|
import { PageContainer } from '@ant-design/pro-layout';
|
||||||
import Editor from '@monaco-editor/react';
|
import Editor from '@monaco-editor/react';
|
||||||
@@ -7,19 +18,50 @@ import { request } from '@/utils/http';
|
|||||||
import styles from './index.module.less';
|
import styles from './index.module.less';
|
||||||
import EditModal from './editModal';
|
import EditModal from './editModal';
|
||||||
import { Controlled as CodeMirror } from 'react-codemirror2';
|
import { Controlled as CodeMirror } from 'react-codemirror2';
|
||||||
import { useCtx, useTheme } from '@/utils/hooks';
|
import SplitPane from 'react-split-pane';
|
||||||
|
import {
|
||||||
|
DeleteOutlined,
|
||||||
|
DownloadOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
EllipsisOutlined,
|
||||||
|
FormOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
PlusSquareOutlined,
|
||||||
|
SearchOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import EditScriptNameModal from './editNameModal';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import { history } from 'umi';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
function getFilterData(keyword: string, data: any) {
|
function getFilterData(keyword: string, data: any) {
|
||||||
|
const expandedKeys: string[] = [];
|
||||||
if (keyword) {
|
if (keyword) {
|
||||||
const tree: any = [];
|
const tree: any = [];
|
||||||
data.forEach((item: any) => {
|
data.forEach((item: any) => {
|
||||||
if (item.title.toLocaleLowerCase().includes(keyword)) {
|
if (item.title.toLocaleLowerCase().includes(keyword)) {
|
||||||
tree.push(item);
|
tree.push(item);
|
||||||
|
} else {
|
||||||
|
const children: any[] = [];
|
||||||
|
(item.children || []).forEach((subItem: any) => {
|
||||||
|
if (subItem.title.toLocaleLowerCase().includes(keyword)) {
|
||||||
|
children.push(subItem);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (children.length > 0) {
|
||||||
|
tree.push({
|
||||||
|
...item,
|
||||||
|
children,
|
||||||
|
});
|
||||||
|
expandedKeys.push(item.key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return { tree };
|
return { tree, expandedKeys };
|
||||||
}
|
}
|
||||||
return { tree: data };
|
return { tree: data, expandedKeys };
|
||||||
}
|
}
|
||||||
|
|
||||||
const LangMap: any = {
|
const LangMap: any = {
|
||||||
@@ -29,10 +71,10 @@ const LangMap: any = {
|
|||||||
'.ts': 'typescript',
|
'.ts': 'typescript',
|
||||||
};
|
};
|
||||||
|
|
||||||
const Script = () => {
|
const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
|
||||||
const [title, setTitle] = useState('请选择脚本文件');
|
const [title, setTitle] = useState('请选择脚本文件');
|
||||||
const [value, setValue] = useState('请选择脚本文件');
|
const [value, setValue] = useState('请选择脚本文件');
|
||||||
const [select, setSelect] = useState();
|
const [select, setSelect] = useState<any>();
|
||||||
const [data, setData] = useState<any[]>([]);
|
const [data, setData] = useState<any[]>([]);
|
||||||
const [filterData, setFilterData] = useState<any[]>([]);
|
const [filterData, setFilterData] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -40,8 +82,12 @@ const Script = () => {
|
|||||||
const [height, setHeight] = useState<number>();
|
const [height, setHeight] = useState<number>();
|
||||||
const treeDom = useRef<any>();
|
const treeDom = useRef<any>();
|
||||||
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
|
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
|
||||||
const { headerStyle, isPhone } = useCtx();
|
const [searchValue, setSearchValue] = useState('');
|
||||||
const { theme } = useTheme();
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const editorRef = useRef<any>(null);
|
||||||
|
const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false);
|
||||||
|
const [currentNode, setCurrentNode] = useState<any>();
|
||||||
|
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
||||||
|
|
||||||
const getScripts = () => {
|
const getScripts = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -50,42 +96,308 @@ const Script = () => {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
setData(data.data);
|
setData(data.data);
|
||||||
setFilterData(data.data);
|
setFilterData(data.data);
|
||||||
|
initGetScript();
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDetail = (node: any) => {
|
const getDetail = (node: any) => {
|
||||||
request.get(`${config.apiPrefix}scripts/${node.value}`).then((data) => {
|
request
|
||||||
setValue(data.data);
|
.get(`${config.apiPrefix}scripts/${node.value}?path=${node.parent || ''}`)
|
||||||
});
|
.then((data) => {
|
||||||
|
setValue(data.data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const initGetScript = () => {
|
||||||
|
const { p, s } = history.location.query as any;
|
||||||
|
if (s) {
|
||||||
|
const obj = {
|
||||||
|
node: {
|
||||||
|
title: s,
|
||||||
|
value: s,
|
||||||
|
key: p ? `${p}/${s}` : s,
|
||||||
|
parent: p,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setExpandedKeys([p]);
|
||||||
|
onTreeSelect([`${p}/${s}`], obj);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelect = (value: any, node: any) => {
|
const onSelect = (value: any, node: any) => {
|
||||||
const newMode = LangMap[value.slice(-3)] || '';
|
if (node.key === select || !value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setValue('加载中...');
|
||||||
|
const newMode = value ? LangMap[value.slice(-3)] : '';
|
||||||
setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode);
|
setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode);
|
||||||
setSelect(value);
|
setSelect(node.key);
|
||||||
setTitle(node.parent || node.value);
|
setTitle(node.parent || node.value);
|
||||||
|
setCurrentNode(node);
|
||||||
getDetail(node);
|
getDetail(node);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onTreeSelect = useCallback((keys: Key[], e: any) => {
|
const onExpand = (expKeys: any) => {
|
||||||
onSelect(keys[0], e.node);
|
setExpandedKeys(expKeys);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
|
const onTreeSelect = useCallback(
|
||||||
|
(keys: Key[], e: any) => {
|
||||||
|
const content = editorRef.current
|
||||||
|
? editorRef.current.getValue().replace(/\r\n/g, '\n')
|
||||||
|
: value;
|
||||||
|
if (content !== value) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: `确认离开`,
|
||||||
|
content: <>当前修改未保存,确定离开吗</>,
|
||||||
|
onOk() {
|
||||||
|
onSelect(keys[0], e.node);
|
||||||
|
setIsEditing(false);
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setIsEditing(false);
|
||||||
|
onSelect(keys[0], e.node);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[value],
|
||||||
|
);
|
||||||
|
|
||||||
const onSearch = useCallback(
|
const onSearch = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
const keyword = e.target.value;
|
const keyword = e.target.value;
|
||||||
const { tree } = getFilterData(keyword.toLocaleLowerCase(), data);
|
debounceSearch(keyword);
|
||||||
setFilterData(tree);
|
|
||||||
},
|
},
|
||||||
[data, setFilterData],
|
[data, setFilterData],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const debounceSearch = useCallback(
|
||||||
|
debounce((keyword) => {
|
||||||
|
setSearchValue(keyword);
|
||||||
|
const { tree, expandedKeys } = getFilterData(
|
||||||
|
keyword.toLocaleLowerCase(),
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
setExpandedKeys(expandedKeys);
|
||||||
|
setFilterData(tree);
|
||||||
|
}, 300),
|
||||||
|
[data, setFilterData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const editFile = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsEditing(true);
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEdit = () => {
|
||||||
|
setIsEditing(false);
|
||||||
|
setValue('加载中...');
|
||||||
|
getDetail(currentNode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveFile = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: `确认保存`,
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
确认保存文件
|
||||||
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||||
|
{currentNode.value}
|
||||||
|
</Text>{' '}
|
||||||
|
,保存后不可恢复
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onOk() {
|
||||||
|
const content = editorRef.current
|
||||||
|
? editorRef.current.getValue().replace(/\r\n/g, '\n')
|
||||||
|
: value;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
request
|
||||||
|
.put(`${config.apiPrefix}scripts`, {
|
||||||
|
data: {
|
||||||
|
filename: currentNode.value,
|
||||||
|
path: currentNode.parent || '',
|
||||||
|
content,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((_data: any) => {
|
||||||
|
if (_data.code === 200) {
|
||||||
|
message.success(`保存成功`);
|
||||||
|
setValue(content);
|
||||||
|
setIsEditing(false);
|
||||||
|
} else {
|
||||||
|
message.error(_data);
|
||||||
|
}
|
||||||
|
resolve(null);
|
||||||
|
})
|
||||||
|
.catch((e) => reject(e));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteFile = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: `确认删除`,
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
确认删除文件
|
||||||
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||||
|
{select}
|
||||||
|
</Text>{' '}
|
||||||
|
,删除后不可恢复
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onOk() {
|
||||||
|
request
|
||||||
|
.delete(`${config.apiPrefix}scripts`, {
|
||||||
|
data: {
|
||||||
|
filename: currentNode.value,
|
||||||
|
path: currentNode.parent || '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((_data: any) => {
|
||||||
|
if (_data.code === 200) {
|
||||||
|
message.success(`删除成功`);
|
||||||
|
let newData = [...data];
|
||||||
|
if (currentNode.parent) {
|
||||||
|
const parentNodeIndex = newData.findIndex(
|
||||||
|
(x) => x.key === currentNode.parent,
|
||||||
|
);
|
||||||
|
const parentNode = newData[parentNodeIndex];
|
||||||
|
const index = parentNode.children.findIndex(
|
||||||
|
(y) => y.key === currentNode.key,
|
||||||
|
);
|
||||||
|
parentNode.children.splice(index, 1);
|
||||||
|
newData.splice(parentNodeIndex, 1, { ...parentNode });
|
||||||
|
} else {
|
||||||
|
const index = newData.findIndex(
|
||||||
|
(x) => x.key === currentNode.key,
|
||||||
|
);
|
||||||
|
newData.splice(index, 1);
|
||||||
|
}
|
||||||
|
setData(newData);
|
||||||
|
} else {
|
||||||
|
message.error(_data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addFile = () => {
|
||||||
|
setIsAddFileModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addFileModalClose = (
|
||||||
|
{ filename, path, key }: { filename: string; path: string; key: string } = {
|
||||||
|
filename: '',
|
||||||
|
path: '',
|
||||||
|
key: '',
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
if (filename) {
|
||||||
|
const newData = [...data];
|
||||||
|
const _file = { title: filename, key, value: filename, parent: path };
|
||||||
|
if (path) {
|
||||||
|
const parentNodeIndex = newData.findIndex((x) => x.key === path);
|
||||||
|
const parentNode = newData[parentNodeIndex];
|
||||||
|
if (parentNode.children && parentNode.children.length > 0) {
|
||||||
|
parentNode.children.unshift(_file);
|
||||||
|
} else {
|
||||||
|
parentNode.children = [_file];
|
||||||
|
}
|
||||||
|
newData.splice(parentNodeIndex, 1, { ...parentNode });
|
||||||
|
} else {
|
||||||
|
newData.unshift(_file);
|
||||||
|
}
|
||||||
|
setData(newData);
|
||||||
|
onSelect(_file.value, _file);
|
||||||
|
setIsEditing(true);
|
||||||
|
}
|
||||||
|
setIsAddFileModalVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadFile = () => {
|
||||||
|
request
|
||||||
|
.post(`${config.apiPrefix}scripts/download`, {
|
||||||
|
data: {
|
||||||
|
filename: currentNode.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((_data: any) => {
|
||||||
|
const blob = new Blob([_data], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = currentNode.value;
|
||||||
|
document.documentElement.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.documentElement.removeChild(a);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const word = searchValue || '';
|
||||||
|
const { tree } = getFilterData(word.toLocaleLowerCase(), data);
|
||||||
|
setFilterData(tree);
|
||||||
|
setSelect('');
|
||||||
|
setCurrentNode(null);
|
||||||
|
setTitle('请选择脚本文件');
|
||||||
|
setValue('请选择脚本文件');
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getScripts();
|
getScripts();
|
||||||
setHeight(treeDom.current.clientHeight);
|
if (treeDom && treeDom.current) {
|
||||||
|
setHeight(treeDom.current.clientHeight);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const menu = isEditing ? (
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item key="save" icon={<PlusOutlined />} onClick={saveFile}>
|
||||||
|
保存
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="exit" icon={<EditOutlined />} onClick={cancelEdit}>
|
||||||
|
退出编辑
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
) : (
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item key="add" icon={<PlusOutlined />} onClick={addFile}>
|
||||||
|
添加
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
key="edit"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={editFile}
|
||||||
|
disabled={!select}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
key="delete"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={deleteFile}
|
||||||
|
disabled={!select}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
className="ql-container-wrapper log-wrapper"
|
className="ql-container-wrapper log-wrapper"
|
||||||
@@ -101,11 +413,45 @@ const Script = () => {
|
|||||||
treeData={data}
|
treeData={data}
|
||||||
placeholder="请选择脚本文件"
|
placeholder="请选择脚本文件"
|
||||||
showSearch
|
showSearch
|
||||||
key="value"
|
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
/>,
|
/>,
|
||||||
|
<Dropdown overlay={menu} trigger={['click']}>
|
||||||
|
<Button type="primary" icon={<EllipsisOutlined />} />
|
||||||
|
</Dropdown>,
|
||||||
|
]
|
||||||
|
: isEditing
|
||||||
|
? [
|
||||||
|
<Button type="primary" onClick={saveFile}>
|
||||||
|
保存
|
||||||
|
</Button>,
|
||||||
|
<Button type="primary" onClick={cancelEdit}>
|
||||||
|
退出编辑
|
||||||
|
</Button>,
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
|
<Tooltip title="新建">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={addFile}
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
/>
|
||||||
|
</Tooltip>,
|
||||||
|
<Tooltip title="编辑">
|
||||||
|
<Button
|
||||||
|
disabled={!select}
|
||||||
|
type="primary"
|
||||||
|
onClick={editFile}
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
/>
|
||||||
|
</Tooltip>,
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
disabled={!select}
|
||||||
|
onClick={deleteFile}
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
/>
|
||||||
|
</Tooltip>,
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -122,24 +468,44 @@ const Script = () => {
|
|||||||
>
|
>
|
||||||
<div className={`${styles['log-container']} log-container`}>
|
<div className={`${styles['log-container']} log-container`}>
|
||||||
{!isPhone && (
|
{!isPhone && (
|
||||||
<div className={styles['left-tree-container']}>
|
<SplitPane split="vertical" size={200} maxSize={-100}>
|
||||||
<Input.Search
|
<div className={styles['left-tree-container']}>
|
||||||
className={styles['left-tree-search']}
|
<Input.Search
|
||||||
onChange={onSearch}
|
className={styles['left-tree-search']}
|
||||||
></Input.Search>
|
onChange={onSearch}
|
||||||
<div className={styles['left-tree-scroller']} ref={treeDom}>
|
></Input.Search>
|
||||||
<Tree
|
<div className={styles['left-tree-scroller']} ref={treeDom}>
|
||||||
className={styles['left-tree']}
|
<Tree
|
||||||
treeData={filterData}
|
className={styles['left-tree']}
|
||||||
showIcon={true}
|
treeData={filterData}
|
||||||
height={height}
|
showIcon={true}
|
||||||
showLine={{ showLeafIcon: true }}
|
height={height}
|
||||||
onSelect={onTreeSelect}
|
selectedKeys={[select]}
|
||||||
></Tree>
|
expandedKeys={expandedKeys}
|
||||||
|
onExpand={onExpand}
|
||||||
|
showLine={{ showLeafIcon: true }}
|
||||||
|
onSelect={onTreeSelect}
|
||||||
|
></Tree>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<Editor
|
||||||
|
language={mode}
|
||||||
|
value={value}
|
||||||
|
theme={theme}
|
||||||
|
options={{
|
||||||
|
readOnly: !isEditing,
|
||||||
|
fontSize: 12,
|
||||||
|
lineNumbersMinChars: 3,
|
||||||
|
folding: false,
|
||||||
|
glyphMargin: false,
|
||||||
|
}}
|
||||||
|
onMount={(editor) => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SplitPane>
|
||||||
)}
|
)}
|
||||||
{isPhone ? (
|
{isPhone && (
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
value={value}
|
value={value}
|
||||||
options={{
|
options={{
|
||||||
@@ -148,36 +514,29 @@ const Script = () => {
|
|||||||
styleActiveLine: true,
|
styleActiveLine: true,
|
||||||
matchBrackets: true,
|
matchBrackets: true,
|
||||||
mode,
|
mode,
|
||||||
readOnly: true,
|
readOnly: !isEditing,
|
||||||
}}
|
}}
|
||||||
onBeforeChange={(editor, data, value) => {
|
onBeforeChange={(editor, data, value) => {
|
||||||
setValue(value);
|
setValue(value);
|
||||||
}}
|
}}
|
||||||
onChange={(editor, data, value) => {}}
|
onChange={(editor, data, value) => {}}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<Editor
|
|
||||||
language={mode}
|
|
||||||
value={value}
|
|
||||||
theme={theme}
|
|
||||||
options={{
|
|
||||||
readOnly: true,
|
|
||||||
fontSize: 12,
|
|
||||||
lineNumbersMinChars: 3,
|
|
||||||
folding: false,
|
|
||||||
glyphMargin: false,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<EditModal
|
<EditModal
|
||||||
visible={isLogModalVisible}
|
visible={isLogModalVisible}
|
||||||
treeData={data}
|
treeData={data}
|
||||||
currentFile={select}
|
currentNode={currentNode}
|
||||||
content={value}
|
content={value}
|
||||||
|
socketMessage={socketMessage}
|
||||||
handleCancel={() => {
|
handleCancel={() => {
|
||||||
setIsLogModalVisible(false);
|
setIsLogModalVisible(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<EditScriptNameModal
|
||||||
|
visible={isAddFileModalVisible}
|
||||||
|
treeData={data}
|
||||||
|
handleCancel={addFileModalClose}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,20 +7,17 @@ const SaveModal = ({
|
|||||||
file,
|
file,
|
||||||
handleCancel,
|
handleCancel,
|
||||||
visible,
|
visible,
|
||||||
isNewFile,
|
|
||||||
}: {
|
}: {
|
||||||
file?: any;
|
file?: any;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
handleCancel: (cks?: any[]) => void;
|
handleCancel: (cks?: any[]) => void;
|
||||||
isNewFile: boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleOk = async (values: any) => {
|
const handleOk = async (values: any) => {
|
||||||
console.log(file.filename);
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const payload = { ...file, ...values };
|
const payload = { ...file, ...values, originFilename: file.filename };
|
||||||
request
|
request
|
||||||
.post(`${config.apiPrefix}scripts`, {
|
.post(`${config.apiPrefix}scripts`, {
|
||||||
data: payload,
|
data: payload,
|
||||||
@@ -72,12 +69,8 @@ const SaveModal = ({
|
|||||||
>
|
>
|
||||||
<Input placeholder="请输入文件名" />
|
<Input placeholder="请输入文件名" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item name="path" label="保存目录">
|
||||||
name="path"
|
<Input placeholder="请输入保存目录,默认scripts目录" />
|
||||||
label="保存目录"
|
|
||||||
rules={[{ required: true, message: '请输入保存目录' }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="请输入保存目录" />
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ const SettingModal = ({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleOk = async (values: any) => {
|
const handleOk = async (values: any) => {
|
||||||
console.log(file.filename);
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const payload = { ...file, ...values };
|
const payload = { ...file, ...values };
|
||||||
request
|
request
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Modal, message, Input, Form, Select } from 'antd';
|
||||||
|
import { request } from '@/utils/http';
|
||||||
|
import config from '@/utils/config';
|
||||||
|
|
||||||
|
const AppModal = ({
|
||||||
|
app,
|
||||||
|
handleCancel,
|
||||||
|
visible,
|
||||||
|
}: {
|
||||||
|
app?: any;
|
||||||
|
visible: boolean;
|
||||||
|
handleCancel: (needUpdate?: boolean) => void;
|
||||||
|
}) => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleOk = async (values: any) => {
|
||||||
|
setLoading(true);
|
||||||
|
const method = app ? 'put' : 'post';
|
||||||
|
const payload = { ...values };
|
||||||
|
if (app) {
|
||||||
|
payload._id = app._id;
|
||||||
|
}
|
||||||
|
const { code, data } = await request[method](`${config.apiPrefix}apps`, {
|
||||||
|
data: payload,
|
||||||
|
});
|
||||||
|
if (code === 200) {
|
||||||
|
message.success(app ? '更新应用成功' : '添加应用成功');
|
||||||
|
} else {
|
||||||
|
message.error(data);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
handleCancel(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.resetFields();
|
||||||
|
}, [app, visible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={app ? '编辑应用' : '新建应用'}
|
||||||
|
visible={visible}
|
||||||
|
forceRender
|
||||||
|
onOk={() => {
|
||||||
|
form
|
||||||
|
.validateFields()
|
||||||
|
.then((values) => {
|
||||||
|
handleOk(values);
|
||||||
|
})
|
||||||
|
.catch((info) => {
|
||||||
|
console.log('Validate Failed:', info);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onCancel={() => handleCancel()}
|
||||||
|
confirmLoading={loading}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
name="form_app_modal"
|
||||||
|
initialValues={app}
|
||||||
|
>
|
||||||
|
<Form.Item name="name" label="名称">
|
||||||
|
<Input placeholder="请输入应用名称" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="scopes" label="权限" rules={[{ required: true }]}>
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
placeholder="请选择模块权限"
|
||||||
|
allowClear
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{config.scopes.map((x) => {
|
||||||
|
return <Select.Option value={x.value}>{x.name}</Select.Option>;
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppModal;
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import { Statistic, Modal, Tag, Button, Spin, message } from 'antd';
|
||||||
|
import { request } from '@/utils/http';
|
||||||
|
import config from '@/utils/config';
|
||||||
|
import { version } from '../../version';
|
||||||
|
|
||||||
|
const { Countdown } = Statistic;
|
||||||
|
|
||||||
|
const CheckUpdate = ({ socketMessage }: any) => {
|
||||||
|
const [updateLoading, setUpdateLoading] = useState(false);
|
||||||
|
const [value, setValue] = useState('');
|
||||||
|
const modalRef = useRef<any>();
|
||||||
|
|
||||||
|
const checkUpgrade = () => {
|
||||||
|
if (updateLoading) return;
|
||||||
|
setUpdateLoading(true);
|
||||||
|
message.loading('检查更新中...', 0);
|
||||||
|
request
|
||||||
|
.put(`${config.apiPrefix}system/update-check`)
|
||||||
|
.then((_data: any) => {
|
||||||
|
message.destroy();
|
||||||
|
const { code, data } = _data;
|
||||||
|
if (code === 200) {
|
||||||
|
if (data.hasNewVersion) {
|
||||||
|
showConfirmUpdateModal(data);
|
||||||
|
} else {
|
||||||
|
showForceUpdateModal();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
message.destroy();
|
||||||
|
console.log(error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setUpdateLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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({
|
||||||
|
width: 500,
|
||||||
|
title: (
|
||||||
|
<>
|
||||||
|
<div>更新可用</div>
|
||||||
|
<div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>
|
||||||
|
新版本{lastVersion}可用。你使用的版本为{version}。
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
content: (
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
paddingTop: 15,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{lastLog}
|
||||||
|
</pre>
|
||||||
|
),
|
||||||
|
okText: '更新',
|
||||||
|
cancelText: '以后再说',
|
||||||
|
onOk() {
|
||||||
|
showUpdatingModal();
|
||||||
|
request
|
||||||
|
.put(`${config.apiPrefix}system/update`)
|
||||||
|
.then((_data: any) => {})
|
||||||
|
.catch((error: any) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const showUpdatingModal = () => {
|
||||||
|
modalRef.current = Modal.info({
|
||||||
|
width: 600,
|
||||||
|
maskClosable: false,
|
||||||
|
closable: false,
|
||||||
|
okButtonProps: { disabled: true },
|
||||||
|
title: '更新日志',
|
||||||
|
centered: true,
|
||||||
|
content: (
|
||||||
|
<div style={{ height: '60vh', overflowY: 'auto' }}>
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!modalRef.current || !socketMessage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { type, message: _message, references } = socketMessage;
|
||||||
|
|
||||||
|
if (type !== 'updateSystemVersion') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newMessage = `${value}\n${_message}`;
|
||||||
|
modalRef.current.update({
|
||||||
|
content: (
|
||||||
|
<div style={{ height: '60vh', overflowY: 'auto' }}>
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{newMessage}
|
||||||
|
</pre>
|
||||||
|
<div id="log-identifier" style={{ paddingBottom: 5 }}></div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
setValue(newMessage);
|
||||||
|
|
||||||
|
document.getElementById('log-identifier') &&
|
||||||
|
document
|
||||||
|
.getElementById('log-identifier')!
|
||||||
|
.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
|
|
||||||
|
if (_message.includes('重启面板')) {
|
||||||
|
message.warning({
|
||||||
|
content: (
|
||||||
|
<span>
|
||||||
|
系统将在
|
||||||
|
<Countdown
|
||||||
|
className="inline-countdown"
|
||||||
|
format="ss"
|
||||||
|
value={Date.now() + 1000 * 10}
|
||||||
|
/>
|
||||||
|
秒后自动刷新
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
duration: 10,
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
}, [socketMessage]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button type="primary" onClick={checkUpgrade}>
|
||||||
|
检查更新
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CheckUpdate;
|
||||||
+331
-64
@@ -1,5 +1,18 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Button, Input, Form, Radio, Tabs } from 'antd';
|
import {
|
||||||
|
Button,
|
||||||
|
InputNumber,
|
||||||
|
Form,
|
||||||
|
Radio,
|
||||||
|
Tabs,
|
||||||
|
Table,
|
||||||
|
Tooltip,
|
||||||
|
Space,
|
||||||
|
Tag,
|
||||||
|
Modal,
|
||||||
|
message,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
import { PageContainer } from '@ant-design/pro-layout';
|
import { PageContainer } from '@ant-design/pro-layout';
|
||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
@@ -9,67 +22,290 @@ import {
|
|||||||
auto as followSystemColorScheme,
|
auto as followSystemColorScheme,
|
||||||
setFetchMethod,
|
setFetchMethod,
|
||||||
} from 'darkreader';
|
} from 'darkreader';
|
||||||
import { history } from 'umi';
|
import AppModal from './appModal';
|
||||||
import { useCtx } from '@/utils/hooks';
|
import {
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import SecuritySettings from './security';
|
||||||
|
import LoginLog from './loginLog';
|
||||||
|
import NotificationSetting from './notification';
|
||||||
|
import CheckUpdate from './checkUpdate';
|
||||||
|
import { useForm } from 'antd/lib/form/Form';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
const optionsWithDisabled = [
|
const optionsWithDisabled = [
|
||||||
{ label: '亮色', value: 'light' },
|
{ label: '亮色', value: 'light' },
|
||||||
{ label: '暗色', value: 'dark' },
|
{ label: '暗色', value: 'dark' },
|
||||||
{ label: '跟随系统', value: 'auto' },
|
{ label: '跟随系统', value: 'auto' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const Password = () => {
|
const Setting = ({
|
||||||
const [value, setValue] = useState('');
|
headerStyle,
|
||||||
|
isPhone,
|
||||||
|
user,
|
||||||
|
reloadUser,
|
||||||
|
reloadTheme,
|
||||||
|
socketMessage,
|
||||||
|
}: any) => {
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Client ID',
|
||||||
|
dataIndex: 'client_id',
|
||||||
|
key: 'client_id',
|
||||||
|
align: 'center' as const,
|
||||||
|
render: (text: string, record: any) => {
|
||||||
|
return <Text copyable>{record.client_id}</Text>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Client Secret',
|
||||||
|
dataIndex: 'client_secret',
|
||||||
|
key: 'client_secret',
|
||||||
|
align: 'center' as const,
|
||||||
|
render: (text: string, record: any) => {
|
||||||
|
return <Text copyable={{ text: record.client_secret }}>*******</Text>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '权限',
|
||||||
|
dataIndex: 'scopes',
|
||||||
|
key: 'scopes',
|
||||||
|
align: 'center' as const,
|
||||||
|
render: (text: string, record: any) => {
|
||||||
|
return record.scopes.map((scope: any) => {
|
||||||
|
return <Tag key={scope}>{(config.scopesMap as any)[scope]}</Tag>;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
align: 'center' as const,
|
||||||
|
render: (text: string, record: any, index: number) => {
|
||||||
|
const isPc = !isPhone;
|
||||||
|
return (
|
||||||
|
<Space size="middle" style={{ paddingLeft: 8 }}>
|
||||||
|
<Tooltip title={isPc ? '编辑' : ''}>
|
||||||
|
<a onClick={() => editApp(record, index)}>
|
||||||
|
<EditOutlined />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title={isPc ? '重置secret' : ''}>
|
||||||
|
<a onClick={() => resetSecret(record, index)}>
|
||||||
|
<ReloadOutlined />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title={isPc ? '删除' : ''}>
|
||||||
|
<a onClick={() => deleteApp(record, index)}>
|
||||||
|
<DeleteOutlined />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const defaultDarken = localStorage.getItem('qinglong_dark_theme') || 'auto';
|
const defaultDarken = localStorage.getItem('qinglong_dark_theme') || 'auto';
|
||||||
const [theme, setTheme] = useState(defaultDarken);
|
const [theme, setTheme] = useState(defaultDarken);
|
||||||
const { headerStyle, isPhone } = useCtx();
|
const [dataSource, setDataSource] = useState<any[]>([]);
|
||||||
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
const handleOk = (values: any) => {
|
const [editedApp, setEditedApp] = useState();
|
||||||
request
|
const [tabActiveKey, setTabActiveKey] = useState('security');
|
||||||
.post(`${config.apiPrefix}user`, {
|
const [loginLogData, setLoginLogData] = useState<any[]>([]);
|
||||||
data: {
|
const [notificationInfo, setNotificationInfo] = useState<any>();
|
||||||
username: values.username,
|
const [logRemoveFrequency, setLogRemoveFrequency] = useState<number>();
|
||||||
password: values.password,
|
const [form] = useForm();
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((data: any) => {
|
|
||||||
localStorage.removeItem(config.authKey);
|
|
||||||
history.push('/login');
|
|
||||||
})
|
|
||||||
.catch((error: any) => {
|
|
||||||
console.log(error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const themeChange = (e: any) => {
|
const themeChange = (e: any) => {
|
||||||
setTheme(e.target.value);
|
setTheme(e.target.value);
|
||||||
localStorage.setItem('qinglong_dark_theme', e.target.value);
|
localStorage.setItem('qinglong_dark_theme', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const importJob = () => {
|
const getApps = () => {
|
||||||
request.get(`${config.apiPrefix}crons/import`).then((data: any) => {
|
setLoading(true);
|
||||||
console.log(data);
|
request
|
||||||
|
.get(`${config.apiPrefix}apps`)
|
||||||
|
.then((data: any) => {
|
||||||
|
setDataSource(data.data);
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addApp = () => {
|
||||||
|
setIsModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const editApp = (record: any, index: number) => {
|
||||||
|
setEditedApp(record);
|
||||||
|
setIsModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteApp = (record: any, index: number) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
确认删除应用{' '}
|
||||||
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||||
|
{record.name}
|
||||||
|
</Text>{' '}
|
||||||
|
吗
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onOk() {
|
||||||
|
request
|
||||||
|
.delete(`${config.apiPrefix}apps`, { data: [record._id] })
|
||||||
|
.then((data: any) => {
|
||||||
|
if (data.code === 200) {
|
||||||
|
message.success('删除成功');
|
||||||
|
const result = [...dataSource];
|
||||||
|
result.splice(index, 1);
|
||||||
|
setDataSource(result);
|
||||||
|
} else {
|
||||||
|
message.error(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetSecret = (record: any, index: number) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认重置',
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
确认重置应用{' '}
|
||||||
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
||||||
|
{record.name}
|
||||||
|
</Text>{' '}
|
||||||
|
的Secret吗
|
||||||
|
<br />
|
||||||
|
<Text type="secondary">重置Secret会让当前应用所有token失效</Text>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onOk() {
|
||||||
|
request
|
||||||
|
.put(`${config.apiPrefix}apps/${record._id}/reset-secret`)
|
||||||
|
.then((data: any) => {
|
||||||
|
if (data.code === 200) {
|
||||||
|
message.success('重置成功');
|
||||||
|
handleApp(data.data);
|
||||||
|
} else {
|
||||||
|
message.error(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = (app?: any) => {
|
||||||
|
setIsModalVisible(false);
|
||||||
|
if (app) {
|
||||||
|
handleApp(app);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApp = (app: any) => {
|
||||||
|
const index = dataSource.findIndex((x) => x._id === app._id);
|
||||||
|
const result = [...dataSource];
|
||||||
|
if (index === -1) {
|
||||||
|
result.push(app);
|
||||||
|
} else {
|
||||||
|
result.splice(index, 1, {
|
||||||
|
...app,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setDataSource(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLoginLog = () => {
|
||||||
|
request
|
||||||
|
.get(`${config.apiPrefix}user/login-log`)
|
||||||
|
.then((data: any) => {
|
||||||
|
setLoginLogData(data.data);
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabChange = (activeKey: string) => {
|
||||||
|
setTabActiveKey(activeKey);
|
||||||
|
if (activeKey === 'app') {
|
||||||
|
getApps();
|
||||||
|
} else if (activeKey === 'login') {
|
||||||
|
getLoginLog();
|
||||||
|
} else if (activeKey === 'notification') {
|
||||||
|
getNotification();
|
||||||
|
} else if (activeKey === 'other') {
|
||||||
|
getLogRemoveFrequency();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNotification = () => {
|
||||||
|
request
|
||||||
|
.get(`${config.apiPrefix}user/notification`)
|
||||||
|
.then((data: any) => {
|
||||||
|
setNotificationInfo(data.data);
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLogRemoveFrequency = () => {
|
||||||
|
request
|
||||||
|
.get(`${config.apiPrefix}system/log/remove`)
|
||||||
|
.then((data: any) => {
|
||||||
|
setLogRemoveFrequency(data.data.frequency);
|
||||||
|
form.setFieldsValue({ frequency: data.data.frequency });
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRemoveLogFrequency = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
request
|
||||||
|
.put(`${config.apiPrefix}system/log/remove`, {
|
||||||
|
data: { frequency: logRemoveFrequency },
|
||||||
|
})
|
||||||
|
.then((data: any) => {
|
||||||
|
message.success('更新成功');
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFetchMethod(window.fetch);
|
setFetchMethod(window.fetch);
|
||||||
if (theme === 'dark') {
|
if (theme === 'dark') {
|
||||||
enableDarkMode({
|
enableDarkMode({});
|
||||||
brightness: 100,
|
|
||||||
contrast: 90,
|
|
||||||
sepia: 10,
|
|
||||||
});
|
|
||||||
} else if (theme === 'light') {
|
} else if (theme === 'light') {
|
||||||
disableDarkMode();
|
disableDarkMode();
|
||||||
} else {
|
} else {
|
||||||
followSystemColorScheme({
|
followSystemColorScheme({});
|
||||||
brightness: 100,
|
|
||||||
contrast: 90,
|
|
||||||
sepia: 10,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
reloadTheme(theme);
|
||||||
}, [theme]);
|
}, [theme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -79,35 +315,44 @@ const Password = () => {
|
|||||||
header={{
|
header={{
|
||||||
style: headerStyle,
|
style: headerStyle,
|
||||||
}}
|
}}
|
||||||
|
extra={
|
||||||
|
tabActiveKey === 'app'
|
||||||
|
? [
|
||||||
|
<Button key="2" type="primary" onClick={() => addApp()}>
|
||||||
|
添加应用
|
||||||
|
</Button>,
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Tabs defaultActiveKey="person" size="small" tabPosition="top">
|
<Tabs
|
||||||
<Tabs.TabPane tab="个人设置" key="person">
|
defaultActiveKey="security"
|
||||||
<Form onFinish={handleOk} layout="vertical">
|
size="small"
|
||||||
<Form.Item
|
tabPosition="top"
|
||||||
label="用户名"
|
onChange={tabChange}
|
||||||
name="username"
|
>
|
||||||
rules={[{ required: true }]}
|
<Tabs.TabPane tab="安全设置" key="security">
|
||||||
hasFeedback
|
<SecuritySettings user={user} userChange={reloadUser} />
|
||||||
style={{ maxWidth: 300 }}
|
|
||||||
>
|
|
||||||
<Input placeholder="用户名" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label="密码"
|
|
||||||
name="password"
|
|
||||||
rules={[{ required: true }]}
|
|
||||||
hasFeedback
|
|
||||||
style={{ maxWidth: 300 }}
|
|
||||||
>
|
|
||||||
<Input type="password" placeholder="密码" />
|
|
||||||
</Form.Item>
|
|
||||||
<Button type="primary" htmlType="submit">
|
|
||||||
保存
|
|
||||||
</Button>
|
|
||||||
</Form>
|
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane tab="其他设置" key="theme">
|
<Tabs.TabPane tab="应用设置" key="app">
|
||||||
<Form layout="vertical">
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
pagination={false}
|
||||||
|
dataSource={dataSource}
|
||||||
|
rowKey="_id"
|
||||||
|
size="middle"
|
||||||
|
scroll={{ x: 768 }}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane tab="通知设置" key="notification">
|
||||||
|
<NotificationSetting data={notificationInfo} />
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane tab="登录日志" key="login">
|
||||||
|
<LoginLog data={loginLogData} />
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane tab="其他设置" key="other">
|
||||||
|
<Form layout="vertical" form={form}>
|
||||||
<Form.Item label="主题设置" name="theme" initialValue={theme}>
|
<Form.Item label="主题设置" name="theme" initialValue={theme}>
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
options={optionsWithDisabled}
|
options={optionsWithDisabled}
|
||||||
@@ -117,11 +362,33 @@ const Password = () => {
|
|||||||
buttonStyle="solid"
|
buttonStyle="solid"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="日志删除频率"
|
||||||
|
name="frequency"
|
||||||
|
tooltip="每x天自动删除x天以前的日志"
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
addonBefore="每"
|
||||||
|
addonAfter="天"
|
||||||
|
style={{ width: 150 }}
|
||||||
|
min={0}
|
||||||
|
onBlur={updateRemoveLogFrequency}
|
||||||
|
onChange={(value) => setLogRemoveFrequency(value)}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="检查更新" name="update">
|
||||||
|
<CheckUpdate socketMessage={socketMessage} />
|
||||||
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
<AppModal
|
||||||
|
visible={isModalVisible}
|
||||||
|
handleCancel={handleCancel}
|
||||||
|
app={editedApp}
|
||||||
|
/>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Password;
|
export default Setting;
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Typography, Table, Tag, Button, Spin, message } from 'antd';
|
||||||
|
import { request } from '@/utils/http';
|
||||||
|
import config from '@/utils/config';
|
||||||
|
|
||||||
|
const { Text, Link } = Typography;
|
||||||
|
|
||||||
|
enum LoginStatus {
|
||||||
|
'成功',
|
||||||
|
'失败',
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LoginStatusColor {
|
||||||
|
'success',
|
||||||
|
'error',
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '序号',
|
||||||
|
align: 'center' as const,
|
||||||
|
width: 50,
|
||||||
|
render: (text: string, record: any, index: number) => {
|
||||||
|
return <span style={{ cursor: 'text' }}>{index + 1} </span>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '登录时间',
|
||||||
|
dataIndex: 'timestamp',
|
||||||
|
key: 'timestamp',
|
||||||
|
align: 'center' as const,
|
||||||
|
render: (text: string, record: any) => {
|
||||||
|
return <span>{new Date(record.timestamp).toLocaleString()}</span>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '登录地址',
|
||||||
|
dataIndex: 'address',
|
||||||
|
key: 'address',
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '登录IP',
|
||||||
|
dataIndex: 'ip',
|
||||||
|
key: 'ip',
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '登录设备',
|
||||||
|
dataIndex: 'platform',
|
||||||
|
key: 'platform',
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '登录状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
align: 'center' as const,
|
||||||
|
render: (text: string, record: any) => {
|
||||||
|
return (
|
||||||
|
<Tag color={LoginStatusColor[record.status]} style={{ marginRight: 0 }}>
|
||||||
|
{LoginStatus[record.status]}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const LoginLog = ({ data }: any) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
pagination={false}
|
||||||
|
dataSource={data}
|
||||||
|
rowKey="_id"
|
||||||
|
size="middle"
|
||||||
|
scroll={{ x: 768 }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginLog;
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Typography, Input, Form, Button, Select, message } from 'antd';
|
||||||
|
import { request } from '@/utils/http';
|
||||||
|
import config from '@/utils/config';
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
const NotificationSetting = ({ data }: any) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [notificationMode, setNotificationMode] = useState<string>('closed');
|
||||||
|
const [fields, setFields] = useState<any[]>([]);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const handleOk = (values: any) => {
|
||||||
|
if (values.type == 'closed') {
|
||||||
|
values.type = '';
|
||||||
|
}
|
||||||
|
request
|
||||||
|
.put(`${config.apiPrefix}user/notification`, {
|
||||||
|
data: {
|
||||||
|
...values,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((_data: any) => {
|
||||||
|
if (_data && _data.code === 200) {
|
||||||
|
message.success(values.type ? '通知发送成功' : '通知关闭成功');
|
||||||
|
} else {
|
||||||
|
message.error(_data.data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const notificationModeChange = (value: string) => {
|
||||||
|
setNotificationMode(value);
|
||||||
|
const _fields = (config.notificationModeMap as any)[value];
|
||||||
|
setFields(_fields || []);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data && data.type) {
|
||||||
|
notificationModeChange(data.type);
|
||||||
|
form.setFieldsValue({ ...data });
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form onFinish={handleOk} form={form} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
label="通知方式"
|
||||||
|
name="type"
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
style={{ maxWidth: 400 }}
|
||||||
|
initialValue={notificationMode}
|
||||||
|
>
|
||||||
|
<Select onChange={notificationModeChange}>
|
||||||
|
{config.notificationModes.map((x) => (
|
||||||
|
<Option value={x.value}>{x.label}</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
{fields.map((x) => (
|
||||||
|
<Form.Item
|
||||||
|
label={x.label}
|
||||||
|
name={x.label}
|
||||||
|
extra={x.tip}
|
||||||
|
rules={[{ required: x.required }]}
|
||||||
|
style={{ maxWidth: 400 }}
|
||||||
|
>
|
||||||
|
<Input.TextArea autoSize={true} placeholder={`请输入${x.label}`} />
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationSetting;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user