mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-12 03:10:48 +08:00
Compare commits
297
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
+6
-8
@@ -1,11 +1,9 @@
|
|||||||
GRPC_PORT=5500
|
MONGODB_URI='mongodb://'
|
||||||
BACK_PORT=5700
|
YIYAN_MONGODB_URI=''
|
||||||
|
|
||||||
LOG_LEVEL='info'
|
CRON_PORT=5500
|
||||||
|
PORT=5600
|
||||||
|
|
||||||
JWT_SECRET=
|
LOG_LEVEL='debug'
|
||||||
JWT_EXPIRES_IN=
|
|
||||||
|
|
||||||
QINIU_AK=
|
SECRET='whyour'
|
||||||
QINIU_SK=
|
|
||||||
QINIU_SCOPE=
|
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
name: "\U0001F41E Bug report"
|
|
||||||
description: Create a report to help us improve
|
|
||||||
body:
|
|
||||||
- type: input
|
|
||||||
id: version
|
|
||||||
attributes:
|
|
||||||
label: Qinglong version
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: steps-to-reproduce
|
|
||||||
attributes:
|
|
||||||
label: Steps to reproduce
|
|
||||||
description: |
|
|
||||||
What do we need to do after opening your repro in order to make the bug happen? Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format lists and code.
|
|
||||||
placeholder: Steps to reproduce
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: expected
|
|
||||||
attributes:
|
|
||||||
label: What is expected?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: actually-happening
|
|
||||||
attributes:
|
|
||||||
label: What is actually happening?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: system-info
|
|
||||||
attributes:
|
|
||||||
label: System Info
|
|
||||||
description: Output of `npx envinfo --system --binaries --browsers`
|
|
||||||
render: shell
|
|
||||||
placeholder: System, Binaries, Browsers
|
|
||||||
- type: textarea
|
|
||||||
id: additional-comments
|
|
||||||
attributes:
|
|
||||||
label: Any additional comments?
|
|
||||||
description: e.g. some background/context of how you ran into this bug.
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
contact_links:
|
|
||||||
- name: Telegram Chat
|
|
||||||
url: https://t.me/jiao_long
|
|
||||||
about: Ask questions and discuss with other Qinglong users in real time.
|
|
||||||
- name: Questions & Discussions
|
|
||||||
url: https://github.com/whyour/qinglong/discussions/new?category=q-a
|
|
||||||
about: Use GitHub discussions for message-board style questions and discussions.
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
name: "\U0001F680 New feature proposal"
|
|
||||||
description: Suggest an idea for this project
|
|
||||||
labels: [":sparkles: feature request"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Thanks for your interest in the project and taking the time to fill out this feature report!
|
|
||||||
- type: textarea
|
|
||||||
id: feature-description
|
|
||||||
attributes:
|
|
||||||
label: Clear and concise description of the problem
|
|
||||||
description: "Explain your use case, context, and rationale behind this feature request. More importantly, what is the **end user experience** you are trying to build that led to the need for this feature?"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: suggested-solution
|
|
||||||
attributes:
|
|
||||||
label: Suggested solution
|
|
||||||
description: "In module [xy] we could provide following implementation..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: alternative
|
|
||||||
attributes:
|
|
||||||
label: Alternative
|
|
||||||
description: Clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
- type: textarea
|
|
||||||
id: additional-context
|
|
||||||
attributes:
|
|
||||||
label: Additional context
|
|
||||||
description: Any other context or screenshots about the feature request here.
|
|
||||||
- type: checkboxes
|
|
||||||
id: checkboxes
|
|
||||||
attributes:
|
|
||||||
label: Validations
|
|
||||||
description: Before submitting the issue, please make sure you do the following
|
|
||||||
options:
|
|
||||||
- label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
|
|
||||||
required: true
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
## PR Type
|
|
||||||
What kind of change does this PR introduce?
|
|
||||||
|
|
||||||
<!-- Please check the one that applies to this PR using "x". -->
|
|
||||||
|
|
||||||
- [ ] Bugfix
|
|
||||||
- [ ] Feature
|
|
||||||
- [ ] Code style update (formatting, local variables)
|
|
||||||
- [ ] Refactoring (no functional changes, no api changes)
|
|
||||||
- [ ] Other... Please describe:
|
|
||||||
|
|
||||||
|
|
||||||
## What is the current behavior?
|
|
||||||
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
|
|
||||||
|
|
||||||
Issue Number: N/A
|
|
||||||
|
|
||||||
|
|
||||||
## What is the new behavior?
|
|
||||||
|
|
||||||
|
|
||||||
## Does this PR introduce a breaking change?
|
|
||||||
|
|
||||||
- [ ] Yes
|
|
||||||
- [ ] No
|
|
||||||
|
|
||||||
|
|
||||||
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
|
|
||||||
|
|
||||||
|
|
||||||
## Other information
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug Fixer
|
|
||||||
description: Fix this issue following our error handling pattern.
|
|
||||||
---
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Comment to be posted to on PRs from first time contributors in your repository
|
|
||||||
newPRWelcomeComment: |
|
|
||||||
💖 Thanks for opening this pull request! 💖
|
|
||||||
Please be patient and we will get back to you as soon as we can.
|
|
||||||
# Comment to be posted to on pull requests merged by a first time user
|
|
||||||
firstPRMergeComment: >
|
|
||||||
Congrats on merging your first pull request! 🎉🎉🎉
|
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
name: Build And Push Docker Image
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
paths-ignore:
|
|
||||||
- "*.md"
|
|
||||||
branches:
|
|
||||||
- "master"
|
|
||||||
- "develop"
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
code_gitlab:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: Yikun/hub-mirror-action@master
|
|
||||||
with:
|
|
||||||
src: github/whyour
|
|
||||||
dst: gitlab/whyour
|
|
||||||
dst_key: ${{ secrets.GITLAB_SSH_PK }}
|
|
||||||
dst_token: ${{ secrets.GITLAB_TOKEN }}
|
|
||||||
static_list: "qinglong"
|
|
||||||
force_update: true
|
|
||||||
|
|
||||||
code_gitee:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: Yikun/hub-mirror-action@master
|
|
||||||
with:
|
|
||||||
src: github/whyour
|
|
||||||
dst: gitee/whyour
|
|
||||||
dst_key: ${{ secrets.GITLAB_SSH_PK }}
|
|
||||||
dst_token: ${{ secrets.GITEE_TOKEN }}
|
|
||||||
static_list: "qinglong"
|
|
||||||
force_update: true
|
|
||||||
|
|
||||||
build-static:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
version: "8.3.1"
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
cache: "pnpm"
|
|
||||||
|
|
||||||
- name: build front and back
|
|
||||||
run: |
|
|
||||||
pnpm install --frozen-lockfile
|
|
||||||
pnpm build:front
|
|
||||||
pnpm build:back
|
|
||||||
|
|
||||||
- name: copy to static repo
|
|
||||||
env:
|
|
||||||
GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static
|
|
||||||
GITHUB_BRANCH: ${{ github.ref_name }}
|
|
||||||
REPO_GITEE: git@gitee.com:whyour/qinglong-static.git
|
|
||||||
REPO_GITLAB: git@gitlab.com:whyour/qinglong-static.git
|
|
||||||
PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}
|
|
||||||
run: |
|
|
||||||
mkdir -p tmp
|
|
||||||
cd ./tmp
|
|
||||||
cp -rf ../static/* ./
|
|
||||||
git init -b ${GITHUB_BRANCH} && git add .
|
|
||||||
git config --local user.name 'github-actions[bot]'
|
|
||||||
git config --local user.email 'github-actions[bot]@users.noreply.github.com'
|
|
||||||
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
|
|
||||||
git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH}
|
|
||||||
|
|
||||||
static_gitlab:
|
|
||||||
needs: build-static
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: Yikun/hub-mirror-action@master
|
|
||||||
with:
|
|
||||||
src: github/whyour
|
|
||||||
dst: gitlab/whyour
|
|
||||||
dst_key: ${{ secrets.GITLAB_SSH_PK }}
|
|
||||||
dst_token: ${{ secrets.GITLAB_TOKEN }}
|
|
||||||
static_list: "qinglong-static"
|
|
||||||
force_update: true
|
|
||||||
|
|
||||||
static_gitee:
|
|
||||||
needs: build-static
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: Yikun/hub-mirror-action@master
|
|
||||||
with:
|
|
||||||
src: github/whyour
|
|
||||||
dst: gitee/whyour
|
|
||||||
dst_key: ${{ secrets.GITLAB_SSH_PK }}
|
|
||||||
dst_token: ${{ secrets.GITEE_TOKEN }}
|
|
||||||
static_list: "qinglong-static"
|
|
||||||
force_update: true
|
|
||||||
|
|
||||||
build:
|
|
||||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
|
||||||
needs: build-static
|
|
||||||
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
version: "8.3.1"
|
|
||||||
- uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
cache: "pnpm"
|
|
||||||
|
|
||||||
- name: Read version from version.yaml
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
VERSION=$(grep '^version:' version.yaml | awk '{print $2}')
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "Version: $VERSION"
|
|
||||||
|
|
||||||
- name: Setup timezone
|
|
||||||
uses: szenius/set-timezone@v2.0
|
|
||||||
with:
|
|
||||||
timezoneLinux: Asia/Shanghai
|
|
||||||
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Login to GHCR
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
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@v5
|
|
||||||
with:
|
|
||||||
images: |
|
|
||||||
${{ github.repository }}
|
|
||||||
ghcr.io/${{ github.repository }}
|
|
||||||
flavor: |
|
|
||||||
latest=false
|
|
||||||
tags: |
|
|
||||||
type=ref,event=branch,enable=${{ github.ref == format('refs/heads/{0}', 'develop') }}
|
|
||||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
|
||||||
type=raw,value=${{ steps.version.outputs.version }},enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
id: docker_build
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
build-args: |
|
|
||||||
MAINTAINER=${{ github.repository_owner }}
|
|
||||||
QL_BRANCH=${{ github.ref_name }}
|
|
||||||
SOURCE_COMMIT=${{ github.sha }}
|
|
||||||
network: host
|
|
||||||
# linux/s390x npm 暂不可用
|
|
||||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/386
|
|
||||||
context: .
|
|
||||||
file: ./docker/Dockerfile
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=registry,ref=whyour/qinglong:cache
|
|
||||||
cache-to: type=registry,ref=whyour/qinglong:cache,mode=max
|
|
||||||
|
|
||||||
- name: Image digest
|
|
||||||
run: |
|
|
||||||
echo ${{ steps.docker_build.outputs.digest }}
|
|
||||||
|
|
||||||
build310:
|
|
||||||
if: ${{ github.ref_name == 'master' }}
|
|
||||||
needs: build-static
|
|
||||||
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
version: "8.3.1"
|
|
||||||
- uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
cache: "pnpm"
|
|
||||||
|
|
||||||
- name: Read version from version.yaml
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
VERSION=$(grep '^version:' version.yaml | awk '{print $2}')
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "Version: $VERSION"
|
|
||||||
|
|
||||||
- name: Setup timezone
|
|
||||||
uses: szenius/set-timezone@v2.0
|
|
||||||
with:
|
|
||||||
timezoneLinux: Asia/Shanghai
|
|
||||||
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Login to GHCR
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Build and push python3.10
|
|
||||||
id: docker_build_310
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
build-args: |
|
|
||||||
MAINTAINER=${{ github.repository_owner }}
|
|
||||||
QL_BRANCH=${{ github.ref_name }}
|
|
||||||
SOURCE_COMMIT=${{ github.sha }}
|
|
||||||
network: host
|
|
||||||
# linux/s390x npm 暂不可用
|
|
||||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/386
|
|
||||||
context: .
|
|
||||||
file: ./docker/310.Dockerfile
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
whyour/qinglong:python3.10
|
|
||||||
whyour/qinglong:${{ steps.version.outputs.version }}-python3.10
|
|
||||||
cache-from: type=registry,ref=whyour/qinglong:cache-python3.10
|
|
||||||
cache-to: type=registry,ref=whyour/qinglong:cache-python3.10,mode=max
|
|
||||||
|
|
||||||
- name: Image digest
|
|
||||||
run: |
|
|
||||||
echo ${{ steps.docker_build_310.outputs.digest }}
|
|
||||||
@@ -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 }}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
name: PublishDockerImage
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout base
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Docker login
|
||||||
|
env:
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin
|
||||||
|
|
||||||
|
- name: Docker buildx image and push on master branch
|
||||||
|
env:
|
||||||
|
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
|
run: |
|
||||||
|
docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x --tag whyour/qinglong:latest docker/
|
||||||
+10
-9
@@ -4,12 +4,12 @@
|
|||||||
/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
|
||||||
/static
|
/dist
|
||||||
/data
|
/build
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -22,9 +22,10 @@
|
|||||||
.env
|
.env
|
||||||
.history
|
.history
|
||||||
.version.ts
|
.version.ts
|
||||||
/.tmp
|
|
||||||
__pycache__
|
/config
|
||||||
/shell/preload/env.*
|
/log
|
||||||
/shell/preload/notify.*
|
/db
|
||||||
/shell/preload/*-notify.json
|
/manual_log
|
||||||
/shell/preload/__ql_notify__.*
|
/scripts
|
||||||
|
/bak
|
||||||
+4
-20
@@ -2,23 +2,7 @@
|
|||||||
**/*.svg
|
**/*.svg
|
||||||
**/*.ejs
|
**/*.ejs
|
||||||
**/*.html
|
**/*.html
|
||||||
/.umi
|
package.json
|
||||||
/.umi-production
|
.umi
|
||||||
/.umi-test
|
.umi-production
|
||||||
/.history
|
.umi-test
|
||||||
/.tmp
|
|
||||||
/node_modules
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-error.log
|
|
||||||
yarn.lock
|
|
||||||
package-lock.json
|
|
||||||
/static
|
|
||||||
/data
|
|
||||||
DS_Store
|
|
||||||
/src/.umi
|
|
||||||
/src/.umi-production
|
|
||||||
/src/.umi-test
|
|
||||||
.env.local
|
|
||||||
.env
|
|
||||||
version.ts
|
|
||||||
/.tmp
|
|
||||||
|
|||||||
@@ -1,29 +1,25 @@
|
|||||||
import { defineConfig } from '@umijs/max';
|
import { defineConfig } from 'umi';
|
||||||
const CompressionPlugin = require('compression-webpack-plugin');
|
const CompressionPlugin = require('compression-webpack-plugin');
|
||||||
|
|
||||||
const baseUrl = process.env.QlBaseUrl || '/';
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
hash: true,
|
hash: true,
|
||||||
jsMinifier: 'terser',
|
layout: false,
|
||||||
antd: {},
|
nodeModulesTransform: {
|
||||||
locale: {
|
type: 'none',
|
||||||
antd: true,
|
|
||||||
title: true,
|
|
||||||
baseNavigator: true,
|
|
||||||
},
|
},
|
||||||
outputPath: 'static/dist',
|
fastRefresh: {},
|
||||||
fastRefresh: true,
|
dynamicImport: {
|
||||||
favicons: [`https://qn.whyour.cn/favicon.svg`],
|
loading: '@/components/pageLoading',
|
||||||
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
|
},
|
||||||
|
favicon: '/images/g5.ico',
|
||||||
proxy: {
|
proxy: {
|
||||||
[`${baseUrl}api`]: {
|
'/api': {
|
||||||
target: 'http://127.0.0.1:5700/',
|
target: 'http://127.0.0.1:5600/',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
ws: true,
|
ws: true,
|
||||||
pathRewrite: { [`^${baseUrl}api`]: '/api' },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
chainWebpack: ((config: any) => {
|
chainWebpack: (config) => {
|
||||||
config.plugin('compression-webpack-plugin').use(
|
config.plugin('compression-webpack-plugin').use(
|
||||||
new CompressionPlugin({
|
new CompressionPlugin({
|
||||||
algorithm: 'gzip',
|
algorithm: 'gzip',
|
||||||
@@ -32,13 +28,22 @@ export default defineConfig({
|
|||||||
minRatio: 0.6,
|
minRatio: 0.6,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}) as any,
|
},
|
||||||
headScripts: [`./api/env.js`],
|
externals: {
|
||||||
copy: [
|
react: 'window.React',
|
||||||
{
|
'react-dom': 'window.ReactDOM',
|
||||||
from: 'node_modules/monaco-editor/min/vs',
|
darkreader: 'window.DarkReader',
|
||||||
to: 'static/dist/monaco-editor/min/vs',
|
codemirror: 'window.CodeMirror',
|
||||||
},
|
'sockjs-client': 'window.SockJS',
|
||||||
|
},
|
||||||
|
scripts: [
|
||||||
|
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
|
||||||
|
'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
|
||||||
|
'https://cdn.jsdelivr.net/npm/darkreader@4/darkreader.min.js',
|
||||||
|
'https://cdn.jsdelivr.net/npm/codemirror@5/lib/codemirror.min.js',
|
||||||
|
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/shell/shell.js',
|
||||||
|
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/python/python.js',
|
||||||
|
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/javascript/javascript.js',
|
||||||
|
'https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js',
|
||||||
],
|
],
|
||||||
npmClient: 'pnpm',
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,201 +1,22 @@
|
|||||||
Apache License
|
MIT LICENSE
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
Copyright (c) 2021-present whyour
|
||||||
|
|
||||||
1. Definitions.
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
The above copyright notice and this permission notice shall be
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
the copyright owner that is granting the License.
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
other entities that control, are controlled by, or are under common
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
control with that entity. For the purposes of this definition,
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright 2021 WHYOUR <https://github.com/whyour>.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-101
@@ -1,101 +0,0 @@
|
|||||||
<div align="center">
|
|
||||||
<img width="100" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
|
|
||||||
|
|
||||||
<h1 align="center">Qinglong</h1>
|
|
||||||
|
|
||||||
[简体中文](./README.md) | English
|
|
||||||
|
|
||||||
Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
|
||||||
|
|
||||||
[![npm version][npm-version-image]][npm-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
|
|
||||||
|
|
||||||
[npm-version-image]: https://img.shields.io/npm/v/@whyour/qinglong?style=flat
|
|
||||||
[npm-version-url]: https://www.npmjs.com/package/@whyour/qinglong?activeTab=readme
|
|
||||||
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
|
|
||||||
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
|
||||||
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
|
|
||||||
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
|
|
||||||
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
|
|
||||||
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
|
||||||
|
|
||||||
[Demo](http://demo.qinglong.online:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
|
|
||||||
|
|
||||||
[演示](http://demo.qinglong.online:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
|
|
||||||
</div>
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Support for multiple scripting languages (python3, javaScript, shell, typescript)
|
|
||||||
- Support online management of scripts, environment variables, configuration files
|
|
||||||
- Support online view task log
|
|
||||||
- Support second-level task setting
|
|
||||||
- Support system level notification
|
|
||||||
- Support dark mode
|
|
||||||
- Support cell phone operation
|
|
||||||
|
|
||||||
## Version
|
|
||||||
|
|
||||||
### docker
|
|
||||||
|
|
||||||
The `latest` image is built on `alpine` and the `debian` image is built on `debian-slim`. If you need to use a dependency that is not supported by `alpine`, it is recommended that you use the `debian` image.
|
|
||||||
|
|
||||||
**⚠️ Important**: If you need to run Docker as a **non-root user**, please use the `debian` image. Alpine's `crond` requires root privileges.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker pull whyour/qinglong:latest
|
|
||||||
docker pull whyour/qinglong:debian
|
|
||||||
```
|
|
||||||
|
|
||||||
### npm
|
|
||||||
|
|
||||||
The npm version supports `debian/ubuntu/alpine` systems and requires `node/npm/python3/pip3/pnpm` to be installed.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm i @whyour/qinglong
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
[View Documentation](https://qinglong.online/guide/getting-started/installation-guide)
|
|
||||||
|
|
||||||
## Built-in API
|
|
||||||
|
|
||||||
[View Documentation](https://qinglong.online/guide/user-guide/built-in-api)
|
|
||||||
|
|
||||||
## Built-in commands
|
|
||||||
|
|
||||||
[View Documentation](https://qinglong.online/guide/user-guide/basic-explanation)
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/whyour/qinglong.git
|
|
||||||
cd qinglong
|
|
||||||
cp .env.example .env
|
|
||||||
# Recommended use pnpm https://pnpm.io/zh/installation
|
|
||||||
npm install -g pnpm@8.3.1
|
|
||||||
pnpm install
|
|
||||||
pnpm start
|
|
||||||
```
|
|
||||||
|
|
||||||
Open your browser and visit <http://127.0.0.1:5700>
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- [nevinee](https://gitee.com/evine)
|
|
||||||
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
|
|
||||||
- [Ant Design](https://ant.design)
|
|
||||||
- [Ant Design Pro](https://pro.ant.design/)
|
|
||||||
- [Umijs](https://umijs.org)
|
|
||||||
- [darkreader](https://github.com/darkreader/darkreader)
|
|
||||||
- [admin-server](https://github.com/sunpu007/admin-server)
|
|
||||||
|
|
||||||
## Name Origin
|
|
||||||
|
|
||||||
The Green Dragon, also known as the Canglong, is one of the four elephants and one of the [four spirits of the heavens](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5) in traditional Chinese culture. According to the Five Elements, it is a spirit animal representing the East as a green dragon, the five elements are wood, and the season represented is spring, with the eight trigrams dominating vibration. Like the Ying Long, the Cang Long has feathered wings. According to the Zhang Guo Xing Zong (Zhang Guo Xing Zong), "a true dragon is one that has complementary wings".
|
|
||||||
|
|
||||||
In the Book of the Later Han Dynasty (後漢書-律曆志下), it is written: "The sun is in the sky, a cold and a summer, the four seasons are ready, all things are changed, the regency moves, and the green dragon moves to the star, which is called the year. (The Year of the Star)
|
|
||||||
|
|
||||||
Among the [twenty-eight Chinese constellations](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF), the Green Dragon is the generic name for the seven eastern constellations (Horn, Hyper, Diao, Fang, Heart, Tail and Minchi). It is known in Taoism as "Mengzhang" and in different Taoist scriptures as "Dijun", "Shengjian", "Shenjian" and He is also known in different Daoist scriptures as "Dijun", "Shengjun", "Shenjun" and "Ghost Catcher"[1], and is the guardian deity of Daoism, together with the White Tiger Supervisor of Soldiers.
|
|
||||||
@@ -1,31 +1,30 @@
|
|||||||
<div align="center">
|
<p align="center">
|
||||||
<img width="100" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
|
<a href="https://github.com/whyour/qinglong">
|
||||||
|
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<h1 align="center">青龙</h1>
|
<h1 align="center">青龙</h1>
|
||||||
|
|
||||||
简体中文 | [English](./README-en.md)
|
<div align="center">
|
||||||
|
|
||||||
支持 Python3、JavaScript、Shell、Typescript 的定时任务管理平台
|
支持python3、javaScript、shell、typescript 的定时任务管理面板(A timed task management panel that supports typescript, javaScript, python3, and shell.)
|
||||||
|
|
||||||
Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
|
||||||
|
|
||||||
[![npm version][npm-version-image]][npm-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
|
|
||||||
|
|
||||||
[npm-version-image]: https://img.shields.io/npm/v/@whyour/qinglong?style=flat
|
|
||||||
[npm-version-url]: https://www.npmjs.com/package/@whyour/qinglong?activeTab=readme
|
|
||||||
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
|
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
|
||||||
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
|
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
|
||||||
|
[docker-version-url]: https://hub.docker.com/r/whyour/qinglong/tags?page=1&ordering=last_updated
|
||||||
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
|
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
|
||||||
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
|
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
|
||||||
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
||||||
|
|
||||||
[Demo](http://demo.qinglong.online:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
|
|
||||||
|
|
||||||
[演示](http://demo.qinglong.online:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||

|
[](https://whyour.cn)
|
||||||
|
|
||||||
|
简体中文 | [English](./README-en.md)
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
|
|
||||||
@@ -37,52 +36,105 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
|||||||
- 支持暗黑模式
|
- 支持暗黑模式
|
||||||
- 支持手机端操作
|
- 支持手机端操作
|
||||||
|
|
||||||
## 版本
|
|
||||||
|
|
||||||
### docker
|
|
||||||
|
|
||||||
`latest` 镜像是基于 `alpine` 构建,`debian` 镜像是基于 `debian-slim` 构建。如果需要使用 `alpine` 不支持的依赖,建议使用 `debian` 镜像
|
|
||||||
|
|
||||||
**⚠️ 重要提示**: 如果您需要以**非 root 用户**运行 Docker,请使用 `debian` 镜像。Alpine 的 `crond` 需要 root 权限。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker pull whyour/qinglong:latest
|
|
||||||
docker pull whyour/qinglong:debian
|
|
||||||
```
|
|
||||||
|
|
||||||
### npm
|
|
||||||
|
|
||||||
npm 版本支持 `debian/ubuntu/alpine` 系统,需要自行安装 `node/npm/python3/pip3/pnpm`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm i @whyour/qinglong
|
|
||||||
```
|
|
||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
|
|
||||||
[查看文档](https://qinglong.online/guide/getting-started/installation-guide)
|
### 本机部署
|
||||||
|
|
||||||
## 内置 API
|
|
||||||
|
|
||||||
[查看文档](https://qinglong.online/guide/user-guide/built-in-api)
|
|
||||||
|
|
||||||
## 内置命令
|
|
||||||
|
|
||||||
[查看文档](https://qinglong.online/guide/user-guide/basic-explanation)
|
|
||||||
|
|
||||||
## 开发
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/whyour/qinglong.git
|
# 待完善
|
||||||
cd qinglong
|
|
||||||
cp .env.example .env
|
|
||||||
# 推荐使用 pnpm https://pnpm.io/zh/installation
|
|
||||||
npm install -g pnpm@8.3.1
|
|
||||||
pnpm install
|
|
||||||
pnpm start
|
|
||||||
```
|
```
|
||||||
|
|
||||||
打开你的浏览器,访问 <http://127.0.0.1:5700>
|
### 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: 任务执行时指定某个环境变量需要执行的账号序号
|
||||||
|
|
||||||
## 链接
|
## 链接
|
||||||
|
|
||||||
@@ -90,10 +142,27 @@ pnpm start
|
|||||||
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
|
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
|
||||||
- [Ant Design](https://ant.design)
|
- [Ant Design](https://ant.design)
|
||||||
- [Ant Design Pro](https://pro.ant.design/)
|
- [Ant Design Pro](https://pro.ant.design/)
|
||||||
- [Umijs](https://umijs.org)
|
- [Umijs3.0](https://umijs.org)
|
||||||
- [darkreader](https://github.com/darkreader/darkreader)
|
- [darkreader](https://github.com/darkreader/darkreader)
|
||||||
- [admin-server](https://github.com/sunpu007/admin-server)
|
- [admin-server](https://github.com/sunpu007/admin-server)
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ git clone git@github.com:whyour/qinglong.git
|
||||||
|
$ cd qinglong
|
||||||
|
$ cp .env.example .env
|
||||||
|
$ 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)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
To report a vulnerability, please open a private vulnerability report at <https://github.com/whyour/qinglong/security>.
|
|
||||||
|
|
||||||
While the discovery of new vulnerabilities is rare, we also recommend always using the latest versions of Qinglong to ensure your application remains as secure as possible.
|
|
||||||
+27
-43
@@ -1,38 +1,21 @@
|
|||||||
|
import { 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/promises';
|
import * as fs from 'fs';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
import { join } from 'path';
|
|
||||||
import { SAMPLE_FILES } from '../config/const';
|
|
||||||
import ConfigService from '../services/config';
|
|
||||||
import { writeFileWithLock } from '../shared/utils';
|
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/configs', route);
|
app.use('/', route);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/sample',
|
'/configs/files',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
res.send({
|
|
||||||
code: 200,
|
|
||||||
data: SAMPLE_FILES,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.get(
|
|
||||||
'/files',
|
|
||||||
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 fileList = await fs.readdir(config.configPath, 'utf-8');
|
const fileList = fs.readdirSync(config.configPath, 'utf-8');
|
||||||
res.send({
|
res.send({
|
||||||
code: 200,
|
code: 200,
|
||||||
data: fileList
|
data: fileList
|
||||||
@@ -42,25 +25,40 @@ export default (app: Router) => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/detail',
|
'/configs/:file',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const configService = Container.get(ConfigService);
|
let content = '';
|
||||||
await configService.getFile(req.query.path as string, res);
|
if (config.blackFileList.includes(req.params.file)) {
|
||||||
|
res.send({ code: 403, message: '文件无法访问' });
|
||||||
|
}
|
||||||
|
if (req.params.file.includes('sample')) {
|
||||||
|
content = getFileContentByName(
|
||||||
|
`${config.samplePath}${req.params.file}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
content = getFileContentByName(
|
||||||
|
`${config.configPath}${req.params.file}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
res.send({ code: 200, data: content });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
'/save',
|
'/configs/save',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
@@ -74,25 +72,11 @@ export default (app: Router) => {
|
|||||||
if (config.blackFileList.includes(name)) {
|
if (config.blackFileList.includes(name)) {
|
||||||
res.send({ code: 403, message: '文件无法访问' });
|
res.send({ code: 403, message: '文件无法访问' });
|
||||||
}
|
}
|
||||||
let path = join(config.configPath, name);
|
const path = `${config.configPath}${name}`;
|
||||||
if (name.startsWith('data/scripts/')) {
|
fs.writeFileSync(path, content);
|
||||||
path = join(config.rootPath, name);
|
|
||||||
}
|
|
||||||
await writeFileWithLock(path, content);
|
|
||||||
res.send({ code: 200, message: '保存成功' });
|
res.send({ code: 200, message: '保存成功' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
logger.error('🔥 error: %o', e);
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.get(
|
|
||||||
'/:file',
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const configService = Container.get(ConfigService);
|
|
||||||
await configService.getFile(req.params.file, res);
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+93
-267
@@ -2,164 +2,21 @@ import { Router, Request, Response, NextFunction } from 'express';
|
|||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import CronService from '../services/cron';
|
import CronService from '../services/cron';
|
||||||
import CronViewService from '../services/cronView';
|
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
import { commonCronSchema } from '../validation/schedule';
|
import cron_parser from 'cron-parser';
|
||||||
|
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/crons', route);
|
app.use('/', route);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/views',
|
'/crons',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const cronViewService = Container.get(CronViewService);
|
|
||||||
const data = await cronViewService.list();
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.post(
|
|
||||||
'/views',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
name: Joi.string().required(),
|
|
||||||
sorts: Joi.array().optional().allow(null),
|
|
||||||
filters: Joi.array().optional(),
|
|
||||||
filterRelation: Joi.string().optional(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const cronViewService = Container.get(CronViewService);
|
|
||||||
const data = await cronViewService.create(req.body);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/views',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
name: Joi.string().required(),
|
|
||||||
id: Joi.number().required(),
|
|
||||||
sorts: Joi.array().optional().allow(null),
|
|
||||||
filters: Joi.array().optional(),
|
|
||||||
filterRelation: Joi.string().optional(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const cronViewService = Container.get(CronViewService);
|
|
||||||
if (req.body.type === 1) {
|
|
||||||
return res.send({ code: 400, message: '参数错误' });
|
|
||||||
} else {
|
|
||||||
const data = await cronViewService.update(req.body);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.delete(
|
|
||||||
'/views',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.array().items(Joi.number().required()),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const cronViewService = Container.get(CronViewService);
|
|
||||||
const data = await cronViewService.remove(req.body);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/views/move',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
fromIndex: Joi.number().required(),
|
|
||||||
toIndex: Joi.number().required(),
|
|
||||||
id: Joi.number().required(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const cronViewService = Container.get(CronViewService);
|
|
||||||
const data = await cronViewService.move(req.body);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/views/disable',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.array().items(Joi.number().required()),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const cronViewService = Container.get(CronViewService);
|
|
||||||
const data = await cronViewService.disabled(req.body);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/views/enable',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.array().items(Joi.number().required()),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const cronViewService = Container.get(CronViewService);
|
|
||||||
const data = await cronViewService.enabled(req.body);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const cronService = Container.get(CronService);
|
|
||||||
const data = await cronService.crontabs(req.query as any);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
route.get(
|
|
||||||
'/detail',
|
|
||||||
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 cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const data = await cronService.find(req.query as any);
|
const data = await cronService.crontabs(
|
||||||
|
req.query.searchValue as string,
|
||||||
|
);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
logger.error('🔥 error: %o', e);
|
||||||
@@ -169,26 +26,35 @@ export default (app: Router) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
'/',
|
'/crons',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object(commonCronSchema),
|
body: Joi.object({
|
||||||
|
command: Joi.string().required(),
|
||||||
|
schedule: Joi.string().required(),
|
||||||
|
name: Joi.string().optional(),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
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 cronService = Container.get(CronService);
|
if (cron_parser.parseExpression(req.body.schedule).hasNext()) {
|
||||||
const data = await cronService.create(req.body);
|
const cronService = Container.get(CronService);
|
||||||
return res.send({ code: 200, data });
|
const data = await cronService.create(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} else {
|
||||||
|
return res.send({ code: 400, message: 'param schedule error' });
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/run',
|
'/crons/run',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.number().required()),
|
body: Joi.array().items(Joi.string().required()),
|
||||||
}),
|
}),
|
||||||
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');
|
||||||
@@ -197,15 +63,16 @@ export default (app: Router) => {
|
|||||||
const data = await cronService.run(req.body);
|
const data = await cronService.run(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/stop',
|
'/crons/stop',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.number().required()),
|
body: Joi.array().items(Joi.string().required()),
|
||||||
}),
|
}),
|
||||||
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');
|
||||||
@@ -214,58 +81,16 @@ export default (app: Router) => {
|
|||||||
const data = await cronService.stop(req.body);
|
const data = await cronService.stop(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
logger.error('🔥 error: %o', e);
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.delete(
|
|
||||||
'/labels',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
ids: Joi.array().items(Joi.number().required()),
|
|
||||||
labels: Joi.array().items(Joi.string().required()),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const cronService = Container.get(CronService);
|
|
||||||
const data = await cronService.removeLabels(
|
|
||||||
req.body.ids,
|
|
||||||
req.body.labels,
|
|
||||||
);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.post(
|
|
||||||
'/labels',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
ids: Joi.array().items(Joi.number().required()),
|
|
||||||
labels: Joi.array().items(Joi.string().required()),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const cronService = Container.get(CronService);
|
|
||||||
const data = await cronService.addLabels(req.body.ids, req.body.labels);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/disable',
|
'/crons/disable',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.number().required()),
|
body: Joi.array().items(Joi.string().required()),
|
||||||
}),
|
}),
|
||||||
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');
|
||||||
@@ -274,15 +99,16 @@ export default (app: Router) => {
|
|||||||
const data = await cronService.disabled(req.body);
|
const data = await cronService.disabled(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/enable',
|
'/crons/enable',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.number().required()),
|
body: Joi.array().items(Joi.string().required()),
|
||||||
}),
|
}),
|
||||||
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');
|
||||||
@@ -291,54 +117,66 @@ export default (app: Router) => {
|
|||||||
const data = await cronService.enabled(req.body);
|
const data = await cronService.enabled(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/:id/log',
|
'/crons/:id/log',
|
||||||
celebrate({
|
celebrate({
|
||||||
params: Joi.object({
|
params: Joi.object({
|
||||||
id: Joi.number().required(),
|
id: Joi.string().required(),
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const cronService = Container.get(CronService);
|
|
||||||
const data = await cronService.log(req.params.id);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
...commonCronSchema,
|
|
||||||
id: Joi.number().required(),
|
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
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 cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const data = await cronService.update(req.body);
|
const data = await cronService.log(req.params.id);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/crons',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
command: Joi.string().optional(),
|
||||||
|
schedule: Joi.string().optional(),
|
||||||
|
name: Joi.string().optional(),
|
||||||
|
_id: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
!req.body.schedule ||
|
||||||
|
cron_parser.parseExpression(req.body.schedule).hasNext()
|
||||||
|
) {
|
||||||
|
const cronService = Container.get(CronService);
|
||||||
|
const data = await cronService.update(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} else {
|
||||||
|
return res.send({ code: 400, message: 'param schedule error' });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.delete(
|
route.delete(
|
||||||
'/',
|
'/crons',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.number().required()),
|
body: Joi.array().items(Joi.string().required()),
|
||||||
}),
|
}),
|
||||||
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');
|
||||||
@@ -347,15 +185,16 @@ export default (app: Router) => {
|
|||||||
const data = await cronService.remove(req.body);
|
const data = await cronService.remove(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/pin',
|
'/crons/pin',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.number().required()),
|
body: Joi.array().items(Joi.string().required()),
|
||||||
}),
|
}),
|
||||||
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');
|
||||||
@@ -364,15 +203,16 @@ export default (app: Router) => {
|
|||||||
const data = await cronService.pin(req.body);
|
const data = await cronService.pin(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/unpin',
|
'/crons/unpin',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.number().required()),
|
body: Joi.array().items(Joi.string().required()),
|
||||||
}),
|
}),
|
||||||
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');
|
||||||
@@ -381,85 +221,71 @@ export default (app: Router) => {
|
|||||||
const data = await cronService.unPin(req.body);
|
const data = await cronService.unPin(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/import',
|
'/crons/import',
|
||||||
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 cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const data = await cronService.importCrontab();
|
const data = await cronService.import_crontab();
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/:id',
|
'/crons/:id',
|
||||||
celebrate({
|
celebrate({
|
||||||
params: Joi.object({
|
params: Joi.object({
|
||||||
id: Joi.number().required(),
|
id: Joi.string().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request<{ id: number }>, 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 cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const data = await cronService.getDb({ id: req.params.id });
|
const data = await cronService.get(req.params.id);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/status',
|
'/crons/status',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
ids: Joi.array().items(Joi.number().required()),
|
ids: Joi.array().items(Joi.string().required()),
|
||||||
status: Joi.string().required(),
|
status: Joi.string().required(),
|
||||||
pid: Joi.string().optional().allow(null),
|
pid: Joi.string().optional(),
|
||||||
log_path: Joi.string().optional().allow(null),
|
log_path: Joi.string().optional(),
|
||||||
last_running_time: Joi.number().optional().allow(null),
|
last_running_time: Joi.number().optional(),
|
||||||
last_execution_time: Joi.number().optional().allow(null),
|
last_execution_time: Joi.number().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const data = await cronService.status({
|
const data = await cronService.status({
|
||||||
...req.body,
|
...req.body,
|
||||||
status: req.body.status ? parseInt(req.body.status) : undefined,
|
status: parseInt(req.body.status),
|
||||||
pid: req.body.pid ? parseInt(req.body.pid) : undefined,
|
pid: parseInt(req.body.pid) || '',
|
||||||
});
|
});
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
logger.error('🔥 error: %o', e);
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.get(
|
|
||||||
'/:id/logs',
|
|
||||||
celebrate({
|
|
||||||
params: Joi.object({
|
|
||||||
id: Joi.number().required(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const cronService = Container.get(CronService);
|
|
||||||
const data = await cronService.logs(req.params.id);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+30
-43
@@ -6,18 +6,9 @@ import { celebrate, Joi } from 'celebrate';
|
|||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/dependencies', route);
|
app.use('/', route);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/',
|
'/dependencies',
|
||||||
celebrate({
|
|
||||||
query:
|
|
||||||
Joi.object({
|
|
||||||
searchValue: Joi.string().optional().allow(''),
|
|
||||||
type: Joi.string().optional().allow(''),
|
|
||||||
status: Joi.string().optional().allow(''),
|
|
||||||
}).unknown(true),
|
|
||||||
}),
|
|
||||||
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 {
|
||||||
@@ -32,125 +23,121 @@ export default (app: Router) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
'/',
|
'/dependencies',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(
|
body: Joi.array().items(
|
||||||
Joi.object({
|
Joi.object({
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
type: Joi.number().required(),
|
type: Joi.number().required(),
|
||||||
remark: Joi.string().optional().allow(''),
|
remark: Joi.number().optional().allow(''),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const dependenceService = Container.get(DependenceService);
|
const dependenceService = Container.get(DependenceService);
|
||||||
const data = await dependenceService.create(req.body);
|
const data = await dependenceService.create(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/',
|
'/dependencies',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
id: Joi.number().required(),
|
_id: Joi.string().required(),
|
||||||
type: Joi.number().required(),
|
type: Joi.number().required(),
|
||||||
remark: Joi.string().optional().allow(''),
|
remark: Joi.number().optional().allow(''),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const dependenceService = Container.get(DependenceService);
|
const dependenceService = Container.get(DependenceService);
|
||||||
const data = await dependenceService.update(req.body);
|
const data = await dependenceService.update(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.delete(
|
route.delete(
|
||||||
'/',
|
'/dependencies',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.number().required()),
|
body: Joi.array().items(Joi.string().required()),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const dependenceService = Container.get(DependenceService);
|
const dependenceService = Container.get(DependenceService);
|
||||||
const data = await dependenceService.remove(req.body);
|
const data = await dependenceService.remove(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.delete(
|
route.delete(
|
||||||
'/force',
|
'/dependencies/force',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.number().required()),
|
body: Joi.array().items(Joi.string().required()),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const dependenceService = Container.get(DependenceService);
|
const dependenceService = Container.get(DependenceService);
|
||||||
const data = await dependenceService.remove(req.body, true);
|
const data = await dependenceService.removeDb(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/:id',
|
'/dependencies/:id',
|
||||||
celebrate({
|
celebrate({
|
||||||
params: Joi.object({
|
params: Joi.object({
|
||||||
id: Joi.number().required(),
|
id: Joi.string().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const dependenceService = Container.get(DependenceService);
|
const dependenceService = Container.get(DependenceService);
|
||||||
const data = await dependenceService.getDb({ id: req.params.id });
|
const data = await dependenceService.get(req.params.id);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/reinstall',
|
'/dependencies/reinstall',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.number().required()),
|
body: Joi.array().items(Joi.string().required()),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const dependenceService = Container.get(DependenceService);
|
const dependenceService = Container.get(DependenceService);
|
||||||
const data = await dependenceService.reInstall(req.body);
|
const data = await dependenceService.reInstall(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
logger.error('🔥 error: %o', e);
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/cancel',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.array().items(Joi.number().required()),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const dependenceService = Container.get(DependenceService);
|
|
||||||
await dependenceService.cancel(req.body);
|
|
||||||
return res.send({ code: 200 });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+46
-121
@@ -1,48 +1,34 @@
|
|||||||
import { Joi, celebrate } from 'celebrate';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import { NextFunction, Request, Response, Router } from 'express';
|
|
||||||
import fs from 'fs';
|
|
||||||
import multer from 'multer';
|
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { Logger } from 'winston';
|
|
||||||
import config from '../config';
|
|
||||||
import { safeJSONParse } from '../config/util';
|
|
||||||
import EnvService from '../services/env';
|
import EnvService from '../services/env';
|
||||||
|
import { Logger } from 'winston';
|
||||||
|
import { celebrate, Joi } from 'celebrate';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
|
||||||
destination: function (req, file, cb) {
|
|
||||||
cb(null, config.scriptPath);
|
|
||||||
},
|
|
||||||
filename: function (req, file, cb) {
|
|
||||||
cb(null, file.originalname);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const upload = multer({ storage: storage });
|
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/envs', route);
|
app.use('/', route);
|
||||||
|
route.get(
|
||||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
'/envs',
|
||||||
const logger: Logger = Container.get('logger');
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
const logger: Logger = Container.get('logger');
|
||||||
const envService = Container.get(EnvService);
|
try {
|
||||||
const data = await envService.envs(req.query.searchValue as string);
|
const envService = Container.get(EnvService);
|
||||||
return res.send({ code: 200, data });
|
const data = await envService.envs(req.query.searchValue as string);
|
||||||
} catch (e) {
|
return res.send({ code: 200, data });
|
||||||
logger.error('🔥 error: %o', e);
|
} catch (e) {
|
||||||
return next(e);
|
logger.error('🔥 error: %o', e);
|
||||||
}
|
return next(e);
|
||||||
});
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
'/',
|
'/envs',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(
|
body: Joi.array().items(
|
||||||
Joi.object({
|
Joi.object({
|
||||||
value: Joi.string().required(),
|
value: Joi.string().required(),
|
||||||
name: Joi.string()
|
name: Joi.string().required(),
|
||||||
.required()
|
|
||||||
.pattern(/^[a-zA-Z_][0-9a-zA-Z_]*$/),
|
|
||||||
remarks: Joi.string().optional().allow(''),
|
remarks: Joi.string().optional().allow(''),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -51,25 +37,23 @@ export default (app: Router) => {
|
|||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const envService = Container.get(EnvService);
|
const envService = Container.get(EnvService);
|
||||||
if (!req.body?.length) {
|
|
||||||
return res.send({ code: 400, message: '参数不正确' });
|
|
||||||
}
|
|
||||||
const data = await envService.create(req.body);
|
const data = await envService.create(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/',
|
'/envs',
|
||||||
celebrate({
|
celebrate({
|
||||||
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().allow('').allow(null),
|
remarks: Joi.string().optional().allow(''),
|
||||||
id: Joi.number().required(),
|
_id: Joi.string().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -79,15 +63,16 @@ export default (app: Router) => {
|
|||||||
const data = await envService.update(req.body);
|
const data = await envService.update(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.delete(
|
route.delete(
|
||||||
'/',
|
'/envs',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.number().required()),
|
body: Joi.array().items(Joi.string().required()),
|
||||||
}),
|
}),
|
||||||
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');
|
||||||
@@ -96,37 +81,40 @@ export default (app: Router) => {
|
|||||||
const data = await envService.remove(req.body);
|
const data = await envService.remove(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/:id/move',
|
'/envs/:id/move',
|
||||||
celebrate({
|
celebrate({
|
||||||
params: Joi.object({
|
params: Joi.object({
|
||||||
id: Joi.number().required(),
|
id: Joi.string().required(),
|
||||||
}),
|
}),
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
fromIndex: Joi.number().required(),
|
fromIndex: Joi.number().required(),
|
||||||
toIndex: Joi.number().required(),
|
toIndex: Joi.number().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const envService = Container.get(EnvService);
|
const envService = Container.get(EnvService);
|
||||||
const data = await envService.move(req.params.id, req.body);
|
const data = await envService.move(req.params.id, req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/disable',
|
'/envs/disable',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.number().required()),
|
body: Joi.array().items(Joi.string().required()),
|
||||||
}),
|
}),
|
||||||
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');
|
||||||
@@ -135,15 +123,16 @@ export default (app: Router) => {
|
|||||||
const data = await envService.disabled(req.body);
|
const data = await envService.disabled(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/enable',
|
'/envs/enable',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.number().required()),
|
body: Joi.array().items(Joi.string().required()),
|
||||||
}),
|
}),
|
||||||
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');
|
||||||
@@ -152,16 +141,17 @@ export default (app: Router) => {
|
|||||||
const data = await envService.enabled(req.body);
|
const data = await envService.enabled(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/name',
|
'/envs/name',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
ids: Joi.array().items(Joi.number().required()),
|
ids: Joi.array().items(Joi.string().required()),
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
@@ -172,92 +162,27 @@ export default (app: Router) => {
|
|||||||
const data = await envService.updateNames(req.body);
|
const data = await envService.updateNames(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/:id',
|
'/envs/:id',
|
||||||
celebrate({
|
celebrate({
|
||||||
params: Joi.object({
|
params: Joi.object({
|
||||||
id: Joi.number().required(),
|
id: Joi.string().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const envService = Container.get(EnvService);
|
|
||||||
const data = await envService.getDb({ id: req.params.id });
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/pin',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.array().items(Joi.number().required()),
|
|
||||||
}),
|
|
||||||
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 envService = Container.get(EnvService);
|
const envService = Container.get(EnvService);
|
||||||
const data = await envService.pin(req.body);
|
const data = await envService.get(req.params.id);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
logger.error('🔥 error: %o', e);
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/unpin',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.array().items(Joi.number().required()),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const envService = Container.get(EnvService);
|
|
||||||
const data = await envService.unPin(req.body);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.post(
|
|
||||||
'/upload',
|
|
||||||
upload.single('env'),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const envService = Container.get(EnvService);
|
|
||||||
const fileContent = await fs.promises.readFile(req!.file!.path, 'utf8');
|
|
||||||
const parseContent = safeJSONParse(fileContent);
|
|
||||||
const data = Array.isArray(parseContent)
|
|
||||||
? parseContent
|
|
||||||
: [parseContent];
|
|
||||||
if (data.every((x) => x.name && x.value)) {
|
|
||||||
const result = await envService.create(
|
|
||||||
data.map((x) => ({
|
|
||||||
name: x.name,
|
|
||||||
value: x.value,
|
|
||||||
remarks: x.remarks,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
return res.send({ code: 200, data: result });
|
|
||||||
} else {
|
|
||||||
return res.send({
|
|
||||||
code: 400,
|
|
||||||
message: '每条数据 name 或者 value 字段不能为空,参考导出文件格式',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import Logger from '../loaders/logger';
|
|
||||||
import { HealthService } from '../services/health';
|
|
||||||
import Container from 'typedi';
|
|
||||||
const route = Router();
|
|
||||||
|
|
||||||
export default (app: Router) => {
|
|
||||||
app.use('/', route);
|
|
||||||
|
|
||||||
route.get('/health', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const healthService = Container.get(HealthService);
|
|
||||||
const health = await healthService.check();
|
|
||||||
res.status(200).send({
|
|
||||||
code: 200,
|
|
||||||
data: health,
|
|
||||||
});
|
|
||||||
} catch (err: any) {
|
|
||||||
Logger.error('Health check failed:', err);
|
|
||||||
res.status(500).send({
|
|
||||||
code: 500,
|
|
||||||
message: 'Health check failed',
|
|
||||||
error: err.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -7,10 +7,6 @@ import cron from './cron';
|
|||||||
import script from './script';
|
import script from './script';
|
||||||
import open from './open';
|
import open from './open';
|
||||||
import dependence from './dependence';
|
import dependence from './dependence';
|
||||||
import system from './system';
|
|
||||||
import subscription from './subscription';
|
|
||||||
import update from './update';
|
|
||||||
import health from './health';
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const app = Router();
|
const app = Router();
|
||||||
@@ -22,10 +18,6 @@ export default () => {
|
|||||||
script(app);
|
script(app);
|
||||||
open(app);
|
open(app);
|
||||||
dependence(app);
|
dependence(app);
|
||||||
system(app);
|
|
||||||
subscription(app);
|
|
||||||
update(app);
|
|
||||||
health(app);
|
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
};
|
};
|
||||||
|
|||||||
+42
-107
@@ -1,140 +1,75 @@
|
|||||||
import { celebrate, Joi } from 'celebrate';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import { NextFunction, Request, Response, Router } from 'express';
|
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
|
import * as fs from 'fs';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import {
|
import { getFileContentByName } from '../config/util';
|
||||||
getFileContentByName,
|
|
||||||
readDirs,
|
|
||||||
removeAnsi,
|
|
||||||
rmPath,
|
|
||||||
} from '../config/util';
|
|
||||||
import LogService from '../services/log';
|
|
||||||
const route = Router();
|
const route = Router();
|
||||||
const blacklist = ['.tmp'];
|
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/logs', route);
|
app.use('/', route);
|
||||||
|
|
||||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const result = await readDirs(config.logPath, config.logPath, blacklist);
|
|
||||||
res.send({
|
|
||||||
code: 200,
|
|
||||||
data: result,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/detail',
|
'/logs',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const logService = Container.get(LogService);
|
const fileList = fs.readdirSync(config.logPath, 'utf-8');
|
||||||
const finalPath = logService.checkFilePath(
|
const dirs = [];
|
||||||
(req.query.path as string) || '',
|
for (let i = 0; i < fileList.length; i++) {
|
||||||
(req.query.file as string) || '',
|
const stat = fs.lstatSync(config.logPath + fileList[i]);
|
||||||
);
|
if (stat.isDirectory()) {
|
||||||
if (!finalPath || blacklist.includes(req.query.path as string)) {
|
const fileListTmp = fs.readdirSync(
|
||||||
return res.send({
|
`${config.logPath}/${fileList[i]}`,
|
||||||
code: 403,
|
'utf-8',
|
||||||
message: '暂无权限',
|
);
|
||||||
});
|
dirs.push({
|
||||||
|
name: fileList[i],
|
||||||
|
isDir: true,
|
||||||
|
files: fileListTmp.reverse(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dirs.push({
|
||||||
|
name: fileList[i],
|
||||||
|
isDir: false,
|
||||||
|
files: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const content = await getFileContentByName(finalPath);
|
res.send({ code: 200, dirs });
|
||||||
res.send({ code: 200, data: removeAnsi(content) });
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/:file',
|
'/logs/:dir/:file',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const logService = Container.get(LogService);
|
const { dir, file } = req.params;
|
||||||
const finalPath = logService.checkFilePath(
|
const content = getFileContentByName(
|
||||||
(req.query.path as string) || '',
|
`${config.logPath}/${dir}/${file}`,
|
||||||
(req.params.file as string) || '',
|
|
||||||
);
|
);
|
||||||
if (!finalPath || blacklist.includes(req.query.path as string)) {
|
|
||||||
return res.send({
|
|
||||||
code: 403,
|
|
||||||
message: '暂无权限',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const content = await getFileContentByName(finalPath);
|
|
||||||
res.send({ code: 200, data: content });
|
res.send({ code: 200, data: content });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.delete(
|
route.get(
|
||||||
'/',
|
'/logs/:file',
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
filename: Joi.string().required(),
|
|
||||||
path: Joi.string().allow(''),
|
|
||||||
type: Joi.string().optional(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
let { filename, path } = req.body as {
|
const { file } = req.params;
|
||||||
filename: string;
|
const content = getFileContentByName(`${config.logPath}/${file}`);
|
||||||
path: string;
|
res.send({ code: 200, data: content });
|
||||||
};
|
|
||||||
const logService = Container.get(LogService);
|
|
||||||
const finalPath = logService.checkFilePath(path, filename);
|
|
||||||
if (!finalPath || blacklist.includes(path)) {
|
|
||||||
return res.send({
|
|
||||||
code: 403,
|
|
||||||
message: '暂无权限',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await rmPath(finalPath);
|
|
||||||
res.send({ code: 200 });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.post(
|
|
||||||
'/download',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
filename: Joi.string().required(),
|
|
||||||
path: Joi.string().allow(''),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
let { filename, path } = req.body as {
|
|
||||||
filename: string;
|
|
||||||
path: string;
|
|
||||||
};
|
|
||||||
const logService = Container.get(LogService);
|
|
||||||
const filePath = logService.checkFilePath(path, filename);
|
|
||||||
if (!filePath) {
|
|
||||||
return res.send({
|
|
||||||
code: 403,
|
|
||||||
message: '暂无权限',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return res.download(filePath, filename, (err) => {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+11
-5
@@ -16,6 +16,7 @@ export default (app: Router) => {
|
|||||||
const data = await openService.list();
|
const data = await openService.list();
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -25,7 +26,7 @@ export default (app: Router) => {
|
|||||||
'/apps',
|
'/apps',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
name: Joi.string().optional().allow('').disallow('system'),
|
name: Joi.string().optional().allow(''),
|
||||||
scopes: Joi.array().items(Joi.string().required()),
|
scopes: Joi.array().items(Joi.string().required()),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
@@ -36,6 +37,7 @@ export default (app: Router) => {
|
|||||||
const data = await openService.create(req.body);
|
const data = await openService.create(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -47,7 +49,7 @@ export default (app: Router) => {
|
|||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
name: Joi.string().optional().allow(''),
|
name: Joi.string().optional().allow(''),
|
||||||
scopes: Joi.array().items(Joi.string()),
|
scopes: Joi.array().items(Joi.string()),
|
||||||
id: Joi.number().required(),
|
_id: Joi.string().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -57,6 +59,7 @@ export default (app: Router) => {
|
|||||||
const data = await openService.update(req.body);
|
const data = await openService.update(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -65,7 +68,7 @@ export default (app: Router) => {
|
|||||||
route.delete(
|
route.delete(
|
||||||
'/apps',
|
'/apps',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.array().items(Joi.number().required()),
|
body: Joi.array().items(Joi.string().required()),
|
||||||
}),
|
}),
|
||||||
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');
|
||||||
@@ -74,6 +77,7 @@ export default (app: Router) => {
|
|||||||
const data = await openService.remove(req.body);
|
const data = await openService.remove(req.body);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -83,16 +87,17 @@ export default (app: Router) => {
|
|||||||
'/apps/:id/reset-secret',
|
'/apps/:id/reset-secret',
|
||||||
celebrate({
|
celebrate({
|
||||||
params: Joi.object({
|
params: Joi.object({
|
||||||
id: Joi.number().required(),
|
id: Joi.string().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request<{ id: number }>, 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 openService = Container.get(OpenService);
|
const openService = Container.get(OpenService);
|
||||||
const data = await openService.resetSecret(req.params.id);
|
const data = await openService.resetSecret(req.params.id);
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -113,6 +118,7 @@ export default (app: Router) => {
|
|||||||
const result = await openService.authToken(req.query as any);
|
const result = await openService.authToken(req.query as any);
|
||||||
return res.send(result);
|
return res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+118
-238
@@ -1,68 +1,73 @@
|
|||||||
import { fileExist, readDirs, readDir, rmPath, IFile } 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/promises';
|
import * as fs from 'fs';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
import path, { join, parse } from 'path';
|
import path, { join } from 'path';
|
||||||
import ScriptService from '../services/script';
|
import ScriptService from '../services/script';
|
||||||
import multer from 'multer';
|
|
||||||
import { writeFileWithLock } from '../shared/utils';
|
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
|
||||||
destination: function (req, file, cb) {
|
|
||||||
cb(null, config.scriptPath);
|
|
||||||
},
|
|
||||||
filename: function (req, file, cb) {
|
|
||||||
cb(null, file.originalname);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const upload = multer({ storage: storage });
|
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/scripts', route);
|
app.use('/', route);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/',
|
'/scripts/files',
|
||||||
celebrate({
|
|
||||||
query: Joi.object({
|
|
||||||
path: Joi.string().optional().allow(''),
|
|
||||||
}).unknown(true),
|
|
||||||
}),
|
|
||||||
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 result: IFile[] = [];
|
const fileList = fs.readdirSync(config.scriptPath, 'utf-8');
|
||||||
const blacklist = [
|
|
||||||
'node_modules',
|
let result = [];
|
||||||
'.git',
|
for (let i = 0; i < fileList.length; i++) {
|
||||||
'.pnpm',
|
const fileOrDir = fileList[i];
|
||||||
'pnpm-lock.yaml',
|
const fPath = path.join(config.scriptPath, fileOrDir);
|
||||||
'yarn.lock',
|
const dirStat = fs.statSync(fPath);
|
||||||
'package-lock.json',
|
if (['node_modules'].includes(fileOrDir)) {
|
||||||
];
|
continue;
|
||||||
if (req.query.path) {
|
}
|
||||||
result = await readDir(
|
|
||||||
req.query.path as string,
|
if (dirStat.isDirectory()) {
|
||||||
config.scriptPath,
|
const childFileList = fs.readdirSync(fPath, 'utf-8');
|
||||||
blacklist,
|
let children = [];
|
||||||
);
|
for (let j = 0; j < childFileList.length; j++) {
|
||||||
} else {
|
const childFile = childFileList[j];
|
||||||
result = await readDirs(
|
const sPath = path.join(config.scriptPath, fileOrDir, childFile);
|
||||||
config.scriptPath,
|
const _fileExist = await fileExist(sPath);
|
||||||
config.scriptPath,
|
if (_fileExist && fs.statSync(sPath).isFile()) {
|
||||||
blacklist,
|
const statObj = fs.statSync(sPath);
|
||||||
(a, b) => {
|
children.push({
|
||||||
if (a.type === b.type) {
|
title: childFile,
|
||||||
return a.title.localeCompare(b.title);
|
value: childFile,
|
||||||
} else {
|
key: `${fileOrDir}/${childFile}`,
|
||||||
return a.type === 'directory' ? -1 : 1;
|
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: result,
|
data: result,
|
||||||
@@ -71,78 +76,47 @@ export default (app: Router) => {
|
|||||||
logger.error('🔥 error: %o', e);
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
route.get(
|
|
||||||
'/detail',
|
|
||||||
celebrate({
|
|
||||||
query: Joi.object({
|
|
||||||
path: Joi.string().optional().allow(''),
|
|
||||||
file: Joi.string().required(),
|
|
||||||
}).unknown(true),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const scriptService = Container.get(ScriptService);
|
|
||||||
const content = await scriptService.getFile(
|
|
||||||
req.query?.path as string || '',
|
|
||||||
req.query.file as string,
|
|
||||||
);
|
|
||||||
res.send({ code: 200, data: content });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/:file',
|
'/scripts/:file',
|
||||||
celebrate({
|
|
||||||
params: Joi.object({
|
|
||||||
file: Joi.string().required(),
|
|
||||||
}),
|
|
||||||
query: Joi.object({
|
|
||||||
path: Joi.string().optional().allow(''),
|
|
||||||
}).unknown(true),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const scriptService = Container.get(ScriptService);
|
const filePath = join(
|
||||||
const content = await scriptService.getFile(
|
config.scriptPath,
|
||||||
req.query?.path as string || '',
|
req.query.path as string,
|
||||||
req.params.file,
|
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);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
'/',
|
'/scripts',
|
||||||
upload.single('file'),
|
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
filename: Joi.string().required(),
|
filename: Joi.string().required(),
|
||||||
path: Joi.string().optional().allow(''),
|
path: Joi.string().optional().allow(''),
|
||||||
content: Joi.string().optional().allow(''),
|
content: Joi.string().allow(''),
|
||||||
originFilename: Joi.string().optional().allow(''),
|
originFilename: Joi.string().allow(''),
|
||||||
directory: Joi.string().optional().allow(''),
|
}),
|
||||||
file: Joi.string().optional().allow(''),
|
|
||||||
}).unknown(true),
|
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
let { filename, path, content, originFilename, directory } =
|
let { filename, path, content, originFilename } = req.body as {
|
||||||
req.body as {
|
filename: string;
|
||||||
filename: string;
|
path: string;
|
||||||
path: string;
|
content: string;
|
||||||
content: string;
|
originFilename: string;
|
||||||
originFilename: string;
|
};
|
||||||
directory: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!path) {
|
if (!path) {
|
||||||
path = config.scriptPath;
|
path = config.scriptPath;
|
||||||
}
|
}
|
||||||
@@ -150,238 +124,144 @@ export default (app: Router) => {
|
|||||||
path += '/';
|
path += '/';
|
||||||
}
|
}
|
||||||
if (!path.startsWith('/')) {
|
if (!path.startsWith('/')) {
|
||||||
path = join(config.scriptPath, path);
|
path = `${config.scriptPath}${path}`;
|
||||||
}
|
}
|
||||||
if (config.writePathList.every((x) => !path.startsWith(x))) {
|
if (config.writePathList.every((x) => !path.startsWith(x))) {
|
||||||
return res.send({
|
return res.send({
|
||||||
code: 403,
|
code: 430,
|
||||||
message: '暂无权限',
|
data: '文件路径禁止访问',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.file) {
|
|
||||||
await fs.rename(req.file.path, join(path, filename));
|
|
||||||
return res.send({ code: 200 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (directory) {
|
|
||||||
await fs.mkdir(join(path, directory), { recursive: true });
|
|
||||||
return res.send({ code: 200 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!originFilename) {
|
if (!originFilename) {
|
||||||
originFilename = filename;
|
originFilename = filename;
|
||||||
}
|
}
|
||||||
const originFilePath = join(
|
const originFilePath = `${path}${originFilename.replace(/\//g, '')}`;
|
||||||
path,
|
const filePath = `${path}${filename.replace(/\//g, '')}`;
|
||||||
`${originFilename.replace(/\//g, '')}`,
|
if (fs.existsSync(originFilePath)) {
|
||||||
);
|
if (!fs.existsSync(config.bakPath)) {
|
||||||
await fs.mkdir(path, { recursive: true });
|
fs.mkdirSync(config.bakPath);
|
||||||
const filePath = join(path, `${filename.replace(/\//g, '')}`);
|
}
|
||||||
const fileExists = await fileExist(filePath);
|
fs.copyFileSync(
|
||||||
if (fileExists) {
|
|
||||||
await fs.copyFile(
|
|
||||||
originFilePath,
|
originFilePath,
|
||||||
join(config.bakPath, originFilename.replace(/\//g, '')),
|
`${config.bakPath}${originFilename.replace(/\//g, '')}`,
|
||||||
);
|
);
|
||||||
if (filename !== originFilename) {
|
if (filename !== originFilename) {
|
||||||
await rmPath(originFilePath);
|
fs.unlinkSync(originFilePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await writeFileWithLock(filePath, content);
|
fs.writeFileSync(filePath, content);
|
||||||
return res.send({ code: 200 });
|
return res.send({ code: 200 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/',
|
'/scripts',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
filename: Joi.string().required(),
|
filename: Joi.string().required(),
|
||||||
path: Joi.string().optional().allow(''),
|
path: Joi.string().optional().allow(''),
|
||||||
content: Joi.string().required().allow(''),
|
content: Joi.string().required(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
let { filename, content, path } = req.body as {
|
let { filename, content, path } = req.body as {
|
||||||
filename: string;
|
filename: string;
|
||||||
content: string;
|
content: string;
|
||||||
path: string;
|
path: string;
|
||||||
};
|
};
|
||||||
const scriptService = Container.get(ScriptService);
|
const filePath = join(config.scriptPath, path, filename);
|
||||||
const filePath = scriptService.checkFilePath(path, filename);
|
fs.writeFileSync(filePath, content);
|
||||||
if (!filePath) {
|
|
||||||
return res.send({
|
|
||||||
code: 403,
|
|
||||||
message: '暂无权限',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await writeFileWithLock(filePath, content);
|
|
||||||
return res.send({ code: 200 });
|
return res.send({ code: 200 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.delete(
|
route.delete(
|
||||||
'/',
|
'/scripts',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
filename: Joi.string().required(),
|
filename: Joi.string().required(),
|
||||||
path: Joi.string().optional().allow(''),
|
path: Joi.string().allow(''),
|
||||||
type: Joi.string().optional(),
|
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
let { filename, path } = req.body as {
|
let { filename, path } = req.body as {
|
||||||
filename: string;
|
filename: string;
|
||||||
path: string;
|
path: string;
|
||||||
};
|
};
|
||||||
if (!path) {
|
const filePath = join(config.scriptPath, path, filename);
|
||||||
path = '';
|
fs.unlinkSync(filePath);
|
||||||
}
|
|
||||||
const scriptService = Container.get(ScriptService);
|
|
||||||
const filePath = scriptService.checkFilePath(path, filename);
|
|
||||||
if (!filePath) {
|
|
||||||
return res.send({
|
|
||||||
code: 403,
|
|
||||||
message: '暂无权限',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await rmPath(filePath);
|
|
||||||
res.send({ code: 200 });
|
res.send({ code: 200 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
'/download',
|
'/scripts/download',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
filename: Joi.string().required(),
|
filename: Joi.string().required(),
|
||||||
path: Joi.string().optional().allow(''),
|
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
let { filename, path } = req.body as {
|
let { filename } = req.body as {
|
||||||
filename: string;
|
filename: string;
|
||||||
path: string;
|
|
||||||
};
|
};
|
||||||
if (!path) {
|
const filePath = `${config.scriptPath}${filename}`;
|
||||||
path = '';
|
// const stats = fs.statSync(filePath);
|
||||||
}
|
// res.set({
|
||||||
const scriptService = Container.get(ScriptService);
|
// 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件
|
||||||
const filePath = scriptService.checkFilePath(path, filename);
|
// 'Content-Disposition': 'attachment; filename=' + filename, //告诉浏览器这是一个需要下载的文件
|
||||||
if (!filePath) {
|
// 'Content-Length': stats.size //文件大小
|
||||||
return res.send({
|
// });
|
||||||
code: 403,
|
// fs.createReadStream(filePath).pipe(res);
|
||||||
message: '暂无权限',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return res.download(filePath, filename, (err) => {
|
return res.download(filePath, filename, (err) => {
|
||||||
if (err) {
|
return next(err);
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/run',
|
'/scripts/run',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
filename: Joi.string().required(),
|
filename: Joi.string().required(),
|
||||||
content: Joi.string().optional().allow(''),
|
|
||||||
path: Joi.string().optional().allow(''),
|
path: 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');
|
||||||
try {
|
try {
|
||||||
let { filename, content, path } = req.body;
|
let { filename, path } = req.body as {
|
||||||
if (!path) {
|
filename: string;
|
||||||
path = '';
|
path: string;
|
||||||
}
|
};
|
||||||
const { name, ext } = parse(filename);
|
const filePath = join(path, filename);
|
||||||
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
|
||||||
await writeFileWithLock(filePath, content || '');
|
|
||||||
|
|
||||||
const scriptService = Container.get(ScriptService);
|
const scriptService = Container.get(ScriptService);
|
||||||
const result = await scriptService.runScript(filePath);
|
const result = await scriptService.runScript(filePath);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
logger.error('🔥 error: %o', e);
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/stop',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
filename: Joi.string().required(),
|
|
||||||
path: Joi.string().optional().allow(''),
|
|
||||||
pid: Joi.number().optional().allow(''),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
let { filename, path, pid } = req.body;
|
|
||||||
if (!path) {
|
|
||||||
path = '';
|
|
||||||
}
|
|
||||||
const { name, ext } = parse(filename);
|
|
||||||
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
|
||||||
const logPath = join(config.logPath, path, `${name}.swap`);
|
|
||||||
|
|
||||||
const scriptService = Container.get(ScriptService);
|
|
||||||
const result = await scriptService.stopScript(filePath, pid);
|
|
||||||
setTimeout(() => {
|
|
||||||
rmPath(logPath);
|
|
||||||
}, 3000);
|
|
||||||
res.send(result);
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/rename',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
filename: Joi.string().required(),
|
|
||||||
path: Joi.string().allow(''),
|
|
||||||
newFilename: Joi.string().required(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
let { filename, path, newFilename } = req.body as {
|
|
||||||
filename: string;
|
|
||||||
path: string;
|
|
||||||
newFilename: string;
|
|
||||||
};
|
|
||||||
if (!path) {
|
|
||||||
path = '';
|
|
||||||
}
|
|
||||||
const filePath = join(config.scriptPath, path, filename);
|
|
||||||
const newPath = join(config.scriptPath, path, newFilename);
|
|
||||||
await fs.rename(filePath, newPath);
|
|
||||||
res.send({ code: 200 });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,294 +0,0 @@
|
|||||||
import { Router, Request, Response, NextFunction } from 'express';
|
|
||||||
import { Container } from 'typedi';
|
|
||||||
import { Logger } from 'winston';
|
|
||||||
import SubscriptionService from '../services/subscription';
|
|
||||||
import { celebrate, Joi } from 'celebrate';
|
|
||||||
import CronExpressionParser from 'cron-parser';
|
|
||||||
const route = Router();
|
|
||||||
|
|
||||||
export default (app: Router) => {
|
|
||||||
app.use('/subscriptions', route);
|
|
||||||
|
|
||||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const subscriptionService = Container.get(SubscriptionService);
|
|
||||||
const data = await subscriptionService.list(
|
|
||||||
req.query.searchValue as string,
|
|
||||||
req.query.ids as string,
|
|
||||||
);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
route.post(
|
|
||||||
'/',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
type: Joi.string().required(),
|
|
||||||
schedule: Joi.string().optional().allow('').allow(null),
|
|
||||||
interval_schedule: Joi.object({
|
|
||||||
type: Joi.string().required(),
|
|
||||||
value: Joi.number().min(1).required(),
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.allow('')
|
|
||||||
.allow(null),
|
|
||||||
name: Joi.string().optional().allow('').allow(null),
|
|
||||||
url: Joi.string().required(),
|
|
||||||
whitelist: Joi.string().optional().allow('').allow(null),
|
|
||||||
blacklist: Joi.string().optional().allow('').allow(null),
|
|
||||||
branch: Joi.string().optional().allow('').allow(null),
|
|
||||||
dependences: Joi.string().optional().allow('').allow(null),
|
|
||||||
pull_type: Joi.string().optional().allow('').allow(null),
|
|
||||||
pull_option: Joi.object().optional().allow('').allow(null),
|
|
||||||
extensions: Joi.string().optional().allow('').allow(null),
|
|
||||||
sub_before: Joi.string().optional().allow('').allow(null),
|
|
||||||
sub_after: Joi.string().optional().allow('').allow(null),
|
|
||||||
schedule_type: Joi.string().required(),
|
|
||||||
alias: Joi.string().required(),
|
|
||||||
proxy: Joi.string().optional().allow('').allow(null),
|
|
||||||
autoAddCron: Joi.boolean().optional().allow('').allow(null),
|
|
||||||
autoDelCron: Joi.boolean().optional().allow('').allow(null),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
!req.body.schedule ||
|
|
||||||
CronExpressionParser.parse(req.body.schedule).hasNext()
|
|
||||||
) {
|
|
||||||
const subscriptionService = Container.get(SubscriptionService);
|
|
||||||
const data = await subscriptionService.create(req.body);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} else {
|
|
||||||
return res.send({ code: 400, message: 'param schedule error' });
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/run',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.array().items(Joi.number().required()),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const subscriptionService = Container.get(SubscriptionService);
|
|
||||||
const data = await subscriptionService.run(req.body);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/stop',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.array().items(Joi.number().required()),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const subscriptionService = Container.get(SubscriptionService);
|
|
||||||
const data = await subscriptionService.stop(req.body);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/disable',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.array().items(Joi.number().required()),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const subscriptionService = Container.get(SubscriptionService);
|
|
||||||
const data = await subscriptionService.disabled(req.body);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/enable',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.array().items(Joi.number().required()),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const subscriptionService = Container.get(SubscriptionService);
|
|
||||||
const data = await subscriptionService.enabled(req.body);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.get(
|
|
||||||
'/:id/log',
|
|
||||||
celebrate({
|
|
||||||
params: Joi.object({
|
|
||||||
id: Joi.number().required(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const subscriptionService = Container.get(SubscriptionService);
|
|
||||||
const data = await subscriptionService.log(req.params.id);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
type: Joi.string().required(),
|
|
||||||
schedule: Joi.string().optional().allow('').allow(null),
|
|
||||||
interval_schedule: Joi.object().optional().allow('').allow(null),
|
|
||||||
name: Joi.string().optional().allow('').allow(null),
|
|
||||||
url: Joi.string().required(),
|
|
||||||
whitelist: Joi.string().optional().allow('').allow(null),
|
|
||||||
blacklist: Joi.string().optional().allow('').allow(null),
|
|
||||||
branch: Joi.string().optional().allow('').allow(null),
|
|
||||||
dependences: Joi.string().optional().allow('').allow(null),
|
|
||||||
pull_type: Joi.string().optional().allow('').allow(null),
|
|
||||||
pull_option: Joi.object().optional().allow('').allow(null),
|
|
||||||
schedule_type: Joi.string().optional().allow('').allow(null),
|
|
||||||
extensions: Joi.string().optional().allow('').allow(null),
|
|
||||||
sub_before: Joi.string().optional().allow('').allow(null),
|
|
||||||
sub_after: Joi.string().optional().allow('').allow(null),
|
|
||||||
alias: Joi.string().required(),
|
|
||||||
proxy: Joi.string().optional().allow('').allow(null),
|
|
||||||
autoAddCron: Joi.boolean().optional().allow('').allow(null),
|
|
||||||
autoDelCron: Joi.boolean().optional().allow('').allow(null),
|
|
||||||
id: Joi.number().required(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
!req.body.schedule ||
|
|
||||||
typeof req.body.schedule === 'object' ||
|
|
||||||
CronExpressionParser.parse(req.body.schedule).hasNext()
|
|
||||||
) {
|
|
||||||
const subscriptionService = Container.get(SubscriptionService);
|
|
||||||
const data = await subscriptionService.update(req.body);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} else {
|
|
||||||
return res.send({ code: 400, message: 'param schedule error' });
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.delete(
|
|
||||||
'/',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.array().items(Joi.number().required()),
|
|
||||||
query: Joi.object({
|
|
||||||
force: Joi.boolean().optional(),
|
|
||||||
t: Joi.number(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const subscriptionService = Container.get(SubscriptionService);
|
|
||||||
const data = await subscriptionService.remove(req.body, req.query);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.get(
|
|
||||||
'/:id',
|
|
||||||
celebrate({
|
|
||||||
params: Joi.object({
|
|
||||||
id: Joi.number().required(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const subscriptionService = Container.get(SubscriptionService);
|
|
||||||
const data = await subscriptionService.getDb({ id: req.params.id });
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/status',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
ids: Joi.array().items(Joi.number().required()),
|
|
||||||
status: Joi.string().required(),
|
|
||||||
pid: Joi.string().optional(),
|
|
||||||
log_path: Joi.string().optional(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const subscriptionService = Container.get(SubscriptionService);
|
|
||||||
const data = await subscriptionService.status({
|
|
||||||
...req.body,
|
|
||||||
status: parseInt(req.body.status),
|
|
||||||
pid: parseInt(req.body.pid) || '',
|
|
||||||
});
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.get(
|
|
||||||
'/:id/logs',
|
|
||||||
celebrate({
|
|
||||||
params: Joi.object({
|
|
||||||
id: Joi.number().required(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const subscriptionService = Container.get(SubscriptionService);
|
|
||||||
const data = await subscriptionService.logs(req.params.id);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,464 +0,0 @@
|
|||||||
import { Router, Request, Response, NextFunction } from 'express';
|
|
||||||
import { Container } from 'typedi';
|
|
||||||
import { Logger } from 'winston';
|
|
||||||
import * as fs from 'fs/promises';
|
|
||||||
import config from '../config';
|
|
||||||
import SystemService from '../services/system';
|
|
||||||
import { celebrate, Joi } from 'celebrate';
|
|
||||||
import UserService from '../services/user';
|
|
||||||
import {
|
|
||||||
getUniqPath,
|
|
||||||
handleLogPath,
|
|
||||||
parseVersion,
|
|
||||||
promiseExec,
|
|
||||||
} from '../config/util';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import multer from 'multer';
|
|
||||||
import { logStreamManager } from '../shared/logStreamManager';
|
|
||||||
|
|
||||||
const route = Router();
|
|
||||||
const storage = multer.diskStorage({
|
|
||||||
destination: function (req, file, cb) {
|
|
||||||
cb(null, config.tmpPath);
|
|
||||||
},
|
|
||||||
filename: function (req, file, cb) {
|
|
||||||
cb(null, 'data.tgz');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const upload = multer({ storage: storage });
|
|
||||||
|
|
||||||
export default (app: Router) => {
|
|
||||||
app.use('/system', route);
|
|
||||||
|
|
||||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const userService = Container.get(UserService);
|
|
||||||
const authInfo = await userService.getAuthInfo();
|
|
||||||
const { version, changeLog, changeLogLink, publishTime } =
|
|
||||||
await parseVersion(config.versionFile);
|
|
||||||
|
|
||||||
let isInitialized = true;
|
|
||||||
if (
|
|
||||||
Object.keys(authInfo).length === 2 &&
|
|
||||||
authInfo.username === 'admin' &&
|
|
||||||
authInfo.password === 'admin'
|
|
||||||
) {
|
|
||||||
isInitialized = false;
|
|
||||||
}
|
|
||||||
res.send({
|
|
||||||
code: 200,
|
|
||||||
data: {
|
|
||||||
isInitialized,
|
|
||||||
version,
|
|
||||||
publishTime: dayjs(publishTime).unix(),
|
|
||||||
branch: process.env.QL_BRANCH || 'master',
|
|
||||||
changeLog,
|
|
||||||
changeLogLink,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
route.get(
|
|
||||||
'/config',
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
const data = await systemService.getSystemConfig();
|
|
||||||
res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/config/log-remove-frequency',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
logRemoveFrequency: Joi.number().allow(null),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
const result = await systemService.updateLogRemoveFrequency(req.body);
|
|
||||||
res.send(result);
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/config/cron-concurrency',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
cronConcurrency: Joi.number().allow(null),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
const result = await systemService.updateCronConcurrency(req.body);
|
|
||||||
res.send(result);
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/config/dependence-proxy',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
dependenceProxy: Joi.string().allow('').allow(null),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
const result = await systemService.updateDependenceProxy(req.body);
|
|
||||||
res.send(result);
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/config/node-mirror',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
nodeMirror: Joi.string().allow('').allow(null),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
res.setHeader('Content-type', 'application/octet-stream');
|
|
||||||
await systemService.updateNodeMirror(req.body, res);
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/config/python-mirror',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
pythonMirror: Joi.string().allow('').allow(null),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
const result = await systemService.updatePythonMirror(req.body);
|
|
||||||
res.send(result);
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/config/linux-mirror',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
linuxMirror: Joi.string().allow('').allow(null),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
res.setHeader('Content-type', 'application/octet-stream');
|
|
||||||
await systemService.updateLinuxMirror(req.body, res);
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/update-check',
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
const result = await systemService.checkUpdate();
|
|
||||||
res.send(result);
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/update',
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
const result = await systemService.updateSystem();
|
|
||||||
res.send(result);
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/reload',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
type: Joi.string().optional().allow('').allow(null),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
const result = await systemService.reloadSystem(req.body.type);
|
|
||||||
res.send(result);
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/notify',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
title: Joi.string().required(),
|
|
||||||
content: Joi.string().required(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
const result = await systemService.notify(req.body);
|
|
||||||
res.send(result);
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/command-run',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
command: Joi.string().required(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
const command = req.body.command;
|
|
||||||
const idStr = `cat ${config.crontabFile} | grep -E "${command}" | perl -pe "s|.*ID=(.*) ${command}.*|\\1|" | head -1 | awk -F " " '{print $1}' | xargs echo -n`;
|
|
||||||
let id = await promiseExec(idStr);
|
|
||||||
const uniqPath = await getUniqPath(command, id);
|
|
||||||
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
|
||||||
const logPath = `${uniqPath}/${logTime}.log`;
|
|
||||||
res.setHeader('Content-type', 'application/octet-stream');
|
|
||||||
await systemService.run(
|
|
||||||
{ ...req.body, logPath },
|
|
||||||
{
|
|
||||||
onStart: async (cp, startTime) => {
|
|
||||||
res.setHeader('QL-Task-Pid', `${cp.pid}`);
|
|
||||||
res.setHeader('QL-Task-Log', `${logPath}`);
|
|
||||||
},
|
|
||||||
onEnd: async (cp, endTime, diff) => {
|
|
||||||
// Close the stream after task completion
|
|
||||||
await logStreamManager.closeStream(await handleLogPath(logPath));
|
|
||||||
res.end();
|
|
||||||
},
|
|
||||||
onError: async (message: string) => {
|
|
||||||
res.write(message);
|
|
||||||
const absolutePath = await handleLogPath(logPath);
|
|
||||||
await logStreamManager.write(absolutePath, message);
|
|
||||||
},
|
|
||||||
onLog: async (message: string) => {
|
|
||||||
res.write(message);
|
|
||||||
const absolutePath = await handleLogPath(logPath);
|
|
||||||
await logStreamManager.write(absolutePath, message);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/command-stop',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
command: Joi.string().optional(),
|
|
||||||
pid: Joi.number().optional(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
const result = await systemService.stop(req.body);
|
|
||||||
res.send(result);
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/data/export',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
type: Joi.array().items(Joi.string()).optional(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
await systemService.exportData(res, req.body.type);
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/data/import',
|
|
||||||
upload.single('data'),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
const result = await systemService.importData();
|
|
||||||
res.send(result);
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.get(
|
|
||||||
'/log',
|
|
||||||
celebrate({
|
|
||||||
query: {
|
|
||||||
startTime: Joi.string().allow('').optional(),
|
|
||||||
endTime: Joi.string().allow('').optional(),
|
|
||||||
t: Joi.string().optional(),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
await systemService.getSystemLog(
|
|
||||||
res,
|
|
||||||
req.query as {
|
|
||||||
startTime?: string;
|
|
||||||
endTime?: string;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.delete(
|
|
||||||
'/log',
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
await systemService.deleteSystemLog();
|
|
||||||
res.send({ code: 200 });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/auth/reset',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
retries: Joi.number().optional(),
|
|
||||||
twoFactorActivated: Joi.boolean().optional(),
|
|
||||||
password: Joi.string().optional(),
|
|
||||||
username: Joi.string().optional(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const userService = Container.get(UserService);
|
|
||||||
await userService.resetAuthInfo(req.body);
|
|
||||||
res.send({ code: 200, message: '更新成功' });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/config/timezone',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
timezone: Joi.string().allow('').allow(null),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
const result = await systemService.updateTimezone(req.body);
|
|
||||||
res.send(result);
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/config/global-ssh-key',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
globalSshKey: Joi.string().allow('').allow(null),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
const result = await systemService.updateGlobalSshKey(req.body);
|
|
||||||
res.send(result);
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/config/dependence-clean',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
type: Joi.string().allow(''),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
const result = await systemService.cleanDependence(req.body.type);
|
|
||||||
res.send(result);
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { NextFunction, Request, Response, Router } from 'express';
|
|
||||||
import Container from 'typedi';
|
|
||||||
import Logger from '../loaders/logger';
|
|
||||||
import SystemService from '../services/system';
|
|
||||||
const route = Router();
|
|
||||||
|
|
||||||
export default (app: Router) => {
|
|
||||||
app.use('/update', route);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/reload',
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
const result = await systemService.reloadSystem();
|
|
||||||
res.send(result);
|
|
||||||
} catch (e) {
|
|
||||||
Logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/system',
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
const result = await systemService.reloadSystem('system');
|
|
||||||
res.send(result);
|
|
||||||
} catch (e) {
|
|
||||||
Logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/data',
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
const result = await systemService.reloadSystem('data');
|
|
||||||
res.send(result);
|
|
||||||
} catch (e) {
|
|
||||||
Logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+135
-60
@@ -1,37 +1,17 @@
|
|||||||
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 * as fs from 'fs';
|
||||||
|
import config from '../config';
|
||||||
import UserService from '../services/user';
|
import UserService from '../services/user';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
import multer from 'multer';
|
import { getFileContentByName } from '../config/util';
|
||||||
import path from 'path';
|
|
||||||
import { v4 as uuidV4 } from 'uuid';
|
|
||||||
import rateLimit from 'express-rate-limit';
|
|
||||||
import config from '../config';
|
|
||||||
import { isDemoEnv, getToken } from '../config/util';
|
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
|
||||||
destination: function (req, file, cb) {
|
|
||||||
cb(null, config.uploadPath);
|
|
||||||
},
|
|
||||||
filename: function (req, file, cb) {
|
|
||||||
const ext = path.parse(file.originalname).ext;
|
|
||||||
const key = uuidV4();
|
|
||||||
cb(null, key + ext);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const upload = multer({ storage: storage });
|
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/user', route);
|
app.use('/', route);
|
||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
'/login',
|
'/login',
|
||||||
rateLimit({
|
|
||||||
windowMs: 15 * 60 * 1000,
|
|
||||||
max: 100,
|
|
||||||
}),
|
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
username: Joi.string().required(),
|
username: Joi.string().required(),
|
||||||
@@ -45,6 +25,7 @@ export default (app: Router) => {
|
|||||||
const data = await userService.login({ ...req.body }, req);
|
const data = await userService.login({ ...req.body }, req);
|
||||||
return res.send(data);
|
return res.send(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -56,17 +37,17 @@ export default (app: Router) => {
|
|||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
const userService = Container.get(UserService);
|
const userService = Container.get(UserService);
|
||||||
const token = getToken(req);
|
await userService.logout(req.platform);
|
||||||
await userService.logout(req.platform, token);
|
|
||||||
res.send({ code: 200 });
|
res.send({ code: 200 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/',
|
'/user',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
username: Joi.string().required(),
|
username: Joi.string().required(),
|
||||||
@@ -74,40 +55,41 @@ export default (app: Router) => {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
if (isDemoEnv()) {
|
|
||||||
return res.send({ code: 450, message: '未知错误' });
|
|
||||||
}
|
|
||||||
const userService = Container.get(UserService);
|
const userService = Container.get(UserService);
|
||||||
await userService.updateUsernameAndPassword(req.body);
|
await userService.updateUsernameAndPassword(req.body);
|
||||||
res.send({ code: 200, message: '更新成功' });
|
res.send({ code: 200, message: '更新成功' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
route.get(
|
||||||
const logger: Logger = Container.get('logger');
|
'/user',
|
||||||
try {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const userService = Container.get(UserService);
|
const logger: Logger = Container.get('logger');
|
||||||
const authInfo = await userService.getAuthInfo();
|
try {
|
||||||
res.send({
|
const userService = Container.get(UserService);
|
||||||
code: 200,
|
const authInfo = await userService.getUserInfo();
|
||||||
data: {
|
res.send({
|
||||||
username: authInfo.username,
|
code: 200,
|
||||||
avatar: authInfo.avatar,
|
data: {
|
||||||
twoFactorActivated: authInfo.twoFactorActivated,
|
username: authInfo.username,
|
||||||
},
|
twoFactorActivated: authInfo.twoFactorActivated,
|
||||||
});
|
},
|
||||||
} catch (e) {
|
});
|
||||||
logger.error('🔥 error: %o', e);
|
} catch (e) {
|
||||||
return next(e);
|
logger.error('🔥 error: %o', e);
|
||||||
}
|
return next(e);
|
||||||
});
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/two-factor/init',
|
'/user/two-factor/init',
|
||||||
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 {
|
||||||
@@ -115,13 +97,14 @@ export default (app: Router) => {
|
|||||||
const data = await userService.initTwoFactor();
|
const data = await userService.initTwoFactor();
|
||||||
res.send({ code: 200, data });
|
res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/two-factor/active',
|
'/user/two-factor/active',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
code: Joi.string().required(),
|
code: Joi.string().required(),
|
||||||
@@ -134,13 +117,14 @@ export default (app: Router) => {
|
|||||||
const data = await userService.activeTwoFactor(req.body.code);
|
const data = await userService.activeTwoFactor(req.body.code);
|
||||||
res.send({ code: 200, data });
|
res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/two-factor/deactive',
|
'/user/two-factor/deactive',
|
||||||
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 {
|
||||||
@@ -148,13 +132,14 @@ export default (app: Router) => {
|
|||||||
const data = await userService.deactiveTwoFactor();
|
const data = await userService.deactiveTwoFactor();
|
||||||
res.send({ code: 200, data });
|
res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/two-factor/login',
|
'/user/two-factor/login',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
code: Joi.string().required(),
|
code: Joi.string().required(),
|
||||||
@@ -169,13 +154,14 @@ export default (app: Router) => {
|
|||||||
const data = await userService.twoFactorLogin(req.body, req);
|
const data = await userService.twoFactorLogin(req.body, req);
|
||||||
res.send(data);
|
res.send(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/login-log',
|
'/user/login-log',
|
||||||
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 {
|
||||||
@@ -183,13 +169,14 @@ export default (app: Router) => {
|
|||||||
const data = await userService.getLoginLog();
|
const data = await userService.getLoginLog();
|
||||||
res.send({ code: 200, data });
|
res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/notification',
|
'/user/notification',
|
||||||
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 {
|
||||||
@@ -197,13 +184,14 @@ export default (app: Router) => {
|
|||||||
const data = await userService.getNotificationMode();
|
const data = await userService.getNotificationMode();
|
||||||
res.send({ code: 200, data });
|
res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/notification',
|
'/user/notification',
|
||||||
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 {
|
||||||
@@ -211,13 +199,46 @@ export default (app: Router) => {
|
|||||||
const result = await userService.updateNotificationMode(req.body);
|
const result = await userService.updateNotificationMode(req.body);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(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(
|
route.put(
|
||||||
'/init',
|
'/init/user',
|
||||||
celebrate({
|
celebrate({
|
||||||
body: Joi.object({
|
body: Joi.object({
|
||||||
username: Joi.string().required(),
|
username: Joi.string().required(),
|
||||||
@@ -231,13 +252,14 @@ export default (app: Router) => {
|
|||||||
await userService.updateUsernameAndPassword(req.body);
|
await userService.updateUsernameAndPassword(req.body);
|
||||||
res.send({ code: 200, message: '更新成功' });
|
res.send({ code: 200, message: '更新成功' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/notification/init',
|
'/init/notification',
|
||||||
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 {
|
||||||
@@ -245,21 +267,74 @@ export default (app: Router) => {
|
|||||||
const result = await userService.updateNotificationMode(req.body);
|
const result = await userService.updateNotificationMode(req.body);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} 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);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/avatar',
|
'/system/log/remove',
|
||||||
upload.single('avatar'),
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
frequency: Joi.number().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
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 userService = Container.get(UserService);
|
const userService = Container.get(UserService);
|
||||||
const result = await userService.updateAvatar(req.file!.filename);
|
const result = await userService.updateLogRemoveFrequency(
|
||||||
|
req.body.frequency,
|
||||||
|
);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} 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);
|
return next(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+31
-301
@@ -1,306 +1,36 @@
|
|||||||
import 'reflect-metadata';
|
import 'reflect-metadata'; // We need this in order to use @Decorators
|
||||||
import cluster, { type Worker } from 'cluster';
|
|
||||||
import compression from 'compression';
|
|
||||||
import cors from 'cors';
|
|
||||||
import express from 'express';
|
|
||||||
import helmet from 'helmet';
|
|
||||||
import { Container } from 'typedi';
|
|
||||||
import config from './config';
|
import config from './config';
|
||||||
|
|
||||||
|
import express from 'express';
|
||||||
|
|
||||||
import Logger from './loaders/logger';
|
import Logger from './loaders/logger';
|
||||||
import { monitoringMiddleware } from './middlewares/monitoring';
|
|
||||||
import { type GrpcServerService } from './services/grpc';
|
|
||||||
import { type HttpServerService } from './services/http';
|
|
||||||
|
|
||||||
interface WorkerMetadata {
|
async function startServer() {
|
||||||
id: number;
|
const app = express();
|
||||||
pid: number;
|
|
||||||
serviceType: string;
|
await require('./loaders/initFile').default();
|
||||||
startTime: Date;
|
|
||||||
|
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, () => {
|
||||||
|
Logger.info(`
|
||||||
|
################################################
|
||||||
|
🛡️ Server listening on port: ${config.port} 🛡️
|
||||||
|
################################################
|
||||||
|
`);
|
||||||
|
})
|
||||||
|
.on('error', (err) => {
|
||||||
|
Logger.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
await require('./loaders/server').default({ server });
|
||||||
}
|
}
|
||||||
|
|
||||||
class Application {
|
startServer();
|
||||||
private app: express.Application;
|
|
||||||
private httpServerService?: HttpServerService;
|
|
||||||
private grpcServerService?: GrpcServerService;
|
|
||||||
private isShuttingDown = false;
|
|
||||||
private workerMetadataMap = new Map<number, WorkerMetadata>();
|
|
||||||
private httpWorker?: Worker;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.app = express();
|
|
||||||
// 创建一个全局中间件,删除查询参数中的t
|
|
||||||
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
||||||
if (req.query.t) {
|
|
||||||
delete req.query.t;
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async start() {
|
|
||||||
try {
|
|
||||||
if (cluster.isPrimary) {
|
|
||||||
await this.initializeDatabase();
|
|
||||||
}
|
|
||||||
if (cluster.isPrimary) {
|
|
||||||
this.startMasterProcess();
|
|
||||||
} else {
|
|
||||||
await this.startWorkerProcess();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error('Failed to start application:', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private startMasterProcess() {
|
|
||||||
// Fork gRPC worker first and wait for it to be ready
|
|
||||||
const grpcWorker = this.forkWorker('grpc');
|
|
||||||
|
|
||||||
// Wait for gRPC worker to signal it's ready before starting HTTP worker
|
|
||||||
this.waitForWorkerReady(grpcWorker, 30000)
|
|
||||||
.then(() => {
|
|
||||||
Logger.info('✌️ gRPC worker is ready, starting HTTP worker');
|
|
||||||
this.httpWorker = this.forkWorker('http');
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
Logger.error('✌️ Failed to wait for gRPC worker:', error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
cluster.on('exit', (worker, code, signal) => {
|
|
||||||
const metadata = this.workerMetadataMap.get(worker.id);
|
|
||||||
if (metadata) {
|
|
||||||
if (!this.isShuttingDown) {
|
|
||||||
Logger.error(
|
|
||||||
`✌️ ${metadata.serviceType} worker ${worker.process.pid} died (${signal || code
|
|
||||||
}). Restarting...`,
|
|
||||||
);
|
|
||||||
// If gRPC worker died, restart it and wait for it to be ready
|
|
||||||
if (metadata.serviceType === 'grpc') {
|
|
||||||
const newGrpcWorker = this.forkWorker('grpc');
|
|
||||||
this.waitForWorkerReady(newGrpcWorker, 30000)
|
|
||||||
.then(() => {
|
|
||||||
Logger.info('✌️ gRPC worker restarted and ready');
|
|
||||||
// Re-register cron jobs by notifying the HTTP worker
|
|
||||||
if (this.httpWorker) {
|
|
||||||
try {
|
|
||||||
this.httpWorker.send('reregister-crons');
|
|
||||||
Logger.info('✌️ Sent reregister-crons message to HTTP worker');
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error('✌️ Failed to send reregister-crons message:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
Logger.error('✌️ Failed to restart gRPC worker:', error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// For HTTP worker, just restart it
|
|
||||||
const newWorker = this.forkWorker(metadata.serviceType);
|
|
||||||
this.httpWorker = newWorker;
|
|
||||||
Logger.info(`✌️ Restarted ${metadata.serviceType} worker (PID: ${newWorker.process.pid})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.workerMetadataMap.delete(worker.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setupMasterShutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
private waitForWorkerReady(worker: Worker, timeoutMs: number): Promise<void> {
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
const messageHandler = (msg: any) => {
|
|
||||||
if (msg === 'ready') {
|
|
||||||
worker.removeListener('message', messageHandler);
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
worker.on('message', messageHandler);
|
|
||||||
|
|
||||||
// Timeout after specified milliseconds
|
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
worker.removeListener('message', messageHandler);
|
|
||||||
reject(new Error(`Worker failed to start within ${timeoutMs / 1000} seconds`));
|
|
||||||
}, timeoutMs);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private forkWorker(serviceType: string): Worker {
|
|
||||||
const worker = cluster.fork({ SERVICE_TYPE: serviceType });
|
|
||||||
|
|
||||||
this.workerMetadataMap.set(worker.id, {
|
|
||||||
id: worker.id,
|
|
||||||
pid: worker.process.pid!,
|
|
||||||
serviceType,
|
|
||||||
startTime: new Date(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return worker;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async initializeDatabase() {
|
|
||||||
const dbLoader = await import('./loaders/db');
|
|
||||||
await dbLoader.default();
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupMiddlewares() {
|
|
||||||
this.app.use(helmet({
|
|
||||||
contentSecurityPolicy: false,
|
|
||||||
}));
|
|
||||||
this.app.use(cors(config.cors));
|
|
||||||
this.app.use(compression());
|
|
||||||
this.app.use(monitoringMiddleware);
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupMasterShutdown() {
|
|
||||||
const shutdown = async () => {
|
|
||||||
if (this.isShuttingDown) return;
|
|
||||||
this.isShuttingDown = true;
|
|
||||||
|
|
||||||
const workers = Object.values(cluster.workers || {});
|
|
||||||
const workerPromises: Promise<void>[] = [];
|
|
||||||
|
|
||||||
workers.forEach((worker) => {
|
|
||||||
if (worker) {
|
|
||||||
const exitPromise = new Promise<void>((resolve) => {
|
|
||||||
worker.once('exit', () => {
|
|
||||||
Logger.info(`✌️ Worker ${worker.process.pid} exited`);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
worker.send('shutdown');
|
|
||||||
} catch (error) {
|
|
||||||
Logger.warn(
|
|
||||||
`✌️ Failed to send shutdown to worker ${worker.process.pid}:`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
workerPromises.push(exitPromise);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.race([
|
|
||||||
Promise.all(workerPromises),
|
|
||||||
new Promise<void>((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
Logger.warn('✌️ Worker shutdown timeout reached');
|
|
||||||
resolve();
|
|
||||||
}, 10000);
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
process.exit(0);
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error('✌️ Error during worker shutdown:', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
process.on('SIGTERM', shutdown);
|
|
||||||
process.on('SIGINT', shutdown);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async startWorkerProcess() {
|
|
||||||
const serviceType = process.env.SERVICE_TYPE;
|
|
||||||
if (!serviceType || !['http', 'grpc'].includes(serviceType)) {
|
|
||||||
Logger.error('✌️ Invalid SERVICE_TYPE:', serviceType);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.info(`✌️ ${serviceType} worker started (PID: ${process.pid})`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (serviceType === 'http') {
|
|
||||||
await this.startHttpService();
|
|
||||||
} else {
|
|
||||||
await this.startGrpcService();
|
|
||||||
}
|
|
||||||
|
|
||||||
process.send?.('ready');
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(`✌️ ${serviceType} worker failed:`, error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async startHttpService() {
|
|
||||||
this.setupMiddlewares();
|
|
||||||
|
|
||||||
const { HttpServerService } = await import('./services/http');
|
|
||||||
this.httpServerService = Container.get(HttpServerService);
|
|
||||||
|
|
||||||
const appLoader = await import('./loaders/app');
|
|
||||||
await appLoader.default({ app: this.app });
|
|
||||||
|
|
||||||
const server = await this.httpServerService.initialize(
|
|
||||||
this.app,
|
|
||||||
config.port,
|
|
||||||
);
|
|
||||||
|
|
||||||
const serverLoader = await import('./loaders/server');
|
|
||||||
await (serverLoader.default as any)({ server });
|
|
||||||
this.setupWorkerShutdown('http');
|
|
||||||
}
|
|
||||||
|
|
||||||
private async startGrpcService() {
|
|
||||||
const { GrpcServerService } = await import('./services/grpc');
|
|
||||||
this.grpcServerService = Container.get(GrpcServerService);
|
|
||||||
|
|
||||||
await this.grpcServerService.initialize();
|
|
||||||
this.setupWorkerShutdown('grpc');
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupWorkerShutdown(serviceType: string) {
|
|
||||||
process.on('message', async (msg) => {
|
|
||||||
if (msg === 'shutdown') {
|
|
||||||
this.gracefulShutdown(serviceType);
|
|
||||||
} else if (msg === 'reregister-crons' && serviceType === 'http') {
|
|
||||||
// Re-register cron jobs when gRPC worker restarts
|
|
||||||
try {
|
|
||||||
Logger.info('✌️ Received reregister-crons message, re-registering cron jobs...');
|
|
||||||
const CronService = (await import('./services/cron')).default;
|
|
||||||
const cronService = Container.get(CronService);
|
|
||||||
await cronService.autosave_crontab();
|
|
||||||
Logger.info('✌️ Cron jobs re-registered successfully');
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error('✌️ Failed to re-register cron jobs:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const shutdown = () => this.gracefulShutdown(serviceType);
|
|
||||||
process.on('SIGTERM', shutdown);
|
|
||||||
process.on('SIGINT', shutdown);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async gracefulShutdown(serviceType: string) {
|
|
||||||
if (this.isShuttingDown) return;
|
|
||||||
this.isShuttingDown = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (serviceType === 'http') {
|
|
||||||
await this.httpServerService?.shutdown();
|
|
||||||
} else {
|
|
||||||
await this.grpcServerService?.shutdown();
|
|
||||||
}
|
|
||||||
process.exit(0);
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(`✌️ [${serviceType}] Error during shutdown:`, error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = new Application();
|
|
||||||
app.start().catch((error) => {
|
|
||||||
Logger.error('🙅♀️ Application failed to start:', error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
export const LOG_END_SYMBOL = ' ';
|
|
||||||
|
|
||||||
export const TASK_COMMAND = 'task';
|
|
||||||
export const QL_COMMAND = 'ql';
|
|
||||||
|
|
||||||
export const TASK_PREFIX = `${TASK_COMMAND} `;
|
|
||||||
export const QL_PREFIX = `${QL_COMMAND} `;
|
|
||||||
|
|
||||||
export const SAMPLE_FILES = [
|
|
||||||
{
|
|
||||||
title: 'config.sample.sh',
|
|
||||||
value: 'sample/config.sample.sh',
|
|
||||||
target: 'config.sh',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'notify.js',
|
|
||||||
value: 'sample/notify.js',
|
|
||||||
target: 'data/scripts/sendNotify.js',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'notify.py',
|
|
||||||
value: 'sample/notify.py',
|
|
||||||
target: 'data/scripts/notify.py',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const PYTHON_INSTALL_DIR = process.env.PYTHON_HOME;
|
|
||||||
|
|
||||||
export const NotificationModeStringMap = {
|
|
||||||
0: 'gotify',
|
|
||||||
1: 'goCqHttpBot',
|
|
||||||
2: 'serverChan',
|
|
||||||
3: 'pushDeer',
|
|
||||||
4: 'bark',
|
|
||||||
5: 'chat',
|
|
||||||
6: 'telegramBot',
|
|
||||||
7: 'dingtalkBot',
|
|
||||||
8: 'weWorkBot',
|
|
||||||
9: 'weWorkApp',
|
|
||||||
10: 'aibotk',
|
|
||||||
11: 'iGot',
|
|
||||||
12: 'pushPlus',
|
|
||||||
13: 'wePlusBot',
|
|
||||||
14: 'email',
|
|
||||||
15: 'pushMe',
|
|
||||||
16: 'feishu',
|
|
||||||
17: 'webhook',
|
|
||||||
18: 'chronocat',
|
|
||||||
19: 'ntfy',
|
|
||||||
20: 'wxPusherBot',
|
|
||||||
} as const;
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import { request as undiciRequest, Dispatcher } from 'undici';
|
|
||||||
|
|
||||||
type RequestBaseOptions = {
|
|
||||||
dispatcher?: Dispatcher;
|
|
||||||
json?: Record<string, any>;
|
|
||||||
form?: string;
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
} & Omit<Dispatcher.RequestOptions<null>, 'origin' | 'path' | 'method'>;
|
|
||||||
|
|
||||||
type RequestOptionsWithOptions = RequestBaseOptions &
|
|
||||||
Partial<Pick<Dispatcher.RequestOptions, 'method'>>;
|
|
||||||
|
|
||||||
type ResponseTypeMap = {
|
|
||||||
json: Record<string, any>;
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ResponseTypeKey = keyof ResponseTypeMap;
|
|
||||||
|
|
||||||
async function request(
|
|
||||||
url: string,
|
|
||||||
options?: RequestOptionsWithOptions,
|
|
||||||
): Promise<Dispatcher.ResponseData<null>> {
|
|
||||||
const { json, form, body, headers = {}, ...rest } = options || {};
|
|
||||||
const finalHeaders = { ...headers } as Record<string, string>;
|
|
||||||
let finalBody = body;
|
|
||||||
|
|
||||||
if (json) {
|
|
||||||
finalHeaders['content-type'] = 'application/json';
|
|
||||||
finalBody = JSON.stringify(json);
|
|
||||||
} else if (form) {
|
|
||||||
finalBody = form;
|
|
||||||
delete finalHeaders['content-type'];
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await undiciRequest(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: finalHeaders,
|
|
||||||
body: finalBody,
|
|
||||||
...rest,
|
|
||||||
});
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function post<T extends ResponseTypeKey = 'json'>(
|
|
||||||
url: string,
|
|
||||||
options?: RequestBaseOptions & { responseType?: T },
|
|
||||||
): Promise<ResponseTypeMap[T]> {
|
|
||||||
const resp = await request(url, { ...options, method: 'POST' });
|
|
||||||
|
|
||||||
const rawText = await resp.body.text();
|
|
||||||
|
|
||||||
if (options?.responseType === 'text') {
|
|
||||||
return rawText as ResponseTypeMap[T];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return JSON.parse(rawText) as ResponseTypeMap[T];
|
|
||||||
} catch {
|
|
||||||
return rawText as ResponseTypeMap[T];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const httpClient = {
|
|
||||||
post,
|
|
||||||
request,
|
|
||||||
};
|
|
||||||
+40
-145
@@ -1,141 +1,52 @@
|
|||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { createRandomString } from './share';
|
import { createRandomString } from './util';
|
||||||
|
|
||||||
dotenv.config({
|
|
||||||
path: path.join(__dirname, '../../.env'),
|
|
||||||
});
|
|
||||||
|
|
||||||
interface Config {
|
|
||||||
port: number;
|
|
||||||
grpcPort: number;
|
|
||||||
nodeEnv: string;
|
|
||||||
isDevelopment: boolean;
|
|
||||||
isProduction: boolean;
|
|
||||||
jwt: {
|
|
||||||
secret: string;
|
|
||||||
expiresIn?: string;
|
|
||||||
};
|
|
||||||
cors: {
|
|
||||||
origin: string[];
|
|
||||||
methods: string[];
|
|
||||||
};
|
|
||||||
logs: {
|
|
||||||
level: string;
|
|
||||||
};
|
|
||||||
api: {
|
|
||||||
prefix: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const config: Config = {
|
|
||||||
port: parseInt(process.env.BACK_PORT || '5700', 10),
|
|
||||||
grpcPort: parseInt(process.env.GRPC_PORT || '5500', 10),
|
|
||||||
nodeEnv: process.env.NODE_ENV || 'development',
|
|
||||||
isDevelopment: process.env.NODE_ENV === 'development',
|
|
||||||
isProduction: process.env.NODE_ENV === 'production',
|
|
||||||
logs: {
|
|
||||||
level: process.env.LOG_LEVEL || 'silly',
|
|
||||||
},
|
|
||||||
api: {
|
|
||||||
prefix: '/api',
|
|
||||||
},
|
|
||||||
jwt: {
|
|
||||||
secret: process.env.JWT_SECRET || 'whyour-secret',
|
|
||||||
expiresIn: process.env.JWT_EXPIRES_IN,
|
|
||||||
},
|
|
||||||
cors: {
|
|
||||||
origin: process.env.CORS_ORIGIN
|
|
||||||
? process.env.CORS_ORIGIN.split(',')
|
|
||||||
: ['*'],
|
|
||||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
||||||
|
|
||||||
if (!process.env.QL_DIR) {
|
const lastVersionFile =
|
||||||
let qlHomePath = path.join(__dirname, '../../');
|
'https://raw.githubusercontent.com/whyour/qinglong/master/src/version.ts';
|
||||||
if (qlHomePath.endsWith('/static/')) {
|
|
||||||
qlHomePath = path.join(qlHomePath, '../');
|
|
||||||
}
|
|
||||||
process.env.QL_DIR = qlHomePath.replace(/\/$/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastVersionFile = `https://qn.whyour.cn/version.yaml`;
|
const envFound = dotenv.config();
|
||||||
|
const rootPath = process.cwd();
|
||||||
// Get and normalize QlBaseUrl
|
const envFile = path.join(rootPath, 'config/env.sh');
|
||||||
let baseUrl = process.env.QlBaseUrl || '';
|
const confFile = path.join(rootPath, 'config/config.sh');
|
||||||
if (baseUrl) {
|
const sampleFile = path.join(rootPath, 'sample/config.sample.sh');
|
||||||
// Ensure it starts with /
|
const crontabFile = path.join(rootPath, 'config/crontab.list');
|
||||||
if (!baseUrl.startsWith('/')) {
|
const confBakDir = path.join(rootPath, 'config/bak/');
|
||||||
baseUrl = `/${baseUrl}`;
|
const authConfigFile = path.join(rootPath, 'config/auth.json');
|
||||||
}
|
const extraFile = path.join(rootPath, 'config/extra.sh');
|
||||||
// Remove trailing slash for consistency in route definitions
|
const configPath = path.join(rootPath, 'config/');
|
||||||
if (baseUrl.endsWith('/')) {
|
const scriptPath = path.join(rootPath, 'scripts/');
|
||||||
baseUrl = baseUrl.slice(0, -1);
|
const bakPath = path.join(rootPath, 'bak/');
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rootPath = process.env.QL_DIR as string;
|
|
||||||
const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
|
|
||||||
|
|
||||||
let dataPath = path.join(rootPath, 'data/');
|
|
||||||
|
|
||||||
if (process.env.QL_DATA_DIR) {
|
|
||||||
dataPath = process.env.QL_DATA_DIR.replace(/\/$/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
const shellPath = path.join(rootPath, 'shell/');
|
|
||||||
const preloadPath = path.join(shellPath, 'preload/');
|
|
||||||
const tmpPath = path.join(rootPath, '.tmp/');
|
|
||||||
const samplePath = path.join(rootPath, 'sample/');
|
const samplePath = path.join(rootPath, 'sample/');
|
||||||
const configPath = path.join(dataPath, 'config/');
|
const logPath = path.join(rootPath, 'log/');
|
||||||
const scriptPath = path.join(dataPath, 'scripts/');
|
|
||||||
const repoPath = path.join(dataPath, 'repo/');
|
|
||||||
const bakPath = path.join(dataPath, 'bak/');
|
|
||||||
const logPath = path.join(dataPath, 'log/');
|
|
||||||
const dbPath = path.join(dataPath, 'db/');
|
|
||||||
const uploadPath = path.join(dataPath, 'upload/');
|
|
||||||
const sshdPath = path.join(dataPath, 'ssh.d/');
|
|
||||||
const systemLogPath = path.join(dataPath, 'syslog/');
|
|
||||||
const dependenceCachePath = path.join(dataPath, 'dep_cache/');
|
|
||||||
|
|
||||||
const envFile = path.join(preloadPath, 'env.sh');
|
|
||||||
const jsEnvFile = path.join(preloadPath, 'env.js');
|
|
||||||
const pyEnvFile = path.join(preloadPath, 'env.py');
|
|
||||||
const jsNotifyFile = path.join(preloadPath, '__ql_notify__.js');
|
|
||||||
const pyNotifyFile = path.join(preloadPath, '__ql_notify__.py');
|
|
||||||
const confFile = path.join(configPath, 'config.sh');
|
|
||||||
const crontabFile = path.join(configPath, 'crontab.list');
|
|
||||||
const authConfigFile = path.join(configPath, 'auth.json');
|
|
||||||
const extraFile = path.join(configPath, 'extra.sh');
|
|
||||||
const confBakDir = path.join(dataPath, 'config/bak/');
|
|
||||||
const sampleFile = path.join(samplePath, 'config.sample.sh');
|
|
||||||
const sqliteFile = path.join(samplePath, 'database.sqlite');
|
|
||||||
|
|
||||||
const authError = '错误的用户名密码,请重试';
|
const authError = '错误的用户名密码,请重试';
|
||||||
const loginFaild = '请先登录!';
|
const loginFaild = '请先登录!';
|
||||||
const configString = 'config sample crontab shareCode diy';
|
const configString = 'config sample crontab shareCode diy';
|
||||||
const versionFile = path.join(rootPath, 'version.yaml');
|
const dbPath = path.join(rootPath, 'db/');
|
||||||
const dataTgzFile = path.join(tmpPath, 'data.tgz');
|
const cronDbFile = path.join(rootPath, 'db/crontab.db');
|
||||||
const shareShellFile = path.join(shellPath, 'share.sh');
|
const envDbFile = path.join(rootPath, 'db/env.db');
|
||||||
const dependenceProxyFile = path.join(configPath, 'dependence-proxy.sh');
|
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 ⚠️");
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...config,
|
port: parseInt(process.env.PORT as string, 10),
|
||||||
jwt: config.jwt,
|
cronPort: parseInt(process.env.CRON_PORT as string, 10),
|
||||||
baseUrl,
|
secret: process.env.SECRET || createRandomString(16, 32),
|
||||||
|
logs: {
|
||||||
|
level: process.env.LOG_LEVEL || 'silly',
|
||||||
|
},
|
||||||
|
api: {
|
||||||
|
prefix: '/api',
|
||||||
|
},
|
||||||
rootPath,
|
rootPath,
|
||||||
tmpPath,
|
|
||||||
dataPath,
|
|
||||||
dataTgzFile,
|
|
||||||
shareShellFile,
|
|
||||||
dependenceProxyFile,
|
|
||||||
configString,
|
configString,
|
||||||
loginFaild,
|
loginFaild,
|
||||||
authError,
|
authError,
|
||||||
@@ -147,48 +58,32 @@ export default {
|
|||||||
sampleFile,
|
sampleFile,
|
||||||
confFile,
|
confFile,
|
||||||
envFile,
|
envFile,
|
||||||
jsEnvFile,
|
|
||||||
pyEnvFile,
|
|
||||||
jsNotifyFile,
|
|
||||||
pyNotifyFile,
|
|
||||||
dbPath,
|
dbPath,
|
||||||
uploadPath,
|
cronDbFile,
|
||||||
|
envDbFile,
|
||||||
|
appDbFile,
|
||||||
|
authDbFile,
|
||||||
|
dependenceDbFile,
|
||||||
configPath,
|
configPath,
|
||||||
scriptPath,
|
scriptPath,
|
||||||
repoPath,
|
|
||||||
samplePath,
|
samplePath,
|
||||||
blackFileList: [
|
blackFileList: [
|
||||||
'auth.json',
|
'auth.json',
|
||||||
'config.sh.sample',
|
'config.sh.sample',
|
||||||
'cookie.sh',
|
'cookie.sh',
|
||||||
'crontab.list',
|
'crontab.list',
|
||||||
'dependence-proxy.sh',
|
|
||||||
'env.sh',
|
'env.sh',
|
||||||
'env.js',
|
|
||||||
'env.py',
|
|
||||||
'token.json',
|
|
||||||
],
|
],
|
||||||
writePathList: [configPath, scriptPath],
|
writePathList: [configPath, scriptPath],
|
||||||
bakPath,
|
bakPath,
|
||||||
apiWhiteList: [
|
apiWhiteList: [
|
||||||
'/api/user/login',
|
'/api/login',
|
||||||
'/api/health',
|
|
||||||
'/open/auth/token',
|
'/open/auth/token',
|
||||||
'/api/user/two-factor/login',
|
'/api/user/two-factor/login',
|
||||||
'/api/system',
|
'/api/system',
|
||||||
'/api/user/init',
|
'/api/init/user',
|
||||||
'/api/user/notification/init',
|
'/api/init/notification',
|
||||||
'/open/user/login',
|
|
||||||
'/open/user/two-factor/login',
|
|
||||||
'/open/system',
|
|
||||||
'/open/user/init',
|
|
||||||
'/open/user/notification/init',
|
|
||||||
],
|
],
|
||||||
versionFile,
|
versionFile,
|
||||||
lastVersionFile,
|
lastVersionFile,
|
||||||
sqliteFile,
|
|
||||||
sshdPath,
|
|
||||||
systemLogPath,
|
|
||||||
dependenceCachePath,
|
|
||||||
maxTokensPerPlatform: 10, // Maximum number of concurrent sessions per platform
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { Request, Response } from 'express';
|
|
||||||
import pick from 'lodash/pick';
|
|
||||||
|
|
||||||
let pickedEnv: Record<string, string>;
|
|
||||||
|
|
||||||
function getPickedEnv() {
|
|
||||||
if (pickedEnv) return pickedEnv;
|
|
||||||
const picked = pick(process.env, ['QlBaseUrl', 'DeployEnv', 'QL_DIR']);
|
|
||||||
if (picked.QlBaseUrl) {
|
|
||||||
if (!picked.QlBaseUrl.startsWith('/')) {
|
|
||||||
picked.QlBaseUrl = `/${picked.QlBaseUrl}`;
|
|
||||||
}
|
|
||||||
if (!picked.QlBaseUrl.endsWith('/')) {
|
|
||||||
picked.QlBaseUrl = `${picked.QlBaseUrl}/`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pickedEnv = picked as Record<string, string>;
|
|
||||||
return picked;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function serveEnv(_req: Request, res: Response) {
|
|
||||||
res.type('.js');
|
|
||||||
res.send(
|
|
||||||
Object.entries(getPickedEnv())
|
|
||||||
.map(([k, v]) => `window.__ENV__${k}=${JSON.stringify(v)};`)
|
|
||||||
.join('\n'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
export function createRandomString(min: number, max: number): string {
|
|
||||||
const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
|
||||||
const english = [
|
|
||||||
'a',
|
|
||||||
'b',
|
|
||||||
'c',
|
|
||||||
'd',
|
|
||||||
'e',
|
|
||||||
'f',
|
|
||||||
'g',
|
|
||||||
'h',
|
|
||||||
'i',
|
|
||||||
'j',
|
|
||||||
'k',
|
|
||||||
'l',
|
|
||||||
'm',
|
|
||||||
'n',
|
|
||||||
'o',
|
|
||||||
'p',
|
|
||||||
'q',
|
|
||||||
'r',
|
|
||||||
's',
|
|
||||||
't',
|
|
||||||
'u',
|
|
||||||
'v',
|
|
||||||
'w',
|
|
||||||
'x',
|
|
||||||
'y',
|
|
||||||
'z',
|
|
||||||
];
|
|
||||||
const ENGLISH = [
|
|
||||||
'A',
|
|
||||||
'B',
|
|
||||||
'C',
|
|
||||||
'D',
|
|
||||||
'E',
|
|
||||||
'F',
|
|
||||||
'G',
|
|
||||||
'H',
|
|
||||||
'I',
|
|
||||||
'J',
|
|
||||||
'K',
|
|
||||||
'L',
|
|
||||||
'M',
|
|
||||||
'N',
|
|
||||||
'O',
|
|
||||||
'P',
|
|
||||||
'Q',
|
|
||||||
'R',
|
|
||||||
'S',
|
|
||||||
'T',
|
|
||||||
'U',
|
|
||||||
'V',
|
|
||||||
'W',
|
|
||||||
'X',
|
|
||||||
'Y',
|
|
||||||
'Z',
|
|
||||||
];
|
|
||||||
const special = ['-', '_'];
|
|
||||||
const config = num.concat(english).concat(ENGLISH).concat(special);
|
|
||||||
|
|
||||||
const arr = [];
|
|
||||||
arr.push(getOne(num));
|
|
||||||
arr.push(getOne(english));
|
|
||||||
arr.push(getOne(ENGLISH));
|
|
||||||
arr.push(getOne(special));
|
|
||||||
|
|
||||||
const len = min + Math.floor(Math.random() * (max - min + 1));
|
|
||||||
|
|
||||||
for (let i = 4; i < len; i++) {
|
|
||||||
arr.push(config[Math.floor(Math.random() * config.length)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newArr = [];
|
|
||||||
for (let j = 0; j < len; j++) {
|
|
||||||
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOne(arr: any[]) {
|
|
||||||
return arr[Math.floor(Math.random() * arr.length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
return newArr.join('');
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { Subscription } from '../data/subscription';
|
|
||||||
import isNil from 'lodash/isNil';
|
|
||||||
|
|
||||||
export function formatUrl(doc: Subscription) {
|
|
||||||
let url = doc.url;
|
|
||||||
let host = '';
|
|
||||||
if (doc.type === 'private-repo') {
|
|
||||||
if (doc.pull_type === 'ssh-key') {
|
|
||||||
host = doc.url!.replace(/.*\@([^\:]+)\:.*/, '$1');
|
|
||||||
url = doc.url!.replace(host, doc.alias);
|
|
||||||
} else {
|
|
||||||
host = doc.url!.replace(/.*\:\/\/([^\/]+)\/.*/, '$1');
|
|
||||||
const { username, password } = doc.pull_option as any;
|
|
||||||
url = doc.url!.replace(host, `${username}:${password}@${host}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { url, host };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatCommand(doc: Subscription, url?: string) {
|
|
||||||
let command = `SUB_ID=${doc.id} ql `;
|
|
||||||
let _url = url || formatUrl(doc).url;
|
|
||||||
const {
|
|
||||||
type,
|
|
||||||
whitelist,
|
|
||||||
blacklist,
|
|
||||||
dependences,
|
|
||||||
branch,
|
|
||||||
extensions,
|
|
||||||
proxy,
|
|
||||||
autoAddCron,
|
|
||||||
autoDelCron,
|
|
||||||
} = doc;
|
|
||||||
if (type === 'file') {
|
|
||||||
command += `raw "${_url}" "${proxy || ''}" "${
|
|
||||||
isNil(autoAddCron) ? true : Boolean(autoAddCron)
|
|
||||||
}" "${isNil(autoDelCron) ? true : Boolean(autoDelCron)}"`;
|
|
||||||
} else {
|
|
||||||
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
|
|
||||||
dependences || ''
|
|
||||||
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}" "${
|
|
||||||
isNil(autoAddCron) ? true : Boolean(autoAddCron)
|
|
||||||
}" "${isNil(autoDelCron) ? true : Boolean(autoDelCron)}"`;
|
|
||||||
}
|
|
||||||
return command;
|
|
||||||
}
|
|
||||||
+164
-523
@@ -1,40 +1,24 @@
|
|||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { exec } from 'child_process';
|
import got from 'got';
|
||||||
import psTreeFun from 'ps-tree';
|
import iconv from 'iconv-lite';
|
||||||
import { promisify } from 'util';
|
|
||||||
import { load } from 'js-yaml';
|
|
||||||
import config from './index';
|
|
||||||
import { PYTHON_INSTALL_DIR, TASK_COMMAND } from './const';
|
|
||||||
import Logger from '../loaders/logger';
|
|
||||||
import { writeFileWithLock } from '../shared/utils';
|
|
||||||
import { DependenceTypes } from '../data/dependence';
|
|
||||||
import { FormData } from 'undici';
|
|
||||||
|
|
||||||
export * from './share';
|
export function getFileContentByName(fileName: string) {
|
||||||
|
if (fs.existsSync(fileName)) {
|
||||||
export async function getFileContentByName(fileName: string) {
|
return fs.readFileSync(fileName, 'utf8');
|
||||||
const _exsit = await fileExist(fileName);
|
|
||||||
if (_exsit) {
|
|
||||||
return await fs.readFile(fileName, 'utf8');
|
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeAnsi(text: string) {
|
export function getLastModifyFilePath(dir: string) {
|
||||||
return text.replace(/\x1b\[\d+m/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getLastModifyFilePath(dir: string) {
|
|
||||||
let filePath = '';
|
let filePath = '';
|
||||||
|
|
||||||
const _exsit = await fileExist(dir);
|
if (fs.existsSync(dir)) {
|
||||||
if (_exsit) {
|
const arr = fs.readdirSync(dir);
|
||||||
const arr = await fs.readdir(dir);
|
|
||||||
|
|
||||||
arr.forEach(async (item) => {
|
arr.forEach((item) => {
|
||||||
const fullpath = path.join(dir, item);
|
const fullpath = path.join(dir, item);
|
||||||
const stats = await fs.lstat(fullpath);
|
const stats = fs.statSync(fullpath);
|
||||||
if (stats.isFile()) {
|
if (stats.isFile()) {
|
||||||
if (stats.mtimeMs >= 0) {
|
if (stats.mtimeMs >= 0) {
|
||||||
filePath = fullpath;
|
filePath = fullpath;
|
||||||
@@ -45,6 +29,91 @@ export async function getLastModifyFilePath(dir: string) {
|
|||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createRandomString(min: number, max: number): string {
|
||||||
|
const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||||
|
const english = [
|
||||||
|
'a',
|
||||||
|
'b',
|
||||||
|
'c',
|
||||||
|
'd',
|
||||||
|
'e',
|
||||||
|
'f',
|
||||||
|
'g',
|
||||||
|
'h',
|
||||||
|
'i',
|
||||||
|
'j',
|
||||||
|
'k',
|
||||||
|
'l',
|
||||||
|
'm',
|
||||||
|
'n',
|
||||||
|
'o',
|
||||||
|
'p',
|
||||||
|
'q',
|
||||||
|
'r',
|
||||||
|
's',
|
||||||
|
't',
|
||||||
|
'u',
|
||||||
|
'v',
|
||||||
|
'w',
|
||||||
|
'x',
|
||||||
|
'y',
|
||||||
|
'z',
|
||||||
|
];
|
||||||
|
const ENGLISH = [
|
||||||
|
'A',
|
||||||
|
'B',
|
||||||
|
'C',
|
||||||
|
'D',
|
||||||
|
'E',
|
||||||
|
'F',
|
||||||
|
'G',
|
||||||
|
'H',
|
||||||
|
'I',
|
||||||
|
'J',
|
||||||
|
'K',
|
||||||
|
'L',
|
||||||
|
'M',
|
||||||
|
'N',
|
||||||
|
'O',
|
||||||
|
'P',
|
||||||
|
'Q',
|
||||||
|
'R',
|
||||||
|
'S',
|
||||||
|
'T',
|
||||||
|
'U',
|
||||||
|
'V',
|
||||||
|
'W',
|
||||||
|
'X',
|
||||||
|
'Y',
|
||||||
|
'Z',
|
||||||
|
];
|
||||||
|
const special = ['-', '_'];
|
||||||
|
const config = num.concat(english).concat(ENGLISH).concat(special);
|
||||||
|
|
||||||
|
const arr = [];
|
||||||
|
arr.push(getOne(num));
|
||||||
|
arr.push(getOne(english));
|
||||||
|
arr.push(getOne(ENGLISH));
|
||||||
|
arr.push(getOne(special));
|
||||||
|
|
||||||
|
const len = min + Math.floor(Math.random() * (max - min + 1));
|
||||||
|
|
||||||
|
for (let i = 4; i < len; i++) {
|
||||||
|
arr.push(config[Math.floor(Math.random() * config.length)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newArr = [];
|
||||||
|
for (let j = 0; j < len; j++) {
|
||||||
|
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOne(arr) {
|
||||||
|
return arr[Math.floor(Math.random() * arr.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return newArr.join('');
|
||||||
|
}
|
||||||
|
|
||||||
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') {
|
||||||
@@ -56,6 +125,69 @@ export function getToken(req: any) {
|
|||||||
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' {
|
export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
|
||||||
const ua = userAgent.toLowerCase();
|
const ua = userAgent.toLowerCase();
|
||||||
const testUa = (regexp: RegExp) => regexp.test(ua);
|
const testUa = (regexp: RegExp) => regexp.test(ua);
|
||||||
@@ -77,19 +209,12 @@ export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
|
|||||||
system = 'android'; // android系统
|
system = 'android'; // android系统
|
||||||
} else if (testUa(/ios|iphone|ipad|ipod|iwatch/g)) {
|
} else if (testUa(/ios|iphone|ipad|ipod|iwatch/g)) {
|
||||||
system = 'ios'; // ios系统
|
system = 'ios'; // ios系统
|
||||||
} else if (testUa(/openharmony/g)) {
|
|
||||||
system = 'openharmony'; // openharmony系统
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let platform = 'desktop';
|
let platform = 'desktop';
|
||||||
if (system === 'windows' || system === 'macos' || system === 'linux') {
|
if (system === 'windows' || system === 'macos' || system === 'linux') {
|
||||||
platform = 'desktop';
|
platform = 'desktop';
|
||||||
} else if (
|
} else if (system === 'android' || system === 'ios' || testUa(/mobile/g)) {
|
||||||
system === 'android' ||
|
|
||||||
system === 'ios' ||
|
|
||||||
system === 'openharmony' ||
|
|
||||||
testUa(/mobile/g)
|
|
||||||
) {
|
|
||||||
platform = 'mobile';
|
platform = 'mobile';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,496 +222,12 @@ export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fileExist(file: any) {
|
export async function fileExist(file: any) {
|
||||||
try {
|
return new Promise((resolve) => {
|
||||||
await fs.access(file);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createFile(file: string, data: string = '') {
|
|
||||||
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
||||||
await writeFileWithLock(file, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handleLogPath(
|
|
||||||
logPath: string,
|
|
||||||
data: string = '',
|
|
||||||
): Promise<string> {
|
|
||||||
const absolutePath = path.resolve(config.logPath, logPath);
|
|
||||||
const logFileExist = await fileExist(absolutePath);
|
|
||||||
if (!logFileExist) {
|
|
||||||
await createFile(absolutePath, data);
|
|
||||||
}
|
|
||||||
return absolutePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function concurrentRun(
|
|
||||||
fnList: Array<() => Promise<any>> = [],
|
|
||||||
max = 5,
|
|
||||||
) {
|
|
||||||
if (!fnList.length) return;
|
|
||||||
|
|
||||||
const replyList: any[] = []; // 收集任务执行结果
|
|
||||||
const startTime = new Date().getTime(); // 记录任务执行开始时间
|
|
||||||
|
|
||||||
// 任务执行程序
|
|
||||||
const schedule = async (index: number) => {
|
|
||||||
return new Promise(async (resolve) => {
|
|
||||||
const fn = fnList[index];
|
|
||||||
if (!fn) return resolve(null);
|
|
||||||
|
|
||||||
// 执行当前异步任务
|
|
||||||
const reply = await fn();
|
|
||||||
replyList[index] = reply;
|
|
||||||
|
|
||||||
// 执行完当前任务后,继续执行任务池的剩余任务
|
|
||||||
await schedule(index + max);
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 任务池执行程序
|
|
||||||
const scheduleList = new Array(max)
|
|
||||||
.fill(0)
|
|
||||||
.map((_, index) => schedule(index));
|
|
||||||
|
|
||||||
// 使用 Promise.all 批量执行
|
|
||||||
const r = await Promise.all(scheduleList);
|
|
||||||
const cost = (new Date().getTime() - startTime) / 1000;
|
|
||||||
|
|
||||||
return replyList;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum FileType {
|
|
||||||
'directory',
|
|
||||||
'file',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IFile {
|
|
||||||
title: string;
|
|
||||||
key: string;
|
|
||||||
type: 'directory' | 'file';
|
|
||||||
parent: string;
|
|
||||||
createTime: number;
|
|
||||||
size?: number;
|
|
||||||
children?: IFile[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dirSort(a: IFile, b: IFile): number {
|
|
||||||
if (a.type === 'file' && b.type === 'file') {
|
|
||||||
return b.createTime - a.createTime;
|
|
||||||
} else if (a.type === 'directory' && b.type === 'directory') {
|
|
||||||
return a.title.localeCompare(b.title);
|
|
||||||
} else {
|
|
||||||
return a.type === 'directory' ? -1 : 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function readDirs(
|
|
||||||
dir: string,
|
|
||||||
baseDir: string = '',
|
|
||||||
blacklist: string[] = [],
|
|
||||||
sort: (a: IFile, b: IFile) => number = dirSort,
|
|
||||||
): Promise<IFile[]> {
|
|
||||||
const relativePath = path.relative(baseDir, dir);
|
|
||||||
const files = await fs.readdir(dir);
|
|
||||||
const result: IFile[] = [];
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const subPath = path.join(dir, file);
|
|
||||||
const stats = await fs.lstat(subPath);
|
|
||||||
const key = path.join(relativePath, file);
|
|
||||||
|
|
||||||
if (blacklist.includes(file) || stats.isSymbolicLink()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stats.isDirectory()) {
|
|
||||||
const children = await readDirs(subPath, baseDir, blacklist, sort);
|
|
||||||
result.push({
|
|
||||||
title: file,
|
|
||||||
key,
|
|
||||||
type: 'directory',
|
|
||||||
parent: relativePath,
|
|
||||||
createTime: stats.birthtime.getTime(),
|
|
||||||
children: children.sort(sort),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
result.push({
|
|
||||||
title: file,
|
|
||||||
type: 'file',
|
|
||||||
key,
|
|
||||||
parent: relativePath,
|
|
||||||
size: stats.size,
|
|
||||||
createTime: stats.birthtime.getTime(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.sort(sort);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function readDir(
|
|
||||||
dir: string,
|
|
||||||
baseDir: string = '',
|
|
||||||
blacklist: string[] = [],
|
|
||||||
): Promise<IFile[]> {
|
|
||||||
const absoluteDir = path.join(baseDir, dir);
|
|
||||||
const relativePath = path.relative(baseDir, absoluteDir);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const files = await fs.readdir(absoluteDir);
|
|
||||||
const result: IFile[] = [];
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const subPath = path.join(absoluteDir, file);
|
|
||||||
const stats = await fs.lstat(subPath);
|
|
||||||
const key = path.join(relativePath, file);
|
|
||||||
|
|
||||||
if (blacklist.includes(file) || stats.isSymbolicLink()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stats.isDirectory()) {
|
|
||||||
result.push({
|
|
||||||
title: file,
|
|
||||||
type: 'directory',
|
|
||||||
key,
|
|
||||||
parent: relativePath,
|
|
||||||
createTime: stats.birthtime.getTime(),
|
|
||||||
children: [],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
result.push({
|
|
||||||
title: file,
|
|
||||||
type: 'file',
|
|
||||||
key,
|
|
||||||
parent: relativePath,
|
|
||||||
size: stats.size,
|
|
||||||
createTime: stats.birthtime.getTime(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.code === 'ENOENT') {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function promiseExec(command: string): Promise<string> {
|
|
||||||
try {
|
|
||||||
const { stderr, stdout } = await promisify(exec)(command, {
|
|
||||||
maxBuffer: 200 * 1024 * 1024,
|
|
||||||
encoding: 'utf8',
|
|
||||||
});
|
|
||||||
return stdout || stderr;
|
|
||||||
} catch (error) {
|
|
||||||
return JSON.stringify(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function promiseExecSuccess(command: string): Promise<string> {
|
|
||||||
try {
|
|
||||||
const { stdout } = await promisify(exec)(command, {
|
|
||||||
maxBuffer: 200 * 1024 * 1024,
|
|
||||||
encoding: 'utf8',
|
|
||||||
});
|
|
||||||
return stdout || '';
|
|
||||||
} catch (error) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseHeaders(headers: string) {
|
|
||||||
if (!headers) return {};
|
|
||||||
|
|
||||||
const parsed: any = {};
|
|
||||||
let key: string;
|
|
||||||
let val: string;
|
|
||||||
let i: number;
|
|
||||||
|
|
||||||
headers &&
|
|
||||||
headers.split('\n').forEach(function parser(line) {
|
|
||||||
i = line.indexOf(':');
|
|
||||||
key = line.substring(0, i).trim().toLowerCase();
|
|
||||||
val = line.substring(i + 1).trim();
|
|
||||||
|
|
||||||
if (!key) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
|
|
||||||
});
|
|
||||||
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseString(
|
|
||||||
input: string,
|
|
||||||
valueFormatFn?: (v: string) => string,
|
|
||||||
): Record<string, string> {
|
|
||||||
const regex = /(\w+):\s*((?:(?!\n\w+:).)*)/g;
|
|
||||||
const matches: Record<string, string> = {};
|
|
||||||
|
|
||||||
let match;
|
|
||||||
while ((match = regex.exec(input)) !== null) {
|
|
||||||
const [, key, value] = match;
|
|
||||||
const _key = key.trim();
|
|
||||||
if (!_key || matches[_key]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let _value = value.trim();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_value = valueFormatFn ? valueFormatFn(_value) : _value;
|
fs.accessSync(file);
|
||||||
const jsonValue = JSON.parse(_value);
|
resolve(true);
|
||||||
matches[_key] = jsonValue;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
matches[_key] = _value;
|
resolve(false);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseBody(
|
|
||||||
body: string,
|
|
||||||
contentType:
|
|
||||||
| 'application/json'
|
|
||||||
| 'multipart/form-data'
|
|
||||||
| 'application/x-www-form-urlencoded'
|
|
||||||
| 'text/plain',
|
|
||||||
valueFormatFn?: (v: string) => string,
|
|
||||||
) {
|
|
||||||
if (contentType === 'text/plain' || !body) {
|
|
||||||
return valueFormatFn && body ? valueFormatFn(body) : body;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = parseString(body, valueFormatFn);
|
|
||||||
|
|
||||||
switch (contentType) {
|
|
||||||
case 'multipart/form-data':
|
|
||||||
return Object.keys(parsed).reduce((p, c) => {
|
|
||||||
p.append(c, parsed[c]);
|
|
||||||
return p;
|
|
||||||
}, new FormData());
|
|
||||||
case 'application/x-www-form-urlencoded':
|
|
||||||
return Object.keys(parsed).reduce((p, c) => {
|
|
||||||
return p ? `${p}&${c}=${parsed[c]}` : `${c}=${parsed[c]}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function psTree(pid: number): Promise<number[]> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
psTreeFun(pid, (err: any, children) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
resolve(children.map((x) => Number(x.PID)).filter((x) => !isNaN(x)));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function killTask(pid: number) {
|
|
||||||
const pids = await psTree(pid);
|
|
||||||
|
|
||||||
if (pids.length) {
|
|
||||||
try {
|
|
||||||
[pid, ...pids].reverse().forEach((x) => {
|
|
||||||
process.kill(x, 15);
|
|
||||||
});
|
|
||||||
} catch (error) {}
|
|
||||||
} else {
|
|
||||||
process.kill(pid, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getPid(cmd: string) {
|
|
||||||
const taskCommand = `ps -eo pid,command | grep "${cmd}" | grep -v grep | awk '{print $1}' | head -1 | xargs echo -n`;
|
|
||||||
const pid = await promiseExec(taskCommand);
|
|
||||||
return pid ? Number(pid) : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAllPids(cmd: string): Promise<number[]> {
|
|
||||||
const taskCommand = `ps -eo pid,command | grep "${cmd}" | grep -v grep | awk '{print $1}'`;
|
|
||||||
const pidsStr = await promiseExec(taskCommand);
|
|
||||||
if (!pidsStr) return [];
|
|
||||||
return pidsStr
|
|
||||||
.split('\n')
|
|
||||||
.map((p) => Number(p.trim()))
|
|
||||||
.filter((p) => !isNaN(p) && p > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function killAllTasks(cmd: string): Promise<void> {
|
|
||||||
const pids = await getAllPids(cmd);
|
|
||||||
for (const pid of pids) {
|
|
||||||
try {
|
|
||||||
await killTask(pid);
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore errors if process already terminated
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IVersion {
|
|
||||||
version: string;
|
|
||||||
changeLogLink: string;
|
|
||||||
changeLog: string;
|
|
||||||
publishTime: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function parseVersion(path: string): Promise<IVersion> {
|
|
||||||
return load(await fs.readFile(path, 'utf8')) as IVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseContentVersion(content: string): IVersion {
|
|
||||||
return load(content) as IVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getUniqPath(
|
|
||||||
command: string,
|
|
||||||
id: string,
|
|
||||||
): Promise<string> {
|
|
||||||
let suffix = '';
|
|
||||||
if (/^\d+$/.test(id)) {
|
|
||||||
suffix = `_${id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let items = command.split(/ +/);
|
|
||||||
|
|
||||||
const maxTimeCommandIndex = items.findIndex((x) => x === '-m');
|
|
||||||
if (maxTimeCommandIndex !== -1) {
|
|
||||||
items = items.slice(maxTimeCommandIndex + 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
let str = items[0];
|
|
||||||
if (items[0] === TASK_COMMAND) {
|
|
||||||
str = items[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
const dotIndex = str.lastIndexOf('.');
|
|
||||||
|
|
||||||
if (dotIndex !== -1) {
|
|
||||||
str = str.slice(0, dotIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
const slashIndex = str.lastIndexOf('/');
|
|
||||||
|
|
||||||
let tempStr = '';
|
|
||||||
if (slashIndex !== -1) {
|
|
||||||
tempStr = str.slice(0, slashIndex);
|
|
||||||
const _slashIndex = tempStr.lastIndexOf('/');
|
|
||||||
if (_slashIndex !== -1) {
|
|
||||||
tempStr = tempStr.slice(_slashIndex + 1);
|
|
||||||
}
|
|
||||||
str = `${tempStr}_${str.slice(slashIndex + 1)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${str}${suffix}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function safeJSONParse(value?: string) {
|
|
||||||
if (!value) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return JSON.parse(value);
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error('[safeJSONParse失败]', error);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function rmPath(path: string) {
|
|
||||||
try {
|
|
||||||
const _exsit = await fileExist(path);
|
|
||||||
if (_exsit) {
|
|
||||||
await fs.rm(path, { force: true, recursive: true, maxRetries: 5 });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error('[rmPath失败]', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setSystemTimezone(timezone: string): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
if (!(await fileExist(`/usr/share/zoneinfo/${timezone}`))) {
|
|
||||||
throw new Error('Invalid timezone');
|
|
||||||
}
|
|
||||||
|
|
||||||
await promiseExec(`ln -sf /usr/share/zoneinfo/${timezone} /etc/localtime`);
|
|
||||||
await promiseExec(`echo "${timezone}" > /etc/timezone`);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error('[setSystemTimezone失败]', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getGetCommand(type: DependenceTypes, name: string): string {
|
|
||||||
const baseCommands = {
|
|
||||||
[DependenceTypes.nodejs]: `pnpm ls -g | grep "${name}" | head -1`,
|
|
||||||
[DependenceTypes.python3]: `
|
|
||||||
python3 -c "exec('''
|
|
||||||
name='${name}'
|
|
||||||
try:
|
|
||||||
from importlib.metadata import version
|
|
||||||
print(version(name))
|
|
||||||
except:
|
|
||||||
import importlib.util as u
|
|
||||||
import importlib.metadata as m
|
|
||||||
spec=u.find_spec(name)
|
|
||||||
print(name if spec else '')
|
|
||||||
''')"`,
|
|
||||||
[DependenceTypes.linux]: `apk info -es ${name}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
return baseCommands[type];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getInstallCommand(type: DependenceTypes, name: string): string {
|
|
||||||
const baseCommands = {
|
|
||||||
[DependenceTypes.nodejs]: 'pnpm add -g',
|
|
||||||
[DependenceTypes.python3]:
|
|
||||||
'pip3 install --disable-pip-version-check --root-user-action=ignore',
|
|
||||||
[DependenceTypes.linux]: 'apk add --no-check-certificate',
|
|
||||||
};
|
|
||||||
|
|
||||||
let command = baseCommands[type];
|
|
||||||
|
|
||||||
if (type === DependenceTypes.python3 && PYTHON_INSTALL_DIR) {
|
|
||||||
command = `${command} --prefix=${PYTHON_INSTALL_DIR}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${command} ${name.trim()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUninstallCommand(
|
|
||||||
type: DependenceTypes,
|
|
||||||
name: string,
|
|
||||||
): string {
|
|
||||||
const baseCommands = {
|
|
||||||
[DependenceTypes.nodejs]: 'pnpm remove -g',
|
|
||||||
[DependenceTypes.python3]:
|
|
||||||
'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y',
|
|
||||||
[DependenceTypes.linux]: 'apk del',
|
|
||||||
};
|
|
||||||
|
|
||||||
return `${baseCommands[type]} ${name.trim()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isDemoEnv() {
|
|
||||||
return process.env.DeployEnv === 'demo';
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
}
|
||||||
+12
-65
@@ -1,93 +1,40 @@
|
|||||||
import { sequelize } from '.';
|
|
||||||
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
|
||||||
|
|
||||||
export class Crontab {
|
export class Crontab {
|
||||||
name?: string;
|
name?: string;
|
||||||
command: string;
|
command: string;
|
||||||
schedule?: string;
|
schedule: string;
|
||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
|
created?: number;
|
||||||
saved?: boolean;
|
saved?: boolean;
|
||||||
id?: number;
|
_id?: string;
|
||||||
status?: CrontabStatus;
|
status?: CrontabStatus;
|
||||||
isSystem?: 1 | 0;
|
isSystem?: 1 | 0;
|
||||||
pid?: number;
|
pid?: number;
|
||||||
isDisabled?: 1 | 0;
|
isDisabled?: 1 | 0;
|
||||||
log_path?: string;
|
log_path?: string;
|
||||||
isPinned?: 1 | 0;
|
isPinned?: 1 | 0;
|
||||||
labels?: string[];
|
|
||||||
last_running_time?: number;
|
|
||||||
last_execution_time?: number;
|
|
||||||
sub_id?: number;
|
|
||||||
extra_schedules?: Array<{ schedule: string }>;
|
|
||||||
task_before?: string;
|
|
||||||
task_after?: string;
|
|
||||||
log_name?: string;
|
|
||||||
allow_multiple_instances?: 1 | 0;
|
|
||||||
|
|
||||||
constructor(options: Crontab) {
|
constructor(options: Crontab) {
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
this.command = options.command.trim();
|
this.command = options.command;
|
||||||
this.schedule = options.schedule;
|
this.schedule = options.schedule;
|
||||||
this.saved = options.saved;
|
this.saved = options.saved;
|
||||||
this.id = options.id;
|
this._id = options._id;
|
||||||
this.status =
|
this.created = options.created;
|
||||||
typeof options.status === 'number' && CrontabStatus[options.status]
|
this.status = CrontabStatus[options.status]
|
||||||
? options.status
|
? options.status
|
||||||
: CrontabStatus.idle;
|
: CrontabStatus.idle;
|
||||||
this.timestamp = new Date().toString();
|
this.timestamp = new Date().toString();
|
||||||
this.isSystem = options.isSystem || 0;
|
this.isSystem = options.isSystem || 0;
|
||||||
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;
|
this.isPinned = options.isPinned || 0;
|
||||||
this.labels = options.labels || [];
|
|
||||||
this.last_running_time = options.last_running_time || 0;
|
|
||||||
this.last_execution_time = options.last_execution_time || 0;
|
|
||||||
this.sub_id = options.sub_id;
|
|
||||||
this.extra_schedules = options.extra_schedules;
|
|
||||||
this.task_before = options.task_before;
|
|
||||||
this.task_after = options.task_after;
|
|
||||||
this.log_name = options.log_name;
|
|
||||||
this.allow_multiple_instances = options.allow_multiple_instances || 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CrontabStatus {
|
export enum CrontabStatus {
|
||||||
'running' = 0,
|
'running',
|
||||||
'queued' = 0.5,
|
'idle',
|
||||||
'idle' = 1,
|
|
||||||
'disabled',
|
'disabled',
|
||||||
|
'queued',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
|
|
||||||
export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
|
||||||
name: {
|
|
||||||
unique: 'compositeIndex',
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
},
|
|
||||||
command: {
|
|
||||||
unique: 'compositeIndex',
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
},
|
|
||||||
schedule: {
|
|
||||||
unique: 'compositeIndex',
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
},
|
|
||||||
timestamp: DataTypes.STRING,
|
|
||||||
saved: DataTypes.BOOLEAN,
|
|
||||||
status: DataTypes.NUMBER,
|
|
||||||
isSystem: DataTypes.NUMBER,
|
|
||||||
pid: DataTypes.NUMBER,
|
|
||||||
isDisabled: DataTypes.NUMBER,
|
|
||||||
isPinned: DataTypes.NUMBER,
|
|
||||||
log_path: DataTypes.STRING,
|
|
||||||
labels: DataTypes.JSON,
|
|
||||||
last_running_time: DataTypes.NUMBER,
|
|
||||||
last_execution_time: DataTypes.NUMBER,
|
|
||||||
sub_id: { type: DataTypes.NUMBER, allowNull: true },
|
|
||||||
extra_schedules: DataTypes.JSON,
|
|
||||||
task_before: DataTypes.STRING,
|
|
||||||
task_after: DataTypes.STRING,
|
|
||||||
log_name: DataTypes.STRING,
|
|
||||||
allow_multiple_instances: DataTypes.NUMBER,
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
import { sequelize } from '.';
|
|
||||||
import { DataTypes, Model } from 'sequelize';
|
|
||||||
|
|
||||||
export enum CronViewType {
|
|
||||||
'系统' = 1,
|
|
||||||
'个人',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SortType {
|
|
||||||
type: 'ASC' | 'DESC';
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FilterType {
|
|
||||||
property: string;
|
|
||||||
operation: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CrontabView {
|
|
||||||
name?: string;
|
|
||||||
id?: number;
|
|
||||||
position?: number;
|
|
||||||
isDisabled?: 1 | 0;
|
|
||||||
filters?: FilterType[];
|
|
||||||
sorts?: SortType[];
|
|
||||||
filterRelation?: 'and' | 'or';
|
|
||||||
type?: CronViewType;
|
|
||||||
|
|
||||||
constructor(options: CrontabView) {
|
|
||||||
this.name = options.name;
|
|
||||||
this.id = options.id;
|
|
||||||
this.position = options.position;
|
|
||||||
this.isDisabled = options.isDisabled || 0;
|
|
||||||
this.filters = options.filters;
|
|
||||||
this.sorts = options.sorts;
|
|
||||||
this.filterRelation = options.filterRelation;
|
|
||||||
this.type = options.type || CronViewType.个人;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CronViewInstance
|
|
||||||
extends Model<CrontabView, CrontabView>,
|
|
||||||
CrontabView {}
|
|
||||||
export const CrontabViewModel = sequelize.define<CronViewInstance>(
|
|
||||||
'CrontabView',
|
|
||||||
{
|
|
||||||
name: {
|
|
||||||
unique: 'name',
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
},
|
|
||||||
position: DataTypes.NUMBER,
|
|
||||||
isDisabled: DataTypes.NUMBER,
|
|
||||||
filters: DataTypes.JSON,
|
|
||||||
sorts: DataTypes.JSON,
|
|
||||||
filterRelation: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
type: DataTypes.NUMBER,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
+18
-33
@@ -1,24 +1,20 @@
|
|||||||
import { sequelize } from '.';
|
|
||||||
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
|
||||||
|
|
||||||
export class Dependence {
|
export class Dependence {
|
||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
id?: number;
|
created?: number;
|
||||||
status: DependenceStatus;
|
_id?: string;
|
||||||
type: DependenceTypes;
|
status?: DependenceStatus;
|
||||||
name: string;
|
type?: DependenceTypes;
|
||||||
|
name?: number;
|
||||||
log?: string[];
|
log?: string[];
|
||||||
remark?: string;
|
remark?: string;
|
||||||
|
|
||||||
constructor(options: Dependence) {
|
constructor(options: Dependence) {
|
||||||
this.id = options.id;
|
this._id = options._id;
|
||||||
this.status =
|
this.created = options.created || new Date().valueOf();
|
||||||
typeof options.status === 'number' && DependenceStatus[options.status]
|
this.status = options.status || DependenceStatus.installing;
|
||||||
? options.status
|
|
||||||
: DependenceStatus.queued;
|
|
||||||
this.type = options.type || DependenceTypes.nodejs;
|
this.type = options.type || DependenceTypes.nodejs;
|
||||||
this.timestamp = new Date().toString();
|
this.timestamp = new Date().toString();
|
||||||
this.name = options.name.trim();
|
this.name = options.name;
|
||||||
this.log = options.log || [];
|
this.log = options.log || [];
|
||||||
this.remark = options.remark || '';
|
this.remark = options.remark || '';
|
||||||
}
|
}
|
||||||
@@ -31,8 +27,6 @@ export enum DependenceStatus {
|
|||||||
'removing',
|
'removing',
|
||||||
'removed',
|
'removed',
|
||||||
'removeFailed',
|
'removeFailed',
|
||||||
'queued',
|
|
||||||
'cancelled',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DependenceTypes {
|
export enum DependenceTypes {
|
||||||
@@ -41,23 +35,14 @@ export enum DependenceTypes {
|
|||||||
'linux',
|
'linux',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum versionDependenceCommandTypes {
|
export enum InstallDependenceCommandTypes {
|
||||||
'@',
|
'npm i -g --force',
|
||||||
'==',
|
'pip3 install',
|
||||||
'=',
|
'apk add --no-cache -f',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DependenceInstance
|
export enum unInstallDependenceCommandTypes {
|
||||||
extends Model<Dependence, Dependence>,
|
'npm uninstall -g --force',
|
||||||
Dependence {}
|
'pip3 uninstall -y',
|
||||||
export const DependenceModel = sequelize.define<DependenceInstance>(
|
'apk del -f',
|
||||||
'Dependence',
|
}
|
||||||
{
|
|
||||||
name: DataTypes.STRING,
|
|
||||||
type: DataTypes.NUMBER,
|
|
||||||
timestamp: DataTypes.STRING,
|
|
||||||
status: DataTypes.NUMBER,
|
|
||||||
log: DataTypes.JSON,
|
|
||||||
remark: DataTypes.STRING,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|||||||
+9
-29
@@ -1,28 +1,22 @@
|
|||||||
import { DataTypes, Model } from 'sequelize';
|
|
||||||
import { sequelize } from '.';
|
|
||||||
|
|
||||||
export class Env {
|
export class Env {
|
||||||
value?: string;
|
value?: string;
|
||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
id?: number;
|
created?: number;
|
||||||
|
_id?: string;
|
||||||
status?: EnvStatus;
|
status?: EnvStatus;
|
||||||
position?: number;
|
position?: number;
|
||||||
name?: string;
|
name?: number;
|
||||||
remarks?: string;
|
remarks?: number;
|
||||||
isPinned?: 1 | 0;
|
|
||||||
|
|
||||||
constructor(options: Env) {
|
constructor(options: Env) {
|
||||||
this.value = options.value;
|
this.value = options.value;
|
||||||
this.id = options.id;
|
this._id = options._id;
|
||||||
this.status =
|
this.created = options.created || new Date().valueOf();
|
||||||
typeof options.status === 'number' && EnvStatus[options.status]
|
this.status = options.status || EnvStatus.normal;
|
||||||
? options.status
|
|
||||||
: EnvStatus.normal;
|
|
||||||
this.timestamp = new Date().toString();
|
this.timestamp = new Date().toString();
|
||||||
this.position = options.position;
|
this.position = options.position;
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
this.remarks = options.remarks || '';
|
this.remarks = options.remarks;
|
||||||
this.isPinned = options.isPinned || 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,18 +25,4 @@ export enum EnvStatus {
|
|||||||
'disabled',
|
'disabled',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const maxPosition = 9000000000000000;
|
export const initEnvPosition = 9999999999;
|
||||||
export const initPosition = 4500000000000000;
|
|
||||||
export const stepPosition = 10000000000;
|
|
||||||
export const minPosition = 100;
|
|
||||||
|
|
||||||
export interface EnvInstance extends Model<Env, Env>, Env {}
|
|
||||||
export const EnvModel = sequelize.define<EnvInstance>('Env', {
|
|
||||||
value: { type: DataTypes.STRING, unique: 'compositeIndex' },
|
|
||||||
timestamp: DataTypes.STRING,
|
|
||||||
status: DataTypes.NUMBER,
|
|
||||||
position: DataTypes.NUMBER,
|
|
||||||
name: { type: DataTypes.STRING, unique: 'compositeIndex' },
|
|
||||||
remarks: DataTypes.STRING,
|
|
||||||
isPinned: DataTypes.NUMBER,
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { Sequelize, Transaction } from 'sequelize';
|
|
||||||
import config from '../config/index';
|
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
export const sequelize = new Sequelize({
|
|
||||||
dialect: 'sqlite',
|
|
||||||
storage: join(config.dbPath, 'database.sqlite'),
|
|
||||||
logging: false,
|
|
||||||
retry: {
|
|
||||||
max: 10,
|
|
||||||
match: ['SQLITE_BUSY: database is locked'],
|
|
||||||
},
|
|
||||||
pool: {
|
|
||||||
max: 5,
|
|
||||||
min: 2,
|
|
||||||
idle: 30000,
|
|
||||||
acquire: 30000,
|
|
||||||
evict: 10000,
|
|
||||||
},
|
|
||||||
transactionType: Transaction.TYPES.IMMEDIATE,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type ResponseType<T> = { code: number; data?: T; message?: string };
|
|
||||||
+2
-98
@@ -2,24 +2,14 @@ export enum NotificationMode {
|
|||||||
'gotify' = 'gotify',
|
'gotify' = 'gotify',
|
||||||
'goCqHttpBot' = 'goCqHttpBot',
|
'goCqHttpBot' = 'goCqHttpBot',
|
||||||
'serverChan' = 'serverChan',
|
'serverChan' = 'serverChan',
|
||||||
'pushDeer' = 'pushDeer',
|
|
||||||
'bark' = 'bark',
|
'bark' = 'bark',
|
||||||
'chat' = 'chat',
|
|
||||||
'telegramBot' = 'telegramBot',
|
'telegramBot' = 'telegramBot',
|
||||||
'dingtalkBot' = 'dingtalkBot',
|
'dingtalkBot' = 'dingtalkBot',
|
||||||
'weWorkBot' = 'weWorkBot',
|
'weWorkBot' = 'weWorkBot',
|
||||||
'weWorkApp' = 'weWorkApp',
|
'weWorkApp' = 'weWorkApp',
|
||||||
'aibotk' = 'aibotk',
|
|
||||||
'iGot' = 'iGot',
|
'iGot' = 'iGot',
|
||||||
'pushPlus' = 'pushPlus',
|
'pushPlus' = 'pushPlus',
|
||||||
'wePlusBot' = 'wePlusBot',
|
|
||||||
'email' = 'email',
|
'email' = 'email',
|
||||||
'pushMe' = 'pushMe',
|
|
||||||
'feishu' = 'feishu',
|
|
||||||
'webhook' = 'webhook',
|
|
||||||
'chronocat' = 'Chronocat',
|
|
||||||
'ntfy' = 'ntfy',
|
|
||||||
'wxPusherBot' = 'wxPusherBot',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class NotificationBaseInfo {
|
abstract class NotificationBaseInfo {
|
||||||
@@ -42,23 +32,10 @@ export class ServerChanNotification extends NotificationBaseInfo {
|
|||||||
public serverChanKey = '';
|
public serverChanKey = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PushDeerNotification extends NotificationBaseInfo {
|
|
||||||
public pushDeerKey = '';
|
|
||||||
public pushDeerUrl = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export class synologyChatNotification extends NotificationBaseInfo {
|
|
||||||
public synologyChatUrl = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export class BarkNotification extends NotificationBaseInfo {
|
export class BarkNotification extends NotificationBaseInfo {
|
||||||
public barkPush = '';
|
public barkPush = '';
|
||||||
public barkIcon = 'https://qn.whyour.cn/logo.png';
|
|
||||||
public barkSound = '';
|
public barkSound = '';
|
||||||
public barkGroup = 'qinglong';
|
public barkGroup = 'qinglong';
|
||||||
public barkLevel = 'active';
|
|
||||||
public barkUrl = '';
|
|
||||||
public barkArchive = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TelegramBotNotification extends NotificationBaseInfo {
|
export class TelegramBotNotification extends NotificationBaseInfo {
|
||||||
@@ -67,7 +44,7 @@ export class TelegramBotNotification extends NotificationBaseInfo {
|
|||||||
public telegramBotProxyHost = '';
|
public telegramBotProxyHost = '';
|
||||||
public telegramBotProxyPort = '';
|
public telegramBotProxyPort = '';
|
||||||
public telegramBotProxyAuth = '';
|
public telegramBotProxyAuth = '';
|
||||||
public telegramBotApiHost = 'https://api.telegram.org';
|
public telegramBotApiHost = 'api.telegram.org';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DingtalkBotNotification extends NotificationBaseInfo {
|
export class DingtalkBotNotification extends NotificationBaseInfo {
|
||||||
@@ -77,18 +54,10 @@ export class DingtalkBotNotification extends NotificationBaseInfo {
|
|||||||
|
|
||||||
export class WeWorkBotNotification extends NotificationBaseInfo {
|
export class WeWorkBotNotification extends NotificationBaseInfo {
|
||||||
public weWorkBotKey = '';
|
public weWorkBotKey = '';
|
||||||
public weWorkOrigin = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WeWorkAppNotification extends NotificationBaseInfo {
|
export class WeWorkAppNotification extends NotificationBaseInfo {
|
||||||
public weWorkAppKey = '';
|
public weWorkAppKey = '';
|
||||||
public weWorkOrigin = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AibotkNotification extends NotificationBaseInfo {
|
|
||||||
public aibotkKey: string = '';
|
|
||||||
public aibotkType: 'room' | 'contact' = 'room';
|
|
||||||
public aibotkName: string = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IGotNotification extends NotificationBaseInfo {
|
export class IGotNotification extends NotificationBaseInfo {
|
||||||
@@ -98,88 +67,23 @@ export class IGotNotification extends NotificationBaseInfo {
|
|||||||
export class PushPlusNotification extends NotificationBaseInfo {
|
export class PushPlusNotification extends NotificationBaseInfo {
|
||||||
public pushPlusToken = '';
|
public pushPlusToken = '';
|
||||||
public pushPlusUser = '';
|
public pushPlusUser = '';
|
||||||
public pushPlusTemplate = '';
|
|
||||||
public pushplusChannel = '';
|
|
||||||
public pushplusWebhook = '';
|
|
||||||
public pushplusCallbackUrl = '';
|
|
||||||
public pushplusTo = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WePlusBotNotification extends NotificationBaseInfo {
|
|
||||||
public wePlusBotToken = '';
|
|
||||||
public wePlusBotReceiver = '';
|
|
||||||
public wePlusBotVersion = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EmailNotification extends NotificationBaseInfo {
|
export class EmailNotification extends NotificationBaseInfo {
|
||||||
public emailService: string = '';
|
public emailService: string = '';
|
||||||
public emailUser: string = '';
|
public emailUser: string = '';
|
||||||
public emailPass: string = '';
|
public emailPass: string = '';
|
||||||
public emailTo: string = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PushMeNotification extends NotificationBaseInfo {
|
|
||||||
public pushMeKey: string = '';
|
|
||||||
public pushMeUrl: string = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ChronocatNotification extends NotificationBaseInfo {
|
|
||||||
public chronocatURL: string = '';
|
|
||||||
public chronocatQQ: string = '';
|
|
||||||
public chronocatToken: string = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WebhookNotification extends NotificationBaseInfo {
|
|
||||||
public webhookHeaders: string = '';
|
|
||||||
public webhookBody: string = '';
|
|
||||||
public webhookUrl: string = '';
|
|
||||||
public webhookMethod: 'GET' | 'POST' | 'PUT' = 'GET';
|
|
||||||
public webhookContentType:
|
|
||||||
| 'application/json'
|
|
||||||
| 'multipart/form-data'
|
|
||||||
| 'application/x-www-form-urlencoded' = 'application/json';
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LarkNotification extends NotificationBaseInfo {
|
|
||||||
public larkKey = '';
|
|
||||||
public larkSecret = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NtfyNotification extends NotificationBaseInfo {
|
|
||||||
public ntfyUrl = '';
|
|
||||||
public ntfyTopic = '';
|
|
||||||
public ntfyPriority = '';
|
|
||||||
public ntfyToken = '';
|
|
||||||
public ntfyUsername = '';
|
|
||||||
public ntfyPassword = '';
|
|
||||||
public ntfyActions = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WxPusherBotNotification extends NotificationBaseInfo {
|
|
||||||
public wxPusherBotAppToken = '';
|
|
||||||
public wxPusherBotTopicIds = '';
|
|
||||||
public wxPusherBotUids = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NotificationInfo
|
export interface NotificationInfo
|
||||||
extends GoCqHttpBotNotification,
|
extends GoCqHttpBotNotification,
|
||||||
GotifyNotification,
|
GotifyNotification,
|
||||||
ServerChanNotification,
|
ServerChanNotification,
|
||||||
PushDeerNotification,
|
|
||||||
synologyChatNotification,
|
|
||||||
BarkNotification,
|
BarkNotification,
|
||||||
TelegramBotNotification,
|
TelegramBotNotification,
|
||||||
DingtalkBotNotification,
|
DingtalkBotNotification,
|
||||||
WeWorkBotNotification,
|
WeWorkBotNotification,
|
||||||
WeWorkAppNotification,
|
WeWorkAppNotification,
|
||||||
AibotkNotification,
|
|
||||||
IGotNotification,
|
IGotNotification,
|
||||||
PushPlusNotification,
|
PushPlusNotification,
|
||||||
WePlusBotNotification,
|
EmailNotification {}
|
||||||
EmailNotification,
|
|
||||||
PushMeNotification,
|
|
||||||
WebhookNotification,
|
|
||||||
ChronocatNotification,
|
|
||||||
LarkNotification,
|
|
||||||
NtfyNotification,
|
|
||||||
WxPusherBotNotification {}
|
|
||||||
|
|||||||
+10
-15
@@ -1,36 +1,31 @@
|
|||||||
import { sequelize } from '.';
|
|
||||||
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
|
||||||
|
|
||||||
export class App {
|
export class App {
|
||||||
name: string;
|
name: string;
|
||||||
scopes: AppScope[];
|
scopes: AppScope[];
|
||||||
client_id: string;
|
client_id: string;
|
||||||
client_secret: string;
|
client_secret: string;
|
||||||
tokens?: AppToken[];
|
tokens?: AppToken[];
|
||||||
id?: number;
|
_id?: string;
|
||||||
|
|
||||||
constructor(options: App) {
|
constructor(options: App) {
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
this.scopes = options.scopes;
|
this.scopes = options.scopes;
|
||||||
this.client_id = options.client_id;
|
this.client_id = options.client_id;
|
||||||
this.client_secret = options.client_secret;
|
this.client_secret = options.client_secret;
|
||||||
this.id = options.id;
|
this._id = options._id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppToken {
|
export interface AppToken {
|
||||||
value: string;
|
value: string;
|
||||||
type?: 'Bearer';
|
type: 'Bearer';
|
||||||
expiration: number;
|
expiration: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs' | 'system';
|
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
|
||||||
|
|
||||||
export interface AppInstance extends Model<App, App>, App {}
|
export enum CrontabStatus {
|
||||||
export const AppModel = sequelize.define<AppInstance>('App', {
|
'running',
|
||||||
name: { type: DataTypes.STRING, unique: 'name' },
|
'idle',
|
||||||
scopes: DataTypes.JSON,
|
'disabled',
|
||||||
client_id: DataTypes.STRING,
|
'queued',
|
||||||
client_secret: DataTypes.STRING,
|
}
|
||||||
tokens: DataTypes.JSON,
|
|
||||||
});
|
|
||||||
|
|||||||
+2
-7
@@ -1,7 +1,7 @@
|
|||||||
export class SockMessage {
|
export class SockMessage {
|
||||||
message?: string;
|
message?: string;
|
||||||
type?: SockMessageType;
|
type?: SockMessageType;
|
||||||
references?: number[];
|
references?: string[];
|
||||||
|
|
||||||
constructor(options: SockMessage) {
|
constructor(options: SockMessage) {
|
||||||
this.type = options.type;
|
this.type = options.type;
|
||||||
@@ -13,10 +13,5 @@ export class SockMessage {
|
|||||||
export type SockMessageType =
|
export type SockMessageType =
|
||||||
| 'ping'
|
| 'ping'
|
||||||
| 'installDependence'
|
| 'installDependence'
|
||||||
| 'uninstallDependence'
|
|
||||||
| 'updateSystemVersion'
|
| 'updateSystemVersion'
|
||||||
| 'manuallyRunScript'
|
| 'manuallyRunScript';
|
||||||
| 'runSubscriptionEnd'
|
|
||||||
| 'reloadSystem'
|
|
||||||
| 'updateNodeMirror'
|
|
||||||
| 'updateLinuxMirror';
|
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
import { sequelize } from '.';
|
|
||||||
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
|
||||||
import { SimpleIntervalSchedule } from 'toad-scheduler';
|
|
||||||
|
|
||||||
type SimpleIntervalScheduleUnit = keyof SimpleIntervalSchedule;
|
|
||||||
export class Subscription {
|
|
||||||
id?: number;
|
|
||||||
name?: string;
|
|
||||||
type?: 'public-repo' | 'private-repo' | 'file';
|
|
||||||
schedule_type?: 'crontab' | 'interval';
|
|
||||||
schedule?: string;
|
|
||||||
interval_schedule?: { type: SimpleIntervalScheduleUnit; value: number };
|
|
||||||
url?: string;
|
|
||||||
whitelist?: string;
|
|
||||||
blacklist?: string;
|
|
||||||
dependences?: string;
|
|
||||||
branch?: string;
|
|
||||||
status?: SubscriptionStatus;
|
|
||||||
pull_type?: 'ssh-key' | 'user-pwd';
|
|
||||||
pull_option?:
|
|
||||||
| { private_key: string }
|
|
||||||
| { username: string; password: string };
|
|
||||||
pid?: number;
|
|
||||||
is_disabled?: 1 | 0;
|
|
||||||
log_path?: string;
|
|
||||||
alias: string;
|
|
||||||
command?: string;
|
|
||||||
extensions?: string;
|
|
||||||
sub_before?: string;
|
|
||||||
sub_after?: string;
|
|
||||||
proxy?: string;
|
|
||||||
autoAddCron?: 1 | 0;
|
|
||||||
autoDelCron?: 1 | 0;
|
|
||||||
|
|
||||||
constructor(options: Subscription) {
|
|
||||||
this.id = options.id;
|
|
||||||
this.name = options.name || options.alias;
|
|
||||||
this.type = options.type;
|
|
||||||
this.schedule = options.schedule;
|
|
||||||
this.status = this.status =
|
|
||||||
typeof options.status === 'number' && SubscriptionStatus[options.status]
|
|
||||||
? options.status
|
|
||||||
: SubscriptionStatus.idle;
|
|
||||||
this.url = options.url;
|
|
||||||
this.whitelist = options.whitelist;
|
|
||||||
this.blacklist = options.blacklist;
|
|
||||||
this.dependences = options.dependences;
|
|
||||||
this.branch = options.branch;
|
|
||||||
this.pull_type = options.pull_type;
|
|
||||||
this.pull_option = options.pull_option;
|
|
||||||
this.pid = options.pid;
|
|
||||||
this.is_disabled = options.is_disabled;
|
|
||||||
this.log_path = options.log_path;
|
|
||||||
this.schedule_type = options.schedule_type;
|
|
||||||
this.alias = options.alias;
|
|
||||||
this.interval_schedule = options.interval_schedule;
|
|
||||||
this.extensions = options.extensions;
|
|
||||||
this.sub_before = options.sub_before;
|
|
||||||
this.sub_after = options.sub_after;
|
|
||||||
this.proxy = options.proxy;
|
|
||||||
this.autoAddCron = options.autoAddCron ? 1 : 0;
|
|
||||||
this.autoDelCron = options.autoDelCron ? 1 : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum SubscriptionStatus {
|
|
||||||
'running',
|
|
||||||
'idle',
|
|
||||||
'disabled',
|
|
||||||
'queued',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SubscriptionInstance
|
|
||||||
extends Model<Subscription, Subscription>,
|
|
||||||
Subscription {}
|
|
||||||
export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
|
|
||||||
'Subscription',
|
|
||||||
{
|
|
||||||
name: {
|
|
||||||
unique: 'compositeIndex',
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
},
|
|
||||||
url: {
|
|
||||||
unique: 'compositeIndex',
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
},
|
|
||||||
schedule: {
|
|
||||||
unique: 'compositeIndex',
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
},
|
|
||||||
interval_schedule: {
|
|
||||||
unique: 'compositeIndex',
|
|
||||||
type: DataTypes.JSON,
|
|
||||||
},
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
whitelist: DataTypes.STRING,
|
|
||||||
blacklist: DataTypes.STRING,
|
|
||||||
status: DataTypes.NUMBER,
|
|
||||||
dependences: DataTypes.STRING,
|
|
||||||
extensions: DataTypes.STRING,
|
|
||||||
sub_before: DataTypes.STRING,
|
|
||||||
sub_after: DataTypes.STRING,
|
|
||||||
branch: DataTypes.STRING,
|
|
||||||
pull_type: DataTypes.STRING,
|
|
||||||
pull_option: DataTypes.JSON,
|
|
||||||
pid: DataTypes.NUMBER,
|
|
||||||
is_disabled: DataTypes.NUMBER,
|
|
||||||
log_path: DataTypes.STRING,
|
|
||||||
schedule_type: DataTypes.STRING,
|
|
||||||
alias: { type: DataTypes.STRING, unique: 'alias' },
|
|
||||||
proxy: { type: DataTypes.STRING, allowNull: true },
|
|
||||||
autoAddCron: { type: DataTypes.NUMBER, allowNull: true },
|
|
||||||
autoDelCron: { type: DataTypes.NUMBER, allowNull: true },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import { sequelize } from '.';
|
|
||||||
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
|
||||||
import { NotificationInfo } from './notify';
|
|
||||||
|
|
||||||
export class SystemInfo {
|
|
||||||
ip?: string;
|
|
||||||
type: AuthDataType;
|
|
||||||
info?: SystemModelInfo;
|
|
||||||
id?: number;
|
|
||||||
|
|
||||||
constructor(options: SystemInfo) {
|
|
||||||
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',
|
|
||||||
'systemConfig' = 'systemConfig',
|
|
||||||
'authConfig' = 'authConfig',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SystemConfigInfo {
|
|
||||||
logRemoveFrequency?: number;
|
|
||||||
cronConcurrency?: number;
|
|
||||||
dependenceProxy?: string;
|
|
||||||
nodeMirror?: string;
|
|
||||||
pythonMirror?: string;
|
|
||||||
linuxMirror?: string;
|
|
||||||
timezone?: string;
|
|
||||||
globalSshKey?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LoginLogInfo {
|
|
||||||
timestamp?: number;
|
|
||||||
address?: string;
|
|
||||||
ip?: string;
|
|
||||||
platform?: string;
|
|
||||||
status?: LoginStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TokenInfo {
|
|
||||||
value: string;
|
|
||||||
timestamp: number;
|
|
||||||
ip: string;
|
|
||||||
address: string;
|
|
||||||
platform: string;
|
|
||||||
/**
|
|
||||||
* Token expiration time in seconds since Unix epoch.
|
|
||||||
* If undefined, the token uses JWT's built-in expiration.
|
|
||||||
*/
|
|
||||||
expiration?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthInfo {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
retries: number;
|
|
||||||
lastlogon: number;
|
|
||||||
lastip: string;
|
|
||||||
lastaddr: string;
|
|
||||||
platform: string;
|
|
||||||
isTwoFactorChecking: boolean;
|
|
||||||
token: string;
|
|
||||||
tokens: Record<string, string | TokenInfo[]>;
|
|
||||||
twoFactorActivated: boolean;
|
|
||||||
twoFactorSecret: string;
|
|
||||||
avatar: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SystemModelInfo = SystemConfigInfo &
|
|
||||||
Partial<NotificationInfo> &
|
|
||||||
LoginLogInfo &
|
|
||||||
Partial<AuthInfo>;
|
|
||||||
|
|
||||||
export interface SystemInstance
|
|
||||||
extends Model<SystemInfo, SystemInfo>,
|
|
||||||
SystemInfo {}
|
|
||||||
export const SystemModel = sequelize.define<SystemInstance>('Auth', {
|
|
||||||
ip: DataTypes.STRING,
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
info: {
|
|
||||||
type: DataTypes.JSON,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
declare namespace Express {
|
||||||
|
interface Request {
|
||||||
|
platform: 'desktop' | 'mobile';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
export enum ScheduleType {
|
|
||||||
BOOT = '@boot',
|
|
||||||
ONCE = '@once',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ScheduleValidator = (schedule?: string) => boolean;
|
|
||||||
export type CronSchedulerPayload = {
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
schedule: string;
|
|
||||||
command: string;
|
|
||||||
extra_schedules: Array<{ schedule: string }>;
|
|
||||||
};
|
|
||||||
+11
-17
@@ -1,28 +1,22 @@
|
|||||||
import expressLoader from './express';
|
import expressLoader from './express';
|
||||||
import depInjectorLoader from './depInjector';
|
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 { Application } from 'express';
|
||||||
import linkDeps from './deps';
|
import linkDeps from './deps';
|
||||||
import initTask from './initTask';
|
|
||||||
import initFile from './initFile';
|
|
||||||
|
|
||||||
export default async ({ app }: { app: Application }) => {
|
export default async ({ expressApp }: { expressApp: Application }) => {
|
||||||
depInjectorLoader();
|
await dependencyInjectorLoader({
|
||||||
Logger.info('✌️ Dependency loaded');
|
models: [],
|
||||||
|
});
|
||||||
|
Logger.info('✌️ Dependency Injector loaded');
|
||||||
|
|
||||||
await linkDeps();
|
await expressLoader({ app: expressApp });
|
||||||
Logger.info('✌️ Link deps loaded');
|
Logger.info('✌️ Express loaded');
|
||||||
|
|
||||||
initFile();
|
|
||||||
Logger.info('✌️ Init file loaded');
|
|
||||||
|
|
||||||
await initData();
|
await initData();
|
||||||
Logger.info('✌️ Init data loaded');
|
Logger.info('✌️ init data loaded');
|
||||||
|
|
||||||
initTask();
|
await linkDeps();
|
||||||
Logger.info('✌️ Init task loaded');
|
Logger.info('✌️ link deps');
|
||||||
|
|
||||||
expressLoader({ app });
|
|
||||||
Logger.info('✌️ Express loaded');
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import Container from 'typedi';
|
|
||||||
import CronService from '../services/cron';
|
|
||||||
|
|
||||||
export default async () => {
|
|
||||||
const cronService = Container.get(CronService);
|
|
||||||
|
|
||||||
await cronService.bootTask();
|
|
||||||
};
|
|
||||||
+51
-47
@@ -1,59 +1,63 @@
|
|||||||
|
import DataStore from 'nedb';
|
||||||
|
import config from '../config';
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
import { EnvModel } from '../data/env';
|
import fs from 'fs';
|
||||||
import { CrontabModel } from '../data/cron';
|
import { fileExist } from '../config/util';
|
||||||
import { DependenceModel } from '../data/dependence';
|
|
||||||
import { AppModel } from '../data/open';
|
|
||||||
import { SystemModel } from '../data/system';
|
|
||||||
import { SubscriptionModel } from '../data/subscription';
|
|
||||||
import { CrontabViewModel } from '../data/cronView';
|
|
||||||
import { sequelize } from '../data';
|
|
||||||
|
|
||||||
export default async () => {
|
interface Dbs {
|
||||||
try {
|
cronDb: DataStore;
|
||||||
await CrontabModel.sync();
|
dependenceDb: DataStore;
|
||||||
await DependenceModel.sync();
|
envDb: DataStore;
|
||||||
await AppModel.sync();
|
appDb: DataStore;
|
||||||
await SystemModel.sync();
|
authDb: DataStore;
|
||||||
await EnvModel.sync();
|
}
|
||||||
await SubscriptionModel.sync();
|
|
||||||
await CrontabViewModel.sync();
|
|
||||||
|
|
||||||
// 初始化新增字段
|
const db: Dbs = {} as any;
|
||||||
const migrations = [
|
|
||||||
{
|
async function truncateDb() {
|
||||||
table: 'CrontabViews',
|
return new Promise(async (resolve) => {
|
||||||
column: 'filterRelation',
|
const files = [
|
||||||
type: 'VARCHAR(255)',
|
config.cronDbFile,
|
||||||
},
|
config.dependenceDbFile,
|
||||||
{ table: 'Subscriptions', column: 'proxy', type: 'VARCHAR(255)' },
|
config.envDbFile,
|
||||||
{ table: 'CrontabViews', column: 'type', type: 'NUMBER' },
|
config.appDbFile,
|
||||||
{ table: 'Subscriptions', column: 'autoAddCron', type: 'NUMBER' },
|
config.authDbFile,
|
||||||
{ table: 'Subscriptions', column: 'autoDelCron', type: 'NUMBER' },
|
|
||||||
{ table: 'Crontabs', column: 'sub_id', type: 'NUMBER' },
|
|
||||||
{ table: 'Crontabs', column: 'extra_schedules', type: 'JSON' },
|
|
||||||
{ table: 'Crontabs', column: 'task_before', type: 'TEXT' },
|
|
||||||
{ table: 'Crontabs', column: 'task_after', type: 'TEXT' },
|
|
||||||
{ table: 'Crontabs', column: 'log_name', type: 'VARCHAR(255)' },
|
|
||||||
{
|
|
||||||
table: 'Crontabs',
|
|
||||||
column: 'allow_multiple_instances',
|
|
||||||
type: 'NUMBER',
|
|
||||||
},
|
|
||||||
{ table: 'Envs', column: 'isPinned', type: 'NUMBER' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const migration of migrations) {
|
for (const file of files) {
|
||||||
try {
|
const _fileExist = await fileExist(file);
|
||||||
await sequelize.query(
|
if (_fileExist && fs.statSync(file).size >= 1024 * 1024 * 500) {
|
||||||
`alter table ${migration.table} add column ${migration.column} ${migration.type}`,
|
fs.truncateSync(file, 1024 * 1024 * 500);
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
// Column already exists or other error, continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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');
|
Logger.info('✌️ DB loaded');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error('✌️ DB load failed', error);
|
Logger.info('✌️ DB load failed');
|
||||||
|
Logger.info(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const dbs: Dbs = db;
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import LoggerInstance from './logger';
|
import LoggerInstance from './logger';
|
||||||
|
|
||||||
export default () => {
|
export default ({ models }: { models: { name: string; model: any }[] }) => {
|
||||||
try {
|
try {
|
||||||
|
models.forEach((m) => {
|
||||||
|
Container.set(m.name, m.model);
|
||||||
|
});
|
||||||
|
|
||||||
Container.set('logger', LoggerInstance);
|
Container.set('logger', LoggerInstance);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
LoggerInstance.error('🔥 Error on dependency injector loader: %o', e);
|
LoggerInstance.error('🔥 Error on dependency injector loader: %o', e);
|
||||||
+11
-55
@@ -1,67 +1,23 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs';
|
||||||
import os from 'os';
|
|
||||||
import chokidar from 'chokidar';
|
import chokidar from 'chokidar';
|
||||||
import config from '../config/index';
|
import config from '../config/index';
|
||||||
import Logger from './logger';
|
|
||||||
|
|
||||||
async function linkToNodeModule(src: string, dst?: string) {
|
function linkToNodeModule(src: string, dst?: string) {
|
||||||
const target = path.join(config.rootPath, 'node_modules', dst || src);
|
const target = path.join(config.rootPath, 'node_modules', dst || src);
|
||||||
const source = path.join(config.rootPath, src);
|
const source = path.join(config.rootPath, src);
|
||||||
|
|
||||||
try {
|
fs.lstat(target, (err, stat) => {
|
||||||
const stats = await fs.lstat(target);
|
if (!stat) {
|
||||||
if (!stats) {
|
fs.symlink(source, target, 'dir', (err) => {
|
||||||
await fs.symlink(source, target, 'dir');
|
if (err) throw err;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) { }
|
});
|
||||||
}
|
|
||||||
|
|
||||||
async function linkCommand() {
|
|
||||||
const homeDir = os.homedir();
|
|
||||||
let userBinDir = path.join(homeDir, 'bin');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fs.mkdir(userBinDir, { recursive: true });
|
|
||||||
await linkCommandToDir(userBinDir);
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error('Linking command failed:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function linkCommandToDir(commandDir: string) {
|
|
||||||
const linkShell = [
|
|
||||||
{
|
|
||||||
src: 'update.sh',
|
|
||||||
dest: 'ql',
|
|
||||||
tmp: 'ql_tmp',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: 'task.sh',
|
|
||||||
dest: 'task',
|
|
||||||
tmp: 'task_tmp',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const link of linkShell) {
|
|
||||||
const source = path.join(config.rootPath, 'shell', link.src);
|
|
||||||
const target = path.join(commandDir, link.dest);
|
|
||||||
const tmpTarget = path.join(commandDir, link.tmp);
|
|
||||||
try {
|
|
||||||
const stats = await fs.lstat(tmpTarget);
|
|
||||||
if (stats) {
|
|
||||||
await fs.unlink(tmpTarget);
|
|
||||||
}
|
|
||||||
} catch (error) { }
|
|
||||||
|
|
||||||
await fs.symlink(source, tmpTarget);
|
|
||||||
await fs.rename(tmpTarget, target);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async (src: string = 'deps') => {
|
export default async (src: string = 'deps') => {
|
||||||
await linkCommand();
|
linkToNodeModule(src);
|
||||||
await linkToNodeModule(src);
|
|
||||||
|
|
||||||
const source = path.join(config.rootPath, src);
|
const source = path.join(config.rootPath, src);
|
||||||
const watcher = chokidar.watch(source, {
|
const watcher = chokidar.watch(source, {
|
||||||
@@ -70,6 +26,6 @@ export default async (src: string = 'deps') => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
watcher
|
watcher
|
||||||
.on('add', () => linkToNodeModule(src))
|
.on('add', (path) => linkToNodeModule(src))
|
||||||
.on('change', () => linkToNodeModule(src));
|
.on('change', (path) => linkToNodeModule(src));
|
||||||
};
|
};
|
||||||
|
|||||||
+57
-79
@@ -1,42 +1,41 @@
|
|||||||
import express, { Request, Response, NextFunction, Application } from 'express';
|
import { Request, Response, NextFunction, Application } from 'express';
|
||||||
import bodyParser from 'body-parser';
|
import bodyParser from 'body-parser';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import routes from '../api';
|
import routes from '../api';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { UnauthorizedError, expressjwt } from 'express-jwt';
|
import jwt from 'express-jwt';
|
||||||
import { getPlatform, getToken } from '../config/util';
|
import fs from 'fs';
|
||||||
|
import { getFileContentByName, getPlatform, getToken } from '../config/util';
|
||||||
|
import Container from 'typedi';
|
||||||
|
import OpenService from '../services/open';
|
||||||
import rewrite from 'express-urlrewrite';
|
import rewrite from 'express-urlrewrite';
|
||||||
import { errors } from 'celebrate';
|
import UserService from '../services/user';
|
||||||
import { serveEnv } from '../config/serverEnv';
|
import handler from 'serve-handler';
|
||||||
import { IKeyvStore, shareStore } from '../shared/store';
|
import * as Sentry from '@sentry/node';
|
||||||
import { isValidToken } from '../shared/auth';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
export default ({ app }: { app: Application }) => {
|
export default ({ app }: { app: Application }) => {
|
||||||
app.set('trust proxy', 'loopback');
|
app.enable('trust proxy');
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
|
||||||
// Rewrite URLs to strip baseUrl prefix if configured
|
|
||||||
// This allows the rest of the app to work without baseUrl awareness
|
|
||||||
if (config.baseUrl) {
|
|
||||||
app.use(rewrite(`${config.baseUrl}/*`, '/$1'));
|
|
||||||
}
|
|
||||||
|
|
||||||
app.get(`${config.api.prefix}/env.js`, serveEnv);
|
|
||||||
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
|
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (req.path.startsWith('/api') || req.path.startsWith('/open')) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
return handler(req, res, {
|
||||||
|
public: '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 }));
|
||||||
|
|
||||||
const frontendPath = path.join(config.rootPath, 'static/dist');
|
|
||||||
app.use(express.static(frontendPath));
|
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
expressjwt({
|
jwt({
|
||||||
secret: config.jwt.secret,
|
secret: config.secret as string,
|
||||||
algorithms: ['HS384'],
|
algorithms: ['HS384'],
|
||||||
}).unless({
|
}).unless({
|
||||||
path: [...config.apiWhiteList, /^\/(?!api\/).*/],
|
path: [...config.apiWhiteList, /^\/open\//],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -50,17 +49,11 @@ export default ({ app }: { app: Application }) => {
|
|||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(async (req: Request, res, next) => {
|
app.use(async (req, res, next) => {
|
||||||
if (!['/open/', '/api/'].some((x) => req.path.startsWith(x))) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerToken = getToken(req);
|
const headerToken = getToken(req);
|
||||||
if (req.path.startsWith('/open/')) {
|
if (req.path.startsWith('/open/')) {
|
||||||
const apps = await shareStore.getApps();
|
const openService = Container.get(OpenService);
|
||||||
const doc = apps?.filter((x) =>
|
const doc = await openService.findTokenByValue(headerToken);
|
||||||
x.tokens?.find((y) => y.value === headerToken),
|
|
||||||
)?.[0];
|
|
||||||
if (doc && doc.tokens && doc.tokens.length > 0) {
|
if (doc && doc.tokens && doc.tokens.length > 0) {
|
||||||
const currentToken = doc.tokens.find((x) => x.value === headerToken);
|
const currentToken = doc.tokens.find((x) => x.value === headerToken);
|
||||||
const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/);
|
const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/);
|
||||||
@@ -75,40 +68,49 @@ export default ({ app }: { app: Application }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const originPath = `${req.baseUrl}${req.path === '/' ? '' : req.path}`;
|
|
||||||
if (
|
if (
|
||||||
!headerToken &&
|
!headerToken &&
|
||||||
originPath &&
|
req.path &&
|
||||||
config.apiWhiteList.includes(originPath)
|
config.apiWhiteList.includes(req.path) &&
|
||||||
|
req.path !== '/api/crons/status'
|
||||||
|
) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
const remoteAddress = req.socket.remoteAddress;
|
||||||
|
if (
|
||||||
|
remoteAddress === '::ffff:127.0.0.1' &&
|
||||||
|
req.path === '/api/crons/status'
|
||||||
) {
|
) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
const authInfo = await shareStore.getAuthInfo();
|
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||||
if (isValidToken(authInfo, headerToken, req.platform)) {
|
if (data) {
|
||||||
return next();
|
const { token = '', tokens = {} } = JSON.parse(data);
|
||||||
|
if (headerToken === token || tokens[req.platform] === headerToken) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorCode = headerToken ? 'invalid_token' : 'credentials_required';
|
const err: any = new Error('UnauthorizedError');
|
||||||
const errorMessage = headerToken
|
err.status = 401;
|
||||||
? 'jwt malformed'
|
|
||||||
: 'No authorization token was found';
|
|
||||||
const err = new UnauthorizedError(errorCode, { message: errorMessage });
|
|
||||||
next(err);
|
next(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(async (req, res, next) => {
|
app.use(async (req, res, next) => {
|
||||||
if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) {
|
if (!['/api/init/user', '/api/init/notification'].includes(req.path)) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
const authInfo =
|
const userService = Container.get(UserService);
|
||||||
(await shareStore.getAuthInfo()) || ({} as IKeyvStore['authInfo']);
|
const authInfo = await userService.getUserInfo();
|
||||||
|
const envDbContent = getFileContentByName(config.envDbFile);
|
||||||
|
|
||||||
let isInitialized = true;
|
let isInitialized = true;
|
||||||
if (
|
if (
|
||||||
Object.keys(authInfo).length === 2 &&
|
Object.keys(authInfo).length === 2 &&
|
||||||
authInfo.username === 'admin' &&
|
authInfo.username === 'admin' &&
|
||||||
authInfo.password === 'admin'
|
authInfo.password === 'admin' &&
|
||||||
|
envDbContent.length === 0
|
||||||
) {
|
) {
|
||||||
isInitialized = false;
|
isInitialized = false;
|
||||||
}
|
}
|
||||||
@@ -123,18 +125,13 @@ export default ({ app }: { app: Application }) => {
|
|||||||
app.use(rewrite('/open/*', '/api/$1'));
|
app.use(rewrite('/open/*', '/api/$1'));
|
||||||
app.use(config.api.prefix, routes());
|
app.use(config.api.prefix, routes());
|
||||||
|
|
||||||
app.get('*', (_, res, next) => {
|
app.use((req, res, next) => {
|
||||||
const indexPath = path.join(frontendPath, 'index.html');
|
const err: any = new Error('Not Found');
|
||||||
res.sendFile(indexPath, (err) => {
|
err['status'] = 404;
|
||||||
if (err) {
|
next(err);
|
||||||
const err: any = new Error('Not Found');
|
|
||||||
err['status'] = 404;
|
|
||||||
next(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(errors());
|
app.use(Sentry.Handlers.errorHandler());
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
(
|
(
|
||||||
@@ -153,27 +150,6 @@ export default ({ app }: { app: Application }) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
app.use(
|
|
||||||
(
|
|
||||||
err: Error & { errors: any[] },
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction,
|
|
||||||
) => {
|
|
||||||
if (err.name.includes('Sequelize')) {
|
|
||||||
return res
|
|
||||||
.status(500)
|
|
||||||
.send({
|
|
||||||
code: 400,
|
|
||||||
message: `${err.message}`,
|
|
||||||
errors: err.errors,
|
|
||||||
})
|
|
||||||
.end();
|
|
||||||
}
|
|
||||||
return next(err);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
(
|
(
|
||||||
err: Error & { status: number },
|
err: Error & { status: number },
|
||||||
@@ -181,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,
|
||||||
|
|||||||
+67
-199
@@ -1,223 +1,91 @@
|
|||||||
import DependenceService from '../services/dependence';
|
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, CrontabModel, 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 { DependenceModel, DependenceStatus } from '../data/dependence';
|
import _ from 'lodash';
|
||||||
import { Op } from 'sequelize';
|
import { dbs } from '../loaders/db';
|
||||||
import config from '../config';
|
|
||||||
import { CrontabViewModel, CronViewType } from '../data/cronView';
|
|
||||||
import { initPosition } from '../data/env';
|
|
||||||
import { AuthDataType, SystemModel } from '../data/system';
|
|
||||||
import SystemService from '../services/system';
|
|
||||||
import UserService from '../services/user';
|
|
||||||
import { writeFile, readFile } from 'fs/promises';
|
|
||||||
import { createRandomString, fileExist, safeJSONParse } from '../config/util';
|
|
||||||
import OpenService from '../services/open';
|
|
||||||
import { shareStore } from '../shared/store';
|
|
||||||
import Logger from './logger';
|
|
||||||
import { AppModel } from '../data/open';
|
|
||||||
|
|
||||||
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 dependenceService = Container.get(DependenceService);
|
const dependenceService = Container.get(DependenceService);
|
||||||
const systemService = Container.get(SystemService);
|
const cronDb = dbs.cronDb;
|
||||||
const userService = Container.get(UserService);
|
const dependenceDb = dbs.dependenceDb;
|
||||||
const openService = Container.get(OpenService);
|
|
||||||
|
|
||||||
// 初始化增加系统配置
|
|
||||||
let systemApp = (
|
|
||||||
await AppModel.findOne({
|
|
||||||
where: { name: 'system' },
|
|
||||||
})
|
|
||||||
)?.get({ plain: true });
|
|
||||||
if (!systemApp) {
|
|
||||||
systemApp = await AppModel.create({
|
|
||||||
name: 'system',
|
|
||||||
scopes: ['crons', 'system'],
|
|
||||||
client_id: createRandomString(12, 12),
|
|
||||||
client_secret: createRandomString(24, 24),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const [systemConfig] = await SystemModel.findOrCreate({
|
|
||||||
where: { type: AuthDataType.systemConfig },
|
|
||||||
});
|
|
||||||
await SystemModel.findOrCreate({
|
|
||||||
where: { type: AuthDataType.notification },
|
|
||||||
});
|
|
||||||
const [authConfig] = await SystemModel.findOrCreate({
|
|
||||||
where: { type: AuthDataType.authConfig },
|
|
||||||
});
|
|
||||||
if (!authConfig?.info) {
|
|
||||||
let authInfo = {
|
|
||||||
username: 'admin',
|
|
||||||
password: 'admin',
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const authFileExist = await fileExist(config.authConfigFile);
|
|
||||||
if (authFileExist) {
|
|
||||||
const content = await readFile(config.authConfigFile, 'utf8');
|
|
||||||
authInfo = safeJSONParse(content);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
Logger.warn('Failed to read auth config file, using default credentials');
|
|
||||||
}
|
|
||||||
await SystemModel.upsert({
|
|
||||||
id: authConfig?.id,
|
|
||||||
info: authInfo,
|
|
||||||
type: AuthDataType.authConfig,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const installDependencies = async () => {
|
|
||||||
const docs = await DependenceModel.findAll({
|
|
||||||
where: {},
|
|
||||||
order: [
|
|
||||||
['type', 'DESC'],
|
|
||||||
['createdAt', 'DESC'],
|
|
||||||
],
|
|
||||||
raw: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await DependenceModel.update(
|
|
||||||
{ status: DependenceStatus.queued, log: [] },
|
|
||||||
{ where: { id: docs.map((x) => x.id!) } },
|
|
||||||
);
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
|
||||||
await dependenceService.installDependenceOneByOne(docs);
|
|
||||||
|
|
||||||
const bootAfterLoader = await import('./bootAfter');
|
|
||||||
bootAfterLoader.default();
|
|
||||||
}, 5000);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始化更新 linux/python/nodejs 镜像源配置
|
|
||||||
if (systemConfig.info?.pythonMirror) {
|
|
||||||
systemService.updatePythonMirror({
|
|
||||||
pythonMirror: systemConfig.info?.pythonMirror,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (systemConfig.info?.linuxMirror) {
|
|
||||||
systemService.updateLinuxMirror(
|
|
||||||
{
|
|
||||||
linuxMirror: systemConfig.info?.linuxMirror,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
() => installDependencies(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
installDependencies();
|
|
||||||
}
|
|
||||||
if (systemConfig.info?.nodeMirror) {
|
|
||||||
systemService.updateNodeMirror({
|
|
||||||
nodeMirror: systemConfig.info?.nodeMirror,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化新增默认全部任务视图
|
|
||||||
CrontabViewModel.findAll({
|
|
||||||
where: { type: CronViewType.系统, name: '全部任务' },
|
|
||||||
raw: true,
|
|
||||||
}).then((docs) => {
|
|
||||||
if (docs.length === 0) {
|
|
||||||
CrontabViewModel.create({
|
|
||||||
name: '全部任务',
|
|
||||||
type: CronViewType.系统,
|
|
||||||
position: initPosition / 2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化更新所有任务状态为空闲
|
// 初始化更新所有任务状态为空闲
|
||||||
await CrontabModel.update({ status: CrontabStatus.idle }, { where: {} });
|
cronDb.update(
|
||||||
|
{ status: { $in: [CrontabStatus.running, CrontabStatus.queued] } },
|
||||||
|
{ $set: { status: CrontabStatus.idle } },
|
||||||
|
{ multi: true },
|
||||||
|
);
|
||||||
|
|
||||||
// 初始化时执行一次所有的 ql repo 任务
|
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
|
||||||
CrontabModel.findAll({
|
dependenceDb.find({ status: { $in: [0, 1, 2] } }).exec(async (err, docs) => {
|
||||||
where: {
|
const groups = _.groupBy(docs, 'type');
|
||||||
isDisabled: { [Op.ne]: 1 },
|
for (const key in groups) {
|
||||||
command: {
|
if (Object.prototype.hasOwnProperty.call(groups, key)) {
|
||||||
[Op.or]: [{ [Op.like]: `%ql repo%` }, { [Op.like]: `%ql raw%` }],
|
const group = groups[key];
|
||||||
},
|
const depIds = group.map((x) => x._id);
|
||||||
},
|
for (const dep of depIds) {
|
||||||
}).then((docs) => {
|
await dependenceService.reInstall([dep]);
|
||||||
for (let i = 0; i < docs.length; i++) {
|
}
|
||||||
const doc = docs[i];
|
|
||||||
if (doc) {
|
|
||||||
exec(doc.command);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新2.11.3以前的脚本路径
|
// 初始化时执行一次所有的ql repo 任务
|
||||||
CrontabModel.findAll({
|
cronDb
|
||||||
where: {
|
.find({
|
||||||
command: {
|
command: /ql (repo|raw)/,
|
||||||
[Op.or]: [
|
isDisabled: { $ne: 1 },
|
||||||
{ [Op.like]: `%\/${config.rootPath}\/scripts\/%` },
|
})
|
||||||
{ [Op.like]: `%\/${config.rootPath}\/config\/%` },
|
.exec((err, docs) => {
|
||||||
{ [Op.like]: `%\/${config.rootPath}\/log\/%` },
|
for (let i = 0; i < docs.length; i++) {
|
||||||
{ [Op.like]: `%\/${config.rootPath}\/db\/%` },
|
const doc = docs[i];
|
||||||
],
|
if (doc) {
|
||||||
},
|
exec(doc.command);
|
||||||
},
|
|
||||||
}).then(async (docs) => {
|
|
||||||
for (let i = 0; i < docs.length; i++) {
|
|
||||||
const doc = docs[i];
|
|
||||||
if (doc) {
|
|
||||||
if (doc.command.includes(`${config.rootPath}/scripts/`)) {
|
|
||||||
await CrontabModel.update(
|
|
||||||
{ command: doc.command.replace(`${config.rootPath}/scripts/`, '') },
|
|
||||||
{ where: { id: doc.id } },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (doc.command.includes(`${config.rootPath}/log/`)) {
|
|
||||||
await CrontabModel.update(
|
|
||||||
{
|
|
||||||
command: `${config.dataPath}/log/${doc.command.replace(
|
|
||||||
`${config.rootPath}/log/`,
|
|
||||||
'',
|
|
||||||
)}`,
|
|
||||||
},
|
|
||||||
{ where: { id: doc.id } },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (doc.command.includes(`${config.rootPath}/config/`)) {
|
|
||||||
await CrontabModel.update(
|
|
||||||
{
|
|
||||||
command: `${config.dataPath}/config/${doc.command.replace(
|
|
||||||
`${config.rootPath}/config/`,
|
|
||||||
'',
|
|
||||||
)}`,
|
|
||||||
},
|
|
||||||
{ where: { id: doc.id } },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (doc.command.includes(`${config.rootPath}/db/`)) {
|
|
||||||
await CrontabModel.update(
|
|
||||||
{
|
|
||||||
command: `${config.dataPath}/db/${doc.command.replace(
|
|
||||||
`${config.rootPath}/db/`,
|
|
||||||
'',
|
|
||||||
)}`,
|
|
||||||
},
|
|
||||||
{ where: { id: doc.id } },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
// patch 禁用状态字段改变
|
||||||
|
cronDb
|
||||||
|
.find({
|
||||||
|
status: CrontabStatus.disabled,
|
||||||
|
})
|
||||||
|
.exec((err, docs) => {
|
||||||
|
if (docs.length > 0) {
|
||||||
|
const ids = docs.map((x) => x._id);
|
||||||
|
cronDb.update(
|
||||||
|
{ _id: { $in: ids } },
|
||||||
|
{ $set: { status: CrontabStatus.idle, isDisabled: 1 } },
|
||||||
|
{ multi: true },
|
||||||
|
(err) => {
|
||||||
|
cronService.autosave_crontab();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 初始化保存一次ck和定时任务数据
|
// 初始化保存一次ck和定时任务数据
|
||||||
await cronService.autosave_crontab();
|
await cronService.autosave_crontab();
|
||||||
await envService.set_envs();
|
await envService.set_envs();
|
||||||
|
|
||||||
const authInfo = await userService.getAuthInfo();
|
|
||||||
const apps = await openService.findApps();
|
|
||||||
await shareStore.updateAuthInfo(authInfo);
|
|
||||||
if (apps?.length) {
|
|
||||||
await shareStore.updateApps(apps);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function randomSchedule(from: number, to: number) {
|
||||||
|
const result: any[] = [];
|
||||||
|
const arr = [...Array(from).keys()];
|
||||||
|
let count = arr.length;
|
||||||
|
for (let i = 0; i < to; i++) {
|
||||||
|
const index = ~~(Math.random() * count) + i;
|
||||||
|
if (result.includes(arr[index])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result[i] = arr[index];
|
||||||
|
arr[index] = arr[i];
|
||||||
|
count--;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|||||||
+34
-115
@@ -1,127 +1,46 @@
|
|||||||
import fs from 'fs/promises';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import os from 'os';
|
import dotenv from 'dotenv';
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
import { fileExist } from '../config/util';
|
import { fileExist } from '../config/util';
|
||||||
import { writeFileWithLock } from '../shared/utils';
|
|
||||||
|
|
||||||
const rootPath = process.env.QL_DIR as string;
|
const rootPath = process.cwd();
|
||||||
let dataPath = path.join(rootPath, 'data/');
|
const confFile = path.join(rootPath, 'config/config.sh');
|
||||||
|
const sampleConfigFile = path.join(rootPath, 'sample/config.sample.sh');
|
||||||
if (process.env.QL_DATA_DIR) {
|
const sampleAuthFile = path.join(rootPath, 'sample/auth.sample.json');
|
||||||
dataPath = process.env.QL_DATA_DIR.replace(/\/$/g, '');
|
const authConfigFile = path.join(rootPath, 'config/auth.json');
|
||||||
}
|
const configPath = path.join(rootPath, 'config/');
|
||||||
|
const scriptPath = path.join(rootPath, 'scripts/');
|
||||||
const preloadPath = path.join(rootPath, 'shell/preload/');
|
const logPath = path.join(rootPath, 'log/');
|
||||||
const configPath = path.join(dataPath, 'config/');
|
|
||||||
const scriptPath = path.join(dataPath, 'scripts/');
|
|
||||||
const logPath = path.join(dataPath, 'log/');
|
|
||||||
const uploadPath = path.join(dataPath, 'upload/');
|
|
||||||
const bakPath = path.join(dataPath, 'bak/');
|
|
||||||
const samplePath = path.join(rootPath, 'sample/');
|
|
||||||
const tmpPath = path.join(logPath, '.tmp/');
|
|
||||||
const confFile = path.join(configPath, 'config.sh');
|
|
||||||
const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
|
|
||||||
const sampleTaskShellFile = path.join(samplePath, 'task.sample.sh');
|
|
||||||
const sampleNotifyJsFile = path.join(samplePath, 'notify.js');
|
|
||||||
const sampleNotifyPyFile = path.join(samplePath, 'notify.py');
|
|
||||||
const scriptNotifyJsFile = path.join(scriptPath, 'sendNotify.js');
|
|
||||||
const scriptNotifyPyFile = path.join(scriptPath, 'notify.py');
|
|
||||||
const jsNotifyFile = path.join(preloadPath, '__ql_notify__.js');
|
|
||||||
const pyNotifyFile = path.join(preloadPath, '__ql_notify__.py');
|
|
||||||
const TaskBeforeFile = path.join(configPath, 'task_before.sh');
|
|
||||||
const TaskBeforeJsFile = path.join(configPath, 'task_before.js');
|
|
||||||
const TaskBeforePyFile = path.join(configPath, 'task_before.py');
|
|
||||||
const TaskAfterFile = path.join(configPath, 'task_after.sh');
|
|
||||||
const homedir = os.homedir();
|
|
||||||
const sshPath = path.resolve(homedir, '.ssh');
|
|
||||||
const sshdPath = path.join(dataPath, 'ssh.d');
|
|
||||||
const systemLogPath = path.join(dataPath, 'syslog');
|
|
||||||
|
|
||||||
const directories = [
|
|
||||||
configPath,
|
|
||||||
scriptPath,
|
|
||||||
preloadPath,
|
|
||||||
logPath,
|
|
||||||
tmpPath,
|
|
||||||
uploadPath,
|
|
||||||
sshPath,
|
|
||||||
bakPath,
|
|
||||||
sshdPath,
|
|
||||||
systemLogPath,
|
|
||||||
];
|
|
||||||
|
|
||||||
const files = [
|
|
||||||
{
|
|
||||||
target: confFile,
|
|
||||||
source: sampleConfigFile,
|
|
||||||
checkExistence: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: jsNotifyFile,
|
|
||||||
source: sampleNotifyJsFile,
|
|
||||||
checkExistence: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: pyNotifyFile,
|
|
||||||
source: sampleNotifyPyFile,
|
|
||||||
checkExistence: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: scriptNotifyJsFile,
|
|
||||||
source: sampleNotifyJsFile,
|
|
||||||
checkExistence: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: scriptNotifyPyFile,
|
|
||||||
source: sampleNotifyPyFile,
|
|
||||||
checkExistence: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: TaskBeforeFile,
|
|
||||||
source: sampleTaskShellFile,
|
|
||||||
checkExistence: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: TaskBeforeJsFile,
|
|
||||||
content:
|
|
||||||
'// The JavaScript code that executes before the JavaScript task execution will execute.',
|
|
||||||
checkExistence: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: TaskBeforePyFile,
|
|
||||||
content:
|
|
||||||
'# The Python code that executes before the Python task execution will execute.',
|
|
||||||
checkExistence: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: TaskAfterFile,
|
|
||||||
source: sampleTaskShellFile,
|
|
||||||
checkExistence: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
for (const dirPath of directories) {
|
const authFileExist = await fileExist(authConfigFile);
|
||||||
if (!(await fileExist(dirPath))) {
|
const confFileExist = await fileExist(confFile);
|
||||||
await fs.mkdir(dirPath);
|
const scriptDirExist = await fileExist(scriptPath);
|
||||||
}
|
const logDirExist = await fileExist(logPath);
|
||||||
|
const configDirExist = await fileExist(configPath);
|
||||||
|
|
||||||
|
if (!configDirExist) {
|
||||||
|
fs.mkdirSync(configPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const item of files) {
|
if (!authFileExist) {
|
||||||
const exists = await fileExist(item.target);
|
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
|
||||||
if (!item.checkExistence || !exists) {
|
|
||||||
if (!item.content && !item.source) {
|
|
||||||
throw new Error(
|
|
||||||
`Neither content nor source specified for ${item.target}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const content =
|
|
||||||
item.content ||
|
|
||||||
(await fs.readFile(item.source!, { encoding: 'utf-8' }));
|
|
||||||
await writeFileWithLock(item.target, content);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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');
|
Logger.info('✌️ Init file down');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
import { Container } from 'typedi';
|
|
||||||
import SystemService from '../services/system';
|
|
||||||
import ScheduleService, { ScheduleTaskType } from '../services/schedule';
|
|
||||||
import SubscriptionService from '../services/subscription';
|
|
||||||
import SshKeyService from '../services/sshKey';
|
|
||||||
import config from '../config';
|
|
||||||
import { fileExist } from '../config/util';
|
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
export default async () => {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
const scheduleService = Container.get(ScheduleService);
|
|
||||||
const subscriptionService = Container.get(SubscriptionService);
|
|
||||||
const sshKeyService = Container.get(SshKeyService);
|
|
||||||
|
|
||||||
// 生成内置token
|
|
||||||
let tokenCommand = `ts-node-transpile-only ${join(
|
|
||||||
config.rootPath,
|
|
||||||
'back/token.ts',
|
|
||||||
)}`;
|
|
||||||
const tokenFile = join(config.rootPath, 'static/build/token.js');
|
|
||||||
|
|
||||||
if (await fileExist(tokenFile)) {
|
|
||||||
tokenCommand = `node ${tokenFile}`;
|
|
||||||
}
|
|
||||||
const cron = {
|
|
||||||
id: NaN,
|
|
||||||
name: '生成token',
|
|
||||||
command: tokenCommand,
|
|
||||||
runOrigin: 'system',
|
|
||||||
} as ScheduleTaskType;
|
|
||||||
await scheduleService.cancelIntervalTask(cron);
|
|
||||||
scheduleService.createIntervalTask(
|
|
||||||
cron,
|
|
||||||
{
|
|
||||||
days: 28,
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 运行删除日志任务
|
|
||||||
const data = await systemService.getSystemConfig();
|
|
||||||
if (data && data.info) {
|
|
||||||
if (data.info.logRemoveFrequency) {
|
|
||||||
const rmlogCron = {
|
|
||||||
id: data.id as number,
|
|
||||||
name: '删除日志',
|
|
||||||
command: `ql rmlog ${data.info.logRemoveFrequency}`,
|
|
||||||
runOrigin: 'system' as const,
|
|
||||||
};
|
|
||||||
await scheduleService.cancelIntervalTask(rmlogCron);
|
|
||||||
scheduleService.createIntervalTask(
|
|
||||||
rmlogCron,
|
|
||||||
{
|
|
||||||
days: data.info.logRemoveFrequency,
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
systemService.updateTimezone(data.info);
|
|
||||||
|
|
||||||
// Apply global SSH key if configured
|
|
||||||
if (data.info.globalSshKey) {
|
|
||||||
await sshKeyService.addGlobalSSHKey(data.info.globalSshKey, 'global');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await subscriptionService.setSshConfig();
|
|
||||||
const subs = await subscriptionService.list();
|
|
||||||
for (const sub of subs) {
|
|
||||||
subscriptionService.handleTask(sub.get({ plain: true }), !sub.is_disabled);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
+23
-54
@@ -1,63 +1,32 @@
|
|||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import 'winston-daily-rotate-file';
|
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
const levelMap: Record<string, string> = {
|
const transports = [];
|
||||||
info: 'ℹ️', // info图标
|
if (process.env.NODE_ENV !== 'development') {
|
||||||
warn: '⚠️', // 警告图标
|
transports.push(new winston.transports.Console());
|
||||||
error: '❌', // 错误图标
|
} else {
|
||||||
debug: '🐛', // debug调试图标
|
transports.push(
|
||||||
};
|
new winston.transports.Console({
|
||||||
|
format: winston.format.combine(
|
||||||
const baseFormat = [
|
winston.format.cli(),
|
||||||
winston.format.splat(),
|
winston.format.splat(),
|
||||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
),
|
||||||
winston.format.align(),
|
}),
|
||||||
];
|
);
|
||||||
|
}
|
||||||
const consoleFormat = winston.format.combine(
|
|
||||||
winston.format.colorize({ level: true }),
|
|
||||||
...baseFormat,
|
|
||||||
winston.format.printf((info) => {
|
|
||||||
return `[${info.level} ${info.timestamp}]:${info.message}`;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const plainFormat = winston.format.combine(
|
|
||||||
winston.format.uncolorize(),
|
|
||||||
...baseFormat,
|
|
||||||
winston.format.printf((info) => {
|
|
||||||
return `[${levelMap[info.level] || ''}${info.level} ${info.timestamp}]:${
|
|
||||||
info.message
|
|
||||||
}`;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const consoleTransport = new winston.transports.Console({
|
|
||||||
format: consoleFormat,
|
|
||||||
level: 'debug',
|
|
||||||
});
|
|
||||||
|
|
||||||
const fileTransport = new winston.transports.DailyRotateFile({
|
|
||||||
filename: path.join(config.systemLogPath, '%DATE%.log'),
|
|
||||||
datePattern: 'YYYY-MM-DD',
|
|
||||||
maxSize: '20m',
|
|
||||||
maxFiles: '7d',
|
|
||||||
format: plainFormat,
|
|
||||||
level: config.logs.level || 'info',
|
|
||||||
});
|
|
||||||
|
|
||||||
const LoggerInstance = winston.createLogger({
|
const LoggerInstance = winston.createLogger({
|
||||||
level: 'debug',
|
level: config.logs.level,
|
||||||
levels: winston.config.npm.levels,
|
levels: winston.config.npm.levels,
|
||||||
transports: [consoleTransport, fileTransport],
|
format: winston.format.combine(
|
||||||
exceptionHandlers: [consoleTransport, fileTransport],
|
winston.format.timestamp({
|
||||||
rejectionHandlers: [consoleTransport, fileTransport],
|
format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
});
|
}),
|
||||||
|
winston.format.errors({ stack: true }),
|
||||||
LoggerInstance.on('error', (error) => {
|
winston.format.splat(),
|
||||||
console.error('Logger error:', error);
|
winston.format.json(),
|
||||||
|
),
|
||||||
|
transports,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default LoggerInstance;
|
export default LoggerInstance;
|
||||||
|
|||||||
@@ -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');
|
||||||
|
};
|
||||||
@@ -6,11 +6,11 @@ export default async ({ server }: { server: Server }) => {
|
|||||||
await Sock({ server });
|
await Sock({ server });
|
||||||
Logger.info('✌️ Sock loaded');
|
Logger.info('✌️ Sock loaded');
|
||||||
|
|
||||||
process.on('uncaughtException', (error) => {
|
process.on('SIGINT', () => {
|
||||||
Logger.error('Uncaught exception:', error);
|
Logger.info('✌️ Server need close');
|
||||||
});
|
server.close(() => {
|
||||||
|
Logger.info('✌️ Server closed');
|
||||||
process.on('unhandledRejection', (reason, promise) => {
|
process.exit(0);
|
||||||
Logger.error('Unhandled rejection:', reason, promise);
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+21
-16
@@ -1,37 +1,42 @@
|
|||||||
import sockJs from 'sockjs';
|
import sockJs from 'sockjs';
|
||||||
import { Server } from 'http';
|
import { Server } from 'http';
|
||||||
|
import Logger from './logger';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import SockService from '../services/sock';
|
import SockService from '../services/sock';
|
||||||
|
import config from '../config/index';
|
||||||
|
import fs from 'fs';
|
||||||
import { getPlatform } from '../config/util';
|
import { getPlatform } from '../config/util';
|
||||||
import { shareStore } from '../shared/store';
|
|
||||||
import { isValidToken } from '../shared/auth';
|
|
||||||
import config from '../config';
|
|
||||||
|
|
||||||
export default async ({ server }: { server: Server }) => {
|
export default async ({ server }: { server: Server }) => {
|
||||||
const echo = sockJs.createServer({ prefix: `${config.baseUrl}/api/ws`, log: () => { } });
|
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
|
||||||
const sockService = Container.get(SockService);
|
const sockService = Container.get(SockService);
|
||||||
|
|
||||||
echo.on('connection', async (conn) => {
|
echo.on('connection', (conn) => {
|
||||||
if (!conn.headers || !conn.url || !conn.pathname) {
|
if (!conn.headers || !conn.url || !conn.pathname) {
|
||||||
conn.close('404');
|
conn.close('404');
|
||||||
}
|
}
|
||||||
|
|
||||||
const authInfo = await shareStore.getAuthInfo();
|
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||||
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
|
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
|
||||||
const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
|
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);
|
||||||
|
|
||||||
if (isValidToken(authInfo, headerToken, platform)) {
|
conn.on('data', (message) => {
|
||||||
sockService.addClient(conn);
|
conn.write(message);
|
||||||
|
});
|
||||||
|
|
||||||
conn.on('data', (message) => {
|
conn.on('close', function () {
|
||||||
conn.write(message);
|
sockService.removeClient(conn);
|
||||||
});
|
});
|
||||||
|
|
||||||
conn.on('close', function () {
|
return;
|
||||||
sockService.removeClient(conn);
|
} else {
|
||||||
});
|
conn.write(JSON.stringify({ type: 'ping', message: 'whyour' }));
|
||||||
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.close('404');
|
conn.close('404');
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
|
||||||
import Logger from '../loaders/logger';
|
|
||||||
import { performance } from 'perf_hooks';
|
|
||||||
import { metricsService } from '../services/metrics';
|
|
||||||
|
|
||||||
interface RequestMetrics {
|
|
||||||
method: string;
|
|
||||||
path: string;
|
|
||||||
duration: number;
|
|
||||||
statusCode: number;
|
|
||||||
timestamp: number;
|
|
||||||
platform?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestMetrics: RequestMetrics[] = [];
|
|
||||||
|
|
||||||
export const monitoringMiddleware = (
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction,
|
|
||||||
) => {
|
|
||||||
const start = performance.now();
|
|
||||||
const originalEnd = res.end;
|
|
||||||
|
|
||||||
res.end = function (chunk?: any, encoding?: any, cb?: any) {
|
|
||||||
const duration = performance.now() - start;
|
|
||||||
const metric: RequestMetrics = {
|
|
||||||
method: req.method,
|
|
||||||
path: req.path,
|
|
||||||
duration,
|
|
||||||
statusCode: res.statusCode,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
platform: req.platform,
|
|
||||||
};
|
|
||||||
|
|
||||||
requestMetrics.push(metric);
|
|
||||||
metricsService.record('http_request', duration, {
|
|
||||||
method: req.method,
|
|
||||||
path: req.path,
|
|
||||||
statusCode: res.statusCode.toString(),
|
|
||||||
...(req.platform && { platform: req.platform }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (requestMetrics.length > 1000) {
|
|
||||||
requestMetrics.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (duration > 1000) {
|
|
||||||
Logger.warn(
|
|
||||||
`Slow request detected: ${req.method} ${
|
|
||||||
req.path
|
|
||||||
} took ${duration.toFixed(2)}ms`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return originalEnd.call(this, chunk, encoding, cb);
|
|
||||||
};
|
|
||||||
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMetrics = () => {
|
|
||||||
return {
|
|
||||||
totalRequests: requestMetrics.length,
|
|
||||||
averageDuration:
|
|
||||||
requestMetrics.reduce((acc, curr) => acc + curr.duration, 0) /
|
|
||||||
requestMetrics.length,
|
|
||||||
requestsByMethod: requestMetrics.reduce((acc, curr) => {
|
|
||||||
acc[curr.method] = (acc[curr.method] || 0) + 1;
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string, number>),
|
|
||||||
requestsByPlatform: requestMetrics.reduce((acc, curr) => {
|
|
||||||
if (curr.platform) {
|
|
||||||
acc[curr.platform] = (acc[curr.platform] || 0) + 1;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string, number>),
|
|
||||||
recentRequests: requestMetrics.slice(-10),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
package com.ql.api;
|
|
||||||
|
|
||||||
message EnvItem {
|
|
||||||
optional int32 id = 1;
|
|
||||||
optional string name = 2;
|
|
||||||
optional string value = 3;
|
|
||||||
optional string remarks = 4;
|
|
||||||
optional int32 status = 5;
|
|
||||||
optional int64 position = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
message GetEnvsRequest { string searchValue = 1; }
|
|
||||||
|
|
||||||
message CreateEnvRequest { repeated EnvItem envs = 1; }
|
|
||||||
|
|
||||||
message UpdateEnvRequest { EnvItem env = 1; }
|
|
||||||
|
|
||||||
message DeleteEnvsRequest { repeated int32 ids = 1; }
|
|
||||||
|
|
||||||
message MoveEnvRequest {
|
|
||||||
int32 id = 1;
|
|
||||||
int32 fromIndex = 2;
|
|
||||||
int32 toIndex = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message DisableEnvsRequest { repeated int32 ids = 1; }
|
|
||||||
|
|
||||||
message EnableEnvsRequest { repeated int32 ids = 1; }
|
|
||||||
|
|
||||||
message UpdateEnvNamesRequest {
|
|
||||||
repeated int32 ids = 1;
|
|
||||||
string name = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message GetEnvByIdRequest { int32 id = 1; }
|
|
||||||
|
|
||||||
message EnvsResponse {
|
|
||||||
int32 code = 1;
|
|
||||||
repeated EnvItem data = 2;
|
|
||||||
optional string message = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message EnvResponse {
|
|
||||||
int32 code = 1;
|
|
||||||
EnvItem data = 2;
|
|
||||||
optional string message = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Response {
|
|
||||||
int32 code = 1;
|
|
||||||
optional string message = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ExtraScheduleItem { string schedule = 1; }
|
|
||||||
|
|
||||||
message CronItem {
|
|
||||||
optional int32 id = 1;
|
|
||||||
optional string command = 2;
|
|
||||||
optional string schedule = 3;
|
|
||||||
optional string name = 4;
|
|
||||||
repeated string labels = 5;
|
|
||||||
optional int32 sub_id = 6;
|
|
||||||
repeated ExtraScheduleItem extra_schedules = 7;
|
|
||||||
optional string task_before = 8;
|
|
||||||
optional string task_after = 9;
|
|
||||||
optional int32 status = 10;
|
|
||||||
optional string log_path = 11;
|
|
||||||
optional int32 pid = 12;
|
|
||||||
optional int64 last_running_time = 13;
|
|
||||||
optional int64 last_execution_time = 14;
|
|
||||||
}
|
|
||||||
|
|
||||||
message CreateCronRequest {
|
|
||||||
string command = 1;
|
|
||||||
string schedule = 2;
|
|
||||||
optional string name = 3;
|
|
||||||
repeated string labels = 4;
|
|
||||||
optional int32 sub_id = 5;
|
|
||||||
repeated ExtraScheduleItem extra_schedules = 6;
|
|
||||||
optional string task_before = 7;
|
|
||||||
optional string task_after = 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
message UpdateCronRequest {
|
|
||||||
int32 id = 1;
|
|
||||||
optional string command = 2;
|
|
||||||
optional string schedule = 3;
|
|
||||||
optional string name = 4;
|
|
||||||
repeated string labels = 5;
|
|
||||||
optional int32 sub_id = 6;
|
|
||||||
repeated ExtraScheduleItem extra_schedules = 7;
|
|
||||||
optional string task_before = 8;
|
|
||||||
optional string task_after = 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
message DeleteCronsRequest { repeated int32 ids = 1; }
|
|
||||||
|
|
||||||
message GetCronsRequest {
|
|
||||||
optional string searchValue = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message GetCronByIdRequest { int32 id = 1; }
|
|
||||||
|
|
||||||
message EnableCronsRequest { repeated int32 ids = 1; }
|
|
||||||
|
|
||||||
message DisableCronsRequest { repeated int32 ids = 1; }
|
|
||||||
|
|
||||||
message RunCronsRequest { repeated int32 ids = 1; }
|
|
||||||
|
|
||||||
message CronsResponse {
|
|
||||||
int32 code = 1;
|
|
||||||
repeated CronItem data = 2;
|
|
||||||
optional string message = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message CronResponse {
|
|
||||||
int32 code = 1;
|
|
||||||
CronItem data = 2;
|
|
||||||
optional string message = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message CronDetailRequest { string log_path = 1; }
|
|
||||||
|
|
||||||
message CronDetailResponse {
|
|
||||||
int32 code = 1;
|
|
||||||
CronItem data = 2;
|
|
||||||
optional string message = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum NotificationMode {
|
|
||||||
gotify = 0;
|
|
||||||
goCqHttpBot = 1;
|
|
||||||
serverChan = 2;
|
|
||||||
pushDeer = 3;
|
|
||||||
bark = 4;
|
|
||||||
chat = 5;
|
|
||||||
telegramBot = 6;
|
|
||||||
dingtalkBot = 7;
|
|
||||||
weWorkBot = 8;
|
|
||||||
weWorkApp = 9;
|
|
||||||
aibotk = 10;
|
|
||||||
iGot = 11;
|
|
||||||
pushPlus = 12;
|
|
||||||
wePlusBot = 13;
|
|
||||||
email = 14;
|
|
||||||
pushMe = 15;
|
|
||||||
feishu = 16;
|
|
||||||
webhook = 17;
|
|
||||||
chronocat = 18;
|
|
||||||
ntfy = 19;
|
|
||||||
wxPusherBot = 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
message NotificationInfo {
|
|
||||||
NotificationMode type = 1;
|
|
||||||
|
|
||||||
optional string gotifyUrl = 2;
|
|
||||||
optional string gotifyToken = 3;
|
|
||||||
optional int32 gotifyPriority = 4;
|
|
||||||
|
|
||||||
optional string goCqHttpBotUrl = 5;
|
|
||||||
optional string goCqHttpBotToken = 6;
|
|
||||||
optional string goCqHttpBotQq = 7;
|
|
||||||
|
|
||||||
optional string serverChanKey = 8;
|
|
||||||
|
|
||||||
optional string pushDeerKey = 9;
|
|
||||||
optional string pushDeerUrl = 10;
|
|
||||||
|
|
||||||
optional string synologyChatUrl = 11;
|
|
||||||
|
|
||||||
optional string barkPush = 12;
|
|
||||||
optional string barkIcon = 13;
|
|
||||||
optional string barkSound = 14;
|
|
||||||
optional string barkGroup = 15;
|
|
||||||
optional string barkLevel = 16;
|
|
||||||
optional string barkUrl = 17;
|
|
||||||
optional string barkArchive = 18;
|
|
||||||
|
|
||||||
optional string telegramBotToken = 19;
|
|
||||||
optional string telegramBotUserId = 20;
|
|
||||||
optional string telegramBotProxyHost = 21;
|
|
||||||
optional string telegramBotProxyPort = 22;
|
|
||||||
optional string telegramBotProxyAuth = 23;
|
|
||||||
optional string telegramBotApiHost = 24;
|
|
||||||
|
|
||||||
optional string dingtalkBotToken = 25;
|
|
||||||
optional string dingtalkBotSecret = 26;
|
|
||||||
|
|
||||||
optional string weWorkBotKey = 27;
|
|
||||||
optional string weWorkOrigin = 28;
|
|
||||||
|
|
||||||
optional string weWorkAppKey = 29;
|
|
||||||
|
|
||||||
optional string aibotkKey = 30;
|
|
||||||
optional string aibotkType = 31;
|
|
||||||
optional string aibotkName = 32;
|
|
||||||
|
|
||||||
optional string iGotPushKey = 33;
|
|
||||||
|
|
||||||
optional string pushPlusToken = 34;
|
|
||||||
optional string pushPlusUser = 35;
|
|
||||||
optional string pushPlusTemplate = 36;
|
|
||||||
optional string pushplusChannel = 37;
|
|
||||||
optional string pushplusWebhook = 38;
|
|
||||||
optional string pushplusCallbackUrl = 39;
|
|
||||||
optional string pushplusTo = 40;
|
|
||||||
|
|
||||||
optional string wePlusBotToken = 41;
|
|
||||||
optional string wePlusBotReceiver = 42;
|
|
||||||
optional string wePlusBotVersion = 43;
|
|
||||||
|
|
||||||
optional string emailService = 44;
|
|
||||||
optional string emailUser = 45;
|
|
||||||
optional string emailPass = 46;
|
|
||||||
optional string emailTo = 47;
|
|
||||||
|
|
||||||
optional string pushMeKey = 48;
|
|
||||||
optional string pushMeUrl = 49;
|
|
||||||
|
|
||||||
optional string chronocatURL = 50;
|
|
||||||
optional string chronocatQQ = 51;
|
|
||||||
optional string chronocatToken = 52;
|
|
||||||
|
|
||||||
optional string webhookHeaders = 53;
|
|
||||||
optional string webhookBody = 54;
|
|
||||||
optional string webhookUrl = 55;
|
|
||||||
optional string webhookMethod = 56;
|
|
||||||
optional string webhookContentType = 57;
|
|
||||||
|
|
||||||
optional string larkKey = 58;
|
|
||||||
optional string larkSecret = 69;
|
|
||||||
|
|
||||||
optional string ntfyUrl = 59;
|
|
||||||
optional string ntfyTopic = 60;
|
|
||||||
optional string ntfyPriority = 61;
|
|
||||||
optional string ntfyToken = 62;
|
|
||||||
optional string ntfyUsername = 63;
|
|
||||||
optional string ntfyPassword = 64;
|
|
||||||
optional string ntfyActions = 65;
|
|
||||||
|
|
||||||
optional string wxPusherBotAppToken = 66;
|
|
||||||
optional string wxPusherBotTopicIds = 67;
|
|
||||||
optional string wxPusherBotUids = 68;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SystemNotifyRequest {
|
|
||||||
string title = 1;
|
|
||||||
string content = 2;
|
|
||||||
optional NotificationInfo notificationInfo = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
service Api {
|
|
||||||
rpc GetEnvs(GetEnvsRequest) returns (EnvsResponse) {}
|
|
||||||
rpc CreateEnv(CreateEnvRequest) returns (EnvsResponse) {}
|
|
||||||
rpc UpdateEnv(UpdateEnvRequest) returns (EnvResponse) {}
|
|
||||||
rpc DeleteEnvs(DeleteEnvsRequest) returns (Response) {}
|
|
||||||
rpc MoveEnv(MoveEnvRequest) returns (EnvResponse) {}
|
|
||||||
rpc DisableEnvs(DisableEnvsRequest) returns (Response) {}
|
|
||||||
rpc EnableEnvs(EnableEnvsRequest) returns (Response) {}
|
|
||||||
rpc UpdateEnvNames(UpdateEnvNamesRequest) returns (Response) {}
|
|
||||||
rpc GetEnvById(GetEnvByIdRequest) returns (EnvResponse) {}
|
|
||||||
rpc SystemNotify(SystemNotifyRequest) returns (Response) {}
|
|
||||||
rpc GetCronDetail(CronDetailRequest) returns (CronDetailResponse) {}
|
|
||||||
rpc CreateCron(CreateCronRequest) returns (CronResponse) {}
|
|
||||||
rpc UpdateCron(UpdateCronRequest) returns (CronResponse) {}
|
|
||||||
rpc DeleteCrons(DeleteCronsRequest) returns (Response) {}
|
|
||||||
rpc GetCrons(GetCronsRequest) returns (CronsResponse) {}
|
|
||||||
rpc GetCronById(GetCronByIdRequest) returns (CronResponse) {}
|
|
||||||
rpc EnableCrons(EnableCronsRequest) returns (Response) {}
|
|
||||||
rpc DisableCrons(DisableCronsRequest) returns (Response) {}
|
|
||||||
rpc RunCrons(RunCronsRequest) returns (Response) {}
|
|
||||||
}
|
|
||||||
-4740
File diff suppressed because it is too large
Load Diff
@@ -1,26 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
package com.ql.cron;
|
|
||||||
|
|
||||||
service Cron {
|
|
||||||
rpc addCron(AddCronRequest) returns (AddCronResponse);
|
|
||||||
rpc delCron(DeleteCronRequest) returns (DeleteCronResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
message ISchedule { string schedule = 1; }
|
|
||||||
|
|
||||||
message ICron {
|
|
||||||
string id = 1;
|
|
||||||
string schedule = 2;
|
|
||||||
string command = 3;
|
|
||||||
repeated ISchedule extra_schedules = 4;
|
|
||||||
string name = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
message AddCronRequest { repeated ICron crons = 1; }
|
|
||||||
|
|
||||||
message AddCronResponse {}
|
|
||||||
|
|
||||||
message DeleteCronRequest { repeated string ids = 1; }
|
|
||||||
|
|
||||||
message DeleteCronResponse {}
|
|
||||||
@@ -1,525 +0,0 @@
|
|||||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// protoc-gen-ts_proto v2.6.1
|
|
||||||
// protoc v3.21.12
|
|
||||||
// source: back/protos/cron.proto
|
|
||||||
|
|
||||||
/* eslint-disable */
|
|
||||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
|
||||||
import {
|
|
||||||
type CallOptions,
|
|
||||||
ChannelCredentials,
|
|
||||||
Client,
|
|
||||||
type ClientOptions,
|
|
||||||
type ClientUnaryCall,
|
|
||||||
type handleUnaryCall,
|
|
||||||
makeGenericClientConstructor,
|
|
||||||
Metadata,
|
|
||||||
type ServiceError,
|
|
||||||
type UntypedServiceImplementation,
|
|
||||||
} from "@grpc/grpc-js";
|
|
||||||
|
|
||||||
export const protobufPackage = "com.ql.cron";
|
|
||||||
|
|
||||||
export interface ISchedule {
|
|
||||||
schedule: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICron {
|
|
||||||
id: string;
|
|
||||||
schedule: string;
|
|
||||||
command: string;
|
|
||||||
extra_schedules: ISchedule[];
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AddCronRequest {
|
|
||||||
crons: ICron[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AddCronResponse {
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DeleteCronRequest {
|
|
||||||
ids: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DeleteCronResponse {
|
|
||||||
}
|
|
||||||
|
|
||||||
function createBaseISchedule(): ISchedule {
|
|
||||||
return { schedule: "" };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ISchedule: MessageFns<ISchedule> = {
|
|
||||||
encode(message: ISchedule, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
|
||||||
if (message.schedule !== "") {
|
|
||||||
writer.uint32(10).string(message.schedule);
|
|
||||||
}
|
|
||||||
return writer;
|
|
||||||
},
|
|
||||||
|
|
||||||
decode(input: BinaryReader | Uint8Array, length?: number): ISchedule {
|
|
||||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
|
||||||
const message = createBaseISchedule();
|
|
||||||
while (reader.pos < end) {
|
|
||||||
const tag = reader.uint32();
|
|
||||||
switch (tag >>> 3) {
|
|
||||||
case 1: {
|
|
||||||
if (tag !== 10) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.schedule = reader.string();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
reader.skip(tag & 7);
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
|
|
||||||
fromJSON(object: any): ISchedule {
|
|
||||||
return { schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : "" };
|
|
||||||
},
|
|
||||||
|
|
||||||
toJSON(message: ISchedule): unknown {
|
|
||||||
const obj: any = {};
|
|
||||||
if (message.schedule !== "") {
|
|
||||||
obj.schedule = message.schedule;
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
},
|
|
||||||
|
|
||||||
create<I extends Exact<DeepPartial<ISchedule>, I>>(base?: I): ISchedule {
|
|
||||||
return ISchedule.fromPartial(base ?? ({} as any));
|
|
||||||
},
|
|
||||||
fromPartial<I extends Exact<DeepPartial<ISchedule>, I>>(object: I): ISchedule {
|
|
||||||
const message = createBaseISchedule();
|
|
||||||
message.schedule = object.schedule ?? "";
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function createBaseICron(): ICron {
|
|
||||||
return { id: "", schedule: "", command: "", extra_schedules: [], name: "" };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ICron: MessageFns<ICron> = {
|
|
||||||
encode(message: ICron, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
|
||||||
if (message.id !== "") {
|
|
||||||
writer.uint32(10).string(message.id);
|
|
||||||
}
|
|
||||||
if (message.schedule !== "") {
|
|
||||||
writer.uint32(18).string(message.schedule);
|
|
||||||
}
|
|
||||||
if (message.command !== "") {
|
|
||||||
writer.uint32(26).string(message.command);
|
|
||||||
}
|
|
||||||
for (const v of message.extra_schedules) {
|
|
||||||
ISchedule.encode(v!, writer.uint32(34).fork()).join();
|
|
||||||
}
|
|
||||||
if (message.name !== "") {
|
|
||||||
writer.uint32(42).string(message.name);
|
|
||||||
}
|
|
||||||
return writer;
|
|
||||||
},
|
|
||||||
|
|
||||||
decode(input: BinaryReader | Uint8Array, length?: number): ICron {
|
|
||||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
|
||||||
const message = createBaseICron();
|
|
||||||
while (reader.pos < end) {
|
|
||||||
const tag = reader.uint32();
|
|
||||||
switch (tag >>> 3) {
|
|
||||||
case 1: {
|
|
||||||
if (tag !== 10) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.id = reader.string();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
case 2: {
|
|
||||||
if (tag !== 18) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.schedule = reader.string();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
case 3: {
|
|
||||||
if (tag !== 26) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.command = reader.string();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
case 4: {
|
|
||||||
if (tag !== 34) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.extra_schedules.push(ISchedule.decode(reader, reader.uint32()));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
case 5: {
|
|
||||||
if (tag !== 42) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.name = reader.string();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
reader.skip(tag & 7);
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
|
|
||||||
fromJSON(object: any): ICron {
|
|
||||||
return {
|
|
||||||
id: isSet(object.id) ? globalThis.String(object.id) : "",
|
|
||||||
schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : "",
|
|
||||||
command: isSet(object.command) ? globalThis.String(object.command) : "",
|
|
||||||
extra_schedules: globalThis.Array.isArray(object?.extra_schedules)
|
|
||||||
? object.extra_schedules.map((e: any) => ISchedule.fromJSON(e))
|
|
||||||
: [],
|
|
||||||
name: isSet(object.name) ? globalThis.String(object.name) : "",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
toJSON(message: ICron): unknown {
|
|
||||||
const obj: any = {};
|
|
||||||
if (message.id !== "") {
|
|
||||||
obj.id = message.id;
|
|
||||||
}
|
|
||||||
if (message.schedule !== "") {
|
|
||||||
obj.schedule = message.schedule;
|
|
||||||
}
|
|
||||||
if (message.command !== "") {
|
|
||||||
obj.command = message.command;
|
|
||||||
}
|
|
||||||
if (message.extra_schedules?.length) {
|
|
||||||
obj.extra_schedules = message.extra_schedules.map((e) => ISchedule.toJSON(e));
|
|
||||||
}
|
|
||||||
if (message.name !== "") {
|
|
||||||
obj.name = message.name;
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
},
|
|
||||||
|
|
||||||
create<I extends Exact<DeepPartial<ICron>, I>>(base?: I): ICron {
|
|
||||||
return ICron.fromPartial(base ?? ({} as any));
|
|
||||||
},
|
|
||||||
fromPartial<I extends Exact<DeepPartial<ICron>, I>>(object: I): ICron {
|
|
||||||
const message = createBaseICron();
|
|
||||||
message.id = object.id ?? "";
|
|
||||||
message.schedule = object.schedule ?? "";
|
|
||||||
message.command = object.command ?? "";
|
|
||||||
message.extra_schedules = object.extra_schedules?.map((e) => ISchedule.fromPartial(e)) || [];
|
|
||||||
message.name = object.name ?? "";
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function createBaseAddCronRequest(): AddCronRequest {
|
|
||||||
return { crons: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AddCronRequest: MessageFns<AddCronRequest> = {
|
|
||||||
encode(message: AddCronRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
|
||||||
for (const v of message.crons) {
|
|
||||||
ICron.encode(v!, writer.uint32(10).fork()).join();
|
|
||||||
}
|
|
||||||
return writer;
|
|
||||||
},
|
|
||||||
|
|
||||||
decode(input: BinaryReader | Uint8Array, length?: number): AddCronRequest {
|
|
||||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
|
||||||
const message = createBaseAddCronRequest();
|
|
||||||
while (reader.pos < end) {
|
|
||||||
const tag = reader.uint32();
|
|
||||||
switch (tag >>> 3) {
|
|
||||||
case 1: {
|
|
||||||
if (tag !== 10) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.crons.push(ICron.decode(reader, reader.uint32()));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
reader.skip(tag & 7);
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
|
|
||||||
fromJSON(object: any): AddCronRequest {
|
|
||||||
return { crons: globalThis.Array.isArray(object?.crons) ? object.crons.map((e: any) => ICron.fromJSON(e)) : [] };
|
|
||||||
},
|
|
||||||
|
|
||||||
toJSON(message: AddCronRequest): unknown {
|
|
||||||
const obj: any = {};
|
|
||||||
if (message.crons?.length) {
|
|
||||||
obj.crons = message.crons.map((e) => ICron.toJSON(e));
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
},
|
|
||||||
|
|
||||||
create<I extends Exact<DeepPartial<AddCronRequest>, I>>(base?: I): AddCronRequest {
|
|
||||||
return AddCronRequest.fromPartial(base ?? ({} as any));
|
|
||||||
},
|
|
||||||
fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(object: I): AddCronRequest {
|
|
||||||
const message = createBaseAddCronRequest();
|
|
||||||
message.crons = object.crons?.map((e) => ICron.fromPartial(e)) || [];
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function createBaseAddCronResponse(): AddCronResponse {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AddCronResponse: MessageFns<AddCronResponse> = {
|
|
||||||
encode(_: AddCronResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
|
||||||
return writer;
|
|
||||||
},
|
|
||||||
|
|
||||||
decode(input: BinaryReader | Uint8Array, length?: number): AddCronResponse {
|
|
||||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
|
||||||
const message = createBaseAddCronResponse();
|
|
||||||
while (reader.pos < end) {
|
|
||||||
const tag = reader.uint32();
|
|
||||||
switch (tag >>> 3) {
|
|
||||||
}
|
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
reader.skip(tag & 7);
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
|
|
||||||
fromJSON(_: any): AddCronResponse {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
|
|
||||||
toJSON(_: AddCronResponse): unknown {
|
|
||||||
const obj: any = {};
|
|
||||||
return obj;
|
|
||||||
},
|
|
||||||
|
|
||||||
create<I extends Exact<DeepPartial<AddCronResponse>, I>>(base?: I): AddCronResponse {
|
|
||||||
return AddCronResponse.fromPartial(base ?? ({} as any));
|
|
||||||
},
|
|
||||||
fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(_: I): AddCronResponse {
|
|
||||||
const message = createBaseAddCronResponse();
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function createBaseDeleteCronRequest(): DeleteCronRequest {
|
|
||||||
return { ids: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DeleteCronRequest: MessageFns<DeleteCronRequest> = {
|
|
||||||
encode(message: DeleteCronRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
|
||||||
for (const v of message.ids) {
|
|
||||||
writer.uint32(10).string(v!);
|
|
||||||
}
|
|
||||||
return writer;
|
|
||||||
},
|
|
||||||
|
|
||||||
decode(input: BinaryReader | Uint8Array, length?: number): DeleteCronRequest {
|
|
||||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
|
||||||
const message = createBaseDeleteCronRequest();
|
|
||||||
while (reader.pos < end) {
|
|
||||||
const tag = reader.uint32();
|
|
||||||
switch (tag >>> 3) {
|
|
||||||
case 1: {
|
|
||||||
if (tag !== 10) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.ids.push(reader.string());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
reader.skip(tag & 7);
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
|
|
||||||
fromJSON(object: any): DeleteCronRequest {
|
|
||||||
return { ids: globalThis.Array.isArray(object?.ids) ? object.ids.map((e: any) => globalThis.String(e)) : [] };
|
|
||||||
},
|
|
||||||
|
|
||||||
toJSON(message: DeleteCronRequest): unknown {
|
|
||||||
const obj: any = {};
|
|
||||||
if (message.ids?.length) {
|
|
||||||
obj.ids = message.ids;
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
},
|
|
||||||
|
|
||||||
create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(base?: I): DeleteCronRequest {
|
|
||||||
return DeleteCronRequest.fromPartial(base ?? ({} as any));
|
|
||||||
},
|
|
||||||
fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(object: I): DeleteCronRequest {
|
|
||||||
const message = createBaseDeleteCronRequest();
|
|
||||||
message.ids = object.ids?.map((e) => e) || [];
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function createBaseDeleteCronResponse(): DeleteCronResponse {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DeleteCronResponse: MessageFns<DeleteCronResponse> = {
|
|
||||||
encode(_: DeleteCronResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
|
||||||
return writer;
|
|
||||||
},
|
|
||||||
|
|
||||||
decode(input: BinaryReader | Uint8Array, length?: number): DeleteCronResponse {
|
|
||||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
|
||||||
const message = createBaseDeleteCronResponse();
|
|
||||||
while (reader.pos < end) {
|
|
||||||
const tag = reader.uint32();
|
|
||||||
switch (tag >>> 3) {
|
|
||||||
}
|
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
reader.skip(tag & 7);
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
|
|
||||||
fromJSON(_: any): DeleteCronResponse {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
|
|
||||||
toJSON(_: DeleteCronResponse): unknown {
|
|
||||||
const obj: any = {};
|
|
||||||
return obj;
|
|
||||||
},
|
|
||||||
|
|
||||||
create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(base?: I): DeleteCronResponse {
|
|
||||||
return DeleteCronResponse.fromPartial(base ?? ({} as any));
|
|
||||||
},
|
|
||||||
fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(_: I): DeleteCronResponse {
|
|
||||||
const message = createBaseDeleteCronResponse();
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CronService = typeof CronService;
|
|
||||||
export const CronService = {
|
|
||||||
addCron: {
|
|
||||||
path: "/com.ql.cron.Cron/addCron",
|
|
||||||
requestStream: false,
|
|
||||||
responseStream: false,
|
|
||||||
requestSerialize: (value: AddCronRequest) => Buffer.from(AddCronRequest.encode(value).finish()),
|
|
||||||
requestDeserialize: (value: Buffer) => AddCronRequest.decode(value),
|
|
||||||
responseSerialize: (value: AddCronResponse) => Buffer.from(AddCronResponse.encode(value).finish()),
|
|
||||||
responseDeserialize: (value: Buffer) => AddCronResponse.decode(value),
|
|
||||||
},
|
|
||||||
delCron: {
|
|
||||||
path: "/com.ql.cron.Cron/delCron",
|
|
||||||
requestStream: false,
|
|
||||||
responseStream: false,
|
|
||||||
requestSerialize: (value: DeleteCronRequest) => Buffer.from(DeleteCronRequest.encode(value).finish()),
|
|
||||||
requestDeserialize: (value: Buffer) => DeleteCronRequest.decode(value),
|
|
||||||
responseSerialize: (value: DeleteCronResponse) => Buffer.from(DeleteCronResponse.encode(value).finish()),
|
|
||||||
responseDeserialize: (value: Buffer) => DeleteCronResponse.decode(value),
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export interface CronServer extends UntypedServiceImplementation {
|
|
||||||
addCron: handleUnaryCall<AddCronRequest, AddCronResponse>;
|
|
||||||
delCron: handleUnaryCall<DeleteCronRequest, DeleteCronResponse>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CronClient extends Client {
|
|
||||||
addCron(
|
|
||||||
request: AddCronRequest,
|
|
||||||
callback: (error: ServiceError | null, response: AddCronResponse) => void,
|
|
||||||
): ClientUnaryCall;
|
|
||||||
addCron(
|
|
||||||
request: AddCronRequest,
|
|
||||||
metadata: Metadata,
|
|
||||||
callback: (error: ServiceError | null, response: AddCronResponse) => void,
|
|
||||||
): ClientUnaryCall;
|
|
||||||
addCron(
|
|
||||||
request: AddCronRequest,
|
|
||||||
metadata: Metadata,
|
|
||||||
options: Partial<CallOptions>,
|
|
||||||
callback: (error: ServiceError | null, response: AddCronResponse) => void,
|
|
||||||
): ClientUnaryCall;
|
|
||||||
delCron(
|
|
||||||
request: DeleteCronRequest,
|
|
||||||
callback: (error: ServiceError | null, response: DeleteCronResponse) => void,
|
|
||||||
): ClientUnaryCall;
|
|
||||||
delCron(
|
|
||||||
request: DeleteCronRequest,
|
|
||||||
metadata: Metadata,
|
|
||||||
callback: (error: ServiceError | null, response: DeleteCronResponse) => void,
|
|
||||||
): ClientUnaryCall;
|
|
||||||
delCron(
|
|
||||||
request: DeleteCronRequest,
|
|
||||||
metadata: Metadata,
|
|
||||||
options: Partial<CallOptions>,
|
|
||||||
callback: (error: ServiceError | null, response: DeleteCronResponse) => void,
|
|
||||||
): ClientUnaryCall;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CronClient = makeGenericClientConstructor(CronService, "com.ql.cron.Cron") as unknown as {
|
|
||||||
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): CronClient;
|
|
||||||
service: typeof CronService;
|
|
||||||
serviceName: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
|
||||||
|
|
||||||
export type DeepPartial<T> = T extends Builtin ? T
|
|
||||||
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
|
||||||
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
|
||||||
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
|
||||||
: Partial<T>;
|
|
||||||
|
|
||||||
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
|
||||||
export type Exact<P, I extends P> = P extends Builtin ? P
|
|
||||||
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
|
||||||
|
|
||||||
function isSet(value: any): boolean {
|
|
||||||
return value !== null && value !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MessageFns<T> {
|
|
||||||
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
|
||||||
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
|
||||||
fromJSON(object: any): T;
|
|
||||||
toJSON(message: T): unknown;
|
|
||||||
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
|
||||||
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
package com.ql.health;
|
|
||||||
|
|
||||||
message HealthCheckRequest {
|
|
||||||
string service = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message HealthCheckResponse {
|
|
||||||
enum ServingStatus {
|
|
||||||
UNKNOWN = 0;
|
|
||||||
SERVING = 1;
|
|
||||||
NOT_SERVING = 2;
|
|
||||||
SERVICE_UNKNOWN = 3;
|
|
||||||
}
|
|
||||||
ServingStatus status = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
service Health {
|
|
||||||
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
|
|
||||||
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
|
|
||||||
}
|
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// protoc-gen-ts_proto v2.6.1
|
|
||||||
// protoc v3.21.12
|
|
||||||
// source: back/protos/health.proto
|
|
||||||
|
|
||||||
/* eslint-disable */
|
|
||||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
|
||||||
import {
|
|
||||||
type CallOptions,
|
|
||||||
ChannelCredentials,
|
|
||||||
Client,
|
|
||||||
type ClientOptions,
|
|
||||||
type ClientReadableStream,
|
|
||||||
type ClientUnaryCall,
|
|
||||||
type handleServerStreamingCall,
|
|
||||||
type handleUnaryCall,
|
|
||||||
makeGenericClientConstructor,
|
|
||||||
Metadata,
|
|
||||||
type ServiceError,
|
|
||||||
type UntypedServiceImplementation,
|
|
||||||
} from "@grpc/grpc-js";
|
|
||||||
|
|
||||||
export const protobufPackage = "com.ql.health";
|
|
||||||
|
|
||||||
export interface HealthCheckRequest {
|
|
||||||
service: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HealthCheckResponse {
|
|
||||||
status: HealthCheckResponse_ServingStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum HealthCheckResponse_ServingStatus {
|
|
||||||
UNKNOWN = 0,
|
|
||||||
SERVING = 1,
|
|
||||||
NOT_SERVING = 2,
|
|
||||||
SERVICE_UNKNOWN = 3,
|
|
||||||
UNRECOGNIZED = -1,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function healthCheckResponse_ServingStatusFromJSON(object: any): HealthCheckResponse_ServingStatus {
|
|
||||||
switch (object) {
|
|
||||||
case 0:
|
|
||||||
case "UNKNOWN":
|
|
||||||
return HealthCheckResponse_ServingStatus.UNKNOWN;
|
|
||||||
case 1:
|
|
||||||
case "SERVING":
|
|
||||||
return HealthCheckResponse_ServingStatus.SERVING;
|
|
||||||
case 2:
|
|
||||||
case "NOT_SERVING":
|
|
||||||
return HealthCheckResponse_ServingStatus.NOT_SERVING;
|
|
||||||
case 3:
|
|
||||||
case "SERVICE_UNKNOWN":
|
|
||||||
return HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN;
|
|
||||||
case -1:
|
|
||||||
case "UNRECOGNIZED":
|
|
||||||
default:
|
|
||||||
return HealthCheckResponse_ServingStatus.UNRECOGNIZED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function healthCheckResponse_ServingStatusToJSON(object: HealthCheckResponse_ServingStatus): string {
|
|
||||||
switch (object) {
|
|
||||||
case HealthCheckResponse_ServingStatus.UNKNOWN:
|
|
||||||
return "UNKNOWN";
|
|
||||||
case HealthCheckResponse_ServingStatus.SERVING:
|
|
||||||
return "SERVING";
|
|
||||||
case HealthCheckResponse_ServingStatus.NOT_SERVING:
|
|
||||||
return "NOT_SERVING";
|
|
||||||
case HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN:
|
|
||||||
return "SERVICE_UNKNOWN";
|
|
||||||
case HealthCheckResponse_ServingStatus.UNRECOGNIZED:
|
|
||||||
default:
|
|
||||||
return "UNRECOGNIZED";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createBaseHealthCheckRequest(): HealthCheckRequest {
|
|
||||||
return { service: "" };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HealthCheckRequest: MessageFns<HealthCheckRequest> = {
|
|
||||||
encode(message: HealthCheckRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
|
||||||
if (message.service !== "") {
|
|
||||||
writer.uint32(10).string(message.service);
|
|
||||||
}
|
|
||||||
return writer;
|
|
||||||
},
|
|
||||||
|
|
||||||
decode(input: BinaryReader | Uint8Array, length?: number): HealthCheckRequest {
|
|
||||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
|
||||||
const message = createBaseHealthCheckRequest();
|
|
||||||
while (reader.pos < end) {
|
|
||||||
const tag = reader.uint32();
|
|
||||||
switch (tag >>> 3) {
|
|
||||||
case 1: {
|
|
||||||
if (tag !== 10) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.service = reader.string();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
reader.skip(tag & 7);
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
|
|
||||||
fromJSON(object: any): HealthCheckRequest {
|
|
||||||
return { service: isSet(object.service) ? globalThis.String(object.service) : "" };
|
|
||||||
},
|
|
||||||
|
|
||||||
toJSON(message: HealthCheckRequest): unknown {
|
|
||||||
const obj: any = {};
|
|
||||||
if (message.service !== "") {
|
|
||||||
obj.service = message.service;
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
},
|
|
||||||
|
|
||||||
create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(base?: I): HealthCheckRequest {
|
|
||||||
return HealthCheckRequest.fromPartial(base ?? ({} as any));
|
|
||||||
},
|
|
||||||
fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(object: I): HealthCheckRequest {
|
|
||||||
const message = createBaseHealthCheckRequest();
|
|
||||||
message.service = object.service ?? "";
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function createBaseHealthCheckResponse(): HealthCheckResponse {
|
|
||||||
return { status: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HealthCheckResponse: MessageFns<HealthCheckResponse> = {
|
|
||||||
encode(message: HealthCheckResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
|
||||||
if (message.status !== 0) {
|
|
||||||
writer.uint32(8).int32(message.status);
|
|
||||||
}
|
|
||||||
return writer;
|
|
||||||
},
|
|
||||||
|
|
||||||
decode(input: BinaryReader | Uint8Array, length?: number): HealthCheckResponse {
|
|
||||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
|
||||||
const message = createBaseHealthCheckResponse();
|
|
||||||
while (reader.pos < end) {
|
|
||||||
const tag = reader.uint32();
|
|
||||||
switch (tag >>> 3) {
|
|
||||||
case 1: {
|
|
||||||
if (tag !== 8) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.status = reader.int32() as any;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
reader.skip(tag & 7);
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
|
|
||||||
fromJSON(object: any): HealthCheckResponse {
|
|
||||||
return { status: isSet(object.status) ? healthCheckResponse_ServingStatusFromJSON(object.status) : 0 };
|
|
||||||
},
|
|
||||||
|
|
||||||
toJSON(message: HealthCheckResponse): unknown {
|
|
||||||
const obj: any = {};
|
|
||||||
if (message.status !== 0) {
|
|
||||||
obj.status = healthCheckResponse_ServingStatusToJSON(message.status);
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
},
|
|
||||||
|
|
||||||
create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(base?: I): HealthCheckResponse {
|
|
||||||
return HealthCheckResponse.fromPartial(base ?? ({} as any));
|
|
||||||
},
|
|
||||||
fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(object: I): HealthCheckResponse {
|
|
||||||
const message = createBaseHealthCheckResponse();
|
|
||||||
message.status = object.status ?? 0;
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export type HealthService = typeof HealthService;
|
|
||||||
export const HealthService = {
|
|
||||||
check: {
|
|
||||||
path: "/com.ql.health.Health/Check",
|
|
||||||
requestStream: false,
|
|
||||||
responseStream: false,
|
|
||||||
requestSerialize: (value: HealthCheckRequest) => Buffer.from(HealthCheckRequest.encode(value).finish()),
|
|
||||||
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
|
|
||||||
responseSerialize: (value: HealthCheckResponse) => Buffer.from(HealthCheckResponse.encode(value).finish()),
|
|
||||||
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
path: "/com.ql.health.Health/Watch",
|
|
||||||
requestStream: false,
|
|
||||||
responseStream: true,
|
|
||||||
requestSerialize: (value: HealthCheckRequest) => Buffer.from(HealthCheckRequest.encode(value).finish()),
|
|
||||||
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
|
|
||||||
responseSerialize: (value: HealthCheckResponse) => Buffer.from(HealthCheckResponse.encode(value).finish()),
|
|
||||||
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export interface HealthServer extends UntypedServiceImplementation {
|
|
||||||
check: handleUnaryCall<HealthCheckRequest, HealthCheckResponse>;
|
|
||||||
watch: handleServerStreamingCall<HealthCheckRequest, HealthCheckResponse>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HealthClient extends Client {
|
|
||||||
check(
|
|
||||||
request: HealthCheckRequest,
|
|
||||||
callback: (error: ServiceError | null, response: HealthCheckResponse) => void,
|
|
||||||
): ClientUnaryCall;
|
|
||||||
check(
|
|
||||||
request: HealthCheckRequest,
|
|
||||||
metadata: Metadata,
|
|
||||||
callback: (error: ServiceError | null, response: HealthCheckResponse) => void,
|
|
||||||
): ClientUnaryCall;
|
|
||||||
check(
|
|
||||||
request: HealthCheckRequest,
|
|
||||||
metadata: Metadata,
|
|
||||||
options: Partial<CallOptions>,
|
|
||||||
callback: (error: ServiceError | null, response: HealthCheckResponse) => void,
|
|
||||||
): ClientUnaryCall;
|
|
||||||
watch(request: HealthCheckRequest, options?: Partial<CallOptions>): ClientReadableStream<HealthCheckResponse>;
|
|
||||||
watch(
|
|
||||||
request: HealthCheckRequest,
|
|
||||||
metadata?: Metadata,
|
|
||||||
options?: Partial<CallOptions>,
|
|
||||||
): ClientReadableStream<HealthCheckResponse>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HealthClient = makeGenericClientConstructor(HealthService, "com.ql.health.Health") as unknown as {
|
|
||||||
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): HealthClient;
|
|
||||||
service: typeof HealthService;
|
|
||||||
serviceName: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
|
||||||
|
|
||||||
export type DeepPartial<T> = T extends Builtin ? T
|
|
||||||
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
|
||||||
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
|
||||||
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
|
||||||
: Partial<T>;
|
|
||||||
|
|
||||||
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
|
||||||
export type Exact<P, I extends P> = P extends Builtin ? P
|
|
||||||
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
|
||||||
|
|
||||||
function isSet(value: any): boolean {
|
|
||||||
return value !== null && value !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MessageFns<T> {
|
|
||||||
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
|
||||||
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
|
||||||
fromJSON(object: any): T;
|
|
||||||
toJSON(message: T): unknown;
|
|
||||||
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
|
||||||
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import schedule from 'node-schedule';
|
||||||
|
import express from 'express';
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import Logger from './loaders/logger';
|
||||||
|
import { CrontabStatus } from './data/cron';
|
||||||
|
import config from './config';
|
||||||
|
import { dbs } from './loaders/db';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
const cronDb = dbs.cronDb;
|
||||||
|
|
||||||
|
cronDb
|
||||||
|
.find({})
|
||||||
|
.sort({ created: 1 })
|
||||||
|
.exec((err, docs) => {
|
||||||
|
if (err) {
|
||||||
|
Logger.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (docs && docs.length > 0) {
|
||||||
|
for (let i = 0; i < docs.length; i++) {
|
||||||
|
const task = docs[i];
|
||||||
|
const _schedule = task.schedule && task.schedule.split(/ +/);
|
||||||
|
if (
|
||||||
|
_schedule &&
|
||||||
|
_schedule.length > 5 &&
|
||||||
|
task.status !== CrontabStatus.disabled &&
|
||||||
|
!task.isDisabled
|
||||||
|
) {
|
||||||
|
schedule.scheduleJob(task.schedule, function () {
|
||||||
|
let command = task.command as string;
|
||||||
|
if (!command.includes('task ') && !command.includes('ql ')) {
|
||||||
|
command = `task ${command}`;
|
||||||
|
}
|
||||||
|
exec(command);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
app
|
||||||
|
.listen(config.cronPort, async () => {
|
||||||
|
await require('./loaders/sentry').default({ expressApp: app });
|
||||||
|
await require('./loaders/db').default();
|
||||||
|
|
||||||
|
await run();
|
||||||
|
Logger.info(`
|
||||||
|
################################################
|
||||||
|
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
|
||||||
|
################################################
|
||||||
|
`);
|
||||||
|
})
|
||||||
|
.on('error', (err) => {
|
||||||
|
Logger.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
|
|
||||||
import { AddCronRequest, AddCronResponse } from '../protos/cron';
|
|
||||||
import nodeSchedule from 'node-schedule';
|
|
||||||
import { scheduleStacks } from './data';
|
|
||||||
import { runCron } from '../shared/runCron';
|
|
||||||
import Logger from '../loaders/logger';
|
|
||||||
|
|
||||||
const addCron = (
|
|
||||||
call: ServerUnaryCall<AddCronRequest, AddCronResponse>,
|
|
||||||
callback: sendUnaryData<AddCronResponse>,
|
|
||||||
) => {
|
|
||||||
for (const item of call.request.crons) {
|
|
||||||
const { id, schedule, command, extra_schedules, name } = item;
|
|
||||||
if (scheduleStacks.has(id)) {
|
|
||||||
scheduleStacks.get(id)?.forEach((x) => x.cancel());
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.info(
|
|
||||||
'[schedule][创建定时任务] 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
schedule,
|
|
||||||
command,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (extra_schedules?.length) {
|
|
||||||
extra_schedules.forEach((x) => {
|
|
||||||
Logger.info(
|
|
||||||
'[schedule][创建定时任务] 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
x.schedule,
|
|
||||||
command,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduleStacks.set(id, [
|
|
||||||
nodeSchedule.scheduleJob(id, schedule, async () => {
|
|
||||||
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
|
|
||||||
runCron(command, item);
|
|
||||||
}),
|
|
||||||
...(extra_schedules?.length
|
|
||||||
? extra_schedules.map((x) =>
|
|
||||||
nodeSchedule.scheduleJob(id, x.schedule, async () => {
|
|
||||||
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
|
|
||||||
runCron(command, item);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
: []),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(null, null);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { addCron };
|
|
||||||
@@ -1,444 +0,0 @@
|
|||||||
import 'reflect-metadata';
|
|
||||||
import { Container } from 'typedi';
|
|
||||||
import EnvService from '../services/env';
|
|
||||||
import { sendUnaryData, ServerUnaryCall } from '@grpc/grpc-js';
|
|
||||||
import {
|
|
||||||
CreateEnvRequest,
|
|
||||||
CronItem,
|
|
||||||
DeleteEnvsRequest,
|
|
||||||
DisableEnvsRequest,
|
|
||||||
EnableEnvsRequest,
|
|
||||||
EnvItem,
|
|
||||||
EnvResponse,
|
|
||||||
EnvsResponse,
|
|
||||||
GetEnvByIdRequest,
|
|
||||||
GetEnvsRequest,
|
|
||||||
MoveEnvRequest,
|
|
||||||
Response,
|
|
||||||
SystemNotifyRequest,
|
|
||||||
UpdateEnvNamesRequest,
|
|
||||||
UpdateEnvRequest,
|
|
||||||
} from '../protos/api';
|
|
||||||
import LoggerInstance from '../loaders/logger';
|
|
||||||
import pick from 'lodash/pick';
|
|
||||||
import SystemService from '../services/system';
|
|
||||||
import CronService from '../services/cron';
|
|
||||||
import {
|
|
||||||
CronDetailRequest,
|
|
||||||
CronDetailResponse,
|
|
||||||
CreateCronRequest,
|
|
||||||
UpdateCronRequest,
|
|
||||||
DeleteCronsRequest,
|
|
||||||
CronResponse,
|
|
||||||
GetCronsRequest,
|
|
||||||
CronsResponse,
|
|
||||||
GetCronByIdRequest,
|
|
||||||
EnableCronsRequest,
|
|
||||||
DisableCronsRequest,
|
|
||||||
RunCronsRequest,
|
|
||||||
} from '../protos/api';
|
|
||||||
import { NotificationInfo } from '../data/notify';
|
|
||||||
|
|
||||||
Container.set('logger', LoggerInstance);
|
|
||||||
|
|
||||||
export const getEnvs = async (
|
|
||||||
call: ServerUnaryCall<GetEnvsRequest, EnvsResponse>,
|
|
||||||
callback: sendUnaryData<EnvsResponse>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const envService = Container.get(EnvService);
|
|
||||||
const data = await envService.envs(call.request.searchValue);
|
|
||||||
callback(null, {
|
|
||||||
code: 200,
|
|
||||||
data: data.map((x) => ({ ...x, remarks: x.remarks || '' })),
|
|
||||||
});
|
|
||||||
} catch (e: any) {
|
|
||||||
callback(null, {
|
|
||||||
code: 500,
|
|
||||||
data: [],
|
|
||||||
message: e.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createEnv = async (
|
|
||||||
call: ServerUnaryCall<CreateEnvRequest, EnvsResponse>,
|
|
||||||
callback: sendUnaryData<EnvsResponse>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const envService = Container.get(EnvService);
|
|
||||||
const data = await envService.create(call.request.envs);
|
|
||||||
callback(null, { code: 200, data });
|
|
||||||
} catch (e: any) {
|
|
||||||
callback(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateEnv = async (
|
|
||||||
call: ServerUnaryCall<UpdateEnvRequest, EnvResponse>,
|
|
||||||
callback: sendUnaryData<EnvResponse>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
if (!call.request.env?.id) {
|
|
||||||
return callback(null, {
|
|
||||||
code: 400,
|
|
||||||
data: undefined,
|
|
||||||
message: 'id parameter is required',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const envService = Container.get(EnvService);
|
|
||||||
const data = await envService.update(
|
|
||||||
pick(call.request.env, ['id', 'name', 'value', 'remarks']) as EnvItem,
|
|
||||||
);
|
|
||||||
callback(null, { code: 200, data });
|
|
||||||
} catch (e: any) {
|
|
||||||
callback(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteEnvs = async (
|
|
||||||
call: ServerUnaryCall<DeleteEnvsRequest, Response>,
|
|
||||||
callback: sendUnaryData<Response>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
if (!call.request.ids || call.request.ids.length === 0) {
|
|
||||||
return callback(null, {
|
|
||||||
code: 400,
|
|
||||||
message: 'ids parameter is required',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const envService = Container.get(EnvService);
|
|
||||||
await envService.remove(call.request.ids);
|
|
||||||
callback(null, { code: 200 });
|
|
||||||
} catch (e: any) {
|
|
||||||
callback(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const moveEnv = async (
|
|
||||||
call: ServerUnaryCall<MoveEnvRequest, EnvResponse>,
|
|
||||||
callback: sendUnaryData<EnvResponse>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
if (!call.request.id) {
|
|
||||||
return callback(null, {
|
|
||||||
code: 400,
|
|
||||||
data: undefined,
|
|
||||||
message: 'id parameter is required',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const envService = Container.get(EnvService);
|
|
||||||
const data = await envService.move(call.request.id, {
|
|
||||||
fromIndex: call.request.fromIndex,
|
|
||||||
toIndex: call.request.toIndex,
|
|
||||||
});
|
|
||||||
callback(null, { code: 200, data });
|
|
||||||
} catch (e: any) {
|
|
||||||
callback(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const disableEnvs = async (
|
|
||||||
call: ServerUnaryCall<DisableEnvsRequest, Response>,
|
|
||||||
callback: sendUnaryData<Response>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
if (!call.request.ids || call.request.ids.length === 0) {
|
|
||||||
return callback(null, {
|
|
||||||
code: 400,
|
|
||||||
message: 'ids parameter is required',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const envService = Container.get(EnvService);
|
|
||||||
await envService.disabled(call.request.ids);
|
|
||||||
callback(null, { code: 200 });
|
|
||||||
} catch (e: any) {
|
|
||||||
callback(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const enableEnvs = async (
|
|
||||||
call: ServerUnaryCall<EnableEnvsRequest, Response>,
|
|
||||||
callback: sendUnaryData<Response>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
if (!call.request.ids || call.request.ids.length === 0) {
|
|
||||||
return callback(null, {
|
|
||||||
code: 400,
|
|
||||||
message: 'ids parameter is required',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const envService = Container.get(EnvService);
|
|
||||||
await envService.enabled(call.request.ids);
|
|
||||||
callback(null, { code: 200 });
|
|
||||||
} catch (e: any) {
|
|
||||||
callback(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateEnvNames = async (
|
|
||||||
call: ServerUnaryCall<UpdateEnvNamesRequest, Response>,
|
|
||||||
callback: sendUnaryData<Response>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
if (!call.request.ids || call.request.ids.length === 0) {
|
|
||||||
return callback(null, {
|
|
||||||
code: 400,
|
|
||||||
message: 'ids parameter is required',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const envService = Container.get(EnvService);
|
|
||||||
await envService.updateNames({
|
|
||||||
ids: call.request.ids,
|
|
||||||
name: call.request.name,
|
|
||||||
});
|
|
||||||
callback(null, { code: 200 });
|
|
||||||
} catch (e: any) {
|
|
||||||
callback(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getEnvById = async (
|
|
||||||
call: ServerUnaryCall<GetEnvByIdRequest, EnvResponse>,
|
|
||||||
callback: sendUnaryData<EnvResponse>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
if (!call.request.id) {
|
|
||||||
return callback(null, {
|
|
||||||
code: 400,
|
|
||||||
data: undefined,
|
|
||||||
message: 'id parameter is required',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const envService = Container.get(EnvService);
|
|
||||||
const data = await envService.getDb({ id: call.request.id });
|
|
||||||
callback(null, {
|
|
||||||
code: 200,
|
|
||||||
data: { ...data, remarks: data.remarks || '' },
|
|
||||||
});
|
|
||||||
} catch (e: any) {
|
|
||||||
callback(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const systemNotify = async (
|
|
||||||
call: ServerUnaryCall<SystemNotifyRequest, Response>,
|
|
||||||
callback: sendUnaryData<Response>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const systemService = Container.get(SystemService);
|
|
||||||
const data = await systemService.notify({
|
|
||||||
title: call.request.title,
|
|
||||||
content: call.request.content,
|
|
||||||
notificationInfo: call.request.notificationInfo as unknown as NotificationInfo,
|
|
||||||
});
|
|
||||||
callback(null, data);
|
|
||||||
} catch (e: any) {
|
|
||||||
callback(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeCronData = (data: CronItem | null): CronItem | undefined => {
|
|
||||||
if (!data) return undefined;
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
sub_id: data.sub_id ?? undefined,
|
|
||||||
extra_schedules: data.extra_schedules ?? undefined,
|
|
||||||
pid: data.pid ?? undefined,
|
|
||||||
task_before: data.task_before ?? undefined,
|
|
||||||
task_after: data.task_after ?? undefined,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCronDetail = async (
|
|
||||||
call: ServerUnaryCall<CronDetailRequest, CronDetailResponse>,
|
|
||||||
callback: sendUnaryData<CronDetailResponse>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
if (!call.request.log_path) {
|
|
||||||
return callback(null, {
|
|
||||||
code: 400,
|
|
||||||
data: undefined,
|
|
||||||
message: 'log_path is required',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const cronService = Container.get(CronService);
|
|
||||||
const data = (await cronService.find({
|
|
||||||
log_path: call.request.log_path,
|
|
||||||
})) as CronItem;
|
|
||||||
callback(null, { code: 200, data: normalizeCronData(data) });
|
|
||||||
} catch (e: any) {
|
|
||||||
callback(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createCron = async (
|
|
||||||
call: ServerUnaryCall<CreateCronRequest, CronResponse>,
|
|
||||||
callback: sendUnaryData<CronResponse>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const cronService = Container.get(CronService);
|
|
||||||
const data = (await cronService.create(call.request)) as CronItem;
|
|
||||||
callback(null, { code: 200, data: normalizeCronData(data) });
|
|
||||||
} catch (e: any) {
|
|
||||||
callback(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateCron = async (
|
|
||||||
call: ServerUnaryCall<UpdateCronRequest, CronResponse>,
|
|
||||||
callback: sendUnaryData<CronResponse>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const cronService = Container.get(CronService);
|
|
||||||
const { id, ...fields } = call.request;
|
|
||||||
|
|
||||||
const updateRequest = {
|
|
||||||
id,
|
|
||||||
...Object.entries(fields).reduce((acc: any, [key, value]) => {
|
|
||||||
if (value !== undefined) {
|
|
||||||
acc[key] = value;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {}),
|
|
||||||
} as UpdateCronRequest;
|
|
||||||
|
|
||||||
const data = (await cronService.update(updateRequest)) as CronItem;
|
|
||||||
callback(null, { code: 200, data: normalizeCronData(data) });
|
|
||||||
} catch (e: any) {
|
|
||||||
callback(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteCrons = async (
|
|
||||||
call: ServerUnaryCall<DeleteCronsRequest, Response>,
|
|
||||||
callback: sendUnaryData<Response>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const cronService = Container.get(CronService);
|
|
||||||
await cronService.remove(call.request.ids);
|
|
||||||
callback(null, { code: 200 });
|
|
||||||
} catch (e: any) {
|
|
||||||
callback(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCrons = async (
|
|
||||||
call: ServerUnaryCall<GetCronsRequest, CronsResponse>,
|
|
||||||
callback: sendUnaryData<CronsResponse>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const cronService = Container.get(CronService);
|
|
||||||
const result = await cronService.crontabs({
|
|
||||||
searchValue: call.request.searchValue || '',
|
|
||||||
page: '0',
|
|
||||||
size: '0',
|
|
||||||
sorter: '',
|
|
||||||
filters: '',
|
|
||||||
queryString: '',
|
|
||||||
});
|
|
||||||
const data = result.data.map((x) => normalizeCronData(x as CronItem));
|
|
||||||
callback(null, {
|
|
||||||
code: 200,
|
|
||||||
data: data.filter((x): x is CronItem => x !== undefined),
|
|
||||||
});
|
|
||||||
} catch (e: any) {
|
|
||||||
callback(null, {
|
|
||||||
code: 500,
|
|
||||||
data: [],
|
|
||||||
message: e.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCronById = async (
|
|
||||||
call: ServerUnaryCall<GetCronByIdRequest, CronResponse>,
|
|
||||||
callback: sendUnaryData<CronResponse>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
if (!call.request.id) {
|
|
||||||
return callback(null, {
|
|
||||||
code: 400,
|
|
||||||
data: undefined,
|
|
||||||
message: 'id parameter is required',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const cronService = Container.get(CronService);
|
|
||||||
const data = (await cronService.getDb({ id: call.request.id })) as CronItem;
|
|
||||||
callback(null, { code: 200, data: normalizeCronData(data) });
|
|
||||||
} catch (e: any) {
|
|
||||||
callback(null, {
|
|
||||||
code: 404,
|
|
||||||
data: undefined,
|
|
||||||
message: e.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const enableCrons = async (
|
|
||||||
call: ServerUnaryCall<EnableCronsRequest, Response>,
|
|
||||||
callback: sendUnaryData<Response>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
if (!call.request.ids || call.request.ids.length === 0) {
|
|
||||||
return callback(null, {
|
|
||||||
code: 400,
|
|
||||||
message: 'ids parameter is required',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const cronService = Container.get(CronService);
|
|
||||||
await cronService.enabled(call.request.ids);
|
|
||||||
callback(null, { code: 200 });
|
|
||||||
} catch (e: any) {
|
|
||||||
callback(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const disableCrons = async (
|
|
||||||
call: ServerUnaryCall<DisableCronsRequest, Response>,
|
|
||||||
callback: sendUnaryData<Response>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
if (!call.request.ids || call.request.ids.length === 0) {
|
|
||||||
return callback(null, {
|
|
||||||
code: 400,
|
|
||||||
message: 'ids parameter is required',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const cronService = Container.get(CronService);
|
|
||||||
await cronService.disabled(call.request.ids);
|
|
||||||
callback(null, { code: 200 });
|
|
||||||
} catch (e: any) {
|
|
||||||
callback(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const runCrons = async (
|
|
||||||
call: ServerUnaryCall<RunCronsRequest, Response>,
|
|
||||||
callback: sendUnaryData<Response>,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
if (!call.request.ids || call.request.ids.length === 0) {
|
|
||||||
return callback(null, {
|
|
||||||
code: 400,
|
|
||||||
message: 'ids parameter is required',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const cronService = Container.get(CronService);
|
|
||||||
await cronService.run(call.request.ids);
|
|
||||||
callback(null, { code: 200 });
|
|
||||||
} catch (e: any) {
|
|
||||||
callback(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { credentials } from '@grpc/grpc-js';
|
|
||||||
import {
|
|
||||||
AddCronRequest,
|
|
||||||
AddCronResponse,
|
|
||||||
CronClient,
|
|
||||||
DeleteCronRequest,
|
|
||||||
DeleteCronResponse,
|
|
||||||
} from '../protos/cron';
|
|
||||||
import config from '../config';
|
|
||||||
|
|
||||||
class Client {
|
|
||||||
private client = new CronClient(
|
|
||||||
`0.0.0.0:${config.grpcPort}`,
|
|
||||||
credentials.createInsecure(),
|
|
||||||
{ 'grpc.enable_http_proxy': 0 },
|
|
||||||
);
|
|
||||||
|
|
||||||
addCron(request: AddCronRequest['crons']): Promise<AddCronResponse> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.client.addCron({ crons: request }, (err, res) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
resolve(res);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
delCron(request: DeleteCronRequest['ids']): Promise<DeleteCronResponse> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.client.delCron({ ids: request }, (err, res) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
resolve(res);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new Client();
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import nodeSchedule from 'node-schedule';
|
|
||||||
import { ToadScheduler } from 'toad-scheduler';
|
|
||||||
|
|
||||||
export const scheduleStacks = new Map<string, nodeSchedule.Job[]>();
|
|
||||||
|
|
||||||
export const intervalSchedule = new ToadScheduler();
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
|
|
||||||
import { DeleteCronRequest, DeleteCronResponse } from '../protos/cron';
|
|
||||||
import { scheduleStacks } from './data';
|
|
||||||
import Logger from '../loaders/logger';
|
|
||||||
|
|
||||||
const delCron = (
|
|
||||||
call: ServerUnaryCall<DeleteCronRequest, DeleteCronResponse>,
|
|
||||||
callback: sendUnaryData<DeleteCronResponse>,
|
|
||||||
) => {
|
|
||||||
for (const id of call.request.ids) {
|
|
||||||
if (scheduleStacks.has(id)) {
|
|
||||||
Logger.info(
|
|
||||||
'[schedule][取消定时任务] 任务ID: %s',
|
|
||||||
id,
|
|
||||||
);
|
|
||||||
scheduleStacks.get(id)?.forEach(x => x.cancel());
|
|
||||||
scheduleStacks.delete(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(null, null);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { delCron };
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
|
|
||||||
import { HealthCheckRequest, HealthCheckResponse } from '../protos/health';
|
|
||||||
import config from '../config';
|
|
||||||
import { promiseExec } from '../config/util';
|
|
||||||
|
|
||||||
const check = async (
|
|
||||||
call: ServerUnaryCall<HealthCheckRequest, HealthCheckResponse>,
|
|
||||||
callback: sendUnaryData<HealthCheckResponse>,
|
|
||||||
) => {
|
|
||||||
switch (call.request.service) {
|
|
||||||
case 'cron':
|
|
||||||
const res = await promiseExec(
|
|
||||||
`curl -s --noproxy '*' http://0.0.0.0:${config.port}/api/system`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res.includes('200')) {
|
|
||||||
return callback(null, { status: 1 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const qinglongErrLog = await promiseExec(
|
|
||||||
`tail -n 300 ~/.pm2/logs/qinglong-error.log`,
|
|
||||||
);
|
|
||||||
return callback(
|
|
||||||
new Error(`${qinglongErrLog || ''}\n${res}`.trim()),
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return callback(null, { status: 1 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export { check };
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
|
||||||
import path, { join } from 'path';
|
|
||||||
import config from '../config';
|
|
||||||
import { getFileContentByName } from '../config/util';
|
|
||||||
import { Response } from 'express';
|
|
||||||
import { request } from 'undici';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export default class ConfigService {
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
public async getFile(filePath: string, res: Response) {
|
|
||||||
let content = '';
|
|
||||||
const avaliablePath = [config.rootPath, config.configPath].map((x) =>
|
|
||||||
path.resolve(x, filePath),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
config.blackFileList.includes(filePath) ||
|
|
||||||
avaliablePath.every(
|
|
||||||
(x) =>
|
|
||||||
!x.startsWith(config.scriptPath) && !x.startsWith(config.configPath),
|
|
||||||
) ||
|
|
||||||
!filePath
|
|
||||||
) {
|
|
||||||
return res.send({ code: 403, message: '文件无法访问' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filePath.startsWith('sample/')) {
|
|
||||||
const res = await request(
|
|
||||||
`https://gitlab.com/whyour/qinglong/-/raw/master/${filePath}`,
|
|
||||||
);
|
|
||||||
content = await res.body.text();
|
|
||||||
} else if (filePath.startsWith('data/scripts/')) {
|
|
||||||
content = await getFileContentByName(join(config.rootPath, filePath));
|
|
||||||
} else {
|
|
||||||
content = await getFileContentByName(join(config.configPath, filePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
res.send({ code: 200, data: content });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+360
-672
File diff suppressed because it is too large
Load Diff
@@ -1,125 +0,0 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
|
||||||
import winston from 'winston';
|
|
||||||
import { CrontabView, CrontabViewModel } from '../data/cronView';
|
|
||||||
import {
|
|
||||||
initPosition,
|
|
||||||
maxPosition,
|
|
||||||
minPosition,
|
|
||||||
stepPosition,
|
|
||||||
} from '../data/env';
|
|
||||||
import { FindOptions } from 'sequelize';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export default class CronViewService {
|
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
|
||||||
|
|
||||||
public async create(payload: CrontabView): Promise<CrontabView> {
|
|
||||||
let position = initPosition;
|
|
||||||
const views = await this.list();
|
|
||||||
if (views && views.length > 0 && views[views.length - 1].position) {
|
|
||||||
position = views[views.length - 1].position as number;
|
|
||||||
}
|
|
||||||
position = position / 2;
|
|
||||||
const tab = new CrontabView({ ...payload, position });
|
|
||||||
const doc = await this.insert(tab);
|
|
||||||
|
|
||||||
await this.checkPosition(tab.position!);
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async insert(payload: CrontabView): Promise<CrontabView> {
|
|
||||||
return await CrontabViewModel.create(payload, { returning: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async update(payload: CrontabView): Promise<CrontabView> {
|
|
||||||
const doc = await this.getDb({ id: payload.id });
|
|
||||||
const tab = new CrontabView({ ...doc, ...payload });
|
|
||||||
const newDoc = await this.updateDb(tab);
|
|
||||||
return newDoc;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateDb(payload: CrontabView): Promise<CrontabView> {
|
|
||||||
await CrontabViewModel.update(payload, { where: { id: payload.id } });
|
|
||||||
return await this.getDb({ id: payload.id });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async remove(ids: number[]) {
|
|
||||||
await CrontabViewModel.destroy({ where: { id: ids } });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async list(): Promise<CrontabView[]> {
|
|
||||||
try {
|
|
||||||
const result = await CrontabViewModel.findAll({
|
|
||||||
where: {},
|
|
||||||
order: [['position', 'DESC']],
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getDb(
|
|
||||||
query: FindOptions<CrontabView>['where'],
|
|
||||||
): Promise<CrontabView> {
|
|
||||||
const doc: any = await CrontabViewModel.findOne({ where: { ...query } });
|
|
||||||
if (!doc) {
|
|
||||||
throw new Error(`CronView ${JSON.stringify(query)} not found`);
|
|
||||||
}
|
|
||||||
return doc.get({ plain: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async disabled(ids: number[]) {
|
|
||||||
await CrontabViewModel.update({ isDisabled: 1 }, { where: { id: ids } });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async enabled(ids: number[]) {
|
|
||||||
await CrontabViewModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
|
||||||
}
|
|
||||||
|
|
||||||
private async checkPosition(position: number) {
|
|
||||||
const precisionPosition = parseFloat(position.toPrecision(16));
|
|
||||||
if (precisionPosition < minPosition || precisionPosition > maxPosition) {
|
|
||||||
const envs = await this.list();
|
|
||||||
let position = initPosition;
|
|
||||||
for (const env of envs) {
|
|
||||||
position = position - stepPosition;
|
|
||||||
await this.updateDb({ id: env.id, position });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPrecisionPosition(position: number): number {
|
|
||||||
return parseFloat(position.toPrecision(16));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async move({
|
|
||||||
id,
|
|
||||||
fromIndex,
|
|
||||||
toIndex,
|
|
||||||
}: {
|
|
||||||
fromIndex: number;
|
|
||||||
toIndex: number;
|
|
||||||
id: number;
|
|
||||||
}): Promise<CrontabView> {
|
|
||||||
let targetPosition: number;
|
|
||||||
const isUpward = fromIndex > toIndex;
|
|
||||||
const views = await this.list();
|
|
||||||
if (toIndex === 0 || toIndex === views.length - 1) {
|
|
||||||
targetPosition = isUpward
|
|
||||||
? views[0].position! * 2
|
|
||||||
: views[toIndex].position! / 2;
|
|
||||||
} else {
|
|
||||||
targetPosition = isUpward
|
|
||||||
? (views[toIndex].position! + views[toIndex - 1].position!) / 2
|
|
||||||
: (views[toIndex].position! + views[toIndex + 1].position!) / 2;
|
|
||||||
}
|
|
||||||
const newDoc = await this.update({
|
|
||||||
id,
|
|
||||||
position: this.getPrecisionPosition(targetPosition),
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.checkPosition(targetPosition);
|
|
||||||
return newDoc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+218
-313
@@ -1,380 +1,285 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
import DataStore from 'nedb';
|
||||||
import {
|
import {
|
||||||
Dependence,
|
Dependence,
|
||||||
|
InstallDependenceCommandTypes,
|
||||||
DependenceStatus,
|
DependenceStatus,
|
||||||
DependenceTypes,
|
DependenceTypes,
|
||||||
DependenceModel,
|
unInstallDependenceCommandTypes,
|
||||||
versionDependenceCommandTypes,
|
|
||||||
} from '../data/dependence';
|
} from '../data/dependence';
|
||||||
import { spawn } from 'cross-spawn';
|
import _ from 'lodash';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
import SockService from './sock';
|
import SockService from './sock';
|
||||||
import { FindOptions, Op } from 'sequelize';
|
import { dbs } from '../loaders/db';
|
||||||
import {
|
|
||||||
fileExist,
|
|
||||||
getPid,
|
|
||||||
killTask,
|
|
||||||
promiseExecSuccess,
|
|
||||||
getInstallCommand,
|
|
||||||
getUninstallCommand,
|
|
||||||
getGetCommand,
|
|
||||||
} from '../config/util';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import taskLimit from '../shared/pLimit';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class DependenceService {
|
export default class DependenceService {
|
||||||
|
private dependenceDb = dbs.dependenceDb;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject('logger') private logger: winston.Logger,
|
@Inject('logger') private logger: winston.Logger,
|
||||||
private sockService: SockService,
|
private sockService: SockService,
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
public async create(payloads: Dependence[]): Promise<Dependence[]> {
|
public async create(payloads: Dependence[]): Promise<Dependence[]> {
|
||||||
const tabs = payloads.map((x) => {
|
const tabs = payloads.map((x) => {
|
||||||
const tab = new Dependence({ ...x, status: DependenceStatus.queued });
|
const tab = new Dependence({ ...x, status: DependenceStatus.installing });
|
||||||
return tab;
|
return tab;
|
||||||
});
|
});
|
||||||
const docs = await this.insert(tabs);
|
const docs = await this.insert(tabs);
|
||||||
this.installDependenceOneByOne(docs);
|
this.installOrUninstallDependencies(docs);
|
||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async insert(payloads: Dependence[]): Promise<Dependence[]> {
|
public async insert(payloads: Dependence[]): Promise<Dependence[]> {
|
||||||
const docs = await DependenceModel.bulkCreate(payloads);
|
return new Promise((resolve) => {
|
||||||
return docs;
|
this.dependenceDb.insert(payloads, (err, docs) => {
|
||||||
|
if (err) {
|
||||||
|
this.logger.error(err);
|
||||||
|
} else {
|
||||||
|
resolve(docs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async update(
|
public async update(
|
||||||
payload: Dependence & { id: string },
|
payload: Dependence & { _id: string },
|
||||||
): Promise<Dependence> {
|
): Promise<Dependence> {
|
||||||
const { id, ...other } = payload;
|
const { _id, ...other } = payload;
|
||||||
const doc = await this.getDb({ id });
|
const doc = await this.get(_id);
|
||||||
const tab = new Dependence({
|
const tab = new Dependence({
|
||||||
...doc,
|
...doc,
|
||||||
...other,
|
...other,
|
||||||
status: DependenceStatus.queued,
|
status: DependenceStatus.installing,
|
||||||
});
|
});
|
||||||
const newDoc = await this.updateDb(tab);
|
const newDoc = await this.updateDb(tab);
|
||||||
this.installDependenceOneByOne([newDoc]);
|
this.installOrUninstallDependencies([newDoc]);
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateDb(payload: Dependence): Promise<Dependence> {
|
private async updateDb(payload: Dependence): Promise<Dependence> {
|
||||||
await DependenceModel.update(payload, { where: { id: payload.id } });
|
return new Promise((resolve) => {
|
||||||
return await this.getDb({ id: payload.id });
|
this.dependenceDb.update(
|
||||||
}
|
{ _id: payload._id },
|
||||||
|
payload,
|
||||||
public async remove(ids: number[], force = false): Promise<Dependence[]> {
|
{ returnUpdatedDocs: true },
|
||||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
(err, num, doc) => {
|
||||||
for (const doc of docs) {
|
if (err) {
|
||||||
taskLimit.removeQueuedDependency(doc);
|
this.logger.error(err);
|
||||||
}
|
} else {
|
||||||
const unInstalledDeps = docs.filter(
|
resolve(doc as Dependence);
|
||||||
(x) => x.status !== DependenceStatus.installed,
|
}
|
||||||
);
|
},
|
||||||
const installedDeps = docs.filter(
|
|
||||||
(x) => x.status === DependenceStatus.installed,
|
|
||||||
);
|
|
||||||
await this.removeDb(unInstalledDeps.map((x) => x.id!));
|
|
||||||
|
|
||||||
if (installedDeps.length) {
|
|
||||||
await DependenceModel.update(
|
|
||||||
{ status: DependenceStatus.queued, log: [] },
|
|
||||||
{ where: { id: ids } },
|
|
||||||
);
|
);
|
||||||
|
});
|
||||||
this.installDependenceOneByOne(docs, false, force);
|
|
||||||
}
|
|
||||||
return docs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async removeDb(ids: number[]) {
|
public async remove(ids: string[]) {
|
||||||
await DependenceModel.destroy({ where: { id: ids } });
|
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(
|
public async dependencies(
|
||||||
{
|
{ searchValue, type }: { searchValue: string; type: string },
|
||||||
searchValue,
|
sort: any = { position: -1 },
|
||||||
type,
|
|
||||||
status,
|
|
||||||
}: {
|
|
||||||
searchValue: string;
|
|
||||||
type: keyof typeof DependenceTypes;
|
|
||||||
status: string;
|
|
||||||
},
|
|
||||||
sort: any = [],
|
|
||||||
query: any = {},
|
query: any = {},
|
||||||
): Promise<Dependence[]> {
|
): Promise<Dependence[]> {
|
||||||
let condition = query;
|
let condition = { ...query, type: DependenceTypes[type as any] };
|
||||||
if (type && DependenceTypes[type] !== undefined) {
|
|
||||||
condition.type = DependenceTypes[type];
|
|
||||||
}
|
|
||||||
if (status) {
|
|
||||||
condition.status = status.split(',').map(Number);
|
|
||||||
}
|
|
||||||
if (searchValue) {
|
if (searchValue) {
|
||||||
const encodeText = encodeURI(searchValue);
|
const reg = new RegExp(searchValue);
|
||||||
condition.name = {
|
condition = {
|
||||||
[Op.or]: [
|
...condition,
|
||||||
{ [Op.like]: `%${searchValue}%` },
|
$or: [
|
||||||
{ [Op.like]: `%${encodeText}%` },
|
{
|
||||||
|
name: reg,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
try {
|
const newDocs = await this.find(condition, sort);
|
||||||
return await this.find(condition, sort);
|
return newDocs;
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public installDependenceOneByOne(
|
public async reInstall(ids: string[]): Promise<Dependence[]> {
|
||||||
docs: 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,
|
isInstall: boolean = true,
|
||||||
force: boolean = false,
|
|
||||||
): Promise<void> {
|
|
||||||
docs.forEach((dep) => {
|
|
||||||
this.installOrUninstallDependency(dep, isInstall, force);
|
|
||||||
});
|
|
||||||
|
|
||||||
return taskLimit.waitDependencyQueueDone();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async reInstall(ids: number[]): Promise<Dependence[]> {
|
|
||||||
await DependenceModel.update(
|
|
||||||
{ status: DependenceStatus.queued, log: [] },
|
|
||||||
{ where: { id: ids } },
|
|
||||||
);
|
|
||||||
|
|
||||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
|
||||||
for (const doc of docs) {
|
|
||||||
taskLimit.removeQueuedDependency(doc);
|
|
||||||
}
|
|
||||||
this.installDependenceOneByOne(docs, true, true);
|
|
||||||
return docs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async cancel(ids: number[]) {
|
|
||||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
|
||||||
for (const doc of docs) {
|
|
||||||
taskLimit.removeQueuedDependency(doc);
|
|
||||||
const depInstallCommand = getInstallCommand(doc.type, doc.name);
|
|
||||||
const depUnInstallCommand = getUninstallCommand(doc.type, doc.name);
|
|
||||||
const pids = await Promise.all([
|
|
||||||
getPid(depInstallCommand),
|
|
||||||
getPid(depUnInstallCommand),
|
|
||||||
]);
|
|
||||||
for (const pid of pids) {
|
|
||||||
pid && (await killTask(pid));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await DependenceModel.update(
|
|
||||||
{ status: DependenceStatus.cancelled },
|
|
||||||
{ where: { id: ids } },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async find(query: any, sort: any = []): Promise<Dependence[]> {
|
|
||||||
const docs = await DependenceModel.findAll({
|
|
||||||
where: { ...query },
|
|
||||||
order: [...sort, ['createdAt', 'DESC']],
|
|
||||||
});
|
|
||||||
return docs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getDb(
|
|
||||||
query: FindOptions<Dependence>['where'],
|
|
||||||
): Promise<Dependence> {
|
|
||||||
const doc: any = await DependenceModel.findOne({ where: { ...query } });
|
|
||||||
if (!doc) {
|
|
||||||
throw new Error(`Dependency ${JSON.stringify(query)} not found`);
|
|
||||||
}
|
|
||||||
return doc.get({ plain: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
private async updateLog(ids: number[], log: string): Promise<void> {
|
|
||||||
taskLimit.updateDepLog(async () => {
|
|
||||||
const docs = await DependenceModel.findAll({ where: { id: ids } });
|
|
||||||
for (const doc of docs) {
|
|
||||||
const newLog = doc?.log ? [...doc.log, log] : [log];
|
|
||||||
await DependenceModel.update(
|
|
||||||
{ log: newLog },
|
|
||||||
{ where: { id: doc.id } },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public installOrUninstallDependency(
|
|
||||||
dependency: Dependence,
|
|
||||||
isInstall: boolean = true,
|
|
||||||
force: boolean = false,
|
|
||||||
) {
|
) {
|
||||||
return taskLimit.runDependeny(dependency, () => {
|
return new Promise((resolve) => {
|
||||||
return new Promise(async (resolve) => {
|
if (dependencies.length === 0) {
|
||||||
if (taskLimit.firstDependencyId !== dependency.id) {
|
resolve(null);
|
||||||
return resolve(null);
|
return;
|
||||||
}
|
}
|
||||||
|
const depNames = dependencies.map((x) => x.name).join(' ');
|
||||||
taskLimit.removeQueuedDependency(dependency);
|
const depRunCommand = (
|
||||||
|
isInstall
|
||||||
const depIds = [dependency.id!];
|
? InstallDependenceCommandTypes
|
||||||
const status = isInstall
|
: unInstallDependenceCommandTypes
|
||||||
? DependenceStatus.installing
|
)[dependencies[0].type as any];
|
||||||
: DependenceStatus.removing;
|
const actionText = isInstall ? '安装' : '删除';
|
||||||
await DependenceModel.update({ status }, { where: { id: depIds } });
|
const depIds = dependencies.map((x) => x._id) as string[];
|
||||||
|
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
|
||||||
const socketMessageType = isInstall
|
const startTime = Date.now();
|
||||||
? 'installDependence'
|
this.sockService.sendMessage({
|
||||||
: 'uninstallDependence';
|
type: 'installDependence',
|
||||||
let depName = dependency.name.trim();
|
message: `开始${actionText}依赖 ${depNames},开始时间 ${new Date(
|
||||||
const command = isInstall
|
startTime,
|
||||||
? getInstallCommand(dependency.type, depName)
|
).toLocaleString()}`,
|
||||||
: getUninstallCommand(dependency.type, depName);
|
references: depIds,
|
||||||
const actionText = isInstall ? '安装' : '删除';
|
});
|
||||||
const startTime = dayjs();
|
this.updateLog(
|
||||||
|
depIds,
|
||||||
const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format(
|
`开始${actionText}依赖 ${depNames},开始时间 ${new Date(
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
startTime,
|
||||||
)}\n\n`;
|
).toLocaleString()}\n`,
|
||||||
|
);
|
||||||
|
cp.stdout.on('data', (data) => {
|
||||||
this.sockService.sendMessage({
|
this.sockService.sendMessage({
|
||||||
type: socketMessageType,
|
type: 'installDependence',
|
||||||
message,
|
message: data.toString(),
|
||||||
references: depIds,
|
references: depIds,
|
||||||
});
|
});
|
||||||
this.updateLog(depIds, message);
|
this.updateLog(depIds, data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
// 判断是否已经安装过依赖
|
cp.stderr.on('data', (data) => {
|
||||||
if (isInstall && !force) {
|
this.sockService.sendMessage({
|
||||||
const getCommand = getGetCommand(dependency.type, depName);
|
type: 'installDependence',
|
||||||
const depVersionStr = versionDependenceCommandTypes[dependency.type];
|
message: data.toString(),
|
||||||
let depVersion = '';
|
references: depIds,
|
||||||
if (depName.includes(depVersionStr)) {
|
});
|
||||||
const symbolRegx = new RegExp(
|
this.updateLog(depIds, data.toString());
|
||||||
`(.*)${depVersionStr}([0-9\\.\\-\\+a-zA-Z]*)`,
|
});
|
||||||
);
|
|
||||||
const [, _depName, _depVersion] = depName.match(symbolRegx) || [];
|
|
||||||
if (_depVersion && _depName) {
|
|
||||||
depName = _depName;
|
|
||||||
depVersion = _depVersion;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const isNodeDependence = dependency.type === DependenceTypes.nodejs;
|
|
||||||
const isLinuxDependence = dependency.type === DependenceTypes.linux;
|
|
||||||
const isPythonDependence =
|
|
||||||
dependency.type === DependenceTypes.python3;
|
|
||||||
const depInfo = (await promiseExecSuccess(getCommand))
|
|
||||||
.replace(/\s{2,}/, ' ')
|
|
||||||
.replace(/\s+$/, '');
|
|
||||||
|
|
||||||
if (
|
cp.on('error', (err) => {
|
||||||
depInfo &&
|
this.sockService.sendMessage({
|
||||||
((isNodeDependence && depInfo.split(' ')?.[0] === depName) ||
|
type: 'installDependence',
|
||||||
(isLinuxDependence &&
|
message: JSON.stringify(err),
|
||||||
depInfo.toLocaleLowerCase().includes('installed')) ||
|
references: depIds,
|
||||||
isPythonDependence) &&
|
});
|
||||||
(!depVersion || depInfo.includes(depVersion))
|
this.updateLog(depIds, JSON.stringify(err));
|
||||||
) {
|
resolve(null);
|
||||||
const endTime = dayjs();
|
});
|
||||||
const _message = `检测到已经安装 ${depName}\n\n${depInfo}\n\n跳过安装\n\n依赖${actionText}成功,结束时间 ${endTime.format(
|
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
cp.on('close', (code) => {
|
||||||
)},耗时 ${endTime.diff(startTime, 'second')} 秒`;
|
const endTime = Date.now();
|
||||||
this.sockService.sendMessage({
|
const isSucceed = code === 0;
|
||||||
type: socketMessageType,
|
const resultText = isSucceed ? '成功' : '失败';
|
||||||
message: _message,
|
|
||||||
references: depIds,
|
this.sockService.sendMessage({
|
||||||
});
|
type: 'installDependence',
|
||||||
this.updateLog(depIds, _message);
|
message: `依赖${actionText}${resultText},结束时间 ${new Date(
|
||||||
await DependenceModel.update(
|
endTime,
|
||||||
{ status: DependenceStatus.installed },
|
).toLocaleString()},耗时 ${(endTime - startTime) / 1000} 秒`,
|
||||||
{ where: { id: depIds } },
|
references: depIds,
|
||||||
);
|
});
|
||||||
return resolve(null);
|
this.updateLog(
|
||||||
}
|
depIds,
|
||||||
}
|
`依赖${actionText}${resultText},结束时间 ${new Date(
|
||||||
const dependenceProxyFileExist = await fileExist(
|
endTime,
|
||||||
config.dependenceProxyFile,
|
).toLocaleString()},耗时 ${(endTime - startTime) / 1000} 秒`,
|
||||||
);
|
);
|
||||||
const proxyStr = dependenceProxyFileExist
|
|
||||||
? `source ${config.dependenceProxyFile} &&`
|
|
||||||
: '';
|
|
||||||
const cp = spawn(`${proxyStr} ${command}`, {
|
|
||||||
shell: '/bin/bash',
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.stdout.on('data', async (data) => {
|
let status = null;
|
||||||
this.sockService.sendMessage({
|
if (isSucceed) {
|
||||||
type: socketMessageType,
|
status = isInstall
|
||||||
message: data.toString(),
|
? DependenceStatus.installed
|
||||||
references: depIds,
|
: DependenceStatus.removed;
|
||||||
});
|
} else {
|
||||||
this.updateLog(depIds, data.toString());
|
status = isInstall
|
||||||
});
|
? DependenceStatus.installFailed
|
||||||
|
: DependenceStatus.removeFailed;
|
||||||
|
}
|
||||||
|
this.dependenceDb.update(
|
||||||
|
{ _id: { $in: depIds } },
|
||||||
|
{
|
||||||
|
$set: { status },
|
||||||
|
$unset: { pid: true },
|
||||||
|
},
|
||||||
|
{ multi: true },
|
||||||
|
);
|
||||||
|
|
||||||
cp.stderr.on('data', async (data) => {
|
// 如果删除依赖成功,3秒后删除数据库记录
|
||||||
this.sockService.sendMessage({
|
if (isSucceed && !isInstall) {
|
||||||
type: socketMessageType,
|
setTimeout(() => {
|
||||||
message: data.toString(),
|
|
||||||
references: depIds,
|
|
||||||
});
|
|
||||||
this.updateLog(depIds, data.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.on('error', async (err) => {
|
|
||||||
this.sockService.sendMessage({
|
|
||||||
type: socketMessageType,
|
|
||||||
message: JSON.stringify(err),
|
|
||||||
references: depIds,
|
|
||||||
});
|
|
||||||
this.updateLog(depIds, JSON.stringify(err));
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.on('exit', async (code) => {
|
|
||||||
const endTime = dayjs();
|
|
||||||
const isSucceed = code === 0;
|
|
||||||
const resultText = isSucceed ? '成功' : '失败';
|
|
||||||
|
|
||||||
const message = `\n依赖${actionText}${resultText},结束时间 ${endTime.format(
|
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
|
||||||
)},耗时 ${endTime.diff(startTime, 'second')} 秒`;
|
|
||||||
this.sockService.sendMessage({
|
|
||||||
type: socketMessageType,
|
|
||||||
message,
|
|
||||||
references: depIds,
|
|
||||||
});
|
|
||||||
this.updateLog(depIds, message);
|
|
||||||
|
|
||||||
let status: number;
|
|
||||||
if (isSucceed) {
|
|
||||||
status = isInstall
|
|
||||||
? DependenceStatus.installed
|
|
||||||
: DependenceStatus.removed;
|
|
||||||
} else {
|
|
||||||
status = isInstall
|
|
||||||
? DependenceStatus.installFailed
|
|
||||||
: DependenceStatus.removeFailed;
|
|
||||||
}
|
|
||||||
const docs = await DependenceModel.findAll({ where: { id: depIds } });
|
|
||||||
const _docIds = docs
|
|
||||||
.filter((x) => x.status !== DependenceStatus.cancelled)
|
|
||||||
.map((x) => x.id!);
|
|
||||||
|
|
||||||
if (_docIds.length > 0) {
|
|
||||||
await DependenceModel.update(
|
|
||||||
{ status },
|
|
||||||
{ where: { id: _docIds } },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果删除依赖成功或者强制删除
|
|
||||||
if ((isSucceed || force) && !isInstall) {
|
|
||||||
this.removeDb(depIds);
|
this.removeDb(depIds);
|
||||||
}
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
resolve(null);
|
resolve(null);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+138
-148
@@ -1,74 +1,84 @@
|
|||||||
import groupBy from 'lodash/groupBy';
|
import { Service, Inject } from 'typedi';
|
||||||
import { FindOptions, Op } from 'sequelize';
|
|
||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
|
import { getFileContentByName } from '../config/util';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import {
|
import * as fs from 'fs';
|
||||||
Env,
|
import DataStore from 'nedb';
|
||||||
EnvModel,
|
import { Env, EnvStatus, initEnvPosition } from '../data/env';
|
||||||
EnvStatus,
|
import _ from 'lodash';
|
||||||
initPosition,
|
import { dbs } from '../loaders/db';
|
||||||
maxPosition,
|
|
||||||
minPosition,
|
|
||||||
stepPosition,
|
|
||||||
} from '../data/env';
|
|
||||||
import { writeFileWithLock } from '../shared/utils';
|
|
||||||
import { sequelize } from '../data';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class EnvService {
|
export default class EnvService {
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) { }
|
private envDb = dbs.envDb;
|
||||||
|
|
||||||
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
public async create(payloads: Env[]): Promise<Env[]> {
|
public async create(payloads: Env[]): Promise<Env[]> {
|
||||||
const envs = await this.envs();
|
const envs = await this.envs();
|
||||||
let position = initPosition;
|
let position = initEnvPosition;
|
||||||
if (
|
if (envs && envs.length > 0 && envs[envs.length - 1].position) {
|
||||||
envs &&
|
position = envs[envs.length - 1].position as number;
|
||||||
envs.length > 0 &&
|
|
||||||
typeof envs[envs.length - 1].position === 'number'
|
|
||||||
) {
|
|
||||||
position = envs[envs.length - 1].position!;
|
|
||||||
}
|
}
|
||||||
const tabs = payloads.map((x) => {
|
const tabs = payloads.map((x) => {
|
||||||
position = position - stepPosition;
|
position = position / 2;
|
||||||
const tab = new Env({ ...x, position });
|
const tab = new Env({ ...x, position });
|
||||||
return tab;
|
return tab;
|
||||||
});
|
});
|
||||||
const docs = await this.insert(tabs);
|
const docs = await this.insert(tabs);
|
||||||
await this.set_envs();
|
await this.set_envs();
|
||||||
await this.checkPosition(tabs[tabs.length - 1].position!);
|
|
||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async insert(payloads: Env[]): Promise<Env[]> {
|
public async insert(payloads: Env[]): Promise<Env[]> {
|
||||||
const result: Env[] = [];
|
return new Promise((resolve) => {
|
||||||
for (const env of payloads) {
|
this.envDb.insert(payloads, (err, docs) => {
|
||||||
const doc = await EnvModel.create(env, { returning: true });
|
if (err) {
|
||||||
result.push(doc);
|
this.logger.error(err);
|
||||||
}
|
} else {
|
||||||
return result;
|
resolve(docs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async update(payload: Env): Promise<Env> {
|
public async update(payload: Env): Promise<Env> {
|
||||||
const doc = await this.getDb({ id: payload.id });
|
const { _id, ...other } = payload;
|
||||||
const tab = new Env({ ...doc, ...payload });
|
const doc = await this.get(_id);
|
||||||
|
const tab = new Env({ ...doc, ...other });
|
||||||
const newDoc = await this.updateDb(tab);
|
const newDoc = await this.updateDb(tab);
|
||||||
await this.set_envs();
|
await this.set_envs();
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateDb(payload: Env): Promise<Env> {
|
private async updateDb(payload: Env): Promise<Env> {
|
||||||
await EnvModel.update({ ...payload }, { where: { id: payload.id } });
|
return new Promise((resolve) => {
|
||||||
return await this.getDb({ id: payload.id });
|
this.envDb.update(
|
||||||
|
{ _id: payload._id },
|
||||||
|
payload,
|
||||||
|
{ returnUpdatedDocs: true },
|
||||||
|
(err, num, doc) => {
|
||||||
|
if (err) {
|
||||||
|
this.logger.error(err);
|
||||||
|
} else {
|
||||||
|
resolve(doc as Env);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async remove(ids: number[]) {
|
public async remove(ids: string[]) {
|
||||||
await EnvModel.destroy({ where: { id: ids } });
|
return new Promise((resolve: any) => {
|
||||||
await this.set_envs();
|
this.envDb.remove({ _id: { $in: ids } }, { multi: true }, async (err) => {
|
||||||
|
await this.set_envs();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async move(
|
public async move(
|
||||||
id: number,
|
_id: string,
|
||||||
{
|
{
|
||||||
fromIndex,
|
fromIndex,
|
||||||
toIndex,
|
toIndex,
|
||||||
@@ -82,161 +92,141 @@ export default class EnvService {
|
|||||||
const envs = await this.envs();
|
const envs = await this.envs();
|
||||||
if (toIndex === 0 || toIndex === envs.length - 1) {
|
if (toIndex === 0 || toIndex === envs.length - 1) {
|
||||||
targetPosition = isUpward
|
targetPosition = isUpward
|
||||||
? envs[0].position! + stepPosition
|
? envs[0].position * 2
|
||||||
: envs[toIndex].position! - stepPosition;
|
: envs[toIndex].position / 2;
|
||||||
} else {
|
} else {
|
||||||
targetPosition = isUpward
|
targetPosition = isUpward
|
||||||
? (envs[toIndex].position! + envs[toIndex - 1].position!) / 2
|
? (envs[toIndex].position + envs[toIndex - 1].position) / 2
|
||||||
: (envs[toIndex].position! + envs[toIndex + 1].position!) / 2;
|
: (envs[toIndex].position + envs[toIndex + 1].position) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newDoc = await this.update({
|
const newDoc = await this.update({
|
||||||
id,
|
_id,
|
||||||
position: this.getPrecisionPosition(targetPosition),
|
position: targetPosition,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.checkPosition(targetPosition, envs[toIndex].position!);
|
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkPosition(position: number, edge: number = 0) {
|
public async envs(
|
||||||
const precisionPosition = parseFloat(position.toPrecision(16));
|
searchText: string = '',
|
||||||
if (
|
sort: any = { position: -1 },
|
||||||
precisionPosition < minPosition ||
|
query: any = {},
|
||||||
precisionPosition > maxPosition ||
|
): Promise<Env[]> {
|
||||||
Math.abs(precisionPosition - edge) < minPosition
|
|
||||||
) {
|
|
||||||
const envs = await this.envs();
|
|
||||||
let position = initPosition;
|
|
||||||
for (const env of envs) {
|
|
||||||
position = position - stepPosition;
|
|
||||||
await this.updateDb({ id: env.id, position });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPrecisionPosition(position: number): number {
|
|
||||||
return parseFloat(position.toPrecision(16));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async envs(searchText: string = '', query: any = {}): Promise<Env[]> {
|
|
||||||
let condition = { ...query };
|
let condition = { ...query };
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
const encodeText = encodeURI(searchText);
|
const reg = new RegExp(searchText);
|
||||||
const reg = {
|
|
||||||
[Op.or]: [
|
|
||||||
{ [Op.like]: `%${searchText}%` },
|
|
||||||
{ [Op.like]: `%${encodeText}%` },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
condition = {
|
condition = {
|
||||||
...condition,
|
$or: [
|
||||||
[Op.or]: [
|
|
||||||
{
|
|
||||||
name: reg,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
value: reg,
|
value: reg,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: reg,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
remarks: reg,
|
remarks: reg,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
try {
|
const newDocs = await this.find(condition, sort);
|
||||||
const result = await this.find(condition, [
|
return newDocs;
|
||||||
[sequelize.literal('COALESCE(`isPinned`, 0)'), 'DESC'],
|
|
||||||
['position', 'DESC'],
|
|
||||||
['createdAt', 'ASC'],
|
|
||||||
]);
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async find(query: any, sort: any = []): Promise<Env[]> {
|
private async find(query: any, sort: any): Promise<Env[]> {
|
||||||
const docs = await EnvModel.findAll({
|
return new Promise((resolve) => {
|
||||||
where: { ...query },
|
this.envDb
|
||||||
order: [...sort],
|
.find(query)
|
||||||
|
.sort({ ...sort })
|
||||||
|
.exec((err, docs) => {
|
||||||
|
resolve(docs);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
return docs.map((x) => x.get({ plain: true }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDb(query: FindOptions<Env>['where']): Promise<Env> {
|
public async get(_id: string): Promise<Env> {
|
||||||
const doc: any = await EnvModel.findOne({ where: { ...query } });
|
return new Promise((resolve) => {
|
||||||
if (!doc) {
|
this.envDb.find({ _id }).exec((err, docs) => {
|
||||||
throw new Error(`Env ${JSON.stringify(query)} not found`);
|
resolve(docs[0]);
|
||||||
}
|
});
|
||||||
return doc.get({ plain: true });
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async disabled(ids: number[]) {
|
public async getBySort(sort: any): Promise<Env> {
|
||||||
await EnvModel.update(
|
return new Promise((resolve) => {
|
||||||
{ status: EnvStatus.disabled },
|
this.envDb
|
||||||
{ where: { id: ids } },
|
.find({})
|
||||||
);
|
.sort({ ...sort })
|
||||||
await this.set_envs();
|
.limit(1)
|
||||||
|
.exec((err, docs) => {
|
||||||
|
resolve(docs[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async enabled(ids: number[]) {
|
public async disabled(ids: string[]) {
|
||||||
await EnvModel.update({ status: EnvStatus.normal }, { where: { id: ids } });
|
return new Promise((resolve: any) => {
|
||||||
await this.set_envs();
|
this.envDb.update(
|
||||||
|
{ _id: { $in: ids } },
|
||||||
|
{ $set: { status: EnvStatus.disabled } },
|
||||||
|
{ multi: true },
|
||||||
|
async (err) => {
|
||||||
|
await this.set_envs();
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateNames({ ids, name }: { ids: number[]; name: string }) {
|
public async enabled(ids: string[]) {
|
||||||
await EnvModel.update({ name }, { where: { id: ids } });
|
return new Promise((resolve: any) => {
|
||||||
await this.set_envs();
|
this.envDb.update(
|
||||||
|
{ _id: { $in: ids } },
|
||||||
|
{ $set: { status: EnvStatus.normal } },
|
||||||
|
{ multi: true },
|
||||||
|
async (err, num) => {
|
||||||
|
await this.set_envs();
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async pin(ids: number[]) {
|
public async updateNames({ ids, name }: { ids: string[]; name: string }) {
|
||||||
await EnvModel.update({ isPinned: 1 }, { where: { id: ids } });
|
return new Promise((resolve: any) => {
|
||||||
}
|
this.envDb.update(
|
||||||
|
{ _id: { $in: ids } },
|
||||||
public async unPin(ids: number[]) {
|
{ $set: { name } },
|
||||||
await EnvModel.update({ isPinned: 0 }, { where: { id: ids } });
|
{ multi: true },
|
||||||
|
async (err, num) => {
|
||||||
|
await this.set_envs();
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async set_envs() {
|
public async set_envs() {
|
||||||
const envs = await this.envs('', {
|
const envs = await this.envs(
|
||||||
name: { [Op.not]: null },
|
'',
|
||||||
status: EnvStatus.normal,
|
{ position: -1 },
|
||||||
});
|
{ name: { $exists: true } },
|
||||||
const groups = groupBy(envs, 'name');
|
);
|
||||||
|
const groups = _.groupBy(envs, 'name');
|
||||||
let env_string = '';
|
let env_string = '';
|
||||||
let js_env_string = '';
|
|
||||||
let py_env_string = 'import os\n';
|
|
||||||
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];
|
||||||
|
|
||||||
// 忽略不符合bash要求的环境变量名称
|
// 忽略不符合bash要求的环境变量名称
|
||||||
if (/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(key)) {
|
if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) {
|
||||||
let value = group
|
env_string += `export ${key}="${_(group)
|
||||||
.map((x) => x.value)
|
.filter((x) => x.status !== EnvStatus.disabled)
|
||||||
|
.map('value')
|
||||||
.join('&')
|
.join('&')
|
||||||
.replace(/'/g, "'\\''")
|
.replace(/ /g, '')}"\n`;
|
||||||
.trim();
|
|
||||||
env_string += `export ${key}='${value}'\n`;
|
|
||||||
const _env_value = `${group
|
|
||||||
.map((x) => x.value)
|
|
||||||
.join('&')
|
|
||||||
.replace(/\\/g, '\\\\')}`;
|
|
||||||
js_env_string += `process.env.${key}=\`${_env_value.replace(
|
|
||||||
/\`/g,
|
|
||||||
'\\`',
|
|
||||||
)}\`;\n`;
|
|
||||||
py_env_string += `os.environ['${key}']='''${_env_value.replace(
|
|
||||||
/\'/g,
|
|
||||||
"\\'",
|
|
||||||
)}'''\n`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await writeFileWithLock(config.envFile, env_string);
|
fs.writeFileSync(config.envFile, env_string);
|
||||||
await writeFileWithLock(config.jsEnvFile, js_env_string);
|
|
||||||
await writeFileWithLock(config.pyEnvFile, py_env_string);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
import { Server, ServerCredentials } from '@grpc/grpc-js';
|
|
||||||
import { CronService } from '../protos/cron';
|
|
||||||
import { HealthService } from '../protos/health';
|
|
||||||
import { ApiService } from '../protos/api';
|
|
||||||
import { addCron } from '../schedule/addCron';
|
|
||||||
import { delCron } from '../schedule/delCron';
|
|
||||||
import { check } from '../schedule/health';
|
|
||||||
import * as Api from '../schedule/api';
|
|
||||||
import Logger from '../loaders/logger';
|
|
||||||
import { promisify } from 'util';
|
|
||||||
import config from '../config';
|
|
||||||
import { metricsService } from './metrics';
|
|
||||||
import { Service } from 'typedi';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export class GrpcServerService {
|
|
||||||
private server: Server = new Server({ 'grpc.enable_http_proxy': 0 });
|
|
||||||
|
|
||||||
async initialize() {
|
|
||||||
try {
|
|
||||||
this.server.addService(HealthService, { check });
|
|
||||||
this.server.addService(CronService, { addCron, delCron });
|
|
||||||
this.server.addService(ApiService, Api);
|
|
||||||
|
|
||||||
const grpcPort = config.grpcPort;
|
|
||||||
const bindAsync = promisify(this.server.bindAsync).bind(this.server);
|
|
||||||
await bindAsync(
|
|
||||||
`0.0.0.0:${grpcPort}`,
|
|
||||||
ServerCredentials.createInsecure(),
|
|
||||||
);
|
|
||||||
Logger.debug(`✌️ gRPC service started successfully`);
|
|
||||||
|
|
||||||
metricsService.record('grpc_service_start', 1, {
|
|
||||||
port: grpcPort.toString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return grpcPort;
|
|
||||||
} catch (err) {
|
|
||||||
Logger.error('Failed to start gRPC service:', err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async shutdown() {
|
|
||||||
try {
|
|
||||||
if (this.server) {
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
this.server.tryShutdown(() => {
|
|
||||||
Logger.debug('gRPC service stopped');
|
|
||||||
metricsService.record('grpc_service_stop', 1);
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Logger.error('Error while shutting down gRPC service:', err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getServer() {
|
|
||||||
return this.server;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { Service } from 'typedi';
|
|
||||||
import Logger from '../loaders/logger';
|
|
||||||
import { GrpcServerService } from './grpc';
|
|
||||||
import { HttpServerService } from './http';
|
|
||||||
|
|
||||||
interface HealthStatus {
|
|
||||||
status: 'ok' | 'error';
|
|
||||||
services: {
|
|
||||||
http: boolean;
|
|
||||||
grpc: boolean;
|
|
||||||
};
|
|
||||||
metrics: {
|
|
||||||
uptime: number;
|
|
||||||
memory: {
|
|
||||||
used: number;
|
|
||||||
total: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export class HealthService {
|
|
||||||
private startTime = Date.now();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private grpcServerService: GrpcServerService,
|
|
||||||
private httpServerService: HttpServerService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async check(): Promise<HealthStatus> {
|
|
||||||
const status: HealthStatus = {
|
|
||||||
status: 'ok',
|
|
||||||
services: {
|
|
||||||
http: true,
|
|
||||||
grpc: true,
|
|
||||||
},
|
|
||||||
metrics: {
|
|
||||||
uptime: Math.floor((Date.now() - this.startTime) / 1000),
|
|
||||||
memory: {
|
|
||||||
used: process.memoryUsage().heapUsed,
|
|
||||||
total: process.memoryUsage().heapTotal,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const httpServer = this.httpServerService.getServer();
|
|
||||||
if (!httpServer) {
|
|
||||||
status.services.http = false;
|
|
||||||
status.status = 'error';
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
status.services.http = false;
|
|
||||||
status.status = 'error';
|
|
||||||
Logger.error('HTTP server check failed:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const grpcServer = this.grpcServerService.getServer();
|
|
||||||
if (!grpcServer) {
|
|
||||||
status.services.grpc = false;
|
|
||||||
status.status = 'error';
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
status.services.grpc = false;
|
|
||||||
status.status = 'error';
|
|
||||||
Logger.error('gRPC server check failed:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import Logger from '../loaders/logger';
|
|
||||||
import { metricsService } from './metrics';
|
|
||||||
import { Service } from 'typedi';
|
|
||||||
import { Server } from 'http';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export class HttpServerService {
|
|
||||||
private server?: Server = undefined;
|
|
||||||
|
|
||||||
async initialize(expressApp: express.Application, port: number) {
|
|
||||||
try {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.server = expressApp.listen(port, '0.0.0.0', () => {
|
|
||||||
Logger.debug(`✌️ HTTP service started successfully`);
|
|
||||||
metricsService.record('http_service_start', 1, {
|
|
||||||
port: port.toString(),
|
|
||||||
});
|
|
||||||
resolve(this.server);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.server?.on('error', (err: Error) => {
|
|
||||||
Logger.error('Failed to start HTTP service:', err);
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Logger.error('Failed to start HTTP service:', err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async shutdown() {
|
|
||||||
try {
|
|
||||||
if (this.server) {
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
this.server?.close(() => {
|
|
||||||
Logger.debug('HTTP service stopped');
|
|
||||||
metricsService.record('http_service_stop', 1);
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Logger.error('Error while shutting down HTTP service:', err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getServer() {
|
|
||||||
return this.server;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import path from 'path';
|
|
||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import winston from 'winston';
|
|
||||||
import config from '../config';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export default class LogService {
|
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
|
||||||
|
|
||||||
public checkFilePath(filePath: string, fileName: string) {
|
|
||||||
const finalPath = path.resolve(config.logPath, filePath, fileName);
|
|
||||||
return finalPath.startsWith(config.logPath) ? finalPath : '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import { performance } from 'perf_hooks';
|
|
||||||
import Logger from '../loaders/logger';
|
|
||||||
|
|
||||||
interface Metric {
|
|
||||||
name: string;
|
|
||||||
value: number;
|
|
||||||
timestamp: number;
|
|
||||||
tags?: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
class MetricsService {
|
|
||||||
private metrics: Metric[] = [];
|
|
||||||
private static instance: MetricsService;
|
|
||||||
|
|
||||||
private constructor() {
|
|
||||||
// 定期清理旧数据
|
|
||||||
setInterval(() => {
|
|
||||||
const oneHourAgo = Date.now() - 3600000;
|
|
||||||
this.metrics = this.metrics.filter(m => m.timestamp > oneHourAgo);
|
|
||||||
}, 60000);
|
|
||||||
}
|
|
||||||
|
|
||||||
static getInstance(): MetricsService {
|
|
||||||
if (!MetricsService.instance) {
|
|
||||||
MetricsService.instance = new MetricsService();
|
|
||||||
}
|
|
||||||
return MetricsService.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
record(name: string, value: number, tags?: Record<string, string>) {
|
|
||||||
this.metrics.push({
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
tags,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
measure(name: string, fn: () => void, tags?: Record<string, string>) {
|
|
||||||
const start = performance.now();
|
|
||||||
try {
|
|
||||||
fn();
|
|
||||||
} finally {
|
|
||||||
const duration = performance.now() - start;
|
|
||||||
this.record(name, duration, tags);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async measureAsync(name: string, fn: () => Promise<void>, tags?: Record<string, string>) {
|
|
||||||
const start = performance.now();
|
|
||||||
try {
|
|
||||||
await fn();
|
|
||||||
} finally {
|
|
||||||
const duration = performance.now() - start;
|
|
||||||
this.record(name, duration, tags);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getMetrics(name?: string, tags?: Record<string, string>) {
|
|
||||||
let filtered = this.metrics;
|
|
||||||
|
|
||||||
if (name) {
|
|
||||||
filtered = filtered.filter(m => m.name === name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tags) {
|
|
||||||
filtered = filtered.filter(m => {
|
|
||||||
if (!m.tags) return false;
|
|
||||||
return Object.entries(tags).every(([key, value]) => m.tags![key] === value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
count: filtered.length,
|
|
||||||
average: filtered.reduce((acc, curr) => acc + curr.value, 0) / filtered.length,
|
|
||||||
min: Math.min(...filtered.map(m => m.value)),
|
|
||||||
max: Math.max(...filtered.map(m => m.value)),
|
|
||||||
metrics: filtered,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
report() {
|
|
||||||
const report = {
|
|
||||||
timestamp: Date.now(),
|
|
||||||
metrics: this.getMetrics(),
|
|
||||||
};
|
|
||||||
Logger.info('性能指标报告:', report);
|
|
||||||
return report;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const metricsService = MetricsService.getInstance();
|
|
||||||
+153
-660
@@ -1,11 +1,11 @@
|
|||||||
import crypto from 'crypto';
|
|
||||||
import nodemailer from 'nodemailer';
|
|
||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import { parseBody, parseHeaders } from '../config/util';
|
|
||||||
import { NotificationInfo } from '../data/notify';
|
import { NotificationInfo } from '../data/notify';
|
||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
import UserService from './user';
|
import UserService from './user';
|
||||||
import { httpClient } from '../config/http';
|
import got from 'got';
|
||||||
import { ProxyAgent } from 'undici';
|
import nodemailer from 'nodemailer';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class NotificationService {
|
export default class NotificationService {
|
||||||
@@ -16,59 +16,37 @@ export default class NotificationService {
|
|||||||
['gotify', this.gotify],
|
['gotify', this.gotify],
|
||||||
['goCqHttpBot', this.goCqHttpBot],
|
['goCqHttpBot', this.goCqHttpBot],
|
||||||
['serverChan', this.serverChan],
|
['serverChan', this.serverChan],
|
||||||
['pushDeer', this.pushDeer],
|
|
||||||
['chat', this.chat],
|
|
||||||
['bark', this.bark],
|
['bark', this.bark],
|
||||||
['telegramBot', this.telegramBot],
|
['telegramBot', this.telegramBot],
|
||||||
['dingtalkBot', this.dingtalkBot],
|
['dingtalkBot', this.dingtalkBot],
|
||||||
['weWorkBot', this.weWorkBot],
|
['weWorkBot', this.weWorkBot],
|
||||||
['weWorkApp', this.weWorkApp],
|
['weWorkApp', this.weWorkApp],
|
||||||
['aibotk', this.aibotk],
|
|
||||||
['iGot', this.iGot],
|
['iGot', this.iGot],
|
||||||
['pushPlus', this.pushPlus],
|
['pushPlus', this.pushPlus],
|
||||||
['wePlusBot', this.wePlusBot],
|
|
||||||
['email', this.email],
|
['email', this.email],
|
||||||
['pushMe', this.pushMe],
|
|
||||||
['webhook', this.webhook],
|
|
||||||
['lark', this.lark],
|
|
||||||
['chronocat', this.chronocat],
|
|
||||||
['ntfy', this.ntfy],
|
|
||||||
['wxPusherBot', this.wxPusherBot],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
private timeout = 10000;
|
||||||
private title = '';
|
private title = '';
|
||||||
private content = '';
|
private content = '';
|
||||||
private params!: Omit<NotificationInfo, 'type'>;
|
private params!: Omit<NotificationInfo, 'type'>;
|
||||||
private gotOption = {
|
|
||||||
timeout: 10000,
|
|
||||||
retry: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {}
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
public async notify(
|
public async notify(
|
||||||
title: string,
|
title: string,
|
||||||
content: string,
|
content: string,
|
||||||
notificationInfo?: NotificationInfo,
|
|
||||||
): Promise<boolean | undefined> {
|
): Promise<boolean | undefined> {
|
||||||
let { type, ...rest } = await this.userService.getNotificationMode();
|
const { type, ...rest } = await this.userService.getNotificationMode();
|
||||||
if (notificationInfo?.type) {
|
|
||||||
type = notificationInfo?.type;
|
|
||||||
}
|
|
||||||
if (type) {
|
if (type) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.content = content;
|
this.content = content;
|
||||||
let params = rest;
|
this.params = rest;
|
||||||
if (notificationInfo) {
|
|
||||||
const { type: _, ...others } = notificationInfo;
|
|
||||||
params = { ...rest, ...others };
|
|
||||||
}
|
|
||||||
this.params = params;
|
|
||||||
const notificationModeAction = this.modeMap.get(type);
|
const notificationModeAction = this.modeMap.get(type);
|
||||||
try {
|
try {
|
||||||
return await notificationModeAction?.call(this);
|
return await notificationModeAction?.call(this);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -91,156 +69,73 @@ export default class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async gotify() {
|
private async gotify() {
|
||||||
const { gotifyUrl, gotifyToken, gotifyPriority = 1 } = this.params;
|
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
|
||||||
try {
|
const res: any = await got
|
||||||
const res = await httpClient.post(
|
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
|
||||||
`${gotifyUrl}/message?token=${gotifyToken}`,
|
timeout: this.timeout,
|
||||||
{
|
retry: 0,
|
||||||
...this.gotOption,
|
body: `title=${encodeURIComponent(
|
||||||
body: `title=${encodeURIComponent(
|
this.title,
|
||||||
this.title,
|
)}&message=${encodeURIComponent(
|
||||||
)}&message=${encodeURIComponent(
|
this.content,
|
||||||
this.content,
|
)}&priority=${gotifyPriority}`,
|
||||||
)}&priority=${gotifyPriority}`,
|
headers: {
|
||||||
headers: {
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
})
|
||||||
if (typeof res.id === 'number') {
|
.json();
|
||||||
return true;
|
return typeof res.id === 'number';
|
||||||
} else {
|
|
||||||
throw new Error(JSON.stringify(res));
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.response ? error.response.body : error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async goCqHttpBot() {
|
private async goCqHttpBot() {
|
||||||
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
|
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
|
||||||
try {
|
const res: any = await got
|
||||||
const res = await httpClient.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
|
.post(
|
||||||
...this.gotOption,
|
`${goCqHttpBotUrl}?access_token=${goCqHttpBotToken}&${goCqHttpBotQq}`,
|
||||||
json: { message: `${this.title}\n${this.content}` },
|
{
|
||||||
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
|
timeout: this.timeout,
|
||||||
});
|
retry: 0,
|
||||||
if (res.retcode === 0) {
|
json: { message: `${this.title}\n${this.content}` },
|
||||||
return true;
|
},
|
||||||
} else {
|
)
|
||||||
throw new Error(JSON.stringify(res));
|
.json();
|
||||||
}
|
return res.retcode === 0;
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.response ? error.response.body : error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async serverChan() {
|
private async serverChan() {
|
||||||
const { serverChanKey } = this.params;
|
const { serverChanKey } = this.params;
|
||||||
const matchResult = serverChanKey.match(/^sctp(\d+)t/i);
|
const url = serverChanKey.startsWith('SCT')
|
||||||
const url =
|
? `https://sctapi.ftqq.com/${serverChanKey}.send`
|
||||||
matchResult && matchResult[1]
|
: `https://sc.ftqq.com/${serverChanKey}.send`;
|
||||||
? `https://${matchResult[1]}.push.ft07.com/send/${serverChanKey}.send`
|
const res: any = await got
|
||||||
: `https://sctapi.ftqq.com/${serverChanKey}.send`;
|
.post(url, {
|
||||||
|
timeout: this.timeout,
|
||||||
try {
|
retry: 0,
|
||||||
const res = await httpClient.post(url, {
|
body: `title=${this.title}&desp=${this.content}`,
|
||||||
...this.gotOption,
|
|
||||||
body: `title=${encodeURIComponent(
|
|
||||||
this.title,
|
|
||||||
)}&desp=${encodeURIComponent(this.content)}`,
|
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
});
|
})
|
||||||
if (res.errno === 0 || res.data.errno === 0) {
|
.json();
|
||||||
return true;
|
return res.errno === 0 || res.data.errno === 0;
|
||||||
} else {
|
|
||||||
throw new Error(JSON.stringify(res));
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.response ? error.response.body : error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async pushDeer() {
|
|
||||||
const { pushDeerKey, pushDeerUrl } = this.params;
|
|
||||||
const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`;
|
|
||||||
try {
|
|
||||||
const res = await httpClient.post(url, {
|
|
||||||
...this.gotOption,
|
|
||||||
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
|
|
||||||
this.title,
|
|
||||||
)}&desp=${encodeURIComponent(this.content)}&type=markdown`,
|
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
||||||
});
|
|
||||||
if (
|
|
||||||
res.content.result.length !== undefined &&
|
|
||||||
res.content.result.length > 0
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(JSON.stringify(res));
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.response ? error.response.body : error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async chat() {
|
|
||||||
const { synologyChatUrl } = this.params;
|
|
||||||
try {
|
|
||||||
const res = await httpClient.post(synologyChatUrl, {
|
|
||||||
...this.gotOption,
|
|
||||||
body: `payload={"text":"${this.title}\n${this.content}"}`,
|
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
||||||
});
|
|
||||||
if (res.success) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(JSON.stringify(res));
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.response ? error.response.body : error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async bark() {
|
private async bark() {
|
||||||
let {
|
let { barkPush, barkSound, barkGroup } = this.params;
|
||||||
barkPush,
|
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
|
||||||
barkIcon = '',
|
|
||||||
barkSound = '',
|
|
||||||
barkGroup = '',
|
|
||||||
barkLevel = '',
|
|
||||||
barkUrl = '',
|
|
||||||
barkArchive = '',
|
|
||||||
} = this.params;
|
|
||||||
if (!barkPush.startsWith('http')) {
|
|
||||||
barkPush = `https://api.day.app/${barkPush}`;
|
barkPush = `https://api.day.app/${barkPush}`;
|
||||||
}
|
}
|
||||||
const url = `${barkPush}`;
|
const url = `${barkPush}/${encodeURIComponent(
|
||||||
const body = {
|
this.title,
|
||||||
title: this.title,
|
)}/${encodeURIComponent(
|
||||||
body: this.content,
|
this.content,
|
||||||
icon: barkIcon,
|
)}?sound=${barkSound}&group=${barkGroup}`;
|
||||||
sound: barkSound,
|
const res: any = await got
|
||||||
group: barkGroup,
|
.get(url, {
|
||||||
isArchive: barkArchive,
|
timeout: this.timeout,
|
||||||
level: barkLevel,
|
retry: 0,
|
||||||
url: barkUrl,
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
};
|
})
|
||||||
try {
|
.json();
|
||||||
const res = await httpClient.post(url, {
|
return res.code === 200;
|
||||||
...this.gotOption,
|
|
||||||
json: body,
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
});
|
|
||||||
if (res.code === 200) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(JSON.stringify(res));
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.response ? error.response.body : error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async telegramBot() {
|
private async telegramBot() {
|
||||||
@@ -253,30 +148,35 @@ export default class NotificationService {
|
|||||||
telegramBotUserId,
|
telegramBotUserId,
|
||||||
} = this.params;
|
} = this.params;
|
||||||
const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : '';
|
const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : '';
|
||||||
const url = `${
|
const url = `https://${
|
||||||
telegramBotApiHost ? telegramBotApiHost : 'https://api.telegram.org'
|
telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org'
|
||||||
}/bot${telegramBotToken}/sendMessage`;
|
}/bot${telegramBotToken}/sendMessage`;
|
||||||
let agent;
|
let agent;
|
||||||
if (telegramBotProxyHost && telegramBotProxyPort) {
|
if (telegramBotProxyHost && telegramBotProxyPort) {
|
||||||
agent = new ProxyAgent({
|
const options: any = {
|
||||||
uri: `http://${authStr}${telegramBotProxyHost}:${telegramBotProxyPort}`,
|
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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
try {
|
const res: any = await got
|
||||||
const res = await httpClient.post(url, {
|
.post(url, {
|
||||||
...this.gotOption,
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
|
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
dispatcher: agent,
|
agent,
|
||||||
});
|
})
|
||||||
if (res.ok) {
|
.json();
|
||||||
return true;
|
return !!res.ok;
|
||||||
} else {
|
|
||||||
throw new Error(JSON.stringify(res));
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.response ? error.response.body : error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async dingtalkBot() {
|
private async dingtalkBot() {
|
||||||
@@ -290,63 +190,54 @@ export default class NotificationService {
|
|||||||
secretParam = `×tamp=${dateNow}&sign=${result}`;
|
secretParam = `×tamp=${dateNow}&sign=${result}`;
|
||||||
}
|
}
|
||||||
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
|
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
|
||||||
try {
|
const res: any = await got
|
||||||
const res = await httpClient.post(url, {
|
.post(url, {
|
||||||
...this.gotOption,
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
json: {
|
json: {
|
||||||
msgtype: 'text',
|
msgtype: 'text',
|
||||||
text: {
|
text: {
|
||||||
content: ` ${this.title}\n\n${this.content}`,
|
content: ` ${this.title}\n\n${this.content}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
if (res.errcode === 0) {
|
.json();
|
||||||
return true;
|
return res.errcode === 0;
|
||||||
} else {
|
|
||||||
throw new Error(JSON.stringify(res));
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.response ? error.response.body : error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async weWorkBot() {
|
private async weWorkBot() {
|
||||||
const { weWorkBotKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } =
|
const { weWorkBotKey } = this.params;
|
||||||
this.params;
|
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`;
|
||||||
const url = `${weWorkOrigin}/cgi-bin/webhook/send?key=${weWorkBotKey}`;
|
const res: any = await got
|
||||||
try {
|
.post(url, {
|
||||||
const res = await httpClient.post(url, {
|
timeout: this.timeout,
|
||||||
...this.gotOption,
|
retry: 0,
|
||||||
json: {
|
json: {
|
||||||
msgtype: 'text',
|
msgtype: 'text',
|
||||||
text: {
|
text: {
|
||||||
content: ` ${this.title}\n\n${this.content}`,
|
content: ` ${this.title}\n\n${this.content}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
if (res.errcode === 0) {
|
.json();
|
||||||
return true;
|
return res.errcode === 0;
|
||||||
} else {
|
|
||||||
throw new Error(JSON.stringify(res));
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.response ? error.response.body : error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async weWorkApp() {
|
private async weWorkApp() {
|
||||||
const { weWorkAppKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } =
|
const { weWorkAppKey } = this.params;
|
||||||
this.params;
|
|
||||||
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
|
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
|
||||||
weWorkAppKey.split(',');
|
weWorkAppKey.split(',');
|
||||||
const url = `${weWorkOrigin}/cgi-bin/gettoken`;
|
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
|
||||||
const tokenRes = await httpClient.post(url, {
|
const { access_token } = await got
|
||||||
...this.gotOption,
|
.post(url, {
|
||||||
json: {
|
timeout: this.timeout,
|
||||||
corpid,
|
retry: 0,
|
||||||
corpsecret,
|
json: {
|
||||||
},
|
corpid,
|
||||||
});
|
corpsecret,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
|
||||||
let options: any = {
|
let options: any = {
|
||||||
msgtype: 'mpnews',
|
msgtype: 'mpnews',
|
||||||
@@ -386,11 +277,12 @@ export default class NotificationService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const res: any = await got
|
||||||
const res = await httpClient.post(
|
.post(
|
||||||
`${weWorkOrigin}/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
|
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${access_token}`,
|
||||||
{
|
{
|
||||||
...this.gotOption,
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
json: {
|
json: {
|
||||||
touser,
|
touser,
|
||||||
agentid,
|
agentid,
|
||||||
@@ -398,464 +290,65 @@ export default class NotificationService {
|
|||||||
...options,
|
...options,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
.json();
|
||||||
|
|
||||||
if (res.errcode === 0) {
|
return res.errcode === 0;
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(JSON.stringify(res));
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.response ? error.response.body : error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async aibotk() {
|
|
||||||
const { aibotkKey, aibotkType, aibotkName } = this.params;
|
|
||||||
let url = '';
|
|
||||||
let json = {};
|
|
||||||
switch (aibotkType) {
|
|
||||||
case 'room':
|
|
||||||
url = 'https://api-bot.aibotk.com/openapi/v1/chat/room';
|
|
||||||
json = {
|
|
||||||
apiKey: `${aibotkKey}`,
|
|
||||||
roomName: `${aibotkName}`,
|
|
||||||
message: {
|
|
||||||
type: 1,
|
|
||||||
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case 'contact':
|
|
||||||
url = 'https://api-bot.aibotk.com/openapi/v1/chat/contact';
|
|
||||||
json = {
|
|
||||||
apiKey: `${aibotkKey}`,
|
|
||||||
name: `${aibotkName}`,
|
|
||||||
message: {
|
|
||||||
type: 1,
|
|
||||||
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await httpClient.post(url, {
|
|
||||||
...this.gotOption,
|
|
||||||
json: {
|
|
||||||
...json,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (res.code === 0) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(JSON.stringify(res));
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.response ? error.response.body : error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async iGot() {
|
private async iGot() {
|
||||||
const { iGotPushKey } = this.params;
|
const { iGotPushKey } = this.params;
|
||||||
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
|
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
|
||||||
try {
|
const res: any = await got
|
||||||
const res = await httpClient.post(url, {
|
.post(url, {
|
||||||
...this.gotOption,
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
body: `title=${this.title}&content=${this.content}`,
|
body: `title=${this.title}&content=${this.content}`,
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
});
|
})
|
||||||
|
.json();
|
||||||
|
|
||||||
if (res.ret === 0) {
|
return res.ret === 0;
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(JSON.stringify(res));
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.response ? error.response.body : error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async pushPlus() {
|
private async pushPlus() {
|
||||||
const {
|
const { pushPlusToken, pushPlusUser } = this.params;
|
||||||
pushPlusToken,
|
|
||||||
pushPlusUser,
|
|
||||||
pushplusWebhook,
|
|
||||||
pushPlusTemplate,
|
|
||||||
pushplusChannel,
|
|
||||||
pushplusCallbackUrl,
|
|
||||||
pushplusTo,
|
|
||||||
} = this.params;
|
|
||||||
const url = `https://www.pushplus.plus/send`;
|
const url = `https://www.pushplus.plus/send`;
|
||||||
try {
|
const res: any = await got
|
||||||
let body = {
|
.post(url, {
|
||||||
...this.gotOption,
|
timeout: this.timeout,
|
||||||
|
retry: 0,
|
||||||
json: {
|
json: {
|
||||||
token: `${pushPlusToken}`,
|
token: `${pushPlusToken}`,
|
||||||
title: `${this.title}`,
|
title: `${this.title}`,
|
||||||
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
|
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
|
||||||
topic: `${pushPlusUser || ''}`,
|
topic: `${pushPlusUser || ''}`,
|
||||||
template: `${pushPlusTemplate || 'html'}`,
|
|
||||||
channel: `${pushplusChannel || 'wechat'}`,
|
|
||||||
webhook: `${pushplusWebhook || ''}`,
|
|
||||||
callbackUrl: `${pushplusCallbackUrl || ''}`,
|
|
||||||
to: `${pushplusTo || ''}`,
|
|
||||||
},
|
},
|
||||||
};
|
})
|
||||||
|
.json();
|
||||||
|
|
||||||
const res = await httpClient.post(url, body);
|
return res.code === 200;
|
||||||
|
|
||||||
if (res.code === 200) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(JSON.stringify(res));
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.response ? error.response.body : error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async wePlusBot() {
|
|
||||||
const { wePlusBotToken, wePlusBotReceiver, wePlusBotVersion } = this.params;
|
|
||||||
|
|
||||||
let content = this.content;
|
|
||||||
let template = 'txt';
|
|
||||||
if (this.content.length > 800) {
|
|
||||||
template = 'html';
|
|
||||||
content = content.replace(/[\n\r]/g, '<br>');
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = `https://www.weplusbot.com/send`;
|
|
||||||
try {
|
|
||||||
const res = await httpClient.post(url, {
|
|
||||||
...this.gotOption,
|
|
||||||
json: {
|
|
||||||
token: `${wePlusBotToken}`,
|
|
||||||
title: `${this.title}`,
|
|
||||||
template: `${template}`,
|
|
||||||
content: `${content}`,
|
|
||||||
receiver: `${wePlusBotReceiver || ''}`,
|
|
||||||
version: `${wePlusBotVersion || 'pro'}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.code === 200) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(JSON.stringify(res));
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.response ? error.response.body : error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async lark() {
|
|
||||||
let { larkKey, larkSecret } = this.params;
|
|
||||||
|
|
||||||
if (!larkKey.startsWith('http')) {
|
|
||||||
larkKey = `https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const body: Record<string, any> = {
|
|
||||||
msg_type: 'text',
|
|
||||||
content: { text: `${this.title}\n\n${this.content}` },
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add signature if secret is provided
|
|
||||||
// Note: Feishu's signature algorithm uses timestamp+"\n"+secret as the HMAC key
|
|
||||||
// and signs an empty message, which differs from typical HMAC usage
|
|
||||||
if (larkSecret) {
|
|
||||||
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
||||||
const stringToSign = `${timestamp}\n${larkSecret}`;
|
|
||||||
const hmac = crypto.createHmac('sha256', stringToSign);
|
|
||||||
const sign = hmac.digest('base64');
|
|
||||||
body.timestamp = timestamp;
|
|
||||||
body.sign = sign;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await httpClient.post(larkKey, {
|
|
||||||
...this.gotOption,
|
|
||||||
json: body,
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
});
|
|
||||||
if (res.StatusCode === 0 || res.code === 0) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(JSON.stringify(res));
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.response ? error.response.body : error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async email() {
|
private async email() {
|
||||||
const { emailPass, emailService, emailUser, emailTo } = this.params;
|
const { emailPass, emailService, emailUser } = this.params;
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
service: emailService,
|
||||||
|
auth: {
|
||||||
|
user: emailUser,
|
||||||
|
pass: emailPass,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
const info = await transporter.sendMail({
|
||||||
const transporter = nodemailer.createTransport({
|
from: `"青龙快讯" <${emailUser}>`,
|
||||||
service: emailService,
|
to: `${emailUser}`,
|
||||||
auth: {
|
subject: `${this.title}`,
|
||||||
user: emailUser,
|
html: `${this.content.replace(/\n/g, '<br/>')}`,
|
||||||
pass: emailPass,
|
});
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const info = await transporter.sendMail({
|
transporter.close();
|
||||||
from: `"青龙快讯" <${emailUser}>`,
|
|
||||||
to: emailTo ? emailTo.split(';') : emailUser,
|
|
||||||
subject: `${this.title}`,
|
|
||||||
html: `${this.content.replace(/\n/g, '<br/>')}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
transporter.close();
|
return !!info.messageId;
|
||||||
|
|
||||||
if (info.messageId) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(JSON.stringify(info));
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async pushMe() {
|
|
||||||
const { pushMeKey, pushMeUrl } = this.params;
|
|
||||||
try {
|
|
||||||
const res = await httpClient.post<'text'>(
|
|
||||||
pushMeUrl || 'https://push.i-i.me/',
|
|
||||||
{
|
|
||||||
...this.gotOption,
|
|
||||||
json: {
|
|
||||||
push_key: pushMeKey,
|
|
||||||
title: this.title,
|
|
||||||
content: this.content,
|
|
||||||
},
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (res === 'success') {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(res);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.response ? error.response.body : error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ntfy() {
|
|
||||||
const {
|
|
||||||
ntfyUrl,
|
|
||||||
ntfyTopic,
|
|
||||||
ntfyPriority,
|
|
||||||
ntfyToken,
|
|
||||||
ntfyUsername,
|
|
||||||
ntfyPassword,
|
|
||||||
ntfyActions,
|
|
||||||
} = this.params;
|
|
||||||
// 编码函数
|
|
||||||
const encodeRfc2047 = (text: string, charset: string = 'UTF-8'): string => {
|
|
||||||
const encodedText = Buffer.from(text).toString('base64');
|
|
||||||
return `=?${charset}?B?${encodedText}?=`;
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
Title: encodeRfc2047(this.title),
|
|
||||||
Priority: `${ntfyPriority || '3'}`,
|
|
||||||
Icon: 'https://qn.whyour.cn/logo.png',
|
|
||||||
};
|
|
||||||
if (ntfyToken) {
|
|
||||||
headers['Authorization'] = `Bearer ${ntfyToken}`;
|
|
||||||
} else if (ntfyUsername && ntfyPassword) {
|
|
||||||
headers['Authorization'] = `Basic ${Buffer.from(
|
|
||||||
`${ntfyUsername}:${ntfyPassword}`,
|
|
||||||
).toString('base64')}`;
|
|
||||||
}
|
|
||||||
if (ntfyActions) {
|
|
||||||
headers['Actions'] = encodeRfc2047(ntfyActions);
|
|
||||||
}
|
|
||||||
const res = await httpClient.request(
|
|
||||||
`${ntfyUrl || 'https://ntfy.sh'}/${ntfyTopic}`,
|
|
||||||
{
|
|
||||||
...this.gotOption,
|
|
||||||
body: `${this.content}`,
|
|
||||||
headers: headers,
|
|
||||||
method: 'POST',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (res.statusCode === 200) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(await res.body.text());
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.response ? error.response.body : error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async wxPusherBot() {
|
|
||||||
const { wxPusherBotAppToken, wxPusherBotTopicIds, wxPusherBotUids } =
|
|
||||||
this.params;
|
|
||||||
// 处理 topicIds,将分号分隔的字符串转为数组
|
|
||||||
const topicIds = wxPusherBotTopicIds
|
|
||||||
? wxPusherBotTopicIds
|
|
||||||
.split(';')
|
|
||||||
.map((id) => id.trim())
|
|
||||||
.filter((id) => id)
|
|
||||||
.map((id) => parseInt(id))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// 处理 uids,将分号分隔的字符串转为数组
|
|
||||||
const uids = wxPusherBotUids
|
|
||||||
? wxPusherBotUids
|
|
||||||
.split(';')
|
|
||||||
.map((uid) => uid.trim())
|
|
||||||
.filter((uid) => uid)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// topic_ids 和 uids 至少要有一个
|
|
||||||
if (!topicIds.length && !uids.length) {
|
|
||||||
throw new Error('wxPusher 服务的 TopicIds 和 Uids 至少配置一个才行');
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = `https://wxpusher.zjiecode.com/api/send/message`;
|
|
||||||
try {
|
|
||||||
const res = await httpClient.post(url, {
|
|
||||||
...this.gotOption,
|
|
||||||
json: {
|
|
||||||
appToken: wxPusherBotAppToken,
|
|
||||||
content: `<h1>${this.title}</h1><br/><div style='white-space: pre-wrap;'>${this.content}</div>`,
|
|
||||||
summary: this.title,
|
|
||||||
contentType: 2,
|
|
||||||
topicIds: topicIds,
|
|
||||||
uids: uids,
|
|
||||||
verifyPayType: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.code === 1000) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(JSON.stringify(res));
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.response ? error.response.body : error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async chronocat() {
|
|
||||||
const { chronocatURL, chronocatQQ, chronocatToken } = this.params;
|
|
||||||
try {
|
|
||||||
const user_ids = chronocatQQ
|
|
||||||
.match(/user_id=(\d+)/g)
|
|
||||||
?.map((match: any) => match.split('=')[1]);
|
|
||||||
const group_ids = chronocatQQ
|
|
||||||
.match(/group_id=(\d+)/g)
|
|
||||||
?.map((match: any) => match.split('=')[1]);
|
|
||||||
|
|
||||||
const url = `${chronocatURL}/api/message/send`;
|
|
||||||
const headers = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${chronocatToken}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [chat_type, ids] of [
|
|
||||||
[1, user_ids],
|
|
||||||
[2, group_ids],
|
|
||||||
]) {
|
|
||||||
if (!ids) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let _ids: any = ids;
|
|
||||||
for (const chat_id of _ids) {
|
|
||||||
const data = {
|
|
||||||
peer: {
|
|
||||||
chatType: chat_type,
|
|
||||||
peerUin: chat_id,
|
|
||||||
},
|
|
||||||
elements: [
|
|
||||||
{
|
|
||||||
elementType: 1,
|
|
||||||
textElement: {
|
|
||||||
content: `${this.title}\n\n${this.content}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const res = await httpClient.request(url, {
|
|
||||||
...this.gotOption,
|
|
||||||
json: data,
|
|
||||||
headers,
|
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
if (res.statusCode === 200) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(await res.body.text());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.response ? error.response.body : error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async webhook() {
|
|
||||||
const {
|
|
||||||
webhookUrl,
|
|
||||||
webhookBody,
|
|
||||||
webhookHeaders,
|
|
||||||
webhookMethod,
|
|
||||||
webhookContentType,
|
|
||||||
} = this.params;
|
|
||||||
|
|
||||||
if (!webhookUrl?.includes('$title') && !webhookBody?.includes('$title')) {
|
|
||||||
throw new Error('Url 或者 Body 中必须包含 $title');
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers = parseHeaders(webhookHeaders);
|
|
||||||
const body = parseBody(webhookBody, webhookContentType, (v) =>
|
|
||||||
v?.replaceAll('$title', this.title)?.replaceAll('$content', this.content),
|
|
||||||
);
|
|
||||||
const bodyParam = this.formatBody(webhookContentType, body);
|
|
||||||
const options = {
|
|
||||||
method: webhookMethod,
|
|
||||||
headers,
|
|
||||||
...this.gotOption,
|
|
||||||
allowGetBody: true,
|
|
||||||
...bodyParam,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const formatUrl = webhookUrl
|
|
||||||
?.replaceAll('$title', encodeURIComponent(this.title))
|
|
||||||
?.replaceAll('$content', encodeURIComponent(this.content));
|
|
||||||
const res = await httpClient.request(formatUrl, options);
|
|
||||||
const text = await res.body.text();
|
|
||||||
if (String(res.statusCode).startsWith('20')) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(await res.body.text());
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.response ? error.response.body : error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatBody(contentType: string, body: any): object {
|
|
||||||
if (!body) return {};
|
|
||||||
switch (contentType) {
|
|
||||||
case 'application/json':
|
|
||||||
return { json: body };
|
|
||||||
case 'multipart/form-data':
|
|
||||||
return { form: body };
|
|
||||||
case 'application/x-www-form-urlencoded':
|
|
||||||
case 'text/plain':
|
|
||||||
return { body };
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+117
-127
@@ -1,77 +1,91 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import { createRandomString } from '../config/util';
|
import { createRandomString } from '../config/util';
|
||||||
import { App, AppModel } from '../data/open';
|
import config from '../config';
|
||||||
|
import DataStore from 'nedb';
|
||||||
|
import { App } from '../data/open';
|
||||||
import { v4 as uuidV4 } from 'uuid';
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
import sequelize, { Op } from 'sequelize';
|
import { dbs } from '../loaders/db';
|
||||||
import { shareStore } from '../shared/store';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class OpenService {
|
export default class OpenService {
|
||||||
|
private appDb = dbs.appDb;
|
||||||
|
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
public async findApps(): Promise<App[] | null> {
|
public async findTokenByValue(token: string): Promise<App> {
|
||||||
const docs = await this.find({});
|
return new Promise((resolve) => {
|
||||||
return docs;
|
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> {
|
public async create(payload: App): Promise<App> {
|
||||||
const tab = { ...payload };
|
const tab = new App({ ...payload });
|
||||||
tab.client_id = createRandomString(12, 12);
|
tab.client_id = createRandomString(12, 12);
|
||||||
tab.client_secret = createRandomString(24, 24);
|
tab.client_secret = createRandomString(24, 24);
|
||||||
const doc = await this.insert(tab);
|
const docs = await this.insert([tab]);
|
||||||
const apps = await this.find({});
|
return { ...docs[0], tokens: [] };
|
||||||
await shareStore.updateApps(apps);
|
|
||||||
return { ...doc, tokens: [] };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async insert(payload: App): Promise<App> {
|
public async insert(payloads: App[]): Promise<App[]> {
|
||||||
const doc = await AppModel.create(payload, { returning: true });
|
return new Promise((resolve) => {
|
||||||
return doc.get({ plain: true });
|
this.appDb.insert(payloads, (err, docs) => {
|
||||||
|
if (err) {
|
||||||
|
this.logger.error(err);
|
||||||
|
} else {
|
||||||
|
resolve(docs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async update(payload: App): Promise<App> {
|
public async update(payload: App): Promise<App> {
|
||||||
const newDoc = await this.updateDb({
|
const { _id, client_id, client_secret, tokens, ...other } = payload;
|
||||||
name: payload.name,
|
const doc = await this.get(_id);
|
||||||
scopes: payload.scopes,
|
const tab = new App({ ...doc, ...other });
|
||||||
id: payload.id,
|
const newDoc = await this.updateDb(tab);
|
||||||
} as App);
|
|
||||||
return { ...newDoc, tokens: [] };
|
return { ...newDoc, tokens: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateDb(payload: Partial<App>): Promise<App> {
|
private async updateDb(payload: App): Promise<App> {
|
||||||
await AppModel.update(payload, { where: { id: payload.id } });
|
return new Promise((resolve) => {
|
||||||
const apps = await this.find({});
|
this.appDb.update(
|
||||||
await shareStore.updateApps(apps);
|
{ _id: payload._id },
|
||||||
return apps?.find((x) => x.id === payload.id) as App;
|
payload,
|
||||||
|
{ returnUpdatedDocs: true },
|
||||||
|
(err, num, doc, up) => {
|
||||||
|
if (err) {
|
||||||
|
this.logger.error(err);
|
||||||
|
} else {
|
||||||
|
resolve(doc);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDb(query: Record<string, any>): Promise<App> {
|
public async remove(ids: string[]) {
|
||||||
const doc = await AppModel.findOne({ where: query });
|
return new Promise((resolve: any) => {
|
||||||
if (!doc) {
|
this.appDb.remove({ _id: { $in: ids } }, { multi: true }, async (err) => {
|
||||||
throw new Error(`App ${JSON.stringify(query)} not found`);
|
resolve();
|
||||||
}
|
});
|
||||||
return doc.get({ plain: true });
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async remove(ids: number[]) {
|
public async resetSecret(_id: string): Promise<App> {
|
||||||
await AppModel.destroy({ where: { id: ids } });
|
const doc = await this.get(_id);
|
||||||
const apps = await this.find({});
|
const tab = new App({ ...doc });
|
||||||
await shareStore.updateApps(apps);
|
tab.client_secret = createRandomString(24, 24);
|
||||||
}
|
tab.tokens = [];
|
||||||
|
|
||||||
public async resetSecret(id: number): Promise<App> {
|
|
||||||
const tab: Partial<App> = {
|
|
||||||
client_secret: createRandomString(24, 24),
|
|
||||||
tokens: [],
|
|
||||||
id,
|
|
||||||
};
|
|
||||||
// const doc = await this.get(id);
|
|
||||||
// const tab = new App({ ...doc });
|
|
||||||
// tab.client_secret = createRandomString(24, 24);
|
|
||||||
// tab.tokens = [];
|
|
||||||
// const newDoc = await this.updateDb(tab);
|
|
||||||
// return newDoc;
|
|
||||||
const newDoc = await this.updateDb(tab);
|
const newDoc = await this.updateDb(tab);
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
@@ -79,36 +93,46 @@ export default class OpenService {
|
|||||||
public async list(
|
public async list(
|
||||||
searchText: string = '',
|
searchText: string = '',
|
||||||
sort: any = {},
|
sort: any = {},
|
||||||
query: Record<string, any> = {},
|
query: any = {},
|
||||||
): Promise<App[]> {
|
): Promise<App[]> {
|
||||||
let condition = { ...query };
|
let condition = { ...query };
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
const encodeText = encodeURI(searchText);
|
const reg = new RegExp(searchText);
|
||||||
const reg = {
|
condition = {
|
||||||
[Op.or]: [
|
$or: [
|
||||||
{ [Op.like]: `%${searchText}%` },
|
{
|
||||||
{ [Op.like]: `%${encodeText}%` },
|
value: reg,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: reg,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
remarks: reg,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
condition = {
|
|
||||||
...condition,
|
|
||||||
name: reg,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const result = await this.find(condition);
|
|
||||||
return result
|
|
||||||
.filter((x) => x.name !== 'system')
|
|
||||||
.map((x) => ({ ...x, tokens: [] }));
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
const newDocs = await this.find(condition, sort);
|
||||||
|
return newDocs.map((x) => ({ ...x, tokens: [] }));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async find(query: Record<string, any>, sort?: any): Promise<App[]> {
|
private async find(query: any, sort: any): Promise<App[]> {
|
||||||
const docs = await AppModel.findAll({ where: { ...query } });
|
return new Promise((resolve) => {
|
||||||
return docs.map((x) => x.get({ plain: true }));
|
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({
|
public async authToken({
|
||||||
@@ -118,64 +142,30 @@ export default class OpenService {
|
|||||||
client_id: string;
|
client_id: string;
|
||||||
client_secret: string;
|
client_secret: string;
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
let token = uuidV4();
|
const token = uuidV4();
|
||||||
const expiration = Math.round(Date.now() / 1000) + 2592000; // 2592000 30天
|
const expiration = Math.round(Date.now() / 1000) + 2592000; // 2592000 30天
|
||||||
const doc = await AppModel.findOne({ where: { client_id, client_secret } });
|
return new Promise((resolve) => {
|
||||||
if (doc) {
|
this.appDb.find({ client_id, client_secret }).exec((err, docs) => {
|
||||||
const timestamp = Math.round(Date.now() / 1000);
|
if (docs && docs[0]) {
|
||||||
const invalidTokens = (doc.tokens || []).filter(
|
this.appDb.update(
|
||||||
(x) => x.expiration >= timestamp,
|
{ client_id, client_secret },
|
||||||
);
|
{ $push: { tokens: { value: token, expiration } } },
|
||||||
let tokens = invalidTokens;
|
{},
|
||||||
if (invalidTokens.length >= 5) {
|
(err, num, doc) => {
|
||||||
tokens = [
|
resolve({
|
||||||
...invalidTokens.slice(0, 4),
|
code: 200,
|
||||||
{ ...invalidTokens[4], expiration },
|
data: {
|
||||||
];
|
token,
|
||||||
token = invalidTokens[4].value;
|
token_type: 'Bearer',
|
||||||
} else {
|
expiration,
|
||||||
tokens = [...invalidTokens, { value: token, expiration }];
|
},
|
||||||
}
|
});
|
||||||
await AppModel.update(
|
},
|
||||||
{ tokens },
|
);
|
||||||
{ where: { client_id, client_secret } },
|
} else {
|
||||||
);
|
resolve({ code: 400, message: 'client_id或client_seret有误' });
|
||||||
const apps = await this.find({});
|
}
|
||||||
await shareStore.updateApps(apps);
|
});
|
||||||
return {
|
|
||||||
code: 200,
|
|
||||||
data: {
|
|
||||||
token,
|
|
||||||
token_type: 'Bearer',
|
|
||||||
expiration,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return { code: 400, message: 'client_id 或 client_seret 有误' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async generateSystemToken(): Promise<{
|
|
||||||
value: string;
|
|
||||||
expiration: number;
|
|
||||||
}> {
|
|
||||||
const apps = await shareStore.getApps();
|
|
||||||
const systemApp = apps?.find((x) => x.name === 'system');
|
|
||||||
if (!systemApp) {
|
|
||||||
throw new Error('system app not found');
|
|
||||||
}
|
|
||||||
const now = Math.round(Date.now() / 1000);
|
|
||||||
const currentToken = systemApp.tokens?.find((x) => x.expiration > now);
|
|
||||||
if (currentToken) {
|
|
||||||
return currentToken;
|
|
||||||
}
|
|
||||||
const { data } = await this.authToken({
|
|
||||||
client_id: systemApp.client_id,
|
|
||||||
client_secret: systemApp.client_secret,
|
|
||||||
});
|
});
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
value: data.token,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-219
@@ -1,145 +1,18 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import nodeSchedule from 'node-schedule';
|
import nodeSchedule from 'node-schedule';
|
||||||
import { ChildProcessWithoutNullStreams } from 'child_process';
|
import { Crontab } from '../data/cron';
|
||||||
import {
|
import { exec } from 'child_process';
|
||||||
ToadScheduler,
|
|
||||||
LongIntervalJob,
|
|
||||||
SimpleIntervalSchedule,
|
|
||||||
Task,
|
|
||||||
} from 'toad-scheduler';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import taskLimit from '../shared/pLimit';
|
|
||||||
import { spawn } from 'cross-spawn';
|
|
||||||
|
|
||||||
export interface ScheduleTaskType {
|
|
||||||
id?: number;
|
|
||||||
command: string;
|
|
||||||
name?: string;
|
|
||||||
schedule?: string;
|
|
||||||
runOrigin: 'subscription' | 'system' | 'script';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TaskCallbacks {
|
|
||||||
onBefore?: (startTime: dayjs.Dayjs) => Promise<void>;
|
|
||||||
onStart?: (
|
|
||||||
cp: ChildProcessWithoutNullStreams,
|
|
||||||
startTime: dayjs.Dayjs,
|
|
||||||
) => Promise<void>;
|
|
||||||
onEnd?: (
|
|
||||||
cp: ChildProcessWithoutNullStreams,
|
|
||||||
endTime: dayjs.Dayjs,
|
|
||||||
diff: number,
|
|
||||||
) => Promise<void>;
|
|
||||||
onLog?: (message: string) => Promise<void>;
|
|
||||||
onError?: (message: string) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class ScheduleService {
|
export default class ScheduleService {
|
||||||
private scheduleStacks = new Map<string, nodeSchedule.Job>();
|
private scheduleStacks = new Map<string, nodeSchedule.Job>();
|
||||||
|
|
||||||
private intervalSchedule = new ToadScheduler();
|
|
||||||
|
|
||||||
private taskLimitMap = {
|
|
||||||
system: 'runWithSystemLimit' as const,
|
|
||||||
script: 'runWithScriptLimit' as const,
|
|
||||||
subscription: 'runWithSubscriptionLimit' as const,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
async runTask(
|
async generateSchedule({ _id = '', command, name, schedule }: Crontab) {
|
||||||
command: string,
|
|
||||||
callbacks: TaskCallbacks = {},
|
|
||||||
params: {
|
|
||||||
schedule?: string;
|
|
||||||
name?: string;
|
|
||||||
command?: string;
|
|
||||||
id: string;
|
|
||||||
runOrigin: 'subscription' | 'system' | 'script';
|
|
||||||
},
|
|
||||||
completionTime: 'start' | 'end' = 'end',
|
|
||||||
) {
|
|
||||||
const { runOrigin, ...others } = params;
|
|
||||||
|
|
||||||
return taskLimit[this.taskLimitMap[runOrigin]](others, () => {
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
this.logger.info(
|
|
||||||
`[panel][开始执行任务] 参数: ${JSON.stringify({
|
|
||||||
...others,
|
|
||||||
command,
|
|
||||||
})}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const startTime = dayjs();
|
|
||||||
await callbacks.onBefore?.(startTime);
|
|
||||||
|
|
||||||
const cp = spawn(command, { shell: '/bin/bash' });
|
|
||||||
|
|
||||||
callbacks.onStart?.(cp, startTime);
|
|
||||||
completionTime === 'start' && resolve(cp.pid);
|
|
||||||
|
|
||||||
cp.stdout.on('data', async (data) => {
|
|
||||||
await callbacks.onLog?.(data.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.stderr.on('data', async (data) => {
|
|
||||||
this.logger.info(
|
|
||||||
'[panel][执行任务失败] 命令: %s, 错误信息: %j',
|
|
||||||
command,
|
|
||||||
data.toString(),
|
|
||||||
);
|
|
||||||
await callbacks.onError?.(data.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.on('error', async (err) => {
|
|
||||||
this.logger.error(
|
|
||||||
'[panel][创建任务失败] 命令: %s, 错误信息: %j',
|
|
||||||
command,
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
await callbacks.onError?.(JSON.stringify(err));
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.on('exit', async (code) => {
|
|
||||||
this.logger.info(
|
|
||||||
'[panel][执行任务结束] 参数: %s, 退出码: %j',
|
|
||||||
JSON.stringify({
|
|
||||||
...others,
|
|
||||||
command,
|
|
||||||
}),
|
|
||||||
code,
|
|
||||||
);
|
|
||||||
const endTime = dayjs();
|
|
||||||
await callbacks.onEnd?.(
|
|
||||||
cp,
|
|
||||||
endTime,
|
|
||||||
endTime.diff(startTime, 'seconds'),
|
|
||||||
);
|
|
||||||
resolve({ ...others, pid: cp.pid, code });
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(
|
|
||||||
'[panel][执行任务失败] 命令: %s, 错误信息: %j',
|
|
||||||
command,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
await callbacks.onError?.(JSON.stringify(error));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async createCronTask(
|
|
||||||
{ id = 0, command, name, schedule = '', runOrigin }: ScheduleTaskType,
|
|
||||||
callbacks?: TaskCallbacks,
|
|
||||||
runImmediately = false,
|
|
||||||
) {
|
|
||||||
const _id = this.formatId(id);
|
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
'[panel][创建cron任务] 任务ID: %s, cron: %s, 任务名: %s, 执行命令: %s',
|
'[创建定时任务],任务ID: %s,cron: %s,任务名: %s,执行命令: %s',
|
||||||
_id,
|
_id,
|
||||||
schedule,
|
schedule,
|
||||||
name,
|
name,
|
||||||
@@ -149,97 +22,41 @@ export default class ScheduleService {
|
|||||||
this.scheduleStacks.set(
|
this.scheduleStacks.set(
|
||||||
_id,
|
_id,
|
||||||
nodeSchedule.scheduleJob(_id, schedule, async () => {
|
nodeSchedule.scheduleJob(_id, schedule, async () => {
|
||||||
this.runTask(command, callbacks, {
|
try {
|
||||||
name,
|
exec(command, async (error, stdout, stderr) => {
|
||||||
schedule,
|
if (error) {
|
||||||
command,
|
await this.logger.info(
|
||||||
id: _id,
|
'执行任务%s失败,时间:%s, 错误信息:%j',
|
||||||
runOrigin,
|
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 {
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (runImmediately) {
|
|
||||||
this.runTask(command, callbacks, {
|
|
||||||
name,
|
|
||||||
schedule,
|
|
||||||
command,
|
|
||||||
id: _id,
|
|
||||||
runOrigin,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
|
async cancelSchedule({ _id = '', name }: Crontab) {
|
||||||
const _id = this.formatId(id);
|
this.logger.info('[取消定时任务],任务名:%s', name);
|
||||||
this.logger.info('[panel][取消定时任务] 任务名: %s', name);
|
this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel();
|
||||||
if (this.scheduleStacks.has(_id)) {
|
|
||||||
this.scheduleStacks.get(_id)?.cancel();
|
|
||||||
this.scheduleStacks.delete(_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createIntervalTask(
|
|
||||||
{ id = 0, command, name = '', runOrigin }: ScheduleTaskType,
|
|
||||||
schedule: SimpleIntervalSchedule,
|
|
||||||
runImmediately = true,
|
|
||||||
callbacks?: TaskCallbacks,
|
|
||||||
) {
|
|
||||||
const _id = this.formatId(id);
|
|
||||||
this.logger.info(
|
|
||||||
'[panel][创建interval任务] 任务ID: %s, 任务名: %s, 执行命令: %s',
|
|
||||||
_id,
|
|
||||||
name,
|
|
||||||
command,
|
|
||||||
);
|
|
||||||
const task = new Task(
|
|
||||||
name,
|
|
||||||
() => {
|
|
||||||
this.runTask(command, callbacks, {
|
|
||||||
name,
|
|
||||||
command,
|
|
||||||
id: _id,
|
|
||||||
runOrigin,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
this.logger.error(
|
|
||||||
'[panel][执行任务失败] 命令: %s, 错误信息: %j',
|
|
||||||
command,
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const job = new LongIntervalJob(
|
|
||||||
{ runImmediately: false, ...schedule },
|
|
||||||
task,
|
|
||||||
{ id: _id },
|
|
||||||
);
|
|
||||||
|
|
||||||
this.intervalSchedule.addIntervalJob(job);
|
|
||||||
|
|
||||||
if (runImmediately) {
|
|
||||||
this.runTask(command, callbacks, {
|
|
||||||
name,
|
|
||||||
command,
|
|
||||||
id: _id,
|
|
||||||
runOrigin,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) {
|
|
||||||
const _id = this.formatId(id);
|
|
||||||
this.logger.info(
|
|
||||||
'[panel][取消interval任务] 任务ID: %s, 任务名: %s',
|
|
||||||
_id,
|
|
||||||
name,
|
|
||||||
);
|
|
||||||
this.intervalSchedule.removeById(_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatId(id: number): string {
|
|
||||||
return String(id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-64
@@ -1,82 +1,43 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import path, { join } from 'path';
|
import { spawn } from 'child_process';
|
||||||
import SockService from './sock';
|
import SockService from './sock';
|
||||||
import CronService from './cron';
|
|
||||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
|
||||||
import config from '../config';
|
|
||||||
import { TASK_COMMAND } from '../config/const';
|
|
||||||
import { getFileContentByName, getPid, killTask, rmPath } from '../config/util';
|
|
||||||
import taskLimit from '../shared/pLimit';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class ScriptService {
|
export default class ScriptService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject('logger') private logger: winston.Logger,
|
@Inject('logger') private logger: winston.Logger,
|
||||||
private sockService: SockService,
|
private sockService: SockService,
|
||||||
private cronService: CronService,
|
|
||||||
private scheduleService: ScheduleService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private taskCallbacks(filePath: string): TaskCallbacks {
|
public async runScript(path: string) {
|
||||||
return {
|
const cp = spawn(`task -l ${path} now`, { shell: '/bin/bash' });
|
||||||
onEnd: async (cp, endTime, diff) => {
|
|
||||||
await rmPath(filePath);
|
|
||||||
},
|
|
||||||
onError: async (message: string) => {
|
|
||||||
this.sockService.sendMessage({
|
|
||||||
type: 'manuallyRunScript',
|
|
||||||
message,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onLog: async (message: string) => {
|
|
||||||
this.sockService.sendMessage({
|
|
||||||
type: 'manuallyRunScript',
|
|
||||||
message,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async runScript(filePath: string) {
|
this.sockService.sendMessage({
|
||||||
const relativePath = path.relative(config.scriptPath, filePath);
|
type: 'manuallyRunScript',
|
||||||
const command = `${TASK_COMMAND} ${relativePath} now`;
|
message: `开始执行脚本`,
|
||||||
const pid = await this.scheduleService.runTask(
|
});
|
||||||
`real_time=true ${command}`,
|
cp.stdout.on('data', (data) => {
|
||||||
this.taskCallbacks(filePath),
|
this.sockService.sendMessage({
|
||||||
{ command, id: relativePath.replace(/ /g, '-'), runOrigin: 'script' },
|
type: 'manuallyRunScript',
|
||||||
'start',
|
message: data.toString(),
|
||||||
);
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return { code: 200, data: pid };
|
cp.stderr.on('data', (data) => {
|
||||||
}
|
this.sockService.sendMessage({
|
||||||
|
type: 'manuallyRunScript',
|
||||||
|
message: data.toString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
public async stopScript(filePath: string, pid: number) {
|
cp.on('error', (err) => {
|
||||||
if (!pid) {
|
this.sockService.sendMessage({
|
||||||
const relativePath = path.relative(config.scriptPath, filePath);
|
type: 'manuallyRunScript',
|
||||||
taskLimit.removeQueuedCron(relativePath.replace(/ /g, '-'));
|
message: JSON.stringify(err),
|
||||||
pid = (await getPid(`${TASK_COMMAND} ${relativePath} now`)) as number;
|
});
|
||||||
}
|
});
|
||||||
try {
|
|
||||||
await killTask(pid);
|
|
||||||
} catch (error) {}
|
|
||||||
|
|
||||||
return { code: 200 };
|
return { code: 200 };
|
||||||
}
|
}
|
||||||
|
|
||||||
public checkFilePath(filePath: string, fileName: string) {
|
|
||||||
const finalPath = path.resolve(config.scriptPath, filePath, fileName);
|
|
||||||
return finalPath.startsWith(config.scriptPath) ? finalPath : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getFile(filePath: string, fileName: string) {
|
|
||||||
const finalPath = this.checkFilePath(filePath, fileName);
|
|
||||||
|
|
||||||
if (!finalPath) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = await getFileContentByName(finalPath);
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { SockMessage } from '../data/sock';
|
|||||||
export default class SockService {
|
export default class SockService {
|
||||||
private clients: Connection[] = [];
|
private clients: Connection[] = [];
|
||||||
|
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) { }
|
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||||
|
|
||||||
public getClients() {
|
public getClients() {
|
||||||
return this.clients;
|
return this.clients;
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
|
||||||
import winston from 'winston';
|
|
||||||
import fs from 'fs/promises';
|
|
||||||
import os from 'os';
|
|
||||||
import path from 'path';
|
|
||||||
import { Subscription } from '../data/subscription';
|
|
||||||
import { formatUrl } from '../config/subscription';
|
|
||||||
import config from '../config';
|
|
||||||
import { fileExist, rmPath } from '../config/util';
|
|
||||||
import { writeFileWithLock } from '../shared/utils';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export default class SshKeyService {
|
|
||||||
private homedir = os.homedir();
|
|
||||||
private sshPath = config.sshdPath;
|
|
||||||
private sshConfigFilePath = path.resolve(this.homedir, '.ssh', 'config');
|
|
||||||
private sshConfigHeader = `Include ${path.join(this.sshPath, '*.config')}`;
|
|
||||||
|
|
||||||
constructor(@Inject('logger') private logger: winston.Logger) {
|
|
||||||
this.initSshConfigFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async initSshConfigFile() {
|
|
||||||
let config = '';
|
|
||||||
const _exist = await fileExist(this.sshConfigFilePath);
|
|
||||||
if (_exist) {
|
|
||||||
config = await fs.readFile(this.sshConfigFilePath, { encoding: 'utf-8' });
|
|
||||||
} else {
|
|
||||||
await writeFileWithLock(this.sshConfigFilePath, '', { mode: '600' });
|
|
||||||
}
|
|
||||||
if (!config.includes(this.sshConfigHeader)) {
|
|
||||||
await writeFileWithLock(
|
|
||||||
this.sshConfigFilePath,
|
|
||||||
`${this.sshConfigHeader}\n\n${config}`,
|
|
||||||
{ mode: '600' },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async generatePrivateKeyFile(
|
|
||||||
alias: string,
|
|
||||||
key: string,
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
await writeFileWithLock(
|
|
||||||
path.join(this.sshPath, alias),
|
|
||||||
`${key}${os.EOL}`,
|
|
||||||
{
|
|
||||||
mode: '400',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('生成私钥文件失败', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async removePrivateKeyFile(alias: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
const filePath = path.join(this.sshPath, alias);
|
|
||||||
await rmPath(filePath);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('删除私钥文件失败', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async generateSingleSshConfig(
|
|
||||||
alias: string,
|
|
||||||
host: string,
|
|
||||||
proxy?: string,
|
|
||||||
) {
|
|
||||||
if (host === 'github.com') {
|
|
||||||
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa`;
|
|
||||||
}
|
|
||||||
const proxyStr = proxy
|
|
||||||
? ` ProxyCommand nc -v -x ${proxy} %h %p 2>/dev/null\n`
|
|
||||||
: '';
|
|
||||||
const config = `Host ${alias}\n Hostname ${host}\n IdentityFile ${path.join(
|
|
||||||
this.sshPath,
|
|
||||||
alias,
|
|
||||||
)}\n StrictHostKeyChecking no\n${proxyStr}`;
|
|
||||||
await writeFileWithLock(
|
|
||||||
`${path.join(this.sshPath, `${alias}.config`)}`,
|
|
||||||
config,
|
|
||||||
{
|
|
||||||
encoding: 'utf8',
|
|
||||||
mode: '600',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async removeSshConfig(alias: string) {
|
|
||||||
try {
|
|
||||||
const filePath = path.join(this.sshPath, `${alias}.config`);
|
|
||||||
await rmPath(filePath);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`删除ssh配置文件${alias}失败`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async addSSHKey(
|
|
||||||
key: string,
|
|
||||||
alias: string,
|
|
||||||
host: string,
|
|
||||||
proxy?: string,
|
|
||||||
): Promise<void> {
|
|
||||||
await this.generatePrivateKeyFile(alias, key);
|
|
||||||
await this.generateSingleSshConfig(alias, host, proxy);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async removeSSHKey(
|
|
||||||
alias: string,
|
|
||||||
host: string,
|
|
||||||
proxy?: string,
|
|
||||||
): Promise<void> {
|
|
||||||
await this.removePrivateKeyFile(alias);
|
|
||||||
await this.removeSshConfig(alias);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setSshConfig(docs: Subscription[]) {
|
|
||||||
for (const doc of docs) {
|
|
||||||
if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {
|
|
||||||
const { alias, proxy } = doc;
|
|
||||||
const { host } = formatUrl(doc);
|
|
||||||
await this.removePrivateKeyFile(alias);
|
|
||||||
await this.removeSshConfig(alias);
|
|
||||||
await this.generatePrivateKeyFile(
|
|
||||||
alias,
|
|
||||||
(doc.pull_option as any).private_key,
|
|
||||||
);
|
|
||||||
await this.generateSingleSshConfig(alias, host, proxy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async addGlobalSSHKey(key: string, alias: string): Promise<void> {
|
|
||||||
await this.generatePrivateKeyFile(`~global_${alias}`, key);
|
|
||||||
// Create a global SSH config entry that matches all hosts
|
|
||||||
// This allows the key to be used for any Git repository
|
|
||||||
await this.generateGlobalSshConfig(`~global_${alias}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async removeGlobalSSHKey(alias: string): Promise<void> {
|
|
||||||
await this.removePrivateKeyFile(`~global_${alias}`);
|
|
||||||
await this.removeSshConfig(`~global_${alias}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async generateGlobalSshConfig(alias: string) {
|
|
||||||
// Create a config that matches all hosts, making this key globally available
|
|
||||||
const config = `Host *\n IdentityFile ${path.join(
|
|
||||||
this.sshPath,
|
|
||||||
alias,
|
|
||||||
)}\n StrictHostKeyChecking no\n`;
|
|
||||||
await writeFileWithLock(
|
|
||||||
`${path.join(this.sshPath, `${alias}.config`)}`,
|
|
||||||
config,
|
|
||||||
{
|
|
||||||
encoding: 'utf8',
|
|
||||||
mode: '600',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,398 +0,0 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
|
||||||
import winston from 'winston';
|
|
||||||
import config from '../config';
|
|
||||||
import {
|
|
||||||
Subscription,
|
|
||||||
SubscriptionInstance,
|
|
||||||
SubscriptionModel,
|
|
||||||
SubscriptionStatus,
|
|
||||||
} from '../data/subscription';
|
|
||||||
import { ChildProcessWithoutNullStreams } from 'child_process';
|
|
||||||
import {
|
|
||||||
getFileContentByName,
|
|
||||||
concurrentRun,
|
|
||||||
fileExist,
|
|
||||||
createFile,
|
|
||||||
killTask,
|
|
||||||
handleLogPath,
|
|
||||||
promiseExec,
|
|
||||||
rmPath,
|
|
||||||
} from '../config/util';
|
|
||||||
import fs from 'fs/promises';
|
|
||||||
import { FindOptions, Op } from 'sequelize';
|
|
||||||
import path, { join } from 'path';
|
|
||||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
|
||||||
import { SimpleIntervalSchedule } from 'toad-scheduler';
|
|
||||||
import SockService from './sock';
|
|
||||||
import SshKeyService from './sshKey';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { LOG_END_SYMBOL } from '../config/const';
|
|
||||||
import { formatCommand, formatUrl } from '../config/subscription';
|
|
||||||
import { CrontabModel } from '../data/cron';
|
|
||||||
import CrontabService from './cron';
|
|
||||||
import taskLimit from '../shared/pLimit';
|
|
||||||
import { logStreamManager } from '../shared/logStreamManager';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export default class SubscriptionService {
|
|
||||||
constructor(
|
|
||||||
@Inject('logger') private logger: winston.Logger,
|
|
||||||
private scheduleService: ScheduleService,
|
|
||||||
private sockService: SockService,
|
|
||||||
private sshKeyService: SshKeyService,
|
|
||||||
private crontabService: CrontabService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public async list(
|
|
||||||
searchText?: string,
|
|
||||||
ids?: string,
|
|
||||||
): Promise<SubscriptionInstance[]> {
|
|
||||||
let query = {};
|
|
||||||
const subIds = JSON.parse(ids || '[]');
|
|
||||||
if (searchText) {
|
|
||||||
const reg = {
|
|
||||||
[Op.or]: [
|
|
||||||
{ [Op.like]: `%${searchText}%` },
|
|
||||||
{ [Op.like]: `%${encodeURI(searchText)}%` },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
query = {
|
|
||||||
[Op.or]: [
|
|
||||||
{
|
|
||||||
name: reg,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: reg,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const result = await SubscriptionModel.findAll({
|
|
||||||
where: { ...query, ...(ids ? { id: subIds } : undefined) },
|
|
||||||
order: [
|
|
||||||
['is_disabled', 'ASC'],
|
|
||||||
['createdAt', 'DESC'],
|
|
||||||
],
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handleTask(
|
|
||||||
doc: Subscription,
|
|
||||||
needCreate = true,
|
|
||||||
runImmediately = false,
|
|
||||||
) {
|
|
||||||
const { url } = formatUrl(doc);
|
|
||||||
|
|
||||||
doc.command = formatCommand(doc, url as string);
|
|
||||||
|
|
||||||
if (doc.schedule_type === 'crontab') {
|
|
||||||
this.scheduleService.cancelCronTask(doc as any);
|
|
||||||
needCreate &&
|
|
||||||
(await this.scheduleService.createCronTask(
|
|
||||||
{ ...doc, runOrigin: 'subscription' } as any,
|
|
||||||
this.taskCallbacks(doc),
|
|
||||||
runImmediately,
|
|
||||||
));
|
|
||||||
} else if (doc.interval_schedule) {
|
|
||||||
this.scheduleService.cancelIntervalTask(doc as any);
|
|
||||||
const { type, value } = doc.interval_schedule;
|
|
||||||
needCreate &&
|
|
||||||
(await this.scheduleService.createIntervalTask(
|
|
||||||
{ ...doc, runOrigin: 'subscription' } as any,
|
|
||||||
{ [type]: value } as SimpleIntervalSchedule,
|
|
||||||
runImmediately,
|
|
||||||
this.taskCallbacks(doc),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setSshConfig() {
|
|
||||||
const docs = await SubscriptionModel.findAll();
|
|
||||||
await this.sshKeyService.setSshConfig(docs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private taskCallbacks(doc: Subscription): TaskCallbacks {
|
|
||||||
return {
|
|
||||||
onBefore: async (startTime) => {
|
|
||||||
const logTime = startTime.format('YYYY-MM-DD-HH-mm-ss');
|
|
||||||
const logPath = `${doc.alias}/${logTime}.log`;
|
|
||||||
await SubscriptionModel.update(
|
|
||||||
{
|
|
||||||
status: SubscriptionStatus.running,
|
|
||||||
log_path: logPath,
|
|
||||||
},
|
|
||||||
{ where: { id: doc.id } },
|
|
||||||
);
|
|
||||||
const absolutePath = await handleLogPath(
|
|
||||||
logPath as string,
|
|
||||||
`## 开始执行... ${startTime.format('YYYY-MM-DD HH:mm:ss')}\n`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 执行sub_before
|
|
||||||
let beforeStr = '';
|
|
||||||
try {
|
|
||||||
if (doc.sub_before) {
|
|
||||||
await logStreamManager.write(absolutePath, `\n## 执行before命令...\n\n`);
|
|
||||||
beforeStr = await promiseExec(doc.sub_before);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
beforeStr =
|
|
||||||
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
|
|
||||||
}
|
|
||||||
if (beforeStr) {
|
|
||||||
await logStreamManager.write(absolutePath, `${beforeStr}\n`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onStart: async (cp: ChildProcessWithoutNullStreams, startTime) => {
|
|
||||||
await SubscriptionModel.update(
|
|
||||||
{
|
|
||||||
pid: cp.pid,
|
|
||||||
},
|
|
||||||
{ where: { id: doc.id } },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onEnd: async (cp, endTime, diff) => {
|
|
||||||
const sub = await this.getDb({ id: doc.id });
|
|
||||||
const absolutePath = await handleLogPath(sub.log_path as string);
|
|
||||||
|
|
||||||
// 执行 sub_after
|
|
||||||
let afterStr = '';
|
|
||||||
try {
|
|
||||||
if (sub.sub_after) {
|
|
||||||
await logStreamManager.write(absolutePath, `\n\n## 执行after命令...\n\n`);
|
|
||||||
afterStr = await promiseExec(sub.sub_after);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
afterStr =
|
|
||||||
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
|
|
||||||
}
|
|
||||||
if (afterStr) {
|
|
||||||
await logStreamManager.write(absolutePath, `${afterStr}\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await logStreamManager.write(
|
|
||||||
absolutePath,
|
|
||||||
`\n## 执行结束... ${endTime.format(
|
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
|
||||||
)} 耗时 ${diff} 秒${LOG_END_SYMBOL}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Close the stream after task completion
|
|
||||||
await logStreamManager.closeStream(absolutePath);
|
|
||||||
|
|
||||||
await SubscriptionModel.update(
|
|
||||||
{ status: SubscriptionStatus.idle, pid: undefined },
|
|
||||||
{ where: { id: sub.id } },
|
|
||||||
);
|
|
||||||
|
|
||||||
this.sockService.sendMessage({
|
|
||||||
type: 'runSubscriptionEnd',
|
|
||||||
message: '订阅执行完成',
|
|
||||||
references: [doc.id as number],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: async (message: string) => {
|
|
||||||
const sub = await this.getDb({ id: doc.id });
|
|
||||||
const absolutePath = await handleLogPath(sub.log_path as string);
|
|
||||||
await logStreamManager.write(absolutePath, `\n${message}`);
|
|
||||||
},
|
|
||||||
onLog: async (message: string) => {
|
|
||||||
const sub = await this.getDb({ id: doc.id });
|
|
||||||
const absolutePath = await handleLogPath(sub.log_path as string);
|
|
||||||
await logStreamManager.write(absolutePath, `\n${message}`);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async create(payload: Subscription): Promise<Subscription> {
|
|
||||||
const tab = new Subscription(payload);
|
|
||||||
const doc = await this.insert(tab);
|
|
||||||
await this.handleTask(doc.get({ plain: true }));
|
|
||||||
await this.setSshConfig();
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async insert(payload: Subscription): Promise<SubscriptionInstance> {
|
|
||||||
return await SubscriptionModel.create(payload, { returning: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async update(payload: Subscription): Promise<Subscription> {
|
|
||||||
const doc = await this.getDb({ id: payload.id });
|
|
||||||
const tab = new Subscription({ ...doc, ...payload });
|
|
||||||
const newDoc = await this.updateDb(tab);
|
|
||||||
await this.handleTask(newDoc, !newDoc.is_disabled);
|
|
||||||
await this.setSshConfig();
|
|
||||||
return newDoc;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateDb(payload: Subscription): Promise<Subscription> {
|
|
||||||
await SubscriptionModel.update(payload, { where: { id: payload.id } });
|
|
||||||
return await this.getDb({ id: payload.id });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async status({
|
|
||||||
ids,
|
|
||||||
status,
|
|
||||||
pid,
|
|
||||||
log_path,
|
|
||||||
last_running_time = 0,
|
|
||||||
last_execution_time = 0,
|
|
||||||
}: {
|
|
||||||
ids: number[];
|
|
||||||
status: SubscriptionStatus;
|
|
||||||
pid: number;
|
|
||||||
log_path: string;
|
|
||||||
last_running_time: number;
|
|
||||||
last_execution_time: number;
|
|
||||||
}) {
|
|
||||||
const options: any = {
|
|
||||||
status,
|
|
||||||
pid,
|
|
||||||
log_path,
|
|
||||||
last_execution_time,
|
|
||||||
};
|
|
||||||
if (last_running_time > 0) {
|
|
||||||
options.last_running_time = last_running_time;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await SubscriptionModel.update(
|
|
||||||
{ ...options },
|
|
||||||
{ where: { id: ids } },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async remove(ids: number[], query: { force?: boolean }) {
|
|
||||||
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
|
||||||
for (const doc of docs) {
|
|
||||||
await this.handleTask(doc.get({ plain: true }), false);
|
|
||||||
}
|
|
||||||
await SubscriptionModel.destroy({ where: { id: ids } });
|
|
||||||
await this.setSshConfig();
|
|
||||||
|
|
||||||
if (query?.force === true) {
|
|
||||||
const crons = await CrontabModel.findAll({ where: { sub_id: ids } });
|
|
||||||
if (crons?.length) {
|
|
||||||
await this.crontabService.remove(crons.map((x) => x.id!));
|
|
||||||
}
|
|
||||||
for (const doc of docs) {
|
|
||||||
const filePath = join(config.scriptPath, doc.alias);
|
|
||||||
const repoPath = join(config.repoPath, doc.alias);
|
|
||||||
await rmPath(filePath);
|
|
||||||
await rmPath(repoPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getDb(
|
|
||||||
query: FindOptions<Subscription>['where'],
|
|
||||||
): Promise<Subscription> {
|
|
||||||
const doc = await SubscriptionModel.findOne({ where: { ...query } });
|
|
||||||
if (!doc) {
|
|
||||||
throw new Error(`Subscription ${JSON.stringify(query)} not found`);
|
|
||||||
}
|
|
||||||
return doc.get({ plain: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async run(ids: number[]) {
|
|
||||||
await SubscriptionModel.update(
|
|
||||||
{ status: SubscriptionStatus.queued },
|
|
||||||
{ where: { id: ids } },
|
|
||||||
);
|
|
||||||
ids.forEach((id) => {
|
|
||||||
this.runSingle(id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async stop(ids: number[]) {
|
|
||||||
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
|
||||||
for (const doc of docs) {
|
|
||||||
if (doc.pid) {
|
|
||||||
try {
|
|
||||||
await killTask(doc.pid);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await SubscriptionModel.update(
|
|
||||||
{ status: SubscriptionStatus.idle, pid: undefined },
|
|
||||||
{ where: { id: ids } },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async runSingle(subscriptionId: number) {
|
|
||||||
const subscription = await this.getDb({ id: subscriptionId });
|
|
||||||
if (subscription.status !== SubscriptionStatus.queued) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const command = formatCommand(subscription);
|
|
||||||
|
|
||||||
this.scheduleService.runTask(command, this.taskCallbacks(subscription), {
|
|
||||||
name: subscription.name,
|
|
||||||
schedule: subscription.schedule,
|
|
||||||
command,
|
|
||||||
id: String(subscription.id),
|
|
||||||
runOrigin: 'subscription',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async disabled(ids: number[]) {
|
|
||||||
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
|
|
||||||
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
|
||||||
await this.setSshConfig();
|
|
||||||
for (const doc of docs) {
|
|
||||||
await this.handleTask(doc.get({ plain: true }), false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async enabled(ids: number[]) {
|
|
||||||
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
|
|
||||||
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
|
|
||||||
await this.setSshConfig();
|
|
||||||
for (const doc of docs) {
|
|
||||||
await this.handleTask(doc.get({ plain: true }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async log(id: number) {
|
|
||||||
const doc = await this.getDb({ id });
|
|
||||||
if (!doc || !doc.log_path) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const absolutePath = await handleLogPath(doc.log_path as string);
|
|
||||||
return await getFileContentByName(absolutePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async logs(id: number) {
|
|
||||||
const doc = await this.getDb({ id });
|
|
||||||
if (!doc) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (doc.log_path) {
|
|
||||||
const relativeDir = path.dirname(`${doc.log_path}`);
|
|
||||||
const dir = path.resolve(config.logPath, relativeDir);
|
|
||||||
const _exist = await fileExist(dir);
|
|
||||||
if (_exist) {
|
|
||||||
let files = await fs.readdir(dir);
|
|
||||||
return (
|
|
||||||
await Promise.all(
|
|
||||||
files.map(async (x) => ({
|
|
||||||
filename: x,
|
|
||||||
directory: relativeDir.replace(config.logPath, ''),
|
|
||||||
time: (await fs.lstat(`${dir}/${x}`)).birthtimeMs,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
).sort((a, b) => b.time - a.time);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,564 +0,0 @@
|
|||||||
import { spawn } from 'cross-spawn';
|
|
||||||
import { Response } from 'express';
|
|
||||||
import fs from 'fs';
|
|
||||||
import { Agent, request } from 'undici';
|
|
||||||
import sum from 'lodash/sum';
|
|
||||||
import path from 'path';
|
|
||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import winston from 'winston';
|
|
||||||
import config from '../config';
|
|
||||||
import { NotificationModeStringMap, TASK_COMMAND } from '../config/const';
|
|
||||||
import {
|
|
||||||
getPid,
|
|
||||||
killTask,
|
|
||||||
parseContentVersion,
|
|
||||||
parseVersion,
|
|
||||||
promiseExec,
|
|
||||||
readDirs,
|
|
||||||
rmPath,
|
|
||||||
setSystemTimezone,
|
|
||||||
} from '../config/util';
|
|
||||||
import {
|
|
||||||
DependenceModel,
|
|
||||||
DependenceStatus,
|
|
||||||
DependenceTypes,
|
|
||||||
} from '../data/dependence';
|
|
||||||
import { NotificationInfo } from '../data/notify';
|
|
||||||
import {
|
|
||||||
AuthDataType,
|
|
||||||
SystemInfo,
|
|
||||||
SystemInstance,
|
|
||||||
SystemModel,
|
|
||||||
SystemModelInfo,
|
|
||||||
} from '../data/system';
|
|
||||||
import taskLimit from '../shared/pLimit';
|
|
||||||
import NotificationService from './notify';
|
|
||||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
|
||||||
import SockService from './sock';
|
|
||||||
import os from 'os';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export default class SystemService {
|
|
||||||
@Inject((type) => NotificationService)
|
|
||||||
private notificationService!: NotificationService;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@Inject('logger') private logger: winston.Logger,
|
|
||||||
private scheduleService: ScheduleService,
|
|
||||||
private sockService: SockService,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
public async getSystemConfig() {
|
|
||||||
const doc = await this.getDb({ type: AuthDataType.systemConfig });
|
|
||||||
return {
|
|
||||||
...doc,
|
|
||||||
info: { ...doc.info, timezone: doc.info?.timezone || 'Asia/Shanghai' },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async updateAuthDb(payload: SystemInfo): Promise<SystemInfo> {
|
|
||||||
const { id, ...others } = payload;
|
|
||||||
await SystemModel.update(others, { where: { id } });
|
|
||||||
const doc = await this.getDb({ id });
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getDb(query: any): Promise<SystemInfo> {
|
|
||||||
const doc = await SystemModel.findOne({ where: query });
|
|
||||||
if (!doc) {
|
|
||||||
throw new Error(`System ${JSON.stringify(query)} not found`);
|
|
||||||
}
|
|
||||||
return doc.get({ plain: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateNotificationMode(notificationInfo: NotificationInfo) {
|
|
||||||
const code = Math.random().toString().slice(-6);
|
|
||||||
const isSuccess = await this.notificationService.testNotify(
|
|
||||||
notificationInfo,
|
|
||||||
'青龙',
|
|
||||||
`【蛟龙】测试通知 https://t.me/jiao_long`,
|
|
||||||
);
|
|
||||||
if (isSuccess) {
|
|
||||||
const result = await this.updateAuthDb({
|
|
||||||
type: AuthDataType.notification,
|
|
||||||
info: { ...notificationInfo },
|
|
||||||
});
|
|
||||||
return { code: 200, data: { ...result, code } };
|
|
||||||
} else {
|
|
||||||
return { code: 400, message: '通知发送失败,请检查参数' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateLogRemoveFrequency(info: SystemModelInfo) {
|
|
||||||
const oDoc = await this.getSystemConfig();
|
|
||||||
const result = await this.updateAuthDb({
|
|
||||||
...oDoc,
|
|
||||||
info: { ...oDoc.info, ...info },
|
|
||||||
});
|
|
||||||
const cron = {
|
|
||||||
id: result.id as number,
|
|
||||||
name: '删除日志',
|
|
||||||
command: `ql rmlog ${info.logRemoveFrequency}`,
|
|
||||||
runOrigin: 'system' as const,
|
|
||||||
};
|
|
||||||
if (oDoc.info?.logRemoveFrequency) {
|
|
||||||
await this.scheduleService.cancelIntervalTask(cron);
|
|
||||||
}
|
|
||||||
if (info.logRemoveFrequency && info.logRemoveFrequency > 0) {
|
|
||||||
this.scheduleService.createIntervalTask(
|
|
||||||
cron,
|
|
||||||
{
|
|
||||||
days: info.logRemoveFrequency,
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return { code: 200, data: info };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateCronConcurrency(info: SystemModelInfo) {
|
|
||||||
const oDoc = await this.getSystemConfig();
|
|
||||||
await this.updateAuthDb({
|
|
||||||
...oDoc,
|
|
||||||
info: { ...oDoc.info, ...info },
|
|
||||||
});
|
|
||||||
if (info.cronConcurrency) {
|
|
||||||
await taskLimit.setCustomLimit(info.cronConcurrency);
|
|
||||||
}
|
|
||||||
return { code: 200, data: info };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateDependenceProxy(info: SystemModelInfo) {
|
|
||||||
const oDoc = await this.getSystemConfig();
|
|
||||||
await this.updateAuthDb({
|
|
||||||
...oDoc,
|
|
||||||
info: { ...oDoc.info, ...info },
|
|
||||||
});
|
|
||||||
if (info.dependenceProxy) {
|
|
||||||
await fs.promises.writeFile(
|
|
||||||
config.dependenceProxyFile,
|
|
||||||
`export http_proxy="${info.dependenceProxy}"\nexport https_proxy="${info.dependenceProxy}"`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await fs.promises.rm(config.dependenceProxyFile);
|
|
||||||
}
|
|
||||||
return { code: 200, data: info };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateNodeMirror(info: SystemModelInfo, res?: Response) {
|
|
||||||
const oDoc = await this.getSystemConfig();
|
|
||||||
await this.updateAuthDb({
|
|
||||||
...oDoc,
|
|
||||||
info: { ...oDoc.info, ...info },
|
|
||||||
});
|
|
||||||
let cmd = 'pnpm config delete registry';
|
|
||||||
if (info.nodeMirror) {
|
|
||||||
cmd = `pnpm config set registry ${info.nodeMirror}`;
|
|
||||||
}
|
|
||||||
let command = `cd && ${cmd}`;
|
|
||||||
const docs = await DependenceModel.findAll({
|
|
||||||
where: {
|
|
||||||
type: DependenceTypes.nodejs,
|
|
||||||
status: DependenceStatus.installed,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (docs.length > 0) {
|
|
||||||
command += ` && pnpm i -g`;
|
|
||||||
}
|
|
||||||
this.scheduleService.runTask(
|
|
||||||
command,
|
|
||||||
{
|
|
||||||
onStart: async (cp) => {
|
|
||||||
res?.setHeader('QL-Task-Pid', `${cp.pid}`);
|
|
||||||
res?.end();
|
|
||||||
},
|
|
||||||
onEnd: async () => {
|
|
||||||
this.sockService.sendMessage({
|
|
||||||
type: 'updateNodeMirror',
|
|
||||||
message: 'update node mirror end',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: async (message: string) => {
|
|
||||||
this.sockService.sendMessage({ type: 'updateNodeMirror', message });
|
|
||||||
},
|
|
||||||
onLog: async (message: string) => {
|
|
||||||
this.sockService.sendMessage({ type: 'updateNodeMirror', message });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command,
|
|
||||||
id: 'update-node-mirror',
|
|
||||||
runOrigin: 'system',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updatePythonMirror(info: SystemModelInfo) {
|
|
||||||
const oDoc = await this.getSystemConfig();
|
|
||||||
await this.updateAuthDb({
|
|
||||||
...oDoc,
|
|
||||||
info: { ...oDoc.info, ...info },
|
|
||||||
});
|
|
||||||
let cmd = 'pip config unset global.index-url';
|
|
||||||
if (info.pythonMirror) {
|
|
||||||
cmd = `pip3 config set global.index-url ${info.pythonMirror}`;
|
|
||||||
}
|
|
||||||
await promiseExec(cmd);
|
|
||||||
return { code: 200, data: info };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateLinuxMirror(
|
|
||||||
info: SystemModelInfo,
|
|
||||||
res?: Response,
|
|
||||||
onEnd?: () => void,
|
|
||||||
) {
|
|
||||||
const oDoc = await this.getSystemConfig();
|
|
||||||
await this.updateAuthDb({
|
|
||||||
...oDoc,
|
|
||||||
info: { ...oDoc.info, ...info },
|
|
||||||
});
|
|
||||||
let defaultDomain = 'https://dl-cdn.alpinelinux.org';
|
|
||||||
let targetDomain = 'https://dl-cdn.alpinelinux.org';
|
|
||||||
if (os.platform() !== 'linux') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const content = await fs.promises.readFile('/etc/apk/repositories', {
|
|
||||||
encoding: 'utf-8',
|
|
||||||
});
|
|
||||||
const domainMatch = content.match(/(http.*)\/alpine\/.*/);
|
|
||||||
if (domainMatch) {
|
|
||||||
defaultDomain = domainMatch[1];
|
|
||||||
}
|
|
||||||
if (info.linuxMirror) {
|
|
||||||
targetDomain = info.linuxMirror;
|
|
||||||
}
|
|
||||||
const command = `sed -i 's/${defaultDomain.replace(
|
|
||||||
/\//g,
|
|
||||||
'\\/',
|
|
||||||
)}/${targetDomain.replace(
|
|
||||||
/\//g,
|
|
||||||
'\\/',
|
|
||||||
)}/g' /etc/apk/repositories && apk update -f`;
|
|
||||||
|
|
||||||
this.scheduleService.runTask(
|
|
||||||
command,
|
|
||||||
{
|
|
||||||
onStart: async (cp) => {
|
|
||||||
res?.setHeader('QL-Task-Pid', `${cp.pid}`);
|
|
||||||
res?.end();
|
|
||||||
},
|
|
||||||
onEnd: async () => {
|
|
||||||
this.sockService.sendMessage({
|
|
||||||
type: 'updateLinuxMirror',
|
|
||||||
message: 'update linux mirror end',
|
|
||||||
});
|
|
||||||
onEnd?.();
|
|
||||||
},
|
|
||||||
onError: async (message: string) => {
|
|
||||||
this.sockService.sendMessage({ type: 'updateLinuxMirror', message });
|
|
||||||
},
|
|
||||||
onLog: async (message: string) => {
|
|
||||||
this.sockService.sendMessage({ type: 'updateLinuxMirror', message });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command,
|
|
||||||
id: 'update-linux-mirror',
|
|
||||||
runOrigin: 'system',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async checkUpdate() {
|
|
||||||
try {
|
|
||||||
const currentVersionContent = await parseVersion(config.versionFile);
|
|
||||||
|
|
||||||
let lastVersionContent;
|
|
||||||
try {
|
|
||||||
const { body } = await request(
|
|
||||||
`${config.lastVersionFile}?t=${Date.now()}`,
|
|
||||||
{
|
|
||||||
dispatcher: new Agent({
|
|
||||||
keepAliveTimeout: 30000,
|
|
||||||
keepAliveMaxTimeout: 30000,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const text = await body.text();
|
|
||||||
lastVersionContent = parseContentVersion(text);
|
|
||||||
} catch (error) { }
|
|
||||||
|
|
||||||
if (!lastVersionContent) {
|
|
||||||
lastVersionContent = currentVersionContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
code: 200,
|
|
||||||
data: {
|
|
||||||
hasNewVersion: this.checkHasNewVersion(
|
|
||||||
currentVersionContent.version,
|
|
||||||
lastVersionContent.version,
|
|
||||||
),
|
|
||||||
lastVersion: lastVersionContent.version,
|
|
||||||
lastLog: lastVersionContent.changeLog,
|
|
||||||
lastLogLink: lastVersionContent.changeLogLink,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error: any) {
|
|
||||||
return {
|
|
||||||
code: 400,
|
|
||||||
message: error.message,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkHasNewVersion(curVersion: string, lastVersion: string) {
|
|
||||||
const curArr = curVersion.split('.').map((x) => parseInt(x, 10));
|
|
||||||
const lastArr = lastVersion.split('.').map((x) => parseInt(x, 10));
|
|
||||||
if (curArr[0] < lastArr[0]) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (curArr[0] === lastArr[0] && curArr[1] < lastArr[1]) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
curArr[0] === lastArr[0] &&
|
|
||||||
curArr[1] === lastArr[1] &&
|
|
||||||
curArr[2] < lastArr[2]
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateSystem() {
|
|
||||||
const cp = spawn('real_time=true ql update false', { shell: '/bin/bash' });
|
|
||||||
|
|
||||||
cp.stdout.on('data', (data) => {
|
|
||||||
this.sockService.sendMessage({
|
|
||||||
type: 'updateSystemVersion',
|
|
||||||
message: data.toString(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.stderr.on('data', (data) => {
|
|
||||||
this.sockService.sendMessage({
|
|
||||||
type: 'updateSystemVersion',
|
|
||||||
message: data.toString(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.on('error', (err) => {
|
|
||||||
this.sockService.sendMessage({
|
|
||||||
type: 'updateSystemVersion',
|
|
||||||
message: JSON.stringify(err),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return { code: 200 };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async reloadSystem(target?: 'system' | 'data') {
|
|
||||||
const cmd = `real_time=true ql reload ${target || ''}`;
|
|
||||||
const cp = spawn(cmd, {
|
|
||||||
shell: '/bin/bash',
|
|
||||||
detached: true,
|
|
||||||
stdio: 'ignore',
|
|
||||||
});
|
|
||||||
cp.unref();
|
|
||||||
setTimeout(() => {
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
return { code: 200 };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async notify({
|
|
||||||
title,
|
|
||||||
content,
|
|
||||||
notificationInfo,
|
|
||||||
}: {
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
notificationInfo?: NotificationInfo;
|
|
||||||
}) {
|
|
||||||
const typeString =
|
|
||||||
typeof notificationInfo?.type === 'number'
|
|
||||||
? NotificationModeStringMap[notificationInfo.type]
|
|
||||||
: undefined;
|
|
||||||
if (notificationInfo && typeString) {
|
|
||||||
notificationInfo.type = typeString;
|
|
||||||
}
|
|
||||||
const isSuccess = await this.notificationService.notify(
|
|
||||||
title,
|
|
||||||
content,
|
|
||||||
notificationInfo,
|
|
||||||
);
|
|
||||||
if (isSuccess) {
|
|
||||||
return { code: 200, message: '通知发送成功' };
|
|
||||||
} else {
|
|
||||||
return { code: 400, message: '通知发送失败,请检查系统设置/通知配置' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async run({ command, logPath }: { command: string; logPath?: string }, callback: TaskCallbacks) {
|
|
||||||
if (!command.startsWith(TASK_COMMAND)) {
|
|
||||||
command = `${TASK_COMMAND} ${command}`;
|
|
||||||
}
|
|
||||||
const logPathPrefix = logPath ? `real_log_path=${logPath}` : ''
|
|
||||||
this.scheduleService.runTask(`${logPathPrefix} real_time=true ${command}`, callback, {
|
|
||||||
command,
|
|
||||||
id: command.replace(/ /g, '-'),
|
|
||||||
runOrigin: 'system',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async stop({ command, pid }: { command: string; pid: number }) {
|
|
||||||
if (!pid && !command) {
|
|
||||||
return { code: 400, message: '参数错误' };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pid) {
|
|
||||||
await killTask(pid);
|
|
||||||
return { code: 200 };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!command.startsWith(TASK_COMMAND)) {
|
|
||||||
command = `${TASK_COMMAND} ${command}`;
|
|
||||||
}
|
|
||||||
const _pid = await getPid(command);
|
|
||||||
if (_pid) {
|
|
||||||
await killTask(_pid);
|
|
||||||
return { code: 200 };
|
|
||||||
} else {
|
|
||||||
return { code: 400, message: '任务未找到' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async exportData(res: Response, type?: string[]) {
|
|
||||||
try {
|
|
||||||
let dataDirs = ['db', 'upload'];
|
|
||||||
if (type && type.length) {
|
|
||||||
dataDirs = dataDirs.concat(type.filter((x) => x !== 'base'));
|
|
||||||
}
|
|
||||||
const dataPaths = dataDirs.map((dir) => `data/${dir}`);
|
|
||||||
await promiseExec(
|
|
||||||
`cd ${config.dataPath} && cd ../ && tar -zcvf ${config.dataTgzFile
|
|
||||||
} ${dataPaths.join(' ')}`,
|
|
||||||
);
|
|
||||||
res.download(config.dataTgzFile);
|
|
||||||
} catch (error: any) {
|
|
||||||
return res.send({ code: 400, message: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async importData() {
|
|
||||||
try {
|
|
||||||
await promiseExec(`rm -rf ${path.join(config.tmpPath, 'data')}`);
|
|
||||||
const res = await promiseExec(
|
|
||||||
`cd ${config.tmpPath} && tar -zxvf ${config.dataTgzFile}`,
|
|
||||||
);
|
|
||||||
return { code: 200, data: res };
|
|
||||||
} catch (error: any) {
|
|
||||||
return { code: 400, message: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getSystemLog(
|
|
||||||
res: Response,
|
|
||||||
query: {
|
|
||||||
startTime?: string;
|
|
||||||
endTime?: string;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
const startTime = dayjs(query.startTime || undefined)
|
|
||||||
.startOf('d')
|
|
||||||
.valueOf();
|
|
||||||
const endTime = dayjs(query.endTime || undefined)
|
|
||||||
.endOf('d')
|
|
||||||
.valueOf();
|
|
||||||
const result = await readDirs(config.systemLogPath, config.systemLogPath);
|
|
||||||
const logs = result
|
|
||||||
.reverse()
|
|
||||||
.filter((x) => x.title.endsWith('.log'))
|
|
||||||
.filter((x) => x.createTime >= startTime && x.createTime <= endTime);
|
|
||||||
|
|
||||||
res.set({
|
|
||||||
'Content-Length': sum(logs.map((x) => x.size)),
|
|
||||||
});
|
|
||||||
(function sendFiles(res, fileNames) {
|
|
||||||
if (fileNames.length === 0) {
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentLog = fileNames.shift();
|
|
||||||
if (currentLog) {
|
|
||||||
const currentFileStream = fs.createReadStream(
|
|
||||||
path.join(config.systemLogPath, currentLog.title),
|
|
||||||
);
|
|
||||||
currentFileStream.on('end', () => {
|
|
||||||
sendFiles(res, fileNames);
|
|
||||||
});
|
|
||||||
currentFileStream.pipe(res, { end: false });
|
|
||||||
}
|
|
||||||
})(res, logs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async deleteSystemLog() {
|
|
||||||
const result = await readDirs(config.systemLogPath, config.systemLogPath);
|
|
||||||
const logs = result.reverse().filter((x) => x.title.endsWith('.log'));
|
|
||||||
for (const log of logs) {
|
|
||||||
await rmPath(path.join(config.systemLogPath, log.title));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateTimezone(info: SystemModelInfo) {
|
|
||||||
if (!info.timezone) {
|
|
||||||
info.timezone = 'Asia/Shanghai';
|
|
||||||
}
|
|
||||||
const oDoc = await this.getSystemConfig();
|
|
||||||
await this.updateAuthDb({
|
|
||||||
...oDoc,
|
|
||||||
info: { ...oDoc.info, ...info },
|
|
||||||
});
|
|
||||||
const success = await setSystemTimezone(info.timezone);
|
|
||||||
if (success) {
|
|
||||||
return { code: 200, data: info };
|
|
||||||
} else {
|
|
||||||
return { code: 400, message: '设置时区失败' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateGlobalSshKey(info: SystemModelInfo) {
|
|
||||||
const oDoc = await this.getSystemConfig();
|
|
||||||
const result = await this.updateAuthDb({
|
|
||||||
...oDoc,
|
|
||||||
info: { ...oDoc.info, ...info },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Apply the global SSH key
|
|
||||||
const SshKeyService = require('./sshKey').default;
|
|
||||||
const Container = require('typedi').Container;
|
|
||||||
const sshKeyService = Container.get(SshKeyService);
|
|
||||||
|
|
||||||
if (info.globalSshKey) {
|
|
||||||
await sshKeyService.addGlobalSSHKey(info.globalSshKey, 'global');
|
|
||||||
} else {
|
|
||||||
await sshKeyService.removeGlobalSSHKey('global');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { code: 200, data: result };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async cleanDependence(type: 'node' | 'python3') {
|
|
||||||
if (!type || !['node', 'python3'].includes(type)) {
|
|
||||||
return { code: 400, message: '参数错误' };
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const finalPath = path.join(config.dependenceCachePath, type);
|
|
||||||
await fs.promises.rm(finalPath, { recursive: true });
|
|
||||||
} catch (error) { }
|
|
||||||
return { code: 200 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+332
-358
@@ -1,35 +1,27 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import { createRandomString } from '../config/util';
|
import { createRandomString, getNetIp, getPlatform } from '../config/util';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import _ from 'lodash';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { authenticator } from '@otplib/preset-default';
|
import { authenticator } from '@otplib/preset-default';
|
||||||
import {
|
import DataStore from 'nedb';
|
||||||
AuthDataType,
|
import { AuthDataType, AuthInfo, LoginStatus } from '../data/auth';
|
||||||
SystemInfo,
|
|
||||||
SystemModel,
|
|
||||||
SystemModelInfo,
|
|
||||||
LoginStatus,
|
|
||||||
AuthInfo,
|
|
||||||
TokenInfo,
|
|
||||||
} from '../data/system';
|
|
||||||
import { NotificationInfo } from '../data/notify';
|
import { NotificationInfo } from '../data/notify';
|
||||||
import NotificationService from './notify';
|
import NotificationService from './notify';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import ScheduleService from './schedule';
|
import ScheduleService from './schedule';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
import SockService from './sock';
|
import SockService from './sock';
|
||||||
import dayjs from 'dayjs';
|
import got from 'got';
|
||||||
import IP2Region from 'ip2region';
|
import { dbs } from '../loaders/db';
|
||||||
import requestIp from 'request-ip';
|
|
||||||
import uniq from 'lodash/uniq';
|
|
||||||
import pickBy from 'lodash/pickBy';
|
|
||||||
import isNil from 'lodash/isNil';
|
|
||||||
import { shareStore } from '../shared/store';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class UserService {
|
export default class UserService {
|
||||||
@Inject((type) => NotificationService)
|
@Inject((type) => NotificationService)
|
||||||
private notificationService!: NotificationService;
|
private notificationService!: NotificationService;
|
||||||
|
private authDb = dbs.authDb;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject('logger') private logger: winston.Logger,
|
@Inject('logger') private logger: winston.Logger,
|
||||||
@@ -45,209 +37,185 @@ export default class UserService {
|
|||||||
req: Request,
|
req: Request,
|
||||||
needTwoFactor = true,
|
needTwoFactor = true,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
|
if (!fs.existsSync(config.authConfigFile)) {
|
||||||
|
return this.initAuthInfo();
|
||||||
|
}
|
||||||
|
|
||||||
let { username, password } = payloads;
|
let { username, password } = payloads;
|
||||||
const content = await this.getAuthInfo();
|
const content = this.getAuthInfo();
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
let {
|
if (content) {
|
||||||
username: cUsername,
|
let {
|
||||||
password: cPassword,
|
username: cUsername,
|
||||||
retries = 0,
|
password: cPassword,
|
||||||
lastlogon,
|
retries = 0,
|
||||||
lastip,
|
lastlogon,
|
||||||
lastaddr,
|
lastip,
|
||||||
twoFactorActivated,
|
lastaddr,
|
||||||
tokens = {},
|
twoFactorActivated,
|
||||||
platform,
|
twoFactorActived,
|
||||||
} = content;
|
tokens = {},
|
||||||
const retriesTime = Math.pow(3, retries) * 1000;
|
platform,
|
||||||
if (retries > 2 && timestamp - lastlogon < retriesTime) {
|
} = content;
|
||||||
const waitTime = Math.ceil(
|
// patch old field
|
||||||
(retriesTime - (timestamp - lastlogon)) / 1000,
|
twoFactorActivated = twoFactorActivated || twoFactorActived;
|
||||||
);
|
|
||||||
return {
|
|
||||||
code: 410,
|
|
||||||
message: `失败次数过多,请${waitTime}秒后重试`,
|
|
||||||
data: waitTime,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
username === cUsername &&
|
(cUsername === 'admin' && cPassword === 'admin') ||
|
||||||
password === cPassword &&
|
!cUsername ||
|
||||||
twoFactorActivated &&
|
!cPassword
|
||||||
needTwoFactor
|
) {
|
||||||
) {
|
return this.initAuthInfo();
|
||||||
await this.updateAuthInfo(content, {
|
}
|
||||||
isTwoFactorChecking: true,
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
code: 420,
|
|
||||||
message: '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const ip = requestIp.getClientIp(req) || '';
|
if (retries > 2 && Date.now() - lastlogon < Math.pow(3, retries) * 1000) {
|
||||||
const query = new IP2Region();
|
|
||||||
const ipAddress = query.search(ip);
|
|
||||||
let address = '';
|
|
||||||
if (ipAddress) {
|
|
||||||
const { country, province, city, isp } = ipAddress;
|
|
||||||
address = uniq([country, province, city, isp]).filter(Boolean).join(' ');
|
|
||||||
}
|
|
||||||
if (username === cUsername && password === cPassword) {
|
|
||||||
const data = createRandomString(50, 100);
|
|
||||||
const expiration = twoFactorActivated ? '60d' : '20d';
|
|
||||||
let token = jwt.sign({ data }, config.jwt.secret, {
|
|
||||||
expiresIn: config.jwt.expiresIn || expiration,
|
|
||||||
algorithm: 'HS384',
|
|
||||||
});
|
|
||||||
|
|
||||||
const tokenInfo: TokenInfo = {
|
|
||||||
value: token,
|
|
||||||
timestamp,
|
|
||||||
ip,
|
|
||||||
address,
|
|
||||||
platform: req.platform,
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedTokens = this.addTokenToList(
|
|
||||||
tokens,
|
|
||||||
req.platform,
|
|
||||||
tokenInfo,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.updateAuthInfo(content, {
|
|
||||||
token,
|
|
||||||
tokens: updatedTokens,
|
|
||||||
lastlogon: timestamp,
|
|
||||||
retries: 0,
|
|
||||||
lastip: ip,
|
|
||||||
lastaddr: address,
|
|
||||||
platform: req.platform,
|
|
||||||
isTwoFactorChecking: false,
|
|
||||||
});
|
|
||||||
this.notificationService.notify(
|
|
||||||
'登录通知',
|
|
||||||
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${
|
|
||||||
req.platform
|
|
||||||
}端 登录成功,ip地址 ${ip}`,
|
|
||||||
);
|
|
||||||
await this.insertDb({
|
|
||||||
type: AuthDataType.loginLog,
|
|
||||||
info: {
|
|
||||||
timestamp,
|
|
||||||
address,
|
|
||||||
ip,
|
|
||||||
platform: req.platform,
|
|
||||||
status: LoginStatus.success,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.getLoginLog();
|
|
||||||
return {
|
|
||||||
code: 200,
|
|
||||||
data: {
|
|
||||||
token,
|
|
||||||
lastip,
|
|
||||||
lastaddr,
|
|
||||||
lastlogon,
|
|
||||||
retries,
|
|
||||||
platform,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
await this.updateAuthInfo(content, {
|
|
||||||
retries: retries + 1,
|
|
||||||
lastlogon: timestamp,
|
|
||||||
lastip: ip,
|
|
||||||
lastaddr: address,
|
|
||||||
platform: req.platform,
|
|
||||||
});
|
|
||||||
this.notificationService.notify(
|
|
||||||
'登录通知',
|
|
||||||
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${
|
|
||||||
req.platform
|
|
||||||
}端 登录失败,ip地址 ${ip}`,
|
|
||||||
);
|
|
||||||
await this.insertDb({
|
|
||||||
type: AuthDataType.loginLog,
|
|
||||||
info: {
|
|
||||||
timestamp,
|
|
||||||
address,
|
|
||||||
ip,
|
|
||||||
platform: req.platform,
|
|
||||||
status: LoginStatus.fail,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.getLoginLog();
|
|
||||||
if (retries > 2) {
|
|
||||||
const waitTime = Math.round(Math.pow(3, retries + 1));
|
|
||||||
return {
|
return {
|
||||||
code: 410,
|
code: 410,
|
||||||
message: `失败次数过多,请${waitTime}秒后重试`,
|
message: `失败次数过多,请${Math.round(
|
||||||
data: waitTime,
|
(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 {
|
} 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 };
|
return { code: 400, message: config.authError };
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return this.initAuthInfo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async logout(platform: string, tokenValue: string): Promise<any> {
|
public async logout(platform: string): Promise<any> {
|
||||||
if (!platform || !tokenValue) {
|
const authInfo = this.getAuthInfo();
|
||||||
this.logger.warn('Invalid logout parameters - empty platform or token');
|
this.updateAuthInfo(authInfo, {
|
||||||
return;
|
token: '',
|
||||||
}
|
tokens: { ...authInfo.tokens, [platform]: '' },
|
||||||
|
|
||||||
const authInfo = await this.getAuthInfo();
|
|
||||||
|
|
||||||
// Verify the token exists before attempting to remove it
|
|
||||||
const tokenExists = this.findTokenInList(
|
|
||||||
authInfo.tokens,
|
|
||||||
platform,
|
|
||||||
tokenValue,
|
|
||||||
);
|
|
||||||
if (!tokenExists && authInfo.token !== tokenValue) {
|
|
||||||
// Token not found, but don't throw error - user may have already logged out
|
|
||||||
this.logger.info(
|
|
||||||
`Logout attempted for non-existent token on platform: ${platform}`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedTokens = this.removeTokenFromList(
|
|
||||||
authInfo.tokens,
|
|
||||||
platform,
|
|
||||||
tokenValue,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.updateAuthInfo(authInfo, {
|
|
||||||
token: authInfo.token === tokenValue ? '' : authInfo.token,
|
|
||||||
tokens: updatedTokens,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLoginLog(): Promise<Array<SystemModelInfo | undefined>> {
|
public async getLoginLog(): Promise<AuthInfo[]> {
|
||||||
const docs = await SystemModel.findAll({
|
return new Promise((resolve) => {
|
||||||
where: { type: AuthDataType.loginLog },
|
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));
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
if (docs && docs.length > 0) {
|
|
||||||
const result = docs.sort(
|
|
||||||
(a, b) => b.info!.timestamp! - a.info!.timestamp!,
|
|
||||||
);
|
|
||||||
if (result.length > 100) {
|
|
||||||
const ids = result.slice(100).map((x) => x.id!);
|
|
||||||
await SystemModel.destroy({
|
|
||||||
where: { id: ids },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result.map((x) => x.info);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async insertDb(payload: SystemInfo): Promise<SystemInfo> {
|
private async insertDb(payload: AuthInfo): Promise<AuthInfo> {
|
||||||
const doc = await SystemModel.create({ ...payload }, { returning: true });
|
return new Promise((resolve) => {
|
||||||
return doc;
|
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({
|
public async updateUsernameAndPassword({
|
||||||
@@ -260,33 +228,36 @@ export default class UserService {
|
|||||||
if (password === 'admin') {
|
if (password === 'admin') {
|
||||||
return { code: 400, message: '密码不能设置为admin' };
|
return { code: 400, message: '密码不能设置为admin' };
|
||||||
}
|
}
|
||||||
const authInfo = await this.getAuthInfo();
|
const authInfo = this.getAuthInfo();
|
||||||
await this.updateAuthInfo(authInfo, { username, password });
|
this.updateAuthInfo(authInfo, { username, password });
|
||||||
return { code: 200, message: '更新成功' };
|
return { code: 200, message: '更新成功' };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateAvatar(avatar: string) {
|
public getUserInfo(): Promise<any> {
|
||||||
const authInfo = await this.getAuthInfo();
|
return new Promise((resolve) => {
|
||||||
await this.updateAuthInfo(authInfo, { avatar });
|
fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
|
||||||
return { code: 200, data: avatar, message: '更新成功' };
|
if (err) console.log(err);
|
||||||
|
resolve(JSON.parse(data));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async initTwoFactor() {
|
public initTwoFactor() {
|
||||||
const secret = authenticator.generateSecret();
|
const secret = authenticator.generateSecret();
|
||||||
const authInfo = await this.getAuthInfo();
|
const authInfo = this.getAuthInfo();
|
||||||
const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);
|
const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);
|
||||||
await this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
|
this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
|
||||||
return { secret, url: otpauth };
|
return { secret, url: otpauth };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async activeTwoFactor(code: string) {
|
public activeTwoFactor(code: string) {
|
||||||
const authInfo = await this.getAuthInfo();
|
const authInfo = this.getAuthInfo();
|
||||||
const isValid = authenticator.verify({
|
const isValid = authenticator.verify({
|
||||||
token: code,
|
token: code,
|
||||||
secret: authInfo.twoFactorSecret,
|
secret: authInfo.twoFactorSecret,
|
||||||
});
|
});
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
await this.updateAuthInfo(authInfo, { twoFactorActivated: true });
|
this.updateAuthInfo(authInfo, { twoFactorActivated: true });
|
||||||
}
|
}
|
||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
@@ -299,7 +270,7 @@ export default class UserService {
|
|||||||
}: { username: string; password: string; code: string },
|
}: { username: string; password: string; code: string },
|
||||||
req: any,
|
req: any,
|
||||||
) {
|
) {
|
||||||
const authInfo = await this.getAuthInfo();
|
const authInfo = this.getAuthInfo();
|
||||||
const { isTwoFactorChecking, twoFactorSecret } = authInfo;
|
const { isTwoFactorChecking, twoFactorSecret } = authInfo;
|
||||||
if (!isTwoFactorChecking) {
|
if (!isTwoFactorChecking) {
|
||||||
return { code: 450, message: '未知错误' };
|
return { code: 450, message: '未知错误' };
|
||||||
@@ -311,17 +282,8 @@ export default class UserService {
|
|||||||
if (isValid) {
|
if (isValid) {
|
||||||
return this.login({ username, password }, req, false);
|
return this.login({ username, password }, req, false);
|
||||||
} else {
|
} else {
|
||||||
const ip = requestIp.getClientIp(req) || '';
|
const { ip, address } = await getNetIp(req);
|
||||||
const query = new IP2Region();
|
this.updateAuthInfo(authInfo, {
|
||||||
const ipAddress = query.search(ip);
|
|
||||||
let address = '';
|
|
||||||
if (ipAddress) {
|
|
||||||
const { country, province, city, isp } = ipAddress;
|
|
||||||
address = uniq([country, province, city, isp])
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ');
|
|
||||||
}
|
|
||||||
await this.updateAuthInfo(authInfo, {
|
|
||||||
lastip: ip,
|
lastip: ip,
|
||||||
lastaddr: address,
|
lastaddr: address,
|
||||||
platform: req.platform,
|
platform: req.platform,
|
||||||
@@ -330,58 +292,71 @@ export default class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deactiveTwoFactor() {
|
public deactiveTwoFactor() {
|
||||||
const authInfo = await this.getAuthInfo();
|
const authInfo = this.getAuthInfo();
|
||||||
await this.updateAuthInfo(authInfo, {
|
this.updateAuthInfo(authInfo, {
|
||||||
twoFactorActivated: false,
|
twoFactorActivated: false,
|
||||||
|
twoFactorActived: false,
|
||||||
twoFactorSecret: '',
|
twoFactorSecret: '',
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAuthInfo() {
|
private getAuthInfo() {
|
||||||
const authInfo = await shareStore.getAuthInfo();
|
const content = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||||
if (authInfo) {
|
return JSON.parse(content || '{}');
|
||||||
return authInfo;
|
|
||||||
}
|
|
||||||
const doc = await this.getDb({ type: AuthDataType.authConfig });
|
|
||||||
return (doc.info || {}) as AuthInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateAuthInfo(authInfo: AuthInfo, info: Partial<AuthInfo>) {
|
private updateAuthInfo(authInfo: any, info: any) {
|
||||||
const result = { ...authInfo, ...info };
|
fs.writeFileSync(
|
||||||
await shareStore.updateAuthInfo(result);
|
config.authConfigFile,
|
||||||
await this.updateAuthDb({
|
JSON.stringify({ ...authInfo, ...info }),
|
||||||
type: AuthDataType.authConfig,
|
);
|
||||||
info: result,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getNotificationMode(): Promise<NotificationInfo> {
|
public async getNotificationMode(): Promise<NotificationInfo> {
|
||||||
const doc = await this.getDb({ type: AuthDataType.notification });
|
return new Promise((resolve) => {
|
||||||
return (doc.info || {}) as NotificationInfo;
|
this.authDb
|
||||||
|
.find({ type: AuthDataType.notification })
|
||||||
|
.exec((err, docs) => {
|
||||||
|
if (err || docs.length === 0) {
|
||||||
|
resolve({} as NotificationInfo);
|
||||||
|
} else {
|
||||||
|
resolve(docs[0].info);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateAuthDb(payload: SystemInfo): Promise<any> {
|
public async getLogRemoveFrequency() {
|
||||||
let doc = await SystemModel.findOne({ where: { type: payload.type } });
|
return new Promise((resolve) => {
|
||||||
if (doc) {
|
this.authDb
|
||||||
const updateResult = await SystemModel.update(payload, {
|
.find({ type: AuthDataType.removeLogFrequency })
|
||||||
where: { id: doc.id },
|
.exec((err, docs) => {
|
||||||
returning: true,
|
if (err || docs.length === 0) {
|
||||||
});
|
resolve({});
|
||||||
doc = updateResult[1][0];
|
} else {
|
||||||
} else {
|
resolve(docs[0].info);
|
||||||
doc = await SystemModel.create(payload, { returning: true });
|
}
|
||||||
}
|
});
|
||||||
return doc;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDb(query: any): Promise<SystemInfo> {
|
private async updateAuthDb(payload: AuthInfo): Promise<any> {
|
||||||
const doc = await SystemModel.findOne({ where: { ...query } });
|
return new Promise((resolve) => {
|
||||||
if (!doc) {
|
this.authDb.update(
|
||||||
throw new Error(`${JSON.stringify(query)} not found`);
|
{ type: payload.type },
|
||||||
}
|
{ ...payload },
|
||||||
return doc.get({ plain: true });
|
{ 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) {
|
public async updateNotificationMode(notificationInfo: NotificationInfo) {
|
||||||
@@ -398,117 +373,116 @@ export default class UserService {
|
|||||||
});
|
});
|
||||||
return { code: 200, data: { ...result, code } };
|
return { code: 200, data: { ...result, code } };
|
||||||
} else {
|
} else {
|
||||||
return { code: 400, message: '通知发送失败,请检查参数' };
|
return { code: 400, data: '通知发送失败,请检查参数' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizeTokens(
|
public async updateLogRemoveFrequency(frequency: number) {
|
||||||
tokens: Record<string, string | TokenInfo[]>,
|
const result = await this.updateAuthDb({
|
||||||
): Record<string, TokenInfo[]> {
|
type: AuthDataType.removeLogFrequency,
|
||||||
const normalized: Record<string, TokenInfo[]> = {};
|
info: { frequency },
|
||||||
|
});
|
||||||
for (const [platform, value] of Object.entries(tokens)) {
|
const cron = {
|
||||||
if (typeof value === 'string') {
|
_id: result._id,
|
||||||
// Legacy format: convert string token to TokenInfo array
|
name: '删除日志',
|
||||||
if (value) {
|
command: `ql rmlog ${frequency}`,
|
||||||
normalized[platform] = [
|
schedule: `5 23 */${frequency} * *`,
|
||||||
{
|
};
|
||||||
value,
|
await this.scheduleService.cancelSchedule(cron);
|
||||||
timestamp: Date.now(),
|
if (frequency > 0) {
|
||||||
ip: '',
|
await this.scheduleService.generateSchedule(cron);
|
||||||
address: '',
|
|
||||||
platform,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
normalized[platform] = [];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Already in new format
|
|
||||||
normalized[platform] = value || [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return { code: 200, data: { ...cron } };
|
||||||
return normalized;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private addTokenToList(
|
public async checkUpdate() {
|
||||||
tokens: Record<string, string | TokenInfo[]>,
|
try {
|
||||||
platform: string,
|
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
||||||
tokenInfo: TokenInfo,
|
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
|
||||||
maxTokensPerPlatform: number = config.maxTokensPerPlatform,
|
|
||||||
): Record<string, TokenInfo[]> {
|
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
||||||
// Validate maxTokensPerPlatform parameter
|
const currentVersion = currentVersionFile.match(versionRegx)![1];
|
||||||
if (!Number.isInteger(maxTokensPerPlatform) || maxTokensPerPlatform < 1) {
|
|
||||||
this.logger.warn(
|
let lastVersion = '';
|
||||||
`Invalid maxTokensPerPlatform value: ${maxTokensPerPlatform}, using default`,
|
let lastLog = '';
|
||||||
);
|
try {
|
||||||
maxTokensPerPlatform = config.maxTokensPerPlatform;
|
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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalized = this.normalizeTokens(tokens);
|
|
||||||
|
|
||||||
if (!normalized[platform]) {
|
|
||||||
normalized[platform] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new token
|
|
||||||
normalized[platform].unshift(tokenInfo);
|
|
||||||
|
|
||||||
// Limit the number of active tokens per platform
|
|
||||||
if (normalized[platform].length > maxTokensPerPlatform) {
|
|
||||||
normalized[platform] = normalized[platform].slice(
|
|
||||||
0,
|
|
||||||
maxTokensPerPlatform,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeTokenFromList(
|
private checkHasNewVersion(curVersion: string, lastVersion: string) {
|
||||||
tokens: Record<string, string | TokenInfo[]>,
|
const curArr = curVersion.split('.').map((x) => parseInt(x, 10));
|
||||||
platform: string,
|
const lastArr = lastVersion.split('.').map((x) => parseInt(x, 10));
|
||||||
tokenValue: string,
|
if (curArr[0] < lastArr[0]) {
|
||||||
): Record<string, TokenInfo[]> {
|
return true;
|
||||||
const normalized = this.normalizeTokens(tokens);
|
|
||||||
|
|
||||||
if (normalized[platform]) {
|
|
||||||
normalized[platform] = normalized[platform].filter(
|
|
||||||
(t) => t.value !== tokenValue,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
if (curArr[0] === lastArr[0] && curArr[1] < lastArr[1]) {
|
||||||
return normalized;
|
return true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
curArr[0] === lastArr[0] &&
|
||||||
|
curArr[1] === lastArr[1] &&
|
||||||
|
curArr[2] < lastArr[2]
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private findTokenInList(
|
public async updateSystem() {
|
||||||
tokens: Record<string, string | TokenInfo[]>,
|
const cp = spawn('ql -l update', { shell: '/bin/bash' });
|
||||||
platform: string,
|
|
||||||
tokenValue: string,
|
|
||||||
): TokenInfo | undefined {
|
|
||||||
const normalized = this.normalizeTokens(tokens);
|
|
||||||
|
|
||||||
if (normalized[platform]) {
|
this.sockService.sendMessage({
|
||||||
return normalized[platform].find((t) => t.value === tokenValue);
|
type: 'updateSystemVersion',
|
||||||
}
|
message: `开始更新系统`,
|
||||||
|
});
|
||||||
|
cp.stdout.on('data', (data) => {
|
||||||
|
this.sockService.sendMessage({
|
||||||
|
type: 'updateSystemVersion',
|
||||||
|
message: data.toString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return undefined;
|
cp.stderr.on('data', (data) => {
|
||||||
}
|
this.sockService.sendMessage({
|
||||||
|
type: 'updateSystemVersion',
|
||||||
|
message: data.toString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
public async resetAuthInfo(info: Partial<AuthInfo>) {
|
cp.on('error', (err) => {
|
||||||
const { retries, twoFactorActivated, password, username } = info;
|
this.sockService.sendMessage({
|
||||||
const authInfo = await this.getAuthInfo();
|
type: 'updateSystemVersion',
|
||||||
const payload = pickBy(
|
message: JSON.stringify(err),
|
||||||
{
|
});
|
||||||
retries,
|
});
|
||||||
twoFactorActivated,
|
|
||||||
password,
|
|
||||||
username,
|
|
||||||
},
|
|
||||||
(x) => !isNil(x),
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.updateAuthInfo(authInfo, payload);
|
return { code: 200 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import { AuthInfo, TokenInfo } from '../data/system';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates if a token exists in the authentication info.
|
|
||||||
* Supports both legacy string tokens and new TokenInfo array format.
|
|
||||||
*
|
|
||||||
* @param authInfo - The authentication information
|
|
||||||
* @param headerToken - The token to validate
|
|
||||||
* @param platform - The platform (desktop, mobile)
|
|
||||||
* @returns true if the token is valid, false otherwise
|
|
||||||
*/
|
|
||||||
export function isValidToken(
|
|
||||||
authInfo: AuthInfo | null | undefined,
|
|
||||||
headerToken: string,
|
|
||||||
platform: string,
|
|
||||||
): boolean {
|
|
||||||
if (!authInfo || !headerToken) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { token = '', tokens = {} } = authInfo;
|
|
||||||
|
|
||||||
// Check legacy token field
|
|
||||||
if (headerToken === token) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check platform-specific tokens (support both legacy string and new TokenInfo[] format)
|
|
||||||
const platformTokens = tokens[platform];
|
|
||||||
|
|
||||||
// Handle null/undefined platformTokens
|
|
||||||
if (platformTokens === null || platformTokens === undefined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof platformTokens === 'string') {
|
|
||||||
// Legacy format: single string token
|
|
||||||
return headerToken === platformTokens;
|
|
||||||
} else if (Array.isArray(platformTokens)) {
|
|
||||||
// New format: array of TokenInfo objects
|
|
||||||
return platformTokens.some((t: TokenInfo) => t && t.value === headerToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unexpected type - log warning and reject
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { Dependence } from '../data/dependence';
|
|
||||||
import { ICron } from '../protos/cron';
|
|
||||||
|
|
||||||
export type Override<
|
|
||||||
T,
|
|
||||||
K extends Partial<{ [P in keyof T]: any }> | string,
|
|
||||||
> = K extends string
|
|
||||||
? Omit<T, K> & { [P in keyof T]: T[P] | unknown }
|
|
||||||
: Omit<T, keyof K> & K;
|
|
||||||
|
|
||||||
export type TCron = Override<Partial<ICron>, { id: string }>;
|
|
||||||
|
|
||||||
export interface IDependencyFn<T> {
|
|
||||||
(): Promise<T>;
|
|
||||||
dependency?: Dependence;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICronFn<T> {
|
|
||||||
(): Promise<T>;
|
|
||||||
cron?: TCron;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISchedule {
|
|
||||||
schedule?: string;
|
|
||||||
name?: string;
|
|
||||||
command?: string;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IScheduleFn<T> {
|
|
||||||
(): Promise<T>;
|
|
||||||
schedule?: ISchedule;
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user