@whyour/qinglong 2.19.2-1 → 2.20.0-1

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 (109) hide show
  1. package/.env.example +1 -2
  2. package/README-en.md +4 -2
  3. package/README.md +4 -2
  4. package/back/protos/api.proto +17 -0
  5. package/docker/310.Dockerfile +23 -6
  6. package/docker/Dockerfile +22 -6
  7. package/docker/docker-entrypoint.sh +27 -14
  8. package/package.json +8 -9
  9. package/sample/notify.js +18 -2
  10. package/sample/notify.py +15 -0
  11. package/sample/ql_sample.js +28 -0
  12. package/shell/api.sh +8 -48
  13. package/shell/check.sh +5 -22
  14. package/shell/preload/client.js +6 -1
  15. package/shell/pub.sh +4 -4
  16. package/shell/share.sh +32 -55
  17. package/shell/task.sh +19 -10
  18. package/shell/update.sh +1 -0
  19. package/static/build/api/dependence.js +7 -1
  20. package/static/build/api/env.js +30 -4
  21. package/static/build/api/script.js +47 -8
  22. package/static/build/api/subscription.js +3 -3
  23. package/static/build/api/system.js +21 -28
  24. package/static/build/api/user.js +2 -1
  25. package/static/build/app.js +96 -18
  26. package/static/build/config/index.js +2 -2
  27. package/static/build/config/util.js +24 -1
  28. package/static/build/data/cron.js +4 -0
  29. package/static/build/data/env.js +3 -1
  30. package/static/build/data/notify.js +1 -0
  31. package/static/build/loaders/db.js +29 -35
  32. package/static/build/loaders/deps.js +22 -5
  33. package/static/build/loaders/express.js +19 -10
  34. package/static/build/loaders/initData.js +25 -1
  35. package/static/build/loaders/initTask.js +6 -0
  36. package/static/build/loaders/sock.js +10 -12
  37. package/static/build/protos/api.js +336 -1
  38. package/static/build/schedule/addCron.js +2 -2
  39. package/static/build/schedule/api.js +100 -1
  40. package/static/build/schedule/delCron.js +1 -1
  41. package/static/build/schedule/health.js +2 -3
  42. package/static/build/services/cron.js +55 -21
  43. package/static/build/services/dependence.js +6 -5
  44. package/static/build/services/env.js +9 -2
  45. package/static/build/services/notify.js +17 -5
  46. package/static/build/services/schedule.js +4 -4
  47. package/static/build/services/sshKey.js +24 -4
  48. package/static/build/services/subscription.js +11 -8
  49. package/static/build/services/system.js +18 -2
  50. package/static/build/services/user.js +83 -4
  51. package/static/build/shared/auth.js +40 -0
  52. package/static/build/shared/logStreamManager.js +104 -0
  53. package/static/build/shared/runCron.js +23 -0
  54. package/static/build/validation/schedule.js +39 -2
  55. package/static/dist/1147.856bb861.async.js +1 -0
  56. package/static/dist/1379.f91563a1.async.js +1 -0
  57. package/static/dist/{2208.3bc521b1.async.js → 2208.7bf7e296.async.js} +1 -1
  58. package/static/dist/3191.da7f3e07.async.js +1 -0
  59. package/static/dist/5691.931f59c5.async.js +1 -0
  60. package/static/dist/7571.4f6240b1.async.js +1 -0
  61. package/static/dist/{8826.3ab4ad84.async.js → 8826.5f289c4d.async.js} +1 -1
  62. package/static/dist/index.html +2 -2
  63. package/static/dist/preload_helper.17d7028f.js +1 -0
  64. package/static/dist/{src__pages__crontab__detail.64ec3a2b.async.js → src__pages__crontab__detail.b07f0c0a.async.js} +1 -1
  65. package/static/dist/src__pages__crontab__index.6b90d8c5.async.js +1 -0
  66. package/static/dist/{src__pages__crontab__logModal.5fa54b25.async.js → src__pages__crontab__logModal.5e6a4bf2.async.js} +1 -1
  67. package/static/dist/src__pages__crontab__modal.2d3d4953.async.js +1 -0
  68. package/static/dist/src__pages__dependence__modal.86604072.async.js +1 -0
  69. package/static/dist/{src__pages__env__editNameModal.665393cd.async.js → src__pages__env__editNameModal.79b7cf83.async.js} +1 -1
  70. package/static/dist/src__pages__env__index.a0a2fece.async.js +1 -0
  71. package/static/dist/{src__pages__env__modal.168498f9.async.js → src__pages__env__modal.b84c1173.async.js} +1 -1
  72. package/static/dist/{src__pages__error__index.d9beeda3.async.js → src__pages__error__index.01fac00e.async.js} +1 -1
  73. package/static/dist/{src__pages__initialization__index.2403c031.async.js → src__pages__initialization__index.2e49cf43.async.js} +1 -1
  74. package/static/dist/{src__pages__script__editNameModal.e36cd111.async.js → src__pages__script__editNameModal.05441c89.async.js} +1 -1
  75. package/static/dist/{src__pages__script__index.82b42e11.async.js → src__pages__script__index.6a23d7b5.async.js} +1 -1
  76. package/static/dist/{src__pages__script__renameModal.f9756f26.async.js → src__pages__script__renameModal.3bb00014.async.js} +1 -1
  77. package/static/dist/{src__pages__script__saveModal.e885e133.async.js → src__pages__script__saveModal.03cc698b.async.js} +1 -1
  78. package/static/dist/{src__pages__script__setting.8c2727b4.async.js → src__pages__script__setting.5a2a2a2c.async.js} +1 -1
  79. package/static/dist/{src__pages__setting__appModal.5a39121e.async.js → src__pages__setting__appModal.7f763fa7.async.js} +1 -1
  80. package/static/dist/src__pages__setting__checkUpdate.46efe057.async.js +1 -0
  81. package/static/dist/src__pages__setting__dependence.e64c4554.async.js +1 -0
  82. package/static/dist/src__pages__setting__index.3a220288.async.js +1 -0
  83. package/static/dist/src__pages__setting__notification.49003b2f.async.js +1 -0
  84. package/static/dist/src__pages__setting__other.0d931d6f.async.js +1 -0
  85. package/static/dist/src__pages__setting__security.a916e056.async.js +1 -0
  86. package/static/dist/{src__pages__setting__systemLog.fc5bdc78.async.js → src__pages__setting__systemLog.cbb0a3bb.async.js} +1 -1
  87. package/static/dist/src__pages__subscription__modal.ade477c1.async.js +1 -0
  88. package/static/dist/umi.ba9d6227.js +1 -0
  89. package/version.yaml +46 -9
  90. package/docker/front.conf +0 -61
  91. package/docker/nginx.conf +0 -45
  92. package/static/dist/2995.2eb218b3.async.js +0 -1
  93. package/static/dist/3191.cc1e31cd.async.js +0 -1
  94. package/static/dist/4046.7fbcfa02.async.js +0 -1
  95. package/static/dist/5713.8519f547.async.js +0 -1
  96. package/static/dist/8851.503b1e64.async.js +0 -1
  97. package/static/dist/preload_helper.0d173b72.js +0 -1
  98. package/static/dist/src__pages__crontab__index.74e7828f.async.js +0 -1
  99. package/static/dist/src__pages__crontab__modal.21258e08.async.js +0 -1
  100. package/static/dist/src__pages__dependence__modal.6639424a.async.js +0 -1
  101. package/static/dist/src__pages__env__index.70340ba7.async.js +0 -1
  102. package/static/dist/src__pages__setting__checkUpdate.c152dfaf.async.js +0 -1
  103. package/static/dist/src__pages__setting__dependence.a46e873d.async.js +0 -1
  104. package/static/dist/src__pages__setting__index.32b260ce.async.js +0 -1
  105. package/static/dist/src__pages__setting__notification.299f6b96.async.js +0 -1
  106. package/static/dist/src__pages__setting__other.8b77afcd.async.js +0 -1
  107. package/static/dist/src__pages__setting__security.e7371daa.async.js +0 -1
  108. package/static/dist/src__pages__subscription__modal.a7fd6a3c.async.js +0 -1
  109. package/static/dist/umi.35a646c6.js +0 -1
