@whyour/qinglong 2.21.0-1 → 2.21.0-10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -2
- package/back/protos/api.proto +3 -0
- package/deploy/kubernetes/README.md +81 -0
- package/deploy/kubernetes/base/kustomization.yaml +5 -0
- package/deploy/kubernetes/base/qinglong.yaml +95 -0
- package/deploy/kubernetes/overlays/example/kustomization.yaml +10 -0
- package/package.json +2 -4
- package/sample/config.sample.sh +7 -2
- package/sample/notify.js +64 -0
- package/sample/notify.py +56 -0
- package/shell/api.sh +7 -1
- package/shell/share.sh +1 -1
- package/static/build/api/cron.js +3 -3
- package/static/build/api/dashboard.js +13 -6
- package/static/build/api/env.js +2 -2
- package/static/build/api/log.js +10 -1
- package/static/build/api/system.js +6 -7
- package/static/build/app.js +19 -19
- package/static/build/config/const.js +1 -0
- package/static/build/config/grpcCerts.js +4 -4
- package/static/build/config/index.js +0 -4
- package/static/build/config/util.js +3 -3
- package/static/build/data/notify.js +8 -1
- package/static/build/data/sock.js +1 -0
- package/static/build/loaders/app.js +6 -6
- package/static/build/loaders/db.js +2 -2
- package/static/build/loaders/express.js +17 -14
- package/static/build/loaders/initData.js +8 -7
- package/static/build/loaders/initFile.js +1 -1
- package/static/build/loaders/initTask.js +3 -2
- package/static/build/loaders/server.js +1 -1
- package/static/build/protos/api.js +23 -1
- package/static/build/schedule/addCron.js +67 -10
- package/static/build/schedule/delCron.js +11 -1
- package/static/build/services/cron.js +103 -53
- package/static/build/services/dependence.js +19 -18
- package/static/build/services/grpc.js +1 -1
- package/static/build/services/http.js +1 -1
- package/static/build/services/notify.js +47 -5
- package/static/build/services/subscription.js +6 -4
- package/static/build/services/system.js +12 -2
- package/static/build/services/user.js +31 -6
- package/static/build/shared/auth.js +7 -1
- package/static/build/shared/i18n.js +59 -4
- package/static/build/shared/pLimit.js +3 -2
- package/static/build/shared/store.js +7 -0
- package/static/build/validation/schedule.js +9 -0
- package/static/dist/1147.62793604.async.js +1 -0
- package/static/dist/2208.85285476.async.js +1 -0
- package/static/dist/2286.f371c743.async.js +1 -0
- package/static/dist/3191.545ab873.async.js +1 -0
- package/static/dist/8826.db34ad8d.async.js +1 -0
- package/static/dist/index.html +2 -2
- package/static/dist/{preload_helper.0431c0f3.js → preload_helper.3048fe5b.js} +1 -1
- package/static/dist/src__pages__crontab__detail.b98f0084.async.js +1 -0
- package/static/dist/src__pages__crontab__index.68e78a33.async.js +1 -0
- package/static/dist/{src__pages__crontab__logModal.0b8cce8c.async.js → src__pages__crontab__logModal.831d5e4b.async.js} +1 -1
- package/static/dist/src__pages__dashboard__index.58177357.async.js +1 -0
- package/static/dist/src__pages__dependence__index.b7bbc795.async.js +1 -0
- package/static/dist/src__pages__dependence__modal.f7bb47ba.async.js +1 -0
- package/static/dist/src__pages__env__editNameModal.48715576.async.js +1 -0
- package/static/dist/src__pages__env__index.452cdc47.async.js +1 -0
- package/static/dist/src__pages__env__modal.e1234834.async.js +1 -0
- package/static/dist/src__pages__error__index.dedb2b90.async.js +1 -0
- package/static/dist/src__pages__initialization__index.22620a77.async.js +1 -0
- package/static/dist/src__pages__log__index.9c66639f.async.js +1 -0
- package/static/dist/src__pages__script__editNameModal.532a3044.async.js +1 -0
- package/static/dist/src__pages__script__index.1fc20f22.async.js +1 -0
- package/static/dist/src__pages__script__renameModal.5cdde919.async.js +1 -0
- package/static/dist/src__pages__script__saveModal.5ffed35f.async.js +1 -0
- package/static/dist/src__pages__script__setting.1a82c6d7.async.js +1 -0
- package/static/dist/src__pages__setting__appModal.43b7758a.async.js +1 -0
- package/static/dist/src__pages__setting__checkUpdate.34d773f2.async.js +1 -0
- package/static/dist/src__pages__setting__dependence.9c04c79a.async.js +1 -0
- package/static/dist/src__pages__setting__index.7e070838.async.js +1 -0
- package/static/dist/src__pages__setting__notification.bac481c2.async.js +1 -0
- package/static/dist/src__pages__setting__other.707cfafe.async.js +1 -0
- package/static/dist/src__pages__setting__security.b7235753.async.js +1 -0
- package/static/dist/src__pages__setting__systemLog.b81a8c1d.async.js +1 -0
- package/static/dist/src__pages__subscription__modal.562448e2.async.js +1 -0
- package/static/dist/{umi.ca04a019.js → umi.a54a59f6.js} +1 -1
- package/CLAUDE.md +0 -43
- package/static/dist/1147.c420132e.async.js +0 -1
- package/static/dist/2208.f7ba3dfa.async.js +0 -1
- package/static/dist/2286.164bb089.async.js +0 -1
- package/static/dist/3191.63263871.async.js +0 -1
- package/static/dist/8826.2447a104.async.js +0 -1
- package/static/dist/src__pages__crontab__detail.b9c36808.async.js +0 -1
- package/static/dist/src__pages__crontab__index.fed99fd0.async.js +0 -1
- package/static/dist/src__pages__dashboard__index.b30f2f47.async.js +0 -1
- package/static/dist/src__pages__dependence__index.3d87ceac.async.js +0 -1
- package/static/dist/src__pages__dependence__modal.11124896.async.js +0 -1
- package/static/dist/src__pages__env__editNameModal.5d264b25.async.js +0 -1
- package/static/dist/src__pages__env__index.baa27d4e.async.js +0 -1
- package/static/dist/src__pages__env__modal.7f2ef1bc.async.js +0 -1
- package/static/dist/src__pages__error__index.f156b45e.async.js +0 -1
- package/static/dist/src__pages__initialization__index.e96d4ba8.async.js +0 -1
- package/static/dist/src__pages__log__index.e0978bae.async.js +0 -1
- package/static/dist/src__pages__script__editNameModal.3e7e100d.async.js +0 -1
- package/static/dist/src__pages__script__index.2765d1b8.async.js +0 -1
- package/static/dist/src__pages__script__renameModal.5e987ef5.async.js +0 -1
- package/static/dist/src__pages__script__saveModal.3f9d23d6.async.js +0 -1
- package/static/dist/src__pages__script__setting.a535793a.async.js +0 -1
- package/static/dist/src__pages__setting__appModal.251cd14f.async.js +0 -1
- package/static/dist/src__pages__setting__checkUpdate.46efe057.async.js +0 -1
- package/static/dist/src__pages__setting__dependence.8a7a9529.async.js +0 -1
- package/static/dist/src__pages__setting__index.cc85fdfb.async.js +0 -1
- package/static/dist/src__pages__setting__notification.390fc905.async.js +0 -1
- package/static/dist/src__pages__setting__other.b22d2165.async.js +0 -1
- package/static/dist/src__pages__setting__security.598720a8.async.js +0 -1
- package/static/dist/src__pages__setting__systemLog.67406721.async.js +0 -1
- package/static/dist/src__pages__subscription__modal.3562c670.async.js +0 -1
package/AGENTS.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!-- gitnexus:start -->
|
|
2
2
|
# GitNexus — Code Intelligence
|
|
3
3
|
|
|
4
|
-
This project is indexed by GitNexus as **qinglong** (
|
|
4
|
+
This project is indexed by GitNexus as **qinglong** (2778 symbols, 6698 relationships, 233 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
|
5
5
|
|
|
6
6
|
> Index stale? Run `node .gitnexus/run.cjs analyze` from the project root — it auto-selects an available runner. No `.gitnexus/run.cjs` yet? `npx gitnexus analyze` (npm 11 crash → `npm i -g gitnexus`; #1939).
|
|
7
7
|
|
|
@@ -40,4 +40,4 @@ This project is indexed by GitNexus as **qinglong** (2740 symbols, 6583 relation
|
|
|
40
40
|
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
|
|
41
41
|
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
|
|
42
42
|
|
|
43
|
-
<!-- gitnexus:end -->
|
|
43
|
+
<!-- gitnexus:end -->
|
package/back/protos/api.proto
CHANGED
|
@@ -151,6 +151,7 @@ enum NotificationMode {
|
|
|
151
151
|
chronocat = 18;
|
|
152
152
|
ntfy = 19;
|
|
153
153
|
wxPusherBot = 20;
|
|
154
|
+
wxPusherSpt = 21;
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
message NotificationInfo {
|
|
@@ -244,6 +245,8 @@ message NotificationInfo {
|
|
|
244
245
|
optional string wxPusherBotAppToken = 66;
|
|
245
246
|
optional string wxPusherBotTopicIds = 67;
|
|
246
247
|
optional string wxPusherBotUids = 68;
|
|
248
|
+
|
|
249
|
+
optional string wxPusherSptList = 70;
|
|
247
250
|
}
|
|
248
251
|
|
|
249
252
|
message SystemNotifyRequest {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Kubernetes deployment
|
|
2
|
+
|
|
3
|
+
This deploys Qinglong as a single-replica `StatefulSet` with persistent data at `/ql/data`.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
kubectl apply -k deploy/kubernetes/overlays/local
|
|
7
|
+
kubectl -n qinglong rollout status statefulset/qinglong
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Open the panel locally:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
kubectl -n qinglong port-forward svc/qinglong 5700:5700
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Then visit <http://127.0.0.1:5700>.
|
|
17
|
+
|
|
18
|
+
## Image registry overlays
|
|
19
|
+
|
|
20
|
+
Use `overlays/example` as the committed template for registry customization:
|
|
21
|
+
|
|
22
|
+
```yaml
|
|
23
|
+
whyour/qinglong:debian -> registry.example.com/whyour/qinglong:debian
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Create `overlays/local/kustomization.yaml` for the actual cluster image. The `local` overlay is ignored by git so private registry names, digests, and credentials-related references stay local.
|
|
27
|
+
|
|
28
|
+
## Storage
|
|
29
|
+
|
|
30
|
+
The manifest creates a 5 GiB `ReadWriteOnce` PVC from the cluster's default `StorageClass`.
|
|
31
|
+
If your cluster has no default storage class, add `storageClassName` under:
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
volumeClaimTemplates:
|
|
35
|
+
- metadata:
|
|
36
|
+
name: data
|
|
37
|
+
spec:
|
|
38
|
+
storageClassName: your-storage-class
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Keep `replicas: 1`. Qinglong stores state in the persistent data directory, including SQLite files, so multiple replicas should not share the same data volume.
|
|
42
|
+
|
|
43
|
+
## Ingress example
|
|
44
|
+
|
|
45
|
+
If you expose Qinglong through an Ingress path other than `/`, set `QlBaseUrl` to the same path with leading and trailing slashes.
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
env:
|
|
49
|
+
- name: QlBaseUrl
|
|
50
|
+
value: "/qinglong/"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Example Ingress:
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
apiVersion: networking.k8s.io/v1
|
|
57
|
+
kind: Ingress
|
|
58
|
+
metadata:
|
|
59
|
+
name: qinglong
|
|
60
|
+
namespace: qinglong
|
|
61
|
+
spec:
|
|
62
|
+
rules:
|
|
63
|
+
- host: qinglong.example.com
|
|
64
|
+
http:
|
|
65
|
+
paths:
|
|
66
|
+
- path: /
|
|
67
|
+
pathType: Prefix
|
|
68
|
+
backend:
|
|
69
|
+
service:
|
|
70
|
+
name: qinglong
|
|
71
|
+
port:
|
|
72
|
+
number: 5700
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Maintenance commands
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
kubectl -n qinglong logs -f statefulset/qinglong
|
|
79
|
+
kubectl -n qinglong exec -it statefulset/qinglong -- ql check
|
|
80
|
+
kubectl -n qinglong exec -it statefulset/qinglong -- ql update
|
|
81
|
+
```
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
apiVersion: v1
|
|
2
|
+
kind: Namespace
|
|
3
|
+
metadata:
|
|
4
|
+
name: qinglong
|
|
5
|
+
---
|
|
6
|
+
apiVersion: v1
|
|
7
|
+
kind: Service
|
|
8
|
+
metadata:
|
|
9
|
+
name: qinglong
|
|
10
|
+
namespace: qinglong
|
|
11
|
+
labels:
|
|
12
|
+
app.kubernetes.io/name: qinglong
|
|
13
|
+
spec:
|
|
14
|
+
type: ClusterIP
|
|
15
|
+
selector:
|
|
16
|
+
app.kubernetes.io/name: qinglong
|
|
17
|
+
ports:
|
|
18
|
+
- name: http
|
|
19
|
+
port: 5700
|
|
20
|
+
targetPort: http
|
|
21
|
+
---
|
|
22
|
+
apiVersion: apps/v1
|
|
23
|
+
kind: StatefulSet
|
|
24
|
+
metadata:
|
|
25
|
+
name: qinglong
|
|
26
|
+
namespace: qinglong
|
|
27
|
+
labels:
|
|
28
|
+
app.kubernetes.io/name: qinglong
|
|
29
|
+
spec:
|
|
30
|
+
serviceName: qinglong
|
|
31
|
+
replicas: 1
|
|
32
|
+
selector:
|
|
33
|
+
matchLabels:
|
|
34
|
+
app.kubernetes.io/name: qinglong
|
|
35
|
+
template:
|
|
36
|
+
metadata:
|
|
37
|
+
labels:
|
|
38
|
+
app.kubernetes.io/name: qinglong
|
|
39
|
+
spec:
|
|
40
|
+
securityContext:
|
|
41
|
+
fsGroup: 5432
|
|
42
|
+
fsGroupChangePolicy: OnRootMismatch
|
|
43
|
+
containers:
|
|
44
|
+
- name: qinglong
|
|
45
|
+
image: whyour/qinglong:debian
|
|
46
|
+
imagePullPolicy: IfNotPresent
|
|
47
|
+
env:
|
|
48
|
+
- name: QlBaseUrl
|
|
49
|
+
value: "/"
|
|
50
|
+
- name: TZ
|
|
51
|
+
value: Asia/Shanghai
|
|
52
|
+
ports:
|
|
53
|
+
- name: http
|
|
54
|
+
containerPort: 5700
|
|
55
|
+
readinessProbe:
|
|
56
|
+
httpGet:
|
|
57
|
+
path: /api/health
|
|
58
|
+
port: http
|
|
59
|
+
initialDelaySeconds: 20
|
|
60
|
+
periodSeconds: 10
|
|
61
|
+
timeoutSeconds: 3
|
|
62
|
+
failureThreshold: 6
|
|
63
|
+
livenessProbe:
|
|
64
|
+
httpGet:
|
|
65
|
+
path: /api/health
|
|
66
|
+
port: http
|
|
67
|
+
initialDelaySeconds: 60
|
|
68
|
+
periodSeconds: 20
|
|
69
|
+
timeoutSeconds: 3
|
|
70
|
+
failureThreshold: 6
|
|
71
|
+
startupProbe:
|
|
72
|
+
httpGet:
|
|
73
|
+
path: /api/health
|
|
74
|
+
port: http
|
|
75
|
+
periodSeconds: 5
|
|
76
|
+
timeoutSeconds: 3
|
|
77
|
+
failureThreshold: 60
|
|
78
|
+
resources:
|
|
79
|
+
requests:
|
|
80
|
+
cpu: 100m
|
|
81
|
+
memory: 256Mi
|
|
82
|
+
limits:
|
|
83
|
+
memory: 1Gi
|
|
84
|
+
volumeMounts:
|
|
85
|
+
- name: data
|
|
86
|
+
mountPath: /ql/data
|
|
87
|
+
volumeClaimTemplates:
|
|
88
|
+
- metadata:
|
|
89
|
+
name: data
|
|
90
|
+
spec:
|
|
91
|
+
accessModes:
|
|
92
|
+
- ReadWriteOnce
|
|
93
|
+
resources:
|
|
94
|
+
requests:
|
|
95
|
+
storage: 5Gi
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whyour/qinglong",
|
|
3
3
|
"packageManager": "pnpm@8.3.1",
|
|
4
|
-
"version": "2.21.0-
|
|
4
|
+
"version": "2.21.0-10",
|
|
5
5
|
"description": "Timed task management platform supporting Python3, JavaScript, Shell, Typescript",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -21,9 +21,7 @@
|
|
|
21
21
|
"panel": "npm run build:back && node static/build/app.js",
|
|
22
22
|
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true,snakeToCamel=false",
|
|
23
23
|
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
|
24
|
-
"postinstall": "max setup 2>/dev/null || true"
|
|
25
|
-
"test": "umi-test",
|
|
26
|
-
"test:coverage": "umi-test --coverage"
|
|
24
|
+
"postinstall": "max setup 2>/dev/null || true"
|
|
27
25
|
},
|
|
28
26
|
"gitHooks": {
|
|
29
27
|
"pre-commit": "lint-staged"
|
package/sample/config.sample.sh
CHANGED
|
@@ -241,14 +241,19 @@ export NTFY_PASSWORD=""
|
|
|
241
241
|
export NTFY_ACTIONS=""
|
|
242
242
|
|
|
243
243
|
## 21. wxPusher
|
|
244
|
-
|
|
245
|
-
|
|
244
|
+
### 方式一:标准发送(更强大)
|
|
245
|
+
#### 官方文档: https://wxpusher.zjiecode.com/docs/
|
|
246
|
+
#### 管理后台: https://wxpusher.zjiecode.com/admin/
|
|
246
247
|
## wxPusher 的 appToken
|
|
247
248
|
export WXPUSHER_APP_TOKEN=""
|
|
248
249
|
## wxPusher 的 topicIds,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
|
249
250
|
export WXPUSHER_TOPIC_IDS=""
|
|
250
251
|
## wxPusher 的 用户ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
|
251
252
|
export WXPUSHER_UIDS=""
|
|
253
|
+
### 方式二:极简发送(最简单,简单好用,一键配置,更推荐)
|
|
254
|
+
#### wxPusher 的 SPT(极简推送),扫码获取 https://wxpusher.zjiecode.com/docs/#/?id=spt
|
|
255
|
+
#### 多个用英文逗号,分隔,最多10个;与上面的 appToken 方式二选一即可
|
|
256
|
+
export WXPUSHER_SPT_LIST=""
|
|
252
257
|
|
|
253
258
|
## 22. 自定义通知
|
|
254
259
|
## 自定义通知 接收回调的URL
|
package/sample/notify.js
CHANGED
|
@@ -147,11 +147,16 @@ const push_config = {
|
|
|
147
147
|
NTFY_PASSWORD: '', // 推送用户密码,可选
|
|
148
148
|
NTFY_ACTIONS: '', // 推送用户动作,可选
|
|
149
149
|
|
|
150
|
+
// 方式一:标准发送(更强大)
|
|
150
151
|
// 官方文档: https://wxpusher.zjiecode.com/docs/
|
|
151
152
|
// 管理后台: https://wxpusher.zjiecode.com/admin/
|
|
152
153
|
WXPUSHER_APP_TOKEN: '', // wxpusher 的 appToken
|
|
153
154
|
WXPUSHER_TOPIC_IDS: '', // wxpusher 的 主题ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
|
154
155
|
WXPUSHER_UIDS: '', // wxpusher 的 用户ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
|
156
|
+
// 方式二:极简发送(最简单,简单好用,一键配置,更推荐)
|
|
157
|
+
// wxPusher 的 SPT(极简推送),扫码获取 https://wxpusher.zjiecode.com/docs/#/?id=spt
|
|
158
|
+
// 多个用英文逗号,分隔,最多10个;与上面的 appToken 方式二选一即可
|
|
159
|
+
WXPUSHER_SPT_LIST: '', // wxpusher 的 SPT(极简推送),多个用英文逗号,分隔,最多10个
|
|
155
160
|
|
|
156
161
|
// 官方文档: https://openilink.com/docs/hub/apps
|
|
157
162
|
OPENILINK_APP_TOKEN: '', // OpeniLink 的 app_token,在 OpeniLink Hub 后台安装 App 后获取
|
|
@@ -1431,6 +1436,64 @@ function wxPusherNotify(text, desp) {
|
|
|
1431
1436
|
});
|
|
1432
1437
|
}
|
|
1433
1438
|
|
|
1439
|
+
function wxPusherSptNotify(text, desp) {
|
|
1440
|
+
return new Promise((resolve) => {
|
|
1441
|
+
const { WXPUSHER_SPT_LIST } = push_config;
|
|
1442
|
+
if (WXPUSHER_SPT_LIST) {
|
|
1443
|
+
// 处理 SPT,将逗号分隔的字符串转为数组
|
|
1444
|
+
const spts = WXPUSHER_SPT_LIST.split(',')
|
|
1445
|
+
.map((spt) => spt.trim())
|
|
1446
|
+
.filter((spt) => spt);
|
|
1447
|
+
|
|
1448
|
+
if (!spts.length) {
|
|
1449
|
+
console.log('wxpusher SPT 不能为空!!');
|
|
1450
|
+
return resolve();
|
|
1451
|
+
}
|
|
1452
|
+
if (spts.length > 10) {
|
|
1453
|
+
console.log('wxpusher SPT 最多支持 10 个!!');
|
|
1454
|
+
return resolve();
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
const body = {
|
|
1458
|
+
content: `<h1>${text}</h1><br/><div style='white-space: pre-wrap;'>${desp}</div>`,
|
|
1459
|
+
summary: text,
|
|
1460
|
+
contentType: 2,
|
|
1461
|
+
// 单个 SPT 用 spt,多个用 sptList
|
|
1462
|
+
...(spts.length === 1 ? { spt: spts[0] } : { sptList: spts }),
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1465
|
+
const options = {
|
|
1466
|
+
url: 'https://wxpusher.zjiecode.com/api/send/message/simple-push',
|
|
1467
|
+
body: JSON.stringify(body),
|
|
1468
|
+
headers: {
|
|
1469
|
+
'Content-Type': 'application/json',
|
|
1470
|
+
},
|
|
1471
|
+
timeout,
|
|
1472
|
+
};
|
|
1473
|
+
|
|
1474
|
+
$.post(options, (err, resp, data) => {
|
|
1475
|
+
try {
|
|
1476
|
+
if (err) {
|
|
1477
|
+
console.log('wxpusher SPT 发送通知消息失败!\n', err);
|
|
1478
|
+
} else {
|
|
1479
|
+
if (data.code === 1000) {
|
|
1480
|
+
console.log('wxpusher SPT 发送通知消息完成!');
|
|
1481
|
+
} else {
|
|
1482
|
+
console.log(`wxpusher SPT 发送通知消息异常:${data.msg}`);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
} catch (e) {
|
|
1486
|
+
$.logErr(e, resp);
|
|
1487
|
+
} finally {
|
|
1488
|
+
resolve(data);
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
} else {
|
|
1492
|
+
resolve();
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1434
1497
|
function openiLinkNotify(text, desp) {
|
|
1435
1498
|
return new Promise((resolve) => {
|
|
1436
1499
|
const { OPENILINK_APP_TOKEN, OPENILINK_HUB_URL, OPENILINK_CONTEXT_TOKEN } =
|
|
@@ -1609,6 +1672,7 @@ async function sendNotify(text, desp, params = {}) {
|
|
|
1609
1672
|
qmsgNotify(text, desp), // 自定义通知
|
|
1610
1673
|
ntfyNotify(text, desp), // Ntfy
|
|
1611
1674
|
wxPusherNotify(text, desp), // wxpusher
|
|
1675
|
+
wxPusherSptNotify(text, desp), // wxpusher SPT
|
|
1612
1676
|
openiLinkNotify(text, desp), // OpeniLink
|
|
1613
1677
|
]);
|
|
1614
1678
|
}
|
package/sample/notify.py
CHANGED
|
@@ -133,9 +133,16 @@ push_config = {
|
|
|
133
133
|
'NTFY_PASSWORD': '', # 推送用户密码,可选
|
|
134
134
|
'NTFY_ACTIONS': '', # 推送用户动作,可选
|
|
135
135
|
|
|
136
|
+
### 方式一:标准发送(更强大)
|
|
137
|
+
#### 官方文档: https://wxpusher.zjiecode.com/docs/
|
|
138
|
+
#### 管理后台: https://wxpusher.zjiecode.com/admin/
|
|
136
139
|
'WXPUSHER_APP_TOKEN': '', # wxpusher 的 appToken 官方文档: https://wxpusher.zjiecode.com/docs/ 管理后台: https://wxpusher.zjiecode.com/admin/
|
|
137
140
|
'WXPUSHER_TOPIC_IDS': '', # wxpusher 的 主题ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
|
138
141
|
'WXPUSHER_UIDS': '', # wxpusher 的 用户ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
|
142
|
+
### 方式二:极简发送(最简单,简单好用,一键配置,更推荐)
|
|
143
|
+
#### wxPusher 的 SPT(极简推送),扫码获取 https://wxpusher.zjiecode.com/docs/#/?id=spt
|
|
144
|
+
#### 多个用英文逗号,分隔,最多10个;与上面的 appToken 方式二选一即可
|
|
145
|
+
'WXPUSHER_SPT_LIST': '', # wxpusher 的 SPT(极简推送),多个用英文逗号,分隔,最多10个 官方文档: https://wxpusher.zjiecode.com/docs/#/?id=spt
|
|
139
146
|
|
|
140
147
|
'OPENILINK_APP_TOKEN': '', # OpeniLink 的 app_token,在 OpeniLink Hub 后台安装 App 后获取 官方文档: https://openilink.com/docs/hub/apps
|
|
141
148
|
'OPENILINK_HUB_URL': '', # OpeniLink Hub 地址,默认为 https://hub.openilink.com,自建 Hub 时填写自己的地址
|
|
@@ -902,6 +909,53 @@ def wxpusher_bot(title: str, content: str) -> None:
|
|
|
902
909
|
print(f"wxpusher 推送失败!错误信息:{response.get('msg')}")
|
|
903
910
|
|
|
904
911
|
|
|
912
|
+
def wxpusher_spt(title: str, content: str) -> None:
|
|
913
|
+
"""
|
|
914
|
+
通过 wxpusher 极简推送(SPT)推送消息。
|
|
915
|
+
支持的环境变量:
|
|
916
|
+
- WXPUSHER_SPT_LIST: SPT, 多个用英文逗号,分隔, 最多10个
|
|
917
|
+
"""
|
|
918
|
+
if not push_config.get("WXPUSHER_SPT_LIST"):
|
|
919
|
+
return
|
|
920
|
+
|
|
921
|
+
# 处理 SPT,将逗号分隔的字符串转为数组
|
|
922
|
+
spts = [
|
|
923
|
+
spt.strip()
|
|
924
|
+
for spt in push_config.get("WXPUSHER_SPT_LIST").split(",")
|
|
925
|
+
if spt.strip()
|
|
926
|
+
]
|
|
927
|
+
|
|
928
|
+
if not spts:
|
|
929
|
+
print("wxpusher 服务的 WXPUSHER_SPT_LIST 不能为空!!")
|
|
930
|
+
return
|
|
931
|
+
if len(spts) > 10:
|
|
932
|
+
print("wxpusher 服务的 WXPUSHER_SPT_LIST 最多支持 10 个!!")
|
|
933
|
+
return
|
|
934
|
+
|
|
935
|
+
print("wxpusher SPT 服务启动")
|
|
936
|
+
|
|
937
|
+
url = "https://wxpusher.zjiecode.com/api/send/message/simple-push"
|
|
938
|
+
|
|
939
|
+
data = {
|
|
940
|
+
"content": f"<h1>{title}</h1><br/><div style='white-space: pre-wrap;'>{content}</div>",
|
|
941
|
+
"summary": title,
|
|
942
|
+
"contentType": 2,
|
|
943
|
+
}
|
|
944
|
+
# 单个 SPT 用 spt,多个用 sptList
|
|
945
|
+
if len(spts) == 1:
|
|
946
|
+
data["spt"] = spts[0]
|
|
947
|
+
else:
|
|
948
|
+
data["sptList"] = spts
|
|
949
|
+
|
|
950
|
+
headers = {"Content-Type": "application/json"}
|
|
951
|
+
response = requests.post(url=url, json=data, headers=headers).json()
|
|
952
|
+
|
|
953
|
+
if response.get("code") == 1000:
|
|
954
|
+
print("wxpusher SPT 推送成功!")
|
|
955
|
+
else:
|
|
956
|
+
print(f"wxpusher SPT 推送失败!错误信息:{response.get('msg')}")
|
|
957
|
+
|
|
958
|
+
|
|
905
959
|
def openilink(title: str, content: str) -> None:
|
|
906
960
|
"""
|
|
907
961
|
通过 OpeniLink 推送消息。
|
|
@@ -1104,6 +1158,8 @@ def add_notify_function():
|
|
|
1104
1158
|
push_config.get("WXPUSHER_TOPIC_IDS") or push_config.get("WXPUSHER_UIDS")
|
|
1105
1159
|
):
|
|
1106
1160
|
notify_function.append(wxpusher_bot)
|
|
1161
|
+
if push_config.get("WXPUSHER_SPT_LIST"):
|
|
1162
|
+
notify_function.append(wxpusher_spt)
|
|
1107
1163
|
if push_config.get("OPENILINK_APP_TOKEN"):
|
|
1108
1164
|
notify_function.append(openilink)
|
|
1109
1165
|
if not notify_function:
|
package/shell/api.sh
CHANGED
|
@@ -141,13 +141,19 @@ update_cron() {
|
|
|
141
141
|
local logPath="$4"
|
|
142
142
|
local lastExecutingTime="${5:-0}"
|
|
143
143
|
local runningTime="${6:-0}"
|
|
144
|
+
local exitCode="${7:-}"
|
|
144
145
|
local currentTimeStamp=$(date +%s)
|
|
146
|
+
local dataRaw="{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime"
|
|
147
|
+
if [[ -n $exitCode ]]; then
|
|
148
|
+
dataRaw="${dataRaw},\"exit_code\":$exitCode"
|
|
149
|
+
fi
|
|
150
|
+
dataRaw="${dataRaw}}"
|
|
145
151
|
local api=$(
|
|
146
152
|
curl -s --noproxy "*" "http://localhost:${ql_port}/open/crons/status?t=$currentTimeStamp" \
|
|
147
153
|
-X 'PUT' \
|
|
148
154
|
-H "Authorization: Bearer ${__ql_token__}" \
|
|
149
155
|
-H "Content-Type: application/json;charset=UTF-8" \
|
|
150
|
-
--data-raw "
|
|
156
|
+
--data-raw "$dataRaw" \
|
|
151
157
|
--compressed
|
|
152
158
|
)
|
|
153
159
|
code=$(echo "$api" | jq -r .code)
|
package/shell/share.sh
CHANGED
|
@@ -414,7 +414,7 @@ handle_task_end() {
|
|
|
414
414
|
[[ "$diff_time" == 0 ]] && diff_time=1
|
|
415
415
|
|
|
416
416
|
if [[ $ID ]]; then
|
|
417
|
-
local error=$(update_cron "\"$ID\"" "1" "$$" "$log_path" "$begin_timestamp" "$diff_time")
|
|
417
|
+
local error=$(update_cron "\"$ID\"" "1" "$$" "$log_path" "$begin_timestamp" "$diff_time" "$exit_code")
|
|
418
418
|
if [[ $error ]]; then
|
|
419
419
|
error_message=", 状态更新失败(${error})"
|
|
420
420
|
fi
|
package/static/build/api/cron.js
CHANGED
|
@@ -247,8 +247,8 @@ exports.default = (app) => {
|
|
|
247
247
|
const logger = typedi_1.Container.get('logger');
|
|
248
248
|
try {
|
|
249
249
|
const cronService = typedi_1.Container.get(cron_1.default);
|
|
250
|
-
const
|
|
251
|
-
return res.send({ code: 200, data });
|
|
250
|
+
const result = await cronService.log(req.params.id);
|
|
251
|
+
return res.send({ code: 200, data: result.content, logStatus: result.status });
|
|
252
252
|
}
|
|
253
253
|
catch (e) {
|
|
254
254
|
return next(e);
|
|
@@ -340,6 +340,7 @@ exports.default = (app) => {
|
|
|
340
340
|
log_path: celebrate_1.Joi.string().optional().allow(null),
|
|
341
341
|
last_running_time: celebrate_1.Joi.number().optional().allow(null),
|
|
342
342
|
last_execution_time: celebrate_1.Joi.number().optional().allow(null),
|
|
343
|
+
exit_code: celebrate_1.Joi.number().optional().allow(null),
|
|
343
344
|
}),
|
|
344
345
|
}), async (req, res, next) => {
|
|
345
346
|
try {
|
|
@@ -360,7 +361,6 @@ exports.default = (app) => {
|
|
|
360
361
|
const instances = await runningInstance_1.RunningInstanceModel.findAll({
|
|
361
362
|
where: {
|
|
362
363
|
cron_id: req.params.id,
|
|
363
|
-
status: runningInstance_1.InstanceStatus.running,
|
|
364
364
|
},
|
|
365
365
|
order: [['started_at', 'DESC']],
|
|
366
366
|
raw: true,
|
|
@@ -10,6 +10,8 @@ const cronStats_1 = require("../data/cronStats");
|
|
|
10
10
|
const runningInstance_1 = require("../data/runningInstance");
|
|
11
11
|
const dayjs_1 = __importDefault(require("dayjs"));
|
|
12
12
|
const os_1 = __importDefault(require("os"));
|
|
13
|
+
const lodash_1 = require("lodash");
|
|
14
|
+
const i18n_1 = require("../shared/i18n");
|
|
13
15
|
const route = (0, express_1.Router)();
|
|
14
16
|
exports.default = (app) => {
|
|
15
17
|
app.use('/dashboard', route);
|
|
@@ -153,7 +155,7 @@ exports.default = (app) => {
|
|
|
153
155
|
crons.forEach((c) => { nameMap[c.id] = c.name || c.command; });
|
|
154
156
|
const data = rows.map((r, i) => ({
|
|
155
157
|
rank: i + 1,
|
|
156
|
-
name: nameMap[Number(r.ref_id)] ||
|
|
158
|
+
name: nameMap[Number(r.ref_id)] || (0, i18n_1.tf)('任务#%s', r.ref_id),
|
|
157
159
|
avgTime: Math.round(Number(r.total_time) / Number(r.run_count)),
|
|
158
160
|
maxTime: Number(r.max_time),
|
|
159
161
|
}));
|
|
@@ -188,7 +190,7 @@ exports.default = (app) => {
|
|
|
188
190
|
crons.forEach((c) => { nameMap[c.id] = c.name || c.command; });
|
|
189
191
|
const data = rows.map((r, i) => ({
|
|
190
192
|
rank: i + 1,
|
|
191
|
-
name: nameMap[Number(r.ref_id)] ||
|
|
193
|
+
name: nameMap[Number(r.ref_id)] || (0, i18n_1.tf)('任务#%s', r.ref_id),
|
|
192
194
|
runCount: Number(r.run_count),
|
|
193
195
|
avgTime: Math.round(Number(r.total_time) / Number(r.run_count)),
|
|
194
196
|
successRate: Number(r.run_count) > 0
|
|
@@ -232,7 +234,7 @@ exports.default = (app) => {
|
|
|
232
234
|
return {
|
|
233
235
|
instanceId: inst.id,
|
|
234
236
|
id: inst.cron_id,
|
|
235
|
-
name: (cron === null || cron === void 0 ? void 0 : cron.name) || (cron === null || cron === void 0 ? void 0 : cron.command) ||
|
|
237
|
+
name: (cron === null || cron === void 0 ? void 0 : cron.name) || (cron === null || cron === void 0 ? void 0 : cron.command) || (0, i18n_1.tf)('任务#%s', inst.cron_id),
|
|
236
238
|
pid: inst.pid,
|
|
237
239
|
elapsed: inst.started_at ? now - inst.started_at : 0,
|
|
238
240
|
logPath: inst.log_path,
|
|
@@ -257,7 +259,7 @@ exports.default = (app) => {
|
|
|
257
259
|
running,
|
|
258
260
|
idleTasks: idleTasks.map((c) => ({
|
|
259
261
|
id: c.id,
|
|
260
|
-
name: c.name || c.command ||
|
|
262
|
+
name: c.name || c.command || (0, i18n_1.tf)('任务#%s', c.id),
|
|
261
263
|
lastRun: c.last_execution_time
|
|
262
264
|
? dayjs_1.default.unix(c.last_execution_time).format('MM-DD HH:mm')
|
|
263
265
|
: '-',
|
|
@@ -283,7 +285,12 @@ exports.default = (app) => {
|
|
|
283
285
|
let rawLabels = c.labels;
|
|
284
286
|
if (typeof rawLabels === 'string')
|
|
285
287
|
rawLabels = JSON.parse(rawLabels);
|
|
286
|
-
const labels = Array.isArray(rawLabels)
|
|
288
|
+
const labels = Array.isArray(rawLabels)
|
|
289
|
+
? [...new Set(rawLabels.filter((l) => !(0, lodash_1.isEmpty)(l)))]
|
|
290
|
+
: [];
|
|
291
|
+
if (labels.length === 0) {
|
|
292
|
+
labels.push((0, i18n_1.t)('未分类'));
|
|
293
|
+
}
|
|
287
294
|
const st = statMap[c.id];
|
|
288
295
|
labels.forEach((label) => {
|
|
289
296
|
if (!labelMap[label])
|
|
@@ -318,7 +325,7 @@ exports.default = (app) => {
|
|
|
318
325
|
code: 200,
|
|
319
326
|
data: {
|
|
320
327
|
platform: os_1.default.platform(),
|
|
321
|
-
uptime: Math.floor(
|
|
328
|
+
uptime: Math.floor(process.uptime()),
|
|
322
329
|
memTotal: os_1.default.totalmem(),
|
|
323
330
|
memFree: os_1.default.freemem(),
|
|
324
331
|
memUsagePercent: ((1 - os_1.default.freemem() / os_1.default.totalmem()) * 100).toFixed(1),
|
package/static/build/api/env.js
CHANGED
|
@@ -47,7 +47,7 @@ exports.default = (app) => {
|
|
|
47
47
|
.required()
|
|
48
48
|
.pattern(/^[a-zA-Z_][0-9a-zA-Z_]*$/),
|
|
49
49
|
remarks: celebrate_1.Joi.string().optional().allow(''),
|
|
50
|
-
labels: celebrate_1.Joi.array().items(celebrate_1.Joi.string().trim()).optional(),
|
|
50
|
+
labels: celebrate_1.Joi.array().items(celebrate_1.Joi.string().trim()).optional().allow(null),
|
|
51
51
|
})),
|
|
52
52
|
}), async (req, res, next) => {
|
|
53
53
|
var _a;
|
|
@@ -70,7 +70,7 @@ exports.default = (app) => {
|
|
|
70
70
|
name: celebrate_1.Joi.string().required(),
|
|
71
71
|
remarks: celebrate_1.Joi.string().optional().allow('').allow(null),
|
|
72
72
|
id: celebrate_1.Joi.number().required(),
|
|
73
|
-
labels: celebrate_1.Joi.array().items(celebrate_1.Joi.string().trim()).optional(),
|
|
73
|
+
labels: celebrate_1.Joi.array().items(celebrate_1.Joi.string().trim()).optional().allow(null),
|
|
74
74
|
}),
|
|
75
75
|
}), async (req, res, next) => {
|
|
76
76
|
const logger = typedi_1.Container.get('logger');
|
package/static/build/api/log.js
CHANGED
|
@@ -10,6 +10,7 @@ const config_1 = __importDefault(require("../config"));
|
|
|
10
10
|
const i18n_1 = require("../shared/i18n");
|
|
11
11
|
const util_1 = require("../config/util");
|
|
12
12
|
const log_1 = __importDefault(require("../services/log"));
|
|
13
|
+
const runningInstance_1 = require("../data/runningInstance");
|
|
13
14
|
const route = (0, express_1.Router)();
|
|
14
15
|
const blacklist = ['.tmp'];
|
|
15
16
|
exports.default = (app) => {
|
|
@@ -38,8 +39,16 @@ exports.default = (app) => {
|
|
|
38
39
|
message: (0, i18n_1.t)('暂无权限'),
|
|
39
40
|
});
|
|
40
41
|
}
|
|
42
|
+
const logPath = `${req.query.path}/${req.query.file}`;
|
|
43
|
+
const runningInstance = await runningInstance_1.RunningInstanceModel.findOne({
|
|
44
|
+
where: { log_path: logPath, status: runningInstance_1.InstanceStatus.running },
|
|
45
|
+
});
|
|
41
46
|
const content = await (0, util_1.getFileContentByName)(finalPath);
|
|
42
|
-
res.send({
|
|
47
|
+
res.send({
|
|
48
|
+
code: 200,
|
|
49
|
+
data: (0, util_1.removeAnsi)(content),
|
|
50
|
+
logStatus: runningInstance ? 'running' : undefined,
|
|
51
|
+
});
|
|
43
52
|
}
|
|
44
53
|
catch (e) {
|
|
45
54
|
return next(e);
|
|
@@ -10,6 +10,7 @@ const system_1 = __importDefault(require("../services/system"));
|
|
|
10
10
|
const celebrate_1 = require("celebrate");
|
|
11
11
|
const user_1 = __importDefault(require("../services/user"));
|
|
12
12
|
const i18n_1 = require("../shared/i18n");
|
|
13
|
+
const auth_1 = require("../shared/auth");
|
|
13
14
|
const util_1 = require("../config/util");
|
|
14
15
|
const dayjs_1 = __importDefault(require("dayjs"));
|
|
15
16
|
const multer_1 = __importDefault(require("multer"));
|
|
@@ -32,12 +33,7 @@ exports.default = (app) => {
|
|
|
32
33
|
const userService = typedi_1.Container.get(user_1.default);
|
|
33
34
|
const authInfo = await userService.getAuthInfo();
|
|
34
35
|
const { version, changeLog, changeLogLink, publishTime } = await (0, util_1.parseVersion)(config_1.default.versionFile);
|
|
35
|
-
|
|
36
|
-
if (Object.keys(authInfo).length === 2 &&
|
|
37
|
-
authInfo.username === 'admin' &&
|
|
38
|
-
authInfo.password === 'admin') {
|
|
39
|
-
isInitialized = false;
|
|
40
|
-
}
|
|
36
|
+
const isInitialized = !(0, auth_1.isDefaultAuthInfo)(authInfo);
|
|
41
37
|
res.send({
|
|
42
38
|
code: 200,
|
|
43
39
|
data: {
|
|
@@ -316,7 +312,10 @@ exports.default = (app) => {
|
|
|
316
312
|
}), async (req, res, next) => {
|
|
317
313
|
try {
|
|
318
314
|
const userService = typedi_1.Container.get(user_1.default);
|
|
319
|
-
await userService.resetAuthInfo(req.body);
|
|
315
|
+
const result = await userService.resetAuthInfo(req.body);
|
|
316
|
+
if (result) {
|
|
317
|
+
return res.send(result);
|
|
318
|
+
}
|
|
320
319
|
res.send({ code: 200, message: (0, i18n_1.t)('更新成功') });
|
|
321
320
|
}
|
|
322
321
|
catch (e) {
|