@whyour/qinglong 2.20.2-3 → 2.21.0

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 (218) hide show
  1. package/.env.example +5 -0
  2. package/AGENTS.md +43 -0
  3. package/CLAUDE.md +43 -0
  4. package/README-en.md +11 -0
  5. package/README.md +11 -0
  6. package/docker/Dockerfile +51 -69
  7. package/docker/Dockerfile.310 +94 -0
  8. package/docker/Dockerfile.debian +120 -0
  9. package/docker/{310.Dockerfile → Dockerfile.debian310} +13 -5
  10. package/docker/docker-entrypoint.sh +95 -3
  11. package/docs/PROJECT_ARCHITECTURE.md +550 -0
  12. package/package.json +28 -25
  13. package/sample/config.sample.sh +12 -1
  14. package/sample/notify.js +81 -9
  15. package/sample/notify.py +54 -11
  16. package/shell/api.sh +44 -20
  17. package/shell/bot.sh +30 -16
  18. package/shell/check.sh +21 -19
  19. package/shell/lang/en.sh +105 -0
  20. package/shell/lang/zh.sh +105 -0
  21. package/shell/otask.sh +114 -28
  22. package/shell/preload/client.js +20 -2
  23. package/shell/preload/esm-loader.mjs +41 -0
  24. package/shell/preload/sitecustomize.js +36 -0
  25. package/shell/pub.sh +8 -8
  26. package/shell/rmlog.sh +5 -5
  27. package/shell/share.sh +49 -30
  28. package/shell/start.sh +40 -27
  29. package/shell/task.sh +10 -2
  30. package/shell/update.sh +22 -22
  31. package/static/build/api/config.js +17 -7
  32. package/static/build/api/cron.js +38 -1
  33. package/static/build/api/dashboard.js +337 -0
  34. package/static/build/api/env.js +40 -2
  35. package/static/build/api/index.js +2 -0
  36. package/static/build/api/log.js +5 -4
  37. package/static/build/api/script.js +34 -9
  38. package/static/build/api/system.js +16 -1
  39. package/static/build/api/user.js +6 -5
  40. package/static/build/app.js +3 -0
  41. package/static/build/config/const.js +7 -6
  42. package/static/build/config/container.js +12 -0
  43. package/static/build/config/grpcCerts.js +150 -0
  44. package/static/build/config/index.js +6 -0
  45. package/static/build/config/util.js +66 -15
  46. package/static/build/data/cron.js +3 -1
  47. package/static/build/data/cronStats.js +59 -0
  48. package/static/build/data/env.js +2 -0
  49. package/static/build/data/notify.js +11 -1
  50. package/static/build/data/runningInstance.js +57 -0
  51. package/static/build/loaders/app.js +1 -1
  52. package/static/build/loaders/db.js +6 -0
  53. package/static/build/loaders/express.js +2 -1
  54. package/static/build/loaders/initData.js +19 -2
  55. package/static/build/loaders/initFile.js +2 -0
  56. package/static/build/schedule/api.js +1 -1
  57. package/static/build/schedule/client.js +9 -1
  58. package/static/build/schedule/health.js +1 -1
  59. package/static/build/services/config.js +19 -5
  60. package/static/build/services/cron.js +118 -8
  61. package/static/build/services/env.js +31 -3
  62. package/static/build/services/grpc.js +32 -6
  63. package/static/build/services/http.js +33 -18
  64. package/static/build/services/notify.js +40 -3
  65. package/static/build/services/open.js +2 -1
  66. package/static/build/services/sshKey.js +6 -3
  67. package/static/build/services/subscription.js +2 -1
  68. package/static/build/services/system.js +22 -7
  69. package/static/build/services/user.js +8 -7
  70. package/static/build/shared/i18n.js +105 -0
  71. package/static/build/shared/pLimit.js +12 -1
  72. package/static/build/shared/runCron.js +5 -0
  73. package/static/build/validation/schedule.js +1 -0
  74. package/static/dist/1089.0ecc5383.async.js +1 -0
  75. package/static/dist/1147.c420132e.async.js +1 -0
  76. package/static/dist/1192.4e5740f2.async.js +1 -0
  77. package/static/dist/1214.c6469b53.async.js +1 -0
  78. package/static/dist/134.54df9e38.async.js +1 -0
  79. package/static/dist/1352.96a77e1d.async.js +1 -0
  80. package/static/dist/{1765.d8e002d7.async.js → 1765.5ac01a93.async.js} +1 -1
  81. package/static/dist/1836.f0b74cf1.async.js +1 -0
  82. package/static/dist/{1885.e0d00d2d.async.js → 1885.e1ea09f6.async.js} +1 -1
  83. package/static/dist/1967.3f3945d0.async.js +1 -0
  84. package/static/dist/{2096.383c1047.async.js → 2096.6d98fd12.async.js} +1 -1
  85. package/static/dist/2208.f7ba3dfa.async.js +1 -0
  86. package/static/dist/2286.164bb089.async.js +1 -0
  87. package/static/dist/2476.4e9b0992.async.js +1 -0
  88. package/static/dist/2537.2b262ee0.async.js +1 -0
  89. package/static/dist/{255.12f03ab2.async.js → 255.8a80b983.async.js} +1 -1
  90. package/static/dist/2635.d4c59d23.async.js +1 -0
  91. package/static/dist/2808.cdc0995c.async.js +1 -0
  92. package/static/dist/{2821.be3dc88e.async.js → 2821.651f31e5.async.js} +1 -1
  93. package/static/dist/2986.4c49eef7.async.js +1 -0
  94. package/static/dist/300.08ac9875.async.js +1 -0
  95. package/static/dist/{3191.70bc19db.async.js → 3191.63263871.async.js} +1 -1
  96. package/static/dist/3714.8d6dba9e.async.js +1 -0
  97. package/static/dist/3906.a5eee612.async.js +1 -0
  98. package/static/dist/{6541.a6d499de.async.js → 3948.4fe809fd.async.js} +1 -1
  99. package/static/dist/{5171.7fc6d0a2.async.js → 4573.16f19278.async.js} +1 -1
  100. package/static/dist/{4859.93e63ea6.async.js → 4859.6592cebb.async.js} +1 -1
  101. package/static/dist/4887.8e3ae573.async.js +1 -0
  102. package/static/dist/{4902.54ecbdb5.async.js → 4902.555f92ab.async.js} +1 -1
  103. package/static/dist/{4934.1ca6b6b0.async.js → 4934.f3539d08.async.js} +1 -1
  104. package/static/dist/5077.cbd111cd.async.js +1 -0
  105. package/static/dist/{6247.b550d996.async.js → 5157.7c5af144.async.js} +1 -1
  106. package/static/dist/{540.481d4708.async.js → 540.19ddf020.async.js} +1 -1
  107. package/static/dist/{5970.10ac4f16.async.js → 5970.d3a6e7bd.async.js} +1 -1
  108. package/static/dist/{6013.2d7bb12a.async.js → 6013.4e38de36.async.js} +1 -1
  109. package/static/dist/{6016.9c379049.async.js → 6016.1e6574b6.async.js} +1 -1
  110. package/static/dist/{6035.5889ddc7.async.js → 6035.32ebfad9.async.js} +1 -1
  111. package/static/dist/6339.ab305e96.async.js +1 -0
  112. package/static/dist/6569.55177f6a.async.js +1 -0
  113. package/static/dist/6610.c111af7e.async.js +1 -0
  114. package/static/dist/{3034.6413c38e.async.js → 6638.87a163d2.async.js} +1 -1
  115. package/static/dist/{6646.5fc37228.async.js → 6646.95e92533.async.js} +1 -1
  116. package/static/dist/6665.1d099ad1.async.js +1 -0
  117. package/static/dist/{7355.b7c0562f.async.js → 7355.f9f263d8.async.js} +1 -1
  118. package/static/dist/{7384.065ccae2.async.js → 7384.1bb449d5.async.js} +1 -1
  119. package/static/dist/7441.ee1bf2bb.async.js +1 -0
  120. package/static/dist/{7508.a31662a3.async.js → 7508.7dcee91c.async.js} +1 -1
  121. package/static/dist/{7802.6b73f16a.async.js → 7802.34255805.async.js} +1 -1
  122. package/static/dist/{7984.e6bb9378.async.js → 7984.594296a5.async.js} +1 -1
  123. package/static/dist/{5653.4fce7ce8.async.js → 8033.5cb31493.async.js} +1 -1
  124. package/static/dist/8147.2fb55202.async.js +1 -0
  125. package/static/dist/8187.48c130d6.async.js +1 -0
  126. package/static/dist/8495.d53e15ca.async.js +1 -0
  127. package/static/dist/8587.315b06c0.async.js +1 -0
  128. package/static/dist/8826.2447a104.async.js +1 -0
  129. package/static/dist/{2742.4852aac8.async.js → 8865.ed665d31.async.js} +1 -1
  130. package/static/dist/901.7ee5c6d3.async.js +1 -0
  131. package/static/dist/9271.231db2ce.async.js +1 -0
  132. package/static/dist/9323.a33f47da.async.js +1 -0
  133. package/static/dist/{9730.30083c91.async.js → 9730.801665a3.async.js} +1 -1
  134. package/static/dist/{9761.627ca3b5.async.js → 9761.360a19d8.async.js} +1 -1
  135. package/static/dist/index.html +2 -2
  136. package/static/dist/layouts__index.a7a2bfe0.async.js +1 -0
  137. package/static/dist/layouts__index.adf0692f.chunk.css +1 -0
  138. package/static/dist/preload_helper.0431c0f3.js +1 -0
  139. package/static/dist/{src__pages__config__index.622b6ee8.async.js → src__pages__config__index.a22ff7dc.async.js} +1 -1
  140. package/static/dist/{src__pages__crontab__const.323d5124.async.js → src__pages__crontab__const.aba07deb.async.js} +1 -1
  141. package/static/dist/src__pages__crontab__detail.b9c36808.async.js +1 -0
  142. package/static/dist/src__pages__crontab__index.fed99fd0.async.js +1 -0
  143. package/static/dist/{src__pages__crontab__logModal.5e6a4bf2.async.js → src__pages__crontab__logModal.0b8cce8c.async.js} +1 -1
  144. package/static/dist/src__pages__crontab__modal.f7041c7c.async.js +1 -0
  145. package/static/dist/src__pages__crontab__type.d7af36e5.async.js +1 -0
  146. package/static/dist/{src__pages__crontab__viewCreateModal.ffcf7a24.async.js → src__pages__crontab__viewCreateModal.4d589f66.async.js} +1 -1
  147. package/static/dist/{src__pages__crontab__viewManageModal.c2724575.async.js → src__pages__crontab__viewManageModal.0e317746.async.js} +1 -1
  148. package/static/dist/src__pages__dashboard__index.b30f2f47.async.js +1 -0
  149. package/static/dist/{src__pages__dependence__logModal.f123e2ac.async.js → src__pages__dependence__logModal.0681830b.async.js} +1 -1
  150. package/static/dist/src__pages__dependence__modal.11124896.async.js +1 -0
  151. package/static/dist/src__pages__env__editNameModal.5d264b25.async.js +1 -0
  152. package/static/dist/src__pages__env__index.baa27d4e.async.js +1 -0
  153. package/static/dist/src__pages__env__modal.7f2ef1bc.async.js +1 -0
  154. package/static/dist/src__pages__error__index.f156b45e.async.js +1 -0
  155. package/static/dist/src__pages__initialization__index.e96d4ba8.async.js +1 -0
  156. package/static/dist/{src__pages__log__index.cf00c9af.async.js → src__pages__log__index.e0978bae.async.js} +1 -1
  157. package/static/dist/{src__pages__login__index.cd6e3152.async.js → src__pages__login__index.8c813eb1.async.js} +1 -1
  158. package/static/dist/{src__pages__script__components__UnsupportedFilePreview__index.39074c68.async.js → src__pages__script__components__UnsupportedFilePreview__index.c347a13a.async.js} +1 -1
  159. package/static/dist/src__pages__script__editNameModal.3e7e100d.async.js +1 -0
  160. package/static/dist/src__pages__script__index.2765d1b8.async.js +1 -0
  161. package/static/dist/src__pages__script__renameModal.5e987ef5.async.js +1 -0
  162. package/static/dist/src__pages__script__saveModal.3f9d23d6.async.js +1 -0
  163. package/static/dist/src__pages__script__setting.a535793a.async.js +1 -0
  164. package/static/dist/src__pages__setting__appModal.251cd14f.async.js +1 -0
  165. package/static/dist/src__pages__setting__dependence.8a7a9529.async.js +1 -0
  166. package/static/dist/src__pages__setting__index.cc85fdfb.async.js +1 -0
  167. package/static/dist/src__pages__setting__notification.390fc905.async.js +1 -0
  168. package/static/dist/src__pages__setting__other.b22d2165.async.js +1 -0
  169. package/static/dist/src__pages__setting__security.598720a8.async.js +1 -0
  170. package/static/dist/src__pages__setting__systemLog.67406721.async.js +1 -0
  171. package/static/dist/{src__pages__subscription__logModal.0caa7283.async.js → src__pages__subscription__logModal.3864b37f.async.js} +1 -1
  172. package/static/dist/src__pages__subscription__modal.3562c670.async.js +1 -0
  173. package/static/dist/{umi.ef8199a4.js → umi.ca04a019.js} +1 -1
  174. package/version.yaml +33 -4
  175. package/sample/notify.py.save +0 -1010
  176. package/static/dist/105.85a5c47a.async.js +0 -1
  177. package/static/dist/1083.f86ce804.async.js +0 -1
  178. package/static/dist/1147.32f41a88.async.js +0 -1
  179. package/static/dist/1352.ab6da08e.async.js +0 -1
  180. package/static/dist/1690.f0290540.async.js +0 -1
  181. package/static/dist/1742.6cbe5aca.async.js +0 -1
  182. package/static/dist/2208.8e3a7325.async.js +0 -1
  183. package/static/dist/5312.74b95311.async.js +0 -1
  184. package/static/dist/5691.931f59c5.async.js +0 -1
  185. package/static/dist/6159.55cb068a.async.js +0 -1
  186. package/static/dist/7025.f4080d63.async.js +0 -1
  187. package/static/dist/739.6be5552a.async.js +0 -1
  188. package/static/dist/7571.4f6240b1.async.js +0 -1
  189. package/static/dist/786.59fc381c.async.js +0 -1
  190. package/static/dist/8317.c44c1ebd.async.js +0 -1
  191. package/static/dist/8826.0291edfd.async.js +0 -1
  192. package/static/dist/955.3c9481f7.async.js +0 -1
  193. package/static/dist/layouts__index.1fce90e0.chunk.css +0 -1
  194. package/static/dist/layouts__index.8dcf1576.async.js +0 -1
  195. package/static/dist/preload_helper.116a62f6.js +0 -1
  196. package/static/dist/src__pages__crontab__detail.b07f0c0a.async.js +0 -1
  197. package/static/dist/src__pages__crontab__index.2e2e1096.async.js +0 -1
  198. package/static/dist/src__pages__crontab__modal.4d8c2a22.async.js +0 -1
  199. package/static/dist/src__pages__crontab__type.db7c1858.async.js +0 -1
  200. package/static/dist/src__pages__dependence__modal.631ffb5b.async.js +0 -1
  201. package/static/dist/src__pages__env__editNameModal.ff85ef8c.async.js +0 -1
  202. package/static/dist/src__pages__env__index.a0a2fece.async.js +0 -1
  203. package/static/dist/src__pages__env__modal.d1004662.async.js +0 -1
  204. package/static/dist/src__pages__error__index.1bc3c90b.async.js +0 -1
  205. package/static/dist/src__pages__initialization__index.8b1cbaf9.async.js +0 -1
  206. package/static/dist/src__pages__script__editNameModal.53424d49.async.js +0 -1
  207. package/static/dist/src__pages__script__index.e65df827.async.js +0 -1
  208. package/static/dist/src__pages__script__renameModal.4bbe7fb1.async.js +0 -1
  209. package/static/dist/src__pages__script__saveModal.cf449f3c.async.js +0 -1
  210. package/static/dist/src__pages__script__setting.b345d59a.async.js +0 -1
  211. package/static/dist/src__pages__setting__appModal.03faec89.async.js +0 -1
  212. package/static/dist/src__pages__setting__dependence.4495c7b6.async.js +0 -1
  213. package/static/dist/src__pages__setting__index.6919c399.async.js +0 -1
  214. package/static/dist/src__pages__setting__notification.d6a3884f.async.js +0 -1
  215. package/static/dist/src__pages__setting__other.0d931d6f.async.js +0 -1
  216. package/static/dist/src__pages__setting__security.91cb545f.async.js +0 -1
  217. package/static/dist/src__pages__setting__systemLog.1d433a48.async.js +0 -1
  218. package/static/dist/src__pages__subscription__modal.0b84f6cf.async.js +0 -1
