Compare commits

..

3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 2c57ada739 Fix: Disable/enable associated cron tasks when subscription is disabled/enabled
When a subscription is disabled, the associated cron tasks (created by the subscription) were still running and updating scripts. This fix ensures that:
- When disabling a subscription, all cron tasks with matching sub_id are also disabled
- When enabling a subscription, all cron tasks with matching sub_id are also enabled

This addresses the actual root cause: subscription tasks don't run when disabled (as the owner correctly pointed out), but the cron tasks created by those subscriptions were still active.

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-11-20 16:11:56 +00:00
copilot-swe-agent[bot] 87a1a3d2eb Fix disabled subscriptions still updating tasks
- Filter setSshConfig() to only configure SSH keys for enabled subscriptions
- Remove SSH keys when subscriptions are disabled
- This prevents disabled subscriptions from running scheduled tasks

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-11-20 15:50:35 +00:00
copilot-swe-agent[bot] 99064bde10 Initial plan 2025-11-20 15:44:23 +00:00
18 changed files with 52 additions and 149 deletions
+9 -19
View File
@@ -129,13 +129,6 @@ jobs:
with:
cache: "pnpm"
- name: Read version from version.yaml
id: version
run: |
VERSION=$(grep '^version:' version.yaml | awk '{print $2}')
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
- name: Setup timezone
uses: szenius/set-timezone@v2.0
with:
@@ -161,13 +154,19 @@ jobs:
images: |
${{ github.repository }}
ghcr.io/${{ github.repository }}
# generate Docker tags based on the following events/attributes
# nightly, master, pr-2, 1.2.3, 1.2, 1
flavor: |
latest=false
tags: |
type=ref,event=branch,enable=${{ github.ref == format('refs/heads/{0}', 'develop') }}
type=schedule,pattern=nightly
type=edge
type=ref,event=pr
type=ref,event=branch,enable=${{ github.ref != format('refs/heads/{0}', 'master') }}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=raw,value=${{ steps.version.outputs.version }},enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -217,13 +216,6 @@ jobs:
with:
cache: "pnpm"
- name: Read version from version.yaml
id: version
run: |
VERSION=$(grep '^version:' version.yaml | awk '{print $2}')
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
- name: Setup timezone
uses: szenius/set-timezone@v2.0
with:
@@ -262,9 +254,7 @@ jobs:
context: .
file: ./docker/310.Dockerfile
push: true
tags: |
whyour/qinglong:python3.10
whyour/qinglong:${{ steps.version.outputs.version }}-python3.10
tags: whyour/qinglong:python3.10
cache-from: type=registry,ref=whyour/qinglong:cache-python3.10
cache-to: type=registry,ref=whyour/qinglong:cache-python3.10,mode=max
-2
View File
@@ -129,7 +129,6 @@ export default (app: Router) => {
content: Joi.string().optional().allow(''),
originFilename: Joi.string().optional().allow(''),
directory: Joi.string().optional().allow(''),
file: Joi.string().optional().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
@@ -176,7 +175,6 @@ export default (app: Router) => {
path,
`${originFilename.replace(/\//g, '')}`,
);
await fs.mkdir(path, { recursive: true });
const filePath = join(path, `${filename.replace(/\//g, '')}`);
const fileExists = await fileExist(filePath);
if (fileExists) {
-1
View File
@@ -142,7 +142,6 @@ export class WebhookNotification extends NotificationBaseInfo {
export class LarkNotification extends NotificationBaseInfo {
public larkKey = '';
public larkSecret = '';
}
export class NtfyNotification extends NotificationBaseInfo {
+2 -6
View File
@@ -507,7 +507,7 @@ export default class CronService {
let { id, command, log_name } = cron;
const uniqPath =
log_name === '/dev/null' || !log_name
log_name === '/dev/null'
? await getUniqPath(command, `${id}`)
: log_name;
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
@@ -641,11 +641,7 @@ export default class CronService {
if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) {
command = `${TASK_PREFIX}${tab.command}`;
}
let commandVariable = `real_time=${Boolean(realTime)} no_tee=true ID=${tab.id} `;
// Only include log_name if it has a truthy value to avoid passing null/undefined to shell
if (tab.log_name) {
commandVariable += `log_name=${tab.log_name} `;
}
let commandVariable = `real_time=${Boolean(realTime)} log_name=${tab.log_name} no_tee=true ID=${tab.id} `;
if (tab.task_before) {
commandVariable += `task_before='${tab.task_before
.replace(/'/g, "'\\''")
+1 -1
View File
@@ -107,7 +107,7 @@ export default class DependenceService {
query: any = {},
): Promise<Dependence[]> {
let condition = query;
if (type && DependenceTypes[type] !== undefined) {
if (DependenceTypes[type]) {
condition.type = DependenceTypes[type];
}
if (status) {
+5 -19
View File
@@ -550,33 +550,19 @@ export default class NotificationService {
}
private async lark() {
let { larkKey, larkSecret } = this.params;
let { larkKey } = this.params;
if (!larkKey.startsWith('http')) {
larkKey = `https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`;
}
const body: Record<string, any> = {
msg_type: 'text',
content: { text: `${this.title}\n\n${this.content}` },
};
// Add signature if secret is provided
// Note: Feishu's signature algorithm uses timestamp+"\n"+secret as the HMAC key
// and signs an empty message, which differs from typical HMAC usage
if (larkSecret) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const stringToSign = `${timestamp}\n${larkSecret}`;
const hmac = crypto.createHmac('sha256', stringToSign);
const sign = hmac.digest('base64');
body.timestamp = timestamp;
body.sign = sign;
}
try {
const res = await httpClient.post(larkKey, {
...this.gotOption,
json: body,
json: {
msg_type: 'text',
content: { text: `${this.title}\n\n${this.content}` },
},
headers: { 'Content-Type': 'application/json' },
});
if (res.StatusCode === 0 || res.code === 0) {
+4 -4
View File
@@ -133,15 +133,15 @@ export default class SshKeyService {
}
public async addGlobalSSHKey(key: string, alias: string): Promise<void> {
await this.generatePrivateKeyFile(`~global_${alias}`, key);
await this.generatePrivateKeyFile(`global_${alias}`, key);
// Create a global SSH config entry that matches all hosts
// This allows the key to be used for any Git repository
await this.generateGlobalSshConfig(`~global_${alias}`);
await this.generateGlobalSshConfig(`global_${alias}`);
}
public async removeGlobalSSHKey(alias: string): Promise<void> {
await this.removePrivateKeyFile(`~global_${alias}`);
await this.removeSshConfig(`~global_${alias}`);
await this.removePrivateKeyFile(`global_${alias}`);
await this.removeSshConfig(`global_${alias}`);
}
private async generateGlobalSshConfig(alias: string) {
+10
View File
@@ -350,6 +350,11 @@ export default class SubscriptionService {
for (const doc of docs) {
await this.handleTask(doc.get({ plain: true }), false);
}
// Disable associated cron tasks
const crons = await CrontabModel.findAll({ where: { sub_id: ids } });
if (crons?.length) {
await this.crontabService.disabled(crons.map((x) => x.id!));
}
}
public async enabled(ids: number[]) {
@@ -359,6 +364,11 @@ export default class SubscriptionService {
for (const doc of docs) {
await this.handleTask(doc.get({ plain: true }));
}
// Enable associated cron tasks
const crons = await CrontabModel.findAll({ where: { sub_id: ids } });
if (crons?.length) {
await this.crontabService.enabled(crons.map((x) => x.id!));
}
}
public async log(id: number) {
+3 -3
View File
@@ -15,11 +15,11 @@ 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;
const allowMultipleInstances =
existingCron?.allow_multiple_instances === 1;
if (
allowSingleInstances &&
!allowMultipleInstances &&
existingCron &&
existingCron.pid &&
(existingCron.status === CrontabStatus.running ||
+1 -1
View File
@@ -81,5 +81,5 @@ export const commonCronSchema = {
'string.max': '日志名称不能超过100个字符',
'string.unsafePath': '绝对路径必须在日志目录内或使用 /dev/null',
}),
allow_multiple_instances: Joi.number().optional().valid(0, 1).allow(null),
allow_multiple_instances: Joi.number().optional().valid(0, 1),
};
+5 -5
View File
@@ -23,11 +23,11 @@ log_with_style() {
if [ -f /etc/alpine-release ]; then
if ! grep -q "^options ndots:0" /etc/resolv.conf 2>/dev/null; then
echo "options ndots:0" >> /etc/resolv.conf
log_with_style "INFO" "🔧 0. 已配置 DNS 解析优化 (ndots:0)"
log_with_style "INFO" "🔧 已配置 DNS 解析优化 (ndots:0)"
fi
fi
log_with_style "INFO" "🚀 1. 检测配置文件..."
log_with_style "INFO" "🚀 1. 检测配置文件..."
load_ql_envs
export_ql_envs
. $dir_shell/env.sh
@@ -41,16 +41,16 @@ log_with_style "INFO" "⚙️ 2. 启动 pm2 服务..."
reload_pm2
if [[ $AutoStartBot == true ]]; then
log_with_style "INFO" "🤖 3. 启动 bot..."
log_with_style "INFO" "🤖 3. 启动 bot..."
nohup ql bot >$dir_log/bot.log 2>&1 &
fi
if [[ $EnableExtraShell == true ]]; then
log_with_style "INFO" "🛠️ 4. 执行自定义脚本..."
log_with_style "INFO" "🛠️ 4. 执行自定义脚本..."
nohup ql extra >$dir_log/extra.log 2>&1 &
fi
log_with_style "SUCCESS" "🎉 容器启动成功!"
log_with_style "SUCCESS" "🎉 容器启动成功!"
crond -f >/dev/null
+2 -18
View File
@@ -52,7 +52,6 @@ const push_config = {
DD_BOT_TOKEN: '', // 钉钉机器人的 DD_BOT_TOKEN
FSKEY: '', // 飞书机器人的 FSKEY
FSSECRET: '', // 飞书机器人的 FSSECRET,对应安全设置里的签名校验密钥
// 推送到个人QQhttp://127.0.0.1/send_private_msg
// 群:http://127.0.0.1/send_group_msg
@@ -990,26 +989,11 @@ function aibotkNotify(text, desp) {
function fsBotNotify(text, desp) {
return new Promise((resolve) => {
const { FSKEY, FSSECRET } = push_config;
const { FSKEY } = push_config;
if (FSKEY) {
const body = { msg_type: 'text', content: { text: `${text}\n\n${desp}` } };
// Add signature if secret is provided
// Note: Feishu's signature algorithm uses timestamp+"\n"+secret as the HMAC key
// and signs an empty message, which differs from typical HMAC usage
if (FSSECRET) {
const crypto = require('crypto');
const timestamp = Math.floor(Date.now() / 1000).toString();
const stringToSign = `${timestamp}\n${FSSECRET}`;
const hmac = crypto.createHmac('sha256', stringToSign);
const sign = hmac.digest('base64');
body.timestamp = timestamp;
body.sign = sign;
}
const options = {
url: `https://open.feishu.cn/open-apis/bot/v2/hook/${FSKEY}`,
json: body,
json: { msg_type: 'text', content: { text: `${text}\n\n${desp}` } },
headers: {
'Content-Type': 'application/json',
},
-15
View File
@@ -49,7 +49,6 @@ push_config = {
'DD_BOT_TOKEN': '', # 钉钉机器人的 DD_BOT_TOKEN
'FSKEY': '', # 飞书机器人的 FSKEY
'FSSECRET': '', # 飞书机器人的 FSSECRET,对应安全设置里的签名校验密钥
'GOBOT_URL': '', # go-cqhttp
# 推送到个人QQhttp://127.0.0.1/send_private_msg
@@ -234,20 +233,6 @@ def feishu_bot(title: str, content: str) -> None:
url = f'https://open.feishu.cn/open-apis/bot/v2/hook/{push_config.get("FSKEY")}'
data = {"msg_type": "text", "content": {"text": f"{title}\n\n{content}"}}
# Add signature if secret is provided
# Note: Feishu's signature algorithm uses timestamp+"\n"+secret as the HMAC key
# and signs an empty message, which differs from typical HMAC usage
if push_config.get("FSSECRET"):
timestamp = str(int(time.time()))
string_to_sign = f'{timestamp}\n{push_config.get("FSSECRET")}'
hmac_code = hmac.new(
string_to_sign.encode("utf-8"), digestmod=hashlib.sha256
).digest()
sign = base64.b64encode(hmac_code).decode("utf-8")
data["timestamp"] = timestamp
data["sign"] = sign
response = requests.post(url, data=json.dumps(data)).json()
if response.get("StatusCode") == 0 or response.get("code") == 0:
-1
View File
@@ -389,7 +389,6 @@
"消息接收人": "message recipient",
"调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版": "Version, you can specify 'pro' for the Professional version and 'personal' for the Personal version. If left blank, it will default to the Professional version.",
"飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973": "Feishu group bot: https://www.feishu.cn/hc/zh-CN/articles/360024984973",
"飞书群组机器人加签密钥,安全设置中开启签名校验后获得": "Feishu group bot signature secret, obtained after enabling signature verification in security settings",
"邮箱服务名称,比如126、163、Gmail、QQ等,支持列表https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json": "Email service name, e.g., 126, 163, Gmail, QQ, etc. Supported list: https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json",
"邮箱地址": "Email Address",
"SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定": "The SMTP login password may also be a special passphrase, depending on the specific email service provider's instructions",
-1
View File
@@ -389,7 +389,6 @@
"消息接收人": "消息接收人",
"调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版": "调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版",
"飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973": "飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973",
"飞书群组机器人加签密钥,安全设置中开启签名校验后获得": "飞书群组机器人加签密钥,安全设置中开启签名校验后获得",
"邮箱服务名称,比如126、163、Gmail、QQ等,支持列表https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json": "邮箱服务名称,比如126、163、Gmail、QQ等,支持列表https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json",
"邮箱地址": "邮箱地址",
"SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定": "SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定",
+1 -1
View File
@@ -16,7 +16,7 @@ const SaveModal = ({
const handleOk = async (values: any) => {
setLoading(true);
const payload = { ...values, originFilename: file.title, content: file.content };
const payload = { ...file, ...values, originFilename: file.title };
request
.post(`${config.apiPrefix}scripts`, payload)
.then(({ code, data }) => {
-6
View File
@@ -395,12 +395,6 @@ export default {
),
required: true,
},
{
label: 'larkSecret',
tip: intl.get(
'飞书群组机器人加签密钥,安全设置中开启签名校验后获得',
),
},
],
email: [
{
+9 -46
View File
@@ -1,47 +1,10 @@
version: 2.20.0
changeLogLink: https://t.me/jiao_long/432
publishTime: 2025-12-10 01:05
version: 2.19.2
changeLogLink: https://t.me/jiao_long/431
publishTime: 2025-06-27 23:59
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. QLAPI 和 openapi 的 systemNotify 支持自定义通知类型和参数
3. ntfy 增加可选的认证与用户动作,感谢 https://github.com/liheji
4. 修复取消安装依赖
5. 修复环境变量过大解析报错
6. 修改服务启动方式