@whyour/qinglong 2.21.0-5 → 2.21.0-7

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 (75) hide show
  1. package/back/protos/api.proto +3 -0
  2. package/deploy/kubernetes/README.md +81 -0
  3. package/deploy/kubernetes/base/kustomization.yaml +5 -0
  4. package/deploy/kubernetes/base/qinglong.yaml +95 -0
  5. package/deploy/kubernetes/overlays/example/kustomization.yaml +10 -0
  6. package/package.json +1 -1
  7. package/sample/config.sample.sh +7 -2
  8. package/sample/notify.js +64 -0
  9. package/sample/notify.py +56 -0
  10. package/static/build/api/env.js +2 -2
  11. package/static/build/app.js +19 -19
  12. package/static/build/config/const.js +1 -0
  13. package/static/build/config/grpcCerts.js +4 -4
  14. package/static/build/data/notify.js +8 -1
  15. package/static/build/loaders/app.js +6 -6
  16. package/static/build/loaders/db.js +2 -2
  17. package/static/build/loaders/initFile.js +1 -1
  18. package/static/build/loaders/server.js +1 -1
  19. package/static/build/protos/api.js +23 -1
  20. package/static/build/schedule/addCron.js +67 -10
  21. package/static/build/schedule/delCron.js +11 -1
  22. package/static/build/services/cron.js +88 -45
  23. package/static/build/services/grpc.js +1 -1
  24. package/static/build/services/http.js +1 -1
  25. package/static/build/services/notify.js +42 -0
  26. package/static/build/shared/i18n.js +7 -0
  27. package/static/build/validation/schedule.js +9 -0
  28. package/static/dist/1147.62793604.async.js +1 -0
  29. package/static/dist/2208.85285476.async.js +1 -0
  30. package/static/dist/2286.f371c743.async.js +1 -0
  31. package/static/dist/8826.db34ad8d.async.js +1 -0
  32. package/static/dist/index.html +2 -2
  33. package/static/dist/{preload_helper.e0bef63b.js → preload_helper.0148f907.js} +1 -1
  34. package/static/dist/{src__pages__crontab__index.6b63ca05.async.js → src__pages__crontab__index.68e78a33.async.js} +1 -1
  35. package/static/dist/src__pages__dependence__modal.f7bb47ba.async.js +1 -0
  36. package/static/dist/src__pages__env__editNameModal.48715576.async.js +1 -0
  37. package/static/dist/src__pages__env__index.452cdc47.async.js +1 -0
  38. package/static/dist/src__pages__env__modal.e1234834.async.js +1 -0
  39. package/static/dist/src__pages__error__index.dedb2b90.async.js +1 -0
  40. package/static/dist/src__pages__initialization__index.22620a77.async.js +1 -0
  41. package/static/dist/src__pages__script__editNameModal.532a3044.async.js +1 -0
  42. package/static/dist/src__pages__script__index.1fc20f22.async.js +1 -0
  43. package/static/dist/src__pages__script__renameModal.5cdde919.async.js +1 -0
  44. package/static/dist/src__pages__script__saveModal.5ffed35f.async.js +1 -0
  45. package/static/dist/src__pages__script__setting.1a82c6d7.async.js +1 -0
  46. package/static/dist/src__pages__setting__appModal.43b7758a.async.js +1 -0
  47. package/static/dist/src__pages__setting__dependence.9c04c79a.async.js +1 -0
  48. package/static/dist/src__pages__setting__index.7e070838.async.js +1 -0
  49. package/static/dist/src__pages__setting__notification.bac481c2.async.js +1 -0
  50. package/static/dist/src__pages__setting__security.b7235753.async.js +1 -0
  51. package/static/dist/src__pages__setting__systemLog.b81a8c1d.async.js +1 -0
  52. package/static/dist/src__pages__subscription__modal.562448e2.async.js +1 -0
  53. package/static/dist/{umi.025a31b3.js → umi.19f46d39.js} +1 -1
  54. package/static/dist/1147.c420132e.async.js +0 -1
  55. package/static/dist/2208.f7ba3dfa.async.js +0 -1
  56. package/static/dist/2286.164bb089.async.js +0 -1
  57. package/static/dist/8826.6879b271.async.js +0 -1
  58. package/static/dist/src__pages__dependence__modal.11124896.async.js +0 -1
  59. package/static/dist/src__pages__env__editNameModal.5d264b25.async.js +0 -1
  60. package/static/dist/src__pages__env__index.9a565d2b.async.js +0 -1
  61. package/static/dist/src__pages__env__modal.7f2ef1bc.async.js +0 -1
  62. package/static/dist/src__pages__error__index.f156b45e.async.js +0 -1
  63. package/static/dist/src__pages__initialization__index.e96d4ba8.async.js +0 -1
  64. package/static/dist/src__pages__script__editNameModal.3e7e100d.async.js +0 -1
  65. package/static/dist/src__pages__script__index.e869812f.async.js +0 -1
  66. package/static/dist/src__pages__script__renameModal.5e987ef5.async.js +0 -1
  67. package/static/dist/src__pages__script__saveModal.3f9d23d6.async.js +0 -1
  68. package/static/dist/src__pages__script__setting.a535793a.async.js +0 -1
  69. package/static/dist/src__pages__setting__appModal.251cd14f.async.js +0 -1
  70. package/static/dist/src__pages__setting__dependence.29f13e8c.async.js +0 -1
  71. package/static/dist/src__pages__setting__index.7a39d415.async.js +0 -1
  72. package/static/dist/src__pages__setting__notification.390fc905.async.js +0 -1
  73. package/static/dist/src__pages__setting__security.598720a8.async.js +0 -1
  74. package/static/dist/src__pages__setting__systemLog.1a5c855f.async.js +0 -1
  75. package/static/dist/src__pages__subscription__modal.3562c670.async.js +0 -1
@@ -58,10 +58,10 @@ exports.default = async () => {
58
58
  // Column already exists or other error, continue
59
59
  }
60
60
  }
61
- logger_1.default.info('✌️ DB loaded');
61
+ logger_1.default.info('[boot] DB loaded');
62
62
  }
63
63
  catch (error) {
64
- logger_1.default.error('✌️ DB load failed', error);
64
+ logger_1.default.error('[boot] DB load failed', error);
65
65
  }
66
66
  };
67
67
  //# sourceMappingURL=db.js.map
@@ -117,6 +117,6 @@ exports.default = async () => {
117
117
  await (0, utils_1.writeFileWithLock)(item.target, content);
118
118
  }
119
119
  }
120
- logger_1.default.info('✌️ Init file down');
120
+ logger_1.default.info('[boot] Init file down');
121
121
  };
122
122
  //# sourceMappingURL=initFile.js.map
@@ -7,7 +7,7 @@ const logger_1 = __importDefault(require("./logger"));
7
7
  const sock_1 = __importDefault(require("./sock"));
8
8
  exports.default = async ({ server }) => {
9
9
  await (0, sock_1.default)({ server });
10
- logger_1.default.info('✌️ Sock loaded');
10
+ logger_1.default.info('[boot] Sock loaded');
11
11
  process.on('uncaughtException', (error) => {
12
12
  logger_1.default.error('Uncaught exception:', error);
13
13
  });
@@ -33,6 +33,7 @@ var NotificationMode;
33
33
  NotificationMode[NotificationMode["chronocat"] = 18] = "chronocat";
34
34
  NotificationMode[NotificationMode["ntfy"] = 19] = "ntfy";
35
35
  NotificationMode[NotificationMode["wxPusherBot"] = 20] = "wxPusherBot";
36
+ NotificationMode[NotificationMode["wxPusherSpt"] = 21] = "wxPusherSpt";
36
37
  NotificationMode[NotificationMode["UNRECOGNIZED"] = -1] = "UNRECOGNIZED";
37
38
  })(NotificationMode || (exports.NotificationMode = NotificationMode = {}));
