@whyour/qinglong 2.21.0-6 → 2.21.0-8

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 (81) 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/api/system.js +6 -7
  12. package/static/build/app.js +19 -19
  13. package/static/build/config/const.js +1 -0
  14. package/static/build/config/grpcCerts.js +4 -4
  15. package/static/build/data/notify.js +8 -1
  16. package/static/build/loaders/app.js +6 -6
  17. package/static/build/loaders/db.js +2 -2
  18. package/static/build/loaders/express.js +1 -6
  19. package/static/build/loaders/initFile.js +1 -1
  20. package/static/build/loaders/server.js +1 -1
  21. package/static/build/protos/api.js +23 -1
  22. package/static/build/schedule/addCron.js +67 -10
  23. package/static/build/schedule/delCron.js +11 -1
  24. package/static/build/services/cron.js +88 -24
  25. package/static/build/services/grpc.js +1 -1
  26. package/static/build/services/http.js +1 -1
  27. package/static/build/services/notify.js +42 -0
  28. package/static/build/services/user.js +3 -0
  29. package/static/build/shared/auth.js +7 -1
  30. package/static/build/shared/i18n.js +7 -0
  31. package/static/build/validation/schedule.js +9 -0
  32. package/static/dist/1147.62793604.async.js +1 -0
  33. package/static/dist/2208.85285476.async.js +1 -0
  34. package/static/dist/2286.f371c743.async.js +1 -0
  35. package/static/dist/3191.545ab873.async.js +1 -0
  36. package/static/dist/8826.db34ad8d.async.js +1 -0
  37. package/static/dist/index.html +2 -2
  38. package/static/dist/{preload_helper.e0bef63b.js → preload_helper.3048fe5b.js} +1 -1
  39. package/static/dist/{src__pages__crontab__index.6b63ca05.async.js → src__pages__crontab__index.68e78a33.async.js} +1 -1
  40. package/static/dist/src__pages__dependence__modal.f7bb47ba.async.js +1 -0
  41. package/static/dist/src__pages__env__editNameModal.48715576.async.js +1 -0
  42. package/static/dist/src__pages__env__index.452cdc47.async.js +1 -0
  43. package/static/dist/src__pages__env__modal.e1234834.async.js +1 -0
  44. package/static/dist/src__pages__error__index.dedb2b90.async.js +1 -0
  45. package/static/dist/src__pages__initialization__index.22620a77.async.js +1 -0
  46. package/static/dist/src__pages__script__editNameModal.532a3044.async.js +1 -0
  47. package/static/dist/src__pages__script__index.1fc20f22.async.js +1 -0
  48. package/static/dist/src__pages__script__renameModal.5cdde919.async.js +1 -0
  49. package/static/dist/src__pages__script__saveModal.5ffed35f.async.js +1 -0
  50. package/static/dist/src__pages__script__setting.1a82c6d7.async.js +1 -0
  51. package/static/dist/src__pages__setting__appModal.43b7758a.async.js +1 -0
  52. package/static/dist/src__pages__setting__dependence.9c04c79a.async.js +1 -0
  53. package/static/dist/src__pages__setting__index.7e070838.async.js +1 -0
  54. package/static/dist/src__pages__setting__notification.bac481c2.async.js +1 -0
  55. package/static/dist/src__pages__setting__security.b7235753.async.js +1 -0
  56. package/static/dist/src__pages__setting__systemLog.b81a8c1d.async.js +1 -0
  57. package/static/dist/src__pages__subscription__modal.562448e2.async.js +1 -0
  58. package/static/dist/{umi.025a31b3.js → umi.a54a59f6.js} +1 -1
  59. package/static/dist/1147.c420132e.async.js +0 -1
  60. package/static/dist/2208.f7ba3dfa.async.js +0 -1
  61. package/static/dist/2286.164bb089.async.js +0 -1
  62. package/static/dist/3191.37d489cf.async.js +0 -1
  63. package/static/dist/8826.6879b271.async.js +0 -1
  64. package/static/dist/src__pages__dependence__modal.11124896.async.js +0 -1
  65. package/static/dist/src__pages__env__editNameModal.5d264b25.async.js +0 -1
  66. package/static/dist/src__pages__env__index.9a565d2b.async.js +0 -1
  67. package/static/dist/src__pages__env__modal.7f2ef1bc.async.js +0 -1
  68. package/static/dist/src__pages__error__index.f156b45e.async.js +0 -1
  69. package/static/dist/src__pages__initialization__index.e96d4ba8.async.js +0 -1
  70. package/static/dist/src__pages__script__editNameModal.3e7e100d.async.js +0 -1
  71. package/static/dist/src__pages__script__index.e869812f.async.js +0 -1
  72. package/static/dist/src__pages__script__renameModal.5e987ef5.async.js +0 -1
  73. package/static/dist/src__pages__script__saveModal.3f9d23d6.async.js +0 -1
  74. package/static/dist/src__pages__script__setting.a535793a.async.js +0 -1
  75. package/static/dist/src__pages__setting__appModal.251cd14f.async.js +0 -1
  76. package/static/dist/src__pages__setting__dependence.29f13e8c.async.js +0 -1
  77. package/static/dist/src__pages__setting__index.7a39d415.async.js +0 -1
  78. package/static/dist/src__pages__setting__notification.390fc905.async.js +0 -1
  79. package/static/dist/src__pages__setting__security.598720a8.async.js +0 -1
  80. package/static/dist/src__pages__setting__systemLog.1a5c855f.async.js +0 -1
  81. package/static/dist/src__pages__subscription__modal.3562c670.async.js +0 -1
