Compare commits

..

6 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 0bbff927b1 Address code review feedback
- Wrapped status updates in try-catch blocks to handle database errors
- Moved CrontabModel import to top of pLimit.ts to avoid repeated dynamic imports

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-30 16:43:16 +00:00
copilot-swe-agent[bot] 58eb9feec0 Fix scheduled task instance mode issues
- Fixed runCron to properly handle allow_multiple_instances setting
- Updated default behavior to single-instance mode for backward compatibility
- Added status updates (running/idle) to track task execution
- Fixed queue limit logic to respect instance mode settings
- For single-instance mode: allow up to 2 queued tasks to handle task killing
- For multi-instance mode: increased queue limit to 10 tasks

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-30 16:41:28 +00:00
copilot-swe-agent[bot] 802ca93a3d Initial plan 2025-12-30 16:33:54 +00:00
whyour d53437d169 更新 2.20.1 2025-12-26 21:17:30 +08:00
whyour d526602d19 修复运行中任务停止操作 2025-12-26 01:07:08 +08:00
whyour 91b44914f6 修复环境变量排序 2025-12-26 00:41:32 +08:00
6 changed files with 75 additions and 61 deletions
+1 -5
View File
@@ -123,11 +123,7 @@ export default ({ app }: { app: Application }) => {
app.use(rewrite('/open/*', '/api/$1'));
app.use(config.api.prefix, routes());
app.get('*', (req, res, next) => {
// Don't serve index.html for API routes
if (req.path.startsWith('/api/')) {
return next();
}
app.get('*', (_, res, next) => {
const indexPath = path.join(frontendPath, 'index.html');
res.sendFile(indexPath, (err) => {
if (err) {
+6 -3
View File
@@ -29,7 +29,7 @@ import { logStreamManager } from '../shared/logStreamManager';
@Service()
export default class CronService {
constructor(@Inject('logger') private logger: winston.Logger) {}
constructor(@Inject('logger') private logger: winston.Logger) { }
private isNodeCron(cron: Crontab) {
const { schedule, extra_schedules } = cron;
@@ -165,7 +165,7 @@ export default class CronService {
let cron;
try {
cron = await this.getDb({ id });
} catch (err) {}
} catch (err) { }
if (!cron) {
continue;
}
@@ -467,7 +467,10 @@ export default class CronService {
for (const doc of docs) {
// Kill all running instances of this task
try {
const command = this.makeCommand(doc);
if (doc.pid) {
await killTask(doc.pid);
}
const command = doc.command.replace(/\s+/g, ' ').trim();
await killAllTasks(command);
this.logger.info(
`[panel][停止所有运行中的任务实例] 任务ID: ${doc.id}, 命令: ${command}`,
+3 -2
View File
@@ -13,10 +13,11 @@ import {
stepPosition,
} from '../data/env';
import { writeFileWithLock } from '../shared/utils';
import { sequelize } from '../data';
@Service()
export default class EnvService {
constructor(@Inject('logger') private logger: winston.Logger) {}
constructor(@Inject('logger') private logger: winston.Logger) { }
public async create(payloads: Env[]): Promise<Env[]> {
const envs = await this.envs();
@@ -146,7 +147,7 @@ export default class EnvService {
}
try {
const result = await this.find(condition, [
['isPinned', 'DESC'],
[sequelize.literal('COALESCE(`isPinned`, 0)'), 'DESC'],
['position', 'DESC'],
['createdAt', 'ASC'],
]);
+28 -2
View File
@@ -14,6 +14,7 @@ import {
import config from '../config';
import { credentials } from '@grpc/grpc-js';
import { ApiClient } from '../protos/api';
import { CrontabModel } from '../data/cron';
class TaskLimit {
private dependenyLimit = new PQueue({ concurrency: 1 });
@@ -131,13 +132,38 @@ class TaskLimit {
let runs = this.queuedCrons.get(cron.id);
const result = runs?.length ? [...runs, fn] : [fn];
const repeatTimes = this.repeatCronNotifyMap.get(cron.id) || 0;
if (result?.length > 5) {
// Check instance mode from database to determine queue limit
let maxQueueSize = 10; // Default for multi-instance mode (increased from 5)
let isSingleInstanceMode = false;
try {
const cronRecord = await CrontabModel.findOne({
where: { id: Number(cron.id) },
});
// Default to single instance mode (0) for backward compatibility
// allow_multiple_instances is 1 for multi-instance, 0 or null/undefined for single instance
isSingleInstanceMode = cronRecord?.allow_multiple_instances !== 1;
if (isSingleInstanceMode) {
// For single instance mode, allow up to 2 queued tasks
// This allows the new task to be queued while the old one is being killed
maxQueueSize = 2;
}
} catch (error) {
Logger.error(
`[schedule][检查实例模式失败] 任务ID: ${cron.id}, 错误: ${error}`,
);
}
if (result?.length > maxQueueSize) {
if (repeatTimes < 3) {
this.repeatCronNotifyMap.set(cron.id, repeatTimes + 1);
const modeStr = isSingleInstanceMode ? '单实例' : '多实例';
this.client.systemNotify(
{
title: '任务重复运行',
content: `任务:${cron.name},命令:${cron.command},定时:${cron.schedule},处于运行中的超过 5 个,请检查定时设置`,
content: `任务:${cron.name}${modeStr}模式),命令:${cron.command},定时:${cron.schedule},处于运行中的超过 ${maxQueueSize} 个,请检查定时设置`,
},
(err, res) => {
if (err) {
+27 -3
View File
@@ -15,11 +15,12 @@ export function runCron(cmd: string, cron: ICron): Promise<number | void> {
});
// Default to single instance mode (0) for backward compatibility
const allowSingleInstances =
existingCron?.allow_multiple_instances === 0;
// allow_multiple_instances is 1 for multi-instance, 0 or null/undefined for single instance
const isSingleInstanceMode =
existingCron?.allow_multiple_instances !== 1;
if (
allowSingleInstances &&
isSingleInstanceMode &&
existingCron &&
existingCron.pid &&
(existingCron.status === CrontabStatus.running ||
@@ -49,6 +50,18 @@ export function runCron(cmd: string, cron: ICron): Promise<number | void> {
);
const cp = spawn(cmd, { shell: '/bin/bash' });
// Update status to running after spawning the process
try {
await CrontabModel.update(
{ status: CrontabStatus.running, pid: cp.pid },
{ where: { id: Number(cron.id) } },
);
} catch (error) {
Logger.error(
`[schedule][更新任务状态失败] 任务ID: ${cron.id}, 错误: ${error}`,
);
}
cp.stderr.on('data', (data) => {
Logger.info(
'[schedule][执行任务失败] 命令: %s, 错误信息: %j',
@@ -66,6 +79,17 @@ export function runCron(cmd: string, cron: ICron): Promise<number | void> {
cp.on('exit', async (code) => {
taskLimit.removeQueuedCron(cron.id);
// Update status to idle after task completes
try {
await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined },
{ where: { id: Number(cron.id) } },
);
} catch (error) {
Logger.error(
`[schedule][更新任务状态失败] 任务ID: ${cron.id}, 错误: ${error}`,
);
}
Logger.info(
'[schedule][执行任务结束] 参数: %s, 退出码: %j',
JSON.stringify({
+10 -46
View File
@@ -1,47 +1,11 @@
version: 2.20.0
changeLogLink: https://t.me/jiao_long/432
publishTime: 2025-12-10 01:05
version: 2.20.1
changeLogLink: https://t.me/jiao_long/433
publishTime: 2025-12-26 22:00
changeLog: |
1. 定时任务(cron / task)相关的大量修复 & 增强
修复 cron 解析错误(修复 parse cron / 升级 cron-parser
修复集群模式下定时任务可能不执行(race condition
定时任务支持订阅筛选
定时任务支持排序调整
定时任务支持自定义日志文件或无日志
修复任务实例默认值
任务支持单实例 / 多实例模式
修复 task 命令软链可能失败问题
2. 日志系统相关的大更新
修复日志目录逻辑
修复 pm2 日志目录
优化日志写入(stream pooling
3. 环境变量(env)系统的改进与修复
修复环境变量复制到剪贴板时可能失败
添加环境变量“置顶”功能
修复 QlPort 与 QlGrpcPort 环境变量在 host network 模式下被忽略
增加全局 SSH 私钥配置
4. Docker / 非 root 用户 / Alpine 兼容性增强
新增非 root Docker 用户支持,自动初始化命令
修复 Alpine 容器 DNS 解析失败(设置 ndots:0
修复 PM2 在 ARM 路由器(Node.js 不兼容)上的启动失败
移除 nginx(可能是考虑更轻量的镜像运行)
5. API 安全与校验增强
Dependencies GET endpoint 增加校验
Script API routes 增加输入校验
修复 JWT 认证问题
Feishu 机器人通知增加签名校验
QLAPI 增加 cron task 管理功能
修复 URIError(错误 cookie 导致白屏)
6. 系统设置
新增多终端/多平台的并发登录会话支持
1. 修复获取依赖管理列表
2. notify.js 修复 TG_PROXY_AUTH 参数拼接
3. QLAPI.notify larkSecret 参数
4. 修复 cron parser 定时规则校验
5. 修复设置 baseUrl 后无法访问
6. 修复环境变量排序
7. 修复定时任务无法停止