Compare commits

...
19 Commits
Author SHA1 Message Date
whyour 4667af4ebe 更新版本 v2.18.0 2025-01-05 22:03:45 +08:00
whyour 3f775a0e6c 修复创建系统应用 2025-01-05 22:03:41 +08:00
whyour fa79de3f05 修复 status 类型 2025-01-05 12:23:13 +08:00
whyour ffa8b25a66 优化初始化文件操作 2025-01-05 00:28:08 +08:00
whyour 05f8bbd26e 写入文件增加文件锁,避免竞争条件引起文件内容异常 2025-01-04 01:22:29 +08:00
whyour 7d43b14f81 修复重置登录错误次数和 tfa 2025-01-02 23:50:20 +08:00
whyour cecc5aeb15 修复初始化 SystemConfig 数据 2025-01-01 21:33:43 +08:00
whyour 43d6ac2071 增加手动停止标识 2024-12-31 00:17:30 +08:00
whyour 678e3e2dc6 修改认证信息存储方式,避免认证信息异常 2024-12-30 14:23:04 +08:00
whyour 75f91e1473 修改通知文件未设置变量提示 2024-12-22 22:15:09 +08:00
whyour bdc45c538c 更新版本 v2.17.13 2024-12-21 22:48:58 +08:00
FanchangWangandGitHub 955c7377d7 新增 wxPusher 推送通道。wxPusher 官方文档: https://wxpusher.zjiecode.com/docs/ (#2594)
* 增加 wxPusher 推送方式

* 补全 wxPusher 推送方式代码

