mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-05 11:09:57 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26d8a721ce | ||
|
|
a3f8856dbe | ||
|
|
90efa1155e | ||
|
|
546b82eed0 | ||
|
|
0aa521a288 | ||
|
|
1ac04a8062 | ||
|
|
e435f6e886 | ||
|
|
db88d037db | ||
|
|
a0c09046c6 | ||
|
|
5a9e53c771 | ||
|
|
be877cfddc | ||
|
|
fdeffebba8 | ||
|
|
3c61e933a1 | ||
|
|
cf236f8324 | ||
|
|
7334566d3c | ||
|
|
4cd6c6ad2e | ||
|
|
81a60254a2 | ||
|
|
1503f07c75 | ||
|
|
0af546cd1d | ||
|
|
0d33d2321a | ||
|
|
79964f149c | ||
|
|
bcb2471768 | ||
|
|
fcf0ee619b | ||
|
|
f3ec352066 | ||
|
|
023d0f1cc4 | ||
|
|
f526d3f972 | ||
|
|
e28f746294 |
@@ -21,3 +21,6 @@ JWT_EXPIRES_IN=
|
|||||||
QINIU_AK=
|
QINIU_AK=
|
||||||
QINIU_SK=
|
QINIU_SK=
|
||||||
QINIU_SCOPE=
|
QINIU_SCOPE=
|
||||||
|
|
||||||
|
# Python 虚拟环境路径,留空则使用系统 Python
|
||||||
|
# PYTHON_VENV_DIR=./.venv
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker run --rm --network none --entrypoint node \
|
docker run --rm --network none --entrypoint node \
|
||||||
qinglong-readiness:${{ matrix.image }} \
|
qinglong-readiness:${{ matrix.image }} \
|
||||||
-e 'const assert=require("node:assert/strict"); const {createRequire}=require("node:module"); for(const [name,want] of Object.entries({"@grpc/grpc-js":"1.14.5",compression:"1.8.2",multer:"2.3.0",sequelize:"6.37.8"}))assert.equal(require(name+"/package.json").version,want); const expressRequire=createRequire(require.resolve("express")); assert.equal(expressRequire("proxy-addr/package.json").version,"2.0.8"); const sockRequire=createRequire(require.resolve("sockjs")); assert.equal(sockRequire("websocket-driver/package.json").version,"0.7.5"); require("sqlite3"); require("nodemailer"); require("undici"); console.log("Minimal production patches and runtime imports verified")'
|
-e 'const assert=require("node:assert/strict"); const {createRequire}=require("node:module"); for(const [name,want] of Object.entries({"@grpc/grpc-js":"1.14.5",compression:"1.8.2",multer:"2.4.0",sequelize:"6.37.8"}))assert.equal(require(name+"/package.json").version,want); const expressRequire=createRequire(require.resolve("express")); assert.equal(expressRequire("proxy-addr/package.json").version,"2.0.8"); const sockRequire=createRequire(require.resolve("sockjs")); assert.equal(sockRequire("websocket-driver/package.json").version,"0.7.5"); require("sqlite3"); require("nodemailer"); require("undici"); console.log("Minimal production patches and runtime imports verified")'
|
||||||
- name: Export native Debian image for isolated dev validation
|
- name: Export native Debian image for isolated dev validation
|
||||||
if: matrix.image == 'debian'
|
if: matrix.image == 'debian'
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Node.js 原生部署与 Python venv
|
||||||
|
|
||||||
|
设置 `QL_DIR` 为安装目录,`QL_DATA_DIR` 为以 `/data` 结尾的绝对持久化数据路径。
|
||||||
|
如需隔离 Python,请在启动进程的环境中显式设置 `PYTHON_VENV_DIR`(绝对路径或相对 `QL_DIR` 的路径),例如 `export PYTHON_VENV_DIR=/srv/qinglong/.venv`。未设置时保留现有 Python prefix 行为。
|
||||||
|
|
||||||
|
- `qinglong`(`shell/start.sh`)安装系统依赖和 Node 工具,并安装 Python requests,然后调用公共启动脚本。
|
||||||
|
- `npm run start:simple`(`shell/start-simplify.sh`)使用已准备的 Node、pm2、nginx、jq 等依赖,不安装系统包、全局 npm 包或 requests;显式指定的 venv 不存在时仍会创建。请先在 venv 中安装 requests 和任务所需依赖。
|
||||||
|
- 两种启动方式均保留已有 `.env`;使用 venv 时清除继承的 `PYTHONPATH`/`PYTHONUSERBASE`,依赖查询、安装和卸载使用该 venv 的 Python。
|
||||||
|
|
||||||
|
该模式需具备现有启动流程所要求的 nginx 配置和 pm2 服务权限。生产环境设置变量应通过 systemd 的 `Environment` 或相应进程配置完成。已有系统 Python 依赖不自动迁移至 venv,需在 venv 中重新安装。
|
||||||
+2
-23
@@ -35,23 +35,6 @@ function isPathAllowed(targetPath: string): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeScriptFile(
|
|
||||||
targetPath: string,
|
|
||||||
content: string,
|
|
||||||
): Promise<boolean> {
|
|
||||||
const target = resolve(targetPath);
|
|
||||||
for (const writableRoot of config.writePathList) {
|
|
||||||
const root = resolve(writableRoot);
|
|
||||||
const prefix = root.endsWith(sep) ? root : root + sep;
|
|
||||||
// Check the actual write target, including filename, at the write boundary.
|
|
||||||
if (!target.startsWith(prefix)) continue;
|
|
||||||
if (!isPathAllowed(target)) return false;
|
|
||||||
await writeFileWithLock(target, content);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function existingWritableRealPath(targetPath: string): Promise<string> {
|
async function existingWritableRealPath(targetPath: string): Promise<string> {
|
||||||
if (!isPathAllowed(targetPath)) {
|
if (!isPathAllowed(targetPath)) {
|
||||||
throw new ScriptHistoryError(403, '暂无权限');
|
throw new ScriptHistoryError(403, '暂无权限');
|
||||||
@@ -390,9 +373,7 @@ export default (app: Router) => {
|
|||||||
await rmPath(originFilePath);
|
await rmPath(originFilePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!(await writeScriptFile(filePath, content))) {
|
await writeFileWithLock(filePath, content);
|
||||||
return res.send({ code: 403, message: t('暂无权限') });
|
|
||||||
}
|
|
||||||
return res.send({ code: 200 });
|
return res.send({ code: 200 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return historyError(e, res, next);
|
return historyError(e, res, next);
|
||||||
@@ -532,9 +513,7 @@ export default (app: Router) => {
|
|||||||
if (!isPathAllowed(filePath)) {
|
if (!isPathAllowed(filePath)) {
|
||||||
return res.send({ code: 403, message: t('暂无权限') });
|
return res.send({ code: 403, message: t('暂无权限') });
|
||||||
}
|
}
|
||||||
if (!(await writeScriptFile(filePath, content || ''))) {
|
await writeFileWithLock(filePath, content || '');
|
||||||
return res.send({ code: 403, message: t('暂无权限') });
|
|
||||||
}
|
|
||||||
|
|
||||||
const scriptService = Container.get(ScriptService);
|
const scriptService = Container.get(ScriptService);
|
||||||
const result = await scriptService.runScript(filePath);
|
const result = await scriptService.runScript(filePath);
|
||||||
|
|||||||
@@ -383,6 +383,19 @@ export default (app: Router) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/notify-log',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const data = await systemService.getNotifyLog();
|
||||||
|
res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
route.delete(
|
route.delete(
|
||||||
'/log',
|
'/log',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const SAMPLE_FILES = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const PYTHON_INSTALL_DIR = process.env.PYTHON_HOME;
|
export const PYTHON_INSTALL_DIR = process.env.PYTHON_HOME;
|
||||||
|
export const PYTHON_VENV_DIR = process.env.PYTHON_VENV_DIR;
|
||||||
|
|
||||||
export const NotificationModeStringMap = {
|
export const NotificationModeStringMap = {
|
||||||
0: 'gotify',
|
0: 'gotify',
|
||||||
|
|||||||
+196
-102
@@ -1,18 +1,25 @@
|
|||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
|
import { statSync } from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { exec, execSync } from 'child_process';
|
import { exec, execSync } from 'child_process';
|
||||||
import psTreeFun from 'ps-tree';
|
import psTreeFun from 'ps-tree';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { load } from 'js-yaml';
|
import { load } from 'js-yaml';
|
||||||
import config from './index';
|
import config from './index';
|
||||||
import { PYTHON_INSTALL_DIR, TASK_COMMAND } from './const';
|
import { PYTHON_INSTALL_DIR, PYTHON_VENV_DIR, TASK_COMMAND } from './const';
|
||||||
import Logger from '../loaders/logger';
|
import Logger from '../loaders/logger';
|
||||||
import { writeFileWithLock } from '../shared/utils';
|
import { writeFileWithLock } from '../shared/utils';
|
||||||
import { DependenceTypes } from '../data/dependence';
|
import { DependenceTypes } from '../data/dependence';
|
||||||
import { FormData } from 'undici';
|
import { FormData } from 'undici';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { maybeSudo, isInContainer } from './container';
|
import { maybeSudo, isInContainer } from './container';
|
||||||
|
import { shellQuote } from '../shared/shellQuote';
|
||||||
import { resolveFileAccess } from '../shared/fileAccess';
|
import { resolveFileAccess } from '../shared/fileAccess';
|
||||||
|
import {
|
||||||
|
mirrorWriteCommand,
|
||||||
|
normalizeMirror,
|
||||||
|
rewriteMirrorSources,
|
||||||
|
} from '../shared/linuxMirror';
|
||||||
|
|
||||||
export * from './share';
|
export * from './share';
|
||||||
|
|
||||||
@@ -670,7 +677,9 @@ export async function setSystemTimezone(timezone: string): Promise<boolean> {
|
|||||||
throw new Error('Invalid timezone');
|
throw new Error('Invalid timezone');
|
||||||
}
|
}
|
||||||
|
|
||||||
await promiseExec(maybeSudo(`ln -sf /usr/share/zoneinfo/${timezone} /etc/localtime`));
|
await promiseExec(
|
||||||
|
maybeSudo(`ln -sf /usr/share/zoneinfo/${timezone} /etc/localtime`),
|
||||||
|
);
|
||||||
await promiseExec(`echo "${timezone}" | ${maybeSudo('tee /etc/timezone')}`);
|
await promiseExec(`echo "${timezone}" | ${maybeSudo('tee /etc/timezone')}`);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -680,61 +689,150 @@ export async function setSystemTimezone(timezone: string): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to check if a name is a GitHub URL
|
||||||
|
function getPythonExecutable(): string {
|
||||||
|
return PYTHON_VENV_DIR
|
||||||
|
? shellQuote(path.resolve(config.rootPath, PYTHON_VENV_DIR, 'bin/python3'))
|
||||||
|
: 'python3';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPythonPip(): string {
|
||||||
|
return PYTHON_VENV_DIR ? `${getPythonExecutable()} -m pip` : 'pip3';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGitHubUrl(name: string): boolean {
|
||||||
|
// Support git+https://, git+http://, https://, and http:// URLs
|
||||||
|
// This covers GitHub URLs and other git-compatible repositories
|
||||||
|
return !!name.match(/^(git\+https?:\/\/|https?:\/\/)/i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if a name is a requirements file
|
||||||
|
function isRequirementsFile(name: string): boolean {
|
||||||
|
return !!name.match(/requirements.*\.(txt|in)$/i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if a name is a pyproject.toml file
|
||||||
|
function isPyprojectToml(name: string): boolean {
|
||||||
|
return name.endsWith('pyproject.toml');
|
||||||
|
}
|
||||||
|
|
||||||
export function getGetCommand(type: DependenceTypes, name: string): string {
|
export function getGetCommand(type: DependenceTypes, name: string): string {
|
||||||
|
const trimmedName = name.trim();
|
||||||
|
|
||||||
|
// For Python dependencies installed from GitHub or requirements files,
|
||||||
|
// we can't reliably check if they're installed, so skip the check
|
||||||
|
if (type === DependenceTypes.python3) {
|
||||||
|
if (
|
||||||
|
isGitHubUrl(trimmedName) ||
|
||||||
|
isRequirementsFile(trimmedName) ||
|
||||||
|
isPyprojectToml(trimmedName)
|
||||||
|
) {
|
||||||
|
// Return a command that will always indicate not installed
|
||||||
|
// This ensures GitHub URLs and requirements files are always installed
|
||||||
|
return 'echo ""';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const baseCommands = {
|
const baseCommands = {
|
||||||
[DependenceTypes.nodejs]: `pnpm ls -g | grep "${name}" | head -1`,
|
[DependenceTypes.nodejs]: `pnpm ls -g | grep "${trimmedName}" | head -1`,
|
||||||
[DependenceTypes.python3]: `
|
[DependenceTypes.python3]: `${getPythonExecutable()} -c ${shellQuote(
|
||||||
python3 -c "exec('''
|
`from importlib.metadata import version\nimport importlib.util as u\nname=${JSON.stringify(
|
||||||
name='${name}'
|
trimmedName,
|
||||||
try:
|
)}\ntry:\n print(version(name))\nexcept Exception:\n print(name if u.find_spec(name) else '')`,
|
||||||
from importlib.metadata import version
|
)}`,
|
||||||
print(version(name))
|
[DependenceTypes.linux]:
|
||||||
except:
|
getOsTypeSync() === 'Alpine'
|
||||||
import importlib.util as u
|
? `apk info -es ${trimmedName}`
|
||||||
import importlib.metadata as m
|
: maybeSudo(`dpkg-query -s ${trimmedName}`),
|
||||||
spec=u.find_spec(name)
|
|
||||||
print(name if spec else '')
|
|
||||||
''')"`,
|
|
||||||
[DependenceTypes.linux]: getOsTypeSync() === 'Alpine'
|
|
||||||
? `apk info -es ${name}`
|
|
||||||
: maybeSudo(`dpkg-query -s ${name}`),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return baseCommands[type];
|
return baseCommands[type];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getInstallCommand(type: DependenceTypes, name: string): string {
|
export function getInstallCommand(type: DependenceTypes, name: string): string {
|
||||||
|
if (type === DependenceTypes.python3) {
|
||||||
|
const prefix =
|
||||||
|
PYTHON_INSTALL_DIR && !PYTHON_VENV_DIR
|
||||||
|
? ` --prefix=${shellQuote(PYTHON_INSTALL_DIR)}`
|
||||||
|
: '';
|
||||||
|
const pip = `${getPythonPip()} install --disable-pip-version-check${
|
||||||
|
PYTHON_VENV_DIR ? '' : ' --root-user-action=ignore'
|
||||||
|
}${prefix}`;
|
||||||
|
name = name.trim();
|
||||||
|
if (isRequirementsFile(name) || isPyprojectToml(name)) {
|
||||||
|
const file = [config.scriptPath, config.repoPath]
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((root) => resolveFileAccess(root, [name]))
|
||||||
|
.find((file) => {
|
||||||
|
if (!file) return false;
|
||||||
|
try {
|
||||||
|
return statSync(file).isFile();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!file)
|
||||||
|
throw new Error(
|
||||||
|
'Python dependency file must be inside scripts or repo',
|
||||||
|
);
|
||||||
|
return isRequirementsFile(name)
|
||||||
|
? `${pip} -r ${shellQuote(file)}`
|
||||||
|
: `${pip} ${shellQuote(path.dirname(file))}`;
|
||||||
|
}
|
||||||
|
return `${pip} -- ${shellQuote(name)}`;
|
||||||
|
}
|
||||||
const baseCommands = {
|
const baseCommands = {
|
||||||
[DependenceTypes.nodejs]: 'pnpm add -g',
|
[DependenceTypes.nodejs]: 'pnpm add -g',
|
||||||
[DependenceTypes.python3]:
|
[DependenceTypes.linux]:
|
||||||
'pip3 install --disable-pip-version-check --root-user-action=ignore',
|
getOsTypeSync() === 'Alpine'
|
||||||
[DependenceTypes.linux]: getOsTypeSync() === 'Alpine'
|
? 'apk add --no-check-certificate'
|
||||||
? 'apk add --no-check-certificate'
|
: maybeSudo('apt-get install -y'),
|
||||||
: maybeSudo('apt-get install -y'),
|
|
||||||
};
|
};
|
||||||
|
return `${baseCommands[type]} ${name.trim()}`;
|
||||||
let command = baseCommands[type];
|
|
||||||
|
|
||||||
if (type === DependenceTypes.python3 && PYTHON_INSTALL_DIR) {
|
|
||||||
command = `${command} --prefix=${PYTHON_INSTALL_DIR}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${command} ${name.trim()}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUninstallCommand(
|
export function getUninstallCommand(
|
||||||
type: DependenceTypes,
|
type: DependenceTypes,
|
||||||
name: string,
|
name: string,
|
||||||
): string {
|
): string {
|
||||||
|
if (type === DependenceTypes.python3) {
|
||||||
|
name = name.trim();
|
||||||
|
if (isRequirementsFile(name)) {
|
||||||
|
const file = [config.scriptPath, config.repoPath]
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((root) => resolveFileAccess(root, [name]))
|
||||||
|
.find((file) => {
|
||||||
|
if (!file) return false;
|
||||||
|
try {
|
||||||
|
return statSync(file).isFile();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!file)
|
||||||
|
throw new Error(
|
||||||
|
'Python dependency file must be inside scripts or repo',
|
||||||
|
);
|
||||||
|
return `${getPythonPip()} uninstall --disable-pip-version-check${
|
||||||
|
PYTHON_VENV_DIR ? '' : ' --root-user-action=ignore'
|
||||||
|
} -y -r ${shellQuote(file)}`;
|
||||||
|
}
|
||||||
|
if (isPyprojectToml(name) || isGitHubUrl(name)) {
|
||||||
|
// A source URL/project can install several distributions and has no safe
|
||||||
|
// inverse. Removing its panel entry leaves installed packages intact.
|
||||||
|
return `printf '%s\\n' ${shellQuote(
|
||||||
|
'Remove installed Python distributions separately by package name.',
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
return `${getPythonPip()} uninstall --disable-pip-version-check${
|
||||||
|
PYTHON_VENV_DIR ? '' : ' --root-user-action=ignore'
|
||||||
|
} -y -- ${shellQuote(name)}`;
|
||||||
|
}
|
||||||
const baseCommands = {
|
const baseCommands = {
|
||||||
[DependenceTypes.nodejs]: 'pnpm remove -g',
|
[DependenceTypes.nodejs]: 'pnpm remove -g',
|
||||||
[DependenceTypes.python3]:
|
[DependenceTypes.linux]:
|
||||||
'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y',
|
getOsTypeSync() === 'Alpine' ? 'apk del' : maybeSudo('apt-get remove -y'),
|
||||||
[DependenceTypes.linux]: getOsTypeSync() === 'Alpine'
|
|
||||||
? 'apk del'
|
|
||||||
: maybeSudo('apt-get remove -y'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return `${baseCommands[type]} ${name.trim()}`;
|
return `${baseCommands[type]} ${name.trim()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,77 +904,73 @@ export async function detectOS(): Promise<
|
|||||||
return osType;
|
return osType;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getCurrentMirrorDomain(
|
|
||||||
filePath: string,
|
|
||||||
): Promise<string | null> {
|
|
||||||
const fileContent = await fs.readFile(filePath, 'utf8');
|
|
||||||
const lines = fileContent.split('\n');
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.trim().startsWith('#')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const match = line.match(/https?:\/\/[^\/]+/);
|
|
||||||
if (match) {
|
|
||||||
return match[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function replaceDomainInFile(
|
|
||||||
filePath: string,
|
|
||||||
oldDomainWithScheme: string,
|
|
||||||
newDomainWithScheme: string,
|
|
||||||
): Promise<void> {
|
|
||||||
let fileContent = await fs.readFile(filePath, 'utf8');
|
|
||||||
let updatedContent = fileContent.replace(
|
|
||||||
new RegExp(oldDomainWithScheme, 'g'),
|
|
||||||
newDomainWithScheme,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!newDomainWithScheme.endsWith('/')) {
|
|
||||||
newDomainWithScheme += '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
await writeFileWithLock(filePath, updatedContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function _updateLinuxMirror(
|
async function _updateLinuxMirror(
|
||||||
osType: string,
|
osType: string,
|
||||||
mirrorDomainWithScheme: string,
|
mirror: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
const defaults: Record<string, string> = {
|
||||||
|
Debian: 'http://deb.debian.org/debian',
|
||||||
|
Ubuntu: 'http://archive.ubuntu.com/ubuntu',
|
||||||
|
Alpine: 'http://dl-cdn.alpinelinux.org/alpine',
|
||||||
|
};
|
||||||
|
if (!defaults[osType])
|
||||||
|
throw new Error('Unsupported OS type for updating mirrors.');
|
||||||
|
const target = mirror || defaults[osType];
|
||||||
|
normalizeMirror(target);
|
||||||
const S = isInContainer() ? 'sudo ' : '';
|
const S = isInContainer() ? 'sudo ' : '';
|
||||||
// APT otherwise returns zero when index downloads fail with transient errors.
|
const update =
|
||||||
const aptUpdate = `${S}apt-get update --error-on=any`;
|
osType === 'Alpine' ? 'apk update' : `${S}apt-get update --error-on=any`;
|
||||||
let filePath: string, currentDomainWithScheme: string | null;
|
let file =
|
||||||
switch (osType) {
|
osType === 'Alpine'
|
||||||
case 'Debian':
|
? '/etc/apk/repositories'
|
||||||
filePath = '/etc/apt/sources.list.d/debian.sources';
|
: `/etc/apt/sources.list.d/${osType.toLowerCase()}.sources`;
|
||||||
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
|
let content = '';
|
||||||
if (currentDomainWithScheme) {
|
try {
|
||||||
return `${S}sed -i 's|${currentDomainWithScheme}|${mirrorDomainWithScheme || 'http://deb.debian.org'}|g' ${filePath} || (${S}mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://deb.debian.org'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates\\nComponents: main\\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg" | ${S}tee ${filePath}) && ${aptUpdate}`;
|
content = await fs.readFile(file, 'utf8');
|
||||||
} else {
|
} catch (error: any) {
|
||||||
return `${S}mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://deb.debian.org'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates\\nComponents: main\\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg" | ${S}tee ${filePath} && ${aptUpdate}`;
|
if (error.code !== 'ENOENT') throw error;
|
||||||
|
if (osType !== 'Alpine') {
|
||||||
|
try {
|
||||||
|
file = '/etc/apt/sources.list';
|
||||||
|
content = await fs.readFile(file, 'utf8');
|
||||||
|
} catch (legacyError: any) {
|
||||||
|
if (legacyError.code !== 'ENOENT') throw legacyError;
|
||||||
|
file = `/etc/apt/sources.list.d/${osType.toLowerCase()}.sources`;
|
||||||
}
|
}
|
||||||
case 'Ubuntu':
|
}
|
||||||
filePath = '/etc/apt/sources.list.d/ubuntu.sources';
|
|
||||||
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
|
|
||||||
if (currentDomainWithScheme) {
|
|
||||||
return `${S}sed -i 's|${currentDomainWithScheme}|${mirrorDomainWithScheme || 'http://archive.ubuntu.com'}|g' ${filePath} || (${S}mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://archive.ubuntu.com'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-backports\\nComponents: main restricted universe multiverse\\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg" | ${S}tee ${filePath}) && ${aptUpdate}`;
|
|
||||||
} else {
|
|
||||||
return `${S}mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://archive.ubuntu.com'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-backports\\nComponents: main restricted universe multiverse\\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg" | ${S}tee ${filePath} && ${aptUpdate}`;
|
|
||||||
}
|
|
||||||
case 'Alpine':
|
|
||||||
filePath = '/etc/apk/repositories';
|
|
||||||
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
|
|
||||||
if (currentDomainWithScheme) {
|
|
||||||
return `sed -i 's|${currentDomainWithScheme}|${mirrorDomainWithScheme || 'http://dl-cdn.alpinelinux.org'}|g' ${filePath} || (mkdir -p /etc/apk && echo -e "\\$(grep VERSION_ID /etc/os-release | cut -d= -f2 | cut -d. -f1,2)/main\\n\\$(grep VERSION_ID /etc/os-release | cut -d= -f2 | cut -d. -f1,2)/community" | sed "s|^|${mirrorDomainWithScheme || 'http://dl-cdn.alpinelinux.org'}/alpine/v|" | tee ${filePath}) && apk update`;
|
|
||||||
} else {
|
|
||||||
return `mkdir -p /etc/apk && echo -e "\\$(grep VERSION_ID /etc/os-release | cut -d= -f2 | cut -d. -f1,2)/main\\n\\$(grep VERSION_ID /etc/os-release | cut -d= -f2 | cut -d. -f1,2)/community" | sed "s|^|${mirrorDomainWithScheme || 'http://dl-cdn.alpinelinux.org'}/alpine/v|" | tee ${filePath} && apk update`;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw Error('Unsupported OS type for updating mirrors.');
|
|
||||||
}
|
}
|
||||||
|
if (!content.trim()) {
|
||||||
|
const release = await fs.readFile('/etc/os-release', 'utf8');
|
||||||
|
if (osType === 'Alpine') {
|
||||||
|
const version = release.match(/^VERSION_ID=["']?(\d+\.\d+)/m)?.[1];
|
||||||
|
if (!version)
|
||||||
|
throw new Error('Cannot determine Alpine repository version');
|
||||||
|
content = `${defaults.Alpine}/v${version}/main\n${defaults.Alpine}/v${version}/community\n`;
|
||||||
|
} else {
|
||||||
|
// The fallback may have read an empty legacy .list. Deb822 stanzas must
|
||||||
|
// be written to a .sources file, leaving the legacy file untouched.
|
||||||
|
file = `/etc/apt/sources.list.d/${osType.toLowerCase()}.sources`;
|
||||||
|
const codename = release.match(/^VERSION_CODENAME=["']?([\w-]+)/m)?.[1];
|
||||||
|
if (!codename)
|
||||||
|
throw new Error('Cannot determine APT repository codename');
|
||||||
|
const ubuntu = osType === 'Ubuntu';
|
||||||
|
content = `Types: deb\nURIs: ${
|
||||||
|
defaults[osType]
|
||||||
|
}\nSuites: ${codename} ${codename}-updates${
|
||||||
|
ubuntu ? ` ${codename}-backports` : ''
|
||||||
|
}\nComponents: ${
|
||||||
|
ubuntu ? 'main restricted universe multiverse' : 'main'
|
||||||
|
}\nSigned-By: /usr/share/keyrings/${osType.toLowerCase()}-archive-keyring.gpg\n`;
|
||||||
|
if (!ubuntu)
|
||||||
|
content += `\nTypes: deb\nURIs: http://security.debian.org/debian-security\nSuites: ${codename}-security\nComponents: main\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mirrorWriteCommand(
|
||||||
|
file,
|
||||||
|
rewriteMirrorSources(content, target),
|
||||||
|
S,
|
||||||
|
update,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateLinuxMirrorFile(mirror: string): Promise<string> {
|
export async function updateLinuxMirrorFile(mirror: string): Promise<string> {
|
||||||
|
|||||||
@@ -28,8 +28,12 @@ export enum AuthDataType {
|
|||||||
'removeLogFrequency' = 'removeLogFrequency',
|
'removeLogFrequency' = 'removeLogFrequency',
|
||||||
'systemConfig' = 'systemConfig',
|
'systemConfig' = 'systemConfig',
|
||||||
'authConfig' = 'authConfig',
|
'authConfig' = 'authConfig',
|
||||||
|
'notifyLog' = 'notifyLog',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const NotifyStatus = LoginStatus;
|
||||||
|
export type NotifyStatus = LoginStatus;
|
||||||
|
|
||||||
export interface SystemConfigInfo {
|
export interface SystemConfigInfo {
|
||||||
lang?: string;
|
lang?: string;
|
||||||
panelTitle?: string;
|
panelTitle?: string;
|
||||||
@@ -53,6 +57,17 @@ export interface LoginLogInfo {
|
|||||||
status?: LoginStatus;
|
status?: LoginStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NotifyLogInfo {
|
||||||
|
timestamp?: number;
|
||||||
|
title?: string;
|
||||||
|
content?: string;
|
||||||
|
status?: NotifyStatus;
|
||||||
|
notifyType?: string;
|
||||||
|
source?: 'system' | 'script';
|
||||||
|
scriptName?: string;
|
||||||
|
taskId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TokenInfo {
|
export interface TokenInfo {
|
||||||
value: string;
|
value: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
@@ -88,6 +103,7 @@ export interface AuthInfo {
|
|||||||
export type SystemModelInfo = SystemConfigInfo &
|
export type SystemModelInfo = SystemConfigInfo &
|
||||||
Partial<NotificationInfo> &
|
Partial<NotificationInfo> &
|
||||||
LoginLogInfo &
|
LoginLogInfo &
|
||||||
|
Partial<NotifyLogInfo> &
|
||||||
Partial<AuthInfo>;
|
Partial<AuthInfo>;
|
||||||
|
|
||||||
export interface SystemInstance
|
export interface SystemInstance
|
||||||
|
|||||||
@@ -179,7 +179,14 @@ export default ({ app }: { app: Application }) => {
|
|||||||
app.use(rewrite('/open/*', '/api/$1'));
|
app.use(rewrite('/open/*', '/api/$1'));
|
||||||
app.use(config.api.prefix, routes());
|
app.use(config.api.prefix, routes());
|
||||||
|
|
||||||
app.get('*', (_, res, next) => {
|
app.get('*', (req, res, next) => {
|
||||||
|
// Don't serve index.html for API routes
|
||||||
|
if (
|
||||||
|
req.path === config.api.prefix ||
|
||||||
|
req.path.startsWith(`${config.api.prefix}/`)
|
||||||
|
) {
|
||||||
|
return res.status(404).json({ code: 404, message: 'Not Found' });
|
||||||
|
}
|
||||||
const indexPath = path.join(frontendPath, 'index.html');
|
const indexPath = path.join(frontendPath, 'index.html');
|
||||||
res.sendFile(indexPath, (err) => {
|
res.sendFile(indexPath, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
@@ -260,6 +260,15 @@ message SystemNotifyRequest {
|
|||||||
optional NotificationInfo notificationInfo = 3;
|
optional NotificationInfo notificationInfo = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message RecordNotifyLogRequest {
|
||||||
|
string title = 1;
|
||||||
|
string content = 2;
|
||||||
|
bool success = 3;
|
||||||
|
string notifyType = 4;
|
||||||
|
string scriptName = 5;
|
||||||
|
string taskId = 6;
|
||||||
|
}
|
||||||
|
|
||||||
service Api {
|
service Api {
|
||||||
rpc GetEnvs(GetEnvsRequest) returns (EnvsResponse) {}
|
rpc GetEnvs(GetEnvsRequest) returns (EnvsResponse) {}
|
||||||
rpc CreateEnv(CreateEnvRequest) returns (EnvsResponse) {}
|
rpc CreateEnv(CreateEnvRequest) returns (EnvsResponse) {}
|
||||||
@@ -271,6 +280,7 @@ service Api {
|
|||||||
rpc UpdateEnvNames(UpdateEnvNamesRequest) returns (Response) {}
|
rpc UpdateEnvNames(UpdateEnvNamesRequest) returns (Response) {}
|
||||||
rpc GetEnvById(GetEnvByIdRequest) returns (EnvResponse) {}
|
rpc GetEnvById(GetEnvByIdRequest) returns (EnvResponse) {}
|
||||||
rpc SystemNotify(SystemNotifyRequest) returns (Response) {}
|
rpc SystemNotify(SystemNotifyRequest) returns (Response) {}
|
||||||
|
rpc RecordNotifyLog(RecordNotifyLogRequest) returns (Response) {}
|
||||||
rpc GetCronDetail(CronDetailRequest) returns (CronDetailResponse) {}
|
rpc GetCronDetail(CronDetailRequest) returns (CronDetailResponse) {}
|
||||||
rpc CreateCron(CreateCronRequest) returns (CronResponse) {}
|
rpc CreateCron(CreateCronRequest) returns (CronResponse) {}
|
||||||
rpc UpdateCron(UpdateCronRequest) returns (CronResponse) {}
|
rpc UpdateCron(UpdateCronRequest) returns (CronResponse) {}
|
||||||
|
|||||||
+175
-1
@@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-ts_proto v2.6.1
|
// protoc-gen-ts_proto v2.6.1
|
||||||
// protoc v3.17.3
|
// protoc v6.31.1
|
||||||
// source: back/protos/api.proto
|
// source: back/protos/api.proto
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
@@ -417,6 +417,15 @@ export interface SystemNotifyRequest {
|
|||||||
notificationInfo?: NotificationInfo | undefined;
|
notificationInfo?: NotificationInfo | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RecordNotifyLogRequest {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
success: boolean;
|
||||||
|
notifyType: string;
|
||||||
|
scriptName: string;
|
||||||
|
taskId: string;
|
||||||
|
}
|
||||||
|
|
||||||
function createBaseEnvItem(): EnvItem {
|
function createBaseEnvItem(): EnvItem {
|
||||||
return {
|
return {
|
||||||
id: undefined,
|
id: undefined,
|
||||||
@@ -4296,6 +4305,146 @@ export const SystemNotifyRequest: MessageFns<SystemNotifyRequest> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function createBaseRecordNotifyLogRequest(): RecordNotifyLogRequest {
|
||||||
|
return { title: "", content: "", success: false, notifyType: "", scriptName: "", taskId: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RecordNotifyLogRequest: MessageFns<RecordNotifyLogRequest> = {
|
||||||
|
encode(message: RecordNotifyLogRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.title !== "") {
|
||||||
|
writer.uint32(10).string(message.title);
|
||||||
|
}
|
||||||
|
if (message.content !== "") {
|
||||||
|
writer.uint32(18).string(message.content);
|
||||||
|
}
|
||||||
|
if (message.success !== false) {
|
||||||
|
writer.uint32(24).bool(message.success);
|
||||||
|
}
|
||||||
|
if (message.notifyType !== "") {
|
||||||
|
writer.uint32(34).string(message.notifyType);
|
||||||
|
}
|
||||||
|
if (message.scriptName !== "") {
|
||||||
|
writer.uint32(42).string(message.scriptName);
|
||||||
|
}
|
||||||
|
if (message.taskId !== "") {
|
||||||
|
writer.uint32(50).string(message.taskId);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): RecordNotifyLogRequest {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
let end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseRecordNotifyLogRequest();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.title = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 18) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.content = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
if (tag !== 24) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success = reader.bool();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 4: {
|
||||||
|
if (tag !== 34) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.notifyType = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 5: {
|
||||||
|
if (tag !== 42) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.scriptName = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 6: {
|
||||||
|
if (tag !== 50) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.taskId = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): RecordNotifyLogRequest {
|
||||||
|
return {
|
||||||
|
title: isSet(object.title) ? globalThis.String(object.title) : "",
|
||||||
|
content: isSet(object.content) ? globalThis.String(object.content) : "",
|
||||||
|
success: isSet(object.success) ? globalThis.Boolean(object.success) : false,
|
||||||
|
notifyType: isSet(object.notifyType) ? globalThis.String(object.notifyType) : "",
|
||||||
|
scriptName: isSet(object.scriptName) ? globalThis.String(object.scriptName) : "",
|
||||||
|
taskId: isSet(object.taskId) ? globalThis.String(object.taskId) : "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: RecordNotifyLogRequest): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.title !== "") {
|
||||||
|
obj.title = message.title;
|
||||||
|
}
|
||||||
|
if (message.content !== "") {
|
||||||
|
obj.content = message.content;
|
||||||
|
}
|
||||||
|
if (message.success !== false) {
|
||||||
|
obj.success = message.success;
|
||||||
|
}
|
||||||
|
if (message.notifyType !== "") {
|
||||||
|
obj.notifyType = message.notifyType;
|
||||||
|
}
|
||||||
|
if (message.scriptName !== "") {
|
||||||
|
obj.scriptName = message.scriptName;
|
||||||
|
}
|
||||||
|
if (message.taskId !== "") {
|
||||||
|
obj.taskId = message.taskId;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<RecordNotifyLogRequest>, I>>(base?: I): RecordNotifyLogRequest {
|
||||||
|
return RecordNotifyLogRequest.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<RecordNotifyLogRequest>, I>>(object: I): RecordNotifyLogRequest {
|
||||||
|
const message = createBaseRecordNotifyLogRequest();
|
||||||
|
message.title = object.title ?? "";
|
||||||
|
message.content = object.content ?? "";
|
||||||
|
message.success = object.success ?? false;
|
||||||
|
message.notifyType = object.notifyType ?? "";
|
||||||
|
message.scriptName = object.scriptName ?? "";
|
||||||
|
message.taskId = object.taskId ?? "";
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export type ApiService = typeof ApiService;
|
export type ApiService = typeof ApiService;
|
||||||
export const ApiService = {
|
export const ApiService = {
|
||||||
getEnvs: {
|
getEnvs: {
|
||||||
@@ -4388,6 +4537,15 @@ export const ApiService = {
|
|||||||
responseSerialize: (value: Response) => Buffer.from(Response.encode(value).finish()),
|
responseSerialize: (value: Response) => Buffer.from(Response.encode(value).finish()),
|
||||||
responseDeserialize: (value: Buffer) => Response.decode(value),
|
responseDeserialize: (value: Buffer) => Response.decode(value),
|
||||||
},
|
},
|
||||||
|
recordNotifyLog: {
|
||||||
|
path: "/com.ql.api.Api/RecordNotifyLog",
|
||||||
|
requestStream: false,
|
||||||
|
responseStream: false,
|
||||||
|
requestSerialize: (value: RecordNotifyLogRequest) => Buffer.from(RecordNotifyLogRequest.encode(value).finish()),
|
||||||
|
requestDeserialize: (value: Buffer) => RecordNotifyLogRequest.decode(value),
|
||||||
|
responseSerialize: (value: Response) => Buffer.from(Response.encode(value).finish()),
|
||||||
|
responseDeserialize: (value: Buffer) => Response.decode(value),
|
||||||
|
},
|
||||||
getCronDetail: {
|
getCronDetail: {
|
||||||
path: "/com.ql.api.Api/GetCronDetail",
|
path: "/com.ql.api.Api/GetCronDetail",
|
||||||
requestStream: false,
|
requestStream: false,
|
||||||
@@ -4482,6 +4640,7 @@ export interface ApiServer extends UntypedServiceImplementation {
|
|||||||
updateEnvNames: handleUnaryCall<UpdateEnvNamesRequest, Response>;
|
updateEnvNames: handleUnaryCall<UpdateEnvNamesRequest, Response>;
|
||||||
getEnvById: handleUnaryCall<GetEnvByIdRequest, EnvResponse>;
|
getEnvById: handleUnaryCall<GetEnvByIdRequest, EnvResponse>;
|
||||||
systemNotify: handleUnaryCall<SystemNotifyRequest, Response>;
|
systemNotify: handleUnaryCall<SystemNotifyRequest, Response>;
|
||||||
|
recordNotifyLog: handleUnaryCall<RecordNotifyLogRequest, Response>;
|
||||||
getCronDetail: handleUnaryCall<CronDetailRequest, CronDetailResponse>;
|
getCronDetail: handleUnaryCall<CronDetailRequest, CronDetailResponse>;
|
||||||
createCron: handleUnaryCall<CreateCronRequest, CronResponse>;
|
createCron: handleUnaryCall<CreateCronRequest, CronResponse>;
|
||||||
updateCron: handleUnaryCall<UpdateCronRequest, CronResponse>;
|
updateCron: handleUnaryCall<UpdateCronRequest, CronResponse>;
|
||||||
@@ -4644,6 +4803,21 @@ export interface ApiClient extends Client {
|
|||||||
options: Partial<CallOptions>,
|
options: Partial<CallOptions>,
|
||||||
callback: (error: ServiceError | null, response: Response) => void,
|
callback: (error: ServiceError | null, response: Response) => void,
|
||||||
): ClientUnaryCall;
|
): ClientUnaryCall;
|
||||||
|
recordNotifyLog(
|
||||||
|
request: RecordNotifyLogRequest,
|
||||||
|
callback: (error: ServiceError | null, response: Response) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
recordNotifyLog(
|
||||||
|
request: RecordNotifyLogRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
callback: (error: ServiceError | null, response: Response) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
recordNotifyLog(
|
||||||
|
request: RecordNotifyLogRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
options: Partial<CallOptions>,
|
||||||
|
callback: (error: ServiceError | null, response: Response) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
getCronDetail(
|
getCronDetail(
|
||||||
request: CronDetailRequest,
|
request: CronDetailRequest,
|
||||||
callback: (error: ServiceError | null, response: CronDetailResponse) => void,
|
callback: (error: ServiceError | null, response: CronDetailResponse) => void,
|
||||||
|
|||||||
+25
-2
@@ -16,6 +16,7 @@ import {
|
|||||||
MoveEnvRequest,
|
MoveEnvRequest,
|
||||||
Response,
|
Response,
|
||||||
SystemNotifyRequest,
|
SystemNotifyRequest,
|
||||||
|
RecordNotifyLogRequest,
|
||||||
UpdateEnvNamesRequest,
|
UpdateEnvNamesRequest,
|
||||||
UpdateEnvRequest,
|
UpdateEnvRequest,
|
||||||
} from '../protos/api';
|
} from '../protos/api';
|
||||||
@@ -238,7 +239,8 @@ export const systemNotify = async (
|
|||||||
const data = await systemService.notify({
|
const data = await systemService.notify({
|
||||||
title: call.request.title,
|
title: call.request.title,
|
||||||
content: call.request.content,
|
content: call.request.content,
|
||||||
notificationInfo: call.request.notificationInfo as unknown as NotificationInfo,
|
notificationInfo: call.request
|
||||||
|
.notificationInfo as unknown as NotificationInfo,
|
||||||
});
|
});
|
||||||
callback(null, data);
|
callback(null, data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -246,10 +248,31 @@ export const systemNotify = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const recordNotifyLog = async (
|
||||||
|
call: ServerUnaryCall<RecordNotifyLogRequest, Response>,
|
||||||
|
callback: sendUnaryData<Response>,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await Container.get(SystemService).recordNotifyLog({
|
||||||
|
title: call.request.title,
|
||||||
|
content: call.request.content,
|
||||||
|
status: call.request.success ? 0 : 1,
|
||||||
|
notifyType: call.request.notifyType,
|
||||||
|
source: 'script',
|
||||||
|
scriptName: call.request.scriptName,
|
||||||
|
taskId: call.request.taskId,
|
||||||
|
});
|
||||||
|
callback(null, { code: 200 });
|
||||||
|
} catch (error: any) {
|
||||||
|
callback(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const normalizeCronData = (data: CronItem | null): CronItem | undefined => {
|
const normalizeCronData = (data: CronItem | null): CronItem | undefined => {
|
||||||
if (!data) return undefined;
|
if (!data) return undefined;
|
||||||
// create() returns a Sequelize instance; spreading it omits attribute getters.
|
// create() returns a Sequelize instance; spreading it omits attribute getters.
|
||||||
const cron = data instanceof Model ? (data.get({ plain: true }) as CronItem) : data;
|
const cron =
|
||||||
|
data instanceof Model ? (data.get({ plain: true }) as CronItem) : data;
|
||||||
return {
|
return {
|
||||||
...cron,
|
...cron,
|
||||||
labels: cron.labels ?? [],
|
labels: cron.labels ?? [],
|
||||||
|
|||||||
+34
-12
@@ -32,10 +32,11 @@ export default class DependenceService {
|
|||||||
constructor(
|
constructor(
|
||||||
@Inject('logger') private logger: winston.Logger,
|
@Inject('logger') private logger: winston.Logger,
|
||||||
private sockService: SockService,
|
private sockService: SockService,
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
public async create(payloads: Dependence[]): Promise<Dependence[]> {
|
public async create(payloads: Dependence[]): Promise<Dependence[]> {
|
||||||
const tabs = payloads.map((x) => {
|
const tabs = payloads.map((x) => {
|
||||||
|
getInstallCommand(x.type, x.name);
|
||||||
const tab = new Dependence({ ...x, status: DependenceStatus.queued });
|
const tab = new Dependence({ ...x, status: DependenceStatus.queued });
|
||||||
return tab;
|
return tab;
|
||||||
});
|
});
|
||||||
@@ -59,6 +60,7 @@ export default class DependenceService {
|
|||||||
...other,
|
...other,
|
||||||
status: DependenceStatus.queued,
|
status: DependenceStatus.queued,
|
||||||
});
|
});
|
||||||
|
getInstallCommand(tab.type, tab.name);
|
||||||
const newDoc = await this.updateDb(tab);
|
const newDoc = await this.updateDb(tab);
|
||||||
this.installDependenceOneByOne([newDoc]);
|
this.installDependenceOneByOne([newDoc]);
|
||||||
return newDoc;
|
return newDoc;
|
||||||
@@ -296,6 +298,23 @@ export default class DependenceService {
|
|||||||
force: boolean = false,
|
force: boolean = false,
|
||||||
) {
|
) {
|
||||||
return taskLimit.runDependeny(dependency, () => {
|
return taskLimit.runDependeny(dependency, () => {
|
||||||
|
let validatedCommand: string;
|
||||||
|
try {
|
||||||
|
validatedCommand = isInstall
|
||||||
|
? getInstallCommand(dependency.type, dependency.name.trim())
|
||||||
|
: getUninstallCommand(dependency.type, dependency.name.trim());
|
||||||
|
} catch (error) {
|
||||||
|
taskLimit.removeQueuedDependency(dependency);
|
||||||
|
return DependenceModel.update(
|
||||||
|
{
|
||||||
|
status: isInstall
|
||||||
|
? DependenceStatus.installFailed
|
||||||
|
: DependenceStatus.removeFailed,
|
||||||
|
log: [String(error)],
|
||||||
|
},
|
||||||
|
{ where: { id: dependency.id } },
|
||||||
|
).then(() => null);
|
||||||
|
}
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
if (taskLimit.firstDependencyId !== dependency.id) {
|
if (taskLimit.firstDependencyId !== dependency.id) {
|
||||||
return resolve(null);
|
return resolve(null);
|
||||||
@@ -345,9 +364,7 @@ export default class DependenceService {
|
|||||||
: DependenceStatus.removing;
|
: DependenceStatus.removing;
|
||||||
await DependenceModel.update({ status }, { where: { id: depIds } });
|
await DependenceModel.update({ status }, { where: { id: depIds } });
|
||||||
|
|
||||||
let command = isInstall
|
let command = validatedCommand;
|
||||||
? getInstallCommand(dependency.type, depName)
|
|
||||||
: getUninstallCommand(dependency.type, depName);
|
|
||||||
if (isLinuxDependence) {
|
if (isLinuxDependence) {
|
||||||
command = isInstall
|
command = isInstall
|
||||||
? `${linuxCommand.install} ${depName.trim()}`
|
? `${linuxCommand.install} ${depName.trim()}`
|
||||||
@@ -470,15 +487,20 @@ export default class DependenceService {
|
|||||||
|
|
||||||
const message =
|
const message =
|
||||||
'\n' +
|
'\n' +
|
||||||
tf('依赖%s%s,结束时间 %s,耗时 %s 秒',
|
tf(
|
||||||
actionText,
|
'依赖%s%s,结束时间 %s,耗时 %s 秒',
|
||||||
resultText,
|
actionText,
|
||||||
endTime.format('YYYY-MM-DD HH:mm:ss'),
|
resultText,
|
||||||
String(endTime.diff(startTime, 'second')),
|
endTime.format('YYYY-MM-DD HH:mm:ss'),
|
||||||
);
|
String(endTime.diff(startTime, 'second')),
|
||||||
|
);
|
||||||
const exitStatus = isSucceed
|
const exitStatus = isSucceed
|
||||||
? (isInstall ? DependenceStatus.installed : DependenceStatus.removed)
|
? isInstall
|
||||||
: (isInstall ? DependenceStatus.installFailed : DependenceStatus.removeFailed);
|
? DependenceStatus.installed
|
||||||
|
: DependenceStatus.removed
|
||||||
|
: isInstall
|
||||||
|
? DependenceStatus.installFailed
|
||||||
|
: DependenceStatus.removeFailed;
|
||||||
this.sockService.sendMessage({
|
this.sockService.sendMessage({
|
||||||
type: socketMessageType,
|
type: socketMessageType,
|
||||||
message,
|
message,
|
||||||
|
|||||||
+12
-28
@@ -12,7 +12,7 @@ export class HttpServerService {
|
|||||||
async initialize(expressApp: express.Application, port: number) {
|
async initialize(expressApp: express.Application, port: number) {
|
||||||
const hostsToTry = [
|
const hostsToTry = [
|
||||||
config.bindHost,
|
config.bindHost,
|
||||||
...(config.bindHost === '::' ? ['0.0.0.0'] : []),
|
...(config.bindHost === '::' ? ['0.0.0.0'] : [])
|
||||||
];
|
];
|
||||||
|
|
||||||
let lastError: Error | null = null;
|
let lastError: Error | null = null;
|
||||||
@@ -20,21 +20,16 @@ export class HttpServerService {
|
|||||||
for (const host of hostsToTry) {
|
for (const host of hostsToTry) {
|
||||||
try {
|
try {
|
||||||
const server = await this.tryListen(expressApp, port, host);
|
const server = await this.tryListen(expressApp, port, host);
|
||||||
Logger.debug(
|
Logger.debug(`[boot] HTTP service started successfully on ${host}:${port}`);
|
||||||
`[boot] HTTP service started successfully on ${host}:${port}`,
|
|
||||||
);
|
|
||||||
metricsService.record('http_service_start', 1, {
|
metricsService.record('http_service_start', 1, {
|
||||||
port: port.toString(),
|
port: port.toString(),
|
||||||
host,
|
host
|
||||||
});
|
});
|
||||||
this.server = server;
|
this.server = server;
|
||||||
return server;
|
return server;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
lastError = err as Error;
|
lastError = err as Error;
|
||||||
Logger.warn(
|
Logger.warn(`Failed to bind HTTP on ${host}:${port}, trying next...`, err);
|
||||||
`Failed to bind HTTP on ${host}:${port}, trying next...`,
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,28 +37,17 @@ export class HttpServerService {
|
|||||||
throw lastError || new Error('Failed to start HTTP service');
|
throw lastError || new Error('Failed to start HTTP service');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async tryListen(
|
private async tryListen(expressApp: express.Application, port: number, host: string): Promise<Server> {
|
||||||
expressApp: express.Application,
|
|
||||||
port: number,
|
|
||||||
host: string,
|
|
||||||
): Promise<Server> {
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// There is one HTTP worker; accepting here avoids primary IPC handoff
|
// There is one HTTP worker; accepting here avoids primary IPC handoff
|
||||||
// for every connection. Restore shared listening for custom clusters.
|
// for every connection. Restore shared listening for custom clusters.
|
||||||
const server = expressApp.listen(
|
const server = expressApp.listen({
|
||||||
{
|
port,
|
||||||
port,
|
host,
|
||||||
host,
|
exclusive: process.env.QL_HTTP_SHARED_LISTEN !== 'true',
|
||||||
exclusive: process.env.QL_HTTP_SHARED_LISTEN !== 'true',
|
}, () => {
|
||||||
},
|
resolve(server);
|
||||||
() => {
|
});
|
||||||
resolve(server);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
server.requestTimeout = 300000;
|
|
||||||
server.headersTimeout = 120000;
|
|
||||||
server.keepAliveTimeout = 65000;
|
|
||||||
|
|
||||||
server.on('error', (err: Error) => {
|
server.on('error', (err: Error) => {
|
||||||
server.close();
|
server.close();
|
||||||
|
|||||||
+41
-25
@@ -1,3 +1,4 @@
|
|||||||
|
import { persistNotificationAudit } from '../shared/notificationAudit';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import nodemailer from 'nodemailer';
|
import nodemailer from 'nodemailer';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
@@ -55,27 +56,33 @@ export default class NotificationService {
|
|||||||
content: string,
|
content: string,
|
||||||
notificationInfo?: NotificationInfo,
|
notificationInfo?: NotificationInfo,
|
||||||
): Promise<boolean | undefined> {
|
): Promise<boolean | undefined> {
|
||||||
let { type, ...rest } = await this.userService.getNotificationMode();
|
let type: string | undefined;
|
||||||
if (notificationInfo?.type) {
|
let success = false;
|
||||||
type = notificationInfo?.type;
|
try {
|
||||||
}
|
const saved = await this.userService.getNotificationMode();
|
||||||
if (type) {
|
type = notificationInfo?.type || saved.type;
|
||||||
this.title = title;
|
if (type) {
|
||||||
this.content = content;
|
this.title = title;
|
||||||
let params = rest;
|
this.content = content;
|
||||||
if (notificationInfo) {
|
const { type: _, ...overrides } = notificationInfo || {};
|
||||||
const { type: _, ...others } = notificationInfo;
|
const { type: _saved, ...rest } = saved;
|
||||||
params = { ...rest, ...others };
|
this.params = { ...rest, ...overrides };
|
||||||
}
|
try {
|
||||||
this.params = params;
|
success = !!(await this.modeMap.get(type)?.call(this));
|
||||||
const notificationModeAction = this.modeMap.get(type);
|
} catch (error) {
|
||||||
try {
|
console.error(error);
|
||||||
return await notificationModeAction?.call(this);
|
}
|
||||||
} catch (error: any) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
}
|
||||||
|
return success;
|
||||||
|
} finally {
|
||||||
|
await persistNotificationAudit({
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
notifyType: type,
|
||||||
|
source: 'system',
|
||||||
|
status: success ? 0 : 1,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async testNotify(
|
public async testNotify(
|
||||||
@@ -84,14 +91,23 @@ export default class NotificationService {
|
|||||||
content: string,
|
content: string,
|
||||||
) {
|
) {
|
||||||
const { type, ...rest } = info;
|
const { type, ...rest } = info;
|
||||||
if (type) {
|
if (!type) return true;
|
||||||
|
let success = false;
|
||||||
|
try {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.content = content;
|
this.content = content;
|
||||||
this.params = rest;
|
this.params = rest;
|
||||||
const notificationModeAction = this.modeMap.get(type);
|
success = !!(await this.modeMap.get(type)?.call(this));
|
||||||
return await notificationModeAction?.call(this);
|
return success;
|
||||||
|
} finally {
|
||||||
|
await persistNotificationAudit({
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
notifyType: type,
|
||||||
|
source: 'system',
|
||||||
|
status: success ? 0 : 1,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseMailRecipients(value?: string) {
|
private parseMailRecipients(value?: string) {
|
||||||
@@ -921,7 +937,8 @@ export default class NotificationService {
|
|||||||
private async openiLink() {
|
private async openiLink() {
|
||||||
const { openiLinkAppToken, openiLinkHubUrl, openiLinkContextToken } =
|
const { openiLinkAppToken, openiLinkHubUrl, openiLinkContextToken } =
|
||||||
this.params;
|
this.params;
|
||||||
const baseUrl = openiLinkHubUrl?.replace(/\/$/, '') || 'https://hub.openilink.com';
|
const baseUrl =
|
||||||
|
openiLinkHubUrl?.replace(/\/$/, '') || 'https://hub.openilink.com';
|
||||||
const url = `${baseUrl}/bot/v1/message/send`;
|
const url = `${baseUrl}/bot/v1/message/send`;
|
||||||
const body: Record<string, string> = {
|
const body: Record<string, string> = {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@@ -974,5 +991,4 @@ export default class NotificationService {
|
|||||||
throw new Error(error.response ? error.response.body : error);
|
throw new Error(error.response ? error.response.body : error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-15
@@ -1,3 +1,4 @@
|
|||||||
|
import { persistNotificationAudit } from '../shared/notificationAudit';
|
||||||
import { spawn } from 'cross-spawn';
|
import { spawn } from 'cross-spawn';
|
||||||
import { execFile } from 'child_process';
|
import { execFile } from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
@@ -32,6 +33,8 @@ import {
|
|||||||
SystemInstance,
|
SystemInstance,
|
||||||
SystemModel,
|
SystemModel,
|
||||||
SystemModelInfo,
|
SystemModelInfo,
|
||||||
|
NotifyStatus,
|
||||||
|
NotifyLogInfo,
|
||||||
} from '../data/system';
|
} from '../data/system';
|
||||||
import taskLimit from '../shared/pLimit';
|
import taskLimit from '../shared/pLimit';
|
||||||
import cronClient from '../schedule/client';
|
import cronClient from '../schedule/client';
|
||||||
@@ -53,7 +56,7 @@ export default class SystemService {
|
|||||||
@Inject('logger') private logger: winston.Logger,
|
@Inject('logger') private logger: winston.Logger,
|
||||||
private scheduleService: ScheduleService,
|
private scheduleService: ScheduleService,
|
||||||
private sockService: SockService,
|
private sockService: SockService,
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
public async getSystemConfig() {
|
public async getSystemConfig() {
|
||||||
const doc = await this.getDb({ type: AuthDataType.systemConfig });
|
const doc = await this.getDb({ type: AuthDataType.systemConfig });
|
||||||
@@ -158,7 +161,9 @@ export default class SystemService {
|
|||||||
if (info.dependenceProxy) {
|
if (info.dependenceProxy) {
|
||||||
await fs.promises.writeFile(
|
await fs.promises.writeFile(
|
||||||
config.dependenceProxyFile,
|
config.dependenceProxyFile,
|
||||||
`export http_proxy=${shellQuote(info.dependenceProxy)}\nexport https_proxy=${shellQuote(info.dependenceProxy)}`,
|
`export http_proxy=${shellQuote(
|
||||||
|
info.dependenceProxy,
|
||||||
|
)}\nexport https_proxy=${shellQuote(info.dependenceProxy)}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await fs.promises.rm(config.dependenceProxyFile);
|
await fs.promises.rm(config.dependenceProxyFile);
|
||||||
@@ -293,7 +298,7 @@ export default class SystemService {
|
|||||||
);
|
);
|
||||||
const text = await body.text();
|
const text = await body.text();
|
||||||
lastVersionContent = parseContentVersion(text);
|
lastVersionContent = parseContentVersion(text);
|
||||||
} catch (error) { }
|
} catch (error) {}
|
||||||
|
|
||||||
if (!lastVersionContent) {
|
if (!lastVersionContent) {
|
||||||
lastVersionContent = currentVersionContent;
|
lastVersionContent = currentVersionContent;
|
||||||
@@ -407,11 +412,13 @@ export default class SystemService {
|
|||||||
if (notificationInfo && typeString) {
|
if (notificationInfo && typeString) {
|
||||||
notificationInfo.type = typeString;
|
notificationInfo.type = typeString;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSuccess = await this.notificationService.notify(
|
const isSuccess = await this.notificationService.notify(
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
notificationInfo,
|
notificationInfo,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
return { code: 200, message: t('通知发送成功') };
|
return { code: 200, message: t('通知发送成功') };
|
||||||
} else {
|
} else {
|
||||||
@@ -419,16 +426,36 @@ export default class SystemService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async run({ command, logPath }: { command: string; logPath?: string }, callback: TaskCallbacks) {
|
public async recordNotifyLog(info: NotifyLogInfo): Promise<void> {
|
||||||
|
await persistNotificationAudit(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getNotifyLog(): Promise<Array<NotifyLogInfo>> {
|
||||||
|
const docs = await SystemModel.findAll({
|
||||||
|
where: { type: AuthDataType.notifyLog },
|
||||||
|
order: [['id', 'DESC']],
|
||||||
|
limit: 200,
|
||||||
|
});
|
||||||
|
return docs.map((x) => ({ ...x.info, id: x.id }));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async run(
|
||||||
|
{ command, logPath }: { command: string; logPath?: string },
|
||||||
|
callback: TaskCallbacks,
|
||||||
|
) {
|
||||||
if (!command.startsWith(TASK_COMMAND)) {
|
if (!command.startsWith(TASK_COMMAND)) {
|
||||||
command = `${TASK_COMMAND} ${command}`;
|
command = `${TASK_COMMAND} ${command}`;
|
||||||
}
|
}
|
||||||
const logPathPrefix = logPath ? `real_log_path=${logPath}` : ''
|
const logPathPrefix = logPath ? `real_log_path=${logPath}` : '';
|
||||||
this.scheduleService.runTask(`${logPathPrefix} real_time=true ${command}`, callback, {
|
this.scheduleService.runTask(
|
||||||
command,
|
`${logPathPrefix} real_time=true ${command}`,
|
||||||
id: command.replace(/ /g, '-'),
|
callback,
|
||||||
runOrigin: 'system',
|
{
|
||||||
});
|
command,
|
||||||
|
id: command.replace(/ /g, '-'),
|
||||||
|
runOrigin: 'system',
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stop({ command, pid }: { command: string; pid: number }) {
|
public async stop({ command, pid }: { command: string; pid: number }) {
|
||||||
@@ -459,8 +486,20 @@ export default class SystemService {
|
|||||||
if (type && type.length) {
|
if (type && type.length) {
|
||||||
dataDirs = dataDirs.concat(type.filter((x) => x !== 'base'));
|
dataDirs = dataDirs.concat(type.filter((x) => x !== 'base'));
|
||||||
}
|
}
|
||||||
const allowed = new Set(['db', 'upload', 'config', 'scripts', 'log', 'deps',
|
const allowed = new Set([
|
||||||
'syslog', 'dep_cache', 'raw', 'repo', 'ssh.d', 'script-history']);
|
'db',
|
||||||
|
'upload',
|
||||||
|
'config',
|
||||||
|
'scripts',
|
||||||
|
'log',
|
||||||
|
'deps',
|
||||||
|
'syslog',
|
||||||
|
'dep_cache',
|
||||||
|
'raw',
|
||||||
|
'repo',
|
||||||
|
'ssh.d',
|
||||||
|
'script-history',
|
||||||
|
]);
|
||||||
if (dataDirs.some((dir) => !allowed.has(dir))) {
|
if (dataDirs.some((dir) => !allowed.has(dir))) {
|
||||||
return res.status(400).send({ code: 400, message: t('参数错误') });
|
return res.status(400).send({ code: 400, message: t('参数错误') });
|
||||||
}
|
}
|
||||||
@@ -470,9 +509,13 @@ export default class SystemService {
|
|||||||
dataDirs = dataDirs.filter((dir) => dir !== 'script-history');
|
dataDirs = dataDirs.filter((dir) => dir !== 'script-history');
|
||||||
}
|
}
|
||||||
const dataPaths = [...new Set(dataDirs)].map((dir) => `data/${dir}`);
|
const dataPaths = [...new Set(dataDirs)].map((dir) => `data/${dir}`);
|
||||||
await promisify(execFile)('tar', ['-zcf', config.dataTgzFile, '--', ...dataPaths], {
|
await promisify(execFile)(
|
||||||
cwd: path.dirname(config.dataPath),
|
'tar',
|
||||||
});
|
['-zcf', config.dataTgzFile, '--', ...dataPaths],
|
||||||
|
{
|
||||||
|
cwd: path.dirname(config.dataPath),
|
||||||
|
},
|
||||||
|
);
|
||||||
res.download(config.dataTgzFile);
|
res.download(config.dataTgzFile);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return res.send({ code: 400, message: error.message });
|
return res.send({ code: 400, message: error.message });
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { shellQuote } from './shellQuote';
|
||||||
|
|
||||||
|
const repositoryNames =
|
||||||
|
/^(debian(?:-security)?|ubuntu(?:-ports)?|alpine|armbian)$/;
|
||||||
|
|
||||||
|
export function normalizeMirror(mirror: string): URL {
|
||||||
|
if (/\s/.test(mirror)) throw new Error('Invalid Linux mirror URL');
|
||||||
|
const url = new URL(mirror);
|
||||||
|
if (
|
||||||
|
!['http:', 'https:'].includes(url.protocol) ||
|
||||||
|
url.username ||
|
||||||
|
url.password ||
|
||||||
|
url.search ||
|
||||||
|
url.hash
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'Linux mirror must be an HTTP(S) URL without credentials, query or fragment',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replaceMirrorUrl(current: string, mirror: string): string {
|
||||||
|
const target = normalizeMirror(mirror);
|
||||||
|
const previous = new URL(current);
|
||||||
|
const oldSegments = previous.pathname.split('/').filter(Boolean);
|
||||||
|
const newSegments = target.pathname.split('/').filter(Boolean);
|
||||||
|
const repositoryIndex = oldSegments.findIndex((part) =>
|
||||||
|
repositoryNames.test(part),
|
||||||
|
);
|
||||||
|
const targetIndex = newSegments.findIndex((part) =>
|
||||||
|
repositoryNames.test(part),
|
||||||
|
);
|
||||||
|
const prefix =
|
||||||
|
targetIndex >= 0 ? newSegments.slice(0, targetIndex) : newSegments;
|
||||||
|
if (repositoryIndex >= 0) {
|
||||||
|
const repository = oldSegments[repositoryIndex];
|
||||||
|
let suffix = oldSegments.slice(repositoryIndex + 1);
|
||||||
|
// Repair paths already corrupted by repeated authority-only replacement.
|
||||||
|
while (suffix[0] === repository) suffix.shift();
|
||||||
|
return `${target.origin}/${[...prefix, repository, ...suffix].join('/')}`;
|
||||||
|
}
|
||||||
|
// Unknown repository paths: a full URL replaces the old root, a hostname
|
||||||
|
// preserves it. Never append the newly entered root to the existing root.
|
||||||
|
return `${target.origin}/${(newSegments.length
|
||||||
|
? newSegments
|
||||||
|
: oldSegments
|
||||||
|
).join('/')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rewriteMirrorSources(content: string, mirror: string): string {
|
||||||
|
normalizeMirror(mirror);
|
||||||
|
return content
|
||||||
|
.split(/(\r?\n[ \t]*\r?\n)/)
|
||||||
|
.map((stanza) => {
|
||||||
|
const signedBy = stanza.match(/^Signed-By:\s*([^\r\n]*)/im)?.[1];
|
||||||
|
return stanza
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => {
|
||||||
|
if (!/^\s*(?:URIs:|deb(?:-src)?\s|https?:\/\/)/.test(line))
|
||||||
|
return line;
|
||||||
|
const legacy = /^\s*deb(?:-src)?\s/.test(line);
|
||||||
|
const key = legacy
|
||||||
|
? line.match(/\bsigned-by=([^\]\s]+)/i)?.[1]
|
||||||
|
: signedBy;
|
||||||
|
// In .list files a # starts an inline comment; its URLs are metadata.
|
||||||
|
const comment = legacy ? line.indexOf('#') : -1;
|
||||||
|
const source = comment < 0 ? line : line.slice(0, comment);
|
||||||
|
const rewritten = source.replace(/https?:\/\/[^\s#]+/g, (url) =>
|
||||||
|
isSystemRepository(url, key) ? replaceMirrorUrl(url, mirror) : url,
|
||||||
|
);
|
||||||
|
return rewritten + (comment < 0 ? '' : line.slice(comment));
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSystemRepository(url: string, signedBy?: string): boolean {
|
||||||
|
const keys =
|
||||||
|
signedBy
|
||||||
|
?.trim()
|
||||||
|
.split(/[\s,]+/)
|
||||||
|
.filter(Boolean) || [];
|
||||||
|
const archiveKeyring =
|
||||||
|
keys.length > 0 &&
|
||||||
|
keys.every((key) =>
|
||||||
|
/\/(?:debian|ubuntu|armbian)-archive-keyring\.(?:gpg|asc)$/.test(key),
|
||||||
|
);
|
||||||
|
// A separate signing key identifies a third-party archive, even if its
|
||||||
|
// repository happens to be called /debian or /ubuntu.
|
||||||
|
if (keys.length > 0 && !archiveKeyring) return false;
|
||||||
|
const segments = new URL(url).pathname.split('/').filter(Boolean);
|
||||||
|
const repositoryIndex = segments.findIndex((part) =>
|
||||||
|
repositoryNames.test(part),
|
||||||
|
);
|
||||||
|
if (repositoryIndex < 0) return false;
|
||||||
|
// Without archive metadata, accept only conventional archive roots. A
|
||||||
|
// vendor path such as Docker's /linux/debian is not the Debian archive.
|
||||||
|
return (
|
||||||
|
archiveKeyring ||
|
||||||
|
repositoryIndex === 0 ||
|
||||||
|
segments
|
||||||
|
.slice(0, repositoryIndex)
|
||||||
|
.every((part) => /^mirrors?$/.test(part)) ||
|
||||||
|
segments[repositoryIndex] === 'alpine'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mirrorWriteCommand(
|
||||||
|
file: string,
|
||||||
|
content: string,
|
||||||
|
prefix: string,
|
||||||
|
update: string,
|
||||||
|
): string {
|
||||||
|
return `${prefix}mkdir -p ${shellQuote(
|
||||||
|
file.slice(0, file.lastIndexOf('/')),
|
||||||
|
)} && printf '%s' ${shellQuote(content)} | ${prefix}tee ${shellQuote(
|
||||||
|
file,
|
||||||
|
)} >/dev/null && ${update}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Op } from 'sequelize';
|
||||||
|
import {
|
||||||
|
SystemModel,
|
||||||
|
AuthDataType,
|
||||||
|
NotifyStatus,
|
||||||
|
NotifyLogInfo,
|
||||||
|
} from '../data/system';
|
||||||
|
|
||||||
|
// Both backend sends and script reports use the same bounded, best-effort sink.
|
||||||
|
export async function persistNotificationAudit(
|
||||||
|
info: NotifyLogInfo,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await SystemModel.create({
|
||||||
|
type: AuthDataType.notifyLog,
|
||||||
|
info: {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
title: String(info.title || '').slice(0, 512),
|
||||||
|
content: String(info.content || '').slice(0, 8192),
|
||||||
|
status:
|
||||||
|
info.status === NotifyStatus.success
|
||||||
|
? NotifyStatus.success
|
||||||
|
: NotifyStatus.fail,
|
||||||
|
notifyType: String(info.notifyType || '').slice(0, 64),
|
||||||
|
source: info.source === 'script' ? 'script' : 'system',
|
||||||
|
scriptName: String(info.scriptName || '').slice(0, 512),
|
||||||
|
taskId: /^\d+$/.test(info.taskId || '') ? info.taskId : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const oldest = await SystemModel.findAll({
|
||||||
|
where: { type: AuthDataType.notifyLog },
|
||||||
|
attributes: ['id'],
|
||||||
|
order: [['id', 'DESC']],
|
||||||
|
offset: 200,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
if (oldest[0]?.id) {
|
||||||
|
await SystemModel.destroy({
|
||||||
|
where: {
|
||||||
|
type: AuthDataType.notifyLog,
|
||||||
|
id: { [Op.lte]: oldest[0].id },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to persist notification audit log');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ export async function writeFileWithLock(
|
|||||||
}
|
}
|
||||||
if (!(await fileExist(filePath))) {
|
if (!(await fileExist(filePath))) {
|
||||||
const fileHandle = await open(filePath, 'w');
|
const fileHandle = await open(filePath, 'w');
|
||||||
await fileHandle.close();
|
fileHandle.close();
|
||||||
}
|
}
|
||||||
const lockfilePath = getUniqueLockPath(filePath);
|
const lockfilePath = getUniqueLockPath(filePath);
|
||||||
|
|
||||||
@@ -34,12 +34,9 @@ export async function writeFileWithLock(
|
|||||||
},
|
},
|
||||||
lockfilePath,
|
lockfilePath,
|
||||||
});
|
});
|
||||||
try {
|
await writeFile(filePath, content, { encoding: 'utf8', ...options });
|
||||||
await writeFile(filePath, content, { encoding: 'utf8', ...options });
|
if (options?.mode) {
|
||||||
if (options?.mode) {
|
await chmod(filePath, options.mode);
|
||||||
await chmod(filePath, options.mode);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
await release();
|
|
||||||
}
|
}
|
||||||
|
await release();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ export interface OpenOperation {
|
|||||||
anonymous?: boolean;
|
anonymous?: boolean;
|
||||||
}
|
}
|
||||||
export const openOperations: readonly OpenOperation[] = [
|
export const openOperations: readonly OpenOperation[] = [
|
||||||
|
{
|
||||||
|
name: 'system notification-logs',
|
||||||
|
method: 'GET',
|
||||||
|
path: 'system/notify-log',
|
||||||
|
},
|
||||||
|
|
||||||
{ name: 'task view-list', method: 'GET', path: 'crons/views' },
|
{ name: 'task view-list', method: 'GET', path: 'crons/views' },
|
||||||
{
|
{
|
||||||
name: 'task view-create',
|
name: 'task view-create',
|
||||||
@@ -478,7 +484,12 @@ export const openOperations: readonly OpenOperation[] = [
|
|||||||
path: 'system/config/dependence-clean',
|
path: 'system/config/dependence-clean',
|
||||||
body: 'json',
|
body: 'json',
|
||||||
},
|
},
|
||||||
{ name: 'dashboard record', method: 'POST', path: 'dashboard/record', body: 'json' },
|
{
|
||||||
|
name: 'dashboard record',
|
||||||
|
method: 'POST',
|
||||||
|
path: 'dashboard/record',
|
||||||
|
body: 'json',
|
||||||
|
},
|
||||||
{ name: 'dashboard overview', method: 'GET', path: 'dashboard/overview' },
|
{ name: 'dashboard overview', method: 'GET', path: 'dashboard/overview' },
|
||||||
{ name: 'dashboard trend', method: 'GET', path: 'dashboard/trend' },
|
{ name: 'dashboard trend', method: 'GET', path: 'dashboard/trend' },
|
||||||
{ name: 'dashboard top-time', method: 'GET', path: 'dashboard/top-time' },
|
{ name: 'dashboard top-time', method: 'GET', path: 'dashboard/top-time' },
|
||||||
|
|||||||
@@ -409,7 +409,7 @@ test('cron metadata precedence agrees with unchanged legacy add_cron', (t) => {
|
|||||||
'--noprofile',
|
'--noprofile',
|
||||||
'--norc',
|
'--norc',
|
||||||
'-c',
|
'-c',
|
||||||
`${definition}\nt() { :; }\nnotify_api() { :; }\nadd_cron_api() { printf '%s' "$1"; }\nadd_cron "$dir_scripts/list" repo`,
|
`${definition}\nt() { :; }\nnotify_api() { :; }\nadd_cron_api() { printf '%s:%s:%s:%s' "$1" "$2" "$3" "$4"; }\nadd_cron "$dir_scripts/list" repo`,
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# 通知日志
|
||||||
|
|
||||||
|
通知日志记录系统统一通知入口(包括登录通知、配置测试和系统通知 API),以及任务使用内置 `notify.js`、`notify.py` 或 `QLAPI.notify` 发出的通知。脚本通知按实际配置的渠道各记录一条结果,未配置渠道和跳过的标题不记账。
|
||||||
|
|
||||||
|
日志包含时间、标题、正文、渠道、结果和来源;脚本来源另包含文件名与任务 ID。成功表示通知渠道返回成功或接受请求,不表示最终收件人已经收到。多目标渠道只在所有目标成功时记为成功。
|
||||||
|
|
||||||
|
所有入口共用数据库中的最近 200 条日志。标题最多 512 字符、正文最多 8192 字符;上报字段不包含通知密码、令牌或渠道配置。日志页受系统设置的管理员权限保护。
|
||||||
|
|
||||||
|
脚本通过现有的本机 mTLS gRPC 服务上报,独立客户端最多等待 1.5 秒。旧面板、服务不可用或数据库写入失败不会改变通知发送结果。Python 使用 Node 上报子进程,并限制在 3 秒内。
|
||||||
|
|
||||||
|
自行保存的旧版通知库需要更新为面板内置版本才能上报。脚本自行调用 HTTP、SMTP 或第三方 SDK 时不经过这些入口,无法自动纳入通知日志。
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Python 依赖来源
|
||||||
|
|
||||||
|
依赖管理中可手动添加 Python 包名、`git+https://` 等 pip 支持的源 URL,或脚本/订阅仓库目录内的 requirements 文件、pyproject.toml 路径。文件路径支持目录空格,必须位于持久化 scripts 或 repo 根目录内;越界与指向根目录外的符号链接会被拒绝。
|
||||||
|
|
||||||
|
requirements 使用 `pip install -r`,pyproject 使用完整项目目录安装,保留相对依赖文件和项目源码。拉取订阅不会自动安装仓库声明的依赖。
|
||||||
|
|
||||||
|
删除 requirements 条目会调用 `pip uninstall -r`。源 URL 或 pyproject 条目无法可靠逆向推断其安装的所有分发包,因此删除该条目只移除面板记录;需另外按包名卸载已安装的包,日志会明确提示。
|
||||||
+8
-5
@@ -28,7 +28,8 @@
|
|||||||
"panel": "npm run build:back && node static/build/app.js",
|
"panel": "npm run build:back && node static/build/app.js",
|
||||||
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true,snakeToCamel=false",
|
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true,snakeToCamel=false",
|
||||||
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
||||||
"postinstall": "max setup 2>/dev/null || true"
|
"postinstall": "max setup 2>/dev/null || true",
|
||||||
|
"start:simple": "bash shell/start-simplify.sh"
|
||||||
},
|
},
|
||||||
"gitHooks": {
|
"gitHooks": {
|
||||||
"pre-commit": "lint-staged"
|
"pre-commit": "lint-staged"
|
||||||
@@ -76,7 +77,9 @@
|
|||||||
"@codemirror/state": "6.5.4",
|
"@codemirror/state": "6.5.4",
|
||||||
"@codemirror/view": "6.39.16",
|
"@codemirror/view": "6.39.16",
|
||||||
"proxy-addr": "2.0.8",
|
"proxy-addr": "2.0.8",
|
||||||
"websocket-driver": "0.7.5"
|
"websocket-driver": "0.7.5",
|
||||||
|
"brace-expansion@1": "1.1.21",
|
||||||
|
"brace-expansion@2": "2.1.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -108,9 +111,9 @@
|
|||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"keyv": "^5.2.3",
|
"keyv": "^5.2.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"multer": "2.3.0",
|
"multer": "2.4.0",
|
||||||
"node-cron": "4.6.0",
|
"node-cron": "4.6.0",
|
||||||
"nodemailer": "^8.0.1",
|
"nodemailer": "^10.0.9",
|
||||||
"p-queue-cjs": "7.3.4",
|
"p-queue-cjs": "7.3.4",
|
||||||
"proper-lockfile": "^4.1.2",
|
"proper-lockfile": "^4.1.2",
|
||||||
"ps-tree": "^1.2.0",
|
"ps-tree": "^1.2.0",
|
||||||
@@ -171,7 +174,7 @@
|
|||||||
"concurrently": "^7.0.0",
|
"concurrently": "^7.0.0",
|
||||||
"file-saver": "2.0.2",
|
"file-saver": "2.0.2",
|
||||||
"lint-staged": "^13.0.3",
|
"lint-staged": "^13.0.3",
|
||||||
"moment": "2.30.1",
|
"moment": "2.31.0",
|
||||||
"monaco-editor": "0.33.0",
|
"monaco-editor": "0.33.0",
|
||||||
"nodemon": "^3.0.1",
|
"nodemon": "^3.0.1",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
|
|||||||
Generated
+368
-43
@@ -6,6 +6,8 @@ overrides:
|
|||||||
'@codemirror/view': 6.39.16
|
'@codemirror/view': 6.39.16
|
||||||
proxy-addr: 2.0.8
|
proxy-addr: 2.0.8
|
||||||
websocket-driver: 0.7.5
|
websocket-driver: 0.7.5
|
||||||
|
brace-expansion@1: 1.1.21
|
||||||
|
brace-expansion@2: 2.1.7
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ant-design/plots':
|
'@ant-design/plots':
|
||||||
@@ -28,7 +30,7 @@ dependencies:
|
|||||||
version: 12.0.1
|
version: 12.0.1
|
||||||
body-parser:
|
body-parser:
|
||||||
specifier: ^1.20.3
|
specifier: ^1.20.3
|
||||||
version: 1.20.3
|
version: 1.20.6
|
||||||
celebrate:
|
celebrate:
|
||||||
specifier: ^15.0.3
|
specifier: ^15.0.3
|
||||||
version: 15.0.3
|
version: 15.0.3
|
||||||
@@ -73,7 +75,7 @@ dependencies:
|
|||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
http-proxy-middleware:
|
http-proxy-middleware:
|
||||||
specifier: ^3.0.3
|
specifier: ^3.0.3
|
||||||
version: 3.0.3
|
version: 3.0.7
|
||||||
iconv-lite:
|
iconv-lite:
|
||||||
specifier: ^0.6.3
|
specifier: ^0.6.3
|
||||||
version: 0.6.3
|
version: 0.6.3
|
||||||
@@ -82,7 +84,7 @@ dependencies:
|
|||||||
version: 2.3.0(@types/node@17.0.45)
|
version: 2.3.0(@types/node@17.0.45)
|
||||||
js-yaml:
|
js-yaml:
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.3.2
|
||||||
jsonwebtoken:
|
jsonwebtoken:
|
||||||
specifier: ^9.0.2
|
specifier: ^9.0.2
|
||||||
version: 9.0.2
|
version: 9.0.2
|
||||||
@@ -93,14 +95,14 @@ dependencies:
|
|||||||
specifier: ^4.17.21
|
specifier: ^4.17.21
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
multer:
|
multer:
|
||||||
specifier: 2.3.0
|
specifier: 2.4.0
|
||||||
version: 2.3.0
|
version: 2.4.0
|
||||||
node-cron:
|
node-cron:
|
||||||
specifier: 4.6.0
|
specifier: 4.6.0
|
||||||
version: 4.6.0
|
version: 4.6.0
|
||||||
nodemailer:
|
nodemailer:
|
||||||
specifier: ^8.0.1
|
specifier: ^10.0.9
|
||||||
version: 8.0.1
|
version: 10.0.9
|
||||||
p-queue-cjs:
|
p-queue-cjs:
|
||||||
specifier: 7.3.4
|
specifier: 7.3.4
|
||||||
version: 7.3.4
|
version: 7.3.4
|
||||||
@@ -133,7 +135,7 @@ dependencies:
|
|||||||
version: 0.10.0
|
version: 0.10.0
|
||||||
undici:
|
undici:
|
||||||
specifier: ^7.9.0
|
specifier: ^7.9.0
|
||||||
version: 7.9.0
|
version: 7.29.1
|
||||||
uuid:
|
uuid:
|
||||||
specifier: ^11.0.3
|
specifier: ^11.0.3
|
||||||
version: 11.0.3
|
version: 11.0.3
|
||||||
@@ -264,7 +266,7 @@ devDependencies:
|
|||||||
version: 4.23.0(antd@4.24.16)(react-dom@18.3.1)(react@18.3.1)
|
version: 4.23.0(antd@4.24.16)(react-dom@18.3.1)(react@18.3.1)
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.4.0
|
specifier: ^1.4.0
|
||||||
version: 1.7.8
|
version: 1.20.0
|
||||||
compression-webpack-plugin:
|
compression-webpack-plugin:
|
||||||
specifier: 9.2.0
|
specifier: 9.2.0
|
||||||
version: 9.2.0
|
version: 9.2.0
|
||||||
@@ -278,8 +280,8 @@ devDependencies:
|
|||||||
specifier: ^13.0.3
|
specifier: ^13.0.3
|
||||||
version: 13.3.0
|
version: 13.3.0
|
||||||
moment:
|
moment:
|
||||||
specifier: 2.30.1
|
specifier: 2.31.0
|
||||||
version: 2.30.1
|
version: 2.31.0
|
||||||
monaco-editor:
|
monaco-editor:
|
||||||
specifier: 0.33.0
|
specifier: 0.33.0
|
||||||
version: 0.33.0
|
version: 0.33.0
|
||||||
@@ -876,7 +878,7 @@ packages:
|
|||||||
'@babel/runtime': 7.26.0
|
'@babel/runtime': 7.26.0
|
||||||
antd: 4.24.16(react-dom@18.3.1)(react@18.3.1)
|
antd: 4.24.16(react-dom@18.3.1)(react@18.3.1)
|
||||||
classnames: 2.5.1
|
classnames: 2.5.1
|
||||||
moment: 2.30.1
|
moment: 2.31.0
|
||||||
rc-util: 5.43.0(react-dom@18.3.1)(react@18.3.1)
|
rc-util: 5.43.0(react-dom@18.3.1)(react@18.3.1)
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
@@ -2879,7 +2881,7 @@ packages:
|
|||||||
globals: 13.24.0
|
globals: 13.24.0
|
||||||
ignore: 5.3.2
|
ignore: 5.3.2
|
||||||
import-fresh: 3.3.0
|
import-fresh: 3.3.0
|
||||||
js-yaml: 4.1.0
|
js-yaml: 4.3.2
|
||||||
minimatch: 3.1.5
|
minimatch: 3.1.5
|
||||||
strip-json-comments: 3.1.1
|
strip-json-comments: 3.1.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -3060,7 +3062,7 @@ packages:
|
|||||||
camelcase: 5.3.1
|
camelcase: 5.3.1
|
||||||
find-up: 4.1.0
|
find-up: 4.1.0
|
||||||
get-package-type: 0.1.0
|
get-package-type: 0.1.0
|
||||||
js-yaml: 3.14.1
|
js-yaml: 3.15.2
|
||||||
resolve-from: 5.0.0
|
resolve-from: 5.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@@ -5369,7 +5371,7 @@ packages:
|
|||||||
fast-deep-equal: 3.1.3
|
fast-deep-equal: 3.1.3
|
||||||
intl: 1.2.5
|
intl: 1.2.5
|
||||||
lodash: 4.17.21
|
lodash: 4.17.21
|
||||||
moment: 2.30.1
|
moment: 2.31.0
|
||||||
qiankun: 2.10.16
|
qiankun: 2.10.16
|
||||||
react-intl: 3.12.1(react@18.3.1)
|
react-intl: 3.12.1(react@18.3.1)
|
||||||
react-redux: 8.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1)(redux@4.2.1)
|
react-redux: 8.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1)(redux@4.2.1)
|
||||||
@@ -5677,7 +5679,6 @@ packages:
|
|||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
|
||||||
|
|
||||||
/agentkeepalive@4.5.0:
|
/agentkeepalive@4.5.0:
|
||||||
resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
|
resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
|
||||||
@@ -5867,7 +5868,7 @@ packages:
|
|||||||
classnames: 2.5.1
|
classnames: 2.5.1
|
||||||
copy-to-clipboard: 3.3.3
|
copy-to-clipboard: 3.3.3
|
||||||
lodash: 4.17.21
|
lodash: 4.17.21
|
||||||
moment: 2.30.1
|
moment: 2.31.0
|
||||||
rc-cascader: 3.7.3(react-dom@18.3.1)(react@18.3.1)
|
rc-cascader: 3.7.3(react-dom@18.3.1)(react@18.3.1)
|
||||||
rc-checkbox: 3.0.1(react-dom@18.3.1)(react@18.3.1)
|
rc-checkbox: 3.0.1(react-dom@18.3.1)(react@18.3.1)
|
||||||
rc-collapse: 3.4.2(react-dom@18.3.1)(react@18.3.1)
|
rc-collapse: 3.4.2(react-dom@18.3.1)(react@18.3.1)
|
||||||
@@ -6358,18 +6359,6 @@ packages:
|
|||||||
big-integer: 1.6.52
|
big-integer: 1.6.52
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/brace-expansion@1.1.12:
|
|
||||||
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
|
|
||||||
dependencies:
|
|
||||||
balanced-match: 1.0.2
|
|
||||||
concat-map: 0.0.1
|
|
||||||
|
|
||||||
/brace-expansion@2.0.2:
|
|
||||||
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
|
|
||||||
dependencies:
|
|
||||||
balanced-match: 1.0.2
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/braces@3.0.3:
|
/braces@3.0.3:
|
||||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -8302,7 +8291,7 @@ packages:
|
|||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
is-path-inside: 3.0.3
|
is-path-inside: 3.0.3
|
||||||
js-sdsl: 4.4.2
|
js-sdsl: 4.4.2
|
||||||
js-yaml: 4.1.0
|
js-yaml: 4.3.2
|
||||||
json-stable-stringify-without-jsonify: 1.0.1
|
json-stable-stringify-without-jsonify: 1.0.1
|
||||||
levn: 0.4.1
|
levn: 0.4.1
|
||||||
lodash.merge: 4.6.2
|
lodash.merge: 4.6.2
|
||||||
@@ -8513,7 +8502,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
accepts: 1.3.8
|
accepts: 1.3.8
|
||||||
array-flatten: 1.1.1
|
array-flatten: 1.1.1
|
||||||
body-parser: 1.20.3
|
body-parser: 1.20.6
|
||||||
content-disposition: 0.5.4
|
content-disposition: 0.5.4
|
||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
cookie: 0.7.1
|
cookie: 0.7.1
|
||||||
@@ -8663,7 +8652,7 @@ packages:
|
|||||||
/file-stream-rotator@0.6.1:
|
/file-stream-rotator@0.6.1:
|
||||||
resolution: {integrity: sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==}
|
resolution: {integrity: sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
moment: 2.30.1
|
moment: 2.31.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/fill-range@7.1.1:
|
/fill-range@7.1.1:
|
||||||
@@ -9334,7 +9323,6 @@ packages:
|
|||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
|
||||||
|
|
||||||
/human-signals@2.1.0:
|
/human-signals@2.1.0:
|
||||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||||
@@ -10587,7 +10575,7 @@ packages:
|
|||||||
engines: {node: '>=8.6'}
|
engines: {node: '>=8.6'}
|
||||||
dependencies:
|
dependencies:
|
||||||
braces: 3.0.3
|
braces: 3.0.3
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.2
|
||||||
|
|
||||||
/miller-rabin@4.0.1:
|
/miller-rabin@4.0.1:
|
||||||
resolution: {integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==}
|
resolution: {integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==}
|
||||||
@@ -10654,19 +10642,19 @@ packages:
|
|||||||
/minimatch@3.1.2:
|
/minimatch@3.1.2:
|
||||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 1.1.12
|
brace-expansion: 1.1.21
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/minimatch@3.1.5:
|
/minimatch@3.1.5:
|
||||||
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
|
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 1.1.12
|
brace-expansion: 1.1.21
|
||||||
|
|
||||||
/minimatch@9.0.9:
|
/minimatch@9.0.9:
|
||||||
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
|
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 2.0.2
|
brace-expansion: 2.1.7
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/minimist-options@4.1.0:
|
/minimist-options@4.1.0:
|
||||||
@@ -10735,11 +10723,12 @@ packages:
|
|||||||
/moment-timezone@0.5.46:
|
/moment-timezone@0.5.46:
|
||||||
resolution: {integrity: sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw==}
|
resolution: {integrity: sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
moment: 2.30.1
|
moment: 2.31.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/moment@2.30.1:
|
/moment@2.31.0:
|
||||||
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
|
resolution:
|
||||||
|
integrity: sha512-0acOTfMiWOheYS4eoWb80yYMb/JLvVv9SHbs2PehaDzfUG0Bw855SKyk0IKTnPGa5+U2bmi3W68l1+sGLX/pvw==
|
||||||
|
|
||||||
/monaco-editor@0.33.0:
|
/monaco-editor@0.33.0:
|
||||||
resolution: {integrity: sha512-VcRWPSLIUEgQJQIE0pVT8FcGBIgFoxz7jtqctE+IiCxWugD0DwgyQBcZBhdSrdMC84eumoqMZsGl2GTreOzwqw==}
|
resolution: {integrity: sha512-VcRWPSLIUEgQJQIE0pVT8FcGBIgFoxz7jtqctE+IiCxWugD0DwgyQBcZBhdSrdMC84eumoqMZsGl2GTreOzwqw==}
|
||||||
@@ -10870,7 +10859,7 @@ packages:
|
|||||||
semver: 7.7.4
|
semver: 7.7.4
|
||||||
tar: 7.5.20
|
tar: 7.5.20
|
||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
undici: 6.27.0
|
undici: 6.29.0
|
||||||
which: 6.0.1
|
which: 6.0.1
|
||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
@@ -12726,7 +12715,7 @@ packages:
|
|||||||
classnames: 2.5.1
|
classnames: 2.5.1
|
||||||
date-fns: 2.30.0
|
date-fns: 2.30.0
|
||||||
dayjs: 1.11.13
|
dayjs: 1.11.13
|
||||||
moment: 2.30.1
|
moment: 2.31.0
|
||||||
rc-trigger: 5.3.4(react-dom@18.3.1)(react@18.3.1)
|
rc-trigger: 5.3.4(react-dom@18.3.1)(react@18.3.1)
|
||||||
rc-util: 5.43.0(react-dom@18.3.1)(react@18.3.1)
|
rc-util: 5.43.0(react-dom@18.3.1)(react@18.3.1)
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
@@ -13953,7 +13942,7 @@ packages:
|
|||||||
dottie: 2.0.6
|
dottie: 2.0.6
|
||||||
inflection: 1.13.4
|
inflection: 1.13.4
|
||||||
lodash: 4.17.21
|
lodash: 4.17.21
|
||||||
moment: 2.30.1
|
moment: 2.31.0
|
||||||
moment-timezone: 0.5.46
|
moment-timezone: 0.5.46
|
||||||
pg-connection-string: 2.7.0
|
pg-connection-string: 2.7.0
|
||||||
retry-as-promised: 7.0.4
|
retry-as-promised: 7.0.4
|
||||||
@@ -15794,6 +15783,342 @@ packages:
|
|||||||
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
|
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/brace-expansion@2.1.7:
|
||||||
|
resolution: {integrity: sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==}
|
||||||
|
dependencies:
|
||||||
|
balanced-match: 1.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
|
||||||
|
/brace-expansion@1.1.21:
|
||||||
|
resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==}
|
||||||
|
dependencies:
|
||||||
|
balanced-match: 1.0.2
|
||||||
|
concat-map: 0.0.1
|
||||||
|
|
||||||
|
|
||||||
|
/axios@1.20.0:
|
||||||
|
resolution: {integrity: sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==}
|
||||||
|
dependencies:
|
||||||
|
follow-redirects: 1.16.1(debug@4.4.3)
|
||||||
|
form-data: 4.0.6
|
||||||
|
https-proxy-agent: 5.0.1
|
||||||
|
proxy-from-env: 2.1.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- debug
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
|
||||||
|
/proxy-from-env@2.1.0:
|
||||||
|
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
|
||||||
|
/form-data@4.0.6:
|
||||||
|
resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
dependencies:
|
||||||
|
asynckit: 0.4.0
|
||||||
|
combined-stream: 1.0.8
|
||||||
|
es-set-tostringtag: 2.1.0
|
||||||
|
hasown: 2.0.4
|
||||||
|
mime-types: 2.1.35
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
|
||||||
|
/hasown@2.0.4:
|
||||||
|
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
|
||||||
|
/es-set-tostringtag@2.1.0:
|
||||||
|
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
|
get-intrinsic: 1.3.0
|
||||||
|
has-tostringtag: 1.0.2
|
||||||
|
hasown: 2.0.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
|
||||||
|
/get-intrinsic@1.3.0:
|
||||||
|
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers: 1.0.2
|
||||||
|
es-define-property: 1.0.1
|
||||||
|
es-errors: 1.3.0
|
||||||
|
es-object-atoms: 1.1.2
|
||||||
|
function-bind: 1.1.2
|
||||||
|
get-proto: 1.0.1
|
||||||
|
gopd: 1.2.0
|
||||||
|
has-symbols: 1.1.0
|
||||||
|
hasown: 2.0.4
|
||||||
|
math-intrinsics: 1.1.0
|
||||||
|
|
||||||
|
|
||||||
|
/math-intrinsics@1.1.0:
|
||||||
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
|
||||||
|
/gopd@1.2.0:
|
||||||
|
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
|
||||||
|
/get-proto@1.0.1:
|
||||||
|
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
dunder-proto: 1.0.1
|
||||||
|
es-object-atoms: 1.1.2
|
||||||
|
|
||||||
|
|
||||||
|
/es-object-atoms@1.1.2:
|
||||||
|
resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
|
|
||||||
|
|
||||||
|
/dunder-proto@1.0.1:
|
||||||
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers: 1.0.2
|
||||||
|
es-errors: 1.3.0
|
||||||
|
gopd: 1.2.0
|
||||||
|
|
||||||
|
|
||||||
|
/call-bind-apply-helpers@1.0.2:
|
||||||
|
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
|
||||||
|
/es-define-property@1.0.1:
|
||||||
|
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
|
||||||
|
/follow-redirects@1.16.1(debug@4.4.3):
|
||||||
|
resolution: {integrity: sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
peerDependencies:
|
||||||
|
debug: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
debug:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.3
|
||||||
|
|
||||||
|
|
||||||
|
/undici@7.29.1:
|
||||||
|
resolution: {integrity: sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==}
|
||||||
|
engines: {node: '>=20.18.1'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
|
||||||
|
/undici@6.29.0:
|
||||||
|
resolution: {integrity: sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==}
|
||||||
|
engines: {node: '>=18.17'}
|
||||||
|
dev: false
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
|
||||||
|
/nodemailer@10.0.9:
|
||||||
|
resolution: {integrity: sha512-BF0qcyplCwp+jMk6HCjFykBz/YhhZSsxrARhOldLwFWH+8kGjQsd2WIMIZhqEuyXRoGFi0ONbDeWDMDoL8MhLw==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
|
||||||
|
/multer@2.4.0:
|
||||||
|
resolution: {integrity: sha512-7dqa0ZcFfzbefdTuIkzOSMvZWC0J7FLqBOjJUZvDCXShIURWKxAyTT1wHhnE5q19c7jOJf43IYYKjBmZVZmvhg==}
|
||||||
|
engines: {node: '>= 10.16.0'}
|
||||||
|
dependencies:
|
||||||
|
append-field: 1.0.0
|
||||||
|
busboy: 1.6.0
|
||||||
|
type-is: 1.6.18
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
|
||||||
|
/js-yaml@4.3.2:
|
||||||
|
resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
argparse: 2.0.1
|
||||||
|
|
||||||
|
|
||||||
|
/js-yaml@3.15.2:
|
||||||
|
resolution: {integrity: sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
argparse: 1.0.10
|
||||||
|
esprima: 4.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
|
||||||
|
/http-proxy-middleware@3.0.7:
|
||||||
|
resolution: {integrity: sha512-iwbQltVlx8bCrqePUM8C+hllHvdawVhQJaLrj1X7qllkvFQdXFsr16pW/mo9+JDVjN+QO2XUx9jd8SmoFkE5qw==}
|
||||||
|
engines: {node: ^14.18.0 || ^16.10.0 || >=18.0.0}
|
||||||
|
dependencies:
|
||||||
|
'@types/http-proxy': 1.17.17
|
||||||
|
debug: 4.4.3
|
||||||
|
http-proxy: 1.18.1(debug@4.4.3)
|
||||||
|
is-glob: 4.0.3
|
||||||
|
is-plain-object: 5.1.0
|
||||||
|
micromatch: 4.0.8
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
|
||||||
|
/picomatch@2.3.2:
|
||||||
|
resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
|
||||||
|
engines: {node: '>=8.6'}
|
||||||
|
|
||||||
|
|
||||||
|
/is-plain-object@5.1.0:
|
||||||
|
resolution: {integrity: sha512-bUi/yjmtKYcRVUtWRGr0UA6xEFh2I6zWUwMrUXB3s7bmYCaZ8a+0ZsTRkrawh/mzlSD1Y0Ph8bp/U+TvBpWDNw==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
|
||||||
|
/http-proxy@1.18.1(debug@4.4.3):
|
||||||
|
resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
dependencies:
|
||||||
|
eventemitter3: 4.0.7
|
||||||
|
follow-redirects: 1.16.1(debug@4.4.3)
|
||||||
|
requires-port: 1.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- debug
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
|
||||||
|
/@types/http-proxy@1.17.17:
|
||||||
|
resolution: {integrity: sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 17.0.45
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
|
||||||
|
/body-parser@1.20.6:
|
||||||
|
resolution: {integrity: sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==}
|
||||||
|
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||||
|
dependencies:
|
||||||
|
bytes: 3.1.2
|
||||||
|
content-type: 1.0.5
|
||||||
|
debug: 2.6.9
|
||||||
|
depd: 2.0.0
|
||||||
|
destroy: 1.2.0
|
||||||
|
http-errors: 2.0.1
|
||||||
|
iconv-lite: 0.4.24
|
||||||
|
on-finished: 2.4.1
|
||||||
|
qs: 6.15.3
|
||||||
|
raw-body: 2.5.3
|
||||||
|
type-is: 1.6.18
|
||||||
|
unpipe: 1.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
|
||||||
|
/raw-body@2.5.3:
|
||||||
|
resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
dependencies:
|
||||||
|
bytes: 3.1.2
|
||||||
|
http-errors: 2.0.1
|
||||||
|
iconv-lite: 0.4.24
|
||||||
|
unpipe: 1.0.0
|
||||||
|
|
||||||
|
|
||||||
|
/http-errors@2.0.1:
|
||||||
|
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
dependencies:
|
||||||
|
depd: 2.0.0
|
||||||
|
inherits: 2.0.4
|
||||||
|
setprototypeof: 1.2.0
|
||||||
|
statuses: 2.0.2
|
||||||
|
toidentifier: 1.0.1
|
||||||
|
|
||||||
|
|
||||||
|
/statuses@2.0.2:
|
||||||
|
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
|
||||||
|
/qs@6.15.3:
|
||||||
|
resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==}
|
||||||
|
engines: {node: '>=0.6'}
|
||||||
|
dependencies:
|
||||||
|
es-define-property: 1.0.1
|
||||||
|
side-channel: 1.1.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
|
||||||
|
/side-channel@1.1.1:
|
||||||
|
resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
|
object-inspect: 1.13.4
|
||||||
|
side-channel-list: 1.0.1
|
||||||
|
side-channel-map: 1.0.1
|
||||||
|
side-channel-weakmap: 1.0.2
|
||||||
|
|
||||||
|
|
||||||
|
/side-channel-weakmap@1.0.2:
|
||||||
|
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bound: 1.0.4
|
||||||
|
es-errors: 1.3.0
|
||||||
|
get-intrinsic: 1.3.0
|
||||||
|
object-inspect: 1.13.4
|
||||||
|
side-channel-map: 1.0.1
|
||||||
|
|
||||||
|
|
||||||
|
/side-channel-map@1.0.1:
|
||||||
|
resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bound: 1.0.4
|
||||||
|
es-errors: 1.3.0
|
||||||
|
get-intrinsic: 1.3.0
|
||||||
|
object-inspect: 1.13.4
|
||||||
|
|
||||||
|
|
||||||
|
/object-inspect@1.13.4:
|
||||||
|
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
|
||||||
|
/call-bound@1.0.4:
|
||||||
|
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers: 1.0.2
|
||||||
|
get-intrinsic: 1.3.0
|
||||||
|
|
||||||
|
|
||||||
|
/side-channel-list@1.0.1:
|
||||||
|
resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
|
object-inspect: 1.13.4
|
||||||
|
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
autoInstallPeers: true
|
autoInstallPeers: true
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|||||||
+308
-135
File diff suppressed because it is too large
Load Diff
+162
-27
@@ -10,6 +10,8 @@ import threading
|
|||||||
import time
|
import time
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import smtplib
|
import smtplib
|
||||||
|
import subprocess
|
||||||
|
from typing import Optional
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.header import Header
|
from email.header import Header
|
||||||
from email.utils import formataddr
|
from email.utils import formataddr
|
||||||
@@ -161,10 +163,11 @@ for k in push_config:
|
|||||||
push_config[k] = v
|
push_config[k] = v
|
||||||
|
|
||||||
|
|
||||||
def bark(title: str, content: str) -> None:
|
def bark(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
使用 bark 推送消息。
|
使用 bark 推送消息。
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
if not push_config.get("BARK_PUSH"):
|
if not push_config.get("BARK_PUSH"):
|
||||||
return
|
return
|
||||||
print("bark 服务启动")
|
print("bark 服务启动")
|
||||||
@@ -200,22 +203,28 @@ def bark(title: str, content: str) -> None:
|
|||||||
).json()
|
).json()
|
||||||
|
|
||||||
if response["code"] == 200:
|
if response["code"] == 200:
|
||||||
|
audit_results.append(True)
|
||||||
print("bark 推送成功!")
|
print("bark 推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print("bark 推送失败!")
|
print("bark 推送失败!")
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def console(title: str, content: str) -> None:
|
def console(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
使用 控制台 推送消息。
|
使用 控制台 推送消息。
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
print(f"{title}\n\n{content}")
|
print(f"{title}\n\n{content}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def dingding_bot(title: str, content: str) -> None:
|
def dingding_bot(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
使用 钉钉机器人 推送消息。
|
使用 钉钉机器人 推送消息。
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
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"):
|
||||||
return
|
return
|
||||||
print("钉钉机器人 服务启动")
|
print("钉钉机器人 服务启动")
|
||||||
@@ -236,15 +245,19 @@ def dingding_bot(title: str, content: str) -> None:
|
|||||||
).json()
|
).json()
|
||||||
|
|
||||||
if not response["errcode"]:
|
if not response["errcode"]:
|
||||||
|
audit_results.append(True)
|
||||||
print("钉钉机器人 推送成功!")
|
print("钉钉机器人 推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print("钉钉机器人 推送失败!")
|
print("钉钉机器人 推送失败!")
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def feishu_bot(title: str, content: str) -> None:
|
def feishu_bot(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
使用 飞书机器人 推送消息。
|
使用 飞书机器人 推送消息。
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
if not push_config.get("FSKEY"):
|
if not push_config.get("FSKEY"):
|
||||||
return
|
return
|
||||||
print("飞书 服务启动")
|
print("飞书 服务启动")
|
||||||
@@ -268,15 +281,19 @@ def feishu_bot(title: str, content: str) -> None:
|
|||||||
response = requests.post(url, data=json.dumps(data)).json()
|
response = requests.post(url, data=json.dumps(data)).json()
|
||||||
|
|
||||||
if response.get("StatusCode") == 0 or response.get("code") == 0:
|
if response.get("StatusCode") == 0 or response.get("code") == 0:
|
||||||
|
audit_results.append(True)
|
||||||
print("飞书 推送成功!")
|
print("飞书 推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print("飞书 推送失败!错误信息如下:\n", response)
|
print("飞书 推送失败!错误信息如下:\n", response)
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def go_cqhttp(title: str, content: str) -> None:
|
def go_cqhttp(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
使用 go_cqhttp 推送消息。
|
使用 go_cqhttp 推送消息。
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
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"):
|
||||||
return
|
return
|
||||||
print("go-cqhttp 服务启动")
|
print("go-cqhttp 服务启动")
|
||||||
@@ -285,15 +302,19 @@ def go_cqhttp(title: str, content: str) -> None:
|
|||||||
response = requests.get(url).json()
|
response = requests.get(url).json()
|
||||||
|
|
||||||
if response["status"] == "ok":
|
if response["status"] == "ok":
|
||||||
|
audit_results.append(True)
|
||||||
print("go-cqhttp 推送成功!")
|
print("go-cqhttp 推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print("go-cqhttp 推送失败!")
|
print("go-cqhttp 推送失败!")
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def gotify(title: str, content: str) -> None:
|
def gotify(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
使用 gotify 推送消息。
|
使用 gotify 推送消息。
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
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"):
|
||||||
return
|
return
|
||||||
print("gotify 服务启动")
|
print("gotify 服务启动")
|
||||||
@@ -307,15 +328,19 @@ def gotify(title: str, content: str) -> None:
|
|||||||
response = requests.post(url, data=data).json()
|
response = requests.post(url, data=data).json()
|
||||||
|
|
||||||
if response.get("id"):
|
if response.get("id"):
|
||||||
|
audit_results.append(True)
|
||||||
print("gotify 推送成功!")
|
print("gotify 推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print("gotify 推送失败!")
|
print("gotify 推送失败!")
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def iGot(title: str, content: str) -> None:
|
def iGot(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
使用 iGot 推送消息。
|
使用 iGot 推送消息。
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
if not push_config.get("IGOT_PUSH_KEY"):
|
if not push_config.get("IGOT_PUSH_KEY"):
|
||||||
return
|
return
|
||||||
print("iGot 服务启动")
|
print("iGot 服务启动")
|
||||||
@@ -326,15 +351,19 @@ def iGot(title: str, content: str) -> None:
|
|||||||
response = requests.post(url, data=data, headers=headers).json()
|
response = requests.post(url, data=data, headers=headers).json()
|
||||||
|
|
||||||
if response["ret"] == 0:
|
if response["ret"] == 0:
|
||||||
|
audit_results.append(True)
|
||||||
print("iGot 推送成功!")
|
print("iGot 推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print(f'iGot 推送失败!{response["errMsg"]}')
|
print(f'iGot 推送失败!{response["errMsg"]}')
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def serverJ(title: str, content: str) -> None:
|
def serverJ(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
通过 serverJ 推送消息。
|
通过 serverJ 推送消息。
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
if not push_config.get("PUSH_KEY"):
|
if not push_config.get("PUSH_KEY"):
|
||||||
return
|
return
|
||||||
print("serverJ 服务启动")
|
print("serverJ 服务启动")
|
||||||
@@ -351,15 +380,19 @@ def serverJ(title: str, content: str) -> None:
|
|||||||
response = requests.post(url, data=data).json()
|
response = requests.post(url, data=data).json()
|
||||||
|
|
||||||
if response.get("errno") == 0 or response.get("code") == 0:
|
if response.get("errno") == 0 or response.get("code") == 0:
|
||||||
|
audit_results.append(True)
|
||||||
print("serverJ 推送成功!")
|
print("serverJ 推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print(f'serverJ 推送失败!错误码:{response["message"]}')
|
print(f'serverJ 推送失败!错误码:{response["message"]}')
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def pushdeer(title: str, content: str) -> None:
|
def pushdeer(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
通过PushDeer 推送消息
|
通过PushDeer 推送消息
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
if not push_config.get("DEER_KEY"):
|
if not push_config.get("DEER_KEY"):
|
||||||
return
|
return
|
||||||
print("PushDeer 服务启动")
|
print("PushDeer 服务启动")
|
||||||
@@ -376,15 +409,19 @@ def pushdeer(title: str, content: str) -> None:
|
|||||||
response = requests.post(url, data=data).json()
|
response = requests.post(url, data=data).json()
|
||||||
|
|
||||||
if len(response.get("content").get("result")) > 0:
|
if len(response.get("content").get("result")) > 0:
|
||||||
|
audit_results.append(True)
|
||||||
print("PushDeer 推送成功!")
|
print("PushDeer 推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print("PushDeer 推送失败!错误信息:", response)
|
print("PushDeer 推送失败!错误信息:", response)
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def chat(title: str, content: str) -> None:
|
def chat(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
通过Chat 推送消息
|
通过Chat 推送消息
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
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"):
|
||||||
return
|
return
|
||||||
print("chat 服务启动")
|
print("chat 服务启动")
|
||||||
@@ -393,15 +430,19 @@ def chat(title: str, content: str) -> None:
|
|||||||
response = requests.post(url, data=data)
|
response = requests.post(url, data=data)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
|
audit_results.append(True)
|
||||||
print("Chat 推送成功!")
|
print("Chat 推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print("Chat 推送失败!错误信息:", response)
|
print("Chat 推送失败!错误信息:", response)
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def pushplus_bot(title: str, content: str) -> None:
|
def pushplus_bot(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
通过 pushplus 推送消息。
|
通过 pushplus 推送消息。
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
if not push_config.get("PUSH_PLUS_TOKEN"):
|
if not push_config.get("PUSH_PLUS_TOKEN"):
|
||||||
return
|
return
|
||||||
print("PUSHPLUS 服务启动")
|
print("PUSHPLUS 服务启动")
|
||||||
@@ -424,7 +465,9 @@ def pushplus_bot(title: str, content: str) -> None:
|
|||||||
|
|
||||||
code = response["code"]
|
code = response["code"]
|
||||||
if code == 200:
|
if code == 200:
|
||||||
|
audit_results.append(True)
|
||||||
print("PUSHPLUS 推送请求成功,可根据流水号查询推送结果:" + response["data"])
|
print("PUSHPLUS 推送请求成功,可根据流水号查询推送结果:" + response["data"])
|
||||||
|
audit_results.append(True)
|
||||||
print(
|
print(
|
||||||
"注意:请求成功并不代表推送成功,如未收到消息,请到pushplus官网使用流水号查询推送最终结果"
|
"注意:请求成功并不代表推送成功,如未收到消息,请到pushplus官网使用流水号查询推送最终结果"
|
||||||
)
|
)
|
||||||
@@ -437,16 +480,20 @@ def pushplus_bot(title: str, content: str) -> None:
|
|||||||
response = requests.post(url=url_old, data=body, headers=headers).json()
|
response = requests.post(url=url_old, data=body, headers=headers).json()
|
||||||
|
|
||||||
if response["code"] == 200:
|
if response["code"] == 200:
|
||||||
|
audit_results.append(True)
|
||||||
print("PUSHPLUS(hxtrip) 推送成功!")
|
print("PUSHPLUS(hxtrip) 推送成功!")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print("PUSHPLUS 推送失败!")
|
print("PUSHPLUS 推送失败!")
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def weplus_bot(title: str, content: str) -> None:
|
def weplus_bot(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
通过 微加机器人 推送消息。
|
通过 微加机器人 推送消息。
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
if not push_config.get("WE_PLUS_BOT_TOKEN"):
|
if not push_config.get("WE_PLUS_BOT_TOKEN"):
|
||||||
return
|
return
|
||||||
print("微加机器人 服务启动")
|
print("微加机器人 服务启动")
|
||||||
@@ -469,15 +516,19 @@ def weplus_bot(title: str, content: str) -> None:
|
|||||||
response = requests.post(url=url, data=body, headers=headers).json()
|
response = requests.post(url=url, data=body, headers=headers).json()
|
||||||
|
|
||||||
if response["code"] == 200:
|
if response["code"] == 200:
|
||||||
|
audit_results.append(True)
|
||||||
print("微加机器人 推送成功!")
|
print("微加机器人 推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print("微加机器人 推送失败!")
|
print("微加机器人 推送失败!")
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def qmsg_bot(title: str, content: str) -> None:
|
def qmsg_bot(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
使用 qmsg 推送消息。
|
使用 qmsg 推送消息。
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
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"):
|
||||||
return
|
return
|
||||||
print("qmsg 服务启动")
|
print("qmsg 服务启动")
|
||||||
@@ -487,15 +538,19 @@ def qmsg_bot(title: str, content: str) -> None:
|
|||||||
response = requests.post(url=url, params=payload).json()
|
response = requests.post(url=url, params=payload).json()
|
||||||
|
|
||||||
if response["code"] == 0:
|
if response["code"] == 0:
|
||||||
|
audit_results.append(True)
|
||||||
print("qmsg 推送成功!")
|
print("qmsg 推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print(f'qmsg 推送失败!{response["reason"]}')
|
print(f'qmsg 推送失败!{response["reason"]}')
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def wecom_app(title: str, content: str) -> None:
|
def wecom_app(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
通过 企业微信 APP 推送消息。
|
通过 企业微信 APP 推送消息。
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
if not push_config.get("QYWX_AM"):
|
if not push_config.get("QYWX_AM"):
|
||||||
return
|
return
|
||||||
QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM"))
|
QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM"))
|
||||||
@@ -521,9 +576,12 @@ def wecom_app(title: str, content: str) -> None:
|
|||||||
response = wx.send_mpnews(title, content, media_id, touser)
|
response = wx.send_mpnews(title, content, media_id, touser)
|
||||||
|
|
||||||
if response == "ok":
|
if response == "ok":
|
||||||
|
audit_results.append(True)
|
||||||
print("企业微信推送成功!")
|
print("企业微信推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print("企业微信推送失败!错误信息如下:\n", response)
|
print("企业微信推送失败!错误信息如下:\n", response)
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
class WeCom:
|
class WeCom:
|
||||||
@@ -588,10 +646,11 @@ class WeCom:
|
|||||||
return respone["errmsg"]
|
return respone["errmsg"]
|
||||||
|
|
||||||
|
|
||||||
def wecom_bot(title: str, content: str) -> None:
|
def wecom_bot(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
通过 企业微信机器人 推送消息。
|
通过 企业微信机器人 推送消息。
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
if not push_config.get("QYWX_KEY"):
|
if not push_config.get("QYWX_KEY"):
|
||||||
return
|
return
|
||||||
print("企业微信机器人服务启动")
|
print("企业微信机器人服务启动")
|
||||||
@@ -608,15 +667,19 @@ def wecom_bot(title: str, content: str) -> None:
|
|||||||
).json()
|
).json()
|
||||||
|
|
||||||
if response["errcode"] == 0:
|
if response["errcode"] == 0:
|
||||||
|
audit_results.append(True)
|
||||||
print("企业微信机器人推送成功!")
|
print("企业微信机器人推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print("企业微信机器人推送失败!")
|
print("企业微信机器人推送失败!")
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def telegram_bot(title: str, content: str) -> None:
|
def telegram_bot(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
使用 telegram 机器人 推送消息。
|
使用 telegram 机器人 推送消息。
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
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"):
|
||||||
return
|
return
|
||||||
print("tg 服务启动")
|
print("tg 服务启动")
|
||||||
@@ -652,15 +715,19 @@ def telegram_bot(title: str, content: str) -> None:
|
|||||||
).json()
|
).json()
|
||||||
|
|
||||||
if response["ok"]:
|
if response["ok"]:
|
||||||
|
audit_results.append(True)
|
||||||
print("tg 推送成功!")
|
print("tg 推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print("tg 推送失败!")
|
print("tg 推送失败!")
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def aibotk(title: str, content: str) -> None:
|
def aibotk(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
使用 智能微秘书 推送消息。
|
使用 智能微秘书 推送消息。
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
if (
|
if (
|
||||||
not push_config.get("AIBOTK_KEY")
|
not push_config.get("AIBOTK_KEY")
|
||||||
or not push_config.get("AIBOTK_TYPE")
|
or not push_config.get("AIBOTK_TYPE")
|
||||||
@@ -688,15 +755,19 @@ def aibotk(title: str, content: str) -> None:
|
|||||||
response = requests.post(url=url, data=body, headers=headers).json()
|
response = requests.post(url=url, data=body, headers=headers).json()
|
||||||
print(response)
|
print(response)
|
||||||
if response["code"] == 0:
|
if response["code"] == 0:
|
||||||
|
audit_results.append(True)
|
||||||
print("智能微秘书 推送成功!")
|
print("智能微秘书 推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print(f'智能微秘书 推送失败!{response["error"]}')
|
print(f'智能微秘书 推送失败!{response["error"]}')
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def smtp(title: str, content: str) -> None:
|
def smtp(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
使用 SMTP 邮件 推送消息。
|
使用 SMTP 邮件 推送消息。
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
if (
|
if (
|
||||||
not push_config.get("SMTP_SERVER")
|
not push_config.get("SMTP_SERVER")
|
||||||
or not push_config.get("SMTP_SSL")
|
or not push_config.get("SMTP_SSL")
|
||||||
@@ -736,15 +807,19 @@ def smtp(title: str, content: str) -> None:
|
|||||||
message.as_bytes(),
|
message.as_bytes(),
|
||||||
)
|
)
|
||||||
smtp_server.close()
|
smtp_server.close()
|
||||||
|
audit_results.append(True)
|
||||||
print("SMTP 邮件 推送成功!")
|
print("SMTP 邮件 推送成功!")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
audit_results.append(False)
|
||||||
print(f"SMTP 邮件 推送失败!{e}")
|
print(f"SMTP 邮件 推送失败!{e}")
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def pushme(title: str, content: str) -> None:
|
def pushme(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
使用 PushMe 推送消息。
|
使用 PushMe 推送消息。
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
if not push_config.get("PUSHME_KEY"):
|
if not push_config.get("PUSHME_KEY"):
|
||||||
return
|
return
|
||||||
print("PushMe 服务启动")
|
print("PushMe 服务启动")
|
||||||
@@ -764,15 +839,19 @@ def pushme(title: str, content: str) -> None:
|
|||||||
response = requests.post(url, data=data)
|
response = requests.post(url, data=data)
|
||||||
|
|
||||||
if response.status_code == 200 and response.text == "success":
|
if response.status_code == 200 and response.text == "success":
|
||||||
|
audit_results.append(True)
|
||||||
print("PushMe 推送成功!")
|
print("PushMe 推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print(f"PushMe 推送失败!{response.status_code} {response.text}")
|
print(f"PushMe 推送失败!{response.status_code} {response.text}")
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def chronocat(title: str, content: str) -> None:
|
def chronocat(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
使用 CHRONOCAT 推送消息。
|
使用 CHRONOCAT 推送消息。
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
if (
|
if (
|
||||||
not push_config.get("CHRONOCAT_URL")
|
not push_config.get("CHRONOCAT_URL")
|
||||||
or not push_config.get("CHRONOCAT_QQ")
|
or not push_config.get("CHRONOCAT_QQ")
|
||||||
@@ -807,21 +886,27 @@ def chronocat(title: str, content: str) -> None:
|
|||||||
response = requests.post(url, headers=headers, data=json.dumps(data))
|
response = requests.post(url, headers=headers, data=json.dumps(data))
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
if chat_type == 1:
|
if chat_type == 1:
|
||||||
|
audit_results.append(True)
|
||||||
print(f"QQ个人消息:{ids}推送成功!")
|
print(f"QQ个人消息:{ids}推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(True)
|
||||||
print(f"QQ群消息:{ids}推送成功!")
|
print(f"QQ群消息:{ids}推送成功!")
|
||||||
else:
|
else:
|
||||||
if chat_type == 1:
|
if chat_type == 1:
|
||||||
|
audit_results.append(False)
|
||||||
print(f"QQ个人消息:{ids}推送失败!")
|
print(f"QQ个人消息:{ids}推送失败!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print(f"QQ群消息:{ids}推送失败!")
|
print(f"QQ群消息:{ids}推送失败!")
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def ntfy(title: str, content: str) -> None:
|
def ntfy(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
通过 Ntfy 推送消息
|
通过 Ntfy 推送消息
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
audit_results = []
|
||||||
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"))
|
||||||
@@ -853,12 +938,15 @@ def ntfy(title: str, content: str) -> None:
|
|||||||
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)
|
||||||
if response.status_code == 200: # 使用 response.status_code 进行检查
|
if response.status_code == 200: # 使用 response.status_code 进行检查
|
||||||
|
audit_results.append(True)
|
||||||
print("Ntfy 推送成功!")
|
print("Ntfy 推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print("Ntfy 推送失败!错误信息:", response.text)
|
print("Ntfy 推送失败!错误信息:", response.text)
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def wxpusher_bot(title: str, content: str) -> None:
|
def wxpusher_bot(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
通过 wxpusher 推送消息。
|
通过 wxpusher 推送消息。
|
||||||
支持的环境变量:
|
支持的环境变量:
|
||||||
@@ -866,6 +954,7 @@ def wxpusher_bot(title: str, content: str) -> None:
|
|||||||
- WXPUSHER_TOPIC_IDS: 主题ID, 多个用英文分号;分隔
|
- WXPUSHER_TOPIC_IDS: 主题ID, 多个用英文分号;分隔
|
||||||
- WXPUSHER_UIDS: 用户ID, 多个用英文分号;分隔
|
- WXPUSHER_UIDS: 用户ID, 多个用英文分号;分隔
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
if not push_config.get("WXPUSHER_APP_TOKEN"):
|
if not push_config.get("WXPUSHER_APP_TOKEN"):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -909,17 +998,21 @@ def wxpusher_bot(title: str, content: str) -> None:
|
|||||||
response = requests.post(url=url, json=data, headers=headers).json()
|
response = requests.post(url=url, json=data, headers=headers).json()
|
||||||
|
|
||||||
if response.get("code") == 1000:
|
if response.get("code") == 1000:
|
||||||
|
audit_results.append(True)
|
||||||
print("wxpusher 推送成功!")
|
print("wxpusher 推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print(f"wxpusher 推送失败!错误信息:{response.get('msg')}")
|
print(f"wxpusher 推送失败!错误信息:{response.get('msg')}")
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def wxpusher_spt(title: str, content: str) -> None:
|
def wxpusher_spt(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
通过 wxpusher 极简推送(SPT)推送消息。
|
通过 wxpusher 极简推送(SPT)推送消息。
|
||||||
支持的环境变量:
|
支持的环境变量:
|
||||||
- WXPUSHER_SPT_LIST: SPT, 多个用英文逗号,分隔, 最多10个
|
- WXPUSHER_SPT_LIST: SPT, 多个用英文逗号,分隔, 最多10个
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
if not push_config.get("WXPUSHER_SPT_LIST"):
|
if not push_config.get("WXPUSHER_SPT_LIST"):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -956,17 +1049,21 @@ def wxpusher_spt(title: str, content: str) -> None:
|
|||||||
response = requests.post(url=url, json=data, headers=headers).json()
|
response = requests.post(url=url, json=data, headers=headers).json()
|
||||||
|
|
||||||
if response.get("code") == 1000:
|
if response.get("code") == 1000:
|
||||||
|
audit_results.append(True)
|
||||||
print("wxpusher SPT 推送成功!")
|
print("wxpusher SPT 推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print(f"wxpusher SPT 推送失败!错误信息:{response.get('msg')}")
|
print(f"wxpusher SPT 推送失败!错误信息:{response.get('msg')}")
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def wpush(title: str, content: str) -> None:
|
def wpush(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
通过 WPUSH 推送消息。
|
通过 WPUSH 推送消息。
|
||||||
官方文档: https://wpush.cn/docs
|
官方文档: https://wpush.cn/docs
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
if not push_config.get("WPUSH_APIKEY"):
|
if not push_config.get("WPUSH_APIKEY"):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -986,12 +1083,15 @@ def wpush(title: str, content: str) -> None:
|
|||||||
response = requests.post(url=url, json=data, headers=headers, timeout=15).json()
|
response = requests.post(url=url, json=data, headers=headers, timeout=15).json()
|
||||||
|
|
||||||
if response.get("code") == 0:
|
if response.get("code") == 0:
|
||||||
|
audit_results.append(True)
|
||||||
print("WPUSH 推送成功!")
|
print("WPUSH 推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print(f'WPUSH 推送失败!错误信息:{response.get("message") or response}')
|
print(f'WPUSH 推送失败!错误信息:{response.get("message") or response}')
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def openilink(title: str, content: str) -> None:
|
def openilink(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
通过 OpeniLink 推送消息。
|
通过 OpeniLink 推送消息。
|
||||||
支持的环境变量:
|
支持的环境变量:
|
||||||
@@ -999,6 +1099,7 @@ def openilink(title: str, content: str) -> None:
|
|||||||
- OPENILINK_HUB_URL: OpeniLink Hub 地址,默认为 https://hub.openilink.com
|
- OPENILINK_HUB_URL: OpeniLink Hub 地址,默认为 https://hub.openilink.com
|
||||||
- OPENILINK_CONTEXT_TOKEN: 消息会话上下文 token,可从消息事件中获取
|
- OPENILINK_CONTEXT_TOKEN: 消息会话上下文 token,可从消息事件中获取
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
if not push_config.get("OPENILINK_APP_TOKEN"):
|
if not push_config.get("OPENILINK_APP_TOKEN"):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1023,9 +1124,12 @@ def openilink(title: str, content: str) -> None:
|
|||||||
response = requests.post(url=url, json=data, headers=headers).json()
|
response = requests.post(url=url, json=data, headers=headers).json()
|
||||||
|
|
||||||
if response.get("ok"):
|
if response.get("ok"):
|
||||||
|
audit_results.append(True)
|
||||||
print("OpeniLink 推送成功!")
|
print("OpeniLink 推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print(f'OpeniLink 推送失败!错误信息:{response.get("error")}')
|
print(f'OpeniLink 推送失败!错误信息:{response.get("error")}')
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def parse_headers(headers):
|
def parse_headers(headers):
|
||||||
@@ -1079,10 +1183,11 @@ def parse_body(body, content_type, value_format_fn=None):
|
|||||||
return parsed
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
def custom_notify(title: str, content: str) -> None:
|
def custom_notify(title: str, content: str) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
通过 自定义通知 推送消息。
|
通过 自定义通知 推送消息。
|
||||||
"""
|
"""
|
||||||
|
audit_results = []
|
||||||
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"):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1114,9 +1219,12 @@ def custom_notify(title: str, content: str) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
|
audit_results.append(True)
|
||||||
print("自定义通知推送成功!")
|
print("自定义通知推送成功!")
|
||||||
else:
|
else:
|
||||||
|
audit_results.append(False)
|
||||||
print(f"自定义通知推送失败!{response.status_code} {response.text}")
|
print(f"自定义通知推送失败!{response.status_code} {response.text}")
|
||||||
|
return all(audit_results) if audit_results else None
|
||||||
|
|
||||||
|
|
||||||
def one() -> str:
|
def one() -> str:
|
||||||
@@ -1204,6 +1312,33 @@ def add_notify_function():
|
|||||||
return notify_function
|
return notify_function
|
||||||
|
|
||||||
|
|
||||||
|
def report_notification(channel, title, content, success):
|
||||||
|
ql_dir = os.getenv("QL_DIR")
|
||||||
|
if not ql_dir:
|
||||||
|
return
|
||||||
|
payload = {"title": str(title or "")[:512], "content": str(content or "")[:8192],
|
||||||
|
"success": success, "notifyType": channel,
|
||||||
|
"scriptName": os.path.basename(__import__("sys").argv[0]), "taskId": os.getenv("ID", "")}
|
||||||
|
code = """const fs=require('node:fs'),path=require('node:path');
|
||||||
|
const report=require(path.join(process.env.QL_DIR,'shell/preload/notificationAudit.js'));
|
||||||
|
report(JSON.parse(fs.readFileSync(0,'utf8'))).catch(()=>{});"""
|
||||||
|
try:
|
||||||
|
subprocess.run(["node", "-e", code], input=json.dumps(payload), text=True,
|
||||||
|
capture_output=True, timeout=3)
|
||||||
|
except Exception:
|
||||||
|
pass # Audit availability never changes notification delivery.
|
||||||
|
|
||||||
|
|
||||||
|
def audited_notification(mode, title, content):
|
||||||
|
result = None
|
||||||
|
try:
|
||||||
|
result = mode(title, content)
|
||||||
|
except Exception:
|
||||||
|
result = False
|
||||||
|
print(f"{mode.__name__} 推送失败")
|
||||||
|
report_notification(mode.__name__, title, content, result is True)
|
||||||
|
|
||||||
|
|
||||||
def send(title: str, content: str, ignore_default_config: bool = False, **kwargs):
|
def send(title: str, content: str, ignore_default_config: bool = False, **kwargs):
|
||||||
if kwargs:
|
if kwargs:
|
||||||
global push_config
|
global push_config
|
||||||
@@ -1228,7 +1363,7 @@ def send(title: str, content: str, ignore_default_config: bool = False, **kwargs
|
|||||||
|
|
||||||
notify_function = add_notify_function()
|
notify_function = add_notify_function()
|
||||||
ts = [
|
ts = [
|
||||||
threading.Thread(target=mode, args=(title, content), name=mode.__name__)
|
threading.Thread(target=audited_notification, args=(mode, title, content), name=mode.__name__)
|
||||||
for mode in notify_function
|
for mode in notify_function
|
||||||
]
|
]
|
||||||
[t.start() for t in ts]
|
[t.start() for t in ts]
|
||||||
|
|||||||
+14
-3
@@ -85,9 +85,13 @@ add_cron_api() {
|
|||||||
sub_id="null"
|
sub_id="null"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
local payload
|
||||||
|
payload=$(jq -cn --arg name "$name" --arg command "$command" \
|
||||||
|
--arg schedule "$schedule" --argjson sub_id "$sub_id" \
|
||||||
|
'{name: $name, command: $command, schedule: $schedule, sub_id: $sub_id}') || return $?
|
||||||
local api=$(ql_api_request \
|
local api=$(ql_api_request \
|
||||||
"http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \
|
"http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \
|
||||||
--data-raw "{\"name\":\"${name//\"/\\\"}\",\"command\":\"${command//\"/\\\"}\",\"schedule\":\"$schedule\",\"sub_id\":$sub_id}" \
|
--data-raw "$payload" \
|
||||||
-X POST)
|
-X POST)
|
||||||
code=$(echo "$api" | jq -r .code)
|
code=$(echo "$api" | jq -r .code)
|
||||||
message=$(echo "$api" | jq -r .message)
|
message=$(echo "$api" | jq -r .message)
|
||||||
@@ -112,10 +116,14 @@ update_cron_api() {
|
|||||||
local id="$4"
|
local id="$4"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
local payload
|
||||||
|
payload=$(jq -cn --arg name "$name" --arg command "$command" \
|
||||||
|
--arg schedule "$schedule" --arg id "$id" \
|
||||||
|
'{name: $name, command: $command, schedule: $schedule, id: $id}') || return $?
|
||||||
local api=$(ql_api_request \
|
local api=$(ql_api_request \
|
||||||
"http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \
|
"http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \
|
||||||
-X PUT \
|
-X PUT \
|
||||||
--data-raw "{\"name\":\"${name//\"/\\\"}\",\"command\":\"${command//\"/\\\"}\",\"schedule\":\"$schedule\",\"id\":\"$id\"}")
|
--data-raw "$payload")
|
||||||
code=$(echo "$api" | jq -r .code)
|
code=$(echo "$api" | jq -r .code)
|
||||||
message=$(echo "$api" | jq -r .message)
|
message=$(echo "$api" | jq -r .message)
|
||||||
if [[ $code == 200 ]]; then
|
if [[ $code == 200 ]]; then
|
||||||
@@ -135,10 +143,13 @@ update_cron_command_api() {
|
|||||||
local id="$2"
|
local id="$2"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
local payload
|
||||||
|
payload=$(jq -cn --arg command "$command" --arg id "$id" \
|
||||||
|
'{command: $command, id: $id}') || return $?
|
||||||
local api=$(ql_api_request \
|
local api=$(ql_api_request \
|
||||||
"http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \
|
"http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \
|
||||||
-X PUT \
|
-X PUT \
|
||||||
--data-raw "{\"command\":\"${command//\"/\\\"}\",\"id\":\"$id\"}")
|
--data-raw "$payload")
|
||||||
code=$(echo "$api" | jq -r .code)
|
code=$(echo "$api" | jq -r .code)
|
||||||
message=$(echo "$api" | jq -r .message)
|
message=$(echo "$api" | jq -r .message)
|
||||||
if [[ $code == 200 ]]; then
|
if [[ $code == 200 ]]; then
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class GrpcClient {
|
|||||||
'updateEnvNames',
|
'updateEnvNames',
|
||||||
'getEnvById',
|
'getEnvById',
|
||||||
'systemNotify',
|
'systemNotify',
|
||||||
|
'recordNotifyLog',
|
||||||
'getCronDetail',
|
'getCronDetail',
|
||||||
'createCron',
|
'createCron',
|
||||||
'updateCron',
|
'updateCron',
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
const path = require('node:path');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
// A reporting failure must never terminate a script or change its send result.
|
||||||
|
module.exports = async function reportNotificationAudit(payload) {
|
||||||
|
let client;
|
||||||
|
try {
|
||||||
|
if (!process.env.QL_DIR) return;
|
||||||
|
const grpc = require('@grpc/grpc-js');
|
||||||
|
const loader = require('@grpc/proto-loader');
|
||||||
|
const certDir = path.join(
|
||||||
|
process.env.QL_DATA_DIR || path.join(process.env.QL_DIR, 'data'),
|
||||||
|
'config/grpc',
|
||||||
|
);
|
||||||
|
const credentials = grpc.credentials.createSsl(
|
||||||
|
fs.readFileSync(path.join(certDir, 'ca.crt')),
|
||||||
|
fs.readFileSync(path.join(certDir, 'client.key')),
|
||||||
|
fs.readFileSync(path.join(certDir, 'client.crt')),
|
||||||
|
);
|
||||||
|
const definition = loader.loadSync(
|
||||||
|
path.join(process.env.QL_DIR, 'back/protos/api.proto'),
|
||||||
|
{ keepCase: true, defaults: true },
|
||||||
|
);
|
||||||
|
const Api = grpc.loadPackageDefinition(definition).com.ql.api.Api;
|
||||||
|
client = new Api(
|
||||||
|
`localhost:${process.env.GRPC_PORT || '5500'}`,
|
||||||
|
credentials,
|
||||||
|
{ 'grpc.enable_http_proxy': 0 },
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
client.RecordNotifyLog(
|
||||||
|
payload,
|
||||||
|
new grpc.Metadata(),
|
||||||
|
{ deadline: new Date(Date.now() + 1500) },
|
||||||
|
() => resolve(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
// Missing certificates, old panels and offline services are best effort.
|
||||||
|
} finally {
|
||||||
|
if (client) client.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
: "${QL_DIR:?Please set QL_DIR}"
|
||||||
|
: "${QL_DATA_DIR:?Please set QL_DATA_DIR}"
|
||||||
|
[[ "$QL_DIR" = /* && "$QL_DATA_DIR" = /*/data ]] || { echo "QL_DIR and QL_DATA_DIR must be absolute paths (data directory ends in /data)" >&2; exit 1; }
|
||||||
|
command="$1"
|
||||||
|
|
||||||
|
export PNPM_HOME="${PNPM_HOME:-${QL_DATA_DIR}/dep_cache/node}"
|
||||||
|
if [[ -n "${PYTHON_VENV_DIR:-}" ]]; then
|
||||||
|
[[ "$PYTHON_VENV_DIR" = /* ]] || PYTHON_VENV_DIR="${QL_DIR}/${PYTHON_VENV_DIR}"
|
||||||
|
export PYTHON_VENV_DIR
|
||||||
|
if [[ ! -x "$PYTHON_VENV_DIR/bin/python3" ]]; then
|
||||||
|
python3 -m venv "$PYTHON_VENV_DIR"
|
||||||
|
fi
|
||||||
|
[[ -x "$PYTHON_VENV_DIR/bin/python3" ]] || { echo "Invalid Python venv" >&2; exit 1; }
|
||||||
|
unset PYTHON_HOME PYTHONUSERBASE PYTHONPATH
|
||||||
|
export PATH="${PYTHON_VENV_DIR}/bin:${PATH}:${PNPM_HOME}"
|
||||||
|
export PIP_CACHE_DIR="${PYTHON_VENV_DIR}/pip"
|
||||||
|
else
|
||||||
|
export PYTHON_SHORT_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
|
||||||
|
export PYTHON_HOME="${PYTHON_HOME:-${QL_DATA_DIR}/dep_cache/python3}"
|
||||||
|
export PYTHONUSERBASE="$PYTHON_HOME"
|
||||||
|
export PATH="${PATH}:${PNPM_HOME}:${PYTHON_HOME}/bin"
|
||||||
|
export PIP_CACHE_DIR="${PYTHON_HOME}/pip"
|
||||||
|
export PYTHONPATH="${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages"
|
||||||
|
fi
|
||||||
|
export NODE_PATH="${NODE_PATH:-/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules}"
|
||||||
|
mkdir -p "$PIP_CACHE_DIR"
|
||||||
|
|
||||||
|
cd "${QL_DIR}"
|
||||||
|
if [[ ! -e .env ]]; then
|
||||||
|
# Preserve user configuration, including a file created concurrently.
|
||||||
|
(umask 077; set -C; cat .env.example > .env)
|
||||||
|
fi
|
||||||
|
chmod u+x "${QL_DIR}"/shell/*.sh
|
||||||
|
|
||||||
|
. "${QL_DIR}/shell/share.sh"
|
||||||
|
. "${QL_DIR}/shell/env.sh"
|
||||||
|
|
||||||
|
log_with_style() {
|
||||||
|
local level="$1"
|
||||||
|
local message="$2"
|
||||||
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
printf "\n[%s] [%7s] %s\n" "${timestamp}" "${level}" "${message}"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_with_style "INFO" "🚀 1. 检测配置文件..."
|
||||||
|
import_config "$@"
|
||||||
|
make_dir /etc/nginx/conf.d
|
||||||
|
make_dir /run/nginx
|
||||||
|
fix_config
|
||||||
|
|
||||||
|
pm2 l &>/dev/null
|
||||||
|
|
||||||
|
log_with_style "INFO" "🔄 2. 启动 nginx..."
|
||||||
|
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
log_with_style "INFO" "⚙️ 3. 启动 pm2 服务..."
|
||||||
|
reload_pm2
|
||||||
|
|
||||||
|
if [[ $command != "reload" ]]; then
|
||||||
|
if [[ $AutoStartBot == true ]]; then
|
||||||
|
log_with_style "INFO" "🤖 4. 启动 bot..."
|
||||||
|
nohup ql bot >$dir_log/bot.log 2>&1 &
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $EnableExtraShell == true ]]; then
|
||||||
|
log_with_style "INFO" "🛠️ 5. 执行自定义脚本..."
|
||||||
|
nohup ql extra >$dir_log/extra.log 2>&1 &
|
||||||
|
fi
|
||||||
|
|
||||||
|
pm2 startup
|
||||||
|
pm2 save
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_with_style "SUCCESS" "🎉 启动成功!"
|
||||||
+8
-60
@@ -77,65 +77,13 @@ if [[ $command != "reload" ]]; then
|
|||||||
npm install -g pnpm@8.3.1 pm2 ts-node typescript@5
|
npm install -g pnpm@8.3.1 pm2 ts-node typescript@5
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export PYTHON_SHORT_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
|
if [[ "$command" != "reload" ]]; then
|
||||||
export PNPM_HOME=${QL_DIR}/data/dep_cache/node
|
if [[ -n "${PYTHON_VENV_DIR:-}" ]]; then
|
||||||
export PYTHON_HOME=${QL_DIR}/data/dep_cache/python3
|
[[ "$PYTHON_VENV_DIR" = /* ]] || export PYTHON_VENV_DIR="${QL_DIR}/${PYTHON_VENV_DIR}"
|
||||||
export PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3
|
[[ -x "$PYTHON_VENV_DIR/bin/python3" ]] || python3 -m venv "$PYTHON_VENV_DIR"
|
||||||
|
"$PYTHON_VENV_DIR/bin/python3" -m pip install requests
|
||||||
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin
|
else
|
||||||
export NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules
|
pip3 install --prefix "${PYTHON_HOME:-${QL_DATA_DIR}/dep_cache/python3}" requests
|
||||||
export PIP_CACHE_DIR=${PYTHON_HOME}/pip
|
|
||||||
export PYTHONPATH=${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages
|
|
||||||
|
|
||||||
if [[ $command != "reload" ]]; then
|
|
||||||
pip3 install --prefix ${PYTHON_HOME} requests
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd "${QL_DIR}"
|
|
||||||
if [[ ! -e .env ]]; then
|
|
||||||
# Preserve user configuration, including a file created concurrently.
|
|
||||||
(umask 077; set -C; cat .env.example > .env)
|
|
||||||
fi
|
|
||||||
chmod 777 ${QL_DIR}/shell/*.sh
|
|
||||||
|
|
||||||
. ${QL_DIR}/shell/share.sh
|
|
||||||
. ${QL_DIR}/shell/env.sh
|
|
||||||
|
|
||||||
log_with_style() {
|
|
||||||
local level="$1"
|
|
||||||
local message="$2"
|
|
||||||
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
printf "\n[%s] [%7s] %s\n" "${timestamp}" "${level}" "${message}"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_with_style "INFO" "🚀 1. 检测配置文件..."
|
|
||||||
import_config "$@"
|
|
||||||
make_dir /etc/nginx/conf.d
|
|
||||||
make_dir /run/nginx
|
|
||||||
fix_config
|
|
||||||
|
|
||||||
pm2 l &>/dev/null
|
|
||||||
|
|
||||||
log_with_style "INFO" "🔄 2. 启动 nginx..."
|
|
||||||
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
|
||||||
|
|
||||||
log_with_style "INFO" "⚙️ 3. 启动 pm2 服务..."
|
|
||||||
reload_pm2
|
|
||||||
|
|
||||||
if [[ $command != "reload" ]]; then
|
|
||||||
if [[ $AutoStartBot == true ]]; then
|
|
||||||
log_with_style "INFO" "🤖 4. 启动 bot..."
|
|
||||||
nohup ql bot >$dir_log/bot.log 2>&1 &
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $EnableExtraShell == true ]]; then
|
|
||||||
log_with_style "INFO" "🛠️ 5. 执行自定义脚本..."
|
|
||||||
nohup ql extra >$dir_log/extra.log 2>&1 &
|
|
||||||
fi
|
|
||||||
|
|
||||||
pm2 startup
|
|
||||||
pm2 save
|
|
||||||
fi
|
fi
|
||||||
|
exec /bin/bash "${QL_DIR}/shell/start-simplify.sh" "$@"
|
||||||
log_with_style "SUCCESS" "🎉 启动成功!"
|
|
||||||
|
|||||||
+6
-2
@@ -99,7 +99,9 @@ add_cron() {
|
|||||||
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
|
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
|
||||||
[[ -z $cron_name ]] && cron_name=$(grep "name:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
|
[[ -z $cron_name ]] && cron_name=$(grep "name:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
|
||||||
[[ -z $cron_name ]] && cron_name=$(basename "$file_name")
|
[[ -z $cron_name ]] && cron_name=$(basename "$file_name")
|
||||||
result=$(add_cron_api "${cron_line}:${cmd_task} ${file}:${cron_name}:${SUB_ID}")
|
cron_name=${cron_name//$'\r'/}
|
||||||
|
cron_line=${cron_line//$'\r'/}
|
||||||
|
result=$(add_cron_api "$cron_line" "${cmd_task} ${file}" "$cron_name" "$SUB_ID")
|
||||||
echo -e "$result"
|
echo -e "$result"
|
||||||
if [[ $detail ]]; then
|
if [[ $detail ]]; then
|
||||||
detail="${detail}${result}\n"
|
detail="${detail}${result}\n"
|
||||||
@@ -190,7 +192,9 @@ update_raw() {
|
|||||||
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
|
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
|
||||||
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
|
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
|
||||||
[[ -z $cron_line ]] && cron_line="$default_cron"
|
[[ -z $cron_line ]] && cron_line="$default_cron"
|
||||||
result=$(add_cron_api "${cron_line}:${cmd_task} ${filename}:${cron_name}:${SUB_ID}")
|
cron_name=${cron_name//$'\r'/}
|
||||||
|
cron_line=${cron_line//$'\r'/}
|
||||||
|
result=$(add_cron_api "$cron_line" "${cmd_task} ${filename}" "$cron_name" "$SUB_ID")
|
||||||
echo -e "$result\n"
|
echo -e "$result\n"
|
||||||
notify_api "新增任务通知" "\n$result"
|
notify_api "新增任务通知" "\n$result"
|
||||||
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
|
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
|
||||||
|
|||||||
+11
-3
@@ -395,7 +395,7 @@
|
|||||||
"父目录": "Parent Directory",
|
"父目录": "Parent Directory",
|
||||||
"版本": "Version",
|
"版本": "Version",
|
||||||
"状态": "Status",
|
"状态": "Status",
|
||||||
"环境变量": "Environment Variables",
|
"环境变量": "Environment variable",
|
||||||
"生成数据中...": "Generating data...",
|
"生成数据中...": "Generating data...",
|
||||||
"用于访问所有私有仓库的全局SSH私钥": "Global SSH private key for accessing all private repositories",
|
"用于访问所有私有仓库的全局SSH私钥": "Global SSH private key for accessing all private repositories",
|
||||||
"用户令牌,扫描登录后 我的—>设置->令牌 中获取,参考 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/",
|
||||||
@@ -634,7 +634,6 @@
|
|||||||
"保存配置": "Save",
|
"保存配置": "Save",
|
||||||
"当前生效值": "Effective value",
|
"当前生效值": "Effective value",
|
||||||
"默认配置": "Default",
|
"默认配置": "Default",
|
||||||
"环境变量": "Environment variable",
|
|
||||||
"客户端 IP 诊断": "Client IP diagnostics",
|
"客户端 IP 诊断": "Client IP diagnostics",
|
||||||
"重新诊断": "Run again",
|
"重新诊断": "Run again",
|
||||||
"Socket 地址": "Socket address",
|
"Socket 地址": "Socket address",
|
||||||
@@ -699,5 +698,14 @@
|
|||||||
"仍然保存": "Save anyway",
|
"仍然保存": "Save anyway",
|
||||||
"已保存,本次未记录历史版本": "Saved without a history snapshot",
|
"已保存,本次未记录历史版本": "Saved without a history snapshot",
|
||||||
"已保存,但旧历史清理失败,请检查存储权限和空间": "Saved, but old history could not be removed. Check storage permissions and free space.",
|
"已保存,但旧历史清理失败,请检查存储权限和空间": "Saved, but old history could not be removed. Check storage permissions and free space.",
|
||||||
"保存失败": "Unable to save"
|
"保存失败": "Unable to save",
|
||||||
|
"通知日志": "Notification log",
|
||||||
|
"发送时间": "Sent at",
|
||||||
|
"推送渠道": "Channel",
|
||||||
|
"发送状态": "Delivery status",
|
||||||
|
"通知来源": "Notification source",
|
||||||
|
"脚本通知": "Script notification",
|
||||||
|
"系统通知": "System notification",
|
||||||
|
"Python支持多种安装方式:\n1. 包名(如:requests)\n2. GitHub链接(如:git+https://github.com/user/repo.git)\n3. requirements文件路径(如:path/to/requirements.txt)\n4. pyproject.toml文件路径": "Python supports package names, Git URLs, and existing requirements.txt or pyproject.toml files under the scripts or repository directory.",
|
||||||
|
"支持包名、GitHub链接、requirements.txt或pyproject.toml路径": "Package name, Git URL, requirements.txt or pyproject.toml path"
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-2
@@ -634,7 +634,6 @@
|
|||||||
"保存配置": "保存配置",
|
"保存配置": "保存配置",
|
||||||
"当前生效值": "当前生效值",
|
"当前生效值": "当前生效值",
|
||||||
"默认配置": "默认配置",
|
"默认配置": "默认配置",
|
||||||
"环境变量": "环境变量",
|
|
||||||
"客户端 IP 诊断": "客户端 IP 诊断",
|
"客户端 IP 诊断": "客户端 IP 诊断",
|
||||||
"重新诊断": "重新诊断",
|
"重新诊断": "重新诊断",
|
||||||
"Socket 地址": "Socket 地址",
|
"Socket 地址": "Socket 地址",
|
||||||
@@ -699,5 +698,14 @@
|
|||||||
"仍然保存": "仍然保存",
|
"仍然保存": "仍然保存",
|
||||||
"已保存,本次未记录历史版本": "已保存,本次未记录历史版本",
|
"已保存,本次未记录历史版本": "已保存,本次未记录历史版本",
|
||||||
"已保存,但旧历史清理失败,请检查存储权限和空间": "已保存,但旧历史清理失败,请检查存储权限和空间",
|
"已保存,但旧历史清理失败,请检查存储权限和空间": "已保存,但旧历史清理失败,请检查存储权限和空间",
|
||||||
"保存失败": "保存失败"
|
"保存失败": "保存失败",
|
||||||
|
"通知日志": "通知日志",
|
||||||
|
"发送时间": "发送时间",
|
||||||
|
"推送渠道": "推送渠道",
|
||||||
|
"发送状态": "发送状态",
|
||||||
|
"通知来源": "通知来源",
|
||||||
|
"脚本通知": "脚本通知",
|
||||||
|
"系统通知": "系统通知",
|
||||||
|
"Python支持多种安装方式:\n1. 包名(如:requests)\n2. GitHub链接(如:git+https://github.com/user/repo.git)\n3. requirements文件路径(如:path/to/requirements.txt)\n4. pyproject.toml文件路径": "Python支持多种安装方式:\n1. 包名(如:requests)\n2. GitHub链接(如:git+https://github.com/user/repo.git)\n3. requirements文件路径(如:path/to/requirements.txt)\n4. pyproject.toml文件路径",
|
||||||
|
"支持包名、GitHub链接、requirements.txt或pyproject.toml路径": "支持包名、GitHub链接、requirements.txt或pyproject.toml路径"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ const DependenceModal = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [selectedType, setSelectedType] = useState(
|
||||||
|
DependenceTypes[defaultType as any],
|
||||||
|
);
|
||||||
|
|
||||||
const handleOk = async (values: any) => {
|
const handleOk = async (values: any) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -90,7 +93,7 @@ const DependenceModal = ({
|
|||||||
label={intl.get('依赖类型')}
|
label={intl.get('依赖类型')}
|
||||||
initialValue={DependenceTypes[defaultType as any]}
|
initialValue={DependenceTypes[defaultType as any]}
|
||||||
>
|
>
|
||||||
<Select>
|
<Select onChange={(value) => setSelectedType(value)}>
|
||||||
{config.dependenceTypes.map((x, i) => (
|
{config.dependenceTypes.map((x, i) => (
|
||||||
<Option key={i} value={i}>
|
<Option key={i} value={i}>
|
||||||
{x}
|
{x}
|
||||||
@@ -121,11 +124,24 @@ const DependenceModal = ({
|
|||||||
whitespace: true,
|
whitespace: true,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
tooltip={
|
||||||
|
selectedType === DependenceTypes.python3
|
||||||
|
? intl.get(
|
||||||
|
'Python支持多种安装方式:\n1. 包名(如:requests)\n2. GitHub链接(如:git+https://github.com/user/repo.git)\n3. requirements文件路径(如:path/to/requirements.txt)\n4. pyproject.toml文件路径',
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
rows={4}
|
rows={4}
|
||||||
autoSize={{ minRows: 1, maxRows: 5 }}
|
autoSize={{ minRows: 1, maxRows: 5 }}
|
||||||
placeholder={intl.get('请输入依赖名称')}
|
placeholder={
|
||||||
|
selectedType === DependenceTypes.python3
|
||||||
|
? intl.get(
|
||||||
|
'支持包名、GitHub链接、requirements.txt或pyproject.toml路径',
|
||||||
|
)
|
||||||
|
: intl.get('请输入依赖名称')
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="remark" label={intl.get('备注')}>
|
<Form.Item name="remark" label={intl.get('备注')}>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import SecuritySettings from './security';
|
import SecuritySettings from './security';
|
||||||
import LoginLog from './loginLog';
|
import LoginLog from './loginLog';
|
||||||
|
import NotifyLog from './notifyLog';
|
||||||
import NotificationSetting from './notification';
|
import NotificationSetting from './notification';
|
||||||
import Other from './other';
|
import Other from './other';
|
||||||
import About from './about';
|
import About from './about';
|
||||||
@@ -126,6 +127,7 @@ const Setting = () => {
|
|||||||
const [editedApp, setEditedApp] = useState<any>();
|
const [editedApp, setEditedApp] = useState<any>();
|
||||||
const [tabActiveKey, setTabActiveKey] = useState('security');
|
const [tabActiveKey, setTabActiveKey] = useState('security');
|
||||||
const [loginLogData, setLoginLogData] = useState<any[]>([]);
|
const [loginLogData, setLoginLogData] = useState<any[]>([]);
|
||||||
|
const [notifyLogData, setNotifyLogData] = useState<any[]>([]);
|
||||||
const [notificationInfo, setNotificationInfo] = useState<any>();
|
const [notificationInfo, setNotificationInfo] = useState<any>();
|
||||||
const containergRef = useRef<HTMLDivElement>(null);
|
const containergRef = useRef<HTMLDivElement>(null);
|
||||||
const [height, setHeight] = useState<number>(0);
|
const [height, setHeight] = useState<number>(0);
|
||||||
@@ -254,6 +256,8 @@ const Setting = () => {
|
|||||||
getApps();
|
getApps();
|
||||||
} else if (activeKey === 'login') {
|
} else if (activeKey === 'login') {
|
||||||
getLoginLog();
|
getLoginLog();
|
||||||
|
} else if (activeKey === 'notifylog') {
|
||||||
|
getNotifyLog();
|
||||||
} else if (activeKey === 'notification') {
|
} else if (activeKey === 'notification') {
|
||||||
getNotification();
|
getNotification();
|
||||||
}
|
}
|
||||||
@@ -272,6 +276,19 @@ const Setting = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getNotifyLog = () => {
|
||||||
|
request
|
||||||
|
.get(`${config.apiPrefix}system/notify-log`)
|
||||||
|
.then(({ code, data }) => {
|
||||||
|
if (code === 200) {
|
||||||
|
setNotifyLogData(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDemoEnv) {
|
if (isDemoEnv) {
|
||||||
getApps();
|
getApps();
|
||||||
@@ -345,6 +362,11 @@ const Setting = () => {
|
|||||||
label: intl.get('登录日志'),
|
label: intl.get('登录日志'),
|
||||||
children: <LoginLog height={height} data={loginLogData} />,
|
children: <LoginLog height={height} data={loginLogData} />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'notifylog',
|
||||||
|
label: intl.get('通知日志'),
|
||||||
|
children: <NotifyLog height={height} data={notifyLogData} />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'client-ip',
|
key: 'client-ip',
|
||||||
label: intl.get('客户端 IP'),
|
label: intl.get('客户端 IP'),
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import React from 'react';
|
||||||
|
import { Table, Tag } from 'antd';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
interface NotifyLogItem {
|
||||||
|
id?: number;
|
||||||
|
timestamp?: number;
|
||||||
|
title?: string;
|
||||||
|
content?: string;
|
||||||
|
status?: number;
|
||||||
|
notifyType?: string;
|
||||||
|
source?: 'system' | 'script';
|
||||||
|
scriptName?: string;
|
||||||
|
taskId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NotifyStatusLabel: Record<number, string> = {
|
||||||
|
0: '成功',
|
||||||
|
1: '失败',
|
||||||
|
};
|
||||||
|
|
||||||
|
const NotifyStatusColor: Record<number, string> = {
|
||||||
|
0: 'success',
|
||||||
|
1: 'error',
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: intl.get('通知来源'),
|
||||||
|
key: 'source',
|
||||||
|
width: 220,
|
||||||
|
render: (_: unknown, record: NotifyLogItem) => (
|
||||||
|
<span>
|
||||||
|
{intl.get(record.source === 'script' ? '脚本通知' : '系统通知')}
|
||||||
|
{record.scriptName ? ` · ${record.scriptName}` : ''}
|
||||||
|
{record.taskId ? ` (#${record.taskId})` : ''}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.get('序号'),
|
||||||
|
width: 50,
|
||||||
|
render: (text: string, record: any, index: number) => {
|
||||||
|
return index + 1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.get('发送时间'),
|
||||||
|
dataIndex: 'timestamp',
|
||||||
|
key: 'timestamp',
|
||||||
|
width: 160,
|
||||||
|
render: (text: string, record: any) => {
|
||||||
|
return dayjs(record.timestamp).format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.get('标题'),
|
||||||
|
dataIndex: 'title',
|
||||||
|
key: 'title',
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.get('内容'),
|
||||||
|
dataIndex: 'content',
|
||||||
|
key: 'content',
|
||||||
|
render: (text: string) => {
|
||||||
|
if (!text) return '';
|
||||||
|
return text.length > 100 ? text.slice(0, 100) + '...' : text;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.get('推送渠道'),
|
||||||
|
dataIndex: 'notifyType',
|
||||||
|
key: 'notifyType',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.get('发送状态'),
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
width: 90,
|
||||||
|
render: (text: string, record: NotifyLogItem) => {
|
||||||
|
const statusKey = record.status ?? 1;
|
||||||
|
return (
|
||||||
|
<Tag color={NotifyStatusColor[statusKey]} style={{ marginRight: 0 }}>
|
||||||
|
{intl.get(NotifyStatusLabel[statusKey])}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const NotifyLog = ({
|
||||||
|
data,
|
||||||
|
height,
|
||||||
|
}: {
|
||||||
|
data: Array<NotifyLogItem>;
|
||||||
|
height: number;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
pagination={false}
|
||||||
|
dataSource={data}
|
||||||
|
rowKey="id"
|
||||||
|
size="middle"
|
||||||
|
scroll={{ x: 1000, y: height }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotifyLog;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
const test = require('node:test'),
|
||||||
|
assert = require('node:assert/strict'),
|
||||||
|
fs = require('node:fs'),
|
||||||
|
os = require('node:os'),
|
||||||
|
path = require('node:path'),
|
||||||
|
express = require('express');
|
||||||
|
const load = require('../helpers/load-security-module.cjs');
|
||||||
|
test('unknown API requests return JSON 404 while env.js and SPA deep links work', async (t) => {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-fallback-'));
|
||||||
|
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||||
|
fs.mkdirSync(path.join(dir, 'static/dist'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(dir, 'static/dist/index.html'), 'SPA');
|
||||||
|
const app = express();
|
||||||
|
load('back/loaders/express.ts', {
|
||||||
|
'../api': () => express.Router(),
|
||||||
|
'../config': {
|
||||||
|
rootPath: dir,
|
||||||
|
uploadPath: dir,
|
||||||
|
api: { prefix: '/api' },
|
||||||
|
apiWhiteList: ['/api/env.js'],
|
||||||
|
jwt: { secret: 'test' },
|
||||||
|
},
|
||||||
|
'express-jwt': {
|
||||||
|
expressjwt: () => ({ unless: () => (_q, _r, next) => next() }),
|
||||||
|
UnauthorizedError: class extends Error {},
|
||||||
|
},
|
||||||
|
'../config/util': {
|
||||||
|
getPlatform: () => 'desktop',
|
||||||
|
getToken: () => 'stored',
|
||||||
|
},
|
||||||
|
'../config/serverEnv': {
|
||||||
|
serveEnv: (_q, r) => r.type('js').send('window.QL_CONFIG={};'),
|
||||||
|
},
|
||||||
|
'../shared/store': {
|
||||||
|
shareStore: { getAuthInfo: async () => ({}), getApps: async () => [] },
|
||||||
|
},
|
||||||
|
'../shared/auth': {
|
||||||
|
isValidToken: () => true,
|
||||||
|
isDefaultAuthInfo: () => false,
|
||||||
|
},
|
||||||
|
'../shared/i18n': { t: (s) => s },
|
||||||
|
'../middlewares/uniqueConstraintError': (_q, _r, next) => next(),
|
||||||
|
'../middlewares/protectedPathCase': (_q, _r, next) => next(),
|
||||||
|
'../data/system': {},
|
||||||
|
'../data/open': {},
|
||||||
|
}).default({ app });
|
||||||
|
const server = await new Promise((r) => {
|
||||||
|
const s = app.listen(0, '127.0.0.1', () => r(s));
|
||||||
|
});
|
||||||
|
t.after(() => new Promise((r) => server.close(r)));
|
||||||
|
const url = `http://127.0.0.1:${server.address().port}`;
|
||||||
|
for (const p of ['/api', '/api/missing', '/open/missing']) {
|
||||||
|
const res = await fetch(url + p);
|
||||||
|
assert.equal(res.status, 404, p);
|
||||||
|
assert.match(res.headers.get('content-type'), /json/);
|
||||||
|
assert.equal((await res.json()).code, 404);
|
||||||
|
}
|
||||||
|
const env = await fetch(url + '/api/env.js');
|
||||||
|
assert.match(await env.text(), /QL_CONFIG/);
|
||||||
|
assert.equal(await (await fetch(url + '/script')).text(), 'SPA');
|
||||||
|
});
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
const test = require('node:test'),
|
|
||||||
assert = require('node:assert/strict'),
|
|
||||||
fs = require('node:fs'),
|
|
||||||
os = require('node:os'),
|
|
||||||
path = require('node:path');
|
|
||||||
const load = require('../helpers/load-security-module.cjs');
|
|
||||||
const { writeFileWithLock, getUniqueLockPath } = load('back/shared/utils.ts', {
|
|
||||||
'../config/util': { fileExist: async (p) => fs.existsSync(p) },
|
|
||||||
});
|
|
||||||
test('write failure releases the lock and preserves the original error', async (t) => {
|
|
||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-lock-review-'));
|
|
||||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
||||||
const p = path.join(dir, 'script');
|
|
||||||
fs.writeFileSync(p, 'original');
|
|
||||||
await assert.rejects(
|
|
||||||
writeFileWithLock(p, 'bad', { encoding: 'invalid-encoding' }),
|
|
||||||
(e) => e.code === 'ERR_INVALID_ARG_VALUE',
|
|
||||||
);
|
|
||||||
assert.equal(fs.existsSync(getUniqueLockPath(p)), false);
|
|
||||||
await writeFileWithLock(p, 'retry');
|
|
||||||
assert.equal(fs.readFileSync(p, 'utf8'), 'retry');
|
|
||||||
});
|
|
||||||
test('first write closes its file handle and keeps requested permissions', async (t) => {
|
|
||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-lock-review-'));
|
|
||||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
||||||
const p = path.join(dir, 'new');
|
|
||||||
await writeFileWithLock(p, 'created', { mode: 0o600 });
|
|
||||||
assert.equal(fs.readFileSync(p, 'utf8'), 'created');
|
|
||||||
assert.equal(fs.statSync(p).mode & 0o777, 0o600);
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const test = require('node:test');
|
||||||
|
const fs = require('node:fs/promises');
|
||||||
|
const path = require('node:path');
|
||||||
|
const os = require('node:os');
|
||||||
|
const { spawnSync } = require('node:child_process');
|
||||||
|
const load = require('../helpers/load-security-module.cjs');
|
||||||
|
const hasApt = spawnSync('apt-get', ['--version']).status === 0;
|
||||||
|
const {
|
||||||
|
rewriteMirrorSources,
|
||||||
|
replaceMirrorUrl,
|
||||||
|
normalizeMirror,
|
||||||
|
mirrorWriteCommand,
|
||||||
|
} = require('../../back/shared/linuxMirror');
|
||||||
|
|
||||||
|
async function fixture(t) {
|
||||||
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-issue-fixes-'));
|
||||||
|
t.after(() => fs.rm(root, { force: true, recursive: true }));
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('#2902/#2871 mirror writes are idempotent, preserve suites, security/ports/version paths and quote URLs', async (t) => {
|
||||||
|
const original = `# http://leave.example/debian\nTypes: deb\nURIs: http://deb.debian.org/debian\nSuites: bookworm bookworm-updates\nComponents: main\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg\n\nTypes: deb\nURIs: http://security.debian.org/debian-security\nSuites: bookworm-security\n`;
|
||||||
|
const url = 'https://mirror.example/mirrors/debian/';
|
||||||
|
const changed = rewriteMirrorSources(original, url);
|
||||||
|
assert.equal(rewriteMirrorSources(changed, url), changed);
|
||||||
|
assert.match(
|
||||||
|
changed,
|
||||||
|
/URIs: https:\/\/mirror.example\/mirrors\/debian-security/,
|
||||||
|
);
|
||||||
|
assert.match(changed, /Suites: bookworm-security/);
|
||||||
|
assert.match(changed, /# http:\/\/leave.example\/debian/);
|
||||||
|
assert.equal(
|
||||||
|
replaceMirrorUrl('http://old/debian/debian/debian', url),
|
||||||
|
'https://mirror.example/mirrors/debian',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
replaceMirrorUrl('http://old/armbian/armbian', 'https://new/armbian'),
|
||||||
|
'https://new/armbian',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
replaceMirrorUrl('http://old/ubuntu-ports', 'https://new/ubuntu'),
|
||||||
|
'https://new/ubuntu-ports',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
replaceMirrorUrl('http://old/alpine/v3.22/main', 'https://new/alpine'),
|
||||||
|
'https://new/alpine/v3.22/main',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
replaceMirrorUrl('http://old/debian', 'https://new'),
|
||||||
|
'https://new/debian',
|
||||||
|
);
|
||||||
|
for (const bad of [
|
||||||
|
'file:///tmp/a',
|
||||||
|
'http://u:p@host',
|
||||||
|
'https://host?x=1',
|
||||||
|
'https://host/\nanything',
|
||||||
|
])
|
||||||
|
assert.throws(() => normalizeMirror(bad));
|
||||||
|
const root = await fixture(t);
|
||||||
|
const file = path.join(root, "quote's", 'sources');
|
||||||
|
const command = mirrorWriteCommand(file, changed, '', 'true');
|
||||||
|
const result = spawnSync('bash', ['-c', command], { encoding: 'utf8' });
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(await fs.readFile(file, 'utf8'), changed);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('#2701 shell API serializes CR/LF, quotes, backslashes and colons without corrupting JSON', async () => {
|
||||||
|
const result = spawnSync(
|
||||||
|
'bash',
|
||||||
|
[
|
||||||
|
'-c',
|
||||||
|
`source shell/api.sh
|
||||||
|
ql_api_request() { while [[ $# -gt 0 ]]; do if [[ "$1" == --data-raw ]]; then printf '%s\\n' "$2" >&3; break; fi; shift; done; printf '{"code":200}'; }
|
||||||
|
t() { :; }
|
||||||
|
add_cron_api '0 0 1 1 *' 'task a:b.js' $'quote"slash\\\\colon:\\r\\n\\t中文' 23
|
||||||
|
update_cron_api '0 0 1 1 *' 'task a:b.js' $'quote"slash\\\\colon:\\r\\n\\t中文' 27
|
||||||
|
update_cron_command_api $'task path\\\\with"quotes.js' 27
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe', 'pipe'] },
|
||||||
|
);
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
const payloads = result.output[3].trim().split('\n').map(JSON.parse);
|
||||||
|
assert.equal(payloads[0].name, 'quote"slash\\colon:\r\n\t中文');
|
||||||
|
assert.equal(payloads[0].sub_id, 23);
|
||||||
|
assert.equal(payloads[1].id, '27');
|
||||||
|
assert.equal(payloads[2].command, 'task path\\with"quotes.js');
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const distribution of ['Debian', 'Ubuntu']) {
|
||||||
|
for (const legacyContent of ['', ' \t\r\n']) {
|
||||||
|
test(`${distribution}: empty legacy sources.list falls back to deb822 .sources`, async (t) => {
|
||||||
|
const root = await fixture(t);
|
||||||
|
const legacy = path.join(root, 'sources.list');
|
||||||
|
const sources = path.join(
|
||||||
|
root,
|
||||||
|
'sources.list.d',
|
||||||
|
`${distribution.toLowerCase()}.sources`,
|
||||||
|
);
|
||||||
|
await fs.writeFile(legacy, legacyContent);
|
||||||
|
const util = load('back/config/util.ts', {
|
||||||
|
'fs/promises': {
|
||||||
|
readFile: async (file) => {
|
||||||
|
if (file === '/etc/os-release')
|
||||||
|
return `ID=${distribution.toLowerCase()}\nVERSION_CODENAME=${
|
||||||
|
distribution === 'Debian' ? 'bookworm' : 'noble'
|
||||||
|
}\n`;
|
||||||
|
if (file === '/etc/apt/sources.list')
|
||||||
|
return fs.readFile(legacy, 'utf8');
|
||||||
|
throw Object.assign(new Error('missing fixture source'), {
|
||||||
|
code: 'ENOENT',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
os: { platform: () => 'linux' },
|
||||||
|
'./index': {},
|
||||||
|
'./const': {},
|
||||||
|
'./share': {},
|
||||||
|
'./container': { isInContainer: () => false },
|
||||||
|
'../loaders/logger': {},
|
||||||
|
'../shared/utils': {},
|
||||||
|
'../shared/fileAccess': {},
|
||||||
|
'../data/dependence': {},
|
||||||
|
});
|
||||||
|
const previousOS = process.env.QL_OS_TYPE;
|
||||||
|
let command;
|
||||||
|
try {
|
||||||
|
process.env.QL_OS_TYPE = distribution.toLowerCase();
|
||||||
|
command = await util.updateLinuxMirrorFile('https://mirror.example');
|
||||||
|
} finally {
|
||||||
|
if (previousOS === undefined) delete process.env.QL_OS_TYPE;
|
||||||
|
else process.env.QL_OS_TYPE = previousOS;
|
||||||
|
}
|
||||||
|
// Execute the entire generated writer, redirecting its APT paths into
|
||||||
|
// the fixture and replacing only the package-index refresh.
|
||||||
|
const isolated = command
|
||||||
|
.replaceAll('/etc/apt', root)
|
||||||
|
.replace('apt-get update --error-on=any', 'true');
|
||||||
|
const result = spawnSync('bash', ['-c', isolated], { encoding: 'utf8' });
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(await fs.readFile(legacy, 'utf8'), legacyContent);
|
||||||
|
const generated = await fs.readFile(sources, 'utf8');
|
||||||
|
assert.match(generated, /^Types: deb\nURIs: https:\/\/mirror\.example\//);
|
||||||
|
assert.match(generated, /Signed-By: \/usr\/share\/keyrings\//);
|
||||||
|
assert.match(
|
||||||
|
generated,
|
||||||
|
distribution === 'Debian' ? /bookworm-security/ : /noble-backports/,
|
||||||
|
);
|
||||||
|
if (hasApt) {
|
||||||
|
// Parse both source locations with real APT, without refreshing indexes
|
||||||
|
// or touching the host's configuration/cache.
|
||||||
|
const parsed = spawnSync(
|
||||||
|
'apt-get',
|
||||||
|
[
|
||||||
|
'indextargets',
|
||||||
|
'-o',
|
||||||
|
`Dir::Etc::sourcelist=${legacy}`,
|
||||||
|
'-o',
|
||||||
|
`Dir::Etc::sourceparts=${path.dirname(sources)}`,
|
||||||
|
'-o',
|
||||||
|
`Dir::State::lists=${path.join(root, 'lists')}`,
|
||||||
|
],
|
||||||
|
{ encoding: 'utf8' },
|
||||||
|
);
|
||||||
|
assert.equal(parsed.status, 0, parsed.stderr);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('deb822 mirror changes preserve vendor archives, signing keys and comments across repeated saves', () => {
|
||||||
|
const system =
|
||||||
|
'Types: deb\nURIs: https://deb.debian.org/debian\nSuites: bookworm\nComponents: main\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg\n';
|
||||||
|
const vendors = [
|
||||||
|
'Types: deb\nURIs: https://download.docker.com/linux/debian\nSuites: bookworm\nComponents: stable\nSigned-By: /etc/apt/keyrings/docker.asc\n',
|
||||||
|
'Types: deb\nURIs: https://download.docker.com/linux/debian\nSuites: bookworm\nComponents: stable\n',
|
||||||
|
'Types: deb\nURIs: https://packages.microsoft.com/repos/code\nSuites: stable\nComponents: main\n',
|
||||||
|
'Types: deb\nURIs: https://vendor.example/debian\nSuites: bookworm\nComponents: main\nSigned-By: /etc/apt/keyrings/vendor.gpg\n',
|
||||||
|
];
|
||||||
|
const comment = '# Keep https://vendor.example/debian unchanged\n';
|
||||||
|
const original = [system, ...vendors, comment].join('\n');
|
||||||
|
const first = rewriteMirrorSources(
|
||||||
|
original,
|
||||||
|
'https://mirror.example/mirrors/debian',
|
||||||
|
);
|
||||||
|
assert.ok(first.includes('URIs: https://mirror.example/mirrors/debian\n'));
|
||||||
|
for (const unchanged of [...vendors, comment])
|
||||||
|
assert.ok(first.includes(unchanged));
|
||||||
|
assert.equal(
|
||||||
|
rewriteMirrorSources(first, 'https://mirror.example/mirrors/debian'),
|
||||||
|
first,
|
||||||
|
);
|
||||||
|
const second = rewriteMirrorSources(first, 'https://second.example/debian');
|
||||||
|
assert.ok(second.includes('URIs: https://second.example/debian\n'));
|
||||||
|
for (const unchanged of [...vendors, comment])
|
||||||
|
assert.ok(second.includes(unchanged));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('legacy mirror changes preserve third-party lines, inline comments and source options', () => {
|
||||||
|
const system =
|
||||||
|
'deb [arch=amd64 signed-by=/usr/share/keyrings/debian-archive-keyring.gpg] https://deb.debian.org/debian bookworm main # https://leave.example/debian\n';
|
||||||
|
const security =
|
||||||
|
'deb-src https://security.debian.org/debian-security bookworm-security main\n';
|
||||||
|
const vendors = [
|
||||||
|
'deb [signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable\n',
|
||||||
|
'deb https://download.docker.com/linux/debian bookworm stable\n',
|
||||||
|
'deb [signed-by=/etc/apt/keyrings/vendor.gpg] https://vendor.example/debian bookworm main\n',
|
||||||
|
'deb https://packages.microsoft.com/repos/code stable main\n',
|
||||||
|
'# deb https://deb.debian.org/debian bookworm main\n',
|
||||||
|
];
|
||||||
|
const original = system + security + vendors.join('');
|
||||||
|
const changed = rewriteMirrorSources(
|
||||||
|
original,
|
||||||
|
'https://mirror.example/debian',
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
changed.includes(
|
||||||
|
system.replace(
|
||||||
|
'https://deb.debian.org/debian',
|
||||||
|
'https://mirror.example/debian',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
changed.includes(
|
||||||
|
security.replace(
|
||||||
|
'https://security.debian.org/debian-security',
|
||||||
|
'https://mirror.example/debian-security',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
for (const unchanged of vendors) assert.ok(changed.includes(unchanged));
|
||||||
|
assert.equal(
|
||||||
|
rewriteMirrorSources(changed, 'https://mirror.example/debian'),
|
||||||
|
changed,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Ubuntu ports, Armbian and Alpine roots still switch mirrors without altering repository metadata', () => {
|
||||||
|
for (const url of [
|
||||||
|
'https://ports.ubuntu.com/ubuntu-ports',
|
||||||
|
'https://apt.armbian.com/armbian',
|
||||||
|
'https://old.example/alpine/v3.22/main',
|
||||||
|
'https://old.example/alpine/v3.22/community',
|
||||||
|
]) {
|
||||||
|
const original = `URIs: ${url}\nSuites: noble\nComponents: main\n`;
|
||||||
|
assert.equal(
|
||||||
|
rewriteMirrorSources(original, 'https://mirror.example'),
|
||||||
|
original.replace(url, replaceMirrorUrl(url, 'https://mirror.example')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -129,7 +129,7 @@ Acquire::http::Timeout "1";
|
|||||||
'fs/promises': {
|
'fs/promises': {
|
||||||
readFile: async (file) =>
|
readFile: async (file) =>
|
||||||
file === '/etc/os-release'
|
file === '/etc/os-release'
|
||||||
? distribution
|
? `${distribution}\nVERSION_CODENAME=bookworm\n`
|
||||||
: existingMirror
|
: existingMirror
|
||||||
? 'URIs: http://old.example/debian\n'
|
? 'URIs: http://old.example/debian\n'
|
||||||
: '',
|
: '',
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
const test = require('node:test'),
|
||||||
|
assert = require('node:assert/strict'),
|
||||||
|
{ Sequelize, DataTypes } = require('sequelize');
|
||||||
|
const load = require('../helpers/load-security-module.cjs');
|
||||||
|
const decorators = { Service: () => (x) => x, Inject: () => () => {} };
|
||||||
|
async function fixture(t) {
|
||||||
|
const db = new Sequelize({
|
||||||
|
dialect: 'sqlite',
|
||||||
|
storage: ':memory:',
|
||||||
|
logging: false,
|
||||||
|
});
|
||||||
|
t.after(() => db.close());
|
||||||
|
const rows = db.define('System', {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
info: DataTypes.JSON,
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
const logger = { error() {} };
|
||||||
|
const S = load('back/services/system.ts', {
|
||||||
|
typedi: decorators,
|
||||||
|
'../config': {},
|
||||||
|
'../config/const': { NotificationModeStringMap: { 0: 'gotify' } },
|
||||||
|
'../config/util': {},
|
||||||
|
'../data/dependence': {},
|
||||||
|
'../data/system': {
|
||||||
|
SystemModel: rows,
|
||||||
|
AuthDataType: { notifyLog: 'notifyLog', notification: 'notification' },
|
||||||
|
NotifyStatus: { success: 0, fail: 1 },
|
||||||
|
},
|
||||||
|
'./schedule': class {},
|
||||||
|
'./sock': class {},
|
||||||
|
'./notify': class {},
|
||||||
|
'../shared/pLimit': {},
|
||||||
|
'../schedule/client': {},
|
||||||
|
'../config/index': {},
|
||||||
|
'../shared/store': {},
|
||||||
|
'../shared/i18n': { t: (s) => s },
|
||||||
|
}).default;
|
||||||
|
const service = Object.assign(Object.create(S.prototype), {
|
||||||
|
logger,
|
||||||
|
notificationService: new (load('back/services/notify.ts', {
|
||||||
|
typedi: decorators,
|
||||||
|
'../config/util': {},
|
||||||
|
'../data/system': {
|
||||||
|
SystemModel: rows,
|
||||||
|
AuthDataType: { notifyLog: 'notifyLog' },
|
||||||
|
NotifyStatus: { success: 0, fail: 1 },
|
||||||
|
},
|
||||||
|
'../shared/i18n': { t: (s) => s },
|
||||||
|
'./user': {},
|
||||||
|
}).default)(),
|
||||||
|
});
|
||||||
|
service.notificationService.userService = {
|
||||||
|
getNotificationMode: async () => ({ type: 'email' }),
|
||||||
|
};
|
||||||
|
service.notificationService.modeMap.set('email', async () => true);
|
||||||
|
service.getDb = async () => ({ info: { type: 'email' } });
|
||||||
|
return { rows, service };
|
||||||
|
}
|
||||||
|
test('writes cap logs without reading the page and preserve other system data', async (t) => {
|
||||||
|
const { rows, service } = await fixture(t);
|
||||||
|
await rows.create({ type: 'authConfig', info: { keep: true } });
|
||||||
|
for (let i = 0; i < 205; i++)
|
||||||
|
await service.notify({ title: String(i), content: 'body' });
|
||||||
|
assert.equal(await rows.count({ where: { type: 'notifyLog' } }), 200);
|
||||||
|
const logs = await service.getNotifyLog();
|
||||||
|
assert.equal(logs[0].title, '204');
|
||||||
|
assert.equal(logs.at(-1).title, '5');
|
||||||
|
assert.equal(logs[0].notifyType, 'email');
|
||||||
|
assert.equal(await rows.count({ where: { type: 'authConfig' } }), 1);
|
||||||
|
});
|
||||||
|
test('audit DB failure does not turn a successful notification into failure', async (t) => {
|
||||||
|
const { rows, service } = await fixture(t);
|
||||||
|
const create = rows.create;
|
||||||
|
rows.create = async () => {
|
||||||
|
throw Error('disk full');
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
assert.equal(
|
||||||
|
(await service.notify({ title: 'ok', content: 'ok' })).code,
|
||||||
|
200,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
rows.create = create;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('failed sends are recorded and message size is bounded', async (t) => {
|
||||||
|
const { service } = await fixture(t);
|
||||||
|
service.notificationService.modeMap.set('gotify', async () => false);
|
||||||
|
assert.equal(
|
||||||
|
(
|
||||||
|
await service.notify({
|
||||||
|
title: 'x'.repeat(1000),
|
||||||
|
content: 'y'.repeat(20000),
|
||||||
|
notificationInfo: { type: 0 },
|
||||||
|
})
|
||||||
|
).code,
|
||||||
|
400,
|
||||||
|
);
|
||||||
|
const [row] = await service.getNotifyLog();
|
||||||
|
assert.equal(row.status, 1);
|
||||||
|
assert.equal(row.title.length, 512);
|
||||||
|
assert.equal(row.content.length, 8192);
|
||||||
|
assert.equal(row.notifyType, 'gotify');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('script reports share retention and preserve source without sending twice', async (t) => {
|
||||||
|
const { service } = await fixture(t);
|
||||||
|
await service.recordNotifyLog({
|
||||||
|
title: 'task result',
|
||||||
|
content: 'body',
|
||||||
|
status: 1,
|
||||||
|
notifyType: 'smtp',
|
||||||
|
source: 'script',
|
||||||
|
scriptName: 'job.py',
|
||||||
|
taskId: '42',
|
||||||
|
});
|
||||||
|
const [row] = await service.getNotifyLog();
|
||||||
|
assert.equal(row.source, 'script');
|
||||||
|
assert.equal(row.scriptName, 'job.py');
|
||||||
|
assert.equal(row.taskId, '42');
|
||||||
|
assert.equal(row.status, 1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
const test = require('node:test'),
|
||||||
|
assert = require('node:assert/strict'),
|
||||||
|
fs = require('node:fs'),
|
||||||
|
os = require('node:os'),
|
||||||
|
path = require('node:path'),
|
||||||
|
{ execFileSync } = require('node:child_process');
|
||||||
|
const load = require('../helpers/load-security-module.cjs');
|
||||||
|
test('Python source arguments round-trip literally and local files stay inside maintained roots', (t) => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-pip-source-'));
|
||||||
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||||
|
const scripts = path.join(root, 'scripts'),
|
||||||
|
repo = path.join(root, 'repo');
|
||||||
|
fs.mkdirSync(scripts);
|
||||||
|
fs.mkdirSync(repo);
|
||||||
|
fs.mkdirSync(path.join(scripts, 'project space'));
|
||||||
|
const requirement = path.join(scripts, 'project space', 'requirements.txt');
|
||||||
|
fs.writeFileSync(requirement, 'requests');
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(scripts, 'project space', 'pyproject.toml'),
|
||||||
|
'[project]\nname="sample"',
|
||||||
|
);
|
||||||
|
const outside = path.join(root, 'requirements.txt');
|
||||||
|
fs.writeFileSync(outside, 'outside');
|
||||||
|
fs.symlinkSync(outside, path.join(scripts, 'requirements-link.txt'));
|
||||||
|
const api = load('back/config/util.ts', {
|
||||||
|
'./index': { scriptPath: scripts, repoPath: repo },
|
||||||
|
'./const': { PYTHON_INSTALL_DIR: '/prefix with spaces' },
|
||||||
|
'../loaders/logger': {},
|
||||||
|
'../shared/utils': {},
|
||||||
|
'../data/dependence': {
|
||||||
|
DependenceTypes: { nodejs: 0, python3: 1, linux: 2 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
function args(cmd) {
|
||||||
|
return execFileSync('/bin/bash', [
|
||||||
|
'-c',
|
||||||
|
'pip3(){ printf "%s\\0" "$@"; }; ' + cmd,
|
||||||
|
])
|
||||||
|
.toString()
|
||||||
|
.split('\0')
|
||||||
|
.slice(0, -1);
|
||||||
|
}
|
||||||
|
const req = args(api.getInstallCommand(1, 'project space/requirements.txt'));
|
||||||
|
assert.equal(req.at(-1), requirement);
|
||||||
|
assert.equal(req.at(-2), '-r');
|
||||||
|
assert.ok(req.includes('--prefix=/prefix with spaces'));
|
||||||
|
assert.equal(
|
||||||
|
args(api.getInstallCommand(1, 'project space/pyproject.toml')).at(-1),
|
||||||
|
path.dirname(requirement),
|
||||||
|
);
|
||||||
|
const url = 'git+https://example.invalid/repo.git#egg=pkg;$(echo INJECTED)';
|
||||||
|
assert.equal(args(api.getInstallCommand(1, url)).at(-1), url);
|
||||||
|
assert.throws(() => api.getInstallCommand(1, '../requirements.txt'));
|
||||||
|
assert.throws(() => api.getInstallCommand(1, 'requirements-link.txt'));
|
||||||
|
assert.equal(
|
||||||
|
args(api.getUninstallCommand(1, 'project space/requirements.txt')).at(-1),
|
||||||
|
requirement,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
api.getUninstallCommand(1, url),
|
||||||
|
/Remove installed Python distributions separately/,
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { execFileSync } = require('node:child_process');
|
||||||
|
const load = require('../helpers/load-security-module.cjs');
|
||||||
|
|
||||||
|
test('requirements install, probe and removal stay in the selected venv', (t) => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-source-venv-'));
|
||||||
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||||
|
const venv = path.join(root, 'virtual env');
|
||||||
|
const scripts = path.join(root, 'scripts');
|
||||||
|
fs.mkdirSync(scripts);
|
||||||
|
execFileSync('python3', ['-m', 'venv', venv]);
|
||||||
|
const wheel = path.join(scripts, 'ql_venv_fixture-1.0-py3-none-any.whl');
|
||||||
|
execFileSync('python3', [
|
||||||
|
'-c',
|
||||||
|
`import zipfile,sys
|
||||||
|
with zipfile.ZipFile(sys.argv[1],'w') as z:
|
||||||
|
z.writestr('ql_venv_fixture.py','value = 42')
|
||||||
|
z.writestr('ql_venv_fixture-1.0.dist-info/METADATA','Metadata-Version: 2.1\\nName: ql-venv-fixture\\nVersion: 1.0\\n')
|
||||||
|
z.writestr('ql_venv_fixture-1.0.dist-info/WHEEL','Wheel-Version: 1.0\\nGenerator: fixture\\nRoot-Is-Purelib: true\\nTag: py3-none-any\\n')
|
||||||
|
z.writestr('ql_venv_fixture-1.0.dist-info/RECORD','')`,
|
||||||
|
wheel,
|
||||||
|
]);
|
||||||
|
fs.writeFileSync(path.join(scripts, 'requirements.txt'), wheel + '\n');
|
||||||
|
const api = load('back/config/util.ts', {
|
||||||
|
'./index': {
|
||||||
|
rootPath: root,
|
||||||
|
scriptPath: scripts,
|
||||||
|
repoPath: path.join(root, 'repo'),
|
||||||
|
},
|
||||||
|
'./const': {
|
||||||
|
PYTHON_VENV_DIR: 'virtual env',
|
||||||
|
PYTHON_INSTALL_DIR: '/unused-global-prefix',
|
||||||
|
},
|
||||||
|
'../loaders/logger': {},
|
||||||
|
'../shared/utils': {},
|
||||||
|
'../data/dependence': {
|
||||||
|
DependenceTypes: { nodejs: 0, python3: 1, linux: 2 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const run = (command) =>
|
||||||
|
execFileSync('/bin/bash', ['-c', command], {
|
||||||
|
encoding: 'utf8',
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
PIP_NO_INDEX: '1',
|
||||||
|
PIP_DISABLE_PIP_VERSION_CHECK: '1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const install = api.getInstallCommand(1, 'requirements.txt');
|
||||||
|
assert.doesNotMatch(install, /--prefix=/);
|
||||||
|
run(install);
|
||||||
|
assert.match(run(api.getGetCommand(1, 'ql-venv-fixture')), /1\.0/);
|
||||||
|
assert.equal(
|
||||||
|
run(
|
||||||
|
`'${path.join(
|
||||||
|
venv,
|
||||||
|
'bin/python3',
|
||||||
|
)}' -c 'import ql_venv_fixture; print(ql_venv_fixture.value)'`,
|
||||||
|
).trim(),
|
||||||
|
'42',
|
||||||
|
);
|
||||||
|
run(api.getUninstallCommand(1, 'requirements.txt'));
|
||||||
|
assert.throws(() =>
|
||||||
|
run(`'${path.join(venv, 'bin/python3')}' -c 'import ql_venv_fixture'`),
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
const test = require('node:test'),
|
||||||
|
assert = require('node:assert/strict'),
|
||||||
|
fs = require('node:fs'),
|
||||||
|
os = require('node:os'),
|
||||||
|
path = require('node:path'),
|
||||||
|
{ execFileSync } = require('node:child_process');
|
||||||
|
const load = require('../helpers/load-security-module.cjs');
|
||||||
|
test('venv installs, uninstalls and probes use the selected Python, including spaces', () => {
|
||||||
|
const methods = load('back/config/util.ts', {
|
||||||
|
'./index': { rootPath: '/application' },
|
||||||
|
'./const': {
|
||||||
|
PYTHON_VENV_DIR: 'virtual env',
|
||||||
|
PYTHON_INSTALL_DIR: '/legacy',
|
||||||
|
},
|
||||||
|
'../loaders/logger': {},
|
||||||
|
'../shared/utils': {},
|
||||||
|
'../data/dependence': {
|
||||||
|
DependenceTypes: { nodejs: 0, python3: 1, linux: 2 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
for (const name of [
|
||||||
|
'getInstallCommand',
|
||||||
|
'getUninstallCommand',
|
||||||
|
'getGetCommand',
|
||||||
|
]) {
|
||||||
|
const cmd = methods[name](1, 'requests');
|
||||||
|
assert.match(cmd, /'\/application\/virtual env\/bin\/python3'/);
|
||||||
|
assert.doesNotMatch(cmd, /--prefix=/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('venv setup keeps Python packages isolated and retains existing startup configuration', (t) => {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-venv-'));
|
||||||
|
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||||
|
const ql = path.join(dir, 'application with spaces');
|
||||||
|
fs.mkdirSync(ql);
|
||||||
|
const venv = path.join(dir, 'venv with spaces');
|
||||||
|
execFileSync('python3', ['-m', 'venv', venv]);
|
||||||
|
const source = fs.readFileSync('shell/start-simplify.sh', 'utf8');
|
||||||
|
const snippet = source.slice(
|
||||||
|
source.indexOf('export PNPM_HOME='),
|
||||||
|
source.indexOf('\ncd "${QL_DIR}"'),
|
||||||
|
);
|
||||||
|
const result = execFileSync(
|
||||||
|
'/bin/bash',
|
||||||
|
[
|
||||||
|
'-c',
|
||||||
|
snippet +
|
||||||
|
'\npython3 -c "import sys; print(sys.prefix); print(sys.base_prefix)"',
|
||||||
|
],
|
||||||
|
{
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
QL_DIR: ql,
|
||||||
|
QL_DATA_DIR: path.join(dir, 'data'),
|
||||||
|
PYTHON_VENV_DIR: venv,
|
||||||
|
PYTHONPATH: '/legacy/modules',
|
||||||
|
PYTHONUSERBASE: '/legacy',
|
||||||
|
PATH: process.env.PATH,
|
||||||
|
},
|
||||||
|
encoding: 'utf8',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.trim()
|
||||||
|
.split('\n');
|
||||||
|
assert.equal(fs.realpathSync(result[0]), fs.realpathSync(venv));
|
||||||
|
assert.notEqual(result[1], venv);
|
||||||
|
assert.match(source, /set -C; cat .env.example > .env/);
|
||||||
|
assert.doesNotMatch(source, /npm install|pip3 install|chmod 777/);
|
||||||
|
});
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
const test = require('node:test'),
|
||||||
|
assert = require('node:assert/strict'),
|
||||||
|
fs = require('node:fs'),
|
||||||
|
path = require('node:path'),
|
||||||
|
vm = require('node:vm');
|
||||||
|
const load = require('../helpers/load-security-module.cjs');
|
||||||
|
|
||||||
|
function script(config = {}, response = {}, auditError = false) {
|
||||||
|
const logs = [],
|
||||||
|
source = fs.readFileSync('sample/notify.js', 'utf8');
|
||||||
|
const transport = async () => ({
|
||||||
|
statusCode: response.httpStatus || 200,
|
||||||
|
body: { text: async () => JSON.stringify(response) },
|
||||||
|
});
|
||||||
|
const context = {
|
||||||
|
module: { exports: {} },
|
||||||
|
console: { log() {}, info() {} },
|
||||||
|
process: {
|
||||||
|
env: { QL_DIR: '/fixture', HITOKOTO: 'false', ...config },
|
||||||
|
argv: ['node', '/fixture/scripts/test-job.js'],
|
||||||
|
},
|
||||||
|
Buffer,
|
||||||
|
URL,
|
||||||
|
setTimeout,
|
||||||
|
clearTimeout,
|
||||||
|
};
|
||||||
|
context.require = (name) => {
|
||||||
|
if (name === 'undici')
|
||||||
|
return { request: transport, ProxyAgent: class {}, FormData };
|
||||||
|
if (name === '/fixture/shell/preload/notificationAudit.js')
|
||||||
|
return async (x) => {
|
||||||
|
if (auditError) throw Error('offline');
|
||||||
|
logs.push(x);
|
||||||
|
};
|
||||||
|
if (name === 'nodemailer')
|
||||||
|
return {
|
||||||
|
createTransport: () => ({
|
||||||
|
sendMail: async () => {
|
||||||
|
if (response.smtpFailure) throw Error('auth rejected');
|
||||||
|
return { messageId: 'fixture' };
|
||||||
|
},
|
||||||
|
close() {},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
return require(name);
|
||||||
|
};
|
||||||
|
vm.runInNewContext(source, context, {
|
||||||
|
filename: path.resolve('sample/notify.js'),
|
||||||
|
});
|
||||||
|
return { send: context.module.exports.sendNotify, logs };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('JS helper logs only configured channels, success/failure and script/task provenance', async () => {
|
||||||
|
for (const fail of [false, true]) {
|
||||||
|
const s = script(
|
||||||
|
{
|
||||||
|
SMTP_EMAIL: 'sender@example.test',
|
||||||
|
SMTP_PASSWORD: 'private-password',
|
||||||
|
SMTP_SERVICE: 'fixture',
|
||||||
|
ID: '17',
|
||||||
|
},
|
||||||
|
{ smtpFailure: fail },
|
||||||
|
);
|
||||||
|
await s.send('标题', '正文');
|
||||||
|
assert.equal(s.logs.length, 1);
|
||||||
|
assert.equal(s.logs[0].success, !fail);
|
||||||
|
assert.equal(s.logs[0].notifyType, 'smtp');
|
||||||
|
assert.equal(s.logs[0].scriptName, 'test-job.js');
|
||||||
|
assert.equal(s.logs[0].taskId, '17');
|
||||||
|
assert.ok(!JSON.stringify(s.logs).includes('private-password'));
|
||||||
|
}
|
||||||
|
const s = script();
|
||||||
|
await s.send('none', 'none');
|
||||||
|
assert.equal(s.logs.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('HTTP business rejection is audited as failure and logging errors do not change sends', async () => {
|
||||||
|
for (const [code, success] of [
|
||||||
|
[200, true],
|
||||||
|
[500, false],
|
||||||
|
]) {
|
||||||
|
const s = script({ BARK_PUSH: 'https://fixture.invalid' }, { code });
|
||||||
|
await s.send('title', 'body');
|
||||||
|
assert.equal(s.logs.length, 1);
|
||||||
|
assert.equal(s.logs[0].success, success);
|
||||||
|
}
|
||||||
|
const offline = script(
|
||||||
|
{
|
||||||
|
SMTP_EMAIL: 'sender@example.test',
|
||||||
|
SMTP_PASSWORD: 'fixture',
|
||||||
|
SMTP_SERVICE: 'fixture',
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
await offline.send('still sends', 'body');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('multi-channel helpers write one result per actual channel', async () => {
|
||||||
|
const s = script(
|
||||||
|
{
|
||||||
|
GOTIFY_URL: 'https://fixture.invalid',
|
||||||
|
GOTIFY_TOKEN: 'private-token',
|
||||||
|
SMTP_EMAIL: 'sender@example.test',
|
||||||
|
SMTP_PASSWORD: 'fixture',
|
||||||
|
SMTP_SERVICE: 'fixture',
|
||||||
|
},
|
||||||
|
{ id: 123 },
|
||||||
|
);
|
||||||
|
await s.send('title', 'body');
|
||||||
|
assert.deepEqual(s.logs.map((x) => x.notifyType).sort(), ['gotify', 'smtp']);
|
||||||
|
assert.ok(s.logs.every((x) => x.success));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('script audit protobuf round trips without notification credentials', () => {
|
||||||
|
const p = load('back/protos/api.ts');
|
||||||
|
const value = p.RecordNotifyLogRequest.create({
|
||||||
|
title: '中文',
|
||||||
|
content: '内容',
|
||||||
|
success: false,
|
||||||
|
notifyType: 'smtp',
|
||||||
|
scriptName: 'job.py',
|
||||||
|
taskId: '12',
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
p.RecordNotifyLogRequest.decode(
|
||||||
|
p.RecordNotifyLogRequest.encode(value).finish(),
|
||||||
|
),
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
assert.ok(p.ApiService.recordNotifyLog);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('multi-target Chronocat records one failure if any recipient fails', async () => {
|
||||||
|
let calls = 0;
|
||||||
|
const response = {};
|
||||||
|
Object.defineProperty(response, 'httpStatus', {
|
||||||
|
get: () => (++calls === 1 ? 200 : 500),
|
||||||
|
});
|
||||||
|
const s = script(
|
||||||
|
{
|
||||||
|
CHRONOCAT_URL: 'https://fixture.invalid',
|
||||||
|
CHRONOCAT_TOKEN: 'private-token',
|
||||||
|
CHRONOCAT_QQ: 'user_id=123&group_id=456',
|
||||||
|
},
|
||||||
|
response,
|
||||||
|
);
|
||||||
|
await s.send('title', 'body');
|
||||||
|
assert.equal(calls, 2);
|
||||||
|
assert.equal(s.logs.length, 1);
|
||||||
|
assert.equal(s.logs[0].success, false);
|
||||||
|
});
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const os = require('node:os');
|
||||||
|
const { promisify } = require('node:util');
|
||||||
|
const { execFile } = require('node:child_process');
|
||||||
|
const grpc = require('@grpc/grpc-js');
|
||||||
|
const loader = require('@grpc/proto-loader');
|
||||||
|
const load = require('../helpers/load-security-module.cjs');
|
||||||
|
const run = promisify(execFile);
|
||||||
|
|
||||||
|
test('JS and Python script reports reach the same authenticated RPC with bounded failure', async (t) => {
|
||||||
|
const data = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-audit-rpc-'));
|
||||||
|
t.after(() => fs.rmSync(data, { recursive: true, force: true }));
|
||||||
|
fs.mkdirSync(path.join(data, 'config'));
|
||||||
|
const certs = await load('back/config/grpcCerts.ts', {
|
||||||
|
'./index': { configPath: path.join(data, 'config') },
|
||||||
|
'./util': { fileExist: async (p) => fs.existsSync(p) },
|
||||||
|
'../loaders/logger': { info() {}, error() {} },
|
||||||
|
}).initGrpcCerts();
|
||||||
|
const definition = loader.loadSync(path.resolve('back/protos/api.proto'), {
|
||||||
|
keepCase: true,
|
||||||
|
defaults: true,
|
||||||
|
});
|
||||||
|
const Api = grpc.loadPackageDefinition(definition).com.ql.api.Api;
|
||||||
|
const received = [];
|
||||||
|
const server = new grpc.Server();
|
||||||
|
server.addService(Api.service, {
|
||||||
|
RecordNotifyLog: (call, callback) => {
|
||||||
|
received.push(call.request);
|
||||||
|
callback(null, { code: 200 });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const credentials = grpc.ServerCredentials.createSsl(
|
||||||
|
Buffer.from(certs.caCert),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
private_key: Buffer.from(certs.serverKey),
|
||||||
|
cert_chain: Buffer.from(certs.serverCert),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const port = await new Promise((resolve, reject) =>
|
||||||
|
server.bindAsync('127.0.0.1:0', credentials, (error, port) =>
|
||||||
|
error ? reject(error) : resolve(port),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
t.after(() => server.forceShutdown());
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
QL_DIR: process.cwd(),
|
||||||
|
QL_DATA_DIR: data,
|
||||||
|
GRPC_PORT: String(port),
|
||||||
|
ID: '9',
|
||||||
|
HITOKOTO: 'false',
|
||||||
|
};
|
||||||
|
await run(
|
||||||
|
process.execPath,
|
||||||
|
[
|
||||||
|
'-e',
|
||||||
|
`require('./shell/preload/notificationAudit.js')({title:'JS task',content:'body',success:true,notifyType:'smtp',scriptName:'task.js',taskId:process.env.ID})`,
|
||||||
|
],
|
||||||
|
{ env, timeout: 8000 },
|
||||||
|
);
|
||||||
|
const python = `import importlib.util,sys,types,smtplib,os
|
||||||
|
sys.modules['requests']=types.SimpleNamespace()
|
||||||
|
spec=importlib.util.spec_from_file_location('notify',os.path.join(os.environ['QL_DIR'],'sample/notify.py'))
|
||||||
|
n=importlib.util.module_from_spec(spec);spec.loader.exec_module(n)
|
||||||
|
class Mail:
|
||||||
|
def __init__(self,*a,**kw): pass
|
||||||
|
def login(self,*a): pass
|
||||||
|
def sendmail(self,*a): return {}
|
||||||
|
def close(self): pass
|
||||||
|
smtplib.SMTP_SSL=Mail
|
||||||
|
n.push_config={'SMTP_SERVER':'fixture','SMTP_SSL':'true','SMTP_EMAIL':'sender@example.test','SMTP_PASSWORD':'private-password','SMTP_NAME':'fixture','HITOKOTO':False}
|
||||||
|
n.send('Python success','body')
|
||||||
|
def rejected(*a,**kw): raise RuntimeError('private-password')
|
||||||
|
smtplib.SMTP_SSL=rejected
|
||||||
|
n.send('Python failure','body')
|
||||||
|
`;
|
||||||
|
await run('python3', ['-c', python], { env, timeout: 12000 });
|
||||||
|
assert.deepEqual(
|
||||||
|
received.map((x) => [x.title, x.success]),
|
||||||
|
[
|
||||||
|
['JS task', true],
|
||||||
|
['Python success', true],
|
||||||
|
['Python failure', false],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert.ok(received.every((x) => x.taskId === '9'));
|
||||||
|
assert.ok(!JSON.stringify(received).includes('private-password'));
|
||||||
|
// Missing certificates and an older/unavailable backend must not kill scripts.
|
||||||
|
await run(
|
||||||
|
process.execPath,
|
||||||
|
[
|
||||||
|
'-e',
|
||||||
|
`require('./shell/preload/notificationAudit.js')({title:'offline'}).then(()=>console.log('done'))`,
|
||||||
|
],
|
||||||
|
{ env: { ...env, QL_DATA_DIR: path.join(data, 'missing') }, timeout: 8000 },
|
||||||
|
);
|
||||||
|
server.forceShutdown();
|
||||||
|
const started = Date.now();
|
||||||
|
await run(
|
||||||
|
process.execPath,
|
||||||
|
[
|
||||||
|
'-e',
|
||||||
|
`require('./shell/preload/notificationAudit.js')({title:'offline'})`,
|
||||||
|
],
|
||||||
|
{ env, timeout: 8000 },
|
||||||
|
);
|
||||||
|
assert.ok(Date.now() - started < 5000);
|
||||||
|
});
|
||||||
@@ -7,8 +7,8 @@ const { spawnSync } = require('node:child_process');
|
|||||||
|
|
||||||
// Run the actual startup prefix, stopping before service initialization.
|
// Run the actual startup prefix, stopping before service initialization.
|
||||||
const source = fs
|
const source = fs
|
||||||
.readFileSync('shell/start.sh', 'utf8')
|
.readFileSync('shell/start-simplify.sh', 'utf8')
|
||||||
.split('. ${QL_DIR}/shell/share.sh')[0];
|
.split('. "${QL_DIR}/shell/share.sh"')[0];
|
||||||
for (const command of ['start', 'reload']) {
|
for (const command of ['start', 'reload']) {
|
||||||
for (const existing of [false, true]) {
|
for (const existing of [false, true]) {
|
||||||
test(`${command} ${
|
test(`${command} ${
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ test('decoded SystemNotify requests reach WPUSH with explicit or saved configura
|
|||||||
'../config/const': constants,
|
'../config/const': constants,
|
||||||
'../config/util': {},
|
'../config/util': {},
|
||||||
'../data/dependence': {},
|
'../data/dependence': {},
|
||||||
'../data/system': {},
|
'../data/system': { AuthDataType: { notifyLog: 'notifyLog' }, NotifyStatus: { success: 0, fail: 1 }, SystemModel: { create: async () => {}, findAll: async () => [] } },
|
||||||
'../shared/pLimit': {},
|
'../shared/pLimit': {},
|
||||||
'../schedule/client': {},
|
'../schedule/client': {},
|
||||||
'../shared/i18n': { t: (x) => x },
|
'../shared/i18n': { t: (x) => x },
|
||||||
@@ -79,7 +79,7 @@ test('decoded SystemNotify requests reach WPUSH with explicit or saved configura
|
|||||||
wpushApiKey: 'saved-key',
|
wpushApiKey: 'saved-key',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
const system = new SystemService();
|
const system = new SystemService({ error() {} });
|
||||||
system.notificationService = notification;
|
system.notificationService = notification;
|
||||||
|
|
||||||
for (const info of [
|
for (const info of [
|
||||||
|
|||||||
Reference in New Issue
Block a user