mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-10 03:33:28 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ebbe17aa8 |
@@ -33,10 +33,6 @@ on:
|
|||||||
type: choice
|
type: choice
|
||||||
options: [branch, tag]
|
options: [branch, tag]
|
||||||
default: branch
|
default: branch
|
||||||
npm_rc_version:
|
|
||||||
description: Publish the verified main npm RC from develop, for example 2.25.0-rc.0
|
|
||||||
type: string
|
|
||||||
default: ''
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -474,7 +470,7 @@ jobs:
|
|||||||
retention-days: 14
|
retention-days: 14
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
if: ${{ github.repository == 'whyour/qinglong' && (github.ref == 'refs/heads/master' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/develop')) }}
|
if: ${{ github.repository == 'whyour/qinglong' && github.ref == 'refs/heads/master' }}
|
||||||
needs: [build-alpine, build-debian, verify-npm-package]
|
needs: [build-alpine, build-debian, verify-npm-package]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
@@ -495,8 +491,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish npm package with OIDC
|
- name: Publish npm package with OIDC
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
|
||||||
NPM_RC_VERSION: ${{ inputs.npm_rc_version }}
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
@@ -509,21 +503,11 @@ jobs:
|
|||||||
metadata=$(tar -xOf "$archive" package/package.json)
|
metadata=$(tar -xOf "$archive" package/package.json)
|
||||||
name=$(jq -er '.name | select(. == "@whyour/qinglong")' <<< "$metadata")
|
name=$(jq -er '.name | select(. == "@whyour/qinglong")' <<< "$metadata")
|
||||||
version=$(jq -er '.version | select(type == "string")' <<< "$metadata")
|
version=$(jq -er '.version | select(type == "string")' <<< "$metadata")
|
||||||
if [[ "${GITHUB_EVENT_NAME:-}" == workflow_dispatch && "${GITHUB_REF:-}" == refs/heads/develop ]]; then
|
|
||||||
if [[ ! "${NPM_RC_VERSION:-}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ || "$version" != "${NPM_RC_VERSION:-}" ]]; then
|
|
||||||
echo '::error::Develop npm publication requires the exact requested RC archive version.'
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if npm view "$name@$version" version --json --registry=https://registry.npmjs.org > "$RUNNER_TEMP/npm-version.json" 2> "$RUNNER_TEMP/npm-version-error.log"; then
|
if npm view "$name@$version" version --json --registry=https://registry.npmjs.org > "$RUNNER_TEMP/npm-version.json" 2> "$RUNNER_TEMP/npm-version-error.log"; then
|
||||||
jq -e --arg version "$version" '. == $version' "$RUNNER_TEMP/npm-version.json" > /dev/null
|
jq -e --arg version "$version" '. == $version' "$RUNNER_TEMP/npm-version.json" > /dev/null
|
||||||
echo "::notice::$name@$version is already published; skipping npm publication for this image rebuild."
|
echo "::notice::$name@$version is already published; skipping npm publication for this image rebuild."
|
||||||
elif jq -e '.error.code == "E404"' "$RUNNER_TEMP/npm-version.json" > /dev/null; then
|
elif jq -e '.error.code == "E404"' "$RUNNER_TEMP/npm-version.json" > /dev/null; then
|
||||||
if [[ "${GITHUB_EVENT_NAME:-}" == workflow_dispatch && "${GITHUB_REF:-}" == refs/heads/develop ]]; then
|
npm publish "./$archive" --ignore-scripts --access public --registry=https://registry.npmjs.org
|
||||||
npm publish "./$archive" --ignore-scripts --access public --tag rc --registry=https://registry.npmjs.org
|
|
||||||
else
|
|
||||||
npm publish "./$archive" --ignore-scripts --access public --registry=https://registry.npmjs.org
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
echo '::error::Could not check the published npm version; refusing to publish.'
|
echo '::error::Could not check the published npm version; refusing to publish.'
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export default (app: Router) => {
|
|||||||
res.status(500).send({
|
res.status(500).send({
|
||||||
code: 500,
|
code: 500,
|
||||||
message: 'Health check failed',
|
message: 'Health check failed',
|
||||||
|
error: err.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -310,6 +310,20 @@ export default (app: Router) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/notification/init',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const result = await userService.updateNotificationMode(req.body);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/avatar',
|
'/avatar',
|
||||||
upload.single('avatar'),
|
upload.single('avatar'),
|
||||||
|
|||||||
@@ -180,10 +180,12 @@ export default {
|
|||||||
'/api/user/two-factor/login',
|
'/api/user/two-factor/login',
|
||||||
'/api/system',
|
'/api/system',
|
||||||
'/api/user/init',
|
'/api/user/init',
|
||||||
|
'/api/user/notification/init',
|
||||||
'/open/user/login',
|
'/open/user/login',
|
||||||
'/open/user/two-factor/login',
|
'/open/user/two-factor/login',
|
||||||
'/open/system',
|
'/open/system',
|
||||||
'/open/user/init',
|
'/open/user/init',
|
||||||
|
'/open/user/notification/init',
|
||||||
],
|
],
|
||||||
versionFile,
|
versionFile,
|
||||||
lastVersionFile,
|
lastVersionFile,
|
||||||
|
|||||||
+37
-36
@@ -16,7 +16,6 @@ import { t } from '../shared/i18n';
|
|||||||
import { AppScope } from '../data/open';
|
import { AppScope } from '../data/open';
|
||||||
import uniqueConstraintError from '../middlewares/uniqueConstraintError';
|
import uniqueConstraintError from '../middlewares/uniqueConstraintError';
|
||||||
import protectedPathCase from '../middlewares/protectedPathCase';
|
import protectedPathCase from '../middlewares/protectedPathCase';
|
||||||
import Logger from './logger';
|
|
||||||
|
|
||||||
function resolveTrustProxy(value = process.env.QL_TRUST_PROXY) {
|
function resolveTrustProxy(value = process.env.QL_TRUST_PROXY) {
|
||||||
const setting = value?.trim();
|
const setting = value?.trim();
|
||||||
@@ -192,7 +191,14 @@ export default ({ app }: { app: Application }) => {
|
|||||||
|
|
||||||
app.use(async (req, res, next) => {
|
app.use(async (req, res, next) => {
|
||||||
const pathLower = req.path.toLowerCase();
|
const pathLower = req.path.toLowerCase();
|
||||||
if (!['/api/user/init', '/open/user/init'].includes(pathLower)) {
|
if (
|
||||||
|
![
|
||||||
|
'/api/user/init',
|
||||||
|
'/api/user/notification/init',
|
||||||
|
'/open/user/init',
|
||||||
|
'/open/user/notification/init',
|
||||||
|
].includes(pathLower)
|
||||||
|
) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
const authInfo = (await shareStore.getAuthInfo()) || ({} as AuthInfo);
|
const authInfo = (await shareStore.getAuthInfo()) || ({} as AuthInfo);
|
||||||
@@ -232,23 +238,37 @@ export default ({ app }: { app: Application }) => {
|
|||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
(
|
(
|
||||||
err: Error & { status: number; code?: string },
|
err: Error & { status: number },
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction,
|
next: NextFunction,
|
||||||
) => {
|
) => {
|
||||||
if (err.name === 'UnauthorizedError') {
|
if (err.name === 'UnauthorizedError') {
|
||||||
let message = err.message;
|
return res
|
||||||
if (!config.isDevelopment) {
|
.status(err.status)
|
||||||
if (err.code === 'credentials_bad_scheme') {
|
.send({ code: 401, message: err.message })
|
||||||
message = t('暂无权限');
|
.end();
|
||||||
} else if (err.code === 'credentials_required') {
|
}
|
||||||
message = t('请先登录');
|
return next(err);
|
||||||
} else {
|
},
|
||||||
message = t('Token 已失效');
|
);
|
||||||
}
|
|
||||||
}
|
app.use(
|
||||||
return res.status(401).send({ code: 401, message }).end();
|
(
|
||||||
|
err: Error & { errors: any[] },
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction,
|
||||||
|
) => {
|
||||||
|
if (err.name.includes('Sequelize')) {
|
||||||
|
return res
|
||||||
|
.status(500)
|
||||||
|
.send({
|
||||||
|
code: 400,
|
||||||
|
message: `${err.message}`,
|
||||||
|
errors: err.errors,
|
||||||
|
})
|
||||||
|
.end();
|
||||||
}
|
}
|
||||||
return next(err);
|
return next(err);
|
||||||
},
|
},
|
||||||
@@ -261,29 +281,10 @@ export default ({ app }: { app: Application }) => {
|
|||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction,
|
next: NextFunction,
|
||||||
) => {
|
) => {
|
||||||
const status =
|
res.status(err.status || 500);
|
||||||
Number.isInteger(err.status) && err.status >= 400 && err.status < 600
|
|
||||||
? err.status
|
|
||||||
: 500;
|
|
||||||
if (status >= 500) {
|
|
||||||
const route =
|
|
||||||
typeof req.route?.path === 'string' ? req.route.path : '?';
|
|
||||||
const errorName = /^[A-Za-z][A-Za-z0-9]{0,63}$/.test(err.name)
|
|
||||||
? err.name
|
|
||||||
: 'Error';
|
|
||||||
Logger.error(
|
|
||||||
'HTTP request failed: status=%d method=%s route=%s type=%s',
|
|
||||||
status,
|
|
||||||
req.method,
|
|
||||||
route,
|
|
||||||
errorName,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
res.status(status);
|
|
||||||
res.json({
|
res.json({
|
||||||
code: status,
|
code: err.status || 500,
|
||||||
message:
|
message: err.message,
|
||||||
!config.isDevelopment && status >= 500 ? t('未知错误') : err.message,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -66,16 +66,5 @@ export async function migrateSchema(database: Sequelize): Promise<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Older databases can contain non-JSON values in Envs.labels. Sequelize
|
|
||||||
// parses this column on every environment read, including HTTP startup.
|
|
||||||
await database.query(
|
|
||||||
`UPDATE "Envs" SET "labels" = '[]'
|
|
||||||
WHERE CASE WHEN json_valid("labels")
|
|
||||||
THEN json_type("labels") <> 'array'
|
|
||||||
ELSE 1
|
|
||||||
END`,
|
|
||||||
{ transaction },
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+4
-4
@@ -15,7 +15,7 @@
|
|||||||
"@types/node": "22.19.0",
|
"@types/node": "22.19.0",
|
||||||
"commander": "15.0.0",
|
"commander": "15.0.0",
|
||||||
"esbuild": "0.28.2",
|
"esbuild": "0.28.2",
|
||||||
"tar": "7.5.20",
|
"tar": "7.5.21",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.2.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -573,9 +573,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar": {
|
"node_modules/tar": {
|
||||||
"version": "7.5.20",
|
"version": "7.5.21",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.20.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.21.tgz",
|
||||||
"integrity": "sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ==",
|
"integrity": "sha512-XdhtCvlMywwxpCW8YEq3lOXBJpUPTR2OHHcwLPO3HwsJqOHa2Ok/oJ7ruGzp+JrKoRPVCzJwAdEjqLW/vNRPHA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@
|
|||||||
"esbuild": "0.28.2",
|
"esbuild": "0.28.2",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
"@types/node": "22.19.0",
|
"@types/node": "22.19.0",
|
||||||
"tar": "7.5.20"
|
"tar": "7.5.21"
|
||||||
},
|
},
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ System/user operations below execute remotely through the panel API. They do not
|
|||||||
| `ql user notification-get ` | GET | `user/notification` | --query |
|
| `ql user notification-get ` | GET | `user/notification` | --query |
|
||||||
| `ql user notification-set ` | PUT | `user/notification` | --data, --query |
|
| `ql user notification-set ` | PUT | `user/notification` | --data, --query |
|
||||||
| `ql user init ` | PUT | `user/init` | --data, --query |
|
| `ql user init ` | PUT | `user/init` | --data, --query |
|
||||||
|
| `ql user notification-init ` | PUT | `user/notification/init` | --data, --query |
|
||||||
| `ql user avatar ` | PUT | `user/avatar` | --data, --file (avatar), --query |
|
| `ql user avatar ` | PUT | `user/avatar` | --data, --file (avatar), --query |
|
||||||
| `ql system apply-reload ` | PUT | `update/reload` | --query |
|
| `ql system apply-reload ` | PUT | `update/reload` | --query |
|
||||||
| `ql system apply-system ` | PUT | `update/system` | --query |
|
| `ql system apply-system ` | PUT | `update/system` | --query |
|
||||||
|
|||||||
@@ -608,6 +608,13 @@ export const openOperations: readonly OpenOperation[] = [
|
|||||||
body: 'json',
|
body: 'json',
|
||||||
anonymous: true,
|
anonymous: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'user notification-init',
|
||||||
|
method: 'PUT',
|
||||||
|
path: 'user/notification/init',
|
||||||
|
body: 'json',
|
||||||
|
anonymous: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'user avatar',
|
name: 'user avatar',
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
|||||||
+2
-2
@@ -118,9 +118,9 @@ ENV PNPM_HOME=${QL_DIR}/data/dep_cache/node \
|
|||||||
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin:${HOME}/bin \
|
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin:${HOME}/bin \
|
||||||
NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules \
|
NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules \
|
||||||
PIP_CACHE_DIR=${PYTHON_HOME}/pip \
|
PIP_CACHE_DIR=${PYTHON_HOME}/pip \
|
||||||
PYTHONPATH=${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages:${QL_DIR}/python-base/lib/python${PYTHON_SHORT_VERSION}/site-packages
|
PYTHONPATH=${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages
|
||||||
|
|
||||||
RUN pip3 install --ignore-installed --prefix ${QL_DIR}/python-base requests
|
RUN pip3 install --prefix ${PYTHON_HOME} requests
|
||||||
|
|
||||||
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
|
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
|
||||||
|
|
||||||
|
|||||||
@@ -112,9 +112,9 @@ ENV PNPM_HOME=${QL_DIR}/data/dep_cache/node \
|
|||||||
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin:${HOME}/bin \
|
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin:${HOME}/bin \
|
||||||
NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules \
|
NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules \
|
||||||
PIP_CACHE_DIR=${PYTHON_HOME}/pip \
|
PIP_CACHE_DIR=${PYTHON_HOME}/pip \
|
||||||
PYTHONPATH=${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages:${QL_DIR}/python-base/lib/python${PYTHON_SHORT_VERSION}/site-packages
|
PYTHONPATH=${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages
|
||||||
|
|
||||||
RUN pip3 install --ignore-installed --prefix ${QL_DIR}/python-base requests
|
RUN pip3 install --prefix ${PYTHON_HOME} requests
|
||||||
|
|
||||||
COPY --chown=qinglong:qinglong --from=builder /tmp/build/node_modules/. /ql/node_modules/
|
COPY --chown=qinglong:qinglong --from=builder /tmp/build/node_modules/. /ql/node_modules/
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
# 青龙 2.25.0-rc.0 开发候选
|
|
||||||
|
|
||||||
本候选包含 2.24.0 之后的安全、依赖、编辑器、通知及环境变量修复。主 npm 包使用 `rc` 标签发布,正式 `latest` 保持 2.24.0。
|
|
||||||
|
|
||||||
## 变更
|
|
||||||
|
|
||||||
- 应用权限、首次通知配置与任务环境变量的控制边界更严格;旧数据库中的不安全环境变量标签会在启动前修复,受保护名称不会写入任务环境,服务端错误不会向客户端泄露内部细节。
|
|
||||||
- 依赖管理页面显示已保存状态,用户可以手动复查;配置与脚本编辑器可切换自动换行。
|
|
||||||
- 通知请求和线程等待设置上限,避免无响应的通知渠道拖住任务。
|
|
||||||
- Alpine 和 Debian 镜像的内置 Python 通知依赖放在数据卷之外,挂载空 `/ql/data` 后仍能发送通知。
|
|
||||||
- 独立远程 CLI 候选 `@whyour/qinglong-cli@0.1.5-rc.1` 改善跨平台配置和 OpenAPI 操作,并移除不安全的首次通知命令。
|
|
||||||
|
|
||||||
## 测试与安装
|
|
||||||
|
|
||||||
开发镜像 `whyour/qinglong:develop`、`whyour/qinglong:debian-dev` 与主包 `@whyour/qinglong@2.25.0-rc.0` 应对应同一候选源码。主包可用 `npm install @whyour/qinglong@rc` 安装;CLI 可用 `npm install @whyour/qinglong-cli@rc` 安装。
|
|
||||||
|
|
||||||
升级前备份 `/ql/data`,在目标设备验证任务、订阅、环境变量、依赖与通知。开发候选不建议覆盖正式环境。
|
|
||||||
+11
-7
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@whyour/qinglong",
|
"name": "@whyour/qinglong",
|
||||||
"packageManager": "pnpm@8.3.1",
|
"packageManager": "pnpm@8.3.1",
|
||||||
"version": "2.25.0-rc.0",
|
"version": "2.24.0",
|
||||||
"description": "Timed task management platform supporting Python3, JavaScript, Shell, Typescript",
|
"description": "Timed task management platform supporting Python3, JavaScript, Shell, Typescript",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -116,6 +116,10 @@
|
|||||||
"@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",
|
||||||
|
"body-parser": "1.20.9",
|
||||||
|
"lodash": "4.18.1",
|
||||||
|
"protobufjs": "7.6.1",
|
||||||
|
"undici@6": "6.29.1",
|
||||||
"brace-expansion@1": "1.1.21",
|
"brace-expansion@1": "1.1.21",
|
||||||
"brace-expansion@2": "2.1.7"
|
"brace-expansion@2": "2.1.7"
|
||||||
}
|
}
|
||||||
@@ -127,7 +131,7 @@
|
|||||||
"@grpc/proto-loader": "^0.8.0",
|
"@grpc/proto-loader": "^0.8.0",
|
||||||
"@keyv/sqlite": "^4.0.1",
|
"@keyv/sqlite": "^4.0.1",
|
||||||
"@otplib/preset-default": "^12.0.1",
|
"@otplib/preset-default": "^12.0.1",
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^1.20.9",
|
||||||
"celebrate": "^15.0.3",
|
"celebrate": "^15.0.3",
|
||||||
"chokidar": "^4.0.1",
|
"chokidar": "^4.0.1",
|
||||||
"compression": "1.8.2",
|
"compression": "1.8.2",
|
||||||
@@ -148,10 +152,10 @@
|
|||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"keyv": "^5.2.3",
|
"keyv": "^5.2.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.18.1",
|
||||||
"multer": "2.4.0",
|
"multer": "2.5.0",
|
||||||
"node-cron": "4.6.0",
|
"node-cron": "4.6.0",
|
||||||
"nodemailer": "^10.0.9",
|
"nodemailer": "^10.1.4",
|
||||||
"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",
|
||||||
@@ -162,11 +166,11 @@
|
|||||||
"sqlite3": "npm:@whyour/sqlite3@1.1.4",
|
"sqlite3": "npm:@whyour/sqlite3@1.1.4",
|
||||||
"toad-scheduler": "^3.0.1",
|
"toad-scheduler": "^3.0.1",
|
||||||
"typedi": "^0.10.0",
|
"typedi": "^0.10.0",
|
||||||
"undici": "^7.9.0",
|
"undici": "^7.30.1",
|
||||||
"uuid": "^11.0.3",
|
"uuid": "^11.0.3",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"winston-daily-rotate-file": "^5.0.0",
|
"winston-daily-rotate-file": "^5.0.0",
|
||||||
"tar": "7.5.20"
|
"tar": "7.5.21"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/icons": "^5.0.1",
|
"@ant-design/icons": "^5.0.1",
|
||||||
|
|||||||
Generated
+345
-542
File diff suppressed because it is too large
Load Diff
+30
-78
@@ -18,11 +18,6 @@ from email.utils import formataddr
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
# 单次 HTTP 请求的默认超时(秒),避免推送服务无响应时永久阻塞
|
|
||||||
REQUEST_TIMEOUT = 10
|
|
||||||
# 等待推送线程结束的最长时间(秒),超时后跳过并继续退出
|
|
||||||
NOTIFY_JOIN_TIMEOUT = 30
|
|
||||||
|
|
||||||
# 原先的 print 函数和主线程的锁
|
# 原先的 print 函数和主线程的锁
|
||||||
_print = print
|
_print = print
|
||||||
mutex = threading.Lock()
|
mutex = threading.Lock()
|
||||||
@@ -204,7 +199,7 @@ def bark(title: str, content: str) -> Optional[bool]:
|
|||||||
data[bark_params.get(pair[0])] = pair[1]
|
data[bark_params.get(pair[0])] = pair[1]
|
||||||
headers = {"Content-Type": "application/json;charset=utf-8"}
|
headers = {"Content-Type": "application/json;charset=utf-8"}
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
url=url, data=json.dumps(data), headers=headers, timeout=REQUEST_TIMEOUT
|
url=url, data=json.dumps(data), headers=headers, timeout=15
|
||||||
).json()
|
).json()
|
||||||
|
|
||||||
if response["code"] == 200:
|
if response["code"] == 200:
|
||||||
@@ -246,7 +241,7 @@ def dingding_bot(title: str, content: str) -> Optional[bool]:
|
|||||||
headers = {"Content-Type": "application/json;charset=utf-8"}
|
headers = {"Content-Type": "application/json;charset=utf-8"}
|
||||||
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
|
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
url=url, data=json.dumps(data), headers=headers, timeout=REQUEST_TIMEOUT
|
url=url, data=json.dumps(data), headers=headers, timeout=15
|
||||||
).json()
|
).json()
|
||||||
|
|
||||||
if not response["errcode"]:
|
if not response["errcode"]:
|
||||||
@@ -283,9 +278,7 @@ def feishu_bot(title: str, content: str) -> Optional[bool]:
|
|||||||
data["timestamp"] = timestamp
|
data["timestamp"] = timestamp
|
||||||
data["sign"] = sign
|
data["sign"] = sign
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(url, data=json.dumps(data)).json()
|
||||||
url, data=json.dumps(data), timeout=REQUEST_TIMEOUT
|
|
||||||
).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)
|
audit_results.append(True)
|
||||||
@@ -306,7 +299,7 @@ def go_cqhttp(title: str, content: str) -> Optional[bool]:
|
|||||||
print("go-cqhttp 服务启动")
|
print("go-cqhttp 服务启动")
|
||||||
|
|
||||||
url = f'{push_config.get("GOBOT_URL")}?access_token={push_config.get("GOBOT_TOKEN")}&{push_config.get("GOBOT_QQ")}&message=标题:{title}\n内容:{content}'
|
url = f'{push_config.get("GOBOT_URL")}?access_token={push_config.get("GOBOT_TOKEN")}&{push_config.get("GOBOT_QQ")}&message=标题:{title}\n内容:{content}'
|
||||||
response = requests.get(url, timeout=REQUEST_TIMEOUT).json()
|
response = requests.get(url).json()
|
||||||
|
|
||||||
if response["status"] == "ok":
|
if response["status"] == "ok":
|
||||||
audit_results.append(True)
|
audit_results.append(True)
|
||||||
@@ -332,7 +325,7 @@ def gotify(title: str, content: str) -> Optional[bool]:
|
|||||||
"message": content,
|
"message": content,
|
||||||
"priority": push_config.get("GOTIFY_PRIORITY"),
|
"priority": push_config.get("GOTIFY_PRIORITY"),
|
||||||
}
|
}
|
||||||
response = requests.post(url, data=data, timeout=REQUEST_TIMEOUT).json()
|
response = requests.post(url, data=data).json()
|
||||||
|
|
||||||
if response.get("id"):
|
if response.get("id"):
|
||||||
audit_results.append(True)
|
audit_results.append(True)
|
||||||
@@ -355,9 +348,7 @@ def iGot(title: str, content: str) -> Optional[bool]:
|
|||||||
url = f'https://push.hellyw.com/{push_config.get("IGOT_PUSH_KEY")}'
|
url = f'https://push.hellyw.com/{push_config.get("IGOT_PUSH_KEY")}'
|
||||||
data = {"title": title, "content": content}
|
data = {"title": title, "content": content}
|
||||||
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||||
response = requests.post(
|
response = requests.post(url, data=data, headers=headers).json()
|
||||||
url, data=data, headers=headers, timeout=REQUEST_TIMEOUT
|
|
||||||
).json()
|
|
||||||
|
|
||||||
if response["ret"] == 0:
|
if response["ret"] == 0:
|
||||||
audit_results.append(True)
|
audit_results.append(True)
|
||||||
@@ -386,7 +377,7 @@ def serverJ(title: str, content: str) -> Optional[bool]:
|
|||||||
else:
|
else:
|
||||||
url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send'
|
url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send'
|
||||||
|
|
||||||
response = requests.post(url, data=data, timeout=REQUEST_TIMEOUT).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)
|
audit_results.append(True)
|
||||||
@@ -415,7 +406,7 @@ def pushdeer(title: str, content: str) -> Optional[bool]:
|
|||||||
if push_config.get("DEER_URL"):
|
if push_config.get("DEER_URL"):
|
||||||
url = push_config.get("DEER_URL")
|
url = push_config.get("DEER_URL")
|
||||||
|
|
||||||
response = requests.post(url, data=data, timeout=REQUEST_TIMEOUT).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)
|
audit_results.append(True)
|
||||||
@@ -436,7 +427,7 @@ def chat(title: str, content: str) -> Optional[bool]:
|
|||||||
print("chat 服务启动")
|
print("chat 服务启动")
|
||||||
data = "payload=" + json.dumps({"text": title + "\n" + content})
|
data = "payload=" + json.dumps({"text": title + "\n" + content})
|
||||||
url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN")
|
url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN")
|
||||||
response = requests.post(url, data=data, timeout=REQUEST_TIMEOUT)
|
response = requests.post(url, data=data)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
audit_results.append(True)
|
audit_results.append(True)
|
||||||
@@ -470,9 +461,7 @@ def pushplus_bot(title: str, content: str) -> Optional[bool]:
|
|||||||
}
|
}
|
||||||
body = json.dumps(data).encode(encoding="utf-8")
|
body = json.dumps(data).encode(encoding="utf-8")
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
response = requests.post(
|
response = requests.post(url=url, data=body, headers=headers).json()
|
||||||
url=url, data=body, headers=headers, timeout=REQUEST_TIMEOUT
|
|
||||||
).json()
|
|
||||||
|
|
||||||
code = response["code"]
|
code = response["code"]
|
||||||
if code == 200:
|
if code == 200:
|
||||||
@@ -488,9 +477,7 @@ def pushplus_bot(title: str, content: str) -> Optional[bool]:
|
|||||||
else:
|
else:
|
||||||
url_old = "http://pushplus.hxtrip.com/send"
|
url_old = "http://pushplus.hxtrip.com/send"
|
||||||
headers["Accept"] = "application/json"
|
headers["Accept"] = "application/json"
|
||||||
response = requests.post(
|
response = requests.post(url=url_old, data=body, headers=headers).json()
|
||||||
url=url_old, data=body, headers=headers, timeout=REQUEST_TIMEOUT
|
|
||||||
).json()
|
|
||||||
|
|
||||||
if response["code"] == 200:
|
if response["code"] == 200:
|
||||||
audit_results.append(True)
|
audit_results.append(True)
|
||||||
@@ -526,9 +513,7 @@ def weplus_bot(title: str, content: str) -> Optional[bool]:
|
|||||||
}
|
}
|
||||||
body = json.dumps(data).encode(encoding="utf-8")
|
body = json.dumps(data).encode(encoding="utf-8")
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
response = requests.post(
|
response = requests.post(url=url, data=body, headers=headers).json()
|
||||||
url=url, data=body, headers=headers, timeout=REQUEST_TIMEOUT
|
|
||||||
).json()
|
|
||||||
|
|
||||||
if response["code"] == 200:
|
if response["code"] == 200:
|
||||||
audit_results.append(True)
|
audit_results.append(True)
|
||||||
@@ -550,9 +535,7 @@ def qmsg_bot(title: str, content: str) -> Optional[bool]:
|
|||||||
|
|
||||||
url = f'https://qmsg.zendee.cn/{push_config.get("QMSG_TYPE")}/{push_config.get("QMSG_KEY")}'
|
url = f'https://qmsg.zendee.cn/{push_config.get("QMSG_TYPE")}/{push_config.get("QMSG_KEY")}'
|
||||||
payload = {"msg": f'{title}\n\n{content.replace("----", "-")}'.encode("utf-8")}
|
payload = {"msg": f'{title}\n\n{content.replace("----", "-")}'.encode("utf-8")}
|
||||||
response = requests.post(
|
response = requests.post(url=url, params=payload).json()
|
||||||
url=url, params=payload, timeout=REQUEST_TIMEOUT
|
|
||||||
).json()
|
|
||||||
|
|
||||||
if response["code"] == 0:
|
if response["code"] == 0:
|
||||||
audit_results.append(True)
|
audit_results.append(True)
|
||||||
@@ -616,7 +599,7 @@ class WeCom:
|
|||||||
"corpid": self.CORPID,
|
"corpid": self.CORPID,
|
||||||
"corpsecret": self.CORPSECRET,
|
"corpsecret": self.CORPSECRET,
|
||||||
}
|
}
|
||||||
req = requests.post(url, params=values, timeout=REQUEST_TIMEOUT)
|
req = requests.post(url, params=values)
|
||||||
data = json.loads(req.text)
|
data = json.loads(req.text)
|
||||||
return data["access_token"]
|
return data["access_token"]
|
||||||
|
|
||||||
@@ -632,9 +615,7 @@ class WeCom:
|
|||||||
"safe": "0",
|
"safe": "0",
|
||||||
}
|
}
|
||||||
send_msges = bytes(json.dumps(send_values), "utf-8")
|
send_msges = bytes(json.dumps(send_values), "utf-8")
|
||||||
respone = requests.post(
|
respone = requests.post(send_url, send_msges)
|
||||||
send_url, send_msges, timeout=REQUEST_TIMEOUT
|
|
||||||
)
|
|
||||||
respone = respone.json()
|
respone = respone.json()
|
||||||
return respone["errmsg"]
|
return respone["errmsg"]
|
||||||
|
|
||||||
@@ -660,9 +641,7 @@ class WeCom:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
send_msges = bytes(json.dumps(send_values), "utf-8")
|
send_msges = bytes(json.dumps(send_values), "utf-8")
|
||||||
respone = requests.post(
|
respone = requests.post(send_url, send_msges)
|
||||||
send_url, send_msges, timeout=REQUEST_TIMEOUT
|
|
||||||
)
|
|
||||||
respone = respone.json()
|
respone = respone.json()
|
||||||
return respone["errmsg"]
|
return respone["errmsg"]
|
||||||
|
|
||||||
@@ -684,7 +663,7 @@ def wecom_bot(title: str, content: str) -> Optional[bool]:
|
|||||||
headers = {"Content-Type": "application/json;charset=utf-8"}
|
headers = {"Content-Type": "application/json;charset=utf-8"}
|
||||||
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
|
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
url=url, data=json.dumps(data), headers=headers, timeout=REQUEST_TIMEOUT
|
url=url, data=json.dumps(data), headers=headers, timeout=15
|
||||||
).json()
|
).json()
|
||||||
|
|
||||||
if response["errcode"] == 0:
|
if response["errcode"] == 0:
|
||||||
@@ -732,11 +711,7 @@ def telegram_bot(title: str, content: str) -> Optional[bool]:
|
|||||||
)
|
)
|
||||||
proxies = {"http": proxyStr, "https": proxyStr}
|
proxies = {"http": proxyStr, "https": proxyStr}
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
url=url,
|
url=url, headers=headers, params=payload, proxies=proxies
|
||||||
headers=headers,
|
|
||||||
params=payload,
|
|
||||||
proxies=proxies,
|
|
||||||
timeout=REQUEST_TIMEOUT,
|
|
||||||
).json()
|
).json()
|
||||||
|
|
||||||
if response["ok"]:
|
if response["ok"]:
|
||||||
@@ -777,9 +752,7 @@ def aibotk(title: str, content: str) -> Optional[bool]:
|
|||||||
}
|
}
|
||||||
body = json.dumps(data).encode(encoding="utf-8")
|
body = json.dumps(data).encode(encoding="utf-8")
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
response = requests.post(
|
response = requests.post(url=url, data=body, headers=headers).json()
|
||||||
url=url, data=body, headers=headers, timeout=REQUEST_TIMEOUT
|
|
||||||
).json()
|
|
||||||
print(response)
|
print(response)
|
||||||
if response["code"] == 0:
|
if response["code"] == 0:
|
||||||
audit_results.append(True)
|
audit_results.append(True)
|
||||||
@@ -863,7 +836,7 @@ def pushme(title: str, content: str) -> Optional[bool]:
|
|||||||
"date": push_config.get("date") if push_config.get("date") else "",
|
"date": push_config.get("date") if push_config.get("date") else "",
|
||||||
"type": push_config.get("type") if push_config.get("type") else "",
|
"type": push_config.get("type") if push_config.get("type") else "",
|
||||||
}
|
}
|
||||||
response = requests.post(url, data=data, timeout=REQUEST_TIMEOUT)
|
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)
|
audit_results.append(True)
|
||||||
@@ -910,9 +883,7 @@ def chronocat(title: str, content: str) -> Optional[bool]:
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
response = requests.post(
|
response = requests.post(url, headers=headers, data=json.dumps(data))
|
||||||
url, headers=headers, data=json.dumps(data), timeout=REQUEST_TIMEOUT
|
|
||||||
)
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
if chat_type == 1:
|
if chat_type == 1:
|
||||||
audit_results.append(True)
|
audit_results.append(True)
|
||||||
@@ -965,9 +936,7 @@ def ntfy(title: str, content: str) -> Optional[bool]:
|
|||||||
headers['Actions'] = encode_rfc2047(push_config.get("NTFY_ACTIONS"))
|
headers['Actions'] = encode_rfc2047(push_config.get("NTFY_ACTIONS"))
|
||||||
|
|
||||||
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(
|
response = requests.post(url, data=data, headers=headers)
|
||||||
url, data=data, headers=headers, timeout=REQUEST_TIMEOUT
|
|
||||||
)
|
|
||||||
if response.status_code == 200: # 使用 response.status_code 进行检查
|
if response.status_code == 200: # 使用 response.status_code 进行检查
|
||||||
audit_results.append(True)
|
audit_results.append(True)
|
||||||
print("Ntfy 推送成功!")
|
print("Ntfy 推送成功!")
|
||||||
@@ -1026,9 +995,7 @@ def wxpusher_bot(title: str, content: str) -> Optional[bool]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
response = requests.post(
|
response = requests.post(url=url, json=data, headers=headers).json()
|
||||||
url=url, json=data, headers=headers, timeout=REQUEST_TIMEOUT
|
|
||||||
).json()
|
|
||||||
|
|
||||||
if response.get("code") == 1000:
|
if response.get("code") == 1000:
|
||||||
audit_results.append(True)
|
audit_results.append(True)
|
||||||
@@ -1079,9 +1046,7 @@ def wxpusher_spt(title: str, content: str) -> Optional[bool]:
|
|||||||
data["sptList"] = spts
|
data["sptList"] = spts
|
||||||
|
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
response = requests.post(
|
response = requests.post(url=url, json=data, headers=headers).json()
|
||||||
url=url, json=data, headers=headers, timeout=REQUEST_TIMEOUT
|
|
||||||
).json()
|
|
||||||
|
|
||||||
if response.get("code") == 1000:
|
if response.get("code") == 1000:
|
||||||
audit_results.append(True)
|
audit_results.append(True)
|
||||||
@@ -1115,9 +1080,7 @@ def wpush(title: str, content: str) -> Optional[bool]:
|
|||||||
data["topic_code"] = push_config.get("WPUSH_TOPIC_CODE")
|
data["topic_code"] = push_config.get("WPUSH_TOPIC_CODE")
|
||||||
|
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
response = requests.post(
|
response = requests.post(url=url, json=data, headers=headers, timeout=15).json()
|
||||||
url=url, json=data, headers=headers, timeout=REQUEST_TIMEOUT
|
|
||||||
).json()
|
|
||||||
|
|
||||||
if response.get("code") == 0:
|
if response.get("code") == 0:
|
||||||
audit_results.append(True)
|
audit_results.append(True)
|
||||||
@@ -1158,9 +1121,7 @@ def openilink(title: str, content: str) -> Optional[bool]:
|
|||||||
if push_config.get("OPENILINK_CONTEXT_TOKEN"):
|
if push_config.get("OPENILINK_CONTEXT_TOKEN"):
|
||||||
data["context_token"] = push_config.get("OPENILINK_CONTEXT_TOKEN")
|
data["context_token"] = push_config.get("OPENILINK_CONTEXT_TOKEN")
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(url=url, json=data, headers=headers).json()
|
||||||
url=url, json=data, headers=headers, timeout=REQUEST_TIMEOUT
|
|
||||||
).json()
|
|
||||||
|
|
||||||
if response.get("ok"):
|
if response.get("ok"):
|
||||||
audit_results.append(True)
|
audit_results.append(True)
|
||||||
@@ -1254,7 +1215,7 @@ def custom_notify(title: str, content: str) -> Optional[bool]:
|
|||||||
"$title", urllib.parse.quote_plus(title)
|
"$title", urllib.parse.quote_plus(title)
|
||||||
).replace("$content", urllib.parse.quote_plus(content))
|
).replace("$content", urllib.parse.quote_plus(content))
|
||||||
response = requests.request(
|
response = requests.request(
|
||||||
method=WEBHOOK_METHOD, url=formatted_url, headers=headers, timeout=REQUEST_TIMEOUT, data=body
|
method=WEBHOOK_METHOD, url=formatted_url, headers=headers, timeout=15, data=body
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
@@ -1272,7 +1233,7 @@ def one() -> str:
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
url = "https://v1.hitokoto.cn/"
|
url = "https://v1.hitokoto.cn/"
|
||||||
res = requests.get(url, timeout=REQUEST_TIMEOUT).json()
|
res = requests.get(url).json()
|
||||||
return res["hitokoto"] + " ----" + res["from"]
|
return res["hitokoto"] + " ----" + res["from"]
|
||||||
|
|
||||||
|
|
||||||
@@ -1413,20 +1374,11 @@ def send(title: str, content: str, ignore_default_config: bool = False, **kwargs
|
|||||||
notify_function = add_notify_function()
|
notify_function = add_notify_function()
|
||||||
results = []
|
results = []
|
||||||
ts = [
|
ts = [
|
||||||
threading.Thread(
|
threading.Thread(target=audited_notification, args=(mode, title, content, results), name=mode.__name__)
|
||||||
target=audited_notification,
|
|
||||||
args=(mode, title, content, results),
|
|
||||||
name=mode.__name__,
|
|
||||||
daemon=True,
|
|
||||||
)
|
|
||||||
for mode in notify_function
|
for mode in notify_function
|
||||||
]
|
]
|
||||||
[t.start() for t in ts]
|
[t.start() for t in ts]
|
||||||
for t in ts:
|
[t.join() for t in ts]
|
||||||
t.join(timeout=NOTIFY_JOIN_TIMEOUT)
|
|
||||||
if t.is_alive():
|
|
||||||
results.append((t.name, False))
|
|
||||||
print(f"{t.name} 推送超时(>{NOTIFY_JOIN_TIMEOUT}s),已跳过,任务继续结束")
|
|
||||||
report_notifications(results, title, content)
|
report_notifications(results, title, content)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -508,7 +508,6 @@
|
|||||||
"脚本(只读)": "Scripts (read only)",
|
"脚本(只读)": "Scripts (read only)",
|
||||||
"自动删除任务": "Automatically Delete Tasks",
|
"自动删除任务": "Automatically Delete Tasks",
|
||||||
"自动拆分": "Auto Split",
|
"自动拆分": "Auto Split",
|
||||||
"自动换行": "Word wrap",
|
|
||||||
"自动添加任务": "Automatically Add Tasks",
|
"自动添加任务": "Automatically Add Tasks",
|
||||||
"自动生成": "Auto-generated",
|
"自动生成": "Auto-generated",
|
||||||
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成": "Custom log folder name to distinguish logs from different tasks. Leave blank to auto-generate",
|
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成": "Custom log folder name to distinguish logs from different tasks. Leave blank to auto-generate",
|
||||||
|
|||||||
@@ -508,7 +508,6 @@
|
|||||||
"脚本(只读)": "脚本(只读)",
|
"脚本(只读)": "脚本(只读)",
|
||||||
"自动删除任务": "自动删除任务",
|
"自动删除任务": "自动删除任务",
|
||||||
"自动拆分": "自动拆分",
|
"自动拆分": "自动拆分",
|
||||||
"自动换行": "自动换行",
|
|
||||||
"自动添加任务": "自动添加任务",
|
"自动添加任务": "自动添加任务",
|
||||||
"自动生成": "自动生成",
|
"自动生成": "自动生成",
|
||||||
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成": "自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成",
|
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成": "自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { Button, message, Modal, Space, Switch, TreeSelect } from 'antd';
|
import { Button, message, Modal, TreeSelect } from 'antd';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
import { PageContainer } from '@ant-design/pro-layout';
|
import { PageContainer } from '@ant-design/pro-layout';
|
||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
@@ -15,7 +15,6 @@ import CodeMirror from '@uiw/react-codemirror';
|
|||||||
import { useOutletContext } from '@umijs/max';
|
import { useOutletContext } from '@umijs/max';
|
||||||
import { SharedContext } from '@/layouts';
|
import { SharedContext } from '@/layouts';
|
||||||
import { langs } from '@uiw/codemirror-extensions-langs';
|
import { langs } from '@uiw/codemirror-extensions-langs';
|
||||||
import { EditorView } from '@codemirror/view';
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { getEditorMode } from '@/utils';
|
import { getEditorMode } from '@/utils';
|
||||||
|
|
||||||
@@ -29,11 +28,6 @@ const Config = () => {
|
|||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
const [language, setLanguage] = useState<string>('shell');
|
const [language, setLanguage] = useState<string>('shell');
|
||||||
const [wordWrap, setWordWrap] = useState(
|
|
||||||
() =>
|
|
||||||
(localStorage.getItem('qinglong_editor_word_wrap') ??
|
|
||||||
localStorage.getItem('qinglong_config_word_wrap')) === 'true',
|
|
||||||
);
|
|
||||||
|
|
||||||
const getConfig = (name: string) => {
|
const getConfig = (name: string) => {
|
||||||
request
|
request
|
||||||
@@ -110,20 +104,6 @@ const Config = () => {
|
|||||||
defaultValue="config.sh"
|
defaultValue="config.sh"
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
/>,
|
/>,
|
||||||
<Space key="word-wrap" size={8}>
|
|
||||||
<span>{intl.get('自动换行')}</span>
|
|
||||||
<Switch
|
|
||||||
aria-label={intl.get('自动换行')}
|
|
||||||
checked={wordWrap}
|
|
||||||
onChange={(checked) => {
|
|
||||||
setWordWrap(checked);
|
|
||||||
localStorage.setItem(
|
|
||||||
'qinglong_editor_word_wrap',
|
|
||||||
String(checked),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Space>,
|
|
||||||
<Button
|
<Button
|
||||||
key="1"
|
key="1"
|
||||||
loading={confirmLoading}
|
loading={confirmLoading}
|
||||||
@@ -141,10 +121,7 @@ const Config = () => {
|
|||||||
<CodeMirror
|
<CodeMirror
|
||||||
value={value}
|
value={value}
|
||||||
theme={theme.includes('dark') ? 'dark' : 'light'}
|
theme={theme.includes('dark') ? 'dark' : 'light'}
|
||||||
extensions={[
|
extensions={[langs.shell()]}
|
||||||
langs.shell(),
|
|
||||||
...(wordWrap ? [EditorView.lineWrapping] : []),
|
|
||||||
]}
|
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setValue(value);
|
setValue(value);
|
||||||
}}
|
}}
|
||||||
@@ -159,8 +136,7 @@ const Config = () => {
|
|||||||
lineNumbersMinChars: 3,
|
lineNumbersMinChars: 3,
|
||||||
folding: false,
|
folding: false,
|
||||||
glyphMargin: false,
|
glyphMargin: false,
|
||||||
accessibilitySupport: 'off',
|
accessibilitySupport: 'off'
|
||||||
wordWrap: wordWrap ? 'on' : 'off',
|
|
||||||
}}
|
}}
|
||||||
onMount={(editor) => {
|
onMount={(editor) => {
|
||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
|
|||||||
@@ -1,21 +1,38 @@
|
|||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { Fragment, useEffect, useState } from 'react';
|
||||||
import { Button, Input, Form, Typography, Steps } from 'antd';
|
import {
|
||||||
|
Button,
|
||||||
|
Row,
|
||||||
|
Input,
|
||||||
|
Form,
|
||||||
|
message,
|
||||||
|
Typography,
|
||||||
|
Steps,
|
||||||
|
Select,
|
||||||
|
} from 'antd';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
|
import { history } from '@umijs/max';
|
||||||
import styles from './index.less';
|
import styles from './index.less';
|
||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
|
|
||||||
|
const FormItem = Form.Item;
|
||||||
const { Step } = Steps;
|
const { Step } = Steps;
|
||||||
|
const { Option } = Select;
|
||||||
const { Link } = Typography;
|
const { Link } = Typography;
|
||||||
|
|
||||||
const Initialization = () => {
|
const Initialization = () => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [current, setCurrent] = React.useState(0);
|
const [current, setCurrent] = React.useState(0);
|
||||||
|
const [fields, setFields] = useState<any[]>([]);
|
||||||
|
|
||||||
const next = () => {
|
const next = () => {
|
||||||
setCurrent(current + 1);
|
setCurrent(current + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const prev = () => {
|
||||||
|
setCurrent(current - 1);
|
||||||
|
};
|
||||||
|
|
||||||
const submitAccountSetting = (values: any) => {
|
const submitAccountSetting = (values: any) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
request
|
request
|
||||||
@@ -31,6 +48,23 @@ const Initialization = () => {
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitNotification = (values: any) => {
|
||||||
|
setLoading(true);
|
||||||
|
request
|
||||||
|
.put(`${config.apiPrefix}user/notification/init`, values)
|
||||||
|
.then(({ code, data }) => {
|
||||||
|
if (code === 200) {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const notificationModeChange = (value: string) => {
|
||||||
|
const _fields = (config.notificationModeMap as any)[value];
|
||||||
|
setFields(_fields || []);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.removeItem(config.authKey);
|
localStorage.removeItem(config.authKey);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -61,6 +95,53 @@ const Initialization = () => {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: intl.get('通知设置'),
|
||||||
|
content: (
|
||||||
|
<Form onFinish={submitNotification} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
label={intl.get('通知方式')}
|
||||||
|
name="type"
|
||||||
|
rules={[{ required: true, message: intl.get('请选择通知方式') }]}
|
||||||
|
style={{ maxWidth: 350 }}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
onChange={notificationModeChange}
|
||||||
|
placeholder={intl.get('请选择通知方式')}
|
||||||
|
>
|
||||||
|
{config.notificationModes
|
||||||
|
.filter((x) => x.value !== 'closed')
|
||||||
|
.map((x) => (
|
||||||
|
<Option key={x.value} value={x.value}>
|
||||||
|
{x.label}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
{fields.map((x) => (
|
||||||
|
<Form.Item
|
||||||
|
key={x.label}
|
||||||
|
label={x.label}
|
||||||
|
name={x.label}
|
||||||
|
extra={x.tip}
|
||||||
|
rules={[{ required: x.required }]}
|
||||||
|
style={{ maxWidth: 400 }}
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
autoSize={{ minRows: 1, maxRows: 5 }}
|
||||||
|
placeholder={`${intl.get('请输入')} ${x.label}`}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
<Button type="primary" htmlType="submit" loading={loading}>
|
||||||
|
{intl.get('保存')}
|
||||||
|
</Button>
|
||||||
|
<Button type="link" htmlType="button" onClick={() => next()}>
|
||||||
|
{intl.get('跳过')}
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: intl.get('账户设置'),
|
title: intl.get('账户设置'),
|
||||||
content: (
|
content: (
|
||||||
|
|||||||
@@ -6,16 +6,7 @@ import React, {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useReducer,
|
useReducer,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import {
|
import { Drawer, Button, Tabs, Badge, Select, TreeSelect } from 'antd';
|
||||||
Drawer,
|
|
||||||
Button,
|
|
||||||
Tabs,
|
|
||||||
Badge,
|
|
||||||
Select,
|
|
||||||
Space,
|
|
||||||
Switch,
|
|
||||||
TreeSelect,
|
|
||||||
} from 'antd';
|
|
||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
import SplitPane from 'react-split-pane';
|
import SplitPane from 'react-split-pane';
|
||||||
@@ -33,15 +24,11 @@ const EditModal = ({
|
|||||||
treeData,
|
treeData,
|
||||||
currentNode,
|
currentNode,
|
||||||
content,
|
content,
|
||||||
wordWrap,
|
|
||||||
onWordWrapChange,
|
|
||||||
handleCancel,
|
handleCancel,
|
||||||
}: {
|
}: {
|
||||||
treeData?: any;
|
treeData?: any;
|
||||||
content?: string;
|
content?: string;
|
||||||
currentNode: any;
|
currentNode: any;
|
||||||
wordWrap: boolean;
|
|
||||||
onWordWrapChange: (checked: boolean) => void;
|
|
||||||
handleCancel: () => void;
|
handleCancel: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
@@ -232,16 +219,6 @@ const EditModal = ({
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
width={'100%'}
|
width={'100%'}
|
||||||
extra={
|
|
||||||
<Space size={8}>
|
|
||||||
<span>{intl.get('自动换行')}</span>
|
|
||||||
<Switch
|
|
||||||
aria-label={intl.get('自动换行')}
|
|
||||||
checked={wordWrap}
|
|
||||||
onChange={onWordWrapChange}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
headerStyle={{ padding: '11px 24px' }}
|
headerStyle={{ padding: '11px 24px' }}
|
||||||
onClose={cancel}
|
onClose={cancel}
|
||||||
open={true}
|
open={true}
|
||||||
@@ -264,7 +241,6 @@ const EditModal = ({
|
|||||||
lineNumbersMinChars: 3,
|
lineNumbersMinChars: 3,
|
||||||
glyphMargin: false,
|
glyphMargin: false,
|
||||||
accessibilitySupport: 'off',
|
accessibilitySupport: 'off',
|
||||||
wordWrap: wordWrap ? 'on' : 'off',
|
|
||||||
}}
|
}}
|
||||||
onMount={(editor) => {
|
onMount={(editor) => {
|
||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { PageContainer } from '@ant-design/pro-layout';
|
import { PageContainer } from '@ant-design/pro-layout';
|
||||||
import { EditorView } from '@codemirror/view';
|
|
||||||
import Editor from '@monaco-editor/react';
|
import Editor from '@monaco-editor/react';
|
||||||
import CodeMirror from '@uiw/react-codemirror';
|
import CodeMirror from '@uiw/react-codemirror';
|
||||||
import { history, useOutletContext } from '@umijs/max';
|
import { history, useOutletContext } from '@umijs/max';
|
||||||
@@ -27,8 +26,6 @@ import {
|
|||||||
MenuProps,
|
MenuProps,
|
||||||
message,
|
message,
|
||||||
Modal,
|
Modal,
|
||||||
Space,
|
|
||||||
Switch,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Tree,
|
Tree,
|
||||||
TreeSelect,
|
TreeSelect,
|
||||||
@@ -73,11 +70,6 @@ const Script = () => {
|
|||||||
const [showMonaco, setShowMonaco] = useState(true);
|
const [showMonaco, setShowMonaco] = useState(true);
|
||||||
const [historyVisible, setHistoryVisible] = useState(false);
|
const [historyVisible, setHistoryVisible] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [wordWrap, setWordWrap] = useState(
|
|
||||||
() =>
|
|
||||||
(localStorage.getItem('qinglong_editor_word_wrap') ??
|
|
||||||
localStorage.getItem('qinglong_config_word_wrap')) === 'true',
|
|
||||||
);
|
|
||||||
const savingRef = useRef(false);
|
const savingRef = useRef(false);
|
||||||
const savedContentRef = useRef('');
|
const savedContentRef = useRef('');
|
||||||
const currentKeyRef = useRef(currentNode?.key);
|
const currentKeyRef = useRef(currentNode?.key);
|
||||||
@@ -552,11 +544,6 @@ const Script = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeWordWrap = (checked: boolean) => {
|
|
||||||
setWordWrap(checked);
|
|
||||||
localStorage.setItem('qinglong_editor_word_wrap', String(checked));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
className="ql-container-wrapper log-wrapper"
|
className="ql-container-wrapper log-wrapper"
|
||||||
@@ -580,14 +567,6 @@ const Script = () => {
|
|||||||
}
|
}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
extra={[
|
extra={[
|
||||||
<Space key="word-wrap" size={8}>
|
|
||||||
<span>{intl.get('自动换行')}</span>
|
|
||||||
<Switch
|
|
||||||
aria-label={intl.get('自动换行')}
|
|
||||||
checked={wordWrap}
|
|
||||||
onChange={changeWordWrap}
|
|
||||||
/>
|
|
||||||
</Space>,
|
|
||||||
...(currentNode?.type === 'file' && showMonaco
|
...(currentNode?.type === 'file' && showMonaco
|
||||||
? [
|
? [
|
||||||
<Button
|
<Button
|
||||||
@@ -755,7 +734,6 @@ const Script = () => {
|
|||||||
lineNumbersMinChars: 3,
|
lineNumbersMinChars: 3,
|
||||||
glyphMargin: false,
|
glyphMargin: false,
|
||||||
accessibilitySupport: 'off',
|
accessibilitySupport: 'off',
|
||||||
wordWrap: wordWrap ? 'on' : 'off',
|
|
||||||
}}
|
}}
|
||||||
onMount={(editor) => {
|
onMount={(editor) => {
|
||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
@@ -769,10 +747,7 @@ const Script = () => {
|
|||||||
{isPhone && (
|
{isPhone && (
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
value={value}
|
value={value}
|
||||||
extensions={[
|
extensions={getCodeMirrorExtensions(mode)}
|
||||||
...getCodeMirrorExtensions(mode),
|
|
||||||
...(wordWrap ? [EditorView.lineWrapping] : []),
|
|
||||||
]}
|
|
||||||
theme={theme.includes('dark') ? 'dark' : 'light'}
|
theme={theme.includes('dark') ? 'dark' : 'light'}
|
||||||
readOnly={!isEditing || saving}
|
readOnly={!isEditing || saving}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
@@ -785,8 +760,6 @@ const Script = () => {
|
|||||||
treeData={data}
|
treeData={data}
|
||||||
currentNode={currentNode}
|
currentNode={currentNode}
|
||||||
content={value}
|
content={value}
|
||||||
wordWrap={wordWrap}
|
|
||||||
onWordWrapChange={changeWordWrap}
|
|
||||||
handleCancel={() => {
|
handleCancel={() => {
|
||||||
setIsLogModalVisible(false);
|
setIsLogModalVisible(false);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ const apiWhiteList = [
|
|||||||
`${config.baseUrl}api/user/two-factor/login`,
|
`${config.baseUrl}api/user/two-factor/login`,
|
||||||
`${config.baseUrl}api/system`,
|
`${config.baseUrl}api/system`,
|
||||||
`${config.baseUrl}api/user/init`,
|
`${config.baseUrl}api/user/init`,
|
||||||
|
`${config.baseUrl}api/user/notification/init`,
|
||||||
];
|
];
|
||||||
|
|
||||||
_request.interceptors.request.use((_config) => {
|
_request.interceptors.request.use((_config) => {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ test('unknown API requests return JSON 404 while env.js and SPA deep links work'
|
|||||||
const app = express();
|
const app = express();
|
||||||
load('back/loaders/express.ts', {
|
load('back/loaders/express.ts', {
|
||||||
'../api': () => express.Router(),
|
'../api': () => express.Router(),
|
||||||
'./logger': { error() {} },
|
|
||||||
'../config': {
|
'../config': {
|
||||||
rootPath: dir,
|
rootPath: dir,
|
||||||
uploadPath: dir,
|
uploadPath: dir,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ const test = require('node:test');
|
|||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const util = require('node:util');
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const load = require('../helpers/load-security-module.cjs');
|
const load = require('../helpers/load-security-module.cjs');
|
||||||
@@ -106,8 +105,6 @@ test('HTTP authentication protects init, scopes, expired sessions and config sec
|
|||||||
let envWriteCalls = 0;
|
let envWriteCalls = 0;
|
||||||
let scopedMutationCalls = 0;
|
let scopedMutationCalls = 0;
|
||||||
let initialized = 0;
|
let initialized = 0;
|
||||||
let notificationCalls = 0;
|
|
||||||
const loggedErrors = [];
|
|
||||||
const User = class {};
|
const User = class {};
|
||||||
const Script = class {
|
const Script = class {
|
||||||
getFile(folder, filename) {
|
getFile(folder, filename) {
|
||||||
@@ -124,10 +121,6 @@ test('HTTP authentication protects init, scopes, expired sessions and config sec
|
|||||||
return { code: 200 };
|
return { code: 200 };
|
||||||
},
|
},
|
||||||
getAuthInfo: async () => auth,
|
getAuthInfo: async () => auth,
|
||||||
updateNotificationMode: async () => {
|
|
||||||
notificationCalls++;
|
|
||||||
return { code: 200 };
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
const mocks = {
|
const mocks = {
|
||||||
'../config': config,
|
'../config': config,
|
||||||
@@ -143,7 +136,6 @@ test('HTTP authentication protects init, scopes, expired sessions and config sec
|
|||||||
shareStore: { getAuthInfo: async () => auth, getApps: async () => apps },
|
shareStore: { getAuthInfo: async () => auth, getApps: async () => apps },
|
||||||
},
|
},
|
||||||
'../config/serverEnv': { serveEnv: (_req, res) => res.end() },
|
'../config/serverEnv': { serveEnv: (_req, res) => res.end() },
|
||||||
'./logger': { error: (...args) => loggedErrors.push(util.format(...args)) },
|
|
||||||
'../services/user': User,
|
'../services/user': User,
|
||||||
'../services/script': Script,
|
'../services/script': Script,
|
||||||
'../data/open': {},
|
'../data/open': {},
|
||||||
@@ -217,17 +209,6 @@ test('HTTP authentication protects init, scopes, expired sessions and config sec
|
|||||||
commandCalls++;
|
commandCalls++;
|
||||||
res.json({ code: 200 });
|
res.json({ code: 200 });
|
||||||
});
|
});
|
||||||
router.get('/security-test/error', (_req, _res, next) =>
|
|
||||||
next(new Error('sensitive server detail')),
|
|
||||||
);
|
|
||||||
router.get('/security-test/db-error', (_req, _res, next) => {
|
|
||||||
const error = new Error('sensitive database detail');
|
|
||||||
error.name = 'SequelizeDatabaseError';
|
|
||||||
error.errors = [{ message: 'sensitive row value' }];
|
|
||||||
error.sql = "SELECT * FROM users WHERE password = 'sensitive password'";
|
|
||||||
error.parameters = ['sensitive parameter'];
|
|
||||||
next(error);
|
|
||||||
});
|
|
||||||
return router;
|
return router;
|
||||||
};
|
};
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -257,39 +238,6 @@ test('HTTP authentication protects init, scopes, expired sessions and config sec
|
|||||||
);
|
);
|
||||||
return { status: response.status, body: await response.json() };
|
return { status: response.status, body: await response.json() };
|
||||||
};
|
};
|
||||||
auth.username = 'admin';
|
|
||||||
auth.password = 'admin';
|
|
||||||
for (const prefix of ['/api', '/open', '/panel/api', '/panel/open']) {
|
|
||||||
const response = await request(
|
|
||||||
`${prefix}/user/notification/init`,
|
|
||||||
undefined,
|
|
||||||
'PUT',
|
|
||||||
{ type: 'webhook', webhookUrl: 'http://127.0.0.1/?title=$title' },
|
|
||||||
);
|
|
||||||
assert.equal(response.status, 401, prefix);
|
|
||||||
}
|
|
||||||
assert.equal(notificationCalls, 0);
|
|
||||||
auth.username = 'owner';
|
|
||||||
auth.password = 'configured';
|
|
||||||
assert.equal(
|
|
||||||
(await request('/api/user/notification', valid, 'PUT', {})).body.code,
|
|
||||||
200,
|
|
||||||
);
|
|
||||||
assert.equal(notificationCalls, 1);
|
|
||||||
for (const route of ['/security-test/error', '/security-test/db-error']) {
|
|
||||||
const response = await request(`/api${route}`, valid);
|
|
||||||
assert.equal(response.status, 500);
|
|
||||||
assert.deepEqual(response.body, { code: 500, message: '未知错误' });
|
|
||||||
}
|
|
||||||
assert.equal(loggedErrors.length, 2);
|
|
||||||
assert.match(loggedErrors[1], /type=SequelizeDatabaseError/);
|
|
||||||
assert.ok(loggedErrors.every((line) => !line.includes('sensitive')));
|
|
||||||
const malformedToken = await request('/api/envs', 'not-a-jwt');
|
|
||||||
assert.equal(malformedToken.status, 401);
|
|
||||||
assert.deepEqual(malformedToken.body, {
|
|
||||||
code: 401,
|
|
||||||
message: 'Token 已失效',
|
|
||||||
});
|
|
||||||
for (const url of [
|
for (const url of [
|
||||||
'/api/user/init',
|
'/api/user/init',
|
||||||
'/open/user/init',
|
'/open/user/init',
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
const test = require('node:test'),
|
|
||||||
assert = require('node:assert/strict'),
|
|
||||||
fs = require('node:fs'),
|
|
||||||
path = require('node:path'),
|
|
||||||
{ spawnSync } = require('node:child_process');
|
|
||||||
|
|
||||||
const notifyFile = path.resolve(
|
|
||||||
process.env.NOTIFY_PY_PATH || 'sample/notify.py',
|
|
||||||
);
|
|
||||||
const source = fs.readFileSync(notifyFile, 'utf8');
|
|
||||||
|
|
||||||
function httpCalls(text) {
|
|
||||||
const calls = [];
|
|
||||||
const pattern = /requests\.(?:get|post|request)\s*\(/g;
|
|
||||||
let match;
|
|
||||||
while ((match = pattern.exec(text))) {
|
|
||||||
let depth = 0;
|
|
||||||
let end = match.index + match[0].length - 1;
|
|
||||||
for (; end < text.length; end++) {
|
|
||||||
if (text[end] === '(') depth++;
|
|
||||||
else if (text[end] === ')' && --depth === 0) break;
|
|
||||||
}
|
|
||||||
calls.push(text.slice(match.index, end + 1));
|
|
||||||
}
|
|
||||||
return calls;
|
|
||||||
}
|
|
||||||
|
|
||||||
test('sample/notify.py sends every HTTP request with a timeout', () => {
|
|
||||||
const calls = httpCalls(source);
|
|
||||||
assert.ok(
|
|
||||||
calls.length >= 27,
|
|
||||||
`expected at least 27 HTTP calls, got ${calls.length}`,
|
|
||||||
);
|
|
||||||
const missing = calls.filter((call) => !/\btimeout\s*=/.test(call));
|
|
||||||
assert.deepEqual(missing, []);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('notify.send returns after a bounded wait when a channel hangs', () => {
|
|
||||||
const script = `
|
|
||||||
import os, sys, time, types
|
|
||||||
requests = types.ModuleType("requests")
|
|
||||||
requests.get = requests.post = requests.request = lambda *a, **k: None
|
|
||||||
sys.modules["requests"] = requests
|
|
||||||
sys.path.insert(0, ${JSON.stringify(path.dirname(notifyFile))})
|
|
||||||
os.environ.pop("QL_DIR", None)
|
|
||||||
import notify
|
|
||||||
notify.NOTIFY_JOIN_TIMEOUT = 1
|
|
||||||
notify.push_config = {key: "" for key in notify.push_config}
|
|
||||||
notify.push_config["HITOKOTO"] = False
|
|
||||||
notify.push_config["CONSOLE"] = True
|
|
||||||
def hang(*args, **kwargs):
|
|
||||||
time.sleep(60)
|
|
||||||
notify.console = hang
|
|
||||||
start = time.monotonic()
|
|
||||||
notify.send("title", "content")
|
|
||||||
elapsed = time.monotonic() - start
|
|
||||||
assert elapsed < 5, elapsed
|
|
||||||
print("elapsed=%.2f" % elapsed)
|
|
||||||
`;
|
|
||||||
const python = process.platform === 'win32' ? 'python' : 'python3';
|
|
||||||
const result = spawnSync(python, ['-'], {
|
|
||||||
input: script,
|
|
||||||
encoding: 'utf8',
|
|
||||||
timeout: 15000,
|
|
||||||
env: { ...process.env, QL_DIR: '' },
|
|
||||||
});
|
|
||||||
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
||||||
assert.match(result.stdout, /elapsed=/);
|
|
||||||
});
|
|
||||||
@@ -66,9 +66,6 @@ case 'network': console.log('upstream unavailable'); process.exit(1);
|
|||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
GITHUB_EVENT_NAME: 'push',
|
|
||||||
GITHUB_REF: 'refs/heads/master',
|
|
||||||
NPM_RC_VERSION: '',
|
|
||||||
PATH: `${bin}${path.delimiter}${process.env.PATH}`,
|
PATH: `${bin}${path.delimiter}${process.env.PATH}`,
|
||||||
RUNNER_TEMP: root,
|
RUNNER_TEMP: root,
|
||||||
QL_PUBLISH_SCENARIO: scenario,
|
QL_PUBLISH_SCENARIO: scenario,
|
||||||
|
|||||||
@@ -52,19 +52,6 @@ test('health uses actual readiness and returns HTTP 503 until recovery',async(t)
|
|||||||
assert.equal(r.status,expected);assert.equal(b.code,expected);assert.equal(b.data.services.grpc,ready);
|
assert.equal(r.status,expected);assert.equal(b.code,expected);assert.equal(b.data.services.grpc,ready);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
test('health failure does not reveal internal error details',async(t)=>{
|
|
||||||
const express=require('express');
|
|
||||||
const HealthService=class{};
|
|
||||||
const app=express();load('back/api/health.ts',{
|
|
||||||
typedi:{get:()=>({check:async()=>{throw Error('sensitive health detail');}})},
|
|
||||||
'../services/health':{HealthService},'../loaders/logger':{error(){}},
|
|
||||||
}).default(app);
|
|
||||||
const server=app.listen(0,'127.0.0.1');await new Promise(r=>server.once('listening',r));
|
|
||||||
t.after(()=>new Promise(r=>{server.close(r);server.closeAllConnections();}));
|
|
||||||
const response=await fetch(`http://127.0.0.1:${server.address().port}/health`);
|
|
||||||
assert.equal(response.status,500);
|
|
||||||
assert.deepEqual(await response.json(),{code:500,message:'Health check failed'});
|
|
||||||
});
|
|
||||||
test('recovery registration errors propagate while ordinary autosave retains file synchronization',async()=>{
|
test('recovery registration errors propagate while ordinary autosave retains file synchronization',async()=>{
|
||||||
const source=fs.readFileSync('back/services/cron.ts','utf8');const a=source.indexOf(' public async autosave_crontab('),z=source.indexOf(' public async bootTask',a);
|
const source=fs.readFileSync('back/services/cron.ts','utf8');const a=source.indexOf(' public async autosave_crontab('),z=source.indexOf(' public async bootTask',a);
|
||||||
const js=ts.transpileModule('class Fixture {\n'+source.slice(a,z)+'}\nmodule.exports=Fixture;', {compilerOptions:{target:ts.ScriptTarget.ES2020}}).outputText;
|
const js=ts.transpileModule('class Fixture {\n'+source.slice(a,z)+'}\nmodule.exports=Fixture;', {compilerOptions:{target:ts.ScriptTarget.ES2020}}).outputText;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const test = require('node:test');
|
|||||||
const fs = require('node:fs/promises');
|
const fs = require('node:fs/promises');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
const { Sequelize, DataTypes, QueryTypes } = require('sequelize');
|
const { Sequelize, QueryTypes } = require('sequelize');
|
||||||
const { migrateSchema } = require('../../back/shared/schemaMigrations');
|
const { migrateSchema } = require('../../back/shared/schemaMigrations');
|
||||||
const load = require('../helpers/load-security-module.cjs');
|
const load = require('../helpers/load-security-module.cjs');
|
||||||
|
|
||||||
@@ -46,39 +46,6 @@ test('legacy database upgrades without losing rows and repeated migration is ide
|
|||||||
assert.equal(applied.length, 15);
|
assert.equal(applied.length, 15);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('migration repairs malformed environment labels before ORM reads', async (t) => {
|
|
||||||
const database = await createLegacy();
|
|
||||||
t.after(() => database.close());
|
|
||||||
await migrateSchema(database);
|
|
||||||
for (const [id, labels] of [
|
|
||||||
[1, ''],
|
|
||||||
[2, null],
|
|
||||||
[3, '{invalid'],
|
|
||||||
[4, '{}'],
|
|
||||||
[5, '["keep"]'],
|
|
||||||
]) {
|
|
||||||
await database.query('INSERT INTO "Envs" (id, name, labels) VALUES (?, ?, ?)', {
|
|
||||||
replacements: [id, `env-${id}`, labels],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const EnvModel = database.define('Env', {
|
|
||||||
name: DataTypes.STRING,
|
|
||||||
labels: DataTypes.JSON,
|
|
||||||
}, { timestamps: false });
|
|
||||||
await assert.rejects(EnvModel.findAll(), /Unexpected end of JSON input/);
|
|
||||||
await migrateSchema(database);
|
|
||||||
const rows = await EnvModel.findAll({ order: [['id', 'ASC']] });
|
|
||||||
assert.deepEqual(rows.map((row) => row.labels), [[], [], [], [], ['keep']]);
|
|
||||||
await migrateSchema(database);
|
|
||||||
const repaired = await database.query('SELECT id, labels FROM "Envs" ORDER BY id', {
|
|
||||||
type: QueryTypes.SELECT,
|
|
||||||
});
|
|
||||||
assert.deepEqual(repaired.map(({ labels }) => labels), [
|
|
||||||
'[]', '[]', '[]', '[]', '["keep"]',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('migration failure rolls back added columns and can be retried after repair', async (t) => {
|
test('migration failure rolls back added columns and can be retried after repair', async (t) => {
|
||||||
const database = await createLegacy(':memory:', true);
|
const database = await createLegacy(':memory:', true);
|
||||||
t.after(() => database.close());
|
t.after(() => database.close());
|
||||||
|
|||||||
+11
-9
@@ -1,10 +1,12 @@
|
|||||||
version: 2.25.0-rc.0
|
version: 2.24.0
|
||||||
changeLogLink: https://github.com/whyour/qinglong/blob/develop/docs/releases/2.25.0-rc.0.md
|
changeLogLink: https://github.com/whyour/qinglong/blob/v2.24.0/docs/releases/2.24.0.md
|
||||||
publishTime: "2026-11-10 01:16:12"
|
publishTime: "2026-10-29 02:14:58"
|
||||||
changeLog: |
|
changeLog: |
|
||||||
1. 收紧应用权限、首次通知配置与任务环境变量边界;启动前修复不安全的环境变量标签
|
1. Alpine 与 Debian 默认镜像统一使用 Node.js 24,SQLite 依赖升级至 1.1.4
|
||||||
2. 依赖管理改为按需复查状态,配置与脚本编辑器新增自动换行开关
|
2. 新增 Node.js 原生部署的 Python venv 隔离支持,以及 Python Git URL、requirements、pyproject 依赖源
|
||||||
3. 通知请求及线程等待增加超时,避免无响应渠道阻塞任务
|
3. 新增通知审计日志,完善通知失败记录与配置提示
|
||||||
4. 修复挂载空数据卷后 Python 通知缺少 requests 依赖的问题
|
4. 升级包按平台构建并校验;支持在线暂存、离线安装、失败回退及保留数据迁移
|
||||||
5. 独立远程 CLI 候选版本为 0.1.5-rc.1,改善跨平台配置与 OpenAPI 命令
|
5. 修复任务调度、额外规则、依赖安装、脚本发现和订阅同步等问题
|
||||||
6. 此版本为开发候选;升级前备份数据并在目标设备验证任务、依赖、通知和应用权限
|
6. 独立远程 CLI 更新至 0.1.3,最低 Node.js 版本为 22.12.0
|
||||||
|
7. Alpine 保留 arm/v6 和 arm/v7;Debian 暂不提供 arm/v7,Python 3.10 兼容镜像停止更新
|
||||||
|
8. 升级前备份完整数据,并在目标设备验证脚本、依赖、订阅和通知
|
||||||
|
|||||||
Reference in New Issue
Block a user