@@ -9,6 +9,8 @@ const cron_1 = __importDefault(require("../services/cron"));
9
9
  const cronView_1 = __importDefault(require("../services/cronView"));
10
10
  const celebrate_1 = require("celebrate");
11
11
  const schedule_1 = require("../validation/schedule");
12
+ const runningInstance_1 = require("../data/runningInstance");
13
+ const i18n_1 = require("../shared/i18n");
12
14
  const route = (0, express_1.Router)();
13
15
  exports.default = (app) => {
14
16
  app.use('/crons', route);
@@ -51,7 +53,7 @@ exports.default = (app) => {
51
53
  try {
52
54
  const cronViewService = typedi_1.Container.get(cronView_1.default);
53
55
  if (req.body.type === 1) {
54
- return res.send({ code: 400, message: '参数错误' });
56
+ return res.send({ code: 400, message: (0, i18n_1.t)('参数错误') });
55
57
  }
56
58
  else {
57
59
  const data = await cronViewService.update(req.body);
@@ -349,6 +351,41 @@ exports.default = (app) => {
349
351
  return next(e);
350
352
  }
351
353
  });
354
+ route.get('/:id/instances', (0, celebrate_1.celebrate)({
355
+ params: celebrate_1.Joi.object({
356
+ id: celebrate_1.Joi.number().required(),
357
+ }),
358
+ }), async (req, res, next) => {
359
+ try {
360
+ const instances = await runningInstance_1.RunningInstanceModel.findAll({
361
+ where: {
362
+ cron_id: req.params.id,
363
+ status: runningInstance_1.InstanceStatus.running,
364
+ },
365
+ order: [['started_at', 'DESC']],
366
+ raw: true,
367
+ });
368
+ return res.send({ code: 200, data: instances });
369
+ }
370
+ catch (e) {
371
+ return next(e);
372
+ }
373
+ });
374
+ route.post('/:id/instances/:instanceId/stop', (0, celebrate_1.celebrate)({
375
+ params: celebrate_1.Joi.object({
376
+ id: celebrate_1.Joi.number().required(),
377
+ instanceId: celebrate_1.Joi.number().required(),
378
+ }),
379
+ }), async (req, res, next) => {
380
+ try {
381
+ const cronService = typedi_1.Container.get(cron_1.default);
382
+ const data = await cronService.stopInstance(req.params.instanceId);
383
+ return res.send(data);
384
+ }
385
+ catch (e) {
386
+ return next(e);
387
+ }
388
+ });
352
389
  route.get('/:id/logs', (0, celebrate_1.celebrate)({
353
390
  params: celebrate_1.Joi.object({
354
391
  id: celebrate_1.Joi.number().required(),
@@ -0,0 +1,337 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const express_1 = require("express");
7
+ const sequelize_1 = require("sequelize");
8
+ const cron_1 = require("../data/cron");
9
+ const cronStats_1 = require("../data/cronStats");
10
+ const runningInstance_1 = require("../data/runningInstance");
11
+ const dayjs_1 = __importDefault(require("dayjs"));
12
+ const os_1 = __importDefault(require("os"));
13
+ const route = (0, express_1.Router)();
14
+ exports.default = (app) => {
15
+ app.use('/dashboard', route);
16
+ route.post('/record', async (req, res) => {
17
+ try {
18
+ const { ref_id, code, elapsed } = req.body;
19
+ if (!ref_id)
20
+ return res.send({ code: 400, message: 'ref_id required' });
21
+ const today = (0, dayjs_1.default)().format('YYYY-MM-DD');
22
+ const isSuccess = code === 0 ? 1 : 0;
23
+ const isFail = code !== 0 ? 1 : 0;
24
+ const elapsedMs = (Number(elapsed) || 0) * 1000;
25
+ const existing = await cronStats_1.CrontabStatModel.findOne({
26
+ where: { ref_id: Number(ref_id), date: today },
27
+ });
28
+ if (existing) {
29
+ await cronStats_1.CrontabStatModel.update({
30
+ run_count: (existing.run_count || 0) + 1,
31
+ success_count: (existing.success_count || 0) + isSuccess,
32
+ fail_count: (existing.fail_count || 0) + isFail,
33
+ total_time: (existing.total_time || 0) + elapsedMs,
34
+ max_time: Math.max(existing.max_time || 0, elapsedMs),
35
+ }, { where: { id: existing.id } });
36
+ }
37
+ else {
38
+ await cronStats_1.CrontabStatModel.create({
39
+ ref_id: Number(ref_id),
40
+ date: today,
41
+ run_count: 1,
42
+ success_count: isSuccess,
43
+ fail_count: isFail,
44
+ total_time: elapsedMs,
45
+ max_time: elapsedMs,
46
+ });
47
+ }
48
+ res.send({ code: 200 });
49
+ }
50
+ catch (e) {
51
+ res.send({ code: 500 });
52
+ }
53
+ });
54
+ route.get('/overview', async (req, res, next) => {
55
+ try {
56
+ const today = (0, dayjs_1.default)().format('YYYY-MM-DD');
57
+ const [total, enabled, disabled, stats] = await Promise.all([
58
+ cron_1.CrontabModel.count(),
59
+ cron_1.CrontabModel.count({ where: { isDisabled: 0 } }),
60
+ cron_1.CrontabModel.count({ where: { isDisabled: 1 } }),
61
+ cronStats_1.CrontabStatModel.findOne({
62
+ attributes: [
63
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('run_count')), 'total_runs'],
64
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('success_count')), 'total_success'],
65
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('fail_count')), 'total_fail'],
66
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('total_time')), 'total_time'],
67
+ ],
68
+ where: { date: today },
69
+ raw: true,
70
+ }),
71
+ ]);
72
+ const row = stats;
73
+ const totalRuns = Number(row === null || row === void 0 ? void 0 : row.total_runs) || 0;
74
+ const totalSuccess = Number(row === null || row === void 0 ? void 0 : row.total_success) || 0;
75
+ const totalFail = Number(row === null || row === void 0 ? void 0 : row.total_fail) || 0;
76
+ const totalTime = Number(row === null || row === void 0 ? void 0 : row.total_time) || 0;
77
+ res.send({
78
+ code: 200,
79
+ data: {
80
+ total,
81
+ enabled,
82
+ disabled,
83
+ todayRuns: totalRuns,
84
+ todaySuccess: totalSuccess,
85
+ todayFail: totalFail,
86
+ successRate: totalRuns > 0 ? ((totalSuccess / totalRuns) * 100).toFixed(1) : '0',
87
+ avgTime: totalRuns > 0 ? Math.round(totalTime / totalRuns) : 0,
88
+ },
89
+ });
90
+ }
91
+ catch (e) {
92
+ next(e);
93
+ }
94
+ });
95
+ route.get('/trend', async (req, res, next) => {
96
+ try {
97
+ const days = parseInt(req.query.days) || 7;
98
+ const dates = [];
99
+ for (let i = days - 1; i >= 0; i--) {
100
+ dates.push((0, dayjs_1.default)().subtract(i, 'day').format('YYYY-MM-DD'));
101
+ }
102
+ const rows = (await cronStats_1.CrontabStatModel.findAll({
103
+ attributes: [
104
+ 'date',
105
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('run_count')), 'total_runs'],
106
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('success_count')), 'total_success'],
107
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('fail_count')), 'total_fail'],
108
+ ],
109
+ where: {
110
+ date: { [sequelize_1.Op.in]: dates },
111
+ },
112
+ group: ['date'],
113
+ order: [['date', 'ASC']],
114
+ raw: true,
115
+ }));
116
+ const dataMap = {};
117
+ rows.forEach((r) => {
118
+ dataMap[r.date] = {
119
+ total: Number(r.total_runs) || 0,
120
+ success: Number(r.total_success) || 0,
121
+ fail: Number(r.total_fail) || 0,
122
+ };
123
+ });
124
+ const data = dates.map((d) => (Object.assign({ date: (0, dayjs_1.default)(d).format('MM-DD') }, (dataMap[d] || { total: 0, success: 0, fail: 0 }))));
125
+ res.send({ code: 200, data });
126
+ }
127
+ catch (e) {
128
+ next(e);
129
+ }
130
+ });
131
+ route.get('/top-time', async (req, res, next) => {
132
+ try {
133
+ const today = (0, dayjs_1.default)().format('YYYY-MM-DD');
134
+ const rows = (await cronStats_1.CrontabStatModel.findAll({
135
+ attributes: [
136
+ 'ref_id',
137
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('total_time')), 'total_time'],
138
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('run_count')), 'run_count'],
139
+ [(0, sequelize_1.fn)('MAX', (0, sequelize_1.col)('max_time')), 'max_time'],
140
+ ],
141
+ where: { date: today, run_count: { [sequelize_1.Op.gt]: 0 } },
142
+ group: ['ref_id'],
143
+ order: [[(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('total_time')), 'DESC']],
144
+ limit: 5,
145
+ raw: true,
146
+ }));
147
+ const ids = rows.map((r) => Number(r.ref_id));
148
+ const crons = await cron_1.CrontabModel.findAll({
149
+ where: { id: { [sequelize_1.Op.in]: ids } },
150
+ raw: true,
151
+ });
152
+ const nameMap = {};
153
+ crons.forEach((c) => { nameMap[c.id] = c.name || c.command; });
154
+ const data = rows.map((r, i) => ({
155
+ rank: i + 1,
156
+ name: nameMap[Number(r.ref_id)] || `任务#${r.ref_id}`,
157
+ avgTime: Math.round(Number(r.total_time) / Number(r.run_count)),
158
+ maxTime: Number(r.max_time),
159
+ }));
160
+ res.send({ code: 200, data });
161
+ }
162
+ catch (e) {
163
+ next(e);
164
+ }
165
+ });
166
+ route.get('/top-count', async (req, res, next) => {
167
+ try {
168
+ const today = (0, dayjs_1.default)().format('YYYY-MM-DD');
169
+ const rows = (await cronStats_1.CrontabStatModel.findAll({
170
+ attributes: [
171
+ 'ref_id',
172
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('run_count')), 'run_count'],
173
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('total_time')), 'total_time'],
174
+ [(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('success_count')), 'success_count'],
175
+ ],
176
+ where: { date: today, run_count: { [sequelize_1.Op.gt]: 0 } },
177
+ group: ['ref_id'],
178
+ order: [[(0, sequelize_1.fn)('SUM', (0, sequelize_1.col)('run_count')), 'DESC']],
179
+ limit: 5,
180
+ raw: true,
181
+ }));
182
+ const ids = rows.map((r) => Number(r.ref_id));
183
+ const crons = await cron_1.CrontabModel.findAll({
184
+ where: { id: { [sequelize_1.Op.in]: ids } },
185
+ raw: true,
186
+ });
187
+ const nameMap = {};
188
+ crons.forEach((c) => { nameMap[c.id] = c.name || c.command; });
189
+ const data = rows.map((r, i) => ({
190
+ rank: i + 1,
191
+ name: nameMap[Number(r.ref_id)] || `任务#${r.ref_id}`,
192
+ runCount: Number(r.run_count),
193
+ avgTime: Math.round(Number(r.total_time) / Number(r.run_count)),
194
+ successRate: Number(r.run_count) > 0
195
+ ? ((Number(r.success_count) / Number(r.run_count)) * 100).toFixed(1)
196
+ : '0',
197
+ }));
198
+ res.send({ code: 200, data });
199
+ }
200
+ catch (e) {
201
+ next(e);
202
+ }
203
+ });
204
+ route.get('/runtime', async (req, res, next) => {
205
+ try {
206
+ const runningInstances = await runningInstance_1.RunningInstanceModel.findAll({
207
+ where: {
208
+ status: runningInstance_1.InstanceStatus.running,
209
+ },
210
+ raw: true,
211
+ });
212
+ const queuedCrons = await cron_1.CrontabModel.findAll({
213
+ where: {
214
+ status: 3, // queued
215
+ },
216
+ raw: true,
217
+ });
218
+ // Fetch cron names for running instances
219
+ const cronIds = [
220
+ ...new Set(runningInstances.map((i) => i.cron_id)),
221
+ ];
222
+ const crons = cronIds.length > 0
223
+ ? await cron_1.CrontabModel.findAll({
224
+ where: { id: cronIds },
225
+ raw: true,
226
+ })
227
+ : [];
228
+ const cronMap = new Map(crons.map((c) => [c.id, c]));
229
+ const now = (0, dayjs_1.default)().unix();
230
+ const running = runningInstances.map((inst) => {
231
+ const cron = cronMap.get(inst.cron_id);
232
+ return {
233
+ instanceId: inst.id,
234
+ 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}`,
236
+ pid: inst.pid,
237
+ elapsed: inst.started_at ? now - inst.started_at : 0,
238
+ logPath: inst.log_path,
239
+ };
240
+ });
241
+ const dayAgo = (0, dayjs_1.default)().subtract(24, 'hour').unix();
242
+ const idleTasks = await cron_1.CrontabModel.findAll({
243
+ where: {
244
+ isDisabled: 0,
245
+ status: 1,
246
+ last_execution_time: { [sequelize_1.Op.lt]: dayAgo },
247
+ },
248
+ order: [['last_execution_time', 'ASC']],
249
+ limit: 5,
250
+ raw: true,
251
+ });
252
+ res.send({
253
+ code: 200,
254
+ data: {
255
+ runningCount: running.length,
256
+ queuedCount: queuedCrons.length,
257
+ running,
258
+ idleTasks: idleTasks.map((c) => ({
259
+ id: c.id,
260
+ name: c.name || c.command || `任务#${c.id}`,
261
+ lastRun: c.last_execution_time
262
+ ? dayjs_1.default.unix(c.last_execution_time).format('MM-DD HH:mm')
263
+ : '-',
264
+ })),
265
+ },
266
+ });
267
+ }
268
+ catch (e) {
269
+ next(e);
270
+ }
271
+ });
272
+ route.get('/labels', async (req, res, next) => {
273
+ try {
274
+ const today = (0, dayjs_1.default)().format('YYYY-MM-DD');
275
+ const [crons, stats] = (await Promise.all([
276
+ cron_1.CrontabModel.findAll({ where: { isDisabled: 0 }, raw: true }),
277
+ cronStats_1.CrontabStatModel.findAll({ where: { date: today }, raw: true }),
278
+ ]));
279
+ const statMap = {};
280
+ stats.forEach((s) => { statMap[s.ref_id] = s; });
281
+ const labelMap = {};
282
+ crons.forEach((c) => {
283
+ let rawLabels = c.labels;
284
+ if (typeof rawLabels === 'string')
285
+ rawLabels = JSON.parse(rawLabels);
286
+ const labels = Array.isArray(rawLabels) && rawLabels.length > 0 ? rawLabels : ['未分类'];
287
+ const st = statMap[c.id];
288
+ labels.forEach((label) => {
289
+ if (!labelMap[label])
290
+ labelMap[label] = { count: 0, runs: 0, success: 0, totalTime: 0 };
291
+ labelMap[label].count += 1;
292
+ if (st) {
293
+ labelMap[label].runs += Number(st.run_count) || 0;
294
+ labelMap[label].success += Number(st.success_count) || 0;
295
+ labelMap[label].totalTime += Number(st.total_time) || 0;
296
+ }
297
+ });
298
+ });
299
+ const data = Object.entries(labelMap)
300
+ .map(([label, v]) => ({
301
+ label,
302
+ count: v.count,
303
+ todayRuns: v.runs,
304
+ successRate: v.runs > 0 ? ((v.success / v.runs) * 100).toFixed(1) : '0',
305
+ avgTime: v.runs > 0 ? Math.round(v.totalTime / v.runs) : 0,
306
+ }))
307
+ .sort((a, b) => b.todayRuns - a.todayRuns);
308
+ res.send({ code: 200, data });
309
+ }
310
+ catch (e) {
311
+ next(e);
312
+ }
313
+ });
314
+ route.get('/system', async (req, res, next) => {
315
+ try {
316
+ const memUsage = process.memoryUsage();
317
+ res.send({
318
+ code: 200,
319
+ data: {
320
+ platform: os_1.default.platform(),
321
+ uptime: Math.floor(os_1.default.uptime()),
322
+ memTotal: os_1.default.totalmem(),
323
+ memFree: os_1.default.freemem(),
324
+ memUsagePercent: ((1 - os_1.default.freemem() / os_1.default.totalmem()) * 100).toFixed(1),
325
+ heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024),
326
+ heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024),
327
+ loadAvg: os_1.default.loadavg().map((v) => Number(v.toFixed(2))),
328
+ cpus: os_1.default.cpus().length,
329
+ },
330
+ });
331
+ }
332
+ catch (e) {
333
+ next(e);
334
+ }
335
+ });
336
+ };
337
+ //# sourceMappingURL=dashboard.js.map
@@ -10,6 +10,7 @@ const multer_1 = __importDefault(require("multer"));
10
10
  const typedi_1 = require("typedi");
