Compare commits

..

4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 649f79e7bc Revert version.yaml to 2.19.2 - should not include version bump in bug fix PR
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-15 10:35:01 +00:00
copilot-swe-agent[bot] f887eb6de7 Fix validation error when saving scripts by allowing unknown fields in POST /scripts
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-10 05:34:11 +00:00
copilot-swe-agent[bot] 7ecceafc2d Initial plan 2025-12-10 05:26:41 +00:00
whyour 1776eb58fb 更新版本 2.20.0 2025-12-10 01:26:00 +08:00
21 changed files with 63 additions and 169 deletions
+17 -16
View File
@@ -9,13 +9,15 @@ on:
- "develop"
tags:
- "v*"
schedule:
- cron: "00 20 * * *"
workflow_dispatch:
jobs:
code_gitlab:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: Yikun/hub-mirror-action@master
@@ -30,7 +32,7 @@ jobs:
code_gitee:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: Yikun/hub-mirror-action@master
@@ -45,12 +47,12 @@ jobs:
build-static:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: "8.3.1"
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: "pnpm"
@@ -76,12 +78,12 @@ jobs:
git config --local user.email 'github-actions[bot]@users.noreply.github.com'
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH}
static_gitlab:
needs: build-static
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: Yikun/hub-mirror-action@master
@@ -97,7 +99,7 @@ jobs:
needs: build-static
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: Yikun/hub-mirror-action@master
@@ -110,7 +112,6 @@ jobs:
force_update: true
build:
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
needs: build-static
runs-on: ubuntu-22.04
@@ -120,11 +121,11 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: "8.3.1"
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: "pnpm"
@@ -163,7 +164,7 @@ jobs:
flavor: |
latest=false
tags: |
type=ref,event=branch,enable=${{ github.ref == format('refs/heads/{0}', 'develop') }}
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}}
@@ -208,11 +209,11 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: "8.3.1"
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: "pnpm"
+1 -1
View File
@@ -16,7 +16,7 @@ export default (app: Router) => {
searchValue: Joi.string().optional().allow(''),
type: Joi.string().optional().allow(''),
status: Joi.string().optional().allow(''),
}).unknown(true),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
+3 -4
View File
@@ -29,7 +29,7 @@ export default (app: Router) => {
celebrate({
query: Joi.object({
path: Joi.string().optional().allow(''),
}).unknown(true),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
@@ -79,7 +79,7 @@ export default (app: Router) => {
query: Joi.object({
path: Joi.string().optional().allow(''),
file: Joi.string().required(),
}).unknown(true),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
@@ -103,7 +103,7 @@ export default (app: Router) => {
}),
query: Joi.object({
path: Joi.string().optional().allow(''),
}).unknown(true),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
@@ -176,7 +176,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 -1
View File
@@ -3,7 +3,7 @@ import { Container } from 'typedi';
import { Logger } from 'winston';
import SubscriptionService from '../services/subscription';
import { celebrate, Joi } from 'celebrate';
import CronExpressionParser from 'cron-parser';
import { CronExpressionParser } from 'cron-parser';
const route = Router();
export default (app: Router) => {
-14
View File
@@ -64,19 +64,6 @@ if (!process.env.QL_DIR) {
const lastVersionFile = `https://qn.whyour.cn/version.yaml`;
// Get and normalize QlBaseUrl
let baseUrl = process.env.QlBaseUrl || '';
if (baseUrl) {
// Ensure it starts with /
if (!baseUrl.startsWith('/')) {
baseUrl = `/${baseUrl}`;
}
// Remove trailing slash for consistency in route definitions
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1);
}
}
const rootPath = process.env.QL_DIR as string;
const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
@@ -129,7 +116,6 @@ if (envFound.error) {
export default {
...config,
jwt: config.jwt,
baseUrl,
rootPath,
tmpPath,
dataPath,
+1 -12
View File
@@ -15,13 +15,6 @@ import path from 'path';
export default ({ app }: { app: Application }) => {
app.set('trust proxy', 'loopback');
app.use(cors());
// Rewrite URLs to strip baseUrl prefix if configured
// This allows the rest of the app to work without baseUrl awareness
if (config.baseUrl) {
app.use(rewrite(`${config.baseUrl}/*`, '/$1'));
}
app.get(`${config.api.prefix}/env.js`, serveEnv);
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
@@ -123,11 +116,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) {
+1 -2
View File
@@ -5,10 +5,9 @@ import SockService from '../services/sock';
import { getPlatform } from '../config/util';
import { shareStore } from '../shared/store';
import { isValidToken } from '../shared/auth';
import config from '../config';
export default async ({ server }: { server: Server }) => {
const echo = sockJs.createServer({ prefix: `${config.baseUrl}/api/ws`, log: () => { } });
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
const sockService = Container.get(SockService);
echo.on('connection', async (conn) => {
-1
View File
@@ -231,7 +231,6 @@ message NotificationInfo {
optional string webhookContentType = 57;
optional string larkKey = 58;
optional string larkSecret = 69;
optional string ntfyUrl = 59;
optional string ntfyTopic = 60;
+1 -19
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.6.1
// protoc v3.21.12
// protoc v3.17.3
// source: back/protos/api.proto
/* eslint-disable */
@@ -382,7 +382,6 @@ export interface NotificationInfo {
webhookMethod?: string | undefined;
webhookContentType?: string | undefined;
larkKey?: string | undefined;
larkSecret?: string | undefined;
ntfyUrl?: string | undefined;
ntfyTopic?: string | undefined;
ntfyPriority?: string | undefined;
@@ -2948,7 +2947,6 @@ function createBaseNotificationInfo(): NotificationInfo {
webhookMethod: undefined,
webhookContentType: undefined,
larkKey: undefined,
larkSecret: undefined,
ntfyUrl: undefined,
ntfyTopic: undefined,
ntfyPriority: undefined,
@@ -3138,9 +3136,6 @@ export const NotificationInfo: MessageFns<NotificationInfo> = {
if (message.larkKey !== undefined) {
writer.uint32(466).string(message.larkKey);
}
if (message.larkSecret !== undefined) {
writer.uint32(554).string(message.larkSecret);
}
if (message.ntfyUrl !== undefined) {
writer.uint32(474).string(message.ntfyUrl);
}
@@ -3645,14 +3640,6 @@ export const NotificationInfo: MessageFns<NotificationInfo> = {
message.larkKey = reader.string();
continue;
}
case 69: {
if (tag !== 554) {
break;
}
message.larkSecret = reader.string();
continue;
}
case 59: {
if (tag !== 474) {
break;
@@ -3810,7 +3797,6 @@ export const NotificationInfo: MessageFns<NotificationInfo> = {
webhookMethod: isSet(object.webhookMethod) ? globalThis.String(object.webhookMethod) : undefined,
webhookContentType: isSet(object.webhookContentType) ? globalThis.String(object.webhookContentType) : undefined,
larkKey: isSet(object.larkKey) ? globalThis.String(object.larkKey) : undefined,
larkSecret: isSet(object.larkSecret) ? globalThis.String(object.larkSecret) : undefined,
ntfyUrl: isSet(object.ntfyUrl) ? globalThis.String(object.ntfyUrl) : undefined,
ntfyTopic: isSet(object.ntfyTopic) ? globalThis.String(object.ntfyTopic) : undefined,
ntfyPriority: isSet(object.ntfyPriority) ? globalThis.String(object.ntfyPriority) : undefined,
@@ -4004,9 +3990,6 @@ export const NotificationInfo: MessageFns<NotificationInfo> = {
if (message.larkKey !== undefined) {
obj.larkKey = message.larkKey;
}
if (message.larkSecret !== undefined) {
obj.larkSecret = message.larkSecret;
}
if (message.ntfyUrl !== undefined) {
obj.ntfyUrl = message.ntfyUrl;
}
@@ -4103,7 +4086,6 @@ export const NotificationInfo: MessageFns<NotificationInfo> = {
message.webhookMethod = object.webhookMethod ?? undefined;
message.webhookContentType = object.webhookContentType ?? undefined;
message.larkKey = object.larkKey ?? undefined;
message.larkSecret = object.larkSecret ?? undefined;
message.ntfyUrl = object.ntfyUrl ?? undefined;
message.ntfyTopic = object.ntfyTopic ?? undefined;
message.ntfyPriority = object.ntfyPriority ?? undefined;
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.6.1
// protoc v3.21.12
// protoc v3.17.3
// source: back/protos/cron.proto
/* eslint-disable */
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.6.1
// protoc v3.21.12
// protoc v3.17.3
// source: back/protos/health.proto
/* eslint-disable */
+1 -1
View File
@@ -4,7 +4,7 @@ import config from '../config';
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import { exec, execSync } from 'child_process';
import fs from 'fs/promises';
import CronExpressionParser from 'cron-parser';
import { CronExpressionParser } from 'cron-parser';
import {
getFileContentByName,
fileExist,
+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) {
+1 -1
View File
@@ -1,5 +1,5 @@
import { Joi } from 'celebrate';
import CronExpressionParser from 'cron-parser';
import { CronExpressionParser } from 'cron-parser';
import { ScheduleType } from '../interface/schedule';
import path from 'path';
import config from '../config';
+4 -20
View File
@@ -482,13 +482,9 @@ function tgBotNotify(text, desp) {
timeout,
};
if (TG_PROXY_HOST && TG_PROXY_PORT) {
let proxyHost = TG_PROXY_HOST;
if (TG_PROXY_AUTH && !TG_PROXY_HOST.includes('@')) {
proxyHost = `${TG_PROXY_AUTH}@${TG_PROXY_HOST}`;
}
let agent;
agent = new ProxyAgent({
uri: `http://${proxyHost}:${TG_PROXY_PORT}`,
uri: `http://${TG_PROXY_AUTH}${TG_PROXY_HOST}:${TG_PROXY_PORT}`,
});
options.dispatcher = agent;
}
@@ -996,10 +992,7 @@ function fsBotNotify(text, desp) {
return new Promise((resolve) => {
const { FSKEY, FSSECRET } = push_config;
if (FSKEY) {
const body = {
msg_type: 'text',
content: { text: `${text}\n\n${desp}` },
};
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
@@ -1285,15 +1278,7 @@ function ntfyNotify(text, desp) {
}
return new Promise((resolve) => {
const {
NTFY_URL,
NTFY_TOPIC,
NTFY_PRIORITY,
NTFY_TOKEN,
NTFY_USERNAME,
NTFY_PASSWORD,
NTFY_ACTIONS,
} = push_config;
const { NTFY_URL, NTFY_TOPIC, NTFY_PRIORITY, NTFY_TOKEN, NTFY_USERNAME, NTFY_PASSWORD, NTFY_ACTIONS } = push_config;
if (NTFY_TOPIC) {
const options = {
url: `${NTFY_URL || 'https://ntfy.sh'}/${NTFY_TOPIC}`,
@@ -1308,8 +1293,7 @@ function ntfyNotify(text, desp) {
if (NTFY_TOKEN) {
options.headers['Authorization'] = `Bearer ${NTFY_TOKEN}`;
} else if (NTFY_USERNAME && NTFY_PASSWORD) {
options.headers['Authorization'] =
`Basic ${Buffer.from(`${NTFY_USERNAME}:${NTFY_PASSWORD}`).toString('base64')}`;
options.headers['Authorization'] = `Basic ${Buffer.from(`${NTFY_USERNAME}:${NTFY_PASSWORD}`).toString('base64')}`;
}
if (NTFY_ACTIONS) {
options.headers['Actions'] = encodeRFC2047(NTFY_ACTIONS);
+4 -8
View File
@@ -3,7 +3,7 @@ import config from '@/utils/config';
import { request } from '@/utils/http';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Form, Input, Modal, Select, Space, message } from 'antd';
import CronExpressionParser from 'cron-parser';
import { CronExpressionParser } from 'cron-parser';
import { useEffect, useState } from 'react';
import intl from 'react-intl-universal';
import { getScheduleType, scheduleTypeMap } from './const';
@@ -91,14 +91,10 @@ const CronModal = ({
{ required: true },
{
validator: (_, value) => {
try {
if (!value || CronExpressionParser.parse(value).hasNext()) {
return Promise.resolve();
}
return Promise.reject(intl.get('Cron表达式格式有误'));
} catch (e) {
return Promise.reject(intl.get('Cron表达式格式有误'));
if (!value || CronExpressionParser.parse(value).hasNext()) {
return Promise.resolve();
}
return Promise.reject(intl.get('Cron表达式格式有误'));
},
},
]}
+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 }) => {
+8 -12
View File
@@ -12,7 +12,7 @@ import {
} from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import CronExpressionParser from 'cron-parser';
import { CronExpressionParser } from 'cron-parser';
import isNil from 'lodash/isNil';
const { Option } = Select;
@@ -378,17 +378,13 @@ const SubscriptionModal = ({
{ required: true },
{
validator: (rule, value) => {
try {
if (
scheduleType === 'interval' ||
!value ||
CronExpressionParser.parse(value).hasNext()
) {
return Promise.resolve();
} else {
return Promise.reject(intl.get('Subscription表达式格式有误'));
}
} catch (e) {
if (
scheduleType === 'interval' ||
!value ||
CronExpressionParser.parse(value).hasNext()
) {
return Promise.resolve();
} else {
return Promise.reject(intl.get('Subscription表达式格式有误'));
}
},
+6 -6
View File
@@ -84,12 +84,12 @@ let _request = axios.create({
});
const apiWhiteList = [
`${config.baseUrl}api/user/login`,
`${config.baseUrl}open/auth/token`,
`${config.baseUrl}api/user/two-factor/login`,
`${config.baseUrl}api/system`,
`${config.baseUrl}api/user/init`,
`${config.baseUrl}api/user/notification/init`,
'/api/user/login',
'/open/auth/token',
'/api/user/two-factor/login',
'/api/system',
'/api/user/init',
'/api/user/notification/init',
];
_request.interceptors.request.use((_config) => {
+1 -1
View File
@@ -1,6 +1,6 @@
import intl from 'react-intl-universal';
import { LANG_MAP, LOG_END_SYMBOL } from './const';
import CronExpressionParser from 'cron-parser';
import { CronExpressionParser } from 'cron-parser';
import { ICrontab } from '@/pages/crontab/type';
export default function browserType() {
+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. 修改服务启动方式