mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-12 19:30:48 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4667af4ebe | ||
|
|
3f775a0e6c | ||
|
|
fa79de3f05 | ||
|
|
ffa8b25a66 | ||
|
|
05f8bbd26e | ||
|
|
7d43b14f81 | ||
|
|
cecc5aeb15 | ||
|
|
43d6ac2071 | ||
|
|
678e3e2dc6 | ||
|
|
75f91e1473 | ||
|
|
bdc45c538c | ||
|
|
955c7377d7 | ||
|
|
c71abd8c86 | ||
|
|
ab27a4c908 | ||
|
|
026640a757 | ||
|
|
a1501705c1 | ||
|
|
b321530dcf | ||
|
|
02c6ad8004 | ||
|
|
fff572869e |
+11
-11
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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_DIR,QL_DATA_DIR 必须以 /data 结尾
|
||||||
export QL_DIR=""
|
export QL_DIR=""
|
||||||
export QL_DATA_DIR=""
|
export QL_DATA_DIR=""
|
||||||
# 再次执行
|
# 再次执行
|
||||||
|
|||||||
+2
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 (
|
||||||
|
|||||||
@@ -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
@@ -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');
|
||||||
|
|||||||
@@ -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,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'],
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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: {} });
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-4
@@ -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 {
|
||||||
const res: any = await got
|
let body = {
|
||||||
.post(url, {
|
|
||||||
...this.gotOption,
|
...this.gotOption,
|
||||||
json: {
|
json: {
|
||||||
token: `${pushPlusToken}`,
|
token: `${pushPlusToken}`,
|
||||||
title: `${this.title}`,
|
title: `${this.title}`,
|
||||||
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
|
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
|
||||||
topic: `${pushPlusUser || ''}`,
|
topic: `${pushPlusUser || ''}`,
|
||||||
|
template: `${pushPlusTemplate || 'html'}`,
|
||||||
|
channel: `${pushplusChannel || 'wechat'}`,
|
||||||
|
webhook: `${pushplusWebhook || ''}`,
|
||||||
|
callbackUrl: `${pushplusCallbackUrl || ''}`,
|
||||||
|
to: `${pushplusTo || ''}`
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
|
||||||
|
const res: any = await got
|
||||||
|
.post(url, body)
|
||||||
.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;
|
||||||
|
|||||||
+24
-15
@@ -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_id或client_seret有误' };
|
return { code: 400, message: 'client_id 或 client_seret 有误' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,9 +159,11 @@ export default class OpenService {
|
|||||||
value: string;
|
value: string;
|
||||||
expiration: number;
|
expiration: number;
|
||||||
}> {
|
}> {
|
||||||
let systemApp = (await AppModel.findOne({
|
let systemApp = (
|
||||||
|
await AppModel.findOne({
|
||||||
where: { name: 'system' },
|
where: { name: 'system' },
|
||||||
})) as App;
|
})
|
||||||
|
)?.get({ plain: true });
|
||||||
if (!systemApp) {
|
if (!systemApp) {
|
||||||
systemApp = await this.create({
|
systemApp = await this.create({
|
||||||
name: 'system',
|
name: 'system',
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
+15
-10
@@ -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(
|
||||||
|
path.join(this.sshPath, alias),
|
||||||
|
`${key}${os.EOL}`,
|
||||||
|
{
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
mode: '400',
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
+37
-72
@@ -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,15 +50,9 @@ 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,
|
||||||
@@ -65,21 +61,9 @@ export default class UserService {
|
|||||||
lastip,
|
lastip,
|
||||||
lastaddr,
|
lastaddr,
|
||||||
twoFactorActivated,
|
twoFactorActivated,
|
||||||
twoFactorActived,
|
|
||||||
tokens = {},
|
tokens = {},
|
||||||
platform,
|
platform,
|
||||||
} = content;
|
} = content;
|
||||||
// patch old field
|
|
||||||
twoFactorActivated = twoFactorActivated || twoFactorActived;
|
|
||||||
|
|
||||||
if (
|
|
||||||
(cUsername === 'admin' && cPassword === 'admin') ||
|
|
||||||
!cUsername ||
|
|
||||||
!cPassword
|
|
||||||
) {
|
|
||||||
return this.initAuthInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
const retriesTime = Math.pow(3, retries) * 1000;
|
const retriesTime = Math.pow(3, retries) * 1000;
|
||||||
if (retries > 2 && timestamp - lastlogon < retriesTime) {
|
if (retries > 2 && timestamp - lastlogon < retriesTime) {
|
||||||
const waitTime = Math.ceil(
|
const waitTime = Math.ceil(
|
||||||
@@ -98,7 +82,7 @@ export default class UserService {
|
|||||||
twoFactorActivated &&
|
twoFactorActivated &&
|
||||||
needTwoFactor
|
needTwoFactor
|
||||||
) {
|
) {
|
||||||
this.updateAuthInfo(content, {
|
await this.updateAuthInfo(content, {
|
||||||
isTwoFactorChecking: true,
|
isTwoFactorChecking: true,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@@ -113,9 +97,7 @@ export default class UserService {
|
|||||||
let address = '';
|
let address = '';
|
||||||
if (ipAddress) {
|
if (ipAddress) {
|
||||||
const { country, province, city, isp } = ipAddress;
|
const { country, province, city, isp } = ipAddress;
|
||||||
address = uniq([country, province, city, isp])
|
address = uniq([country, province, city, isp]).filter(Boolean).join(' ');
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ');
|
|
||||||
}
|
}
|
||||||
if (username === cUsername && password === cPassword) {
|
if (username === cUsername && password === cPassword) {
|
||||||
const data = createRandomString(50, 100);
|
const data = createRandomString(50, 100);
|
||||||
@@ -125,7 +107,7 @@ export default class UserService {
|
|||||||
algorithm: 'HS384',
|
algorithm: 'HS384',
|
||||||
});
|
});
|
||||||
|
|
||||||
this.updateAuthInfo(content, {
|
await this.updateAuthInfo(content, {
|
||||||
token,
|
token,
|
||||||
tokens: {
|
tokens: {
|
||||||
...tokens,
|
...tokens,
|
||||||
@@ -160,7 +142,7 @@ export default class UserService {
|
|||||||
data: { token, lastip, lastaddr, lastlogon, retries, platform },
|
data: { token, lastip, lastaddr, lastlogon, retries, platform },
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
this.updateAuthInfo(content, {
|
await this.updateAuthInfo(content, {
|
||||||
retries: retries + 1,
|
retries: retries + 1,
|
||||||
lastlogon: timestamp,
|
lastlogon: timestamp,
|
||||||
lastip: ip,
|
lastip: ip,
|
||||||
@@ -195,14 +177,11 @@ export default class UserService {
|
|||||||
return { code: 400, message: config.authError };
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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) => {},
|
|
||||||
};
|
|
||||||
@@ -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;
|
|
||||||
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+5227
-6094
File diff suppressed because it is too large
Load Diff
+20
-1
@@ -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"
|
||||||
|
|||||||
+111
-12
@@ -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:
|
||||||
|
matchResult && matchResult[1]
|
||||||
? `https://${matchResult[1]}.push.ft07.com/send/${PUSH_KEY}.send`
|
? `https://${matchResult[1]}.push.ft07.com/send/${PUSH_KEY}.send`
|
||||||
: `https://sctapi.ftqq.com/${PUSH_KEY}.send`,
|
: `https://sctapi.ftqq.com/${PUSH_KEY}.send`,
|
||||||
body: `text=${encodeURIComponent(text)}&desp=${encodeURIComponent(desp)}`,
|
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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
|
if (output) {
|
||||||
console.log(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');
|
||||||
|
|||||||
@@ -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,6 +64,7 @@ def run():
|
|||||||
for key, value in env_json.items():
|
for key, value in env_json.items():
|
||||||
os.environ[key] = value
|
os.environ[key] = value
|
||||||
|
|
||||||
|
if len(output) > 0:
|
||||||
print(output)
|
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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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内容的位置",
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
@@ -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,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
@@ -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
@@ -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 个
|
|
||||||
|
|||||||
Reference in New Issue
Block a user