11
11
  const config_1 = __importDefault(require("../config"));
12
12
  const util_1 = require("../config/util");
13
+ const i18n_1 = require("../shared/i18n");
13
14
  const env_1 = __importDefault(require("../services/env"));
14
15
  const route = (0, express_1.Router)();
15
16
  const storage = multer_1.default.diskStorage({
@@ -21,6 +22,10 @@ const storage = multer_1.default.diskStorage({
21
22
  },
22
23
  });
23
24
  const upload = (0, multer_1.default)({ storage: storage });
25
+ const labelSchema = celebrate_1.Joi.array()
26
+ .items(celebrate_1.Joi.string().trim().required())
27
+ .min(1)
28
+ .required();
24
29
  exports.default = (app) => {
25
30
  app.use('/envs', route);
26
31
  route.get('/', async (req, res, next) => {
@@ -42,6 +47,7 @@ exports.default = (app) => {
42
47
  .required()
43
48
  .pattern(/^[a-zA-Z_][0-9a-zA-Z_]*$/),
44
49
  remarks: celebrate_1.Joi.string().optional().allow(''),
50
+ labels: celebrate_1.Joi.array().items(celebrate_1.Joi.string().trim()).optional(),
45
51
  })),
46
52
  }), async (req, res, next) => {
47
53
  var _a;
@@ -49,7 +55,7 @@ exports.default = (app) => {
49
55
  try {
50
56
  const envService = typedi_1.Container.get(env_1.default);
51
57
  if (!((_a = req.body) === null || _a === void 0 ? void 0 : _a.length)) {
52
- return res.send({ code: 400, message: '参数不正确' });
58
+ return res.send({ code: 400, message: (0, i18n_1.t)('参数不正确') });
53
59
  }
54
60
  const data = await envService.create(req.body);
55
61
  return res.send({ code: 200, data });
@@ -64,6 +70,7 @@ exports.default = (app) => {
64
70
  name: celebrate_1.Joi.string().required(),
65
71
  remarks: celebrate_1.Joi.string().optional().allow('').allow(null),
66
72
  id: celebrate_1.Joi.number().required(),
73
+ labels: celebrate_1.Joi.array().items(celebrate_1.Joi.string().trim()).optional(),
67
74
  }),
68
75
  }), async (req, res, next) => {
69
76
  const logger = typedi_1.Container.get('logger');
@@ -190,6 +197,36 @@ exports.default = (app) => {
190
197
  return next(e);
191
198
  }
192
199
  });
