mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-06 00:34:33 +08:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| faa38294d6 | |||
| e8abaeb83c | |||
| bf7350f10b | |||
| 275d8af4e2 | |||
| 544c432f49 | |||
| 6bec52dca1 | |||
| ce599d306f | |||
| d53437d169 | |||
| d526602d19 | |||
| 91b44914f6 | |||
| 4f6c93cc1c | |||
| e326d89571 | |||
| 5f0dafa010 | |||
| dc0b3f2eb2 | |||
| 3db716763d | |||
| fae226745e | |||
| 9330650163 | |||
| 073de76a4a | |||
| c61d1aa828 | |||
| 33fa3aca99 | |||
| c772fc9527 | |||
| c5d2aa3aba | |||
| 02a05f06bd | |||
| 3b0f55caf4 | |||
| 6a3dd4f83c | |||
| 177cd3de81 | |||
| d473c3ae88 | |||
| ee2fbe5335 |
@@ -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,14 +120,21 @@ 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"
|
||||
|
||||
- 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:
|
||||
@@ -154,19 +160,13 @@ 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=schedule,pattern=nightly
|
||||
type=edge
|
||||
type=ref,event=pr
|
||||
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}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@@ -208,14 +208,21 @@ 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"
|
||||
|
||||
- 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:
|
||||
@@ -254,7 +261,9 @@ jobs:
|
||||
context: .
|
||||
file: ./docker/310.Dockerfile
|
||||
push: true
|
||||
tags: whyour/qinglong:python3.10
|
||||
tags: |
|
||||
whyour/qinglong:python3.10
|
||||
whyour/qinglong:${{ steps.version.outputs.version }}-python3.10
|
||||
cache-from: type=registry,ref=whyour/qinglong:cache-python3.10
|
||||
cache-to: type=registry,ref=whyour/qinglong:cache-python3.10,mode=max
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
+6
-4
@@ -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 {
|
||||
@@ -129,7 +129,8 @@ export default (app: Router) => {
|
||||
content: Joi.string().optional().allow(''),
|
||||
originFilename: Joi.string().optional().allow(''),
|
||||
directory: Joi.string().optional().allow(''),
|
||||
}),
|
||||
file: Joi.string().optional().allow(''),
|
||||
}).unknown(true),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
@@ -175,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) {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -426,6 +426,24 @@ export default (app: Router) => {
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/config/global-ssh-key',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
globalSshKey: Joi.string().allow('').allow(null),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const result = await systemService.updateGlobalSshKey(req.body);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/config/dependence-clean',
|
||||
celebrate({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -142,6 +142,7 @@ export class WebhookNotification extends NotificationBaseInfo {
|
||||
|
||||
export class LarkNotification extends NotificationBaseInfo {
|
||||
public larkKey = '';
|
||||
public larkSecret = '';
|
||||
}
|
||||
|
||||
export class NtfyNotification extends NotificationBaseInfo {
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface SystemConfigInfo {
|
||||
pythonMirror?: string;
|
||||
linuxMirror?: string;
|
||||
timezone?: string;
|
||||
globalSshKey?: string;
|
||||
}
|
||||
|
||||
export interface LoginLogInfo {
|
||||
|
||||
+41
-5
@@ -13,8 +13,35 @@ import { isValidToken } from '../shared/auth';
|
||||
import path from 'path';
|
||||
|
||||
export default ({ app }: { app: Application }) => {
|
||||
// Security: Enable strict routing to prevent case-insensitive path bypass
|
||||
app.set('case sensitive routing', true);
|
||||
app.set('strict routing', true);
|
||||
app.set('trust proxy', 'loopback');
|
||||
app.use(cors());
|
||||
|
||||
// Security: Path normalization middleware to prevent case variation attacks
|
||||
app.use((req, res, next) => {
|
||||
const originalPath = req.path;
|
||||
const normalizedPath = originalPath.toLowerCase();
|
||||
|
||||
// Block requests with case variations on protected paths
|
||||
if (originalPath !== normalizedPath &&
|
||||
(normalizedPath.startsWith('/api/') || normalizedPath.startsWith('/open/'))) {
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
message: 'Invalid path format'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// 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));
|
||||
|
||||
@@ -29,7 +56,7 @@ export default ({ app }: { app: Application }) => {
|
||||
secret: config.jwt.secret,
|
||||
algorithms: ['HS384'],
|
||||
}).unless({
|
||||
path: [...config.apiWhiteList, /^\/(?!api\/).*/],
|
||||
path: [...config.apiWhiteList, /^(\/(?!api\/).*)$/i],
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -44,19 +71,20 @@ export default ({ app }: { app: Application }) => {
|
||||
});
|
||||
|
||||
app.use(async (req: Request, res, next) => {
|
||||
if (!['/open/', '/api/'].some((x) => req.path.startsWith(x))) {
|
||||
const pathLower = req.path.toLowerCase();
|
||||
if (!['/open/', '/api/'].some((x) => pathLower.startsWith(x))) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const headerToken = getToken(req);
|
||||
if (req.path.startsWith('/open/')) {
|
||||
if (pathLower.startsWith('/open/')) {
|
||||
const apps = await shareStore.getApps();
|
||||
const doc = apps?.filter((x) =>
|
||||
x.tokens?.find((y) => y.value === headerToken),
|
||||
)?.[0];
|
||||
if (doc && doc.tokens && doc.tokens.length > 0) {
|
||||
const currentToken = doc.tokens.find((x) => x.value === headerToken);
|
||||
const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/);
|
||||
const keyMatch = pathLower.match(/\/open\/([a-z]+)\/*/);
|
||||
const key = keyMatch && keyMatch[1];
|
||||
if (
|
||||
doc.scopes.includes(key as any) &&
|
||||
@@ -91,7 +119,15 @@ export default ({ app }: { app: Application }) => {
|
||||
});
|
||||
|
||||
app.use(async (req, res, next) => {
|
||||
if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) {
|
||||
const pathLower = req.path.toLowerCase();
|
||||
if (
|
||||
![
|
||||
'/api/user/init',
|
||||
'/api/user/notification/init',
|
||||
'/open/user/init',
|
||||
'/open/user/notification/init',
|
||||
].includes(req.path)
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
const authInfo =
|
||||
|
||||
@@ -13,7 +13,7 @@ import { AuthDataType, SystemModel } from '../data/system';
|
||||
import SystemService from '../services/system';
|
||||
import UserService from '../services/user';
|
||||
import { writeFile, readFile } from 'fs/promises';
|
||||
import { createRandomString, fileExist, safeJSONParse } from '../config/util';
|
||||
import { createRandomString, fileExist, isDemoEnv, safeJSONParse } from '../config/util';
|
||||
import OpenService from '../services/open';
|
||||
import { shareStore } from '../shared/store';
|
||||
import Logger from './logger';
|
||||
@@ -50,7 +50,7 @@ export default async () => {
|
||||
const [authConfig] = await SystemModel.findOrCreate({
|
||||
where: { type: AuthDataType.authConfig },
|
||||
});
|
||||
if (!authConfig?.info) {
|
||||
if (!authConfig?.info || isDemoEnv()) {
|
||||
let authInfo = {
|
||||
username: 'admin',
|
||||
password: 'admin',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Container } from 'typedi';
|
||||
import SystemService from '../services/system';
|
||||
import ScheduleService, { ScheduleTaskType } from '../services/schedule';
|
||||
import SubscriptionService from '../services/subscription';
|
||||
import SshKeyService from '../services/sshKey';
|
||||
import config from '../config';
|
||||
import { fileExist } from '../config/util';
|
||||
import { join } from 'path';
|
||||
@@ -10,6 +11,7 @@ export default async () => {
|
||||
const systemService = Container.get(SystemService);
|
||||
const scheduleService = Container.get(ScheduleService);
|
||||
const subscriptionService = Container.get(SubscriptionService);
|
||||
const sshKeyService = Container.get(SshKeyService);
|
||||
|
||||
// 生成内置token
|
||||
let tokenCommand = `ts-node-transpile-only ${join(
|
||||
@@ -57,6 +59,11 @@ export default async () => {
|
||||
}
|
||||
|
||||
systemService.updateTimezone(data.info);
|
||||
|
||||
// Apply global SSH key if configured
|
||||
if (data.info.globalSshKey) {
|
||||
await sshKeyService.addGlobalSSHKey(data.info.globalSshKey, 'global');
|
||||
}
|
||||
}
|
||||
|
||||
await subscriptionService.setSshConfig();
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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 */
|
||||
|
||||
+13
-6
@@ -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}`,
|
||||
@@ -507,7 +510,7 @@ export default class CronService {
|
||||
let { id, command, log_name } = cron;
|
||||
|
||||
const uniqPath =
|
||||
log_name === '/dev/null'
|
||||
log_name === '/dev/null' || !log_name
|
||||
? await getUniqPath(command, `${id}`)
|
||||
: log_name;
|
||||
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
||||
@@ -641,7 +644,11 @@ export default class CronService {
|
||||
if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) {
|
||||
command = `${TASK_PREFIX}${tab.command}`;
|
||||
}
|
||||
let commandVariable = `real_time=${Boolean(realTime)} log_name=${tab.log_name} no_tee=true ID=${tab.id} `;
|
||||
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} `;
|
||||
}
|
||||
if (tab.task_before) {
|
||||
commandVariable += `task_before='${tab.task_before
|
||||
.replace(/'/g, "'\\''")
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'],
|
||||
]);
|
||||
|
||||
+19
-5
@@ -550,19 +550,33 @@ export default class NotificationService {
|
||||
}
|
||||
|
||||
private async lark() {
|
||||
let { larkKey } = this.params;
|
||||
let { larkKey, larkSecret } = 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: {
|
||||
msg_type: 'text',
|
||||
content: { text: `${this.title}\n\n${this.content}` },
|
||||
},
|
||||
json: body,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
if (res.StatusCode === 0 || res.code === 0) {
|
||||
|
||||
@@ -131,4 +131,32 @@ export default class SshKeyService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async addGlobalSSHKey(key: string, alias: string): Promise<void> {
|
||||
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}`);
|
||||
}
|
||||
|
||||
public async removeGlobalSSHKey(alias: string): Promise<void> {
|
||||
await this.removePrivateKeyFile(`~global_${alias}`);
|
||||
await this.removeSshConfig(`~global_${alias}`);
|
||||
}
|
||||
|
||||
private async generateGlobalSshConfig(alias: string) {
|
||||
// Create a config that matches all hosts, making this key globally available
|
||||
const config = `Host *\n IdentityFile ${path.join(
|
||||
this.sshPath,
|
||||
alias,
|
||||
)}\n StrictHostKeyChecking no\n`;
|
||||
await writeFileWithLock(
|
||||
`${path.join(this.sshPath, `${alias}.config`)}`,
|
||||
config,
|
||||
{
|
||||
encoding: 'utf8',
|
||||
mode: '600',
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,6 +530,27 @@ export default class SystemService {
|
||||
}
|
||||
}
|
||||
|
||||
public async updateGlobalSshKey(info: SystemModelInfo) {
|
||||
const oDoc = await this.getSystemConfig();
|
||||
const result = await this.updateAuthDb({
|
||||
...oDoc,
|
||||
info: { ...oDoc.info, ...info },
|
||||
});
|
||||
|
||||
// Apply the global SSH key
|
||||
const SshKeyService = require('./sshKey').default;
|
||||
const Container = require('typedi').Container;
|
||||
const sshKeyService = Container.get(SshKeyService);
|
||||
|
||||
if (info.globalSshKey) {
|
||||
await sshKeyService.addGlobalSSHKey(info.globalSshKey, 'global');
|
||||
} else {
|
||||
await sshKeyService.removeGlobalSSHKey('global');
|
||||
}
|
||||
|
||||
return { code: 200, data: result };
|
||||
}
|
||||
|
||||
public async cleanDependence(type: 'node' | 'python3') {
|
||||
if (!type || !['node', 'python3'].includes(type)) {
|
||||
return { code: 400, message: '参数错误' };
|
||||
|
||||
@@ -15,11 +15,11 @@ export function runCron(cmd: string, cron: ICron): Promise<number | void> {
|
||||
});
|
||||
|
||||
// Default to single instance mode (0) for backward compatibility
|
||||
const allowMultipleInstances =
|
||||
existingCron?.allow_multiple_instances === 1;
|
||||
const allowSingleInstances =
|
||||
existingCron?.allow_multiple_instances === 0;
|
||||
|
||||
if (
|
||||
!allowMultipleInstances &&
|
||||
allowSingleInstances &&
|
||||
existingCron &&
|
||||
existingCron.pid &&
|
||||
(existingCron.status === CrontabStatus.running ||
|
||||
|
||||
@@ -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';
|
||||
@@ -81,5 +81,5 @@ export const commonCronSchema = {
|
||||
'string.max': '日志名称不能超过100个字符',
|
||||
'string.unsafePath': '绝对路径必须在日志目录内或使用 /dev/null',
|
||||
}),
|
||||
allow_multiple_instances: Joi.number().optional().valid(0, 1),
|
||||
allow_multiple_instances: Joi.number().optional().valid(0, 1).allow(null),
|
||||
};
|
||||
|
||||
@@ -69,9 +69,10 @@ RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
||||
|
||||
ENV PNPM_HOME=${QL_DIR}/data/dep_cache/node \
|
||||
PYTHON_HOME=${QL_DIR}/data/dep_cache/python3 \
|
||||
PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3
|
||||
PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3 \
|
||||
HOME=/root
|
||||
|
||||
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin \
|
||||
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin:${HOME}/bin \
|
||||
NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules \
|
||||
PIP_CACHE_DIR=${PYTHON_HOME}/pip \
|
||||
PYTHONPATH=${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages
|
||||
|
||||
+3
-2
@@ -69,9 +69,10 @@ RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
||||
|
||||
ENV PNPM_HOME=${QL_DIR}/data/dep_cache/node \
|
||||
PYTHON_HOME=${QL_DIR}/data/dep_cache/python3 \
|
||||
PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3
|
||||
PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3 \
|
||||
HOME=/root
|
||||
|
||||
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin \
|
||||
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin:${HOME}/bin \
|
||||
NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules \
|
||||
PIP_CACHE_DIR=${PYTHON_HOME}/pip \
|
||||
PYTHONPATH=${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
export PATH="$HOME/bin:$PATH"
|
||||
|
||||
dir_shell=/ql/shell
|
||||
. $dir_shell/share.sh
|
||||
|
||||
@@ -23,11 +21,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" "🔧 已配置 DNS 解析优化 (ndots:0)"
|
||||
log_with_style "INFO" "🔧 0. 已配置 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 +39,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
-2
@@ -77,7 +77,7 @@
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"multer": "2.1.1",
|
||||
"node-schedule": "^2.1.0",
|
||||
"nodemailer": "^6.9.16",
|
||||
"p-queue-cjs": "7.3.4",
|
||||
@@ -118,7 +118,7 @@
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/jsonwebtoken": "^8.5.8",
|
||||
"@types/lodash": "^4.14.185",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/node": "^17.0.21",
|
||||
"@types/node-schedule": "^1.3.2",
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
|
||||
Generated
+30
-22
@@ -1,9 +1,5 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
overrides:
|
||||
sqlite3: git+https://github.com/whyour/node-sqlite3.git#v1.0.3
|
||||
|
||||
@@ -90,8 +86,8 @@ dependencies:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
multer:
|
||||
specifier: 1.4.5-lts.1
|
||||
version: 1.4.5-lts.1
|
||||
specifier: 2.1.1
|
||||
version: 2.1.1
|
||||
node-schedule:
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.1
|
||||
@@ -194,8 +190,8 @@ devDependencies:
|
||||
specifier: ^4.14.185
|
||||
version: 4.17.13
|
||||
'@types/multer':
|
||||
specifier: ^1.4.7
|
||||
version: 1.4.12
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
'@types/node':
|
||||
specifier: ^17.0.21
|
||||
version: 17.0.45
|
||||
@@ -3960,8 +3956,8 @@ packages:
|
||||
resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
|
||||
dev: false
|
||||
|
||||
/@types/multer@1.4.12:
|
||||
resolution: {integrity: sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==}
|
||||
/@types/multer@2.0.0:
|
||||
resolution: {integrity: sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==}
|
||||
dependencies:
|
||||
'@types/express': 4.17.21
|
||||
dev: true
|
||||
@@ -6333,13 +6329,13 @@ packages:
|
||||
/concat-map@0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
||||
/concat-stream@1.6.2:
|
||||
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
|
||||
engines: {'0': node >= 0.8}
|
||||
/concat-stream@2.0.0:
|
||||
resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
|
||||
engines: {'0': node >= 6.0}
|
||||
dependencies:
|
||||
buffer-from: 1.1.2
|
||||
inherits: 2.0.4
|
||||
readable-stream: 2.3.8
|
||||
readable-stream: 3.6.2
|
||||
typedarray: 0.0.6
|
||||
dev: false
|
||||
|
||||
@@ -6436,6 +6432,7 @@ packages:
|
||||
|
||||
/core-util-is@1.0.3:
|
||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||
dev: true
|
||||
|
||||
/cors@2.8.5:
|
||||
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
|
||||
@@ -8288,6 +8285,7 @@ packages:
|
||||
|
||||
/glob@10.4.5:
|
||||
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
hasBin: true
|
||||
dependencies:
|
||||
foreground-child: 3.3.0
|
||||
@@ -8300,7 +8298,7 @@ packages:
|
||||
|
||||
/glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
dependencies:
|
||||
fs.realpath: 1.0.0
|
||||
inflight: 1.0.6
|
||||
@@ -9171,6 +9169,7 @@ packages:
|
||||
|
||||
/isarray@1.0.0:
|
||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||
dev: true
|
||||
|
||||
/isarray@2.0.5:
|
||||
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
|
||||
@@ -10037,6 +10036,7 @@ packages:
|
||||
|
||||
/minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
dev: true
|
||||
|
||||
/minipass-collect@1.0.2:
|
||||
resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==}
|
||||
@@ -10117,6 +10117,7 @@ packages:
|
||||
hasBin: true
|
||||
dependencies:
|
||||
minimist: 1.2.8
|
||||
dev: true
|
||||
|
||||
/mkdirp@1.0.4:
|
||||
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
|
||||
@@ -10151,17 +10152,14 @@ packages:
|
||||
/ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
/multer@1.4.5-lts.1:
|
||||
resolution: {integrity: sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
/multer@2.1.1:
|
||||
resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==}
|
||||
engines: {node: '>= 10.16.0'}
|
||||
dependencies:
|
||||
append-field: 1.0.0
|
||||
busboy: 1.6.0
|
||||
concat-stream: 1.6.2
|
||||
mkdirp: 0.5.6
|
||||
object-assign: 4.1.1
|
||||
concat-stream: 2.0.0
|
||||
type-is: 1.6.18
|
||||
xtend: 4.0.2
|
||||
dev: false
|
||||
|
||||
/mz@2.7.0:
|
||||
@@ -11530,6 +11528,7 @@ packages:
|
||||
|
||||
/process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
dev: true
|
||||
|
||||
/process-okam@0.11.10:
|
||||
resolution: {integrity: sha512-p8e5nl6/OCeMalVb9dSojND5B9m/nq64WsyUfRmrTdLMKcNYcDN++/2I8WV1mTQDqrh2PQ6tIIb2A7/A38eSvw==}
|
||||
@@ -12951,6 +12950,7 @@ packages:
|
||||
safe-buffer: 5.1.2
|
||||
string_decoder: 1.1.1
|
||||
util-deprecate: 1.0.2
|
||||
dev: true
|
||||
|
||||
/readable-stream@3.6.2:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
@@ -13221,6 +13221,7 @@ packages:
|
||||
|
||||
/safe-buffer@5.1.2:
|
||||
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
||||
dev: true
|
||||
|
||||
/safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
@@ -13868,6 +13869,7 @@ packages:
|
||||
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||
dependencies:
|
||||
safe-buffer: 5.1.2
|
||||
dev: true
|
||||
|
||||
/string_decoder@1.3.0:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
@@ -14152,6 +14154,7 @@ packages:
|
||||
/tar@6.2.1:
|
||||
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
||||
engines: {node: '>=10'}
|
||||
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
dependencies:
|
||||
chownr: 2.0.0
|
||||
fs-minipass: 2.1.0
|
||||
@@ -15140,6 +15143,7 @@ packages:
|
||||
/xtend@4.0.2:
|
||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||
engines: {node: '>=0.4'}
|
||||
dev: true
|
||||
|
||||
/y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
@@ -15225,3 +15229,7 @@ packages:
|
||||
- encoding
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
+37
-5
@@ -52,6 +52,7 @@ const push_config = {
|
||||
DD_BOT_TOKEN: '', // 钉钉机器人的 DD_BOT_TOKEN
|
||||
|
||||
FSKEY: '', // 飞书机器人的 FSKEY
|
||||
FSSECRET: '', // 飞书机器人的 FSSECRET,对应安全设置里的签名校验密钥
|
||||
|
||||
// 推送到个人QQ:http://127.0.0.1/send_private_msg
|
||||
// 群:http://127.0.0.1/send_group_msg
|
||||
@@ -481,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;
|
||||
}
|
||||
@@ -989,11 +994,29 @@ function aibotkNotify(text, desp) {
|
||||
|
||||
function fsBotNotify(text, desp) {
|
||||
return new Promise((resolve) => {
|
||||
const { FSKEY } = push_config;
|
||||
const { FSKEY, FSSECRET } = 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: { msg_type: 'text', content: { text: `${text}\n\n${desp}` } },
|
||||
json: body,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
@@ -1262,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}`,
|
||||
@@ -1277,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);
|
||||
|
||||
@@ -49,6 +49,7 @@ push_config = {
|
||||
'DD_BOT_TOKEN': '', # 钉钉机器人的 DD_BOT_TOKEN
|
||||
|
||||
'FSKEY': '', # 飞书机器人的 FSKEY
|
||||
'FSSECRET': '', # 飞书机器人的 FSSECRET,对应安全设置里的签名校验密钥
|
||||
|
||||
'GOBOT_URL': '', # go-cqhttp
|
||||
# 推送到个人QQ:http://127.0.0.1/send_private_msg
|
||||
@@ -233,6 +234,20 @@ 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
-1
@@ -92,7 +92,7 @@ run_normal() {
|
||||
|
||||
cd $dir_scripts
|
||||
local relative_path="${file_param%/*}"
|
||||
if [[ ${file_param} != /* ]] && [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
|
||||
cd ${relative_path}
|
||||
file_param=${file_param/$relative_path\//}
|
||||
fi
|
||||
|
||||
@@ -42,7 +42,7 @@ def run():
|
||||
os.environ["PYTHONPATH"] = prev_pythonpath
|
||||
|
||||
split_str = "__sitecustomize__"
|
||||
file_name = sys.argv[0].replace(f"{os.getenv('dir_scripts')}/", "")
|
||||
file_name = os.path.abspath(sys.argv[0]).replace(f"{os.getenv('dir_scripts')}/", "")
|
||||
|
||||
# 创建临时文件路径
|
||||
temp_file = f"/tmp/env_{os.getpid()}.json"
|
||||
|
||||
+18
-3
@@ -104,7 +104,7 @@
|
||||
"序号": "Number",
|
||||
"备注": "Remarks",
|
||||
"更新时间": "Update Time",
|
||||
"创建时间": "Creation Time",
|
||||
"创建时间": "Created Time",
|
||||
"确认删除依赖": "Confirm to delete the dependency",
|
||||
"确认重新安装": "Confirm to reinstall",
|
||||
"确认取消安装": "Confirm to cancel install",
|
||||
@@ -252,7 +252,7 @@
|
||||
"登录日志": "Login Logs",
|
||||
"其他设置": "Other Settings",
|
||||
"关于": "About",
|
||||
"成功": "Success",
|
||||
"成功": "Successfully",
|
||||
"失败": "Failure",
|
||||
"登录时间": "Login Time",
|
||||
"登录地址": "Login Address",
|
||||
@@ -389,6 +389,7 @@
|
||||
"消息接收人": "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",
|
||||
@@ -538,5 +539,19 @@
|
||||
"单实例模式:定时启动新任务前会自动停止旧任务;多实例模式:允许同时运行多个任务实例": "Single instance mode: automatically stop old task before starting new scheduled task; Multi-instance mode: allow multiple task instances to run simultaneously",
|
||||
"请选择实例模式": "Please select instance mode",
|
||||
"单实例": "Single Instance",
|
||||
"多实例": "Multi-Instance"
|
||||
"多实例": "Multi-Instance",
|
||||
"SSH密钥": "SSH Keys",
|
||||
"别名": "Alias",
|
||||
"编辑SSH密钥": "Edit SSH Key",
|
||||
"创建SSH密钥": "Create SSH Key",
|
||||
"更新SSH密钥成功": "SSH key updated successfully",
|
||||
"创建SSH密钥成功": "SSH key created successfully",
|
||||
"请输入SSH密钥别名": "Please enter SSH key alias",
|
||||
"请输入SSH私钥": "Please enter SSH private key",
|
||||
"请输入SSH私钥内容(以 -----BEGIN 开头)": "Please enter SSH private key content (starts with -----BEGIN)",
|
||||
"确认删除SSH密钥": "Confirm to delete SSH key",
|
||||
"批量": "Batch",
|
||||
"全局SSH私钥": "Global SSH Private Key",
|
||||
"用于访问所有私有仓库的全局SSH私钥": "Global SSH private key for accessing all private repositories",
|
||||
"请输入完整的SSH私钥内容": "Please enter the complete SSH private key content"
|
||||
}
|
||||
|
||||
+16
-1
@@ -389,6 +389,7 @@
|
||||
"消息接收人": "消息接收人",
|
||||
"调用版本;专业版填写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 登录密码,也可能为特殊口令,视具体邮件服务商说明而定",
|
||||
@@ -538,5 +539,19 @@
|
||||
"单实例模式:定时启动新任务前会自动停止旧任务;多实例模式:允许同时运行多个任务实例": "单实例模式:定时启动新任务前会自动停止旧任务;多实例模式:允许同时运行多个任务实例",
|
||||
"请选择实例模式": "请选择实例模式",
|
||||
"单实例": "单实例",
|
||||
"多实例": "多实例"
|
||||
"多实例": "多实例",
|
||||
"SSH密钥": "SSH密钥",
|
||||
"别名": "别名",
|
||||
"编辑SSH密钥": "编辑SSH密钥",
|
||||
"创建SSH密钥": "创建SSH密钥",
|
||||
"更新SSH密钥成功": "更新SSH密钥成功",
|
||||
"创建SSH密钥成功": "创建SSH密钥成功",
|
||||
"请输入SSH密钥别名": "请输入SSH密钥别名",
|
||||
"请输入SSH私钥": "请输入SSH私钥",
|
||||
"请输入SSH私钥内容(以 -----BEGIN 开头)": "请输入SSH私钥内容(以 -----BEGIN 开头)",
|
||||
"确认删除SSH密钥": "确认删除SSH密钥",
|
||||
"批量": "批量",
|
||||
"全局SSH私钥": "全局SSH私钥",
|
||||
"用于访问所有私有仓库的全局SSH私钥": "用于访问所有私有仓库的全局SSH私钥",
|
||||
"请输入完整的SSH私钥内容": "请输入完整的SSH私钥内容"
|
||||
}
|
||||
|
||||
@@ -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表达式格式有误'));
|
||||
},
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -30,6 +30,7 @@ const dataMap = {
|
||||
'log-remove-frequency': 'logRemoveFrequency',
|
||||
'cron-concurrency': 'cronConcurrency',
|
||||
timezone: 'timezone',
|
||||
'global-ssh-key': 'globalSshKey',
|
||||
};
|
||||
|
||||
const exportModules = [
|
||||
@@ -54,6 +55,7 @@ const Other = ({
|
||||
logRemoveFrequency?: number | null;
|
||||
cronConcurrency?: number | null;
|
||||
timezone?: string | null;
|
||||
globalSshKey?: string | null;
|
||||
}>();
|
||||
const [form] = Form.useForm();
|
||||
const [exportLoading, setExportLoading] = useState(false);
|
||||
@@ -308,6 +310,32 @@ const Other = ({
|
||||
</Button>
|
||||
</Input.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={intl.get('全局SSH私钥')}
|
||||
name="globalSshKey"
|
||||
tooltip={intl.get('用于访问所有私有仓库的全局SSH私钥')}
|
||||
>
|
||||
<Input.Group compact>
|
||||
<Input.TextArea
|
||||
value={systemConfig?.globalSshKey || ''}
|
||||
style={{ width: 264 }}
|
||||
autoSize={{ minRows: 3, maxRows: 8 }}
|
||||
placeholder={intl.get('请输入完整的SSH私钥内容')}
|
||||
onChange={(e) => {
|
||||
setSystemConfig({ ...systemConfig, globalSshKey: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</Input.Group>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
updateSystemConfig('global-ssh-key');
|
||||
}}
|
||||
style={{ width: 264, marginTop: 8 }}
|
||||
>
|
||||
{intl.get('确认')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item label={intl.get('语言')} name="lang">
|
||||
<Select
|
||||
defaultValue={localStorage.getItem('lang') || ''}
|
||||
|
||||
@@ -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表达式格式有误'));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -395,6 +395,12 @@ export default {
|
||||
),
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: 'larkSecret',
|
||||
tip: intl.get(
|
||||
'飞书群组机器人加签密钥,安全设置中开启签名校验后获得',
|
||||
),
|
||||
},
|
||||
],
|
||||
email: [
|
||||
{
|
||||
|
||||
+6
-6
@@ -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
@@ -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() {
|
||||
|
||||
+5
-9
@@ -1,10 +1,6 @@
|
||||
version: 2.19.2
|
||||
changeLogLink: https://t.me/jiao_long/431
|
||||
publishTime: 2025-06-27 23:59
|
||||
version: 2.20.2
|
||||
changeLogLink: https://t.me/jiao_long/434
|
||||
publishTime: 2026-03-01 1800
|
||||
changeLog: |
|
||||
1. 备份数据支持选择模块,支持清除依赖缓存
|
||||
2. QLAPI 和 openapi 的 systemNotify 支持自定义通知类型和参数
|
||||
3. ntfy 增加可选的认证与用户动作,感谢 https://github.com/liheji
|
||||
4. 修复取消安装依赖
|
||||
5. 修复环境变量过大解析报错
|
||||
6. 修改服务启动方式
|
||||
1. 修复 path 安全漏洞(重要)
|
||||
|
||||
Reference in New Issue
Block a user