* fix js 方法多进行了一次 JSON 格式化
2024-12-13 14:13:34 +08:00
c71abd8c86 完善pushplus通知方式的代码逻辑 (#2454)
* 1. pushplus通知方式返回结果优化
2. pushplus通知方式适配更多参数

* 细节bug修改

* 删除没用的空格

---------

Co-authored-by: 陈思远 <chensiyuan@yiban.com.cn>
Co-authored-by: 陈大人 <chensiyuan@pushplus.plus>
2024-12-13 14:12:16 +08:00
whyour ab27a4c908 更新 node 依赖 2024-12-12 00:26:11 +08:00
whyour 026640a757 修复任务列表下次运行时间展示 2024-12-03 22:35:34 +08:00
whyour a1501705c1 更新 readme 2024-11-18 23:19:15 +08:00
whyour b321530dcf 修复本地部署设置 dataPath 后,备份目录可能错误 2024-11-18 22:59:07 +08:00
xiaobingtechandGitHub 02c6ad8004 mobile增加识别鸿蒙系统 (#2571)
由于鸿蒙系统的User-Agent如下
Mozilla/5.0 (Phone; OpenHarmony5.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 ArkWeb/4.1.6.1 Mobile
导致目前登录显示desktop
2024-11-18 22:40:14 +08:00
Hugh GaoandGitHub fff572869e chore: Update docker-compose.yml (#2553) 2024-11-08 22:52:19 +08:00
52 changed files with 6189 additions and 6678 deletions
+11 -11
View File
@@ -2,22 +2,22 @@
**/*.svg **/*.svg
**/*.ejs **/*.ejs
**/*.html **/*.html
.umi /.umi
.umi-production /.umi-production
.umi-test /.umi-test
.history /.history
.tmp /.tmp
node_modules /node_modules
npm-debug.log* npm-debug.log*
yarn-error.log yarn-error.log
yarn.lock yarn.lock
package-lock.json package-lock.json
static /static
data /data
DS_Store DS_Store
src/.umi /src/.umi
src/.umi-production /src/.umi-production
src/.umi-test /src/.umi-test
.env.local .env.local
.env .env
version.ts version.ts
+2 -9
View File
@@ -43,19 +43,12 @@ export default defineConfig({
}), }),
); );
}) as any, }) as any,
externals: { headScripts: [`./api/env.js`],
react: 'window.React',
'react-dom': 'window.ReactDOM',
},
headScripts: [
`./api/env.js`,
'https://gw.alipayobjects.com/os/lib/react/18.2.0/umd/react.production.min.js',
'https://gw.alipayobjects.com/os/lib/react-dom/18.2.0/umd/react-dom.production.min.js',
],
copy: [ copy: [
{ {
from: 'node_modules/monaco-editor/min/vs', from: 'node_modules/monaco-editor/min/vs',
to: 'static/dist/monaco-editor/min/vs', to: 'static/dist/monaco-editor/min/vs',
}, },
], ],
npmClient: 'pnpm',
}); });
+1 -1
View File
@@ -126,7 +126,7 @@ curl -sL https://deb.nodesource.com/setup_20.x | sudo -E bash -
npm install -g node-pre-gyp pnpm@8.3.1 npm install -g node-pre-gyp pnpm@8.3.1
npm install -g @whyour/qinglong npm install -g @whyour/qinglong
qinglong qinglong
# Add the environment variables QL_DIR and QL_DATA_DIR when prompted # Add the environment variables QL_DIR and QL_DATA_DIR when prompted, QL_DATA_DIR must end with /data.
export QL_DIR="" export QL_DIR=""
export QL_DATA_DIR="" export QL_DATA_DIR=""
# Run again # Run again
+2 -2
View File
@@ -88,7 +88,7 @@ docker run -dit \
```bash ```bash
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose # curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
mkdir qinglong mkdir qinglong && cd $_
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
# 启动 # 启动
@@ -128,7 +128,7 @@ curl -sL https://deb.nodesource.com/setup_20.x | sudo -E bash -
npm install -g node-pre-gyp pnpm@8.3.1 npm install -g node-pre-gyp pnpm@8.3.1
npm install -g @whyour/qinglong npm install -g @whyour/qinglong
qinglong qinglong
# 根据提示增加环境变量 QL_DIR 和 QL_DATA_DIR # 根据提示增加环境变量 QL_DIR 和 QL_DATA_DIRQL_DATA_DIR 必须以 /data 结尾
export QL_DIR="" export QL_DIR=""
export QL_DATA_DIR="" export QL_DATA_DIR=""
# 再次执行 # 再次执行
+2 -1
View File
@@ -7,6 +7,7 @@ import { celebrate, Joi } from 'celebrate';
import { join } from 'path'; import { join } from 'path';
import { SAMPLE_FILES } from '../config/const'; import { SAMPLE_FILES } from '../config/const';
import ConfigService from '../services/config'; import ConfigService from '../services/config';
import { writeFileWithLock } from '../shared/utils';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
@@ -77,7 +78,7 @@ export default (app: Router) => {
if (name.startsWith('data/scripts/')) { if (name.startsWith('data/scripts/')) {
path = join(config.rootPath, name); path = join(config.rootPath, name);
} }
await fs.writeFile(path, content); await writeFileWithLock(path, content);
res.send({ code: 200, message: '保存成功' }); res.send({ code: 200, message: '保存成功' });
} catch (e) { } catch (e) {
return next(e); return next(e);
+4 -3
View File
@@ -8,6 +8,7 @@ import { celebrate, Joi } from 'celebrate';
import path, { join, parse } from 'path'; import path, { join, parse } from 'path';
import ScriptService from '../services/script'; import ScriptService from '../services/script';
import multer from 'multer'; import multer from 'multer';
import { writeFileWithLock } from '../shared/utils';
const route = Router(); const route = Router();
const storage = multer.diskStorage({ const storage = multer.diskStorage({
@@ -156,7 +157,7 @@ export default (app: Router) => {
await rmPath(originFilePath); await rmPath(originFilePath);
} }
} }
await fs.writeFile(filePath, content); await writeFileWithLock(filePath, content);
return res.send({ code: 200 }); return res.send({ code: 200 });
} catch (e) { } catch (e) {
return next(e); return next(e);
@@ -182,7 +183,7 @@ export default (app: Router) => {
path: string; path: string;
}; };
const filePath = join(config.scriptPath, path, filename); const filePath = join(config.scriptPath, path, filename);
await fs.writeFile(filePath, content); await writeFileWithLock(filePath, content);
return res.send({ code: 200 }); return res.send({ code: 200 });
} catch (e) { } catch (e) {
return next(e); return next(e);
@@ -261,7 +262,7 @@ export default (app: Router) => {
let { filename, content, path } = req.body; let { filename, content, path } = req.body;
const { name, ext } = parse(filename); const { name, ext } = parse(filename);
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`); const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
await fs.writeFile(filePath, content || '', { encoding: 'utf8' }); await writeFileWithLock(filePath, content || '');
const scriptService = Container.get(ScriptService); const scriptService = Container.get(ScriptService);
const result = await scriptService.runScript(filePath); const result = await scriptService.runScript(filePath);
+20 -1
View File
@@ -33,7 +33,7 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const userService = Container.get(UserService); const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo(); const authInfo = await userService.getAuthInfo();
const { version, changeLog, changeLogLink, publishTime } = const { version, changeLog, changeLogLink, publishTime } =
await parseVersion(config.versionFile); await parseVersion(config.versionFile);
@@ -377,4 +377,23 @@ export default (app: Router) => {
} }
}, },
); );
route.put(
'/auth/reset',
celebrate({
body: Joi.object({
retries: Joi.number().optional(),
twoFactorActivated: Joi.boolean().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const userService = Container.get(UserService);
await userService.resetAuthInfo(req.body);
res.send({ code: 200, message: '更新成功' });
} catch (e) {
return next(e);
}
},
);
}; };
+1 -1
View File
@@ -90,7 +90,7 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const userService = Container.get(UserService); const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo(); const authInfo = await userService.getAuthInfo();
res.send({ res.send({
code: 200, code: 200,
data: { data: {
+5 -2
View File
@@ -10,6 +10,7 @@ import { load } from 'js-yaml';
import config from './index'; import config from './index';
import { TASK_COMMAND } from './const'; import { TASK_COMMAND } from './const';
import Logger from '../loaders/logger'; import Logger from '../loaders/logger';
import { writeFileWithLock } from '../shared/utils';
export * from './share'; export * from './share';
@@ -145,12 +146,14 @@ export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
system = 'android'; // android系统 system = 'android'; // android系统
} else if (testUa(/ios|iphone|ipad|ipod|iwatch/g)) { } else if (testUa(/ios|iphone|ipad|ipod|iwatch/g)) {
system = 'ios'; // ios系统 system = 'ios'; // ios系统
} else if (testUa(/openharmony/g)) {
system = 'openharmony'; // openharmony系统
} }
let platform = 'desktop'; let platform = 'desktop';
if (system === 'windows' || system === 'macos' || system === 'linux') { if (system === 'windows' || system === 'macos' || system === 'linux') {
platform = 'desktop'; platform = 'desktop';
} else if (system === 'android' || system === 'ios' || testUa(/mobile/g)) { } else if (system === 'android' || system === 'ios' || system === 'openharmony' || testUa(/mobile/g)) {
platform = 'mobile'; platform = 'mobile';
} }
@@ -168,7 +171,7 @@ export async function fileExist(file: any) {
export async function createFile(file: string, data: string = '') { export async function createFile(file: string, data: string = '') {
await fs.mkdir(path.dirname(file), { recursive: true }); await fs.mkdir(path.dirname(file), { recursive: true });
await fs.writeFile(file, data); await writeFileWithLock(file, data);
} }
export async function handleLogPath( export async function handleLogPath(
+15 -2
View File
@@ -21,6 +21,7 @@ export enum NotificationMode {
'webhook' = 'webhook', 'webhook' = 'webhook',
'chronocat' = 'Chronocat', 'chronocat' = 'Chronocat',
'ntfy' = 'ntfy', 'ntfy' = 'ntfy',
'wxPusherBot' = 'wxPusherBot',
} }
abstract class NotificationBaseInfo { abstract class NotificationBaseInfo {
@@ -100,6 +101,11 @@ export class IGotNotification extends NotificationBaseInfo {
export class PushPlusNotification extends NotificationBaseInfo { export class PushPlusNotification extends NotificationBaseInfo {
public pushPlusToken = ''; public pushPlusToken = '';
public pushPlusUser = ''; public pushPlusUser = '';
public pushPlusTemplate = '';
public pushplusChannel = '';
public pushplusWebhook = '';
public pushplusCallbackUrl = '';
public pushplusTo = '';
} }
export class WePlusBotNotification extends NotificationBaseInfo { export class WePlusBotNotification extends NotificationBaseInfo {
@@ -145,6 +151,13 @@ export class NtfyNotification extends NotificationBaseInfo {
public ntfyTopic = ''; public ntfyTopic = '';
public ntfyPriority = ''; public ntfyPriority = '';
} }
export class WxPusherBotNotification extends NotificationBaseInfo {
public wxPusherBotAppToken = '';
public wxPusherBotTopicIds = '';
public wxPusherBotUids = '';
}
export interface NotificationInfo export interface NotificationInfo
extends GoCqHttpBotNotification, extends GoCqHttpBotNotification,
GotifyNotification, GotifyNotification,
@@ -165,5 +178,5 @@ export interface NotificationInfo
WebhookNotification, WebhookNotification,
ChronocatNotification, ChronocatNotification,
LarkNotification, LarkNotification,
NtfyNotification {} NtfyNotification,
WxPusherBotNotification {}
+1 -1
View File
@@ -24,7 +24,7 @@ export interface AppToken {
expiration: number; expiration: number;
} }
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs'; export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs' | 'system';
export interface AppInstance extends Model<App, App>, App {} export interface AppInstance extends Model<App, App>, App {}
export const AppModel = sequelize.define<AppInstance>('App', { export const AppModel = sequelize.define<AppInstance>('App', {
+22 -2
View File
@@ -27,6 +27,7 @@ export enum AuthDataType {
'notification' = 'notification', 'notification' = 'notification',
'removeLogFrequency' = 'removeLogFrequency', 'removeLogFrequency' = 'removeLogFrequency',
'systemConfig' = 'systemConfig', 'systemConfig' = 'systemConfig',
'authConfig' = 'authConfig',
} }
export interface SystemConfigInfo { export interface SystemConfigInfo {
@@ -46,11 +47,30 @@ export interface LoginLogInfo {
status?: LoginStatus; status?: LoginStatus;
} }
export interface AuthInfo {
username: string;
password: string;
retries: number;
lastlogon: number;
lastip: string;
lastaddr: string;
platform: string;
isTwoFactorChecking: boolean;
token: string;
tokens: Record<string, string>;
twoFactorActivated: boolean;
twoFactorSecret: string;
avatar: string;
}
export type SystemModelInfo = SystemConfigInfo & export type SystemModelInfo = SystemConfigInfo &
Partial<NotificationInfo> & Partial<NotificationInfo> &
LoginLogInfo; LoginLogInfo &
Partial<AuthInfo>;
export interface SystemInstance extends Model<SystemInfo, SystemInfo>, SystemInfo { } export interface SystemInstance
extends Model<SystemInfo, SystemInfo>,
SystemInfo {}
export const SystemModel = sequelize.define<SystemInstance>('Auth', { export const SystemModel = sequelize.define<SystemInstance>('Auth', {
ip: DataTypes.STRING, ip: DataTypes.STRING,
type: DataTypes.STRING, type: DataTypes.STRING,
+14 -16
View File
@@ -3,19 +3,15 @@ import bodyParser from 'body-parser';
import cors from 'cors'; import cors from 'cors';
import routes from '../api'; import routes from '../api';
import config from '../config'; import config from '../config';
import jwt, { UnauthorizedError } from 'express-jwt'; import { UnauthorizedError, expressjwt } from 'express-jwt';
import fs from 'fs/promises'; import { getPlatform, getToken } from '../config/util';
import { getPlatform, getToken, safeJSONParse } from '../config/util';
import Container from 'typedi';
import OpenService from '../services/open';
import rewrite from 'express-urlrewrite'; import rewrite from 'express-urlrewrite';
import UserService from '../services/user';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { EnvModel } from '../data/env';
import { errors } from 'celebrate'; import { errors } from 'celebrate';
import { createProxyMiddleware } from 'http-proxy-middleware'; import { createProxyMiddleware } from 'http-proxy-middleware';
import { serveEnv } from '../config/serverEnv'; import { serveEnv } from '../config/serverEnv';
import Logger from './logger'; import Logger from './logger';
import { IKeyvStore, shareStore } from '../shared/store';
export default ({ app }: { app: Application }) => { export default ({ app }: { app: Application }) => {
app.set('trust proxy', 'loopback'); app.set('trust proxy', 'loopback');
@@ -29,7 +25,7 @@ export default ({ app }: { app: Application }) => {
target: `http://0.0.0.0:${config.publicPort}/api`, target: `http://0.0.0.0:${config.publicPort}/api`,
changeOrigin: true, changeOrigin: true,
pathRewrite: { '/api/public': '' }, pathRewrite: { '/api/public': '' },
logProvider: () => Logger, logger: Logger,
}), }),
); );
@@ -37,7 +33,7 @@ export default ({ app }: { app: Application }) => {
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
app.use( app.use(
jwt({ expressjwt({
secret: config.secret, secret: config.secret,
algorithms: ['HS384'], algorithms: ['HS384'],
}).unless({ }).unless({
@@ -58,8 +54,10 @@ export default ({ app }: { app: Application }) => {
app.use(async (req, res, next) => { app.use(async (req, res, next) => {
const headerToken = getToken(req); const headerToken = getToken(req);
if (req.path.startsWith('/open/')) { if (req.path.startsWith('/open/')) {
const openService = Container.get(OpenService); const apps = await shareStore.getApps();
const doc = await openService.findTokenByValue(headerToken); const doc = apps?.filter((x) =>
x.tokens?.find((y) => y.value === headerToken),
)?.[0];
if (doc && doc.tokens && doc.tokens.length > 0) { if (doc && doc.tokens && doc.tokens.length > 0) {
const currentToken = doc.tokens.find((x) => x.value === headerToken); const currentToken = doc.tokens.find((x) => x.value === headerToken);
const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/); const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/);
@@ -83,9 +81,9 @@ export default ({ app }: { app: Application }) => {
return next(); return next();
} }
const data = await fs.readFile(config.authConfigFile, 'utf8'); const authInfo = await shareStore.getAuthInfo();
if (data && headerToken) { if (authInfo && headerToken) {
const { token = '', tokens = {} } = safeJSONParse(data); const { token = '', tokens = {} } = authInfo;
if (headerToken === token || tokens[req.platform] === headerToken) { if (headerToken === token || tokens[req.platform] === headerToken) {
return next(); return next();
} }
@@ -103,8 +101,8 @@ export default ({ app }: { app: Application }) => {
if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) { if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) {
return next(); return next();
} }
const userService = Container.get(UserService); const authInfo =
const authInfo = await userService.getUserInfo(); (await shareStore.getAuthInfo()) || ({} as IKeyvStore['authInfo']);
let isInitialized = true; let isInitialized = true;
if ( if (
+42 -6
View File
@@ -12,7 +12,11 @@ import { initPosition } from '../data/env';
import { AuthDataType, SystemModel } from '../data/system'; import { AuthDataType, SystemModel } from '../data/system';
import SystemService from '../services/system'; import SystemService from '../services/system';
import UserService from '../services/user'; import UserService from '../services/user';
import { writeFile } from 'fs/promises'; import { writeFile, readFile } from 'fs/promises';
import { safeJSONParse } from '../config/util';
import OpenService from '../services/open';
import { shareStore } from '../shared/store';
import Logger from './logger';
export default async () => { export default async () => {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
@@ -20,14 +24,40 @@ export default async () => {
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
const systemService = Container.get(SystemService); const systemService = Container.get(SystemService);
const userService = Container.get(UserService); const userService = Container.get(UserService);
const openService = Container.get(OpenService);
// 初始化增加系统配置 // 初始化增加系统配置
await SystemModel.upsert({ type: AuthDataType.systemConfig }); const [systemConfig] = await SystemModel.findOrCreate({
await SystemModel.upsert({ type: AuthDataType.notification }); where: { type: AuthDataType.systemConfig },
});
const [notifyConfig] = await SystemModel.findOrCreate({
where: { type: AuthDataType.notification },
});
const [authConfig] = await SystemModel.findOrCreate({
where: { type: AuthDataType.authConfig },
});
if (!authConfig?.info) {
let authInfo = {
username: 'admin',
password: 'admin',
};
try {
const content = await readFile(config.authConfigFile, 'utf8');
authInfo = safeJSONParse(content);
} catch (error) {
Logger.warn('Failed to read auth config file, using default credentials');
}
await SystemModel.upsert({
id: authConfig?.id,
info: authInfo,
type: AuthDataType.authConfig,
});
}
// 初始化通知配置 // 初始化通知配置
const notifyConfig = await userService.getNotificationMode(); if (notifyConfig.info) {
await writeFile(config.systemNotifyFile, JSON.stringify(notifyConfig)); await writeFile(config.systemNotifyFile, JSON.stringify(notifyConfig.info));
}
const installDependencies = () => { const installDependencies = () => {
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖 // 初始化时安装所有处于安装中,安装成功,安装失败的依赖
@@ -50,7 +80,6 @@ export default async () => {
}; };
// 初始化更新 linux/python/nodejs 镜像源配置 // 初始化更新 linux/python/nodejs 镜像源配置
const systemConfig = await systemService.getSystemConfig();
if (systemConfig.info?.pythonMirror) { if (systemConfig.info?.pythonMirror) {
systemService.updatePythonMirror({ systemService.updatePythonMirror({
pythonMirror: systemConfig.info?.pythonMirror, pythonMirror: systemConfig.info?.pythonMirror,
@@ -169,4 +198,11 @@ export default async () => {
// 初始化保存一次ck和定时任务数据 // 初始化保存一次ck和定时任务数据
await cronService.autosave_crontab(); await cronService.autosave_crontab();
await envService.set_envs(); await envService.set_envs();
const authInfo = await userService.getAuthInfo();
const apps = await openService.findApps();
await shareStore.updateAuthInfo(authInfo);
if (apps?.length) {
await shareStore.updateApps(apps);
}
}; };
+77 -103
View File
@@ -3,6 +3,7 @@ import path from 'path';
import os from 'os'; import os from 'os';
import Logger from './logger'; import Logger from './logger';
import { fileExist } from '../config/util'; import { fileExist } from '../config/util';
import { writeFileWithLock } from '../shared/utils';
const rootPath = process.env.QL_DIR as string; const rootPath = process.env.QL_DIR as string;
let dataPath = path.join(rootPath, 'data/'); let dataPath = path.join(rootPath, 'data/');
@@ -20,9 +21,7 @@ const bakPath = path.join(dataPath, 'bak/');
const samplePath = path.join(rootPath, 'sample/'); const samplePath = path.join(rootPath, 'sample/');
const tmpPath = path.join(logPath, '.tmp/'); const tmpPath = path.join(logPath, '.tmp/');
const confFile = path.join(configPath, 'config.sh'); const confFile = path.join(configPath, 'config.sh');
const authConfigFile = path.join(configPath, 'auth.json');
const sampleConfigFile = path.join(samplePath, 'config.sample.sh'); const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
const sampleAuthFile = path.join(samplePath, 'auth.sample.json');
const sampleTaskShellFile = path.join(samplePath, 'task.sample.sh'); const sampleTaskShellFile = path.join(samplePath, 'task.sample.sh');
const sampleNotifyJsFile = path.join(samplePath, 'notify.js'); const sampleNotifyJsFile = path.join(samplePath, 'notify.js');
const sampleNotifyPyFile = path.join(samplePath, 'notify.py'); const sampleNotifyPyFile = path.join(samplePath, 'notify.py');
@@ -39,112 +38,87 @@ const sshPath = path.resolve(homedir, '.ssh');
const sshdPath = path.join(dataPath, 'ssh.d'); const sshdPath = path.join(dataPath, 'ssh.d');
const systemLogPath = path.join(dataPath, 'syslog'); const systemLogPath = path.join(dataPath, 'syslog');
export default async () => { const directories = [
const authFileExist = await fileExist(authConfigFile); configPath,
const confFileExist = await fileExist(confFile); scriptPath,
const scriptDirExist = await fileExist(scriptPath); preloadPath,
const preloadDirExist = await fileExist(preloadPath); logPath,
const logDirExist = await fileExist(logPath); tmpPath,
const configDirExist = await fileExist(configPath); uploadPath,
const uploadDirExist = await fileExist(uploadPath); sshPath,
const sshDirExist = await fileExist(sshPath); bakPath,
const bakDirExist = await fileExist(bakPath); sshdPath,
const sshdDirExist = await fileExist(sshdPath); systemLogPath,
const systemLogDirExist = await fileExist(systemLogPath); ];
const tmpDirExist = await fileExist(tmpPath);
const scriptNotifyJsFileExist = await fileExist(scriptNotifyJsFile);
const scriptNotifyPyFileExist = await fileExist(scriptNotifyPyFile);
const TaskBeforeFileExist = await fileExist(TaskBeforeFile);
const TaskBeforeJsFileExist = await fileExist(TaskBeforeJsFile);
const TaskBeforePyFileExist = await fileExist(TaskBeforePyFile);
const TaskAfterFileExist = await fileExist(TaskAfterFile);
if (!configDirExist) { const files = [
await fs.mkdir(configPath); {
} target: confFile,
source: sampleConfigFile,
if (!scriptDirExist) { checkExistence: true,
await fs.mkdir(scriptPath); },
} {
target: jsNotifyFile,
if (!preloadDirExist) { source: sampleNotifyJsFile,
await fs.mkdir(preloadPath); checkExistence: false,
} },
{
if (!logDirExist) { target: pyNotifyFile,
await fs.mkdir(logPath); source: sampleNotifyPyFile,
} checkExistence: false,
},
if (!tmpDirExist) { {
await fs.mkdir(tmpPath); target: scriptNotifyJsFile,
} source: sampleNotifyJsFile,
checkExistence: true,
if (!uploadDirExist) { },
await fs.mkdir(uploadPath); {
} target: scriptNotifyPyFile,
source: sampleNotifyPyFile,
if (!sshDirExist) { checkExistence: true,
await fs.mkdir(sshPath); },
} {
target: TaskBeforeFile,
if (!bakDirExist) { source: sampleTaskShellFile,
await fs.mkdir(bakPath); checkExistence: true,
} },
{
if (!sshdDirExist) { target: TaskBeforeJsFile,
await fs.mkdir(sshdPath); content:
}
if (!systemLogDirExist) {
await fs.mkdir(systemLogPath);
}
// 初始化文件
if (!authFileExist) {
await fs.writeFile(authConfigFile, await fs.readFile(sampleAuthFile));
}
if (!confFileExist) {
await fs.writeFile(confFile, await fs.readFile(sampleConfigFile));
}
await fs.writeFile(jsNotifyFile, await fs.readFile(sampleNotifyJsFile));
await fs.writeFile(pyNotifyFile, await fs.readFile(sampleNotifyPyFile));
if (!scriptNotifyJsFileExist) {
await fs.writeFile(
scriptNotifyJsFile,
await fs.readFile(sampleNotifyJsFile),
);
}
if (!scriptNotifyPyFileExist) {
await fs.writeFile(
scriptNotifyPyFile,
await fs.readFile(sampleNotifyPyFile),
);
}
if (!TaskBeforeFileExist) {
await fs.writeFile(TaskBeforeFile, await fs.readFile(sampleTaskShellFile));
}
if (!TaskBeforeJsFileExist) {
await fs.writeFile(
TaskBeforeJsFile,
'// The JavaScript code that executes before the JavaScript task execution will execute.', '// The JavaScript code that executes before the JavaScript task execution will execute.',
); checkExistence: true,
} },
{
if (!TaskBeforePyFileExist) { target: TaskBeforePyFile,
await fs.writeFile( content:
TaskBeforePyFile,
'# The Python code that executes before the Python task execution will execute.', '# The Python code that executes before the Python task execution will execute.',
); checkExistence: true,
},
{
target: TaskAfterFile,
source: sampleTaskShellFile,
checkExistence: true,
},
];
export default async () => {
for (const dirPath of directories) {
if (!(await fileExist(dirPath))) {
await fs.mkdir(dirPath);
}
} }
if (!TaskAfterFileExist) { for (const item of files) {
await fs.writeFile(TaskAfterFile, await fs.readFile(sampleTaskShellFile)); const exists = await fileExist(item.target);
if (!item.checkExistence || !exists) {
if (!item.content && !item.source) {
throw new Error(
`Neither content nor source specified for ${item.target}`,
);
}
const content = item.content || (await fs.readFile(item.source!));
await writeFileWithLock(item.target, content);
}
} }
Logger.info('✌️ Init file down'); Logger.info('✌️ Init file down');
+5 -6
View File
@@ -2,9 +2,8 @@ import sockJs from 'sockjs';
import { Server } from 'http'; import { Server } from 'http';
import { Container } from 'typedi'; import { Container } from 'typedi';
import SockService from '../services/sock'; import SockService from '../services/sock';
import config from '../config/index'; import { getPlatform } from '../config/util';
import fs from 'fs/promises'; import { shareStore } from '../shared/store';
import { getPlatform, safeJSONParse } from '../config/util';
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: '/api/ws', log: () => {} });
@@ -15,11 +14,11 @@ export default async ({ server }: { server: Server }) => {
conn.close('404'); conn.close('404');
} }
const data = await fs.readFile(config.authConfigFile, 'utf8'); const authInfo = await shareStore.getAuthInfo();
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop'; const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
const headerToken = conn.url.replace(`${conn.pathname}?token=`, ''); const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
if (data) { if (authInfo) {
const { token = '', tokens = {} } = safeJSONParse(data); const { token = '', tokens = {} } = authInfo;
if (headerToken === token || tokens[platform] === headerToken) { if (headerToken === token || tokens[platform] === headerToken) {
sockService.addClient(conn); sockService.addClient(conn);
+2 -2
View File
@@ -2,7 +2,7 @@ import bodyParser from 'body-parser';
import { errors } from 'celebrate'; import { errors } from 'celebrate';
import cors from 'cors'; import cors from 'cors';
import { Application, NextFunction, Request, Response } from 'express'; import { Application, NextFunction, Request, Response } from 'express';
import jwt from 'express-jwt'; import { expressjwt } from 'express-jwt';
import Container from 'typedi'; import Container from 'typedi';
import config from '../config'; import config from '../config';
import SystemService from '../services/system'; import SystemService from '../services/system';
@@ -16,7 +16,7 @@ export default ({ app }: { app: Application }) => {
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
app.use( app.use(
jwt({ expressjwt({
secret: config.secret, secret: config.secret,
algorithms: ['HS384'], algorithms: ['HS384'],
}), }),
-1
View File
@@ -17,7 +17,6 @@ server.bindAsync(
if (err) { if (err) {
throw err; throw err;
} }
server.start();
Logger.debug(`✌️ 定时服务启动成功!`); Logger.debug(`✌️ 定时服务启动成功!`);
console.debug(`✌️ 定时服务启动成功!`); console.debug(`✌️ 定时服务启动成功!`);
process.send?.('ready'); process.send?.('ready');
+2 -1
View File
@@ -21,6 +21,7 @@ import { spawn } from 'cross-spawn';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import pickBy from 'lodash/pickBy'; import pickBy from 'lodash/pickBy';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { writeFileWithLock } from '../shared/utils';
@Service() @Service()
export default class CronService { export default class CronService {
@@ -601,7 +602,7 @@ export default class CronService {
} }
}); });
await fs.writeFile(config.crontabFile, crontab_string); await writeFileWithLock(config.crontabFile, crontab_string);
execSync(`crontab ${config.crontabFile}`); execSync(`crontab ${config.crontabFile}`);
await CrontabModel.update({ saved: true }, { where: {} }); await CrontabModel.update({ saved: true }, { where: {} });
+1 -1
View File
@@ -353,7 +353,7 @@ export default class DependenceService {
}); });
this.updateLog(depIds, message); this.updateLog(depIds, message);
let status = null; let status: number;
if (isSucceed) { if (isSucceed) {
status = isInstall status = isInstall
? DependenceStatus.installed ? DependenceStatus.installed
+4 -3
View File
@@ -13,6 +13,7 @@ import {
} from '../data/env'; } from '../data/env';
import groupBy from 'lodash/groupBy'; import groupBy from 'lodash/groupBy';
import { FindOptions, Op } from 'sequelize'; import { FindOptions, Op } from 'sequelize';
import { writeFileWithLock } from '../shared/utils';
@Service() @Service()
export default class EnvService { export default class EnvService {
@@ -225,8 +226,8 @@ export default class EnvService {
} }
} }
} }
await fs.writeFile(config.envFile, env_string); await writeFileWithLock(config.envFile, env_string);
await fs.writeFile(config.jsEnvFile, js_env_string); await writeFileWithLock(config.jsEnvFile, js_env_string);
await fs.writeFile(config.pyEnvFile, py_env_string); await writeFileWithLock(config.pyEnvFile, py_env_string);
} }
} }
+63 -10
View File
@@ -36,6 +36,7 @@ export default class NotificationService {
['lark', this.lark], ['lark', this.lark],
['chronocat', this.chronocat], ['chronocat', this.chronocat],
['ntfy', this.ntfy], ['ntfy', this.ntfy],
['wxPusherBot', this.wxPusherBot],
]); ]);
private title = ''; private title = '';
@@ -522,19 +523,26 @@ export default class NotificationService {
} }
private async pushPlus() { private async pushPlus() {
const { pushPlusToken, pushPlusUser } = this.params; const { pushPlusToken, pushPlusUser, pushplusWebhook, pushPlusTemplate, pushplusChannel, pushplusCallbackUrl, pushplusTo} = this.params;
const url = `https://www.pushplus.plus/send`; const url = `https://www.pushplus.plus/send`;
try { try {
let body = {
...this.gotOption,
json: {
token: `${pushPlusToken}`,
title: `${this.title}`,
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
topic: `${pushPlusUser || ''}`,
template: `${pushPlusTemplate || 'html'}`,
channel: `${pushplusChannel || 'wechat'}`,
webhook: `${pushplusWebhook || ''}`,
callbackUrl: `${pushplusCallbackUrl || ''}`,
to: `${pushplusTo || ''}`
},
}
const res: any = await got const res: any = await got
.post(url, { .post(url, body)
...this.gotOption,
json: {
token: `${pushPlusToken}`,
title: `${this.title}`,
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
topic: `${pushPlusUser || ''}`,
},
})
.json(); .json();
if (res.code === 200) { if (res.code === 200) {
@@ -689,6 +697,51 @@ export default class NotificationService {
} }
} }
private async wxPusherBot() {
const { wxPusherBotAppToken, wxPusherBotTopicIds, wxPusherBotUids } = this.params;
// 处理 topicIds,将分号分隔的字符串转为数组
const topicIds = wxPusherBotTopicIds ? wxPusherBotTopicIds.split(';')
.map(id => id.trim())
.filter(id => id)
.map(id => parseInt(id)) : [];
// 处理 uids,将分号分隔的字符串转为数组
const uids = wxPusherBotUids ? wxPusherBotUids.split(';')
.map(uid => uid.trim())
.filter(uid => uid) : [];
// topic_ids 和 uids 至少要有一个
if (!topicIds.length && !uids.length) {
throw new Error('wxPusher 服务的 TopicIds 和 Uids 至少配置一个才行');
}
const url = `https://wxpusher.zjiecode.com/api/send/message`;
try {
const res: any = await got
.post(url, {
...this.gotOption,
json: {
appToken: wxPusherBotAppToken,
content: `<h1>${this.title}</h1><br/><div style='white-space: pre-wrap;'>${this.content}</div>`,
summary: this.title,
contentType: 2,
topicIds: topicIds,
uids: uids,
verifyPayType: 0
},
})
.json();
if (res.code === 1000) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async chronocat() { private async chronocat() {
const { chronocatURL, chronocatQQ, chronocatToken } = this.params; const { chronocatURL, chronocatQQ, chronocatToken } = this.params;
+25 -16
View File
@@ -1,19 +1,18 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import { createRandomString } from '../config/util'; import { createRandomString } from '../config/util';
import config from '../config';
import { App, AppModel } from '../data/open'; import { App, AppModel } from '../data/open';
import { v4 as uuidV4 } from 'uuid'; import { v4 as uuidV4 } from 'uuid';
import sequelize, { Op } from 'sequelize'; import sequelize, { Op } from 'sequelize';
import { shareStore } from '../shared/store';
@Service() @Service()
export default class OpenService { export default class OpenService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
public async findTokenByValue(token: string): Promise<App | null> { public async findApps(): Promise<App[] | null> {
const docs = await this.find({}); const docs = await this.find({});
const doc = docs.filter((x) => x.tokens?.find((y) => y.value === token)); return docs;
return doc[0];
} }
public async create(payload: App): Promise<App> { public async create(payload: App): Promise<App> {
@@ -21,6 +20,8 @@ export default class OpenService {
tab.client_id = createRandomString(12, 12); tab.client_id = createRandomString(12, 12);
tab.client_secret = createRandomString(24, 24); tab.client_secret = createRandomString(24, 24);
const doc = await this.insert(tab); const doc = await this.insert(tab);
const apps = await this.find({});
await shareStore.updateApps(apps);
return { ...doc, tokens: [] }; return { ...doc, tokens: [] };
} }
@@ -34,17 +35,19 @@ export default class OpenService {
name: payload.name, name: payload.name,
scopes: payload.scopes, scopes: payload.scopes,
id: payload.id, id: payload.id,
} as any); } as App);
return { ...newDoc, tokens: [] }; return { ...newDoc, tokens: [] };
} }
private async updateDb(payload: App): Promise<App> { private async updateDb(payload: Partial<App>): Promise<App> {
await AppModel.update(payload, { where: { id: payload.id } }); await AppModel.update(payload, { where: { id: payload.id } });
return await this.getDb({ id: payload.id }); const apps = await this.find({});
await shareStore.updateApps(apps);
return apps?.find((x) => x.id === payload.id) as App;
} }
public async getDb(query: any): Promise<App> { public async getDb(query: Record<string, any>): Promise<App> {
const doc: any = await AppModel.findOne({ where: query }); const doc = await AppModel.findOne({ where: query });
if (!doc) { if (!doc) {
throw new Error(`App ${JSON.stringify(query)} not found`); throw new Error(`App ${JSON.stringify(query)} not found`);
} }
@@ -53,10 +56,12 @@ export default class OpenService {
public async remove(ids: number[]) { public async remove(ids: number[]) {
await AppModel.destroy({ where: { id: ids } }); await AppModel.destroy({ where: { id: ids } });
const apps = await this.find({});
await shareStore.updateApps(apps);
} }
public async resetSecret(id: number): Promise<App> { public async resetSecret(id: number): Promise<App> {
const tab: any = { const tab: Partial<App> = {
client_secret: createRandomString(24, 24), client_secret: createRandomString(24, 24),
tokens: [], tokens: [],
id, id,
@@ -74,7 +79,7 @@ export default class OpenService {
public async list( public async list(
searchText: string = '', searchText: string = '',
sort: any = {}, sort: any = {},
query: any = {}, query: Record<string, any> = {},
): Promise<App[]> { ): Promise<App[]> {
let condition = { ...query }; let condition = { ...query };
if (searchText) { if (searchText) {
@@ -101,7 +106,7 @@ export default class OpenService {
} }
} }
private async find(query: any, sort?: any): Promise<App[]> { private async find(query: Record<string, any>, sort?: any): Promise<App[]> {
const docs = await AppModel.findAll({ where: { ...query } }); const docs = await AppModel.findAll({ where: { ...query } });
return docs.map((x) => x.get({ plain: true })); return docs.map((x) => x.get({ plain: true }));
} }
@@ -135,6 +140,8 @@ export default class OpenService {
{ tokens }, { tokens },
{ where: { client_id, client_secret } }, { where: { client_id, client_secret } },
); );
const apps = await this.find({});
await shareStore.updateApps(apps);
return { return {
code: 200, code: 200,
data: { data: {
@@ -144,7 +151,7 @@ export default class OpenService {
}, },
}; };
} else { } else {
return { code: 400, message: 'client_idclient_seret有误' }; return { code: 400, message: 'client_idclient_seret 有误' };
} }
} }
@@ -152,9 +159,11 @@ export default class OpenService {
value: string; value: string;
expiration: number; expiration: number;
}> { }> {
let systemApp = (await AppModel.findOne({ let systemApp = (
where: { name: 'system' }, await AppModel.findOne({
})) as App; where: { name: 'system' },
})
)?.get({ plain: true });
if (!systemApp) { if (!systemApp) {
systemApp = await this.create({ systemApp = await this.create({
name: 'system', name: 'system',
+1 -1
View File
@@ -214,7 +214,7 @@ export default class ScheduleService {
const job = new LongIntervalJob( const job = new LongIntervalJob(
{ runImmediately: false, ...schedule }, { runImmediately: false, ...schedule },
task, task,
_id, { id: _id },
); );
this.intervalSchedule.addIntervalJob(job); this.intervalSchedule.addIntervalJob(job);
+17 -12
View File
@@ -7,6 +7,7 @@ import { Subscription } from '../data/subscription';
import { formatUrl } from '../config/subscription'; import { formatUrl } from '../config/subscription';
import config from '../config'; import config from '../config';
import { fileExist, rmPath } from '../config/util'; import { fileExist, rmPath } from '../config/util';
import { writeFileWithLock } from '../shared/utils';
@Service() @Service()
export default class SshKeyService { export default class SshKeyService {
@@ -25,13 +26,12 @@ export default class SshKeyService {
if (_exist) { if (_exist) {
config = await fs.readFile(this.sshConfigFilePath, { encoding: 'utf-8' }); config = await fs.readFile(this.sshConfigFilePath, { encoding: 'utf-8' });
} else { } else {
await fs.writeFile(this.sshConfigFilePath, ''); await writeFileWithLock(this.sshConfigFilePath, '');
} }
if (!config.includes(this.sshConfigHeader)) { if (!config.includes(this.sshConfigHeader)) {
await fs.writeFile( await writeFileWithLock(
this.sshConfigFilePath, this.sshConfigFilePath,
`${this.sshConfigHeader}\n\n${config}`, `${this.sshConfigHeader}\n\n${config}`,
{ encoding: 'utf-8' },
); );
} }
} }
@@ -41,10 +41,14 @@ export default class SshKeyService {
key: string, key: string,
): Promise<void> { ): Promise<void> {
try { try {
await fs.writeFile(path.join(this.sshPath, alias), `${key}${os.EOL}`, { await writeFileWithLock(
encoding: 'utf8', path.join(this.sshPath, alias),
mode: '400', `${key}${os.EOL}`,
}); {
encoding: 'utf8',
mode: '400',
},
);
} catch (error) { } catch (error) {
this.logger.error('生成私钥文件失败', error); this.logger.error('生成私钥文件失败', error);
} }
@@ -74,12 +78,9 @@ export default class SshKeyService {
this.sshPath, this.sshPath,
alias, alias,
)}\n StrictHostKeyChecking no\n${proxyStr}`; )}\n StrictHostKeyChecking no\n${proxyStr}`;
await fs.writeFile( await writeFileWithLock(
`${path.join(this.sshPath, `${alias}.config`)}`, `${path.join(this.sshPath, `${alias}.config`)}`,
config, config,
{
encoding: 'utf8',
},
); );
} }
@@ -102,7 +103,11 @@ export default class SshKeyService {
await this.generateSingleSshConfig(alias, host, proxy); await this.generateSingleSshConfig(alias, host, proxy);
} }
public async removeSSHKey(alias: string, host: string, proxy?: string): Promise<void> { public async removeSSHKey(
alias: string,
host: string,
proxy?: string,
): Promise<void> {
await this.removePrivateKeyFile(alias); await this.removePrivateKeyFile(alias);
await this.removeSshConfig(alias); await this.removeSshConfig(alias);
} }
+6 -5
View File
@@ -54,13 +54,14 @@ export default class SystemService {
} }
private async updateAuthDb(payload: SystemInfo): Promise<SystemInfo> { private async updateAuthDb(payload: SystemInfo): Promise<SystemInfo> {
await SystemModel.upsert({ ...payload }); const { id, ...others } = payload;
const doc = await this.getDb({ type: payload.type }); await SystemModel.update(others, { where: { id } });
const doc = await this.getDb({ id });
return doc; return doc;
} }
public async getDb(query: any): Promise<SystemInfo> { public async getDb(query: any): Promise<SystemInfo> {
const doc = await SystemModel.findOne({ where: { ...query } }); const doc = await SystemModel.findOne({ where: query });
if (!doc) { if (!doc) {
throw new Error(`System ${JSON.stringify(query)} not found`); throw new Error(`System ${JSON.stringify(query)} not found`);
} }
@@ -402,7 +403,7 @@ export default class SystemService {
public async exportData(res: Response) { public async exportData(res: Response) {
try { try {
await promiseExec( await promiseExec(
`cd ${config.rootPath} && tar -zcvf ${config.dataTgzFile} data/`, `cd ${config.dataPath} && cd ../ && tar -zcvf ${config.dataTgzFile} data/`,
); );
res.download(config.dataTgzFile); res.download(config.dataTgzFile);
} catch (error: any) { } catch (error: any) {
@@ -414,7 +415,7 @@ export default class SystemService {
try { try {
await promiseExec(`rm -rf ${path.join(config.tmpPath, 'data')}`); await promiseExec(`rm -rf ${path.join(config.tmpPath, 'data')}`);
const res = await promiseExec( const res = await promiseExec(
`cd ${config.tmpPath} && tar -zxvf data.tgz`, `cd ${config.tmpPath} && tar -zxvf ${config.dataTgzFile}`,
); );
return { code: 200, data: res }; return { code: 200, data: res };
} catch (error: any) { } catch (error: any) {
+147 -182
View File
@@ -18,6 +18,7 @@ import {
SystemModel, SystemModel,
SystemModelInfo, SystemModelInfo,
LoginStatus, LoginStatus,
AuthInfo,
} from '../data/system'; } from '../data/system';
import { NotificationInfo } from '../data/notify'; import { NotificationInfo } from '../data/notify';
import NotificationService from './notify'; import NotificationService from './notify';
@@ -28,6 +29,7 @@ import dayjs from 'dayjs';
import IP2Region from 'ip2region'; import IP2Region from 'ip2region';
import requestIp from 'request-ip'; import requestIp from 'request-ip';
import uniq from 'lodash/uniq'; import uniq from 'lodash/uniq';
import { shareStore } from '../shared/store';
@Service() @Service()
export default class UserService { export default class UserService {
@@ -48,161 +50,138 @@ export default class UserService {
req: Request, req: Request,
needTwoFactor = true, needTwoFactor = true,
): Promise<any> { ): Promise<any> {
const _exist = await fileExist(config.authConfigFile);
if (!_exist) {
return this.initAuthInfo();
}
let { username, password } = payloads; let { username, password } = payloads;
const content = await this.getAuthInfo(); const content = await this.getAuthInfo();
const timestamp = Date.now(); const timestamp = Date.now();
if (content) { let {
let { username: cUsername,
username: cUsername, password: cPassword,
password: cPassword, retries = 0,
retries = 0, lastlogon,
lastlogon, lastip,
lastip, lastaddr,
lastaddr, twoFactorActivated,
twoFactorActivated, tokens = {},
twoFactorActived, platform,
tokens = {}, } = content;
platform, const retriesTime = Math.pow(3, retries) * 1000;
} = content; if (retries > 2 && timestamp - lastlogon < retriesTime) {
// patch old field const waitTime = Math.ceil(
twoFactorActivated = twoFactorActivated || twoFactorActived; (retriesTime - (timestamp - lastlogon)) / 1000,
);
return {
code: 410,
message: `失败次数过多,请${waitTime}秒后重试`,
data: waitTime,
};
}
if ( if (
(cUsername === 'admin' && cPassword === 'admin') || username === cUsername &&
!cUsername || password === cPassword &&
!cPassword twoFactorActivated &&
) { needTwoFactor
return this.initAuthInfo(); ) {
} await this.updateAuthInfo(content, {
isTwoFactorChecking: true,
});
return {
code: 420,
message: '',
};
}
const retriesTime = Math.pow(3, retries) * 1000; const ip = requestIp.getClientIp(req) || '';
if (retries > 2 && timestamp - lastlogon < retriesTime) { const query = new IP2Region();
const waitTime = Math.ceil( const ipAddress = query.search(ip);
(retriesTime - (timestamp - lastlogon)) / 1000, let address = '';
); if (ipAddress) {
const { country, province, city, isp } = ipAddress;
address = uniq([country, province, city, isp]).filter(Boolean).join(' ');
}
if (username === cUsername && password === cPassword) {
const data = createRandomString(50, 100);
const expiration = twoFactorActivated ? 60 : 20;
let token = jwt.sign({ data }, config.secret as any, {
expiresIn: 60 * 60 * 24 * expiration,
algorithm: 'HS384',
});
await this.updateAuthInfo(content, {
token,
tokens: {
...tokens,
[req.platform]: token,
},
lastlogon: timestamp,
retries: 0,
lastip: ip,
lastaddr: address,
platform: req.platform,
isTwoFactorChecking: false,
});
this.notificationService.notify(
'登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform
}端 登录成功,ip地址 ${ip}`,
);
await this.insertDb({
type: AuthDataType.loginLog,
info: {
timestamp,
address,
ip,
platform: req.platform,
status: LoginStatus.success,
},
});
this.getLoginLog();
return {
code: 200,
data: { token, lastip, lastaddr, lastlogon, retries, platform },
};
} else {
await this.updateAuthInfo(content, {
retries: retries + 1,
lastlogon: timestamp,
lastip: ip,
lastaddr: address,
platform: req.platform,
});
this.notificationService.notify(
'登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform
}端 登录失败,ip地址 ${ip}`,
);
await this.insertDb({
type: AuthDataType.loginLog,
info: {
timestamp,
address,
ip,
platform: req.platform,
status: LoginStatus.fail,
},
});
this.getLoginLog();
if (retries > 2) {
const waitTime = Math.round(Math.pow(3, retries + 1));
return { return {
code: 410, code: 410,
message: `失败次数过多,请${waitTime}秒后重试`, message: `失败次数过多,请${waitTime}秒后重试`,
data: waitTime, data: waitTime,
}; };
}
if (
username === cUsername &&
password === cPassword &&
twoFactorActivated &&
needTwoFactor
) {
this.updateAuthInfo(content, {
isTwoFactorChecking: true,
});
return {
code: 420,
message: '',
};
}
const ip = requestIp.getClientIp(req) || '';
const query = new IP2Region();
const ipAddress = query.search(ip);
let address = '';
if (ipAddress) {
const { country, province, city, isp } = ipAddress;
address = uniq([country, province, city, isp])
.filter(Boolean)
.join(' ');
}
if (username === cUsername && password === cPassword) {
const data = createRandomString(50, 100);
const expiration = twoFactorActivated ? 60 : 20;
let token = jwt.sign({ data }, config.secret as any, {
expiresIn: 60 * 60 * 24 * expiration,
algorithm: 'HS384',
});
this.updateAuthInfo(content, {
token,
tokens: {
...tokens,
[req.platform]: token,
},
lastlogon: timestamp,
retries: 0,
lastip: ip,
lastaddr: address,
platform: req.platform,
isTwoFactorChecking: false,
});
this.notificationService.notify(
'登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform
}端 登录成功,ip地址 ${ip}`,
);
await this.insertDb({
type: AuthDataType.loginLog,
info: {
timestamp,
address,
ip,
platform: req.platform,
status: LoginStatus.success,
},
});
this.getLoginLog();
return {
code: 200,
data: { token, lastip, lastaddr, lastlogon, retries, platform },
};
} else { } else {
this.updateAuthInfo(content, { return { code: 400, message: config.authError };
retries: retries + 1,
lastlogon: timestamp,
lastip: ip,
lastaddr: address,
platform: req.platform,
});
this.notificationService.notify(
'登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform
}端 登录失败,ip地址 ${ip}`,
);
await this.insertDb({
type: AuthDataType.loginLog,
info: {
timestamp,
address,
ip,
platform: req.platform,
status: LoginStatus.fail,
},
});
this.getLoginLog();
if (retries > 2) {
const waitTime = Math.round(Math.pow(3, retries + 1));
return {
code: 410,
message: `失败次数过多,请${waitTime}秒后重试`,
data: waitTime,
};
} else {
return { code: 400, message: config.authError };
}
} }
} else {
return this.initAuthInfo();
} }
} }
public async logout(platform: string): Promise<any> { public async logout(platform: string): Promise<any> {
const authInfo = await this.getAuthInfo(); const authInfo = await this.getAuthInfo();
this.updateAuthInfo(authInfo, { await this.updateAuthInfo(authInfo, {
token: '', token: '',
tokens: { ...authInfo.tokens, [platform]: '' }, tokens: { ...authInfo.tokens, [platform]: '' },
}); });
@@ -232,20 +211,6 @@ export default class UserService {
return doc; return doc;
} }
private async initAuthInfo() {
await fs.writeFile(
config.authConfigFile,
JSON.stringify({
username: 'admin',
password: 'admin',
}),
);
return {
code: 100,
message: '未找到认证文件,重新初始化',
};
}
public async updateUsernameAndPassword({ public async updateUsernameAndPassword({
username, username,
password, password,
@@ -257,35 +222,21 @@ export default class UserService {
return { code: 400, message: '密码不能设置为admin' }; return { code: 400, message: '密码不能设置为admin' };
} }
const authInfo = await this.getAuthInfo(); const authInfo = await this.getAuthInfo();
this.updateAuthInfo(authInfo, { username, password }); await this.updateAuthInfo(authInfo, { username, password });
return { code: 200, message: '更新成功' }; return { code: 200, message: '更新成功' };
} }
public async updateAvatar(avatar: string) { public async updateAvatar(avatar: string) {
const authInfo = await this.getAuthInfo(); const authInfo = await this.getAuthInfo();
this.updateAuthInfo(authInfo, { avatar }); await this.updateAuthInfo(authInfo, { avatar });
return { code: 200, data: avatar, message: '更新成功' }; return { code: 200, data: avatar, message: '更新成功' };
} }
public async getUserInfo(): Promise<any> {
const authFileExist = await fileExist(config.authConfigFile);
if (!authFileExist) {
await createFile(
config.authConfigFile,
JSON.stringify({
username: 'admin',
password: 'admin',
}),
);
}
return await this.getAuthInfo();
}
public async initTwoFactor() { public async initTwoFactor() {
const secret = authenticator.generateSecret(); const secret = authenticator.generateSecret();
const authInfo = await this.getAuthInfo(); const authInfo = await this.getAuthInfo();
const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret); const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);
this.updateAuthInfo(authInfo, { twoFactorSecret: secret }); await this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
return { secret, url: otpauth }; return { secret, url: otpauth };
} }
@@ -296,7 +247,7 @@ export default class UserService {
secret: authInfo.twoFactorSecret, secret: authInfo.twoFactorSecret,
}); });
if (isValid) { if (isValid) {
this.updateAuthInfo(authInfo, { twoFactorActivated: true }); await this.updateAuthInfo(authInfo, { twoFactorActivated: true });
} }
return isValid; return isValid;
} }
@@ -322,7 +273,7 @@ export default class UserService {
return this.login({ username, password }, req, false); return this.login({ username, password }, req, false);
} else { } else {
const { ip, address } = await getNetIp(req); const { ip, address } = await getNetIp(req);
this.updateAuthInfo(authInfo, { await this.updateAuthInfo(authInfo, {
lastip: ip, lastip: ip,
lastaddr: address, lastaddr: address,
platform: req.platform, platform: req.platform,
@@ -333,24 +284,29 @@ export default class UserService {
public async deactiveTwoFactor() { public async deactiveTwoFactor() {
const authInfo = await this.getAuthInfo(); const authInfo = await this.getAuthInfo();
this.updateAuthInfo(authInfo, { await this.updateAuthInfo(authInfo, {
twoFactorActivated: false, twoFactorActivated: false,
twoFactorActived: false,
twoFactorSecret: '', twoFactorSecret: '',
}); });
return true; return true;
} }
private async getAuthInfo() { public async getAuthInfo() {
const content = await fs.readFile(config.authConfigFile, 'utf8'); const authInfo = await shareStore.getAuthInfo();
return safeJSONParse(content); if (authInfo) {
return authInfo;
}
const doc = await this.getDb({ type: AuthDataType.authConfig });
return (doc.info || {}) as AuthInfo;
} }
private async updateAuthInfo(authInfo: any, info: any) { private async updateAuthInfo(authInfo: AuthInfo, info: Partial<AuthInfo>) {
await fs.writeFile( const result = { ...authInfo, ...info };
config.authConfigFile, await shareStore.updateAuthInfo(result);
JSON.stringify({ ...authInfo, ...info }), await this.updateAuthDb({
); type: AuthDataType.authConfig,
info: result,
});
} }
public async getNotificationMode(): Promise<NotificationInfo> { public async getNotificationMode(): Promise<NotificationInfo> {
@@ -359,7 +315,7 @@ export default class UserService {
} }
private async updateAuthDb(payload: SystemInfo): Promise<any> { private async updateAuthDb(payload: SystemInfo): Promise<any> {
let doc = await SystemModel.findOne({ type: payload.type }); let doc = await SystemModel.findOne({ where: { type: payload.type } });
if (doc) { if (doc) {
const updateResult = await SystemModel.update(payload, { const updateResult = await SystemModel.update(payload, {
where: { id: doc.id }, where: { id: doc.id },
@@ -397,4 +353,13 @@ export default class UserService {
return { code: 400, message: '通知发送失败,请检查参数' }; return { code: 400, message: '通知发送失败,请检查参数' };
} }
} }
public async resetAuthInfo(info: Partial<AuthInfo>) {
const { retries, twoFactorActivated } = info;
const authInfo = await this.getAuthInfo();
await this.updateAuthInfo(authInfo, {
retries,
twoFactorActivated,
});
}
} }
+34
View File
@@ -0,0 +1,34 @@
import { AuthInfo } from '../data/system';
import { App } from '../data/open';
import Keyv from 'keyv';
import KeyvSqlite from '@keyv/sqlite';
import config from '../config';
import path from 'path';
export enum EKeyv {
'apps' = 'apps',
'authInfo' = 'authInfo',
}
export interface IKeyvStore {
apps: App[];
authInfo: AuthInfo;
}
const keyvSqlite = new KeyvSqlite(path.join(config.dbPath, 'keyv.sqlite'));
export const keyvStore = new Keyv<IKeyvStore>({ store: keyvSqlite });
export const shareStore = {
getAuthInfo() {
return keyvStore.get<IKeyvStore['authInfo']>(EKeyv.authInfo);
},
updateAuthInfo(value: IKeyvStore['authInfo']) {
return keyvStore.set<IKeyvStore['authInfo']>(EKeyv.authInfo, value);
},
getApps() {
return keyvStore.get<IKeyvStore['apps']>(EKeyv.apps);
},
updateApps(apps: App[]) {
return keyvStore.set<IKeyvStore['apps']>(EKeyv.apps, apps);
},
};
+27
View File
@@ -0,0 +1,27 @@
import { lock } from 'proper-lockfile';
import { writeFile, open } from 'fs/promises';
import { fileExist } from '../config/util';
export async function writeFileWithLock(
path: string,
content: string | Buffer,
options: Parameters<typeof writeFile>[2] = {},
) {
if (typeof options === 'string') {
options = { encoding: options };
}
if (!(await fileExist(path))) {
const fileHandle = await open(path, 'w');
fileHandle.close();
}
const release = await lock(path, {
retries: {
retries: 10,
factor: 2,
minTimeout: 100,
maxTimeout: 3000,
},
});
await writeFile(path, content, { encoding: 'utf8', ...options });
await release();
}
+2 -10
View File
@@ -6,6 +6,7 @@ import fs from 'fs';
import config from './config'; import config from './config';
import path from 'path'; import path from 'path';
import os from 'os'; import os from 'os';
import { writeFileWithLock } from './shared/utils';
const tokenFile = path.join(config.configPath, 'token.json'); const tokenFile = path.join(config.configPath, 'token.json');
@@ -25,16 +26,7 @@ async function getToken() {
} }
async function writeFile(data: any) { async function writeFile(data: any) {
return new Promise<void>((resolve, reject) => { await writeFileWithLock(tokenFile, `${JSON.stringify(data)}${os.EOL}`);
fs.writeFile(
tokenFile,
`${JSON.stringify(data)}${os.EOL}`,
{ encoding: 'utf8' },
() => {
resolve();
},
);
});
} }
getToken(); getToken();
-13
View File
@@ -1,13 +0,0 @@
import { CommandModule } from 'yargs';
export const updateCommand: CommandModule = {
command: 'update',
describe: 'Update and restart qinglong',
builder: (yargs) => {
return yargs.option('repositority', {
type: 'string',
alias: 'r',
describe: `Specify the release warehouse address of the package`,
});
},
handler: async (argv) => {},
};
-13
View File
@@ -1,13 +0,0 @@
import * as yargs from 'yargs';
import { green, red } from 'chalk';
import { updateCommand } from './commands/update';
yargs
.usage('Usage: ql [command] <options>')
.command(updateCommand)
.fail((err) => {
console.error(`${red(err)}`);
})
.alias('h', 'help')
.showHelp()
.recommendCommands().argv;
+3 -8
View File
@@ -1,15 +1,10 @@
version: '2'
services: services:
web: web:
# alpine 基础镜像版本 image: whyour/qinglong:latest # 基于 Debian 的版本:whyour/qinglong:debian
image: whyour/qinglong:latest
# debian-slim 基础镜像版本
# image: whyour/qinglong:debian
volumes: volumes:
- ./data:/ql/data - ./data:/ql/data
ports: ports:
- "0.0.0.0:5700:5700" - "5700:5700"
environment: environment:
# 部署路径非必须,以斜杠开头和结尾,比如 /test/ QlBaseUrl: '/' # 部署路径非必须,以斜杠开头和结尾,比如 /test/
QlBaseUrl: '/'
restart: unless-stopped restart: unless-stopped
+44 -40
View File
@@ -54,64 +54,68 @@
"react-dom": "18", "react-dom": "18",
"dva-core": "2" "dva-core": "2"
} }
},
"overrides": {
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3"
} }
}, },
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.8.13", "@grpc/grpc-js": "^1.12.3",
"@otplib/preset-default": "^12.0.1", "@otplib/preset-default": "^12.0.1",
"@sentry/node": "^8.26.0", "@sentry/node": "^8.42.0",
"body-parser": "^1.19.2", "body-parser": "^1.20.3",
"celebrate": "^15.0.1", "celebrate": "^15.0.3",
"chokidar": "^3.5.3", "chokidar": "^4.0.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron-parser": "^4.2.1", "cron-parser": "^4.9.0",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.6",
"dayjs": "^1.11.2", "dayjs": "^1.11.13",
"dotenv": "^16.0.0", "dotenv": "^16.4.6",
"express": "^4.17.3", "express": "^4.21.1",
"express-jwt": "^6.1.1", "express-jwt": "^8.4.1",
"express-rate-limit": "^7.0.0", "express-rate-limit": "^7.4.1",
"express-urlrewrite": "^1.4.0", "express-urlrewrite": "^2.0.3",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"got": "^11.8.2", "got": "^11.8.2",
"hpagent": "^1.2.0", "hpagent": "^1.2.0",
"http-proxy-middleware": "^2.0.6", "http-proxy-middleware": "^3.0.3",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",
"nedb": "^1.8.0", "nedb": "^1.8.0",
"node-schedule": "^2.1.0", "node-schedule": "^2.1.0",
"nodemailer": "^6.7.2", "nodemailer": "^6.9.16",
"p-queue-cjs": "7.3.4", "p-queue-cjs": "7.3.4",
"protobufjs": "^7.3.0", "protobufjs": "^7.4.0",
"pstree.remy": "^1.1.8", "pstree.remy": "^1.1.8",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.2.2",
"sequelize": "^6.25.5", "sequelize": "^6.37.5",
"serve-handler": "^6.1.3", "serve-handler": "^6.1.6",
"sockjs": "^0.3.24", "sockjs": "^0.3.24",
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3", "sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3",
"toad-scheduler": "^1.6.0", "toad-scheduler": "^3.0.1",
"typedi": "^0.10.0", "typedi": "^0.10.0",
"uuid": "^8.3.2", "uuid": "^11.0.3",
"winston": "^3.6.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^4.7.1", "winston-daily-rotate-file": "^5.0.0",
"yargs": "^17.3.1",
"tough-cookie": "^4.0.0",
"request-ip": "3.3.0", "request-ip": "3.3.0",
"ip2region": "2.3.0" "ip2region": "2.3.0",
"keyv": "^5.2.3",
"@keyv/sqlite": "^4.0.1",
"proper-lockfile": "^4.1.2"
}, },
"devDependencies": { "devDependencies": {
"moment": "2.30.1", "moment": "2.30.1",
"@ant-design/icons": "^4.7.0", "@ant-design/icons": "^5.0.1",
"@ant-design/pro-layout": "6.38.22", "@ant-design/pro-layout": "6.38.22",
"@codemirror/view": "^6.34.1", "@codemirror/view": "^6.34.1",
"@codemirror/state": "^6.4.1", "@codemirror/state": "^6.4.1",
"@monaco-editor/react": "4.2.1", "@monaco-editor/react": "4.2.1",
"@react-hook/resize-observer": "^1.2.6", "@react-hook/resize-observer": "^2.0.2",
"react-router-dom": "6.26.1", "react-router-dom": "6.26.1",
"@sentry/react": "^8.26.0", "@sentry/react": "^8.42.0",
"@types/body-parser": "^1.19.2", "@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/cross-spawn": "^6.0.2", "@types/cross-spawn": "^6.0.2",
@@ -135,18 +139,19 @@
"@types/sockjs-client": "^1.5.1", "@types/sockjs-client": "^1.5.1",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@types/request-ip": "0.0.41", "@types/request-ip": "0.0.41",
"@types/proper-lockfile": "^4.1.4",
"@uiw/codemirror-extensions-langs": "^4.21.9", "@uiw/codemirror-extensions-langs": "^4.21.9",
"@uiw/react-codemirror": "^4.21.9", "@uiw/react-codemirror": "^4.21.9",
"@umijs/max": "^4.0.72", "@umijs/max": "^4.3.36",
"@umijs/ssr-darkreader": "^4.9.45", "@umijs/ssr-darkreader": "^4.9.45",
"ahooks": "^3.7.8", "ahooks": "^3.7.8",
"ansi-to-react": "^6.1.6", "ansi-to-react": "^6.1.6",
"antd": "^4.24.8", "antd": "^4.24.8",
"antd-img-crop": "^4.2.3", "antd-img-crop": "^4.23.0",
"axios": "^1.4.0", "axios": "^1.4.0",
"compression-webpack-plugin": "9.2.0", "compression-webpack-plugin": "9.2.0",
"concurrently": "^7.0.0", "concurrently": "^7.0.0",
"react-hotkeys-hook": "^4.4.1", "react-hotkeys-hook": "^4.6.1",
"file-saver": "2.0.2", "file-saver": "2.0.2",
"lint-staged": "^13.0.3", "lint-staged": "^13.0.3",
"monaco-editor": "0.33.0", "monaco-editor": "0.33.0",
@@ -157,14 +162,14 @@
"qrcode.react": "^1.0.1", "qrcode.react": "^1.0.1",
"query-string": "^7.1.1", "query-string": "^7.1.1",
"rc-tween-one": "^3.0.6", "rc-tween-one": "^3.0.6",
"rc-virtual-list": "3.5.3", "rc-virtual-list": "3.15.0",
"react": "18.2.0", "react": "18.3.1",
"react-copy-to-clipboard": "^5.1.0", "react-copy-to-clipboard": "^5.1.0",
"react-diff-viewer": "^3.1.1", "react-diff-viewer": "^3.1.1",
"react-dnd": "^14.0.2", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^14.0.0", "react-dnd-html5-backend": "^16.0.1",
"react-dom": "18.2.0", "react-dom": "18.3.1",
"react-intl-universal": "^2.6.21", "react-intl-universal": "^2.12.0",
"react-split-pane": "^0.1.92", "react-split-pane": "^0.1.92",
"sockjs-client": "^1.6.0", "sockjs-client": "^1.6.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
@@ -173,7 +178,6 @@
"typescript": "5.2.2", "typescript": "5.2.2",
"vh-check": "^2.0.5", "vh-check": "^2.0.5",
"virtualizedtableforantd4": "1.3.0", "virtualizedtableforantd4": "1.3.0",
"webpack": "^5.70.0",
"yorkie": "^2.0.0" "yorkie": "^2.0.0"
} }
} }
+5227 -6094
View File
File diff suppressed because it is too large Load Diff
+20 -1
View File
@@ -117,6 +117,16 @@ export PUSH_PLUS_TOKEN=""
## 下方填写您的一对多推送的 "群组编码" ,(一对多推送下面->您的群组(如无则新建)->群组编码) ## 下方填写您的一对多推送的 "群组编码" ,(一对多推送下面->您的群组(如无则新建)->群组编码)
## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送 ## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送
export PUSH_PLUS_USER="" export PUSH_PLUS_USER=""
## 发送模板,支持html,txt,json,markdown,cloudMonitor,jenkins,route,pay
export PUSH_PLUS_TEMPLATE="html"
## 发送渠道,支持wechat,webhook,cp,mail,sms
export PUSH_PLUS_CHANNEL="wechat"
## webhook编码,可在pushplus公众号上扩展配置出更多渠道
export PUSH_PLUS_WEBHOOK=""
## 发送结果回调地址,会把推送最终结果通知到这个地址上
export PUSH_PLUS_CALLBACKURL=""
## 好友令牌,微信公众号渠道填写好友令牌,企业微信渠道填写企业微信用户id
export PUSH_PLUS_TO=""
## 9. 微加机器人 ## 9. 微加机器人
## 官方网站:http://www.weplusbot.com ## 官方网站:http://www.weplusbot.com
@@ -220,8 +230,17 @@ export NTFY_URL=""
export NTFY_TOPIC="" export NTFY_TOPIC=""
export NTFY_PRIORITY="3" export NTFY_PRIORITY="3"
## 21. wxPusher
## 官方文档: https://wxpusher.zjiecode.com/docs/
## 管理后台: https://wxpusher.zjiecode.com/admin/
## wxPusher 的 appToken
export WXPUSHER_APP_TOKEN=""
## wxPusher 的 topicIds,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
export WXPUSHER_TOPIC_IDS=""
## wxPusher 的 用户ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
export WXPUSHER_UIDS=""
## 21. 自定义通知 ## 22. 自定义通知
## 自定义通知 接收回调的URL ## 自定义通知 接收回调的URL
export WEBHOOK_URL="" export WEBHOOK_URL=""
## WEBHOOK_BODY 和 WEBHOOK_HEADERS 多个参数时,直接换行或者使用 $'\n' 连接多行字符串,比如 export dd="line 1"$'\n'"line 2" ## WEBHOOK_BODY 和 WEBHOOK_HEADERS 多个参数时,直接换行或者使用 $'\n' 连接多行字符串,比如 export dd="line 1"$'\n'"line 2"
+113 -14
View File
@@ -40,9 +40,14 @@ const push_config = {
CHAT_URL: '', // synology chat url CHAT_URL: '', // synology chat url
CHAT_TOKEN: '', // synology chat token CHAT_TOKEN: '', // synology chat token
// 官方文档:http://www.pushplus.plus/ // 官方文档:https://www.pushplus.plus/
PUSH_PLUS_TOKEN: '', // push+ 微信推送的用户令牌 PUSH_PLUS_TOKEN: '', // pushplus 推送的用户令牌
PUSH_PLUS_USER: '', // push+ 微信推送的群组编码 PUSH_PLUS_USER: '', // pushplus 推送的群组编码
PUSH_PLUS_TEMPLATE: 'html', // pushplus 发送模板,支持html,txt,json,markdown,cloudMonitor,jenkins,route,pay
PUSH_PLUS_CHANNEL: 'wechat', // pushplus 发送渠道,支持wechat,webhook,cp,mail,sms
PUSH_PLUS_WEBHOOK: '', // pushplus webhook编码,可在pushplus公众号上扩展配置出更多渠道
PUSH_PLUS_CALLBACKURL: '', // pushplus 发送结果回调地址,会把推送最终结果通知到这个地址上
PUSH_PLUS_TO: '', // pushplus 好友令牌,微信公众号渠道填写好友令牌,企业微信渠道填写企业微信用户id
// 微加机器人,官方网站:https://www.weplusbot.com/ // 微加机器人,官方网站:https://www.weplusbot.com/
WE_PLUS_BOT_TOKEN: '', // 微加机器人的用户令牌 WE_PLUS_BOT_TOKEN: '', // 微加机器人的用户令牌
@@ -99,6 +104,12 @@ const push_config = {
NTFY_URL: '', // ntfy地址,如https://ntfy.sh,默认为https://ntfy.sh NTFY_URL: '', // ntfy地址,如https://ntfy.sh,默认为https://ntfy.sh
NTFY_TOPIC: '', // ntfy的消息应用topic NTFY_TOPIC: '', // ntfy的消息应用topic
NTFY_PRIORITY: '3', // 推送消息优先级,默认为3 NTFY_PRIORITY: '3', // 推送消息优先级,默认为3
// 官方文档: https://wxpusher.zjiecode.com/docs/
// 管理后台: https://wxpusher.zjiecode.com/admin/
WXPUSHER_APP_TOKEN: '', // wxpusher 的 appToken
WXPUSHER_TOPIC_IDS: '', // wxpusher 的 主题ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
WXPUSHER_UIDS: '', // wxpusher 的 用户ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
}; };
for (const key in push_config) { for (const key in push_config) {
@@ -231,10 +242,13 @@ function serverNotify(text, desp) {
const matchResult = PUSH_KEY.match(/^sctp(\d+)t/i); const matchResult = PUSH_KEY.match(/^sctp(\d+)t/i);
const options = { const options = {
url: matchResult && matchResult[1] url:
? `https://${matchResult[1]}.push.ft07.com/send/${PUSH_KEY}.send` matchResult && matchResult[1]
: `https://sctapi.ftqq.com/${PUSH_KEY}.send`, ? `https://${matchResult[1]}.push.ft07.com/send/${PUSH_KEY}.send`
body: `text=${encodeURIComponent(text)}&desp=${encodeURIComponent(desp)}`, : `https://sctapi.ftqq.com/${PUSH_KEY}.send`,
body: `text=${encodeURIComponent(text)}&desp=${encodeURIComponent(
desp,
)}`,
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}, },
@@ -765,7 +779,15 @@ function iGotNotify(text, desp, params = {}) {
function pushPlusNotify(text, desp) { function pushPlusNotify(text, desp) {
return new Promise((resolve) => { return new Promise((resolve) => {
const { PUSH_PLUS_TOKEN, PUSH_PLUS_USER } = push_config; const {
PUSH_PLUS_TOKEN,
PUSH_PLUS_USER,
PUSH_PLUS_TEMPLATE,
PUSH_PLUS_CHANNEL,
PUSH_PLUS_WEBHOOK,
PUSH_PLUS_CALLBACKURL,
PUSH_PLUS_TO,
} = push_config;
if (PUSH_PLUS_TOKEN) { if (PUSH_PLUS_TOKEN) {
desp = desp.replace(/[\n\r]/g, '<br>'); // 默认为html, 不支持plaintext desp = desp.replace(/[\n\r]/g, '<br>'); // 默认为html, 不支持plaintext
const body = { const body = {
@@ -773,6 +795,11 @@ function pushPlusNotify(text, desp) {
title: `${text}`, title: `${text}`,
content: `${desp}`, content: `${desp}`,
topic: `${PUSH_PLUS_USER}`, topic: `${PUSH_PLUS_USER}`,
template: `${PUSH_PLUS_TEMPLATE}`,
channel: `${PUSH_PLUS_CHANNEL}`,
webhook: `${PUSH_PLUS_WEBHOOK}`,
callbackUrl: `${PUSH_PLUS_CALLBACKURL}`,
to: `${PUSH_PLUS_TO}`,
}; };
const options = { const options = {
url: `https://www.pushplus.plus/send`, url: `https://www.pushplus.plus/send`,
@@ -786,7 +813,7 @@ function pushPlusNotify(text, desp) {
try { try {
if (err) { if (err) {
console.log( console.log(
`Push+ 发送${ `pushplus 发送${
PUSH_PLUS_USER ? '一对多' : '一对一' PUSH_PLUS_USER ? '一对多' : '一对一'
}通知消息失败😞\n`, }通知消息失败😞\n`,
err, err,
@@ -794,13 +821,15 @@ function pushPlusNotify(text, desp) {
} else { } else {
if (data.code === 200) { if (data.code === 200) {
console.log( console.log(
`Push+ 发送${ `pushplus 发送${
PUSH_PLUS_USER ? '一对多' : '一对一' PUSH_PLUS_USER ? '一对多' : '一对一'
}通知消息完成🎉\n`, }通知请求成功🎉,可根据流水号查询推送结果:${
data.data
}\n注意:请求成功并不代表推送成功,如未收到消息,请到pushplus官网使用流水号查询推送最终结果`,
); );
} else { } else {
console.log( console.log(
`Push+ 发送${ `pushplus 发送${
PUSH_PLUS_USER ? '一对多' : '一对一' PUSH_PLUS_USER ? '一对多' : '一对一'
}通知消息异常 ${data.msg}\n`, }通知消息异常 ${data.msg}\n`,
); );
@@ -1207,8 +1236,8 @@ function ntfyNotify(text, desp) {
url: `${NTFY_URL || 'https://ntfy.sh'}/${NTFY_TOPIC}`, url: `${NTFY_URL || 'https://ntfy.sh'}/${NTFY_TOPIC}`,
body: `${desp}`, body: `${desp}`,
headers: { headers: {
'Title': `${encodeRFC2047(text)}`, Title: `${encodeRFC2047(text)}`,
'Priority': NTFY_PRIORITY || '3' Priority: NTFY_PRIORITY || '3',
}, },
timeout, timeout,
}; };
@@ -1235,6 +1264,75 @@ function ntfyNotify(text, desp) {
}); });
} }
function wxPusherNotify(text, desp) {
return new Promise((resolve) => {
const { WXPUSHER_APP_TOKEN, WXPUSHER_TOPIC_IDS, WXPUSHER_UIDS } =
push_config;
if (WXPUSHER_APP_TOKEN) {
// 处理topic_ids,将分号分隔的字符串转为数组
const topicIds = WXPUSHER_TOPIC_IDS
? WXPUSHER_TOPIC_IDS.split(';')
.map((id) => id.trim())
.filter((id) => id)
.map((id) => parseInt(id))
: [];
// 处理uids,将分号分隔的字符串转为数组
const uids = WXPUSHER_UIDS
? WXPUSHER_UIDS.split(';')
.map((uid) => uid.trim())
.filter((uid) => uid)
: [];
// topic_ids uids 至少有一个
if (!topicIds.length && !uids.length) {
console.log(
'wxpusher 服务的 WXPUSHER_TOPIC_IDS 和 WXPUSHER_UIDS 至少设置一个!!',
);
return resolve();
}
const body = {
appToken: WXPUSHER_APP_TOKEN,
content: `<h1>${text}</h1><br/><div style='white-space: pre-wrap;'>${desp}</div>`,
summary: text,
contentType: 2,
topicIds: topicIds,
uids: uids,
verifyPayType: 0,
};
const options = {
url: 'https://wxpusher.zjiecode.com/api/send/message',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
timeout,
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('wxpusher发送通知消息失败!\n', err);
} else {
if (data.code === 1000) {
console.log('wxpusher发送通知消息完成!');
} else {
console.log(`wxpusher发送通知消息异常:${data.msg}`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
} else {
resolve();
}
});
}
function parseString(input, valueFormatFn) { function parseString(input, valueFormatFn) {
const regex = /(\w+):\s*((?:(?!\n\w+:).)*)/g; const regex = /(\w+):\s*((?:(?!\n\w+:).)*)/g;
@@ -1365,6 +1463,7 @@ async function sendNotify(text, desp, params = {}) {
webhookNotify(text, desp), // 自定义通知 webhookNotify(text, desp), // 自定义通知
qmsgNotify(text, desp), // 自定义通知 qmsgNotify(text, desp), // 自定义通知
ntfyNotify(text, desp), // Ntfy ntfyNotify(text, desp), // Ntfy
wxPusherNotify(text, desp), // wxpusher
]); ]);
} }
+96 -42
View File
@@ -72,8 +72,13 @@ push_config = {
'CHAT_URL': '', # synology chat url 'CHAT_URL': '', # synology chat url
'CHAT_TOKEN': '', # synology chat token 'CHAT_TOKEN': '', # synology chat token
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌 'PUSH_PLUS_TOKEN': '', # pushplus 推送的用户令牌
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码 'PUSH_PLUS_USER': '', # pushplus 推送的群组编码
'PUSH_PLUS_TEMPLATE': 'html', # pushplus 发送模板,支持html,txt,json,markdown,cloudMonitor,jenkins,route,pay
'PUSH_PLUS_CHANNEL': 'wechat', # pushplus 发送渠道,支持wechat,webhook,cp,mail,sms
'PUSH_PLUS_WEBHOOK': '', # pushplus webhook编码,可在pushplus公众号上扩展配置出更多渠道
'PUSH_PLUS_CALLBACKURL': '', # pushplus 发送结果回调地址,会把推送最终结果通知到这个地址上
'PUSH_PLUS_TO': '', # pushplus 好友令牌,微信公众号渠道填写好友令牌,企业微信渠道填写企业微信用户id
'WE_PLUS_BOT_TOKEN': '', # 微加机器人的用户令牌 'WE_PLUS_BOT_TOKEN': '', # 微加机器人的用户令牌
'WE_PLUS_BOT_RECEIVER': '', # 微加机器人的消息接收者 'WE_PLUS_BOT_RECEIVER': '', # 微加机器人的消息接收者
@@ -121,6 +126,10 @@ push_config = {
'NTFY_URL': '', # ntfy地址,如https://ntfy.sh 'NTFY_URL': '', # ntfy地址,如https://ntfy.sh
'NTFY_TOPIC': '', # ntfy的消息应用topic 'NTFY_TOPIC': '', # ntfy的消息应用topic
'NTFY_PRIORITY':'3', # 推送消息优先级,默认为3 'NTFY_PRIORITY':'3', # 推送消息优先级,默认为3
'WXPUSHER_APP_TOKEN': '', # wxpusher 的 appToken 官方文档: https://wxpusher.zjiecode.com/docs/ 管理后台: https://wxpusher.zjiecode.com/admin/
'WXPUSHER_TOPIC_IDS': '', # wxpusher 的 主题ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
'WXPUSHER_UIDS': '', # wxpusher 的 用户ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
} }
# fmt: on # fmt: on
@@ -135,7 +144,6 @@ def bark(title: str, content: str) -> None:
使用 bark 推送消息。 使用 bark 推送消息。
""" """
if not push_config.get("BARK_PUSH"): if not push_config.get("BARK_PUSH"):
print("bark 服务的 BARK_PUSH 未设置!!\n取消推送")
return return
print("bark 服务启动") print("bark 服务启动")
@@ -187,7 +195,6 @@ def dingding_bot(title: str, content: str) -> None:
使用 钉钉机器人 推送消息。 使用 钉钉机器人 推送消息。
""" """
if not push_config.get("DD_BOT_SECRET") or not push_config.get("DD_BOT_TOKEN"): if not push_config.get("DD_BOT_SECRET") or not push_config.get("DD_BOT_TOKEN"):
print("钉钉机器人 服务的 DD_BOT_SECRET 或者 DD_BOT_TOKEN 未设置!!\n取消推送")
return return
print("钉钉机器人 服务启动") print("钉钉机器人 服务启动")
@@ -217,7 +224,6 @@ def feishu_bot(title: str, content: str) -> None:
使用 飞书机器人 推送消息。 使用 飞书机器人 推送消息。
""" """
if not push_config.get("FSKEY"): if not push_config.get("FSKEY"):
print("飞书 服务的 FSKEY 未设置!!\n取消推送")
return return
print("飞书 服务启动") print("飞书 服务启动")
@@ -236,7 +242,6 @@ def go_cqhttp(title: str, content: str) -> None:
使用 go_cqhttp 推送消息。 使用 go_cqhttp 推送消息。
""" """
if not push_config.get("GOBOT_URL") or not push_config.get("GOBOT_QQ"): if not push_config.get("GOBOT_URL") or not push_config.get("GOBOT_QQ"):
print("go-cqhttp 服务的 GOBOT_URL 或 GOBOT_QQ 未设置!!\n取消推送")
return return
print("go-cqhttp 服务启动") print("go-cqhttp 服务启动")
@@ -254,7 +259,6 @@ def gotify(title: str, content: str) -> None:
使用 gotify 推送消息。 使用 gotify 推送消息。
""" """
if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"): if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"):
print("gotify 服务的 GOTIFY_URL 或 GOTIFY_TOKEN 未设置!!\n取消推送")
return return
print("gotify 服务启动") print("gotify 服务启动")
@@ -277,7 +281,6 @@ def iGot(title: str, content: str) -> None:
使用 iGot 推送消息。 使用 iGot 推送消息。
""" """
if not push_config.get("IGOT_PUSH_KEY"): if not push_config.get("IGOT_PUSH_KEY"):
print("iGot 服务的 IGOT_PUSH_KEY 未设置!!\n取消推送")
return return
print("iGot 服务启动") print("iGot 服务启动")
@@ -297,13 +300,12 @@ def serverJ(title: str, content: str) -> None:
通过 serverJ 推送消息。 通过 serverJ 推送消息。
""" """
if not push_config.get("PUSH_KEY"): if not push_config.get("PUSH_KEY"):
print("serverJ 服务的 PUSH_KEY 未设置!!\n取消推送")
return return
print("serverJ 服务启动") print("serverJ 服务启动")
data = {"text": title, "desp": content.replace("\n", "\n\n")} data = {"text": title, "desp": content.replace("\n", "\n\n")}
match = re.match(r'sctp(\d+)t', push_config.get("PUSH_KEY")) match = re.match(r"sctp(\d+)t", push_config.get("PUSH_KEY"))
if match: if match:
num = match.group(1) num = match.group(1)
url = f'https://{num}.push.ft07.com/send/{push_config.get("PUSH_KEY")}.send' url = f'https://{num}.push.ft07.com/send/{push_config.get("PUSH_KEY")}.send'
@@ -323,7 +325,6 @@ def pushdeer(title: str, content: str) -> None:
通过PushDeer 推送消息 通过PushDeer 推送消息
""" """
if not push_config.get("DEER_KEY"): if not push_config.get("DEER_KEY"):
print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送")
return return
print("PushDeer 服务启动") print("PushDeer 服务启动")
data = { data = {
@@ -349,7 +350,6 @@ def chat(title: str, content: str) -> None:
通过Chat 推送消息 通过Chat 推送消息
""" """
if not push_config.get("CHAT_URL") or not push_config.get("CHAT_TOKEN"): if not push_config.get("CHAT_URL") or not push_config.get("CHAT_TOKEN"):
print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送")
return return
print("chat 服务启动") print("chat 服务启动")
data = "payload=" + json.dumps({"text": title + "\n" + content}) data = "payload=" + json.dumps({"text": title + "\n" + content})
@@ -364,26 +364,36 @@ def chat(title: str, content: str) -> None:
def pushplus_bot(title: str, content: str) -> None: def pushplus_bot(title: str, content: str) -> None:
""" """
通过 push+ 推送消息。 通过 pushplus 推送消息。
""" """
if not push_config.get("PUSH_PLUS_TOKEN"): if not push_config.get("PUSH_PLUS_TOKEN"):
print("PUSHPLUS 服务的 PUSH_PLUS_TOKEN 未设置!!\n取消推送")
return return
print("PUSHPLUS 服务启动") print("PUSHPLUS 服务启动")
url = "http://www.pushplus.plus/send" url = "https://www.pushplus.plus/send"
data = { data = {
"token": push_config.get("PUSH_PLUS_TOKEN"), "token": push_config.get("PUSH_PLUS_TOKEN"),
"title": title, "title": title,
"content": content, "content": content,
"topic": push_config.get("PUSH_PLUS_USER"), "topic": push_config.get("PUSH_PLUS_USER"),
"template": push_config.get("PUSH_PLUS_TEMPLATE"),
"channel": push_config.get("PUSH_PLUS_CHANNEL"),
"webhook": push_config.get("PUSH_PLUS_WEBHOOK"),
"callbackUrl": push_config.get("PUSH_PLUS_CALLBACKURL"),
"to": push_config.get("PUSH_PLUS_TO"),
} }
body = json.dumps(data).encode(encoding="utf-8") body = json.dumps(data).encode(encoding="utf-8")
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
response = requests.post(url=url, data=body, headers=headers).json() response = requests.post(url=url, data=body, headers=headers).json()
if response["code"] == 200: code = response["code"]
print("PUSHPLUS 推送成功!") if code == 200:
print("PUSHPLUS 推送请求成功,可根据流水号查询推送结果:" + response["data"])
print(
"注意:请求成功并不代表推送成功,如未收到消息,请到pushplus官网使用流水号查询推送最终结果"
)
elif code == 900 or code == 903 or code == 905 or code == 999:
print(response["msg"])
else: else:
url_old = "http://pushplus.hxtrip.com/send" url_old = "http://pushplus.hxtrip.com/send"
@@ -402,7 +412,6 @@ def weplus_bot(title: str, content: str) -> None:
通过 微加机器人 推送消息。 通过 微加机器人 推送消息。
""" """
if not push_config.get("WE_PLUS_BOT_TOKEN"): if not push_config.get("WE_PLUS_BOT_TOKEN"):
print("微加机器人 服务的 WE_PLUS_BOT_TOKEN 未设置!!\n取消推送")
return return
print("微加机器人 服务启动") print("微加机器人 服务启动")
@@ -434,7 +443,6 @@ def qmsg_bot(title: str, content: str) -> None:
使用 qmsg 推送消息。 使用 qmsg 推送消息。
""" """
if not push_config.get("QMSG_KEY") or not push_config.get("QMSG_TYPE"): if not push_config.get("QMSG_KEY") or not push_config.get("QMSG_TYPE"):
print("qmsg 的 QMSG_KEY 或者 QMSG_TYPE 未设置!!\n取消推送")
return return
print("qmsg 服务启动") print("qmsg 服务启动")
@@ -453,11 +461,10 @@ def wecom_app(title: str, content: str) -> None:
通过 企业微信 APP 推送消息。 通过 企业微信 APP 推送消息。
""" """
if not push_config.get("QYWX_AM"): if not push_config.get("QYWX_AM"):
print("QYWX_AM 未设置!!\n取消推送")
return return
QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM")) QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM"))
if 4 < len(QYWX_AM_AY) > 5: if 4 < len(QYWX_AM_AY) > 5:
print("QYWX_AM 设置错误!!\n取消推送") print("QYWX_AM 设置错误!!")
return return
print("企业微信 APP 服务启动") print("企业微信 APP 服务启动")
@@ -550,7 +557,6 @@ def wecom_bot(title: str, content: str) -> None:
通过 企业微信机器人 推送消息。 通过 企业微信机器人 推送消息。
""" """
if not push_config.get("QYWX_KEY"): if not push_config.get("QYWX_KEY"):
print("企业微信机器人 服务的 QYWX_KEY 未设置!!\n取消推送")
return return
print("企业微信机器人服务启动") print("企业微信机器人服务启动")
@@ -576,7 +582,6 @@ def telegram_bot(title: str, content: str) -> None:
使用 telegram 机器人 推送消息。 使用 telegram 机器人 推送消息。
""" """
if not push_config.get("TG_BOT_TOKEN") or not push_config.get("TG_USER_ID"): if not push_config.get("TG_BOT_TOKEN") or not push_config.get("TG_USER_ID"):
print("tg 服务的 bot_token 或者 user_id 未设置!!\n取消推送")
return return
print("tg 服务启动") print("tg 服务启动")
@@ -625,9 +630,6 @@ def aibotk(title: str, content: str) -> None:
or not push_config.get("AIBOTK_TYPE") or not push_config.get("AIBOTK_TYPE")
or not push_config.get("AIBOTK_NAME") or not push_config.get("AIBOTK_NAME")
): ):
print(
"智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送"
)
return return
print("智能微秘书 服务启动") print("智能微秘书 服务启动")
@@ -666,9 +668,6 @@ def smtp(title: str, content: str) -> None:
or not push_config.get("SMTP_PASSWORD") or not push_config.get("SMTP_PASSWORD")
or not push_config.get("SMTP_NAME") or not push_config.get("SMTP_NAME")
): ):
print(
"SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送"
)
return return
print("SMTP 邮件 服务启动") print("SMTP 邮件 服务启动")
@@ -712,7 +711,6 @@ def pushme(title: str, content: str) -> None:
使用 PushMe 推送消息。 使用 PushMe 推送消息。
""" """
if not push_config.get("PUSHME_KEY"): if not push_config.get("PUSHME_KEY"):
print("PushMe 服务的 PUSHME_KEY 未设置!!\n取消推送")
return return
print("PushMe 服务启动") print("PushMe 服务启动")
@@ -745,7 +743,6 @@ def chronocat(title: str, content: str) -> None:
or not push_config.get("CHRONOCAT_QQ") or not push_config.get("CHRONOCAT_QQ")
or not push_config.get("CHRONOCAT_TOKEN") or not push_config.get("CHRONOCAT_TOKEN")
): ):
print("CHRONOCAT 服务的 CHRONOCAT_URL 或 CHRONOCAT_QQ 未设置!!\n取消推送")
return return
print("CHRONOCAT 服务启动") print("CHRONOCAT 服务启动")
@@ -789,17 +786,17 @@ def ntfy(title: str, content: str) -> None:
""" """
通过 Ntfy 推送消息 通过 Ntfy 推送消息
""" """
def encode_rfc2047(text: str) -> str: def encode_rfc2047(text: str) -> str:
"""将文本编码为符合 RFC 2047 标准的格式""" """将文本编码为符合 RFC 2047 标准的格式"""
encoded_bytes = base64.b64encode(text.encode('utf-8')) encoded_bytes = base64.b64encode(text.encode("utf-8"))
encoded_str = encoded_bytes.decode('utf-8') encoded_str = encoded_bytes.decode("utf-8")
return f'=?utf-8?B?{encoded_str}?=' return f"=?utf-8?B?{encoded_str}?="
if not push_config.get("NTFY_TOPIC"): if not push_config.get("NTFY_TOPIC"):
print("ntfy 服务的 NTFY_TOPIC 未设置!!\n取消推送")
return return
print("ntfy 服务启动") print("ntfy 服务启动")
priority = '3' priority = "3"
if not push_config.get("NTFY_PRIORITY"): if not push_config.get("NTFY_PRIORITY"):
print("ntfy 服务的NTFY_PRIORITY 未设置!!默认设置为3") print("ntfy 服务的NTFY_PRIORITY 未设置!!默认设置为3")
else: else:
@@ -808,11 +805,8 @@ def ntfy(title: str, content: str) -> None:
# 使用 RFC 2047 编码 title # 使用 RFC 2047 编码 title
encoded_title = encode_rfc2047(title) encoded_title = encode_rfc2047(title)
data = content.encode(encoding='utf-8') data = content.encode(encoding="utf-8")
headers = { headers = {"Title": encoded_title, "Priority": priority} # 使用编码后的 title
"Title": encoded_title, # 使用编码后的 title
"Priority": priority
}
url = push_config.get("NTFY_URL") + "/" + push_config.get("NTFY_TOPIC") url = push_config.get("NTFY_URL") + "/" + push_config.get("NTFY_TOPIC")
response = requests.post(url, data=data, headers=headers) response = requests.post(url, data=data, headers=headers)
@@ -821,6 +815,63 @@ def ntfy(title: str, content: str) -> None:
else: else:
print("Ntfy 推送失败!错误信息:", response.text) print("Ntfy 推送失败!错误信息:", response.text)
def wxpusher_bot(title: str, content: str) -> None:
"""
通过 wxpusher 推送消息。
支持的环境变量:
- WXPUSHER_APP_TOKEN: appToken
- WXPUSHER_TOPIC_IDS: 主题ID, 多个用英文分号;分隔
- WXPUSHER_UIDS: 用户ID, 多个用英文分号;分隔
"""
if not push_config.get("WXPUSHER_APP_TOKEN"):
return
url = "https://wxpusher.zjiecode.com/api/send/message"
# 处理topic_ids和uids,将分号分隔的字符串转为数组
topic_ids = []
if push_config.get("WXPUSHER_TOPIC_IDS"):
topic_ids = [
int(id.strip())
for id in push_config.get("WXPUSHER_TOPIC_IDS").split(";")
if id.strip()
]
uids = []
if push_config.get("WXPUSHER_UIDS"):
uids = [
uid.strip()
for uid in push_config.get("WXPUSHER_UIDS").split(";")
if uid.strip()
]
# topic_ids uids 至少有一个
if not topic_ids and not uids:
print("wxpusher 服务的 WXPUSHER_TOPIC_IDS 和 WXPUSHER_UIDS 至少设置一个!!")
return
print("wxpusher 服务启动")
data = {
"appToken": push_config.get("WXPUSHER_APP_TOKEN"),
"content": f"<h1>{title}</h1><br/><div style='white-space: pre-wrap;'>{content}</div>",
"summary": title,
"contentType": 2,
"topicIds": topic_ids,
"uids": uids,
"verifyPayType": 0,
}
headers = {"Content-Type": "application/json"}
response = requests.post(url=url, json=data, headers=headers).json()
if response.get("code") == 1000:
print("wxpusher 推送成功!")
else:
print(f"wxpusher 推送失败!错误信息:{response.get('msg')}")
def parse_headers(headers): def parse_headers(headers):
if not headers: if not headers:
return {} return {}
@@ -877,7 +928,6 @@ def custom_notify(title: str, content: str) -> None:
通过 自定义通知 推送消息。 通过 自定义通知 推送消息。
""" """
if not push_config.get("WEBHOOK_URL") or not push_config.get("WEBHOOK_METHOD"): if not push_config.get("WEBHOOK_URL") or not push_config.get("WEBHOOK_METHOD"):
print("自定义通知的 WEBHOOK_URL 或 WEBHOOK_METHOD 未设置!!\n取消推送")
return return
print("自定义通知服务启动") print("自定义通知服务启动")
@@ -983,6 +1033,10 @@ def add_notify_function():
notify_function.append(custom_notify) notify_function.append(custom_notify)
if push_config.get("NTFY_TOPIC"): if push_config.get("NTFY_TOPIC"):
notify_function.append(ntfy) notify_function.append(ntfy)
if push_config.get("WXPUSHER_APP_TOKEN") and (
push_config.get("WXPUSHER_TOPIC_IDS") or push_config.get("WXPUSHER_UIDS")
):
notify_function.append(wxpusher_bot)
if not notify_function: if not notify_function:
print(f"无推送渠道,请检查通知变量是否正确") print(f"无推送渠道,请检查通知变量是否正确")
return notify_function return notify_function
+26
View File
@@ -231,4 +231,30 @@ find_cron_api() {
fi fi
} }
update_auth_config() {
local body="$1"
local tip="$2"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/auth/reset?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer ${__ql_token__}" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{$body}" \
--compressed
)
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "${tip}成功🎉"
else
echo -e "${tip}失败(${message})"
fi
}
get_token get_token
+7 -1
View File
@@ -56,7 +56,9 @@ function run() {
for (const key in newEnvObject) { for (const key in newEnvObject) {
process.env[key] = newEnvObject[key]; process.env[key] = newEnvObject[key];
} }
console.log(output); if (output) {
console.log(output);
}
if (task_before) { if (task_before) {
console.log('执行前置命令结束\n'); console.log('执行前置命令结束\n');
} }
@@ -89,6 +91,10 @@ try {
return; return;
} }
process.on('SIGTERM', (code) => {
process.exit(15);
});
run(); run();
const { sendNotify } = require('./notify.js'); const { sendNotify } = require('./notify.js');
+9 -1
View File
@@ -5,6 +5,7 @@ import json
import builtins import builtins
import sys import sys
import env import env
import signal
def try_parse_int(value): def try_parse_int(value):
@@ -63,7 +64,8 @@ def run():
for key, value in env_json.items(): for key, value in env_json.items():
os.environ[key] = value os.environ[key] = value
print(output) if len(output) > 0:
print(output)
if task_before: if task_before:
print("执行前置命令结束") print("执行前置命令结束")
@@ -95,7 +97,13 @@ def run():
os.environ[env_param] = env_str os.environ[env_param] = env_str
def handle_sigterm(signum, frame):
sys.exit(15)
try: try:
signal.signal(signal.SIGTERM, handle_sigterm)
run() run()
from notify import send from notify import send
+3 -7
View File
@@ -193,12 +193,6 @@ fix_config() {
echo echo
fi fi
if [[ ! -s $file_auth_user ]]; then
echo -e "复制一份 $file_auth_sample$file_auth_user\n"
cp -fv $file_auth_sample $file_auth_user
echo
fi
if [[ ! -s $file_notify_py ]]; then if [[ ! -s $file_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$file_notify_py\n" echo -e "复制一份 $file_notify_py_sample$file_notify_py\n"
cp -fv $file_notify_py_sample $file_notify_py cp -fv $file_notify_py_sample $file_notify_py
@@ -473,10 +467,12 @@ handle_task_end() {
local end_time=$(format_time "$time_format" "$etime") local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime") local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp)) local diff_time=$(($end_timestamp - $begin_timestamp))
local suffix=""
[[ "$MANUAL" == "true" ]] && suffix="(手动停止)"
[[ "$diff_time" == 0 ]] && diff_time=1 [[ "$diff_time" == 0 ]] && diff_time=1
echo -e "\n## 执行结束... $end_time 耗时 $diff_time 秒     " echo -e "\n## 执行结束$suffix... $end_time 耗时 $diff_time 秒     "
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time" [[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
} }
+1 -1
View File
@@ -6,7 +6,7 @@ dir_shell=$QL_DIR/shell
trap "single_hanle" 2 3 20 15 14 19 1 trap "single_hanle" 2 3 20 15 14 19 1
single_hanle() { single_hanle() {
eval handle_task_end "$@" "$cmd" eval MANUAL=true handle_task_end "$@" "$cmd"
exit 1 exit 1
} }
+2 -6
View File
@@ -537,14 +537,10 @@ main() {
eval . $dir_shell/check.sh $cmd eval . $dir_shell/check.sh $cmd
;; ;;
resetlet) resetlet)
auth_value=$(cat $file_auth_user | jq '.retries =0' -c) eval update_auth_config "\\\"retries\\\":0" "重置登录错误次数" $cmd
echo "$auth_value" >$file_auth_user
eval echo -e "重置登录错误次数成功" $cmd
;; ;;
resettfa) resettfa)
auth_value=$(cat $file_auth_user | jq '.twoFactorActivated =false' | jq '.twoFactorActived =false' -c) eval update_auth_config "\\\"twoFactorActivated\\\":false" "禁用两步验证" $cmd
echo "$auth_value" >$file_auth_user
eval echo -e "禁用两步验证成功" $cmd
;; ;;
*) *)
eval echo -e "命令输入错误...\\\n" $cmd eval echo -e "命令输入错误...\\\n" $cmd
+8
View File
@@ -379,6 +379,11 @@
"iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX": "iGot information push key, e.g., https://push.hellyw.com/XXXXXXXX", "iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX": "iGot information push key, e.g., https://push.hellyw.com/XXXXXXXX",
"微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/": "After WeChat scan login, one-to-one or one-to-many push using the provided token (your Token). If PUSH_PLUS_USER is not provided, it defaults to one-to-one push. See reference at https://www.pushplus.plus/", "微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/": "After WeChat scan login, one-to-one or one-to-many push using the provided token (your Token). If PUSH_PLUS_USER is not provided, it defaults to one-to-one push. See reference at https://www.pushplus.plus/",
"一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)": "The 'group code' for one-to-many push (one-to-many push -> your group (if none, create one) -> group code). If you are the creator of the group, you need to click 'View QR code' to scan and bind, otherwise, you won't receive group messages.", "一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)": "The 'group code' for one-to-many push (one-to-many push -> your group (if none, create one) -> group code). If you are the creator of the group, you need to click 'View QR code' to scan and bind, otherwise, you won't receive group messages.",
"发送模板": "send template, can use type: 'html,txt,json,markdown,cloudMonitor,jenkins,route,pay'",
"发送渠道": "send channel, can use type: 'wechat,webhook,cp,mail,sms'",
"webhook编码": "webhook code",
"发送结果回调地址": "send result callback url",
"好友令牌": "friend token",
"用户令牌,扫描登录后 我的—>设置->令牌 中获取,参考 https://www.weplusbot.com/": "Token, which can be obtained after scanning and logging in, is available under 'My Account' -> 'Settings' -> 'Tokens'. Please refer to the instructions for detailed steps: https://www.weplusbot.com/", "用户令牌,扫描登录后 我的—>设置->令牌 中获取,参考 https://www.weplusbot.com/": "Token, which can be obtained after scanning and logging in, is available under 'My Account' -> 'Settings' -> 'Tokens'. Please refer to the instructions for detailed steps: https://www.weplusbot.com/",
"消息接收人": "message recipient", "消息接收人": "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.", "调用版本;专业版填写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.",
@@ -390,6 +395,9 @@
"自建的PushMeServer消息接口地址,例如:http://127.0.0.1:3010,不填则使用官方消息接口": "The self built PushMeServer message interface address, for example: http://127.0.0.1:3010 If left blank, use the official message interface", "自建的PushMeServer消息接口地址,例如:http://127.0.0.1:3010,不填则使用官方消息接口": "The self built PushMeServer message interface address, for example: http://127.0.0.1:3010 If left blank, use the official message interface",
"ntfy的url地址,例如 https://ntfy.sh'": "The URL address of ntfy, for example, https://ntfy.sh.", "ntfy的url地址,例如 https://ntfy.sh'": "The URL address of ntfy, for example, https://ntfy.sh.",
"ntfy的消息应用topic": "The topic for ntfy's messaging application.", "ntfy的消息应用topic": "The topic for ntfy's messaging application.",
"wxPusherBot的appToken": "wxPusherBot's appToken, obtain according to docs https://wxpusher.zjiecode.com/docs/",
"wxPusherBot的topicIds": "wxPusherBot's topicIds, at least one of topicIds or uids must be configured",
"wxPusherBot的uids": "wxPusherBot's uids, at least one of topicIds or uids must be configured",
"请求方法": "Request Method", "请求方法": "Request Method",
"请求头Content-Type": "Request Header Content-Type", "请求头Content-Type": "Request Header Content-Type",
"请求链接以http或者https开头。url或者body中必须包含$title$content可选,对应api内容的位置": "Request URL should start with http or https. URL or body must contain $title, $content is optional and corresponds to the API content position.", "请求链接以http或者https开头。url或者body中必须包含$title$content可选,对应api内容的位置": "Request URL should start with http or https. URL or body must contain $title, $content is optional and corresponds to the API content position.",
+9 -1
View File
@@ -377,8 +377,13 @@
"好友": "好友", "好友": "好友",
"要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称": "要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称", "要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称": "要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称",
"iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX": "iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX", "iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX": "iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX",
"微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/": "微信扫码登录后一对一推送或一对多推送下面的token(的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/", "微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/": "微信扫码登录后一对一推送或一对多推送下面的token(的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/",
"一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)": "一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)", "一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)": "一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)",
"发送模板": "发送模板,支持html,txt,json,markdown,cloudMonitor,jenkins,route,pay",
"发送渠道": "发送渠道,支持wechat,webhook,cp,mail,sms",
"webhook编码": "webhook编码,可在pushplus公众号上扩展配置出更多渠道",
"发送结果回调地址": "发送结果回调地址,会把推送最终结果通知到这个地址上",
"好友令牌": "好友令牌,微信公众号渠道填写好友令牌,企业微信渠道填写企业微信用户id",
"用户令牌,扫描登录后 我的—>设置->令牌 中获取,参考 https://www.weplusbot.com/": "用户令牌,扫描登录后 我的—>设置->令牌 中获取,参考 https://www.weplusbot.com/", "用户令牌,扫描登录后 我的—>设置->令牌 中获取,参考 https://www.weplusbot.com/": "用户令牌,扫描登录后 我的—>设置->令牌 中获取,参考 https://www.weplusbot.com/",
"消息接收人": "消息接收人", "消息接收人": "消息接收人",
"调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版": "调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版", "调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版": "调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版",
@@ -390,6 +395,9 @@
"自建的PushMeServer消息接口地址,例如:http://127.0.0.1:3010,不填则使用官方消息接口": "自建的PushMeServer消息接口地址,例如:http://127.0.0.1:3010,不填则使用官方消息接口", "自建的PushMeServer消息接口地址,例如:http://127.0.0.1:3010,不填则使用官方消息接口": "自建的PushMeServer消息接口地址,例如:http://127.0.0.1:3010,不填则使用官方消息接口",
"ntfy的url地址,例如 https://ntfy.sh": "ntfy的url地址,例如 https://ntfy.sh", "ntfy的url地址,例如 https://ntfy.sh": "ntfy的url地址,例如 https://ntfy.sh",
"ntfy的消息应用topic": "ntfy的消息应用topic", "ntfy的消息应用topic": "ntfy的消息应用topic",
"wxPusherBot的appToken": "wxPusherBot的appToken, 按照文档获取 https://wxpusher.zjiecode.com/docs/",
"wxPusherBot的topicIds": "wxPusherBot的topicIds, topicIds 和 uids 至少配置一个才行",
"wxPusherBot的uids": "wxPusherBot的uids, topicIds 和 uids 至少配置一个才行",
"请求方法": "请求方法", "请求方法": "请求方法",
"请求头Content-Type": "请求头Content-Type", "请求头Content-Type": "请求头Content-Type",
"请求链接以http或者https开头。url或者body中必须包含$title$content可选,对应api内容的位置": "请求链接以http或者https开头。url或者body中必须包含$title$content可选,对应api内容的位置", "请求链接以http或者https开头。url或者body中必须包含$title$content可选,对应api内容的位置": "请求链接以http或者https开头。url或者body中必须包含$title$content可选,对应api内容的位置",
+48
View File
@@ -97,6 +97,7 @@ export default {
{ value: 'iGot', label: 'IGot' }, { value: 'iGot', label: 'IGot' },
{ value: 'pushPlus', label: 'PushPlus' }, { value: 'pushPlus', label: 'PushPlus' },
{ value: 'wePlusBot', label: intl.get('微加机器人') }, { value: 'wePlusBot', label: intl.get('微加机器人') },
{ value: 'wxPusherBot', label: 'wxPusher' },
{ value: 'chat', label: intl.get('群晖chat') }, { value: 'chat', label: intl.get('群晖chat') },
{ value: 'email', label: intl.get('邮箱') }, { value: 'email', label: intl.get('邮箱') },
{ value: 'lark', label: intl.get('飞书机器人') }, { value: 'lark', label: intl.get('飞书机器人') },
@@ -326,6 +327,36 @@ export default {
'一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)', '一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)',
), ),
}, },
{
label: 'pushplusTemplate',
tip: intl.get(
'发送模板',
),
},
{
label: 'pushplusChannel',
tip: intl.get(
'发送渠道',
),
},
{
label: 'pushplusWebhook',
tip: intl.get(
'webhook编码',
),
},
{
label: 'pushplusCallbackUrl',
tip: intl.get(
'发送结果回调地址',
),
},
{
label: 'pushplusTo',
tip: intl.get(
'好友令牌',
),
},
], ],
wePlusBot: [ wePlusBot: [
{ {
@@ -348,6 +379,23 @@ export default {
), ),
}, },
], ],
wxPusherBot: [
{
label: 'wxPusherBotAppToken',
tip: intl.get('wxPusherBot的appToken'),
required: true,
},
{
label: 'wxPusherBotTopicIds',
tip: intl.get('wxPusherBot的topicIds'),
required: false,
},
{
label: 'wxPusherBotUids',
tip: intl.get('wxPusherBot的uids'),
required: false,
},
],
lark: [ lark: [
{ {
label: 'larkKey', label: 'larkKey',
+2
View File
@@ -6,4 +6,6 @@ export const LANG_MAP = {
'.mjs': 'javascript', '.mjs': 'javascript',
'.sh': 'shell', '.sh': 'shell',
'.ts': 'typescript', '.ts': 'typescript',
'.ini': 'ini',
'.json': 'json'
}; };
+3 -2
View File
@@ -1,6 +1,7 @@
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 cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import { ICrontab } from '@/pages/crontab/type';
export default function browserType() { export default function browserType() {
// 权重:系统 + 系统版本 > 平台 > 内核 + 载体 + 内核版本 + 载体版本 > 外壳 + 外壳版本 // 权重:系统 + 系统版本 > 平台 > 内核 + 载体 + 内核版本 + 载体版本 > 外壳 + 外壳版本
@@ -343,12 +344,12 @@ export function parseCrontab(schedule: string): Date | null {
export function getCrontabsNextDate( export function getCrontabsNextDate(
schedule: string, schedule: string,
extra_schedules: string[], extra_schedules: ICrontab['extra_schedules'],
): Date | null { ): Date | null {
let date = parseCrontab(schedule); let date = parseCrontab(schedule);
if (extra_schedules?.length) { if (extra_schedules?.length) {
extra_schedules.forEach((x) => { extra_schedules.forEach((x) => {
const _date = parseCrontab(x); const _date = parseCrontab(x.schedule);
if (_date && (!date || _date < date)) { if (_date && (!date || _date < date)) {
date = _date; date = _date;
} }
-3
View File
@@ -3,9 +3,6 @@
"target": "es2017", "target": "es2017",
"lib": ["ESNext"], "lib": ["ESNext"],
"typeRoots": ["./node_modules/celebrate/lib", "./node_modules/@types"], "typeRoots": ["./node_modules/celebrate/lib", "./node_modules/@types"],
"paths": {
"@/*": ["./back/*"]
},
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
+2 -6
View File
@@ -22,10 +22,6 @@
"allowJs": true, "allowJs": true,
"noEmit": false "noEmit": false
}, },
"include": ["src/**/*", ".umirc.ts", "typings.d.ts", "back/**/*"], "include": ["src/**/*", ".umirc.ts", "typings.d.ts"],
"exclude": [ "exclude": ["node_modules", "static", "data"]
"node_modules",
"static",
"data",
]
} }
+7 -10
View File
@@ -1,11 +1,8 @@
version: 2.17.12 version: 2.18.0
changeLogLink: https://t.me/jiao_long/422 changeLogLink: https://t.me/jiao_long/424
publishTime: 2024-10-26 15:30 publishTime: 2025-01-05 13:00
changeLog: | changeLog: |
1. 定时任务支持复制 1. 由于安全问题,修改认证信息存储方式,不再使用 auth.json 存储
2. 增加 ntfy 通知 2. 修复初始化 SystemConfig 数据
3. 适配Server酱APP分支(Server酱³),移除已废弃的旧版api入口 3. 修改通知文件未设置时提示
4. 修复任务命令带有 -m 参数时,日志目录生成异常 4. 修复配置文件更新可能异常
5. 修复登录日志无法自动保存
6. 移除任务执行前后的脚本参数
7. 定时任务外漏视图改为 10 个