200
+ route.post('/labels', (0, celebrate_1.celebrate)({
201
+ body: celebrate_1.Joi.object({
202
+ ids: celebrate_1.Joi.array().items(celebrate_1.Joi.number().required()).min(1).required(),
203
+ labels: labelSchema,
204
+ }),
205
+ }), async (req, res, next) => {
206
+ try {
207
+ const envService = typedi_1.Container.get(env_1.default);
208
+ const data = await envService.addLabels(req.body.ids, req.body.labels);
209
+ return res.send({ code: 200, data });
210
+ }
211
+ catch (e) {
212
+ return next(e);
213
+ }
214
+ });
215
+ route.delete('/labels', (0, celebrate_1.celebrate)({
216
+ body: celebrate_1.Joi.object({
217
+ ids: celebrate_1.Joi.array().items(celebrate_1.Joi.number().required()).min(1).required(),
218
+ labels: labelSchema,
219
+ }),
220
+ }), async (req, res, next) => {
221
+ try {
222
+ const envService = typedi_1.Container.get(env_1.default);
223
+ const data = await envService.removeLabels(req.body.ids, req.body.labels);
224
+ return res.send({ code: 200, data });
225
+ }
226
+ catch (e) {
227
+ return next(e);
228
+ }
229
+ });
193
230
  route.post('/upload', upload.single('env'), async (req, res, next) => {
194
231
  const logger = typedi_1.Container.get('logger');
195
232
  try {
@@ -204,13 +241,14 @@ exports.default = (app) => {
204
241
  name: x.name,
205
242
  value: x.value,
206
243
  remarks: x.remarks,
244
+ labels: x.labels,
207
245
  })));
