Compare commits

...
Author SHA1 Message Date
copilot-swe-agent[bot]andwhyour 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]andwhyour 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]andwhyour 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]andwhyour 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
CopilotGitHubwhyourcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
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
CopilotGitHubwhyourcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
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
CopilotGitHubwhyourcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
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
CopilotGitHubwhyourcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
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
CopilotGitHubwhyourwhyourcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
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
CopilotGitHubwhyourcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
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 291 additions and 74 deletions
+15 -16
View File
@@ -9,15 +9,13 @@ on:
- "develop" - "develop"
tags: tags:
- "v*" - "v*"
schedule:
- cron: "00 20 * * *"
workflow_dispatch: workflow_dispatch:
jobs: jobs:
code_gitlab: code_gitlab:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: Yikun/hub-mirror-action@master - uses: Yikun/hub-mirror-action@master
@@ -32,7 +30,7 @@ jobs:
code_gitee: code_gitee:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: Yikun/hub-mirror-action@master - uses: Yikun/hub-mirror-action@master
@@ -47,12 +45,12 @@ jobs:
build-static: build-static:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v4
with: with:
version: "8.3.1" version: "8.3.1"
- uses: actions/setup-node@v4 - uses: actions/setup-node@v6
with: with:
cache: "pnpm" cache: "pnpm"
@@ -83,7 +81,7 @@ jobs:
needs: build-static needs: build-static
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: Yikun/hub-mirror-action@master - uses: Yikun/hub-mirror-action@master
@@ -99,7 +97,7 @@ jobs:
needs: build-static needs: build-static
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: Yikun/hub-mirror-action@master - uses: Yikun/hub-mirror-action@master
@@ -112,6 +110,7 @@ jobs:
force_update: true force_update: true
build: build:
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
needs: build-static needs: build-static
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
@@ -121,11 +120,11 @@ jobs:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v4
with: with:
version: "8.3.1" version: "8.3.1"
- uses: actions/setup-node@v4 - uses: actions/setup-node@v6
with: with:
cache: "pnpm" cache: "pnpm"
@@ -164,7 +163,7 @@ jobs:
flavor: | flavor: |
latest=false latest=false
tags: | 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=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=raw,value=${{ steps.version.outputs.version }},enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=semver,pattern={{version}} type=semver,pattern={{version}}
@@ -209,11 +208,11 @@ jobs:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v4
with: with:
version: "8.3.1" version: "8.3.1"
- uses: actions/setup-node@v4 - uses: actions/setup-node@v6
with: with:
cache: "pnpm" cache: "pnpm"
+1 -1
View File
@@ -16,7 +16,7 @@ export default (app: Router) => {
searchValue: Joi.string().optional().allow(''), searchValue: Joi.string().optional().allow(''),
type: Joi.string().optional().allow(''), type: Joi.string().optional().allow(''),
status: Joi.string().optional().allow(''), status: Joi.string().optional().allow(''),
}), }).unknown(true),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
+5 -4
View File
@@ -29,7 +29,7 @@ export default (app: Router) => {
celebrate({ celebrate({
query: Joi.object({ query: Joi.object({
path: Joi.string().optional().allow(''), path: Joi.string().optional().allow(''),
}), }).unknown(true),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
@@ -79,7 +79,7 @@ export default (app: Router) => {
query: Joi.object({ query: Joi.object({
path: Joi.string().optional().allow(''), path: Joi.string().optional().allow(''),
file: Joi.string().required(), file: Joi.string().required(),
}), }).unknown(true),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
try { try {
@@ -103,7 +103,7 @@ export default (app: Router) => {
}), }),
query: Joi.object({ query: Joi.object({
path: Joi.string().optional().allow(''), path: Joi.string().optional().allow(''),
}), }).unknown(true),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
try { try {
@@ -130,7 +130,7 @@ export default (app: Router) => {
originFilename: Joi.string().optional().allow(''), originFilename: Joi.string().optional().allow(''),
directory: Joi.string().optional().allow(''), directory: Joi.string().optional().allow(''),
file: Joi.string().optional().allow(''), file: Joi.string().optional().allow(''),
}), }).unknown(true),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
try { try {
@@ -176,6 +176,7 @@ export default (app: Router) => {
path, path,
`${originFilename.replace(/\//g, '')}`, `${originFilename.replace(/\//g, '')}`,
); );
await fs.mkdir(path, { recursive: true });
const filePath = join(path, `${filename.replace(/\//g, '')}`); const filePath = join(path, `${filename.replace(/\//g, '')}`);
const fileExists = await fileExist(filePath); const fileExists = await fileExist(filePath);
if (fileExists) { if (fileExists) {
+1 -1
View File
@@ -3,7 +3,7 @@ import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
import SubscriptionService from '../services/subscription'; import SubscriptionService from '../services/subscription';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import { CronExpressionParser } from 'cron-parser'; import CronExpressionParser from 'cron-parser';
const route = Router(); const route = Router();
export default (app: 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`; 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 rootPath = process.env.QL_DIR as string;
const envFound = dotenv.config({ path: path.join(rootPath, '.env') }); const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
@@ -116,6 +129,7 @@ if (envFound.error) {
export default { export default {
...config, ...config,
jwt: config.jwt, jwt: config.jwt,
baseUrl,
rootPath, rootPath,
tmpPath, tmpPath,
dataPath, 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 { 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 = { const baseCommands = {
[DependenceTypes.nodejs]: `pnpm ls -g | grep "${name}" | head -1`, [DependenceTypes.nodejs]: `pnpm ls -g | grep "${trimmedName}" | head -1`,
[DependenceTypes.python3]: ` [DependenceTypes.python3]: `
python3 -c "exec(''' python3 -c "exec('''
name='${name}' name='${trimmedName}'
try: try:
from importlib.metadata import version from importlib.metadata import version
print(version(name)) print(version(name))
@@ -550,7 +581,7 @@ except:
spec=u.find_spec(name) spec=u.find_spec(name)
print(name if spec else '') print(name if spec else '')
''')"`, ''')"`,
[DependenceTypes.linux]: `apk info -es ${name}`, [DependenceTypes.linux]: `apk info -es ${trimmedName}`,
}; };
return baseCommands[type]; return baseCommands[type];
@@ -570,7 +601,33 @@ export function getInstallCommand(type: DependenceTypes, name: string): string {
command = `${command} --prefix=${PYTHON_INSTALL_DIR}`; 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( export function getUninstallCommand(
+7
View File
@@ -15,6 +15,13 @@ import path from 'path';
export default ({ app }: { app: Application }) => { export default ({ app }: { app: Application }) => {
app.set('trust proxy', 'loopback'); app.set('trust proxy', 'loopback');
app.use(cors()); 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.get(`${config.api.prefix}/env.js`, serveEnv);
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath)); 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 { getPlatform } from '../config/util';
import { shareStore } from '../shared/store'; import { shareStore } from '../shared/store';
import { isValidToken } from '../shared/auth'; import { isValidToken } from '../shared/auth';
import config from '../config';
export default async ({ server }: { server: Server }) => { 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); const sockService = Container.get(SockService);
echo.on('connection', async (conn) => { echo.on('connection', async (conn) => {
+1
View File
@@ -231,6 +231,7 @@ message NotificationInfo {
optional string webhookContentType = 57; optional string webhookContentType = 57;
optional string larkKey = 58; optional string larkKey = 58;
optional string larkSecret = 69;
optional string ntfyUrl = 59; optional string ntfyUrl = 59;
optional string ntfyTopic = 60; optional string ntfyTopic = 60;
+19 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT. // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions: // versions:
// protoc-gen-ts_proto v2.6.1 // protoc-gen-ts_proto v2.6.1
// protoc v3.17.3 // protoc v3.21.12
// source: back/protos/api.proto // source: back/protos/api.proto
/* eslint-disable */ /* eslint-disable */
@@ -382,6 +382,7 @@ export interface NotificationInfo {
webhookMethod?: string | undefined; webhookMethod?: string | undefined;
webhookContentType?: string | undefined; webhookContentType?: string | undefined;
larkKey?: string | undefined; larkKey?: string | undefined;
larkSecret?: string | undefined;
ntfyUrl?: string | undefined; ntfyUrl?: string | undefined;
ntfyTopic?: string | undefined; ntfyTopic?: string | undefined;
ntfyPriority?: string | undefined; ntfyPriority?: string | undefined;
@@ -2947,6 +2948,7 @@ function createBaseNotificationInfo(): NotificationInfo {
webhookMethod: undefined, webhookMethod: undefined,
webhookContentType: undefined, webhookContentType: undefined,
larkKey: undefined, larkKey: undefined,
larkSecret: undefined,
ntfyUrl: undefined, ntfyUrl: undefined,
ntfyTopic: undefined, ntfyTopic: undefined,
ntfyPriority: undefined, ntfyPriority: undefined,
@@ -3136,6 +3138,9 @@ export const NotificationInfo: MessageFns<NotificationInfo> = {
if (message.larkKey !== undefined) { if (message.larkKey !== undefined) {
writer.uint32(466).string(message.larkKey); writer.uint32(466).string(message.larkKey);
} }
if (message.larkSecret !== undefined) {
writer.uint32(554).string(message.larkSecret);
}
if (message.ntfyUrl !== undefined) { if (message.ntfyUrl !== undefined) {
writer.uint32(474).string(message.ntfyUrl); writer.uint32(474).string(message.ntfyUrl);
} }
@@ -3640,6 +3645,14 @@ export const NotificationInfo: MessageFns<NotificationInfo> = {
message.larkKey = reader.string(); message.larkKey = reader.string();
continue; continue;
} }
case 69: {
if (tag !== 554) {
break;
}
message.larkSecret = reader.string();
continue;
}
case 59: { case 59: {
if (tag !== 474) { if (tag !== 474) {
break; break;
@@ -3797,6 +3810,7 @@ export const NotificationInfo: MessageFns<NotificationInfo> = {
webhookMethod: isSet(object.webhookMethod) ? globalThis.String(object.webhookMethod) : undefined, webhookMethod: isSet(object.webhookMethod) ? globalThis.String(object.webhookMethod) : undefined,
webhookContentType: isSet(object.webhookContentType) ? globalThis.String(object.webhookContentType) : undefined, webhookContentType: isSet(object.webhookContentType) ? globalThis.String(object.webhookContentType) : undefined,
larkKey: isSet(object.larkKey) ? globalThis.String(object.larkKey) : 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, ntfyUrl: isSet(object.ntfyUrl) ? globalThis.String(object.ntfyUrl) : undefined,
ntfyTopic: isSet(object.ntfyTopic) ? globalThis.String(object.ntfyTopic) : undefined, ntfyTopic: isSet(object.ntfyTopic) ? globalThis.String(object.ntfyTopic) : undefined,
ntfyPriority: isSet(object.ntfyPriority) ? globalThis.String(object.ntfyPriority) : undefined, ntfyPriority: isSet(object.ntfyPriority) ? globalThis.String(object.ntfyPriority) : undefined,
@@ -3990,6 +4004,9 @@ export const NotificationInfo: MessageFns<NotificationInfo> = {
if (message.larkKey !== undefined) { if (message.larkKey !== undefined) {
obj.larkKey = message.larkKey; obj.larkKey = message.larkKey;
} }
if (message.larkSecret !== undefined) {
obj.larkSecret = message.larkSecret;
}
if (message.ntfyUrl !== undefined) { if (message.ntfyUrl !== undefined) {
obj.ntfyUrl = message.ntfyUrl; obj.ntfyUrl = message.ntfyUrl;
} }
@@ -4086,6 +4103,7 @@ export const NotificationInfo: MessageFns<NotificationInfo> = {
message.webhookMethod = object.webhookMethod ?? undefined; message.webhookMethod = object.webhookMethod ?? undefined;
message.webhookContentType = object.webhookContentType ?? undefined; message.webhookContentType = object.webhookContentType ?? undefined;
message.larkKey = object.larkKey ?? undefined; message.larkKey = object.larkKey ?? undefined;
message.larkSecret = object.larkSecret ?? undefined;
message.ntfyUrl = object.ntfyUrl ?? undefined; message.ntfyUrl = object.ntfyUrl ?? undefined;
message.ntfyTopic = object.ntfyTopic ?? undefined; message.ntfyTopic = object.ntfyTopic ?? undefined;
message.ntfyPriority = object.ntfyPriority ?? undefined; message.ntfyPriority = object.ntfyPriority ?? undefined;
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT. // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions: // versions:
// protoc-gen-ts_proto v2.6.1 // protoc-gen-ts_proto v2.6.1
// protoc v3.17.3 // protoc v3.21.12
// source: back/protos/cron.proto // source: back/protos/cron.proto
/* eslint-disable */ /* eslint-disable */
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT. // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions: // versions:
// protoc-gen-ts_proto v2.6.1 // protoc-gen-ts_proto v2.6.1
// protoc v3.17.3 // protoc v3.21.12
// source: back/protos/health.proto // source: back/protos/health.proto
/* eslint-disable */ /* eslint-disable */
+7 -4
View File
@@ -4,7 +4,7 @@ import config from '../config';
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron'; import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import { exec, execSync } from 'child_process'; import { exec, execSync } from 'child_process';
import fs from 'fs/promises'; import fs from 'fs/promises';
import { CronExpressionParser } from 'cron-parser'; import CronExpressionParser from 'cron-parser';
import { import {
getFileContentByName, getFileContentByName,
fileExist, fileExist,
@@ -29,7 +29,7 @@ import { logStreamManager } from '../shared/logStreamManager';
@Service() @Service()
export default class CronService { export default class CronService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) { }
private isNodeCron(cron: Crontab) { private isNodeCron(cron: Crontab) {
const { schedule, extra_schedules } = cron; const { schedule, extra_schedules } = cron;
@@ -165,7 +165,7 @@ export default class CronService {
let cron; let cron;
try { try {
cron = await this.getDb({ id }); cron = await this.getDb({ id });
} catch (err) {} } catch (err) { }
if (!cron) { if (!cron) {
continue; continue;
} }
@@ -467,7 +467,10 @@ export default class CronService {
for (const doc of docs) { for (const doc of docs) {
// Kill all running instances of this task // Kill all running instances of this task
try { try {
const command = this.makeCommand(doc); if (doc.pid) {
await killTask(doc.pid);
}
const command = doc.command.replace(/\s+/g, ' ').trim();
await killAllTasks(command); await killAllTasks(command);
this.logger.info( this.logger.info(
`[panel][停止所有运行中的任务实例] 任务ID: ${doc.id}, 命令: ${command}`, `[panel][停止所有运行中的任务实例] 任务ID: ${doc.id}, 命令: ${command}`,
+1 -1
View File
@@ -107,7 +107,7 @@ export default class DependenceService {
query: any = {}, query: any = {},
): Promise<Dependence[]> { ): Promise<Dependence[]> {
let condition = query; let condition = query;
if (DependenceTypes[type]) { if (type && DependenceTypes[type] !== undefined) {
condition.type = DependenceTypes[type]; condition.type = DependenceTypes[type];
} }
if (status) { if (status) {
+3 -2
View File
@@ -13,10 +13,11 @@ import {
stepPosition, stepPosition,
} from '../data/env'; } from '../data/env';
import { writeFileWithLock } from '../shared/utils'; import { writeFileWithLock } from '../shared/utils';
import { sequelize } from '../data';
@Service() @Service()
export default class EnvService { 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[]> { public async create(payloads: Env[]): Promise<Env[]> {
const envs = await this.envs(); const envs = await this.envs();
@@ -146,7 +147,7 @@ export default class EnvService {
} }
try { try {
const result = await this.find(condition, [ const result = await this.find(condition, [
['isPinned', 'DESC'], [sequelize.literal('COALESCE(`isPinned`, 0)'), 'DESC'],
['position', 'DESC'], ['position', 'DESC'],
['createdAt', 'ASC'], ['createdAt', 'ASC'],
]); ]);
+1 -1
View File
@@ -1,5 +1,5 @@
import { Joi } from 'celebrate'; import { Joi } from 'celebrate';
import { CronExpressionParser } from 'cron-parser'; import CronExpressionParser from 'cron-parser';
import { ScheduleType } from '../interface/schedule'; import { ScheduleType } from '../interface/schedule';
import path from 'path'; import path from 'path';
import config from '../config'; import config from '../config';
+20 -4
View File
@@ -482,9 +482,13 @@ function tgBotNotify(text, desp) {
timeout, timeout,
}; };
if (TG_PROXY_HOST && TG_PROXY_PORT) { 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; let agent;
agent = new ProxyAgent({ agent = new ProxyAgent({
uri: `http://${TG_PROXY_AUTH}${TG_PROXY_HOST}:${TG_PROXY_PORT}`, uri: `http://${proxyHost}:${TG_PROXY_PORT}`,
}); });
options.dispatcher = agent; options.dispatcher = agent;
} }
@@ -992,7 +996,10 @@ function fsBotNotify(text, desp) {
return new Promise((resolve) => { return new Promise((resolve) => {
const { FSKEY, FSSECRET } = push_config; const { FSKEY, FSSECRET } = push_config;
if (FSKEY) { 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 // Add signature if secret is provided
// Note: Feishu's signature algorithm uses timestamp+"\n"+secret as the HMAC key // 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) => { 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) { if (NTFY_TOPIC) {
const options = { const options = {
url: `${NTFY_URL || 'https://ntfy.sh'}/${NTFY_TOPIC}`, url: `${NTFY_URL || 'https://ntfy.sh'}/${NTFY_TOPIC}`,
@@ -1293,7 +1308,8 @@ function ntfyNotify(text, desp) {
if (NTFY_TOKEN) { if (NTFY_TOKEN) {
options.headers['Authorization'] = `Bearer ${NTFY_TOKEN}`; options.headers['Authorization'] = `Bearer ${NTFY_TOKEN}`;
} else if (NTFY_USERNAME && NTFY_PASSWORD) { } 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) { if (NTFY_ACTIONS) {
options.headers['Actions'] = encodeRFC2047(NTFY_ACTIONS); options.headers['Actions'] = encodeRFC2047(NTFY_ACTIONS);
+74
View File
@@ -111,6 +111,76 @@ add_cron() {
notify_api "$path 新增任务" "$detail" 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() { update_repo() {
local url="$1" local url="$1"
@@ -137,6 +207,10 @@ update_repo() {
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "拉取 ${uniq_path} 成功...\n" 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" diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" "$autoAddCron" "$autoDelCron"
else else
echo -e "拉取 ${uniq_path} 失败,请检查日志...\n" echo -e "拉取 ${uniq_path} 失败,请检查日志...\n"
+8 -4
View File
@@ -3,7 +3,7 @@ import config from '@/utils/config';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Form, Input, Modal, Select, Space, message } from 'antd'; 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 { useEffect, useState } from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { getScheduleType, scheduleTypeMap } from './const'; import { getScheduleType, scheduleTypeMap } from './const';
@@ -91,10 +91,14 @@ const CronModal = ({
{ required: true }, { required: true },
{ {
validator: (_, value) => { validator: (_, value) => {
if (!value || CronExpressionParser.parse(value).hasNext()) { try {
return Promise.resolve(); 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 [form] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [selectedType, setSelectedType] = useState(
DependenceTypes[defaultType as any],
);
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
setLoading(true); setLoading(true);
@@ -90,7 +93,7 @@ const DependenceModal = ({
label={intl.get('依赖类型')} label={intl.get('依赖类型')}
initialValue={DependenceTypes[defaultType as any]} initialValue={DependenceTypes[defaultType as any]}
> >
<Select> <Select onChange={(value) => setSelectedType(value)}>
{config.dependenceTypes.map((x, i) => ( {config.dependenceTypes.map((x, i) => (
<Option key={i} value={i}> <Option key={i} value={i}>
{x} {x}
@@ -121,11 +124,24 @@ const DependenceModal = ({
whitespace: true, 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 <Input.TextArea
rows={4} rows={4}
autoSize={{ minRows: 1, maxRows: 5 }} 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>
<Form.Item name="remark" label={intl.get('备注')}> <Form.Item name="remark" label={intl.get('备注')}>
+1 -1
View File
@@ -16,7 +16,7 @@ const SaveModal = ({
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
setLoading(true); setLoading(true);
const payload = { ...file, ...values, originFilename: file.title }; const payload = { ...values, originFilename: file.title, content: file.content };
request request
.post(`${config.apiPrefix}scripts`, payload) .post(`${config.apiPrefix}scripts`, payload)
.then(({ code, data }) => { .then(({ code, data }) => {
+12 -8
View File
@@ -12,7 +12,7 @@ import {
} from 'antd'; } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
import { CronExpressionParser } from 'cron-parser'; import CronExpressionParser from 'cron-parser';
import isNil from 'lodash/isNil'; import isNil from 'lodash/isNil';
const { Option } = Select; const { Option } = Select;
@@ -378,13 +378,17 @@ const SubscriptionModal = ({
{ required: true }, { required: true },
{ {
validator: (rule, value) => { validator: (rule, value) => {
if ( try {
scheduleType === 'interval' || if (
!value || scheduleType === 'interval' ||
CronExpressionParser.parse(value).hasNext() !value ||
) { CronExpressionParser.parse(value).hasNext()
return Promise.resolve(); ) {
} else { return Promise.resolve();
} else {
return Promise.reject(intl.get('Subscription表达式格式有误'));
}
} catch (e) {
return Promise.reject(intl.get('Subscription表达式格式有误')); return Promise.reject(intl.get('Subscription表达式格式有误'));
} }
}, },
+6 -6
View File
@@ -84,12 +84,12 @@ let _request = axios.create({
}); });
const apiWhiteList = [ const apiWhiteList = [
'/api/user/login', `${config.baseUrl}api/user/login`,
'/open/auth/token', `${config.baseUrl}open/auth/token`,
'/api/user/two-factor/login', `${config.baseUrl}api/user/two-factor/login`,
'/api/system', `${config.baseUrl}api/system`,
'/api/user/init', `${config.baseUrl}api/user/init`,
'/api/user/notification/init', `${config.baseUrl}api/user/notification/init`,
]; ];
_request.interceptors.request.use((_config) => { _request.interceptors.request.use((_config) => {
+1 -1
View File
@@ -1,6 +1,6 @@
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { LANG_MAP, LOG_END_SYMBOL } from './const'; import { LANG_MAP, LOG_END_SYMBOL } from './const';
import { CronExpressionParser } from 'cron-parser'; import CronExpressionParser from 'cron-parser';
import { ICrontab } from '@/pages/crontab/type'; import { ICrontab } from '@/pages/crontab/type';
export default function browserType() { export default function browserType() {
+10 -9
View File
@@ -1,10 +1,11 @@
version: 2.19.2 version: 2.20.1
changeLogLink: https://t.me/jiao_long/431 changeLogLink: https://t.me/jiao_long/433
publishTime: 2025-06-27 23:59 publishTime: 2025-12-26 22:00
changeLog: | changeLog: |
1. 备份数据支持选择模块,支持清除依赖缓存 1. 修复获取依赖管理列表
2. QLAPI 和 openapi 的 systemNotify 支持自定义通知类型和参数 2. notify.js 修复 TG_PROXY_AUTH 参数拼接
3. ntfy 增加可选的认证与用户动作,感谢 https://github.com/liheji 3. QLAPI.notify larkSecret 参数
4. 修复取消安装依赖 4. 修复 cron parser 定时规则校验
5. 修复环境变量过大解析报错 5. 修复设置 baseUrl 后无法访问
6. 修改服务启动方式 6. 修复环境变量排序
7. 修复定时任务无法停止