@@ -12,16 +12,16 @@ const initTask_1 = __importDefault(require("./initTask"));
12
12
  const initFile_1 = __importDefault(require("./initFile"));
13
13
  exports.default = async ({ app }) => {
14
14
  (0, depInjector_1.default)();
15
- logger_1.default.info('✌️ Dependency loaded');
15
+ logger_1.default.info('[boot] Dependency loaded');
16
16
  await (0, deps_1.default)();
17
- logger_1.default.info('✌️ Link deps loaded');
17
+ logger_1.default.info('[boot] Link deps loaded');
18
18
  await (0, initFile_1.default)();
19
- logger_1.default.info('✌️ Init file loaded');
19
+ logger_1.default.info('[boot] Init file loaded');
20
20
  await (0, initData_1.default)();
21
- logger_1.default.info('✌️ Init data loaded');
21
+ logger_1.default.info('[boot] Init data loaded');
22
22
  (0, initTask_1.default)();
23
- logger_1.default.info('✌️ Init task loaded');
23
+ logger_1.default.info('[boot] Init task loaded');
24
24
  (0, express_1.default)({ app });
25
- logger_1.default.info('✌️ Express loaded');
25
+ logger_1.default.info('[boot] Express loaded');
26
26
  };
27
27
  //# sourceMappingURL=app.js.map
@@ -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
@@ -121,12 +121,7 @@ exports.default = ({ app }) => {
121
121
  return next();
122
122
  }
123
123
  const authInfo = (await store_1.shareStore.getAuthInfo()) || {};
124
- let isInitialized = true;
125
- if (Object.keys(authInfo).length === 2 &&
126
- authInfo.username === 'admin' &&
127
- authInfo.password === 'admin') {
128
- isInitialized = false;
129
- }
124
+ let isInitialized = !(0, auth_1.isDefaultAuthInfo)(authInfo);
130
125
  if (isInitialized) {
131
126
  return res.send({ code: 450, message: (0, i18n_1.t)('未知错误') });
132
127
  }
@@ -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) {
@@ -547,7 +590,12 @@ let CronService = class CronService {
547
590
  }
548
591
  async disabled(ids) {
549
592
  await cron_1.CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
550
- 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
+ }
551
599
  await this.setCrontab();
552
600
  }
553
601
  async enabled(ids) {
@@ -565,7 +613,15 @@ let CronService = class CronService {
565
613
  if ((0, util_1.isDemoEnv)()) {
566
614
  return;
567
615
  }
568
- 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
+ }
569
625
  await this.setCrontab();
570
626
  }
571
627
  async log(id) {
@@ -719,8 +775,16 @@ let CronService = class CronService {
719
775
  await (0, utils_1.writeFileWithLock)(config_1.default.crontabFile, '');
720
776
  return;
721
777
  }
722
- await client_1.default.addCron(regularCrons);
723
- 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
+ }
724
788
  }
725
789
  async bootTask() {
726
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;
@@ -403,6 +403,9 @@ let UserService = class UserService {
403
403
  }
404
404
  async resetAuthInfo(info) {
405
405
  const { retries, twoFactorActivated, password, username } = info;
406
+ if (password === 'admin') {
407
+ return { code: 400, message: (0, i18n_1.t)('密码不能设置为admin') };
408
+ }
406
409
  const authInfo = await this.getAuthInfo();
407
410
  const payload = (0, pickBy_1.default)({
408
411
  retries,
@@ -1,6 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isValidToken = void 0;
3
+ exports.isValidToken = exports.isDefaultAuthInfo = void 0;
4
+ function isDefaultAuthInfo(authInfo) {
5
+ return (Object.keys(authInfo).length === 2 &&
6
+ authInfo.username === 'admin' &&
7
+ authInfo.password === 'admin');
8
+ }
9
+ exports.isDefaultAuthInfo = isDefaultAuthInfo;
4
10
  /**
5
11
  * Validates if a token exists in the authentication info.
6
12
  * Supports both legacy string tokens and new TokenInfo array format.
@@ -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;