208
246
  return res.send({ code: 200, data: result });
209
247
  }
210
248
  else {
211
249
  return res.send({
212
250
  code: 400,
213
- message: '每条数据 name 或者 value 字段不能为空,参考导出文件格式',
251
+ message: (0, i18n_1.t)('每条数据 name 或者 value 字段不能为空,参考导出文件格式'),
214
252
  });
215
253
  }
216
254
  }
@@ -15,6 +15,7 @@ const dependence_1 = __importDefault(require("./dependence"));
15
15
  const system_1 = __importDefault(require("./system"));
16
16
  const subscription_1 = __importDefault(require("./subscription"));
17
17
  const update_1 = __importDefault(require("./update"));
18
+ const dashboard_1 = __importDefault(require("./dashboard"));
18
19
  const health_1 = __importDefault(require("./health"));
19
20
  exports.default = () => {
20
21
  const app = (0, express_1.Router)();
@@ -29,6 +30,7 @@ exports.default = () => {
29
30
  (0, system_1.default)(app);
30
31
  (0, subscription_1.default)(app);
31
32
  (0, update_1.default)(app);
33
+ (0, dashboard_1.default)(app);
32
34
  (0, health_1.default)(app);
33
35
  return app;
34
36
  };
@@ -7,6 +7,7 @@ const celebrate_1 = require("celebrate");
7
7
  const express_1 = require("express");
8
8
  const typedi_1 = require("typedi");
9
9
  const config_1 = __importDefault(require("../config"));
10
+ const i18n_1 = require("../shared/i18n");
10
11
  const util_1 = require("../config/util");
11
12
  const log_1 = __importDefault(require("../services/log"));
12
13
  const route = (0, express_1.Router)();
@@ -34,7 +35,7 @@ exports.default = (app) => {
34
35
  if (!finalPath || blacklist.includes(req.query.path)) {
35
36
  return res.send({
36
37
  code: 403,
37
- message: '暂无权限',
38
+ message: (0, i18n_1.t)('暂无权限'),
38
39
  });
39
40
  }
40
41
  const content = await (0, util_1.getFileContentByName)(finalPath);
@@ -51,7 +52,7 @@ exports.default = (app) => {
51
52
  if (!finalPath || blacklist.includes(req.query.path)) {
52
53
  return res.send({
53
54
  code: 403,
54
- message: '暂无权限',
55
+ message: (0, i18n_1.t)('暂无权限'),
55
56
  });
56
57
  }
57
58
  const content = await (0, util_1.getFileContentByName)(finalPath);
@@ -75,7 +76,7 @@ exports.default = (app) => {
75
76
  if (!finalPath || blacklist.includes(path)) {
76
77
  return res.send({
77
78
  code: 403,
78
- message: '暂无权限',
79
+ message: (0, i18n_1.t)('暂无权限'),
79
80
  });
80
81
  }
81
82
  await (0, util_1.rmPath)(finalPath);
@@ -98,7 +99,7 @@ exports.default = (app) => {
98
99
  if (!filePath) {
99
100
  return res.send({
100
101
  code: 403,
101
- message: '暂无权限',
102
+ message: (0, i18n_1.t)('暂无权限'),
102
103
  });
103
104
  }
104
105
  return res.download(filePath, filename, (err) => {
@@ -32,11 +32,16 @@ const typedi_1 = require("typedi");
32
32
  const config_1 = __importDefault(require("../config"));
33
33
  const fs = __importStar(require("fs/promises"));
34
34
  const celebrate_1 = require("celebrate");
35
- const path_1 = require("path");
35
+ const path_1 = __importStar(require("path"));
36
36
  const script_1 = __importDefault(require("../services/script"));
37
+ const i18n_1 = require("../shared/i18n");
37
38
  const multer_1 = __importDefault(require("multer"));
38
39
  const utils_1 = require("../shared/utils");
39
40
  const route = (0, express_1.Router)();
41
+ function isPathAllowed(targetPath) {
42
+ const resolved = path_1.default.resolve(targetPath);
43
+ return config_1.default.writePathList.some((x) => resolved.startsWith(x));
44
+ }
40
45
  const storage = multer_1.default.diskStorage({
41
46
  destination: function (req, file, cb) {
42
47
  cb(null, config_1.default.scriptPath);
@@ -145,23 +150,34 @@ exports.default = (app) => {
145
150
  if (config_1.default.writePathList.every((x) => !path.startsWith(x))) {
146
151
  return res.send({
147
152
  code: 403,
148
- message: '暂无权限',
153
+ message: (0, i18n_1.t)('暂无权限'),
149
154
  });
150
155
  }
151
156
  if (req.file) {
152
- await fs.rename(req.file.path, (0, path_1.join)(path, filename));
157
+ const uploadPath = (0, path_1.join)(path, filename);
158
+ if (!isPathAllowed(uploadPath)) {
159
+ return res.send({ code: 403, message: (0, i18n_1.t)('暂无权限') });
160
+ }
161
+ await fs.rename(req.file.path, uploadPath);
153
162
  return res.send({ code: 200 });
154
163
  }
155
164
  if (directory) {
156
- await fs.mkdir((0, path_1.join)(path, directory), { recursive: true });
165
+ const dirPath = (0, path_1.join)(path, directory);
166
+ if (!isPathAllowed(dirPath)) {
167
+ return res.send({ code: 403, message: (0, i18n_1.t)('暂无权限') });
168
+ }
169
+ await fs.mkdir(dirPath, { recursive: true });
157
170
  return res.send({ code: 200 });
158
171
  }
159
172
  if (!originFilename) {
160
173
  originFilename = filename;
161
174
  }
162
- const originFilePath = (0, path_1.join)(path, `${originFilename.replace(/\//g, '')}`);
175
+ const originFilePath = (0, path_1.join)(path, originFilename);
176
+ const filePath = (0, path_1.join)(path, filename);
177
+ if (!isPathAllowed(filePath) || !isPathAllowed(originFilePath)) {
178
+ return res.send({ code: 403, message: (0, i18n_1.t)('暂无权限') });
179
+ }
163
180
  await fs.mkdir(path, { recursive: true });
164
- const filePath = (0, path_1.join)(path, `${filename.replace(/\//g, '')}`);
165
181
  const fileExists = await (0, util_1.fileExist)(filePath);
166
182
  if (fileExists) {
167
183
  await fs.copyFile(originFilePath, (0, path_1.join)(config_1.default.bakPath, originFilename.replace(/\//g, '')));
@@ -190,7 +206,7 @@ exports.default = (app) => {
190
206
  if (!filePath) {
191
207
  return res.send({
192
208
  code: 403,
193
- message: '暂无权限',
209
+ message: (0, i18n_1.t)('暂无权限'),
194
210
  });
195
211
  }
196
212
  await (0, utils_1.writeFileWithLock)(filePath, content);
@@ -217,7 +233,7 @@ exports.default = (app) => {
217
233
  if (!filePath) {
218
234
  return res.send({
219
235
  code: 403,
220
- message: '暂无权限',
236
+ message: (0, i18n_1.t)('暂无权限'),
221
237
  });
222
238
  }
223
239
  await (0, util_1.rmPath)(filePath);
@@ -243,7 +259,7 @@ exports.default = (app) => {
243
259
  if (!filePath) {
244
260
  return res.send({
245
261
  code: 403,
246
- message: '暂无权限',
262
+ message: (0, i18n_1.t)('暂无权限'),
247
263
  });
248
264
  }
249
265
  return res.download(filePath, filename, (err) => {
@@ -271,6 +287,9 @@ exports.default = (app) => {
271
287
  }
272
288
  const { name, ext } = (0, path_1.parse)(filename);
273
289
  const filePath = (0, path_1.join)(config_1.default.scriptPath, path, `${name}.swap${ext}`);
290
+ if (!isPathAllowed(filePath)) {
291
+ return res.send({ code: 403, message: (0, i18n_1.t)('暂无权限') });
292
+ }
274
293
  await (0, utils_1.writeFileWithLock)(filePath, content || '');
275
294
  const scriptService = typedi_1.Container.get(script_1.default);
276
295
  const result = await scriptService.runScript(filePath);
@@ -294,6 +313,9 @@ exports.default = (app) => {
294
313
  }
295
314
  const { name, ext } = (0, path_1.parse)(filename);
296
315
  const filePath = (0, path_1.join)(config_1.default.scriptPath, path, `${name}.swap${ext}`);
316
+ if (!isPathAllowed(filePath)) {
317
+ return res.send({ code: 403, message: (0, i18n_1.t)('暂无权限') });
318
+ }
297
319
  const logPath = (0, path_1.join)(config_1.default.logPath, path, `${name}.swap`);
298
320
  const scriptService = typedi_1.Container.get(script_1.default);
299
321
  const result = await scriptService.stopScript(filePath, pid);
@@ -320,6 +342,9 @@ exports.default = (app) => {
320
342
  }
321
343
  const filePath = (0, path_1.join)(config_1.default.scriptPath, path, filename);
322
344
  const newPath = (0, path_1.join)(config_1.default.scriptPath, path, newFilename);
345
+ if (!isPathAllowed(filePath) || !isPathAllowed(newPath)) {
346
+ return res.send({ code: 403, message: (0, i18n_1.t)('暂无权限') });
347
+ }
323
348
  await fs.rename(filePath, newPath);
324
349
  res.send({ code: 200 });
325
350
  }