@whyour/qinglong 2.21.0-1 → 2.21.0-11

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.
Files changed (112) hide show
  1. package/AGENTS.md +2 -2
  2. package/back/protos/api.proto +3 -0
  3. package/deploy/kubernetes/README.md +81 -0
  4. package/deploy/kubernetes/base/kustomization.yaml +5 -0
  5. package/deploy/kubernetes/base/qinglong.yaml +95 -0
  6. package/deploy/kubernetes/overlays/example/kustomization.yaml +10 -0
  7. package/package.json +2 -4
  8. package/sample/config.sample.sh +7 -2
  9. package/sample/notify.js +64 -0
  10. package/sample/notify.py +56 -0
  11. package/shell/api.sh +7 -1
  12. package/shell/share.sh +1 -1
  13. package/static/build/api/cron.js +3 -3
  14. package/static/build/api/dashboard.js +13 -6
  15. package/static/build/api/env.js +2 -2
  16. package/static/build/api/log.js +10 -1
  17. package/static/build/api/system.js +6 -7
  18. package/static/build/app.js +22 -21
  19. package/static/build/config/const.js +1 -0
  20. package/static/build/config/grpcCerts.js +4 -4
  21. package/static/build/config/index.js +0 -4
  22. package/static/build/config/util.js +10 -4
  23. package/static/build/data/notify.js +8 -1
  24. package/static/build/data/sock.js +1 -0
  25. package/static/build/loaders/app.js +6 -6
  26. package/static/build/loaders/db.js +2 -2
  27. package/static/build/loaders/express.js +17 -14
  28. package/static/build/loaders/initData.js +8 -7
  29. package/static/build/loaders/initFile.js +1 -1
  30. package/static/build/loaders/initTask.js +3 -2
  31. package/static/build/loaders/server.js +1 -1
  32. package/static/build/protos/api.js +23 -1
  33. package/static/build/schedule/addCron.js +67 -10
  34. package/static/build/schedule/delCron.js +11 -1
  35. package/static/build/services/cron.js +103 -53
  36. package/static/build/services/dependence.js +19 -18
  37. package/static/build/services/grpc.js +1 -1
  38. package/static/build/services/http.js +1 -1
  39. package/static/build/services/notify.js +47 -5
  40. package/static/build/services/subscription.js +6 -4
  41. package/static/build/services/system.js +12 -2
  42. package/static/build/services/user.js +31 -6
  43. package/static/build/shared/auth.js +7 -1
  44. package/static/build/shared/i18n.js +59 -4
  45. package/static/build/shared/pLimit.js +3 -2
  46. package/static/build/shared/store.js +7 -0
  47. package/static/build/validation/schedule.js +9 -0
  48. package/static/dist/1147.62793604.async.js +1 -0
  49. package/static/dist/2208.85285476.async.js +1 -0
  50. package/static/dist/2286.f371c743.async.js +1 -0
  51. package/static/dist/3191.545ab873.async.js +1 -0
  52. package/static/dist/8826.db34ad8d.async.js +1 -0
  53. package/static/dist/index.html +2 -2
  54. package/static/dist/{preload_helper.0431c0f3.js → preload_helper.3048fe5b.js} +1 -1
  55. package/static/dist/src__pages__crontab__detail.b98f0084.async.js +1 -0
  56. package/static/dist/src__pages__crontab__index.68e78a33.async.js +1 -0
  57. package/static/dist/{src__pages__crontab__logModal.0b8cce8c.async.js → src__pages__crontab__logModal.831d5e4b.async.js} +1 -1
  58. package/static/dist/src__pages__dashboard__index.58177357.async.js +1 -0
  59. package/static/dist/src__pages__dependence__index.b7bbc795.async.js +1 -0
  60. package/static/dist/src__pages__dependence__modal.f7bb47ba.async.js +1 -0
  61. package/static/dist/src__pages__env__editNameModal.48715576.async.js +1 -0
  62. package/static/dist/src__pages__env__index.452cdc47.async.js +1 -0
  63. package/static/dist/src__pages__env__modal.e1234834.async.js +1 -0
  64. package/static/dist/src__pages__error__index.dedb2b90.async.js +1 -0
  65. package/static/dist/src__pages__initialization__index.22620a77.async.js +1 -0
  66. package/static/dist/src__pages__log__index.9c66639f.async.js +1 -0
  67. package/static/dist/src__pages__script__editNameModal.532a3044.async.js +1 -0
  68. package/static/dist/src__pages__script__index.1fc20f22.async.js +1 -0
  69. package/static/dist/src__pages__script__renameModal.5cdde919.async.js +1 -0
  70. package/static/dist/src__pages__script__saveModal.5ffed35f.async.js +1 -0
  71. package/static/dist/src__pages__script__setting.1a82c6d7.async.js +1 -0
  72. package/static/dist/src__pages__setting__appModal.43b7758a.async.js +1 -0
  73. package/static/dist/src__pages__setting__checkUpdate.34d773f2.async.js +1 -0
  74. package/static/dist/src__pages__setting__dependence.9c04c79a.async.js +1 -0
  75. package/static/dist/src__pages__setting__index.7e070838.async.js +1 -0
  76. package/static/dist/src__pages__setting__notification.bac481c2.async.js +1 -0
  77. package/static/dist/src__pages__setting__other.707cfafe.async.js +1 -0
  78. package/static/dist/src__pages__setting__security.b7235753.async.js +1 -0
  79. package/static/dist/src__pages__setting__systemLog.b81a8c1d.async.js +1 -0
  80. package/static/dist/src__pages__subscription__modal.562448e2.async.js +1 -0
  81. package/static/dist/{umi.ca04a019.js → umi.a54a59f6.js} +1 -1
  82. package/CLAUDE.md +0 -43
  83. package/static/dist/1147.c420132e.async.js +0 -1
  84. package/static/dist/2208.f7ba3dfa.async.js +0 -1
  85. package/static/dist/2286.164bb089.async.js +0 -1
  86. package/static/dist/3191.63263871.async.js +0 -1
  87. package/static/dist/8826.2447a104.async.js +0 -1
  88. package/static/dist/src__pages__crontab__detail.b9c36808.async.js +0 -1
  89. package/static/dist/src__pages__crontab__index.fed99fd0.async.js +0 -1
  90. package/static/dist/src__pages__dashboard__index.b30f2f47.async.js +0 -1
  91. package/static/dist/src__pages__dependence__index.3d87ceac.async.js +0 -1
  92. package/static/dist/src__pages__dependence__modal.11124896.async.js +0 -1
  93. package/static/dist/src__pages__env__editNameModal.5d264b25.async.js +0 -1
  94. package/static/dist/src__pages__env__index.baa27d4e.async.js +0 -1
  95. package/static/dist/src__pages__env__modal.7f2ef1bc.async.js +0 -1
  96. package/static/dist/src__pages__error__index.f156b45e.async.js +0 -1
  97. package/static/dist/src__pages__initialization__index.e96d4ba8.async.js +0 -1
  98. package/static/dist/src__pages__log__index.e0978bae.async.js +0 -1
  99. package/static/dist/src__pages__script__editNameModal.3e7e100d.async.js +0 -1
  100. package/static/dist/src__pages__script__index.2765d1b8.async.js +0 -1
  101. package/static/dist/src__pages__script__renameModal.5e987ef5.async.js +0 -1
  102. package/static/dist/src__pages__script__saveModal.3f9d23d6.async.js +0 -1
  103. package/static/dist/src__pages__script__setting.a535793a.async.js +0 -1
  104. package/static/dist/src__pages__setting__appModal.251cd14f.async.js +0 -1
  105. package/static/dist/src__pages__setting__checkUpdate.46efe057.async.js +0 -1
  106. package/static/dist/src__pages__setting__dependence.8a7a9529.async.js +0 -1
  107. package/static/dist/src__pages__setting__index.cc85fdfb.async.js +0 -1
  108. package/static/dist/src__pages__setting__notification.390fc905.async.js +0 -1
  109. package/static/dist/src__pages__setting__other.b22d2165.async.js +0 -1
  110. package/static/dist/src__pages__setting__security.598720a8.async.js +0 -1
  111. package/static/dist/src__pages__setting__systemLog.67406721.async.js +0 -1
  112. 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** (2740 symbols, 6583 relationships, 230 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
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 -->
@@ -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,5 @@
1
+ apiVersion: kustomize.config.k8s.io/v1beta1
2
+ kind: Kustomization
3
+
4
+ resources:
5
+ - qinglong.yaml
@@ -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
@@ -0,0 +1,10 @@
1
+ apiVersion: kustomize.config.k8s.io/v1beta1
2
+ kind: Kustomization
3
+
4
+ resources:
5
+ - ../../base
6
+
7
+ images:
8
+ - name: whyour/qinglong
9
+ newName: registry.example.com/whyour/qinglong
10
+ newTag: debian
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-1",
4
+ "version": "2.21.0-11",
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"
@@ -241,14 +241,19 @@ export NTFY_PASSWORD=""
241
241
  export NTFY_ACTIONS=""
242
242
 
243
243
  ## 21. wxPusher
244
- ## 官方文档: https://wxpusher.zjiecode.com/docs/
245
- ## 管理后台: https://wxpusher.zjiecode.com/admin/
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 "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
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
@@ -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 data = await cronService.log(req.params.id);
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)] || `任务#${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)] || `任务#${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) || `任务#${inst.cron_id}`,
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 || `任务#${c.id}`,
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) && rawLabels.length > 0 ? 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(os_1.default.uptime()),
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),
@@ -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');
@@ -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({ code: 200, data: (0, util_1.removeAnsi)(content) });
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
- let isInitialized = true;
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) {