@@ -21,7 +21,7 @@ const config_1 = __importDefault(require("../config"));
21
21
  const cron_1 = require("../data/cron");
22
22
  const child_process_1 = require("child_process");
23
23
  const promises_1 = __importDefault(require("fs/promises"));
24
- const cron_parser_1 = __importDefault(require("cron-parser"));
24
+ const cron_parser_1 = require("cron-parser");
25
25
  const util_1 = require("../config/util");
26
26
  const sequelize_1 = require("sequelize");
27
27
  const path_1 = __importDefault(require("path"));
@@ -34,6 +34,7 @@ const pickBy_1 = __importDefault(require("lodash/pickBy"));
34
34
  const omit_1 = __importDefault(require("lodash/omit"));
35
35
  const utils_1 = require("../shared/utils");
36
36
  const schedule_1 = require("../interface/schedule");
37
+ const logStreamManager_1 = require("../shared/logStreamManager");
37
38
  let CronService = class CronService {
38
39
  constructor(logger) {
39
40
  this.logger = logger;
@@ -54,9 +55,28 @@ let CronService = class CronService {
54
55
  isSpecialSchedule(schedule) {
55
56
  return this.isOnceSchedule(schedule) || this.isBootSchedule(schedule);
56
57
  }
58
+ async getLogName(cron) {
59
+ const { log_name, command, id } = cron;
60
+ if (log_name === '/dev/null') {
61
+ return log_name;
62
+ }
63
+ let uniqPath = await (0, util_1.getUniqPath)(command, `${id}`);
64
+ if (log_name) {
65
+ const normalizedLogName = log_name.startsWith('/')
66
+ ? log_name
67
+ : path_1.default.join(config_1.default.logPath, log_name);
68
+ if (normalizedLogName.startsWith(config_1.default.logPath)) {
69
+ uniqPath = log_name;
70
+ }
71
+ }
72
+ const logDirPath = path_1.default.resolve(config_1.default.logPath, `${uniqPath}`);
73
+ await promises_1.default.mkdir(logDirPath, { recursive: true });
74
+ return uniqPath;
75
+ }
57
76
  async create(payload) {
58
77
  const tab = new cron_1.Crontab(payload);
59
78
  tab.saved = false;
79
+ tab.log_name = await this.getLogName(tab);
60
80
  const doc = await this.insert(tab);
61
81
  if ((0, util_1.isDemoEnv)()) {
62
82
  return doc;
@@ -82,6 +102,7 @@ let CronService = class CronService {
82
102
  const doc = await this.getDb({ id: payload.id });
83
103
  const tab = new cron_1.Crontab(Object.assign(Object.assign({}, doc), payload));
84
104
  tab.saved = false;
105
+ tab.log_name = await this.getLogName(tab);
85
106
  const newDoc = await this.updateDb(tab);
86
107
  if (doc.isDisabled === 1 || (0, util_1.isDemoEnv)()) {
87
108
  return newDoc;
@@ -384,13 +405,14 @@ let CronService = class CronService {
384
405
  async stop(ids) {
385
406
  const docs = await cron_1.CrontabModel.findAll({ where: { id: ids } });
386
407
  for (const doc of docs) {
387
- if (doc.pid) {
388
- try {
389
- await (0, util_1.killTask)(doc.pid);
390
- }
391
- catch (error) {
392
- this.logger.error(error);
393
- }
408
+ // Kill all running instances of this task
409
+ try {
410
+ const command = this.makeCommand(doc);
411
+ await (0, util_1.killAllTasks)(command);
412
+ this.logger.info(`[panel][停止所有运行中的任务实例] 任务ID: ${doc.id}, 命令: ${command}`);
413
+ }
414
+ catch (error) {
415
+ this.logger.error(`[panel][停止任务失败] 任务ID: ${doc.id}, 错误: ${error}`);
394
416
  }
395
417
  }
396
418
  await cron_1.CrontabModel.update({ status: cron_1.CrontabStatus.idle, pid: undefined }, { where: { id: ids } });
@@ -398,7 +420,6 @@ let CronService = class CronService {
398
420
  async runSingle(cronId) {
399
421
  return pLimit_1.default.manualRunWithCronLimit(() => {
400
422
  return new Promise(async (resolve) => {
401
- var _a;
402
423
  const cron = await this.getDb({ id: cronId });
403
424
  const params = {
404
425
  name: cron.name,
@@ -411,30 +432,32 @@ let CronService = class CronService {
411
432
  return;
412
433
  }
413
434
  this.logger.info(`[panel][开始执行任务] 参数: ${JSON.stringify(params)}`);
414
- let { id, command, log_path } = cron;
415
- const uniqPath = await (0, util_1.getUniqPath)(command, `${id}`);
435
+ let { id, command, log_name } = cron;
436
+ const uniqPath = log_name === '/dev/null' || !log_name
437
+ ? await (0, util_1.getUniqPath)(command, `${id}`)
438
+ : log_name;
416
439
  const logTime = (0, dayjs_1.default)().format('YYYY-MM-DD-HH-mm-ss-SSS');
417
440
  const logDirPath = path_1.default.resolve(config_1.default.logPath, `${uniqPath}`);
418
- if ((_a = log_path === null || log_path === void 0 ? void 0 : log_path.split('/')) === null || _a === void 0 ? void 0 : _a.every((x) => x !== uniqPath)) {
419
- await promises_1.default.mkdir(logDirPath, { recursive: true });
420
- }
441
+ await promises_1.default.mkdir(logDirPath, { recursive: true });
421
442
  const logPath = `${uniqPath}/${logTime}.log`;
422
443
  const absolutePath = path_1.default.resolve(config_1.default.logPath, `${logPath}`);
423
444
  const cp = (0, cross_spawn_1.spawn)(`real_log_path=${logPath} no_delay=true ${this.makeCommand(cron, true)}`, { shell: '/bin/bash' });
424
445
  await cron_1.CrontabModel.update({ status: cron_1.CrontabStatus.running, pid: cp.pid, log_path: logPath }, { where: { id } });
425
446
  cp.stdout.on('data', async (data) => {
426
- await promises_1.default.appendFile(absolutePath, data.toString());
447
+ await logStreamManager_1.logStreamManager.write(absolutePath, data.toString());
427
448
  });
428
449
  cp.stderr.on('data', async (data) => {
429
450
  this.logger.info('[panel][执行任务失败] 命令: %s, 错误信息: %j', command, data.toString());
430
- await promises_1.default.appendFile(absolutePath, data.toString());
451
+ await logStreamManager_1.logStreamManager.write(absolutePath, data.toString());
431
452
  });
432
453
  cp.on('error', async (err) => {
433
454
  this.logger.error('[panel][创建任务失败] 命令: %s, 错误信息: %j', command, err);
434
- await promises_1.default.appendFile(absolutePath, JSON.stringify(err));
455
+ await logStreamManager_1.logStreamManager.write(absolutePath, JSON.stringify(err));
435
456
  });
436
457
  cp.on('exit', async (code) => {
437
458
  this.logger.info('[panel][执行任务结束] 参数: %s, 退出码: %j', JSON.stringify(params), code);
459
+ // Close the stream after task completion
460
+ await logStreamManager_1.logStreamManager.closeStream(absolutePath);
438
461
  await cron_1.CrontabModel.update({ status: cron_1.CrontabStatus.idle, pid: undefined }, { where: { id } });
439
462
  resolve(Object.assign(Object.assign({}, params), { pid: cp.pid, code }));
440
463
  });
@@ -467,6 +490,9 @@ let CronService = class CronService {
467
490
  if (!doc) {
468
491
  return '';
469
492
  }
493
+ if (doc.log_name === '/dev/null') {
494
+ return '日志设置为忽略';
495
+ }
470
496
  const absolutePath = path_1.default.resolve(config_1.default.logPath, `${doc.log_path}`);
471
497
  const logFileExist = doc.log_path && (await (0, util_1.fileExist)(absolutePath));
472
498
  if (logFileExist) {
@@ -476,7 +502,7 @@ let CronService = class CronService {
476
502
  return typeof doc.status === 'number' &&
477
503
  [cron_1.CrontabStatus.queued, cron_1.CrontabStatus.running].includes(doc.status)
478
504
  ? '运行中...'
479
- : '任务空闲中';
505
+ : '日志不存在...';
480
506
  }
481
507
  }
482
508
  async logs(id) {
@@ -492,7 +518,7 @@ let CronService = class CronService {
492
518
  return (await Promise.all(files.map(async (x) => ({
493
519
  filename: x,
494
520
  directory: relativeDir.replace(config_1.default.logPath, ''),
495
- time: (await promises_1.default.lstat(`${dir}/${x}`)).mtime.getTime(),
521
+ time: (await promises_1.default.lstat(`${dir}/${x}`)).birthtimeMs,
496
522
  })))).sort((a, b) => b.time - a.time);
497
523
  }
498
524
  else {
@@ -505,6 +531,10 @@ let CronService = class CronService {
505
531
  command = `${const_1.TASK_PREFIX}${tab.command}`;
506
532
  }
507
533
  let commandVariable = `real_time=${Boolean(realTime)} no_tee=true ID=${tab.id} `;
534
+ // Only include log_name if it has a truthy value to avoid passing null/undefined to shell
535
+ if (tab.log_name) {
536
+ commandVariable += `log_name=${tab.log_name} `;
537
+ }
508
538
  if (tab.task_before) {
509
539
  commandVariable += `task_before='${tab.task_before
510
540
  .replace(/'/g, "'\\''")
@@ -544,7 +574,11 @@ let CronService = class CronService {
544
574
  await cron_1.CrontabModel.update({ saved: true }, { where: {} });
545
575
  }
546
576
  importCrontab() {
547
- (0, child_process_1.exec)('crontab -l', (error, stdout, stderr) => {
577
+ (0, child_process_1.exec)('crontab -l', (error, stdout) => {
578
+ if (error) {
579
+ const errorMsg = error.message || String(error);
580
+ this.logger.error('[crontab] Failed to read system crontab:', errorMsg);
581
+ }
548
582
  const lines = stdout.split('\n');
549
583
  const namePrefix = new Date().getTime();
550
584
  lines.reverse().forEach(async (line, index) => {
@@ -554,7 +588,7 @@ let CronService = class CronService {
554
588
  const schedule = line.replace(command, '').trim();
555
589
  if (command &&
556
590
  schedule &&
557
- cron_parser_1.default.parseExpression(schedule).hasNext()) {
591
+ cron_parser_1.CronExpressionParser.parse(schedule).hasNext()) {
558
592
  const name = namePrefix + '_' + index;
559
593
  const _crontab = await cron_1.CrontabModel.findOne({
560
594
  where: { command, schedule },
@@ -86,23 +86,24 @@ let DependenceService = class DependenceService {
86
86
  await dependence_1.DependenceModel.destroy({ where: { id: ids } });
87
87
  }
88
88
  async dependencies({ searchValue, type, status, }, sort = [], query = {}) {
89
- let condition = Object.assign(Object.assign({}, query), { type: dependence_1.DependenceTypes[type] });
89
+ let condition = query;
90
+ if (dependence_1.DependenceTypes[type]) {
91
+ condition.type = dependence_1.DependenceTypes[type];
92
+ }
90
93
  if (status) {
91
94
  condition.status = status.split(',').map(Number);
92
95
  }
93
96
  if (searchValue) {
94
97
  const encodeText = encodeURI(searchValue);
95
- const reg = {
98
+ condition.name = {
96
99
  [sequelize_1.Op.or]: [
97
100
  { [sequelize_1.Op.like]: `%${searchValue}%` },
98
101
  { [sequelize_1.Op.like]: `%${encodeText}%` },
99
102
  ],
100
103
  };
101
- condition = Object.assign(Object.assign({}, condition), { name: reg });
102
104
  }
103
105
  try {
104
- const result = await this.find(condition, sort);
105
- return result;
106
+ return await this.find(condition, sort);
106
107
  }
107
108
  catch (error) {
108
109
  throw error;
@@ -15,12 +15,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
15
15
  return (mod && mod.__esModule) ? mod : { "default": mod };
16
16
  };
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
+ const groupBy_1 = __importDefault(require("lodash/groupBy"));
19
+ const sequelize_1 = require("sequelize");
18
20
  const typedi_1 = require("typedi");
19
21
  const winston_1 = __importDefault(require("winston"));
20
22
  const config_1 = __importDefault(require("../config"));
21
23
  const env_1 = require("../data/env");
22
- const groupBy_1 = __importDefault(require("lodash/groupBy"));
23
- const sequelize_1 = require("sequelize");
24
24
  const utils_1 = require("../shared/utils");
25
25
  let EnvService = class EnvService {
26
26
  constructor(logger) {
@@ -128,6 +128,7 @@ let EnvService = class EnvService {
128
128
  }
129
129
  try {
130
130
  const result = await this.find(condition, [
131
+ ['isPinned', 'DESC'],
131
132
  ['position', 'DESC'],
132
133
  ['createdAt', 'ASC'],
133
134
  ]);
@@ -163,6 +164,12 @@ let EnvService = class EnvService {
163
164
  await env_1.EnvModel.update({ name }, { where: { id: ids } });
164
165
  await this.set_envs();
165
166
  }
167
+ async pin(ids) {
168
+ await env_1.EnvModel.update({ isPinned: 1 }, { where: { id: ids } });
169
+ }
170
+ async unPin(ids) {
171
+ await env_1.EnvModel.update({ isPinned: 0 }, { where: { id: ids } });
172
+ }
166
173
  async set_envs() {
167
174
  const envs = await this.envs('', {
168
175
  name: { [sequelize_1.Op.not]: null },
@@ -454,15 +454,27 @@ let NotificationService = class NotificationService {
454
454
  }
455
455
  }
456
456
  async lark() {
457
- let { larkKey } = this.params;
457
+ let { larkKey, larkSecret } = this.params;
458
458
  if (!larkKey.startsWith('http')) {
459
459
  larkKey = `https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`;
460
460
  }
461
+ const body = {
462
+ msg_type: 'text',
463
+ content: { text: `${this.title}\n\n${this.content}` },
464
+ };
465
+ // Add signature if secret is provided
466
+ // Note: Feishu's signature algorithm uses timestamp+"\n"+secret as the HMAC key
467
+ // and signs an empty message, which differs from typical HMAC usage
468
+ if (larkSecret) {
469
+ const timestamp = Math.floor(Date.now() / 1000).toString();
470
+ const stringToSign = `${timestamp}\n${larkSecret}`;
471
+ const hmac = crypto_1.default.createHmac('sha256', stringToSign);
472
+ const sign = hmac.digest('base64');
473
+ body.timestamp = timestamp;
474
+ body.sign = sign;
475
+ }
461
476
  try {
462
- const res = await http_1.httpClient.post(larkKey, Object.assign(Object.assign({}, this.gotOption), { json: {
463
- msg_type: 'text',
464
- content: { text: `${this.title}\n\n${this.content}` },
465
- }, headers: { 'Content-Type': 'application/json' } }));
477
+ const res = await http_1.httpClient.post(larkKey, Object.assign(Object.assign({}, this.gotOption), { json: body, headers: { 'Content-Type': 'application/json' } }));
466
478
  if (res.StatusCode === 0 || res.code === 0) {
467
479
  return true;
468
480
  }
@@ -87,7 +87,7 @@ let ScheduleService = class ScheduleService {
87
87
  }
88
88
  async createCronTask({ id = 0, command, name, schedule = '', runOrigin }, callbacks, runImmediately = false) {
89
89
  const _id = this.formatId(id);
90
- this.logger.info('[panel][创建cron任务], 任务ID: %s, cron: %s, 任务名: %s, 执行命令: %s', _id, schedule, name, command);
90
+ this.logger.info('[panel][创建cron任务] 任务ID: %s, cron: %s, 任务名: %s, 执行命令: %s', _id, schedule, name, command);
91
91
  this.scheduleStacks.set(_id, node_schedule_1.default.scheduleJob(_id, schedule, async () => {
92
92
  this.runTask(command, callbacks, {
93
93
  name,
@@ -110,7 +110,7 @@ let ScheduleService = class ScheduleService {
110
110
  async cancelCronTask({ id = 0, name }) {
111
111
  var _a;
112
112
  const _id = this.formatId(id);
113
- this.logger.info('[panel][取消定时任务], 任务名: %s', name);
113
+ this.logger.info('[panel][取消定时任务] 任务名: %s', name);
114
114
  if (this.scheduleStacks.has(_id)) {
115
115
  (_a = this.scheduleStacks.get(_id)) === null || _a === void 0 ? void 0 : _a.cancel();
116
116
  this.scheduleStacks.delete(_id);
@@ -118,7 +118,7 @@ let ScheduleService = class ScheduleService {
118
118
  }
119
119
  async createIntervalTask({ id = 0, command, name = '', runOrigin }, schedule, runImmediately = true, callbacks) {
120
120
  const _id = this.formatId(id);
121
- this.logger.info('[panel][创建interval任务], 任务ID: %s, 任务名: %s, 执行命令: %s', _id, name, command);
121
+ this.logger.info('[panel][创建interval任务] 任务ID: %s, 任务名: %s, 执行命令: %s', _id, name, command);
122
122
  const task = new toad_scheduler_1.Task(name, () => {
123
123
  this.runTask(command, callbacks, {
124
124
  name,
@@ -142,7 +142,7 @@ let ScheduleService = class ScheduleService {
142
142
  }
143
143
  async cancelIntervalTask({ id = 0, name }) {
144
144
  const _id = this.formatId(id);
145
- this.logger.info('[panel][取消interval任务], 任务ID: %s, 任务名: %s', _id, name);
145
+ this.logger.info('[panel][取消interval任务] 任务ID: %s, 任务名: %s', _id, name);
146
146
  this.intervalSchedule.removeById(_id);
147
147
  }
148
148
  formatId(id) {
@@ -40,16 +40,15 @@ let SshKeyService = class SshKeyService {
40
40
  config = await promises_1.default.readFile(this.sshConfigFilePath, { encoding: 'utf-8' });
41
41
  }
42
42
  else {
43
- await (0, utils_1.writeFileWithLock)(this.sshConfigFilePath, '');
43
+ await (0, utils_1.writeFileWithLock)(this.sshConfigFilePath, '', { mode: '600' });
44
44
  }
45
45
  if (!config.includes(this.sshConfigHeader)) {
46
- await (0, utils_1.writeFileWithLock)(this.sshConfigFilePath, `${this.sshConfigHeader}\n\n${config}`);
46
+ await (0, utils_1.writeFileWithLock)(this.sshConfigFilePath, `${this.sshConfigHeader}\n\n${config}`, { mode: '600' });
47
47
  }
48
48
  }
49
49
  async generatePrivateKeyFile(alias, key) {
50
50
  try {
51
51
  await (0, utils_1.writeFileWithLock)(path_1.default.join(this.sshPath, alias), `${key}${os_1.default.EOL}`, {
52
- encoding: 'utf8',
53
52
  mode: '400',
54
53
  });
55
54
  }
@@ -74,7 +73,10 @@ let SshKeyService = class SshKeyService {
74
73
  ? ` ProxyCommand nc -v -x ${proxy} %h %p 2>/dev/null\n`
75
74
  : '';
76
75
  const config = `Host ${alias}\n Hostname ${host}\n IdentityFile ${path_1.default.join(this.sshPath, alias)}\n StrictHostKeyChecking no\n${proxyStr}`;
77
- await (0, utils_1.writeFileWithLock)(`${path_1.default.join(this.sshPath, `${alias}.config`)}`, config);
76
+ await (0, utils_1.writeFileWithLock)(`${path_1.default.join(this.sshPath, `${alias}.config`)}`, config, {
77
+ encoding: 'utf8',
78
+ mode: '600',
79
+ });
78
80
  }
79
81
  async removeSshConfig(alias) {
80
82
  try {
@@ -105,6 +107,24 @@ let SshKeyService = class SshKeyService {
105
107
  }
106
108
  }
107
109
  }
110
+ async addGlobalSSHKey(key, alias) {
111
+ await this.generatePrivateKeyFile(`~global_${alias}`, key);
112
+ // Create a global SSH config entry that matches all hosts
113
+ // This allows the key to be used for any Git repository
114
+ await this.generateGlobalSshConfig(`~global_${alias}`);
115
+ }
116
+ async removeGlobalSSHKey(alias) {
117
+ await this.removePrivateKeyFile(`~global_${alias}`);
118
+ await this.removeSshConfig(`~global_${alias}`);
119
+ }
120
+ async generateGlobalSshConfig(alias) {
121
+ // Create a config that matches all hosts, making this key globally available
122
+ const config = `Host *\n IdentityFile ${path_1.default.join(this.sshPath, alias)}\n StrictHostKeyChecking no\n`;
123
+ await (0, utils_1.writeFileWithLock)(`${path_1.default.join(this.sshPath, `${alias}.config`)}`, config, {
124
+ encoding: 'utf8',
125
+ mode: '600',
126
+ });
127
+ }
108
128
  };
109
129
  SshKeyService = __decorate([
110
130
  (0, typedi_1.Service)(),
@@ -53,6 +53,7 @@ const const_1 = require("../config/const");
53
53
  const subscription_2 = require("../config/subscription");
54
54
  const cron_1 = require("../data/cron");
55
55
  const cron_2 = __importDefault(require("./cron"));
56
+ const logStreamManager_1 = require("../shared/logStreamManager");
56
57
  let SubscriptionService = class SubscriptionService {
57
58
  constructor(logger, scheduleService, sockService, sshKeyService, crontabService) {
58
59
  this.logger = logger;
@@ -129,7 +130,7 @@ let SubscriptionService = class SubscriptionService {
129
130
  let beforeStr = '';
130
131
  try {
131
132
  if (doc.sub_before) {
132
- await promises_1.default.appendFile(absolutePath, `\n## 执行before命令...\n\n`);
133
+ await logStreamManager_1.logStreamManager.write(absolutePath, `\n## 执行before命令...\n\n`);
133
134
  beforeStr = await (0, util_1.promiseExec)(doc.sub_before);
134
135
  }
135
136
  }
@@ -138,7 +139,7 @@ let SubscriptionService = class SubscriptionService {
138
139
  (error.stderr && error.stderr.toString()) || JSON.stringify(error);
139
140
  }
140
141
  if (beforeStr) {
141
- await promises_1.default.appendFile(absolutePath, `${beforeStr}\n`);
142
+ await logStreamManager_1.logStreamManager.write(absolutePath, `${beforeStr}\n`);
142
143
  }
143
144
  },
144
145
  onStart: async (cp, startTime) => {
@@ -153,7 +154,7 @@ let SubscriptionService = class SubscriptionService {
153
154
  let afterStr = '';
154
155
  try {
155
156
  if (sub.sub_after) {
156
- await promises_1.default.appendFile(absolutePath, `\n\n## 执行after命令...\n\n`);
157
+ await logStreamManager_1.logStreamManager.write(absolutePath, `\n\n## 执行after命令...\n\n`);
157
158
  afterStr = await (0, util_1.promiseExec)(sub.sub_after);
158
159
  }
159
160
  }
@@ -162,9 +163,11 @@ let SubscriptionService = class SubscriptionService {
162
163
  (error.stderr && error.stderr.toString()) || JSON.stringify(error);
163
164
  }
164
165
  if (afterStr) {
165
- await promises_1.default.appendFile(absolutePath, `${afterStr}\n`);
166
+ await logStreamManager_1.logStreamManager.write(absolutePath, `${afterStr}\n`);
166
167
  }
167
- await promises_1.default.appendFile(absolutePath, `\n## 执行结束... ${endTime.format('YYYY-MM-DD HH:mm:ss')} 耗时 ${diff} 秒${const_1.LOG_END_SYMBOL}`);
168
+ await logStreamManager_1.logStreamManager.write(absolutePath, `\n## 执行结束... ${endTime.format('YYYY-MM-DD HH:mm:ss')} 耗时 ${diff} 秒${const_1.LOG_END_SYMBOL}`);
169
+ // Close the stream after task completion
170
+ await logStreamManager_1.logStreamManager.closeStream(absolutePath);
168
171
  await subscription_1.SubscriptionModel.update({ status: subscription_1.SubscriptionStatus.idle, pid: undefined }, { where: { id: sub.id } });
169
172
  this.sockService.sendMessage({
170
173
  type: 'runSubscriptionEnd',
@@ -175,12 +178,12 @@ let SubscriptionService = class SubscriptionService {
175
178
  onError: async (message) => {
176
179
  const sub = await this.getDb({ id: doc.id });
177
180
  const absolutePath = await (0, util_1.handleLogPath)(sub.log_path);
178
- await promises_1.default.appendFile(absolutePath, `\n${message}`);
181
+ await logStreamManager_1.logStreamManager.write(absolutePath, `\n${message}`);
179
182
  },
180
183
  onLog: async (message) => {
181
184
  const sub = await this.getDb({ id: doc.id });
182
185
  const absolutePath = await (0, util_1.handleLogPath)(sub.log_path);
183
- await promises_1.default.appendFile(absolutePath, `\n${message}`);
186
+ await logStreamManager_1.logStreamManager.write(absolutePath, `\n${message}`);
184
187
  },
185
188
  };
186
189
  }
@@ -317,7 +320,7 @@ let SubscriptionService = class SubscriptionService {
317
320
  return (await Promise.all(files.map(async (x) => ({
318
321
  filename: x,
319
322
  directory: relativeDir.replace(config_1.default.logPath, ''),
320
- time: (await promises_1.default.lstat(`${dir}/${x}`)).mtime.getTime(),
323
+ time: (await promises_1.default.lstat(`${dir}/${x}`)).birthtimeMs,
321
324
  })))).sort((a, b) => b.time - a.time);
322
325
  }
323
326
  }
@@ -308,11 +308,12 @@ let SystemService = class SystemService {
308
308
  return { code: 400, message: '通知发送失败,请检查系统设置/通知配置' };
309
309
  }
310
310
  }
311
- async run({ command }, callback) {
311
+ async run({ command, logPath }, callback) {
312
312
  if (!command.startsWith(const_1.TASK_COMMAND)) {
313
313
  command = `${const_1.TASK_COMMAND} ${command}`;
314
314
  }
315
- this.scheduleService.runTask(`real_time=true ${command}`, callback, {
315
+ const logPathPrefix = logPath ? `real_log_path=${logPath}` : '';
316
+ this.scheduleService.runTask(`${logPathPrefix} real_time=true ${command}`, callback, {
316
317
  command,
317
318
  id: command.replace(/ /g, '-'),
318
319
  runOrigin: 'system',
@@ -413,6 +414,21 @@ let SystemService = class SystemService {
413
414
  return { code: 400, message: '设置时区失败' };
414
415
  }
415
416
  }
417
+ async updateGlobalSshKey(info) {
418
+ const oDoc = await this.getSystemConfig();
419
+ const result = await this.updateAuthDb(Object.assign(Object.assign({}, oDoc), { info: Object.assign(Object.assign({}, oDoc.info), info) }));
420
+ // Apply the global SSH key
421
+ const SshKeyService = require('./sshKey').default;
422
+ const Container = require('typedi').Container;
423
+ const sshKeyService = Container.get(SshKeyService);
424
+ if (info.globalSshKey) {
425
+ await sshKeyService.addGlobalSSHKey(info.globalSshKey, 'global');
426
+ }
427
+ else {
428
+ await sshKeyService.removeGlobalSSHKey('global');
429
+ }
430
+ return { code: 200, data: result };
431
+ }
416
432
  async cleanDependence(type) {
417
433
  if (!type || !['node', 'python3'].includes(type)) {
418
434
  return { code: 400, message: '参数错误' };
@@ -79,9 +79,17 @@ let UserService = class UserService {
79
79
  expiresIn: config_1.default.jwt.expiresIn || expiration,
80
80
  algorithm: 'HS384',
81
81
  });
82
+ const tokenInfo = {
83
+ value: token,
84
+ timestamp,
85
+ ip,
86
+ address,
87
+ platform: req.platform,
88
+ };
89
+ const updatedTokens = this.addTokenToList(tokens, req.platform, tokenInfo);
82
90
  await this.updateAuthInfo(content, {
83
91
  token,
84
- tokens: Object.assign(Object.assign({}, tokens), { [req.platform]: token }),
92
+ tokens: updatedTokens,
85
93
  lastlogon: timestamp,
86
94
  retries: 0,
87
95
  lastip: ip,
@@ -146,11 +154,23 @@ let UserService = class UserService {
146
154
  }
147
155
  }
148
156
  }
149
- async logout(platform) {
157
+ async logout(platform, tokenValue) {
158
+ if (!platform || !tokenValue) {
159
+ this.logger.warn('Invalid logout parameters - empty platform or token');
160
+ return;
161
+ }
150
162
  const authInfo = await this.getAuthInfo();
163
+ // Verify the token exists before attempting to remove it
164
+ const tokenExists = this.findTokenInList(authInfo.tokens, platform, tokenValue);
165
+ if (!tokenExists && authInfo.token !== tokenValue) {
166
+ // Token not found, but don't throw error - user may have already logged out
167
+ this.logger.info(`Logout attempted for non-existent token on platform: ${platform}`);
168
+ return;
169
+ }
170
+ const updatedTokens = this.removeTokenFromList(authInfo.tokens, platform, tokenValue);
151
171
  await this.updateAuthInfo(authInfo, {
152
- token: '',
153
- tokens: Object.assign(Object.assign({}, authInfo.tokens), { [platform]: '' }),
172
+ token: authInfo.token === tokenValue ? '' : authInfo.token,
173
+ tokens: updatedTokens,
154
174
  });
155
175
  }
156
176
  async getLoginLog() {
@@ -299,6 +319,65 @@ let UserService = class UserService {
299
319
  return { code: 400, message: '通知发送失败,请检查参数' };
300
320
  }
301
321
  }
322
+ normalizeTokens(tokens) {
323
+ const normalized = {};
324
+ for (const [platform, value] of Object.entries(tokens)) {
325
+ if (typeof value === 'string') {
326
+ // Legacy format: convert string token to TokenInfo array
327
+ if (value) {
328
+ normalized[platform] = [
329
+ {
330
+ value,
331
+ timestamp: Date.now(),
332
+ ip: '',
333
+ address: '',
334
+ platform,
335
+ },
336
+ ];
337
+ }
338
+ else {
339
+ normalized[platform] = [];
340
+ }
341
+ }
342
+ else {
343
+ // Already in new format
344
+ normalized[platform] = value || [];
345
+ }
346
+ }
347
+ return normalized;
348
+ }
349
+ addTokenToList(tokens, platform, tokenInfo, maxTokensPerPlatform = config_1.default.maxTokensPerPlatform) {
350
+ // Validate maxTokensPerPlatform parameter
351
+ if (!Number.isInteger(maxTokensPerPlatform) || maxTokensPerPlatform < 1) {
352
+ this.logger.warn(`Invalid maxTokensPerPlatform value: ${maxTokensPerPlatform}, using default`);
353
+ maxTokensPerPlatform = config_1.default.maxTokensPerPlatform;
354
+ }
355
+ const normalized = this.normalizeTokens(tokens);
356
+ if (!normalized[platform]) {
357
+ normalized[platform] = [];
358
+ }
359
+ // Add new token
360
+ normalized[platform].unshift(tokenInfo);
361
+ // Limit the number of active tokens per platform
362
+ if (normalized[platform].length > maxTokensPerPlatform) {
363
+ normalized[platform] = normalized[platform].slice(0, maxTokensPerPlatform);
364
+ }
365
+ return normalized;
366
+ }
367
+ removeTokenFromList(tokens, platform, tokenValue) {
368
+ const normalized = this.normalizeTokens(tokens);
369
+ if (normalized[platform]) {
370
+ normalized[platform] = normalized[platform].filter((t) => t.value !== tokenValue);
371
+ }
372
+ return normalized;
373
+ }
374
+ findTokenInList(tokens, platform, tokenValue) {
375
+ const normalized = this.normalizeTokens(tokens);
376
+ if (normalized[platform]) {
377
+ return normalized[platform].find((t) => t.value === tokenValue);
378
+ }
379
+ return undefined;
380
+ }
302
381
  async resetAuthInfo(info) {
303
382
  const { retries, twoFactorActivated, password, username } = info;
304
383
  const authInfo = await this.getAuthInfo();
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isValidToken = void 0;
4
+ /**
5
+ * Validates if a token exists in the authentication info.
6
+ * Supports both legacy string tokens and new TokenInfo array format.
7
+ *
8
+ * @param authInfo - The authentication information
9
+ * @param headerToken - The token to validate
10
+ * @param platform - The platform (desktop, mobile)
11
+ * @returns true if the token is valid, false otherwise
12
+ */
13
+ function isValidToken(authInfo, headerToken, platform) {
14
+ if (!authInfo || !headerToken) {
15
+ return false;
16
+ }
17
+ const { token = '', tokens = {} } = authInfo;
18
+ // Check legacy token field
19
+ if (headerToken === token) {
20
+ return true;
21
+ }
22
+ // Check platform-specific tokens (support both legacy string and new TokenInfo[] format)
23
+ const platformTokens = tokens[platform];
24
+ // Handle null/undefined platformTokens
25
+ if (platformTokens === null || platformTokens === undefined) {
26
+ return false;
27
+ }
28
+ if (typeof platformTokens === 'string') {
29
+ // Legacy format: single string token
30
+ return headerToken === platformTokens;
31
+ }
32
+ else if (Array.isArray(platformTokens)) {
33
+ // New format: array of TokenInfo objects
34
+ return platformTokens.some((t) => t && t.value === headerToken);
35
+ }
36
+ // Unexpected type - log warning and reject
37
+ return false;
38
+ }
39
+ exports.isValidToken = isValidToken;
40
+ //# sourceMappingURL=auth.js.map