38
39
  function notificationModeFromJSON(object) {
@@ -100,6 +101,9 @@ function notificationModeFromJSON(object) {
100
101
  case 20:
101
102
  case "wxPusherBot":
102
103
  return NotificationMode.wxPusherBot;
104
+ case 21:
105
+ case "wxPusherSpt":
106
+ return NotificationMode.wxPusherSpt;
103
107
  case -1:
104
108
  case "UNRECOGNIZED":
105
109
  default:
@@ -151,6 +155,8 @@ function notificationModeToJSON(object) {
151
155
  return "ntfy";
152
156
  case NotificationMode.wxPusherBot:
153
157
  return "wxPusherBot";
158
+ case NotificationMode.wxPusherSpt:
159
+ return "wxPusherSpt";
154
160
  case NotificationMode.UNRECOGNIZED:
155
161
  default:
156
162
  return "UNRECOGNIZED";
@@ -2495,6 +2501,7 @@ function createBaseNotificationInfo() {
2495
2501
  wxPusherBotAppToken: undefined,
2496
2502
  wxPusherBotTopicIds: undefined,
2497
2503
  wxPusherBotUids: undefined,
2504
+ wxPusherSptList: undefined,
2498
2505
  };
2499
2506
  }
2500
2507
  exports.NotificationInfo = {
@@ -2706,6 +2713,9 @@ exports.NotificationInfo = {
2706
2713
  if (message.wxPusherBotUids !== undefined) {
2707
2714
  writer.uint32(546).string(message.wxPusherBotUids);
2708
2715
  }
2716
+ if (message.wxPusherSptList !== undefined) {
2717
+ writer.uint32(562).string(message.wxPusherSptList);
2718
+ }
2709
2719
  return writer;
2710
2720
  },
2711
2721
  decode(input, length) {
@@ -3198,6 +3208,13 @@ exports.NotificationInfo = {
3198
3208
  message.wxPusherBotUids = reader.string();
3199
3209
  continue;
3200
3210
  }
3211
+ case 70: {
3212
+ if (tag !== 562) {
3213
+ break;
3214
+ }
3215
+ message.wxPusherSptList = reader.string();
3216
+ continue;
3217
+ }
3201
3218
  }
3202
3219
  if ((tag & 7) === 4 || tag === 0) {
3203
3220
  break;
@@ -3289,6 +3306,7 @@ exports.NotificationInfo = {
3289
3306
  ? globalThis.String(object.wxPusherBotTopicIds)
3290
3307
  : undefined,
3291
3308
  wxPusherBotUids: isSet(object.wxPusherBotUids) ? globalThis.String(object.wxPusherBotUids) : undefined,
3309
+ wxPusherSptList: isSet(object.wxPusherSptList) ? globalThis.String(object.wxPusherSptList) : undefined,
3292
3310
  };
3293
3311
  },
3294
3312
  toJSON(message) {
@@ -3500,13 +3518,16 @@ exports.NotificationInfo = {
3500
3518
  if (message.wxPusherBotUids !== undefined) {
3501
3519
  obj.wxPusherBotUids = message.wxPusherBotUids;
3502
3520
  }
3521
+ if (message.wxPusherSptList !== undefined) {
3522
+ obj.wxPusherSptList = message.wxPusherSptList;
3523
+ }
3503
3524
  return obj;
3504
3525
  },
3505
3526
  create(base) {
3506
3527
  return exports.NotificationInfo.fromPartial(base !== null && base !== void 0 ? base : {});
3507
3528
  },
3508
3529
  fromPartial(object) {
3509
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44;
3530
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45;
3510
3531
  const message = createBaseNotificationInfo();
3511
3532
  message.type = (_a = object.type) !== null && _a !== void 0 ? _a : 0;
3512
3533
  message.gotifyUrl = (_b = object.gotifyUrl) !== null && _b !== void 0 ? _b : undefined;
@@ -3577,6 +3598,7 @@ exports.NotificationInfo = {
3577
3598
  message.wxPusherBotAppToken = (_42 = object.wxPusherBotAppToken) !== null && _42 !== void 0 ? _42 : undefined;
3578
3599
  message.wxPusherBotTopicIds = (_43 = object.wxPusherBotTopicIds) !== null && _43 !== void 0 ? _43 : undefined;
3579
3600
  message.wxPusherBotUids = (_44 = object.wxPusherBotUids) !== null && _44 !== void 0 ? _44 : undefined;
3601
+ message.wxPusherSptList = (_45 = object.wxPusherSptList) !== null && _45 !== void 0 ? _45 : undefined;
3580
3602
  return message;
3581
3603
  },
3582
3604
  };
@@ -4,14 +4,59 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.addCron = void 0;
7
+ const grpc_js_1 = require("@grpc/grpc-js");
7
8
  const node_schedule_1 = __importDefault(require("node-schedule"));
8
9
  const data_1 = require("./data");
9
10
  const runCron_1 = require("../shared/runCron");
10
11
  const logger_1 = __importDefault(require("../loaders/logger"));
12
+ const i18n_1 = require("../shared/i18n");
13
+ /**
14
+ * 预校验 cron 表达式,检测 node-schedule 会拒绝但 cron-parser 会接受的 pattern。
15
+ * node-schedule 对 bare /N(字段以 / 开头,如前无星号/数字前缀的 /6)返回 null,
16
+ * 提前拦截避免走 scheduleJob 后才发现无效。
17
+ */
18
+ const isValidCronField = (cron) => {
19
+ // 检测 bare /N 模式:字段以 / 开头如 "/6",或空格后紧跟 "/6"
20
+ // node-schedule 会对这种字段返回 null
21
+ if (/\s\/\d/.test(cron) || /^\/\d/.test(cron)) {
22
+ return false;
23
+ }
24
+ // 检测 ? 字符:Quartz cron 语法,node-schedule 在大多数位置返回 null
25
+ // cron-parser 接受但 node-schedule 拒绝,提前拦截
26
+ if (/\?/.test(cron)) {
27
+ return false;
28
+ }
29
+ return true;
30
+ };
11
31
  const addCron = (call, callback) => {
12
32
  var _a;
33
+ // ===== 第一遍:预校验所有 cron 表达式 =====
34
+ const validationErrors = [];
35
+ for (const item of call.request.crons) {
36
+ const { id, schedule, extra_schedules } = item;
37
+ if (!isValidCronField(schedule)) {
38
+ validationErrors.push((0, i18n_1.tf)('任务ID %s: 无效的 cron 表达式 "%s"(不支持裸 /N 步长和 ? 字符)', String(id), schedule));
39
+ }
40
+ if (extra_schedules === null || extra_schedules === void 0 ? void 0 : extra_schedules.length) {
41
+ extra_schedules.forEach((x) => {
42
+ if (!isValidCronField(x.schedule)) {
43
+ validationErrors.push((0, i18n_1.tf)('任务ID %s (extra_schedule): 无效的 cron 表达式 "%s"(不支持裸 /N 步长和 ? 字符)', String(id), x.schedule));
44
+ }
45
+ });
46
+ }
47
+ }
48
+ if (validationErrors.length > 0) {
49
+ const details = validationErrors.join('\n');
50
+ const err = new Error(details);
51
+ err.code = grpc_js_1.status.INVALID_ARGUMENT;
52
+ err.details = details;
53
+ callback(err, null);
54
+ return;
55
+ }
56
+ // ===== 第二遍:注册所有任务 =====
13
57
  for (const item of call.request.crons) {
14
58
  const { id, schedule, command, extra_schedules, name } = item;
59
+ // 取消该 id 已有的旧任务
15
60
  if (data_1.scheduleStacks.has(id)) {
16
61
  (_a = data_1.scheduleStacks.get(id)) === null || _a === void 0 ? void 0 : _a.forEach((x) => x.cancel());
17
62
  }
@@ -21,18 +66,30 @@ const addCron = (call, callback) => {
21
66
  logger_1.default.info('[schedule][创建定时任务] 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s', id, name, x.schedule, command);
22
67
  });
23
68
  }
24
- data_1.scheduleStacks.set(id, [
25
- node_schedule_1.default.scheduleJob(id, schedule, async () => {
26
- logger_1.default.info(`[schedule][准备运行任务] 命令: ${command}`);
27
- (0, runCron_1.runCron)(command, item);
28
- }),
29
- ...((extra_schedules === null || extra_schedules === void 0 ? void 0 : extra_schedules.length)
30
- ? extra_schedules.map((x) => node_schedule_1.default.scheduleJob(id, x.schedule, async () => {
69
+ const mainJob = node_schedule_1.default.scheduleJob(id, schedule, async () => {
70
+ logger_1.default.info(`[schedule][准备运行任务] 命令: ${command}`);
71
+ (0, runCron_1.runCron)(command, item);
72
+ });
73
+ if (!mainJob) {
74
+ logger_1.default.warn('[schedule][创建定时任务] scheduleJob 返回 null(不符合预期,已通过预校验): 任务ID: %s, cron: %s', id, schedule);
75
+ }
76
+ const extraJobs = (extra_schedules === null || extra_schedules === void 0 ? void 0 : extra_schedules.length)
77
+ ? extra_schedules.map((x) => {
78
+ const job = node_schedule_1.default.scheduleJob(id, x.schedule, async () => {
31
79
  logger_1.default.info(`[schedule][准备运行任务] 命令: ${command}`);
32
80
  (0, runCron_1.runCron)(command, item);
33
- }))
34
- : []),
35
- ]);
81
+ });
82
+ if (!job) {
83
+ logger_1.default.warn('[schedule][创建定时任务] scheduleJob 返回 null(不符合预期,已通过预校验): 任务ID: %s, cron: %s', id, x.schedule);
84
+ }
85
+ return job;
86
+ })
87
+ : [];
88
+ // 过滤 null(兜底保护,正常情况下预校验已拦截)
89
+ const jobs = [mainJob, ...extraJobs].filter((x) => x != null);
90
+ if (jobs.length > 0) {
91
+ data_1.scheduleStacks.set(id, jobs);
92
+ }
36
93
  }
37
94
  callback(null, null);
38
95
  };
@@ -11,7 +11,17 @@ const delCron = (call, callback) => {
11
11
  for (const id of call.request.ids) {
12
12
  if (data_1.scheduleStacks.has(id)) {
13
13
  logger_1.default.info('[schedule][取消定时任务] 任务ID: %s', id);
14
- (_a = data_1.scheduleStacks.get(id)) === null || _a === void 0 ? void 0 : _a.forEach(x => x.cancel());
14
+ // 过滤掉 nodeSchedule.scheduleJob() 对无效表达式返回的 null
15
+ // 否则对 null 调 cancel() 会让整个取消流程抛出 UNKNOWN 错误,
16
+ // 进而导致 HTTP 端的 remove() 跳过 setCrontab(),造成 crontab.list 残留。
17
+ (_a = data_1.scheduleStacks.get(id)) === null || _a === void 0 ? void 0 : _a.filter((x) => x != null).forEach((x) => {
18
+ try {
19
+ x.cancel();
20
+ }
21
+ catch (error) {
22
+ logger_1.default.warn('[schedule][取消任务失败] 任务ID: %s, 错误: %s', id, (error === null || error === void 0 ? void 0 : error.message) || error);
23
+ }
24
+ });
15
25
  data_1.scheduleStacks.delete(id);
16
26
  }
17
27
  }
@@ -105,15 +105,24 @@ let CronService = class CronService {
105
105
  return doc;
106
106
  }
107
107
  if (this.shouldUseCronClient(doc)) {
108
- await client_1.default.addCron([
109
- {
110
- name: doc.name || '',
111
- id: String(doc.id),
112
- schedule: doc.schedule,
113
- command: this.makeCommand(doc),
114
- extra_schedules: doc.extra_schedules || [],
115
- },
116
- ]);
108
+ try {
109
+ await client_1.default.addCron([
110
+ {
111
+ name: doc.name || '',
112
+ id: String(doc.id),
113
+ schedule: doc.schedule,
114
+ command: this.makeCommand(doc),
115
+ extra_schedules: doc.extra_schedules || [],
116
+ },
117
+ ]);
118
+ }
119
+ catch (error) {
120
+ // gRPC 注册失败时回滚 DB 记录,避免产生"僵尸任务"
121
+ // (DB 和 crontab.list 有记录但调度器永远不会执行)
122
+ await cron_1.CrontabModel.destroy({ where: { id: doc.id } });
123
+ this.logger.error('[crontab] Failed to register cron job in scheduler, task creation rolled back:', (error === null || error === void 0 ? void 0 : error.message) || error);
124
+ throw new Error(`${(0, i18n_1.t)('调度器注册失败,任务创建已回滚')}: ${(error === null || error === void 0 ? void 0 : error.details) || (error === null || error === void 0 ? void 0 : error.message)}`);
125
+ }
117
126
  }
118
127
  await this.setCrontab();
119
128
  return doc;
@@ -130,17 +139,46 @@ let CronService = class CronService {
130
139
  if (doc.isDisabled === 1 || (0, util_1.isDemoEnv)()) {
131
140
  return newDoc;
132
141
  }
133
- await client_1.default.delCron([String(newDoc.id)]);
142
+ try {
143
+ await client_1.default.delCron([String(newDoc.id)]);
144
+ }
145
+ catch (error) {
146
+ this.logger.warn('[crontab] Failed to unregister cron job in scheduler:', (error === null || error === void 0 ? void 0 : error.message) || error);
147
+ }
134
148
  if (this.shouldUseCronClient(newDoc)) {
135
- await client_1.default.addCron([
136
- {
137
- name: doc.name || '',
138
- id: String(newDoc.id),
139
- schedule: newDoc.schedule,
140
- command: this.makeCommand(newDoc),
141
- extra_schedules: newDoc.extra_schedules || [],
142
- },
143
- ]);
149
+ try {
150
+ await client_1.default.addCron([
151
+ {
152
+ name: doc.name || '',
153
+ id: String(newDoc.id),
154
+ schedule: newDoc.schedule,
155
+ command: this.makeCommand(newDoc),
156
+ extra_schedules: newDoc.extra_schedules || [],
157
+ },
158
+ ]);
159
+ }
160
+ catch (error) {
161
+ // gRPC 注册新任务失败 → 回滚 DB 到旧数据,并尝试恢复旧调度注册
162
+ await cron_1.CrontabModel.update(doc, { where: { id: doc.id } });
163
+ if (this.shouldUseCronClient(doc)) {
164
+ try {
165
+ await client_1.default.addCron([
166
+ {
167
+ name: doc.name || '',
168
+ id: String(doc.id),
169
+ schedule: doc.schedule,
170
+ command: this.makeCommand(doc),
171
+ extra_schedules: doc.extra_schedules || [],
172
+ },
173
+ ]);
174
+ }
175
+ catch (_recoveryError) {
176
+ this.logger.warn('[crontab] Failed to restore old cron job in scheduler after rollback:', (_recoveryError === null || _recoveryError === void 0 ? void 0 : _recoveryError.message) || _recoveryError);
177
+ }
178
+ }
179
+ this.logger.error('[crontab] Failed to register updated cron job in scheduler, update rolled back:', (error === null || error === void 0 ? void 0 : error.message) || error);
180
+ throw new Error(`${(0, i18n_1.t)('调度器注册失败,任务更新已回滚')}: ${(error === null || error === void 0 ? void 0 : error.details) || (error === null || error === void 0 ? void 0 : error.message)}`);
181
+ }
144
182
  }
145
183
  await this.setCrontab();
146
184
  return newDoc;
@@ -205,7 +243,12 @@ let CronService = class CronService {
205
243
  }
206
244
  async remove(ids) {
207
245
  await cron_1.CrontabModel.destroy({ where: { id: ids } });
208
- await client_1.default.delCron(ids.map(String));
246
+ try {
247
+ await client_1.default.delCron(ids.map(String));
248
+ }
249
+ catch (error) {
250
+ this.logger.warn('[crontab] Failed to unregister cron job in scheduler:', (error === null || error === void 0 ? void 0 : error.message) || error);
251
+ }
209
252
  await this.setCrontab();
210
253
  }
211
254
  async pin(ids) {
@@ -525,14 +568,6 @@ let CronService = class CronService {
525
568
  const logPath = `${uniqPath}/${logTime}.log`;
526
569
  const absolutePath = path_1.default.resolve(config_1.default.logPath, `${logPath}`);
527
570
  const cp = (0, cross_spawn_1.spawn)(`real_log_path=${logPath} no_delay=true ${this.makeCommand(cron, true)}`, { shell: '/bin/bash' });
528
- const startedAt = (0, dayjs_1.default)().unix();
529
- const instance = await runningInstance_1.RunningInstanceModel.create({
530
- cron_id: id,
531
- pid: cp.pid,
532
- log_path: logPath,
533
- started_at: startedAt,
534
- status: runningInstance_1.InstanceStatus.running,
535
- });
536
571
  await cron_1.CrontabModel.update({ status: cron_1.CrontabStatus.running, pid: cp.pid, log_path: logPath }, { where: { id } });
537
572
  cp.stdout.on('data', async (data) => {
538
573
  await logStreamManager_1.logStreamManager.write(absolutePath, data.toString());
@@ -548,19 +583,6 @@ let CronService = class CronService {
548
583
  cp.on('exit', async (code) => {
549
584
  this.logger.info('[panel][执行任务结束] 参数: %s, 退出码: %j', JSON.stringify(params), code);
550
585
  await logStreamManager_1.logStreamManager.closeStream(absolutePath);
551
- const finishedAt = (0, dayjs_1.default)().unix();
552
- await runningInstance_1.RunningInstanceModel.update({
553
- finished_at: finishedAt,
554
- status: code === 0 ? runningInstance_1.InstanceStatus.finished : runningInstance_1.InstanceStatus.error,
555
- exit_code: code !== null && code !== void 0 ? code : undefined,
556
- }, { where: { id: instance.id } });
557
- // Only set cron to idle if no other running instances exist
558
- const otherRunning = await runningInstance_1.RunningInstanceModel.count({
559
- where: { cron_id: id, status: runningInstance_1.InstanceStatus.running },
560
- });
561
- if (otherRunning === 0) {
562
- await cron_1.CrontabModel.update({ status: cron_1.CrontabStatus.idle, pid: undefined }, { where: { id } });
563
- }
564
586
  resolve(Object.assign(Object.assign({}, params), { pid: cp.pid, code }));
565
587
  });
566
588
  });
@@ -568,7 +590,12 @@ let CronService = class CronService {
568
590
  }
569
591
  async disabled(ids) {
570
592
  await cron_1.CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
571
- await client_1.default.delCron(ids.map(String));
593
+ try {
594
+ await client_1.default.delCron(ids.map(String));
595
+ }
596
+ catch (error) {
597
+ this.logger.warn('[crontab] Failed to unregister cron job in scheduler:', (error === null || error === void 0 ? void 0 : error.message) || error);
598
+ }
572
599
  await this.setCrontab();
573
600
  }
574
601
  async enabled(ids) {
@@ -586,7 +613,15 @@ let CronService = class CronService {
586
613
  if ((0, util_1.isDemoEnv)()) {
587
614
  return;
588
615
  }
589
- await client_1.default.addCron(crons);
616
+ try {
617
+ await client_1.default.addCron(crons);
618
+ }
619
+ catch (error) {
620
+ // gRPC 注册失败 → 回滚启用状态,避免 DB 显示已启用但调度器未注册
621
+ await cron_1.CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
622
+ this.logger.error('[crontab] Failed to register cron job in scheduler, enable rolled back:', (error === null || error === void 0 ? void 0 : error.message) || error);
623
+ throw new Error(`${(0, i18n_1.t)('调度器注册失败,任务启用已回滚')}: ${(error === null || error === void 0 ? void 0 : error.details) || (error === null || error === void 0 ? void 0 : error.message)}`);
624
+ }
590
625
  await this.setCrontab();
591
626
  }
592
627
  async log(id) {
@@ -740,8 +775,16 @@ let CronService = class CronService {
740
775
  await (0, utils_1.writeFileWithLock)(config_1.default.crontabFile, '');
741
776
  return;
742
777
  }
743
- await client_1.default.addCron(regularCrons);
744
- this.setCrontab(tabs);
778
+ // 先同步 crontab.list 与系统 crontab,确保其始终反映数据库真实状态。
779
+ // gRPC 调度注册为尽力而为:失败时不阻断文件同步,调度器重启后会重新注册。
780
+ // 这避免了因调度器短暂不可用导致 crontab.list 与数据库脱节(订阅更新误判任务已存在)。
781
+ await this.setCrontab(tabs);
782
+ try {
783
+ await client_1.default.addCron(regularCrons);
784
+ }
785
+ catch (error) {
786
+ this.logger.warn('[crontab] Failed to register cron job in scheduler:', (error === null || error === void 0 ? void 0 : error.message) || error);
787
+ }
745
788
  }
746
789
  async bootTask() {
747
790
  const tabs = await this.crontabs();
@@ -75,7 +75,7 @@ let GrpcServerService = class GrpcServerService {
75
75
  try {
76
76
  const address = this.formatGrpcAddress(host, grpcPort);
77
77
  await bindAsync(address, credentials);
78
- logger_1.default.debug(`✌️ gRPC service started successfully on ${address}`);
78
+ logger_1.default.debug(`[boot] gRPC service started successfully on ${address}`);
79
79
  metrics_1.metricsService.record('grpc_service_start', 1, {
80
80
  port: grpcPort.toString(),
81
81
  host
@@ -27,7 +27,7 @@ let HttpServerService = class HttpServerService {
27
27
  for (const host of hostsToTry) {
28
28
  try {
29
29
  const server = await this.tryListen(expressApp, port, host);
30
- logger_1.default.debug(`✌️ HTTP service started successfully on ${host}:${port}`);
30
+ logger_1.default.debug(`[boot] HTTP service started successfully on ${host}:${port}`);
31
31
  metrics_1.metricsService.record('http_service_start', 1, {
32
32
  port: port.toString(),
33
33
  host
@@ -55,6 +55,7 @@ let NotificationService = class NotificationService {
55
55
  ['chronocat', this.chronocat],
56
56
  ['ntfy', this.ntfy],
57
57
  ['wxPusherBot', this.wxPusherBot],
58
+ ['wxPusherSpt', this.wxPusherSpt],
58
59
  ['openiLink', this.openiLink],
59
60
  ]);
60
61
  this.title = '';
@@ -620,6 +621,47 @@ let NotificationService = class NotificationService {
620
621
  throw new Error(error.response ? error.response.body : error);
621
622
  }
622
623
  }
624
+ async wxPusherSpt() {
625
+ const { wxPusherSptList } = this.params;
626
+ // 处理 SPT,将逗号分隔的字符串转为数组
627
+ const spts = wxPusherSptList
628
+ ? wxPusherSptList
629
+ .split(',')
630
+ .map((spt) => spt.trim())
631
+ .filter((spt) => spt)
632
+ : [];
633
+ if (!spts.length) {
634
+ throw new Error((0, i18n_1.t)('wxPusher SPT 不能为空'));
635
+ }
636
+ if (spts.length > 10) {
637
+ throw new Error((0, i18n_1.t)('wxPusher SPT 最多支持 10 个'));
638
+ }
639
+ const url = `https://wxpusher.zjiecode.com/api/send/message/simple-push`;
640
+ const json = {
641
+ content: `<h1>${this.title}</h1><br/><div style='white-space: pre-wrap;'>${this.content}</div>`,
642
+ summary: this.title,
643
+ contentType: 2,
644
+ };
645
+ // 单个 SPT 用 spt,多个用 sptList
646
+ if (spts.length === 1) {
647
+ json.spt = spts[0];
648
+ }
649
+ else {
650
+ json.sptList = spts;
651
+ }
652
+ try {
653
+ const res = await http_1.httpClient.post(url, Object.assign(Object.assign({}, this.gotOption), { json }));
654
+ if (res.code === 1000) {
655
+ return true;
656
+ }
657
+ else {
658
+ throw new Error(JSON.stringify(res));
659
+ }
660
+ }
661
+ catch (error) {
662
+ throw new Error(error.response ? error.response.body : error);
663
+ }
664
+ }
623
665
  async chronocat() {
624
666
  var _a, _b;
625
667
  const { chronocatURL, chronocatQQ, chronocatToken } = this.params;
@@ -82,6 +82,8 @@ const messages = {
82
82
  'client_id 或 client_seret 有误': 'Invalid client_id or client_secret',
83
83
  '订阅执行完成': 'Subscription completed',
84
84
  'wxPusher 服务的 TopicIds 和 Uids 至少配置一个才行': 'wxPusher requires at least one of TopicIds or Uids',
85
+ 'wxPusher SPT 不能为空': 'wxPusher SPT cannot be empty',
86
+ 'wxPusher SPT 最多支持 10 个': 'wxPusher SPT supports at most 10 tokens',
85
87
  'Url 或者 Body 中必须包含 $title': 'Url or Body must contain $title',
86
88
  '绝对路径必须在日志目录内或使用 /dev/null': 'Absolute path must be within log directory or use /dev/null',
87
89
  '请先登录': 'Please login first',
@@ -123,6 +125,11 @@ const messages = {
123
125
  '执行before命令...': 'Execute before command...',
124
126
  '执行after命令...': 'Execute after command...',
125
127
  '## 执行结束... %s 耗时 %s 秒': '## Execution finished... %s elapsed %s seconds',
128
+ '调度器注册失败,任务创建已回滚': 'Scheduler registration failed, task creation rolled back',
129
+ '调度器注册失败,任务更新已回滚': 'Scheduler registration failed, task update rolled back',
130
+ '调度器注册失败,任务启用已回滚': 'Scheduler registration failed, task enable rolled back',
131
+ '任务ID %s: 无效的 cron 表达式 "%s"(不支持裸 /N 步长和 ? 字符)': 'Task ID %s: invalid cron expression "%s" (bare /N steps and ? character not supported)',
132
+ '任务ID %s (extra_schedule): 无效的 cron 表达式 "%s"(不支持裸 /N 步长和 ? 字符)': 'Task ID %s (extra_schedule): invalid cron expression "%s" (bare /N steps and ? character not supported)',
126
133
  },
127
134
  };
128
135
  let currentLang = 'zh';
@@ -14,6 +14,15 @@ const validateSchedule = (value, helpers) => {
14
14
  value.startsWith(schedule_1.ScheduleType.BOOT)) {
15
15
  return value;
16
16
  }
17
+ // 检测裸 /N 模式:cron-parser 会接受,但 node-schedule 会返回 null
18
+ // 提前拦截,避免任务入库后调度器注册失败
19
+ if (/\s\/\d/.test(value) || /^\/\d/.test(value)) {
20
+ return helpers.error('any.invalid');
21
+ }
22
+ // 检测 ? 字符:Quartz cron 语法,node-schedule 在大多数字段上返回 null
23
+ if (/\?/.test(value)) {
24
+ return helpers.error('any.invalid');
25
+ }
17
26
  try {
18
27
  if (cron_parser_1.default.parse(value).hasNext()) {
19
28
  return value;
@@ -0,0 +1 @@
1
+ (self.webpackChunk_whyour_qinglong=self.webpackChunk_whyour_qinglong||[]).push([[1147],{3475:function(e,a,i){"use strict";var r=i(72317),t=window.__ENV__QlBaseUrl||"/";a.Z={siteName:r.ZP.get("青龙"),baseUrl:t,apiPrefix:"".concat(t,"api/"),authKey:"token",layouts:[{name:"primary",include:[/.*/],exclude:[/(\/(en|zh))*\/login/]}],scopes:[{name:r.ZP.get("定时任务"),value:"crons"},{name:r.ZP.get("环境变量"),value:"envs"},{name:r.ZP.get("订阅管理"),value:"subscriptions"},{name:r.ZP.get("配置文件"),value:"configs"},{name:r.ZP.get("脚本管理"),value:"scripts"},{name:r.ZP.get("日志管理"),value:"logs"},{name:r.ZP.get("依赖管理"),value:"dependencies"},{name:r.ZP.get("系统信息"),value:"system"}],scopesMap:{crons:r.ZP.get("定时任务"),envs:r.ZP.get("环境变量"),subscriptions:r.ZP.get("订阅管理"),configs:r.ZP.get("配置文件"),scripts:r.ZP.get("脚本管理"),logs:r.ZP.get("日志管理"),dependencies:r.ZP.get("依赖管理"),system:r.ZP.get("系统信息")},notificationModes:[{value:"gotify",label:"Gotify"},{value:"ntfy",label:"Ntfy"},{value:"goCqHttpBot",label:"GoCqHttpBot"},{value:"serverChan",label:r.ZP.get("Server酱")},{value:"pushDeer",label:"PushDeer"},{value:"bark",label:"Bark"},{value:"telegramBot",label:r.ZP.get("Telegram机器人")},{value:"dingtalkBot",label:r.ZP.get("钉钉机器人")},{value:"weWorkBot",label:r.ZP.get("企业微信机器人")},{value:"weWorkApp",label:r.ZP.get("企业微信应用")},{value:"aibotk",label:r.ZP.get("智能微秘书")},{value:"iGot",label:"IGot"},{value:"pushPlus",label:"PushPlus"},{value:"wePlusBot",label:r.ZP.get("微加机器人")},{value:"wxPusherBot",label:"wxPusher"},{value:"wxPusherSpt",label:"WxPusher(极简推送SPT-推荐)"},{value:"openiLink",label:"OpeniLink"},{value:"chat",label:r.ZP.get("群晖chat")},{value:"email",label:r.ZP.get("邮箱")},{value:"lark",label:r.ZP.get("飞书机器人")},{value:"pushMe",label:"PushMe"},{value:"chronocat",label:"Chronocat"},{value:"webhook",label:r.ZP.get("自定义通知")},{value:"closed",label:r.ZP.get("已关闭")}],notificationModeMap:{gotify:[{label:"gotifyUrl",tip:r.ZP.get("gotify的url地址,例如 https://push.example.de:8080"),required:!0},{label:"gotifyToken",tip:r.ZP.get("gotify的消息应用token码"),required:!0},{label:"gotifyPriority",tip:r.ZP.get("推送消息的优先级")}],ntfy:[{label:"ntfyUrl",tip:r.ZP.get("ntfy的url地址,例如 https://ntfy.sh"),required:!0},{label:"ntfyTopic",tip:r.ZP.get("ntfy应用topic"),required:!0},{label:"ntfyPriority",tip:r.ZP.get("推送消息的优先级")},{label:"ntfyToken",tip:r.ZP.get("ntfy应用token")},{label:"ntfyUsername",tip:r.ZP.get("ntfy应用用户名")},{label:"ntfyPassword",tip:r.ZP.get("ntfy应用密码")},{label:"ntfyActions",tip:r.ZP.get("ntfy用户动作")}],chat:[{label:"synologyChatUrl",tip:r.ZP.get("synologyChat的url地址"),required:!0}],goCqHttpBot:[{label:"goCqHttpBotUrl",tip:r.ZP.get("推送到个人QQ: http://127.0.0.1/send_private_msg,群:http://127.0.0.1/send_group_msg"),required:!0},{label:"goCqHttpBotToken",tip:r.ZP.get("访问密钥"),required:!0},{label:"goCqHttpBotQq",tip:r.ZP.get("如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群"),required:!0}],serverChan:[{label:"serverChanKey",tip:r.ZP.get("Server酱SENDKEY"),required:!0}],pushDeer:[{label:"pushDeerKey",tip:r.ZP.get("PushDeer的Key,https://github.com/easychen/pushdeer"),required:!0},{label:"pushDeerUrl",tip:r.ZP.get("PushDeer的自架API endpoint,默认是 https://api2.pushdeer.com/message/push")}],bark:[{label:"barkPush",tip:r.ZP.get("Bark的信息IP/设备码,例如:https://api.day.app/XXXXXXXX"),required:!0},{label:"barkIcon",tip:r.ZP.get("BARK推送图标,自定义推送图标 (需iOS15或以上才能显示)")},{label:"barkSound",tip:r.ZP.get("BARK推送铃声,铃声列表去APP查看复制填写")},{label:"barkGroup",tip:r.ZP.get("BARK推送消息的分组,默认为qinglong")},{label:"barkLevel",tip:r.ZP.get("BARK推送消息的时效性,默认为active")},{label:"barkUrl",tip:r.ZP.get("BARK推送消息的跳转URL")},{label:"barkArchive",tip:r.ZP.get("BARK是否保存推送消息")}],telegramBot:[{label:"telegramBotToken",tip:r.ZP.get("telegram机器人的token,例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw"),required:!0},{label:"telegramBotUserId",tip:r.ZP.get("telegram用户的id,例如:129xxx206"),required:!0},{label:"telegramBotProxyHost",tip:r.ZP.get("代理IP")},{label:"telegramBotProxyPort",tip:r.ZP.get("代理端口")},{label:"telegramBotProxyAuth",tip:r.ZP.get("telegram代理配置认证参数,用户名与密码用英文冒号连接 user:password")},{label:"telegramBotApiHost",tip:r.ZP.get("telegram api自建的反向代理地址,默认tg官方api")}],dingtalkBot:[{label:"dingtalkBotToken",tip:r.ZP.get("钉钉机器人webhook token,例如:5a544165465465645d0f31dca676e7bd07415asdasd"),required:!0},{label:"dingtalkBotSecret",tip:r.ZP.get("密钥,机器人安全设置页面,加签一栏下面显示的SEC开头的字符串")}],weWorkBot:[{label:"weWorkBotKey",tip:r.ZP.get("企业微信机器人的webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770),例如:693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa"),required:!0},{label:"weWorkOrigin",tip:r.ZP.get("企业微信代理地址")}],weWorkApp:[{label:"weWorkAppKey",tip:r.ZP.get("corpid、corpsecret、touser(注:多个成员ID使用|隔开)、agentid、消息类型(选填,不填默认文本消息类型) 注意用,号隔开(英文输入法的逗号),例如:wwcfrs,B-76WERQ,qinglong,1000001,2COat"),required:!0},{label:"weWorkOrigin",tip:r.ZP.get("企业微信代理地址")}],aibotk:[{label:"aibotkKey",tip:r.ZP.get("密钥key,智能微秘书个人中心获取apikey,申请地址:https://wechat.aibotk.com/signup?from=ql"),required:!0},{label:"aibotkType",tip:r.ZP.get("发送的目标,群组或者好友"),required:!0,placeholder:r.ZP.get("请输入要发送的目标"),items:[{value:"room",label:r.ZP.get("群聊")},{value:"contact",label:r.ZP.get("好友")}]},{label:"aibotkName",tip:r.ZP.get("要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称"),required:!0}],iGot:[{label:"iGotPushKey",tip:r.ZP.get("iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX"),required:!0}],pushPlus:[{label:"pushPlusToken",tip:r.ZP.get("微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/"),required:!0},{label:"pushPlusUser",tip:r.ZP.get("一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)")},{label:"pushplusTemplate",tip:r.ZP.get("发送模板")},{label:"pushplusChannel",tip:r.ZP.get("发送渠道")},{label:"pushplusWebhook",tip:r.ZP.get("webhook编码")},{label:"pushplusCallbackUrl",tip:r.ZP.get("发送结果回调地址")},{label:"pushplusTo",tip:r.ZP.get("好友令牌")}],wePlusBot:[{label:"wePlusBotToken",tip:r.ZP.get("用户令牌,扫描登录后 我的—>设置->令牌 中获取,参考 https://www.weplusbot.com/"),required:!0},{label:"wePlusBotReceiver",tip:r.ZP.get("消息接收人")},{label:"wePlusBotVersion",tip:r.ZP.get("调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版")}],wxPusherBot:[{label:"wxPusherBotAppToken",tip:r.ZP.get("wxPusherBot的appToken"),required:!0},{label:"wxPusherBotTopicIds",tip:r.ZP.get("wxPusherBot的topicIds"),required:!1},{label:"wxPusherBotUids",tip:r.ZP.get("wxPusherBot的uids"),required:!1}],wxPusherSpt:[{label:"wxPusherSptList",tip:r.ZP.get("wxPusherSpt的SPT"),required:!0}],openiLink:[{label:"openiLinkAppToken",tip:r.ZP.get("OpeniLink的app_token,在OpeniLink Hub后台安装App后获取,参考 https://openilink.com/docs/hub/apps"),required:!0},{label:"openiLinkHubUrl",tip:r.ZP.get("OpeniLink Hub地址,默认为 https://hub.openilink.com,自建Hub时填写自己的地址")},{label:"openiLinkContextToken",tip:r.ZP.get("OpeniLink的context_token,用于标识消息会话上下文,可从消息事件中获取")}],lark:[{label:"larkKey",tip:r.ZP.get("飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973"),required:!0},{label:"larkSecret",tip:r.ZP.get("飞书群组机器人加签密钥,安全设置中开启签名校验后获得")}],email:[{label:"emailService",tip:r.ZP.get("邮箱服务名称,比如126、163、Gmail、QQ等,支持列表https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json"),required:!0},{label:"emailUser",tip:r.ZP.get("邮箱认证地址"),required:!0},{label:"emailPass",tip:r.ZP.get("SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定"),required:!0},{label:"emailTo",tip:r.ZP.get("收件邮箱地址,多个分号分隔,默认发送给发件邮箱地址")}],pushMe:[{label:"pushMeKey",tip:r.ZP.get("PushMe的Key,https://push.i-i.me/"),required:!0},{label:"pushMeUrl",tip:r.ZP.get("自建的PushMeServer消息接口地址,例如:http://127.0.0.1:3010,不填则使用官方消息接口"),required:!1}],chronocat:[{label:"chronocatURL",tip:r.ZP.get("Chronocat Red 服务的连接地址 https://chronocat.vercel.app/install/docker/official/"),required:!0},{label:"chronocatQQ",tip:r.ZP.get("个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx"),required:!0},{label:"chronocatToken",tip:r.ZP.get("docker安装在持久化config目录下的chronocat.yml文件可找到"),required:!0}],webhook:[{label:"webhookMethod",tip:r.ZP.get("请求方法"),required:!0,items:[{value:"GET"},{value:"POST"},{value:"PUT"}]},{label:"webhookContentType",tip:r.ZP.get("请求头Content-Type"),required:!0,items:[{value:"text/plain"},{value:"application/json"},{value:"multipart/form-data"},{value:"application/x-www-form-urlencoded"}]},{label:"webhookUrl",tip:r.ZP.get("请求链接以http或者https开头。url或者body中必须包含$title,$content可选,对应api内容的位置"),required:!0,placeholder:"https://xxx.cn/api?content=$title\n"},{label:"webhookHeaders",tip:r.ZP.get("请求头格式Custom-Header1: Header1,多个换行分割"),placeholder:"Custom-Header1: Header1\nCustom-Header2: Header2"},{label:"webhookBody",tip:r.ZP.get("请求体格式key1: value1,多个换行分割。url或者body中必须包含$title,$content可选,对应api内容的位置"),placeholder:"key1: $title\nkey2: $content"}]},documentTitleMap:{"/login":r.ZP.get("登录"),"/initialization":r.ZP.get("初始化"),"/crontab":r.ZP.get("定时任务"),"/env":r.ZP.get("环境变量"),"/subscription":r.ZP.get("订阅管理"),"/config":r.ZP.get("配置文件"),"/script":r.ZP.get("脚本管理"),"/diff":r.ZP.get("对比工具"),"/log":r.ZP.get("日志管理"),"/setting":r.ZP.get("系统设置"),"/error":r.ZP.get("错误日志"),"/dependence":r.ZP.get("依赖管理"),"/dashboard":r.ZP.get("仪表盘")},dependenceTypes:["nodejs","python3","linux"]}},97302:function(e,a,i){"use strict";i.d(a,{eY:function(){return r},um:function(){return t},z:function(){return o}});var r="     ",t={".py":"python",".js":"javascript",".mjs":"javascript",".sh":"shell",".ts":"typescript",".ini":"ini",".json":"json"},o=["UTC","Africa/Abidjan","Africa/Accra","Africa/Addis Ababa","Africa/Algiers","Africa/Asmara","Africa/Bamako","Africa/Bangui","Africa/Banjul","Africa/Bissau","Africa/Blantyre","Africa/Brazzaville","Africa/Bujumbura","Africa/Cairo","Africa/Casablanca","Africa/Ceuta","Africa/Conakry","Africa/Dakar","Africa/Dar es Salaam","Africa/Djibouti","Africa/Douala","Africa/El Aaiun","Africa/Freetown","Africa/Gaborone","Africa/Harare","Africa/Johannesburg","Africa/Juba","Africa/Kampala","Africa/Khartoum","Africa/Kigali","Africa/Kinshasa","Africa/Lagos","Africa/Libreville","Africa/Lome","Africa/Luanda","Africa/Lubumbashi","Africa/Lusaka","Africa/Malabo","Africa/Maputo","Africa/Maseru","Africa/Mbabane","Africa/Mogadishu","Africa/Monrovia","Africa/Nairobi","Africa/Ndjamena","Africa/Niamey","Africa/Nouakchott","Africa/Ouagadougou","Africa/Porto-Novo","Africa/Sao Tome","Africa/Tripoli","Africa/Tunis","Africa/Windhoek","America/Adak","America/Anchorage","America/Anguilla","America/Antigua","America/Araguaina","America/Argentina/Buenos Aires","America/Argentina/Catamarca","America/Argentina/Cordoba","America/Argentina/Jujuy","America/Argentina/La Rioja","America/Argentina/Mendoza","America/Argentina/Rio Gallegos","America/Argentina/Salta","America/Argentina/San Juan","America/Argentina/San Luis","America/Argentina/Tucuman","America/Argentina/Ushuaia","America/Aruba","America/Asuncion","America/Atikokan","America/Bahia","America/Bahia Banderas","America/Barbados","America/Belem","America/Belize","America/Blanc-Sablon","America/Boa Vista","America/Bogota","America/Boise","America/Cambridge Bay","America/Campo Grande","America/Cancun","America/Caracas","America/Cayenne","America/Cayman","America/Chicago","America/Chihuahua","America/Ciudad Juarez","America/Costa Rica","America/Creston","America/Cuiaba","America/Curacao","America/Danmarkshavn","America/Dawson","America/Dawson Creek","America/Denver","America/Detroit","America/Dominica","America/Edmonton","America/Eirunepe","America/El Salvador","America/Fort Nelson","America/Fortaleza","America/Glace Bay","America/Goose Bay","America/Grand Turk","America/Grenada","America/Guadeloupe","America/Guatemala","America/Guayaquil","America/Guyana","America/Halifax","America/Havana","America/Hermosillo","America/Indiana/Indianapolis","America/Indiana/Knox","America/Indiana/Marengo","America/Indiana/Petersburg","America/Indiana/Tell City","America/Indiana/Vevay","America/Indiana/Vincennes","America/Indiana/Winamac","America/Inuvik","America/Iqaluit","America/Jamaica","America/Juneau","America/Kentucky/Louisville","America/Kentucky/Monticello","America/Kralendijk","America/La Paz","America/Lima","America/Los Angeles","America/Lower Princes","America/Maceio","America/Managua","America/Manaus","America/Marigot","America/Martinique","America/Matamoros","America/Mazatlan","America/Menominee","America/Merida","America/Metlakatla","America/Mexico City","America/Miquelon","America/Moncton","America/Monterrey","America/Montevideo","America/Montserrat","America/Nassau","America/New York","America/Nome","America/Noronha","America/North Dakota/Beulah","America/North Dakota/Center","America/North Dakota/New Salem","America/Nuuk","America/Ojinaga","America/Panama","America/Paramaribo","America/Phoenix","America/Port of Spain","America/Port-au-Prince","America/Porto Velho","America/Puerto Rico","America/Punta Arenas","America/Rankin Inlet","America/Recife","America/Regina","America/Resolute","America/Rio Branco","America/Santarem","America/Santiago","America/Santo Domingo","America/Sao Paulo","America/Scoresbysund","America/Sitka","America/St Barthelemy","America/St Johns","America/St Kitts","America/St Lucia","America/St Thomas","America/St Vincent","America/Swift Current","America/Tegucigalpa","America/Thule","America/Tijuana","America/Toronto","America/Tortola","America/Vancouver","America/Whitehorse","America/Winnipeg","America/Yakutat","Antarctica/Casey","Antarctica/Davis","Antarctica/DumontDUrville","Antarctica/Macquarie","Antarctica/Mawson","Antarctica/McMurdo","Antarctica/Palmer","Antarctica/Rothera","Antarctica/Syowa","Antarctica/Troll","Antarctica/Vostok","Arctic/Longyearbyen","Asia/Aden","Asia/Almaty","Asia/Amman","Asia/Anadyr","Asia/Aqtau","Asia/Aqtobe","Asia/Ashgabat","Asia/Atyrau","Asia/Baghdad","Asia/Bahrain","Asia/Baku","Asia/Bangkok","Asia/Barnaul","Asia/Beirut","Asia/Bishkek","Asia/Brunei","Asia/Chita","Asia/Choibalsan","Asia/Colombo","Asia/Damascus","Asia/Dhaka","Asia/Dili","Asia/Dubai","Asia/Dushanbe","Asia/Famagusta","Asia/Gaza","Asia/Hebron","Asia/Ho Chi Minh","Asia/Hong Kong","Asia/Hovd","Asia/Irkutsk","Asia/Jakarta","Asia/Jayapura","Asia/Jerusalem","Asia/Kabul","Asia/Kamchatka","Asia/Karachi","Asia/Kathmandu","Asia/Khandyga","Asia/Kolkata","Asia/Krasnoyarsk","Asia/Kuala Lumpur","Asia/Kuching","Asia/Kuwait","Asia/Macau","Asia/Magadan","Asia/Makassar","Asia/Manila","Asia/Muscat","Asia/Nicosia","Asia/Novokuznetsk","Asia/Novosibirsk","Asia/Omsk","Asia/Oral","Asia/Phnom Penh","Asia/Pontianak","Asia/Pyongyang","Asia/Qatar","Asia/Qostanay","Asia/Qyzylorda","Asia/Riyadh","Asia/Sakhalin","Asia/Samarkand","Asia/Seoul","Asia/Shanghai","Asia/Singapore","Asia/Srednekolymsk","Asia/Taipei","Asia/Tashkent","Asia/Tbilisi","Asia/Tehran","Asia/Thimphu","Asia/Tokyo","Asia/Tomsk","Asia/Ulaanbaatar","Asia/Urumqi","Asia/Ust-Nera","Asia/Vientiane","Asia/Vladivostok","Asia/Yakutsk","Asia/Yangon","Asia/Yekaterinburg","Asia/Yerevan","Atlantic/Azores","Atlantic/Bermuda","Atlantic/Canary","Atlantic/Cape Verde","Atlantic/Faroe","Atlantic/Madeira","Atlantic/Reykjavik","Atlantic/South Georgia","Atlantic/St Helena","Atlantic/Stanley","Australia/Adelaide","Australia/Brisbane","Australia/Broken Hill","Australia/Darwin","Australia/Eucla","Australia/Hobart","Australia/Lindeman","Australia/Lord Howe","Australia/Melbourne","Australia/Perth","Australia/Sydney","Etc/GMT","Etc/GMT+1","Etc/GMT+10","Etc/GMT+11","Etc/GMT+12","Etc/GMT+2","Etc/GMT+3","Etc/GMT+4","Etc/GMT+5","Etc/GMT+6","Etc/GMT+7","Etc/GMT+8","Etc/GMT+9","Etc/GMT-1","Etc/GMT-10","Etc/GMT-11","Etc/GMT-12","Etc/GMT-13","Etc/GMT-14","Etc/GMT-2","Etc/GMT-3","Etc/GMT-4","Etc/GMT-5","Etc/GMT-6","Etc/GMT-7","Etc/GMT-8","Etc/GMT-9","Europe/Amsterdam","Europe/Andorra","Europe/Astrakhan","Europe/Athens","Europe/Belgrade","Europe/Berlin","Europe/Bratislava","Europe/Brussels","Europe/Bucharest","Europe/Budapest","Europe/Busingen","Europe/Chisinau","Europe/Copenhagen","Europe/Dublin","Europe/Gibraltar","Europe/Guernsey","Europe/Helsinki","Europe/Isle of Man","Europe/Istanbul","Europe/Jersey","Europe/Kaliningrad","Europe/Kirov","Europe/Kyiv","Europe/Lisbon","Europe/Ljubljana","Europe/London","Europe/Luxembourg","Europe/Madrid","Europe/Malta","Europe/Mariehamn","Europe/Minsk","Europe/Monaco","Europe/Moscow","Europe/Oslo","Europe/Paris","Europe/Podgorica","Europe/Prague","Europe/Riga","Europe/Rome","Europe/Samara","Europe/San Marino","Europe/Sarajevo","Europe/Saratov","Europe/Simferopol","Europe/Skopje","Europe/Sofia","Europe/Stockholm","Europe/Tallinn","Europe/Tirane","Europe/Ulyanovsk","Europe/Vaduz","Europe/Vatican","Europe/Vienna","Europe/Vilnius","Europe/Volgograd","Europe/Warsaw","Europe/Zagreb","Europe/Zurich","Indian/Antananarivo","Indian/Chagos","Indian/Christmas","Indian/Cocos","Indian/Comoro","Indian/Kerguelen","Indian/Mahe","Indian/Maldives","Indian/Mauritius","Indian/Mayotte","Indian/Reunion","Pacific/Apia","Pacific/Auckland","Pacific/Bougainville","Pacific/Chatham","Pacific/Chuuk","Pacific/Easter","Pacific/Efate","Pacific/Fakaofo","Pacific/Fiji","Pacific/Funafuti","Pacific/Galapagos","Pacific/Gambier","Pacific/Guadalcanal","Pacific/Guam","Pacific/Honolulu","Pacific/Kanton","Pacific/Kiritimati","Pacific/Kosrae","Pacific/Kwajalein","Pacific/Majuro","Pacific/Marquesas","Pacific/Midway","Pacific/Nauru","Pacific/Niue","Pacific/Norfolk","Pacific/Noumea","Pacific/Pago Pago","Pacific/Palau","Pacific/Pitcairn","Pacific/Pohnpei","Pacific/Port Moresby","Pacific/Rarotonga","Pacific/Saipan","Pacific/Tahiti","Pacific/Tarawa","Pacific/Tongatapu","Pacific/Wake","Pacific/Wallis"]},16314:function(e,a,i){"use strict";i.d(a,{W:function(){return P}});var r=i(90228),t=i.n(r),o=i(87999),n=i.n(o),c=i(72317),l=i(61508),s=i(33839),u=i(3475),p=i(98783),A=i(69313),g=i(52676);l.ZP.config({duration:2});var d=Date.now(),m=A.Z.create({timeout:6e4,params:{t:d}}),h=["".concat(u.Z.baseUrl,"api/user/login"),"".concat(u.Z.baseUrl,"open/auth/token"),"".concat(u.Z.baseUrl,"api/user/two-factor/login"),"".concat(u.Z.baseUrl,"api/system"),"".concat(u.Z.baseUrl,"api/user/init"),"".concat(u.Z.baseUrl,"api/user/notification/init")];m.interceptors.request.use((function(e){var a=localStorage.getItem(u.Z.authKey);return a&&!h.includes(e.url)?(e.headers.Authorization="Bearer ".concat(a),e):e})),m.interceptors.response.use(function(){var e=n()(t()().mark((function e(a){var i,r,o;return t()().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(i=a.status,![502,504].includes(i)){e.next=5;break}p.history.push("/error"),e.next=18;break;case 5:if(401!==i){e.next=9;break}"/login"!==p.history.location.pathname&&(localStorage.removeItem(u.Z.authKey),p.history.push("/login")),e.next=18;break;case 9:return e.prev=9,200!==(r=a.data).code&&(o=r.message||r.data)&&s.Z.error({message:o,description:null!=r&&r.errors?(0,g.jsx)(g.Fragment,{children:null==r?void 0:r.errors.map((function(e){return(0,g.jsx)("div",{children:e.message})}))}):void 0}),e.abrupt("return",r);case 15:e.prev=15,e.t0=e.catch(9);case 17:case 18:return e.abrupt("return",a);case 19:case"end":return e.stop()}}),e,null,[[9,15]])})));return function(a){return e.apply(this,arguments)}}(),(function(e){if(e.response){var a=e.response.data?e.response.data.message||e.message:e.response.statusText,i=e.response.status;if([502,504].includes(i))p.history.push("/error");else if(401===i)"/login"!==p.history.location.pathname&&(l.ZP.error(c.ZP.get("登录已过期,请重新登录")),localStorage.removeItem(u.Z.authKey),p.history.push("/login"));else{var r,t,o,n;if("function"==typeof(null===(r=e.config)||void 0===r?void 0:r.onError))return null===(n=e.config)||void 0===n?void 0:n.onError(e.response);a&&s.Z.error({message:a,description:null!==(t=e.response)&&void 0!==t&&null!==(t=t.data)&&void 0!==t&&t.errors?(0,g.jsx)(g.Fragment,{children:null===(o=e.response)||void 0===o||null===(o=o.data)||void 0===o||null===(o=o.errors)||void 0===o?void 0:o.map((function(e){return(0,g.jsxs)("div",{children:[e.message," (",e.value,")"]})}))}):void 0})}}else console.log(e.message);return Promise.reject(e)}));var P=m},48916:function(e,a,i){"use strict";i.d(a,{DY:function(){return g},FA:function(){return m},H$:function(){return b},I1:function(){return A},W9:function(){return u},ZP:function(){return s},o4:function(){return f},rS:function(){return P},w7:function(){return p},x$:function(){return d}});var r=i(15558),t=i.n(r),o=i(72317),n=i(97302),c=i(91379),l=i.n(c);function s(){var e=navigator.userAgent.toLowerCase(),a=function(a){return a.test(e)},i=function(a){return(e.match(a)||[]).toString().replace(/[^0-9|_.]/g,"").replace(/_/g,".")},r="unknow";a(/windows|win32|win64|wow32|wow64/g)?r="windows":a(/macintosh|macintel/g)?r="macos":a(/x11/g)?r="linux":a(/android|adr/g)?r="android":a(/ios|iphone|ipad|ipod|iwatch/g)&&(r="ios");var t="unknow";"windows"===r?a(/windows nt 5.0|windows 2000/g)?t="2000":a(/windows nt 5.1|windows xp/g)?t="xp":a(/windows nt 5.2|windows 2003/g)?t="2003":a(/windows nt 6.0|windows vista/g)?t="vista":a(/windows nt 6.1|windows 7/g)?t="7":a(/windows nt 6.2|windows 8/g)?t="8":a(/windows nt 6.3|windows 8.1/g)?t="8.1":a(/windows nt 10.0|windows 10/g)&&(t="10"):"macos"===r?t=i(/os x [\d._]+/g):"android"===r?t=i(/android [\d._]+/g):"ios"===r&&(t=i(/os [\d._]+/g));var o="unknow";"windows"===r||"macos"===r||"linux"===r?o="desktop":("android"===r||"ios"===r||a(/mobile/g))&&(o="mobile");var n="unknow",c="unknow";a(/applewebkit/g)?(n="webkit",a(/edge/g)?c="edge":a(/opr/g)?c="opera":a(/chrome/g)?c="chrome":a(/safari/g)&&(c="safari")):a(/gecko/g)&&a(/firefox/g)?(n="gecko",c="firefox"):a(/presto/g)?(n="presto",c="opera"):a(/trident|compatible|msie/g)&&(n="trident",c="iexplore");var l="unknow";"webkit"===n?l=i(/applewebkit\/[\d._]+/g):"gecko"===n?l=i(/gecko\/[\d._]+/g):"presto"===n?l=i(/presto\/[\d._]+/g):"trident"===n&&(l=i(/trident\/[\d._]+/g));var s="unknow";"chrome"===c?s=i(/chrome\/[\d._]+/g):"safari"===c?s=i(/version\/[\d._]+/g):"firefox"===c?s=i(/firefox\/[\d._]+/g):"opera"===c?s=i(/opr\/[\d._]+/g):"iexplore"===c?s=i(/(msie [\d._]+)|(rv:[\d._]+)/g):"edge"===c&&(s=i(/edge\/[\d._]+/g));var u="none",p="unknow";a(/micromessenger/g)?(u="wechat",p=i(/micromessenger\/[\d._]+/g)):a(/qqbrowser/g)?(u="qq",p=i(/qqbrowser\/[\d._]+/g)):a(/ucbrowser/g)?(u="uc",p=i(/ucbrowser\/[\d._]+/g)):a(/qihu 360se/g)?u="360":a(/2345explorer/g)?(u="2345",p=i(/2345explorer\/[\d._]+/g)):a(/metasr/g)?u="sougou":a(/lbbrowser/g)?u="liebao":a(/maxthon/g)&&(u="maxthon",p=i(/maxthon\/[\d._]+/g));var A=Object.assign({engine:n,engineVs:l,platform:o,supporter:c,supporterVs:s,system:r,systemVs:t},"none"===u?{}:{shell:u,shellVs:p});return console.log("%c\n .d88b. d888888b d8b db d888b db .d88b. d8b db d888b \n.8P Y8. `88' 888o 88 88' Y8b 88 .8P Y8. 888o 88 88' Y8b \n88 88 88 88V8o 88 88 88 88 88 88V8o 88 88 \n88 88 88 88 V8o88 88 ooo 88 88 88 88 V8o88 88 ooo \n`8P d8' .88. 88 V888 88. ~8~ 88booo. `8b d8' 88 V888 88. ~8~ \n `Y88'Y8 Y888888P VP V8P Y888P Y88888P `Y88P' VP V8P Y888P \n \n \n","color: blue;font-size: 14px;"),console.log("%c忘形雨笠烟蓑,知心牧唱樵歌。明月清风共我,闲人三个,从他今古消磨。\n","color: yellow;font-size: 18px;"),console.log("%c青龙运行环境:\n\n系统:".concat(A.system,"/").concat(A.systemVs,"\n浏览器:").concat(A.supporter,"/").concat(A.supporterVs,"\n内核:").concat(A.engine,"/").concat(A.engineVs),"color: green;font-size: 14px;font-weight: bold;"),A}function u(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},a=e.extraHeight,i=e.target;void 0===a&&(a=99);var r=null,t=0;(r=i||document.querySelector(".ant-table-wrapper"))&&(t=r.getBoundingClientRect().top);var o=document.body.clientHeight-t-a;return o}function p(e,a){var i,r,t=window.URL||window.webkitURL||window,o=new Blob([a]),n=document.createElementNS("http://www.w3.org/1999/xhtml","a");n.href=t.createObjectURL(o),n.download=e,i=n,(r=document.createEvent("MouseEvents")).initMouseEvent("click",!0,!1,window,0,0,0,0,0,!1,!1,!1,!1,0,null),i.dispatchEvent(r)}function A(e,a,i){var r=t()(e),o=[];return function e(r){if(r)for(var t=0;t<(null==r?void 0:r.length);t++){if(a(r[t]))return i?void(r[t].children?r[t].children.unshift(i):r[t].children=[i]):void r.splice(t,1);r[t].children&&(o.push(r[t].key),e(r[t].children))}}(r),r}function g(e,a){var i;return function e(r){if(r)for(var t=0;t<(null==r?void 0:r.length);t++)a(r[t])?i=r[t]:r[t].children&&e(r[t].children)}(t()(e)),i}function d(e){return[n.eY,o.ZP.get("执行结束")].some((function(a){return e.includes(a)}))}function m(e){var a=e.split(" ");if("repo"===a[1]||"raw"===a[1])return a[2];var i=a.find((function(e){return[".js",".ts",".sh",".py"].some((function(a){return e.endsWith(a)}))}));if(i){var r,t,o="".concat(window.__ENV__QL_DIR,"/data/scripts");i.startsWith(o)&&(i=i.replace(o,""));var n=i.lastIndexOf("/");return n>=0?(t=i.slice(n+1),r=i.slice(0,n)):(t=i,r=""),[t,r]}}function h(e){try{var a=l().parse(e);if(a)return a.next().toDate()}catch(e){}return null}function P(e,a){var i=h(e);return null!=a&&a.length&&a.forEach((function(e){var a=h(e.schedule);a&&(!i||a<i)&&(i=a)})),i}function b(e){var a=function(e){if(!e)return"";var a=e.split(".");return".".concat(a[a.length-1])}(e);return n.um[a]}function f(){var e=document.createElement("div");e.style.position="fixed",e.style.top="0px",e.style.left="0px",e.style.width="100%",e.style.height="100%",e.style.backgroundColor="transparent",e.style.zIndex="9999",document.body.appendChild(e),e.addEventListener("click",(function(e){e.stopPropagation(),e.preventDefault()})),document.body.style.overflow="hidden"}},16130:function(){},6057:function(){}}]);