Compare commits

..

19 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] fcf0ee619b Improve URL pattern matching and remove redundant checks
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-27 08:51:12 +00:00
copilot-swe-agent[bot] f3ec352066 Address code review feedback: improve pattern matching and error handling
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-27 08:49:41 +00:00
copilot-swe-agent[bot] 023d0f1cc4 Fix pyproject.toml path handling and add error handling
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-27 08:46:12 +00:00
copilot-swe-agent[bot] f526d3f972 Add support for GitHub URLs and requirements files in Python dependencies
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-27 08:41:43 +00:00
copilot-swe-agent[bot] e28f746294 Initial plan 2025-12-27 08:35:03 +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
whyour 4f6c93cc1c 更新 workflow 2025-12-24 01:03:21 +08:00
whyour e326d89571 修复 apiWhiteList 路径 2025-12-23 00:58:09 +08:00
whyour 5f0dafa010 修复 cron-parser import,websocket basepath 2025-12-23 00:28:16 +08:00
Copilot dc0b3f2eb2 Fix QlBaseUrl: use URL rewrite for base path support (#2876)
* Initial plan

* Add QlBaseUrl support to backend routes

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Fix whitelist check to use base-URL-aware paths

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Update websocket and frontend to support base URL

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Address code review feedback: fix JWT regex and path construction

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Fix path construction: use req.path directly for whitelist check

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Add clarifying comments and improve code readability

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Apply code review suggestions: improve clarity and simplify logic

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Simplify baseUrl implementation using URL rewrite

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-22 23:44:29 +08:00
Copilot 3db716763d Fix cron-parser v5 bundling incompatibility causing validation failures (#2877)
* Initial plan

* Fix: Use default import for cron-parser to ensure browser compatibility

Changed from named export `{ CronExpressionParser }` to default export `cronParser` and access `CronExpressionParser` through it. This ensures compatibility with webpack/UmiJS bundling for browser environments while maintaining backend functionality.

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-22 23:43:54 +08:00
Copilot fae226745e Add missing larkSecret field to gRPC NotificationInfo proto (#2880)
* Initial plan

* Add larkSecret field to NotificationInfo proto definition

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-22 23:38:42 +08:00
Copilot 9330650163 Fix TG_PROXY_AUTH concatenation in notify.js - add missing @ separator (#2882)
* Initial plan

* Fix TG_PROXY_AUTH handling in notify.js to match notify.py logic

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* Apply prettier formatting to notify.js

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-22 23:05:06 +08:00
Copilot 073de76a4a Fix validation error when saving scripts in debug window (v2.20.0 regression) (#2862)
* 更新版本 2.20.0

* Initial plan

* Fix validation error when saving scripts by allowing unknown fields in POST /scripts

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

* 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>

---------

Co-authored-by: whyour <imwhyour@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-22 22:43:48 +08:00
Copilot c61d1aa828 Fix enum value 0 causing type filter to fail for NodeJS dependencies (#2869)
* Initial plan

* Fix: Prevent Python3 dependencies from appearing in NodeJs tab

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-12-15 18:21:14 +08:00
whyour 33fa3aca99 更新版本 2.20.0 2025-12-11 01:53:17 +08:00
whyour c772fc9527 修复脚本调试保存文件错误 2025-12-11 01:52:47 +08:00
25 changed files with 290 additions and 73 deletions
+16 -17
View File
@@ -9,15 +9,13 @@ on:
- "develop"
tags:
- "v*"
schedule:
- cron: "00 20 * * *"
workflow_dispatch:
jobs:
code_gitlab:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: Yikun/hub-mirror-action@master
@@ -32,7 +30,7 @@ jobs:
code_gitee:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: Yikun/hub-mirror-action@master
@@ -47,12 +45,12 @@ jobs:
build-static:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
with:
version: "8.3.1"
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
cache: "pnpm"
@@ -78,12 +76,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@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: Yikun/hub-mirror-action@master
@@ -99,7 +97,7 @@ jobs:
needs: build-static
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: Yikun/hub-mirror-action@master
@@ -112,6 +110,7 @@ jobs:
force_update: true
build:
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
needs: build-static
runs-on: ubuntu-22.04
@@ -121,11 +120,11 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
with:
version: "8.3.1"
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
cache: "pnpm"
@@ -164,7 +163,7 @@ jobs:
flavor: |
latest=false
tags: |
type=ref,event=branch,enable=${{ github.ref != format('refs/heads/{0}', 'master') }}
type=ref,event=branch,enable=${{ github.ref == format('refs/heads/{0}', 'develop') }}
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}}
@@ -209,11 +208,11 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
with:
version: "8.3.1"
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
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');
+4 -3
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,6 +176,7 @@ 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,6 +64,19 @@ 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') });
@@ -116,6 +129,7 @@ if (envFound.error) {
export default {
...config,
jwt: config.jwt,
baseUrl,
rootPath,
tmpPath,
dataPath,
+61 -4
View File
@@ -535,12 +535,43 @@ export async function setSystemTimezone(timezone: string): Promise<boolean> {
}
}
// Helper function to check if a name is a GitHub URL
function isGitHubUrl(name: string): boolean {
// Support git+https://, git+http://, https://, and http:// URLs
// This covers GitHub URLs and other git-compatible repositories
return !!name.match(/^(git\+https?:\/\/|https?:\/\/)/i);
}
// Helper function to check if a name is a requirements file
function isRequirementsFile(name: string): boolean {
return !!name.match(/requirements.*\.(txt|in)$/i);
}
// Helper function to check if a name is a pyproject.toml file
function isPyprojectToml(name: string): boolean {
return name.endsWith('pyproject.toml');
}
export function getGetCommand(type: DependenceTypes, name: string): string {
const trimmedName = name.trim();
// For Python dependencies installed from GitHub or requirements files,
// we can't reliably check if they're installed, so skip the check
if (type === DependenceTypes.python3) {
if (isGitHubUrl(trimmedName) ||
isRequirementsFile(trimmedName) ||
isPyprojectToml(trimmedName)) {
// Return a command that will always indicate not installed
// This ensures GitHub URLs and requirements files are always installed
return 'echo ""';
}
}
const baseCommands = {
[DependenceTypes.nodejs]: `pnpm ls -g | grep "${name}" | head -1`,
[DependenceTypes.nodejs]: `pnpm ls -g | grep "${trimmedName}" | head -1`,
[DependenceTypes.python3]: `
python3 -c "exec('''
name='${name}'
name='${trimmedName}'
try:
from importlib.metadata import version
print(version(name))
@@ -550,7 +581,7 @@ except:
spec=u.find_spec(name)
print(name if spec else '')
''')"`,
[DependenceTypes.linux]: `apk info -es ${name}`,
[DependenceTypes.linux]: `apk info -es ${trimmedName}`,
};
return baseCommands[type];
@@ -570,7 +601,33 @@ export function getInstallCommand(type: DependenceTypes, name: string): string {
command = `${command} --prefix=${PYTHON_INSTALL_DIR}`;
}
return `${command} ${name.trim()}`;
const trimmedName = name.trim();
// Handle different installation methods for Python
if (type === DependenceTypes.python3) {
// Check if it's a GitHub URL (support both git+ and direct URLs)
if (isGitHubUrl(trimmedName)) {
return `${command} ${trimmedName}`;
}
// Check if it's a requirements file path
if (isRequirementsFile(trimmedName)) {
return `${command} -r ${trimmedName}`;
}
// Check if it's a pyproject.toml file
if (isPyprojectToml(trimmedName)) {
// For pyproject.toml, install from the directory containing it
const pathMatch = trimmedName.match(/^(.+)\/pyproject\.toml$/);
if (pathMatch) {
// Has a path prefix, use the directory
return `${command} ${pathMatch[1]}`;
} else {
// Just "pyproject.toml", install current directory
return `${command} .`;
}
}
}
return `${command} ${trimmedName}`;
}
export function getUninstallCommand(
+7
View File
@@ -15,6 +15,13 @@ 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));
+2 -1
View File
@@ -5,9 +5,10 @@ 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: '/api/ws', log: () => {} });
const echo = sockJs.createServer({ prefix: `${config.baseUrl}/api/ws`, log: () => { } });
const sockService = Container.get(SockService);
echo.on('connection', async (conn) => {
+1
View File
@@ -231,6 +231,7 @@ message NotificationInfo {
optional string webhookContentType = 57;
optional string larkKey = 58;
optional string larkSecret = 69;
optional string ntfyUrl = 59;
optional string ntfyTopic = 60;
+19 -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.17.3
// protoc v3.21.12
// source: back/protos/api.proto
/* eslint-disable */
@@ -382,6 +382,7 @@ export interface NotificationInfo {
webhookMethod?: string | undefined;
webhookContentType?: string | undefined;
larkKey?: string | undefined;
larkSecret?: string | undefined;
ntfyUrl?: string | undefined;
ntfyTopic?: string | undefined;
ntfyPriority?: string | undefined;
@@ -2947,6 +2948,7 @@ function createBaseNotificationInfo(): NotificationInfo {
webhookMethod: undefined,
webhookContentType: undefined,
larkKey: undefined,
larkSecret: undefined,
ntfyUrl: undefined,
ntfyTopic: undefined,
ntfyPriority: undefined,
@@ -3136,6 +3138,9 @@ 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);
}
@@ -3640,6 +3645,14 @@ 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;
@@ -3797,6 +3810,7 @@ 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,
@@ -3990,6 +4004,9 @@ 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;
}
@@ -4086,6 +4103,7 @@ 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.17.3
// protoc v3.21.12
// 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.17.3
// protoc v3.21.12
// source: back/protos/health.proto
/* eslint-disable */
+7 -4
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,
@@ -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}`,
+1 -1
View File
@@ -107,7 +107,7 @@ export default class DependenceService {
query: any = {},
): Promise<Dependence[]> {
let condition = query;
if (DependenceTypes[type]) {
if (type && DependenceTypes[type] !== undefined) {
condition.type = DependenceTypes[type];
}
if (status) {
+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'],
]);
+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';
+20 -4
View File
@@ -482,9 +482,13 @@ 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://${TG_PROXY_AUTH}${TG_PROXY_HOST}:${TG_PROXY_PORT}`,
uri: `http://${proxyHost}:${TG_PROXY_PORT}`,
});
options.dispatcher = agent;
}
@@ -992,7 +996,10 @@ 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
@@ -1278,7 +1285,15 @@ 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}`,
@@ -1293,7 +1308,8 @@ 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);
+74
View File
@@ -111,6 +111,76 @@ add_cron() {
notify_api "$path 新增任务" "$detail"
}
## 自动安装订阅仓库中的Python依赖
auto_install_python_deps() {
local repo_path="$1"
local uniq_path="$2"
echo -e "\n检测订阅仓库中的Python依赖文件...\n"
get_token
# 检查 requirements.txt
if [[ -f "${repo_path}/requirements.txt" ]]; then
echo -e "发现 requirements.txt,开始自动安装依赖...\n"
local req_file="${dir_scripts}/${uniq_path}/requirements.txt"
# 确保目标目录存在
make_dir "${dir_scripts}/${uniq_path}"
# 复制文件并检查结果
if cp -f "${repo_path}/requirements.txt" "${req_file}" 2>/dev/null; then
# 调用API添加依赖安装任务
local dep_name="${uniq_path}/requirements.txt"
local currentTimeStamp=$(date +%s)
local result=$(curl -s --noproxy "*" "http://127.0.0.1:${ql_port}/open/dependencies?t=$currentTimeStamp" \
-X POST \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Authorization: Bearer ${__ql_token__}" \
--data-raw "[{\"name\":\"${dep_name}\",\"type\":1,\"remark\":\"自动检测:${uniq_path} 订阅依赖\"}]" 2>/dev/null)
local code=$(echo "$result" | jq -r '.code' 2>/dev/null)
if [[ "$code" == "200" ]]; then
echo -e "已添加 requirements.txt 依赖安装任务\n"
else
echo -e "添加 requirements.txt 依赖失败,请手动添加\n"
fi
else
echo -e "复制 requirements.txt 失败,跳过自动安装\n"
fi
fi
# 检查 pyproject.toml
if [[ -f "${repo_path}/pyproject.toml" ]]; then
echo -e "发现 pyproject.toml,开始自动安装依赖...\n"
local pyproject_file="${dir_scripts}/${uniq_path}/pyproject.toml"
# 确保目标目录存在
make_dir "${dir_scripts}/${uniq_path}"
# 复制文件并检查结果
if cp -f "${repo_path}/pyproject.toml" "${pyproject_file}" 2>/dev/null; then
# 调用API添加依赖安装任务
local dep_name="${uniq_path}/pyproject.toml"
local currentTimeStamp=$(date +%s)
local result=$(curl -s --noproxy "*" "http://127.0.0.1:${ql_port}/open/dependencies?t=$currentTimeStamp" \
-X POST \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Authorization: Bearer ${__ql_token__}" \
--data-raw "[{\"name\":\"${dep_name}\",\"type\":1,\"remark\":\"自动检测:${uniq_path} 订阅依赖\"}]" 2>/dev/null)
local code=$(echo "$result" | jq -r '.code' 2>/dev/null)
if [[ "$code" == "200" ]]; then
echo -e "已添加 pyproject.toml 依赖安装任务\n"
else
echo -e "添加 pyproject.toml 依赖失败,请手动添加\n"
fi
else
echo -e "复制 pyproject.toml 失败,跳过自动安装\n"
fi
fi
}
## 更新仓库
update_repo() {
local url="$1"
@@ -137,6 +207,10 @@ update_repo() {
if [[ $exit_status -eq 0 ]]; then
echo -e "拉取 ${uniq_path} 成功...\n"
# 自动检测并安装Python依赖
auto_install_python_deps "${repo_path}" "${uniq_path}"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" "$autoAddCron" "$autoDelCron"
else
echo -e "拉取 ${uniq_path} 失败,请检查日志...\n"
+8 -4
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,10 +91,14 @@ const CronModal = ({
{ required: true },
{
validator: (_, value) => {
if (!value || CronExpressionParser.parse(value).hasNext()) {
return Promise.resolve();
try {
if (!value || CronExpressionParser.parse(value).hasNext()) {
return Promise.resolve();
}
return Promise.reject(intl.get('Cron表达式格式有误'));
} catch (e) {
return Promise.reject(intl.get('Cron表达式格式有误'));
}
return Promise.reject(intl.get('Cron表达式格式有误'));
},
},
]}
+18 -2
View File
@@ -22,6 +22,9 @@ const DependenceModal = ({
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [selectedType, setSelectedType] = useState(
DependenceTypes[defaultType as any],
);
const handleOk = async (values: any) => {
setLoading(true);
@@ -90,7 +93,7 @@ const DependenceModal = ({
label={intl.get('依赖类型')}
initialValue={DependenceTypes[defaultType as any]}
>
<Select>
<Select onChange={(value) => setSelectedType(value)}>
{config.dependenceTypes.map((x, i) => (
<Option key={i} value={i}>
{x}
@@ -121,11 +124,24 @@ const DependenceModal = ({
whitespace: true,
},
]}
tooltip={
selectedType === DependenceTypes.python3
? intl.get(
'Python支持多种安装方式:\n1. 包名(如:requests\n2. GitHub链接(如:git+https://github.com/user/repo.git\n3. requirements文件路径(如:path/to/requirements.txt\n4. pyproject.toml文件路径',
)
: undefined
}
>
<Input.TextArea
rows={4}
autoSize={{ minRows: 1, maxRows: 5 }}
placeholder={intl.get('请输入依赖名称')}
placeholder={
selectedType === DependenceTypes.python3
? intl.get(
'支持包名、GitHub链接、requirements.txt或pyproject.toml路径',
)
: intl.get('请输入依赖名称')
}
/>
</Form.Item>
<Form.Item name="remark" label={intl.get('备注')}>
+1 -1
View File
@@ -16,7 +16,7 @@ const SaveModal = ({
const handleOk = async (values: any) => {
setLoading(true);
const payload = { ...file, ...values, originFilename: file.title };
const payload = { ...values, originFilename: file.title, content: file.content };
request
.post(`${config.apiPrefix}scripts`, payload)
.then(({ code, data }) => {
+12 -8
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,13 +378,17 @@ const SubscriptionModal = ({
{ required: true },
{
validator: (rule, value) => {
if (
scheduleType === 'interval' ||
!value ||
CronExpressionParser.parse(value).hasNext()
) {
return Promise.resolve();
} else {
try {
if (
scheduleType === 'interval' ||
!value ||
CronExpressionParser.parse(value).hasNext()
) {
return Promise.resolve();
} else {
return Promise.reject(intl.get('Subscription表达式格式有误'));
}
} catch (e) {
return Promise.reject(intl.get('Subscription表达式格式有误'));
}
},
+6 -6
View File
@@ -84,12 +84,12 @@ let _request = axios.create({
});
const apiWhiteList = [
'/api/user/login',
'/open/auth/token',
'/api/user/two-factor/login',
'/api/system',
'/api/user/init',
'/api/user/notification/init',
`${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`,
];
_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() {
+10 -9
View File
@@ -1,10 +1,11 @@
version: 2.19.2
changeLogLink: https://t.me/jiao_long/431
publishTime: 2025-06-27 23:59
version: 2.20.1
changeLogLink: https://t.me/jiao_long/433
publishTime: 2025-12-26 22:00
changeLog: |
1. 备份数据支持选择模块,支持清除依赖缓存
2. QLAPI 和 openapi 的 systemNotify 支持自定义通知类型和参数
3. ntfy 增加可选的认证与用户动作,感谢 https://github.com/liheji
4. 修复取消安装依赖
5. 修复环境变量过大解析报错
6. 修改服务启动方式
1. 修复获取依赖管理列表
2. notify.js 修复 TG_PROXY_AUTH 参数拼接
3. QLAPI.notify larkSecret 参数
4. 修复 cron parser 定时规则校验
5. 修复设置 baseUrl 后无法访问
6. 修复环境变量排序
7. 修复定时任务无法停止