diff --git a/.env.example b/.env.example index 0832167e..f86264af 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,11 @@ GRPC_PORT=5500 BACK_PORT=5700 +# 服务绑定地址,默认 ::(IPv6 通配,双栈系统同时支持 IPv4/IPv6) +# 纯 IPv4 环境自动 fallback 到 0.0.0.0,也可手动指定 +# BIND_HOST=0.0.0.0 +# BIND_HOST_GRPC=0.0.0.0 + LOG_LEVEL='info' JWT_SECRET= diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index 63c42609..5630916c 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -11,6 +11,9 @@ on: - "v*" workflow_dispatch: +permissions: + contents: read + jobs: code_gitlab: runs-on: ubuntu-latest @@ -33,26 +36,51 @@ jobs: - uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: Yikun/hub-mirror-action@master - with: - src: github/whyour - dst: gitee/whyour - dst_key: ${{ secrets.GITLAB_SSH_PK }} - dst_token: ${{ secrets.GITEE_TOKEN }} - static_list: "qinglong" - force_update: true + - name: Setup SSH and push to Gitee + env: + GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }} + run: | + set +e + mkdir -p ~/.ssh + printf '%s\n' "${{ secrets.GITLAB_SSH_PK }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -T 10 gitee.com >> ~/.ssh/known_hosts 2>/dev/null + git remote add gitee git@gitee.com:whyour/qinglong.git 2>/dev/null + + if git push --force --all gitee 2>&1; then + echo "::notice::Gitee push --all succeeded" + else + echo "::warning::Push --all failed, trying to create repo via API..." + curl -sS --connect-timeout 30 --max-time 60 \ + -X POST "https://gitee.com/api/v5/user/repos" \ + -H "Content-Type: application/json" \ + -d '{"name":"qinglong","private":"false"}' \ + "?access_token=$GITEE_TOKEN" 2>/dev/null + if git push --force --all gitee 2>&1; then + echo "::notice::Gitee push --all succeeded after repo creation" + else + echo "::warning::Gitee push --all failed after retry" + fi + fi + + if git push --force --tags gitee 2>&1; then + echo "::notice::Gitee push --tags succeeded" + else + echo "::warning::Gitee push --tags failed" + fi build-static: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v6 with: version: "8.3.1" - uses: actions/setup-node@v6 with: cache: "pnpm" + cache-dependency-path: pnpm-lock.yaml - name: build front and back run: | @@ -64,9 +92,6 @@ jobs: env: GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static GITHUB_BRANCH: ${{ github.ref_name }} - REPO_GITEE: git@gitee.com:whyour/qinglong-static.git - REPO_GITLAB: git@gitlab.com:whyour/qinglong-static.git - PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }} run: | mkdir -p tmp cd ./tmp @@ -97,57 +122,53 @@ jobs: needs: build-static runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - uses: Yikun/hub-mirror-action@master - with: - src: github/whyour - dst: gitee/whyour - dst_key: ${{ secrets.GITLAB_SSH_PK }} - dst_token: ${{ secrets.GITEE_TOKEN }} - static_list: "qinglong-static" - force_update: true + - name: Setup SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.GITLAB_SSH_PK }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan github.com gitee.com >> ~/.ssh/known_hosts + - name: Mirror qinglong-static to Gitee + run: | + git clone --mirror https://github.com/whyour/qinglong-static.git static-mirror + cd static-mirror + git remote set-url origin git@gitee.com:whyour/qinglong-static.git + git push --force --mirror - build: + build-alpine: if: ${{ !startsWith(github.ref, 'refs/tags/') }} needs: build-static - runs-on: ubuntu-22.04 - permissions: packages: write contents: read - steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v6 with: version: "8.3.1" - uses: actions/setup-node@v6 with: cache: "pnpm" + cache-dependency-path: pnpm-lock.yaml - name: Read version from version.yaml id: version run: | VERSION=$(grep '^version:' version.yaml | awk '{print $2}') echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Version: $VERSION" - name: Setup timezone - uses: szenius/set-timezone@v2.0 - with: - timezoneLinux: Asia/Shanghai + run: sudo timedatectl set-timezone Asia/Shanghai - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Login to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -155,7 +176,7 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: | ${{ github.repository }} @@ -164,109 +185,256 @@ jobs: latest=false tags: | type=ref,event=branch,enable=${{ github.ref == format('refs/heads/{0}', 'develop') }} - type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} + type=ref,event=branch,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} type=raw,value=${{ steps.version.outputs.version }},enable=${{ github.ref == format('refs/heads/{0}', 'master') }} type=semver,pattern={{version}} - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - - name: Build and push - id: docker_build - uses: docker/build-push-action@v6 + - name: Build and push (Alpine) + uses: docker/build-push-action@v7 with: build-args: | MAINTAINER=${{ github.repository_owner }} QL_BRANCH=${{ github.ref_name }} SOURCE_COMMIT=${{ github.sha }} network: host - # linux/s390x npm 暂不可用 platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/386 context: . file: ./docker/Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - cache-from: type=registry,ref=whyour/qinglong:cache - cache-to: type=registry,ref=whyour/qinglong:cache,mode=max + cache-from: type=registry,ref=whyour/qinglong:cache-alpine + cache-to: type=registry,ref=whyour/qinglong:cache-alpine,mode=max - - name: Image digest - run: | - echo ${{ steps.docker_build.outputs.digest }} - - build310: - if: ${{ github.ref_name == 'master' }} + build-debian: + if: ${{ !startsWith(github.ref, 'refs/tags/') }} needs: build-static - runs-on: ubuntu-22.04 - permissions: packages: write contents: read - steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v6 with: version: "8.3.1" - uses: actions/setup-node@v6 with: cache: "pnpm" + cache-dependency-path: pnpm-lock.yaml - name: Read version from version.yaml id: version run: | VERSION=$(grep '^version:' version.yaml | awk '{print $2}') echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Version: $VERSION" - name: Setup timezone - uses: szenius/set-timezone@v2.0 - with: - timezoneLinux: Asia/Shanghai + run: sudo timedatectl set-timezone Asia/Shanghai - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Login to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v6 + with: + images: | + ${{ github.repository }} + ghcr.io/${{ github.repository }} + flavor: | + latest=false + tags: | + type=raw,value=debian-dev,enable=${{ github.ref == format('refs/heads/{0}', 'develop') }} + type=raw,value=debian,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} + type=raw,value=${{ steps.version.outputs.version }}-debian,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} + - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - - name: Build and push python3.10 - id: docker_build_310 - uses: docker/build-push-action@v6 + - name: Build and push (Debian) + uses: docker/build-push-action@v7 + with: + build-args: | + MAINTAINER=${{ github.repository_owner }} + QL_BRANCH=${{ github.ref_name }} + SOURCE_COMMIT=${{ github.sha }} + network: host + platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x + context: . + file: ./docker/Dockerfile.debian + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=registry,ref=whyour/qinglong:cache-debian + cache-to: type=registry,ref=whyour/qinglong:cache-debian,mode=max + + build-alpine310: + if: ${{ github.ref_name == 'master' }} + needs: build-static + runs-on: ubuntu-22.04 + permissions: + packages: write + contents: read + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v6 + with: + version: "8.3.1" + - uses: actions/setup-node@v6 + with: + cache: "pnpm" + cache-dependency-path: pnpm-lock.yaml + + - name: Read version from version.yaml + id: version + run: | + VERSION=$(grep '^version:' version.yaml | awk '{print $2}') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Setup timezone + run: sudo timedatectl set-timezone Asia/Shanghai + + - name: Login to DockerHub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Login to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build and push (Alpine Python 3.10) + uses: docker/build-push-action@v7 with: build-args: | MAINTAINER=${{ github.repository_owner }} QL_BRANCH=${{ github.ref_name }} SOURCE_COMMIT=${{ github.sha }} network: host - # linux/s390x npm 暂不可用 platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/386 context: . - file: ./docker/310.Dockerfile + file: ./docker/Dockerfile.310 push: true tags: | whyour/qinglong:python3.10 whyour/qinglong:${{ steps.version.outputs.version }}-python3.10 - cache-from: type=registry,ref=whyour/qinglong:cache-python3.10 - cache-to: type=registry,ref=whyour/qinglong:cache-python3.10,mode=max + cache-from: type=registry,ref=whyour/qinglong:cache-alpine-python3.10 + cache-to: type=registry,ref=whyour/qinglong:cache-alpine-python3.10,mode=max - - name: Image digest + build-debian310: + if: ${{ github.ref_name == 'master' }} + needs: build-static + runs-on: ubuntu-22.04 + permissions: + packages: write + contents: read + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v6 + with: + version: "8.3.1" + - uses: actions/setup-node@v6 + with: + cache: "pnpm" + cache-dependency-path: pnpm-lock.yaml + + - name: Read version from version.yaml + id: version run: | - echo ${{ steps.docker_build_310.outputs.digest }} + VERSION=$(grep '^version:' version.yaml | awk '{print $2}') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Setup timezone + run: sudo timedatectl set-timezone Asia/Shanghai + + - name: Login to DockerHub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Login to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build and push (Debian Python 3.10) + uses: docker/build-push-action@v7 + with: + build-args: | + MAINTAINER=${{ github.repository_owner }} + QL_BRANCH=${{ github.ref_name }} + SOURCE_COMMIT=${{ github.sha }} + network: host + platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x + context: . + file: ./docker/Dockerfile.debian310 + push: true + tags: | + whyour/qinglong:debian-python3.10 + whyour/qinglong:${{ steps.version.outputs.version }}-debian-python3.10 + cache-from: type=registry,ref=whyour/qinglong:cache-debian-python3.10 + cache-to: type=registry,ref=whyour/qinglong:cache-debian-python3.10,mode=max + + publish: + if: ${{ github.ref_name == 'master' }} + needs: [build-alpine, build-debian] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v3 + with: + version: "8.3.1" + + - uses: actions/setup-node@v3 + with: + cache: "pnpm" + + - name: build front and back + run: | + pnpm install --frozen-lockfile + pnpm build:front + pnpm build:back + + - name: publish npm package + run: | + echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc + npm publish diff --git a/.gitignore b/.gitignore index 709791ed..c9fc9200 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ __pycache__ /shell/preload/notify.* /shell/preload/*-notify.json /shell/preload/__ql_notify__.* + +.deepseek/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..230f2ff4 --- /dev/null +++ b/.npmignore @@ -0,0 +1,22 @@ +/.tmp/ +/.github/ +/.vscode/ +/.history/ +/back/**/*.ts +/back/**/*.json +/cli/ +/data/ +/src/ +/static/**/*.js.map +/static/**/*.gz +/.editorconfig +/.gitignore +/.prettierignore +/.prettierrc +/.umirc.ts +/nodemon.json +/pnpm-lock.yaml +/tsconfig.back.json +/tsconfig.json +/typings.d.ts +/.env \ No newline at end of file diff --git a/README-en.md b/README-en.md index e0e1eb59..4570d6d5 100644 --- a/README-en.md +++ b/README-en.md @@ -48,6 +48,17 @@ docker pull whyour/qinglong:latest docker pull whyour/qinglong:debian ``` +When running the `debian` image as a non-root user, specify `--user qinglong`: + +```bash +docker run -d \ + -v /path/to/ql/data:/ql/data \ + -p 5700:5700 \ + --user qinglong \ + --name qinglong \ + whyour/qinglong:debian +``` + ### npm The npm version supports `debian/ubuntu/alpine` systems and requires `node/npm/python3/pip3/pnpm` to be installed. diff --git a/README.md b/README.md index fadc841b..9b7cdd55 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,17 @@ docker pull whyour/qinglong:latest docker pull whyour/qinglong:debian ``` +使用 `debian` 镜像以非 root 用户运行时,需指定 `--user qinglong`: + +```bash +docker run -d \ + -v /path/to/ql/data:/ql/data \ + -p 5700:5700 \ + --user qinglong \ + --name qinglong \ + whyour/qinglong:debian +``` + ### npm npm 版本支持 `debian/ubuntu/alpine` 系统,需要自行安装 `node/npm/python3/pip3/pnpm` diff --git a/back/api/config.ts b/back/api/config.ts index 99649fb3..55c740fd 100644 --- a/back/api/config.ts +++ b/back/api/config.ts @@ -14,7 +14,7 @@ export default (app: Router) => { app.use('/configs', route); route.get( - '/sample', + '/samples', async (req: Request, res: Response, next: NextFunction) => { try { res.send({ diff --git a/back/api/env.ts b/back/api/env.ts index 834bdd51..e58cd3ae 100644 --- a/back/api/env.ts +++ b/back/api/env.ts @@ -18,6 +18,10 @@ const storage = multer.diskStorage({ }, }); const upload = multer({ storage: storage }); +const labelSchema = Joi.array() + .items(Joi.string().trim().required()) + .min(1) + .required(); export default (app: Router) => { app.use('/envs', route); @@ -44,6 +48,7 @@ export default (app: Router) => { .required() .pattern(/^[a-zA-Z_][0-9a-zA-Z_]*$/), remarks: Joi.string().optional().allow(''), + labels: Joi.array().items(Joi.string().trim()).optional(), }), ), }), @@ -70,6 +75,7 @@ export default (app: Router) => { name: Joi.string().required(), remarks: Joi.string().optional().allow('').allow(null), id: Joi.number().required(), + labels: Joi.array().items(Joi.string().trim()).optional(), }), }), async (req: Request, res: Response, next: NextFunction) => { @@ -230,6 +236,44 @@ export default (app: Router) => { }, ); + route.post( + '/labels', + celebrate({ + body: Joi.object({ + ids: Joi.array().items(Joi.number().required()).min(1).required(), + labels: labelSchema, + }), + }), + async (req: Request, res: Response, next: NextFunction) => { + try { + const envService = Container.get(EnvService); + const data = await envService.addLabels(req.body.ids, req.body.labels); + return res.send({ code: 200, data }); + } catch (e) { + return next(e); + } + }, + ); + + route.delete( + '/labels', + celebrate({ + body: Joi.object({ + ids: Joi.array().items(Joi.number().required()).min(1).required(), + labels: labelSchema, + }), + }), + async (req: Request, res: Response, next: NextFunction) => { + try { + const envService = Container.get(EnvService); + const data = await envService.removeLabels(req.body.ids, req.body.labels); + return res.send({ code: 200, data }); + } catch (e) { + return next(e); + } + }, + ); + route.post( '/upload', upload.single('env'), @@ -248,6 +292,7 @@ export default (app: Router) => { name: x.name, value: x.value, remarks: x.remarks, + labels: x.labels, })), ); return res.send({ code: 200, data: result }); diff --git a/back/api/user.ts b/back/api/user.ts index bed0fd78..6155f0a4 100644 --- a/back/api/user.ts +++ b/back/api/user.ts @@ -140,12 +140,12 @@ export default (app: Router) => { ); route.put( - '/two-factor/deactive', + '/two-factor/deactivate', async (req: Request, res: Response, next: NextFunction) => { const logger: Logger = Container.get('logger'); try { const userService = Container.get(UserService); - const data = await userService.deactiveTwoFactor(); + const data = await userService.deactivateTwoFactor(); res.send({ code: 200, data }); } catch (e) { return next(e); diff --git a/back/config/const.ts b/back/config/const.ts index e5083f43..59350a98 100644 --- a/back/config/const.ts +++ b/back/config/const.ts @@ -49,3 +49,38 @@ export const NotificationModeStringMap = { 19: 'ntfy', 20: 'wxPusherBot', } as const; + +export const LINUX_DEPENDENCE_COMMAND: Record< + 'Debian' | 'Ubuntu' | 'Alpine', + { + install: string; + uninstall: string; + info: string; + check(info: string): boolean; + } +> = { + Debian: { + install: 'sudo apt-get install -y', + uninstall: 'sudo apt-get remove -y', + info: 'sudo dpkg-query -s', + check(info: string) { + return info.includes('install ok installed'); + }, + }, + Ubuntu: { + install: 'sudo apt-get install -y', + uninstall: 'sudo apt-get remove -y', + info: 'sudo dpkg-query -s', + check(info: string) { + return info.includes('install ok installed'); + }, + }, + Alpine: { + install: 'apk add --no-check-certificate', + uninstall: 'apk del', + info: 'apk info -es', + check(info: string) { + return info.includes('installed'); + }, + }, +}; diff --git a/back/config/index.ts b/back/config/index.ts index f7c7a54c..fb1c9ca5 100644 --- a/back/config/index.ts +++ b/back/config/index.ts @@ -9,6 +9,8 @@ dotenv.config({ interface Config { port: number; grpcPort: number; + bindHost: string; + bindHostGrpc: string; nodeEnv: string; isDevelopment: boolean; isProduction: boolean; @@ -31,6 +33,8 @@ interface Config { const config: Config = { port: parseInt(process.env.BACK_PORT || '5700', 10), grpcPort: parseInt(process.env.GRPC_PORT || '5500', 10), + bindHost: process.env.BIND_HOST || '::', + bindHostGrpc: process.env.BIND_HOST_GRPC || '::', nodeEnv: process.env.NODE_ENV || 'development', isDevelopment: process.env.NODE_ENV === 'development', isProduction: process.env.NODE_ENV === 'production', diff --git a/back/config/util.ts b/back/config/util.ts index 7d61f74f..1b994e31 100644 --- a/back/config/util.ts +++ b/back/config/util.ts @@ -1,6 +1,6 @@ import * as fs from 'fs/promises'; import * as path from 'path'; -import { exec } from 'child_process'; +import { exec, execSync } from 'child_process'; import psTreeFun from 'ps-tree'; import { promisify } from 'util'; import { load } from 'js-yaml'; @@ -10,9 +10,38 @@ import Logger from '../loaders/logger'; import { writeFileWithLock } from '../shared/utils'; import { DependenceTypes } from '../data/dependence'; import { FormData } from 'undici'; +import os from 'os'; export * from './share'; +let osType: 'Debian' | 'Ubuntu' | 'Alpine' | undefined; + +function getOsTypeSync(): 'Debian' | 'Ubuntu' | 'Alpine' | undefined { + // 1. 环境变量覆盖 + const envOs = process.env.QL_OS_TYPE?.toLowerCase(); + if (envOs === 'alpine') return 'Alpine'; + if (envOs === 'debian') return 'Debian'; + if (envOs === 'ubuntu') return 'Ubuntu'; + + // 2. 模块缓存(由 detectOS 设置) + if (osType) return osType; + + // 3. 能力检测:检查包管理器二进制 + try { + execSync('which apt-get', { stdio: 'ignore' }); + return 'Debian'; + } catch { + try { + execSync('which apk', { stdio: 'ignore' }); + return 'Alpine'; + } catch { + // macOS / 未知系统 + } + } + + return undefined; +} + export async function getFileContentByName(fileName: string) { const _exsit = await fileExist(fileName); if (_exsit) { @@ -550,7 +579,9 @@ except: spec=u.find_spec(name) print(name if spec else '') ''')"`, - [DependenceTypes.linux]: `apk info -es ${name}`, + [DependenceTypes.linux]: getOsTypeSync() === 'Alpine' + ? `apk info -es ${name}` + : `sudo dpkg-query -s ${name}`, }; return baseCommands[type]; @@ -561,7 +592,9 @@ export function getInstallCommand(type: DependenceTypes, name: string): string { [DependenceTypes.nodejs]: 'pnpm add -g', [DependenceTypes.python3]: 'pip3 install --disable-pip-version-check --root-user-action=ignore', - [DependenceTypes.linux]: 'apk add --no-check-certificate', + [DependenceTypes.linux]: getOsTypeSync() === 'Alpine' + ? 'apk add --no-check-certificate' + : 'sudo apt-get install -y', }; let command = baseCommands[type]; @@ -581,7 +614,9 @@ export function getUninstallCommand( [DependenceTypes.nodejs]: 'pnpm remove -g', [DependenceTypes.python3]: 'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y', - [DependenceTypes.linux]: 'apk del', + [DependenceTypes.linux]: getOsTypeSync() === 'Alpine' + ? 'apk del' + : 'sudo apt-get remove -y', }; return `${baseCommands[type]} ${name.trim()}`; @@ -590,3 +625,160 @@ export function getUninstallCommand( export function isDemoEnv() { return process.env.DeployEnv === 'demo'; } + +async function getOSReleaseInfo(): Promise { + const osRelease = await fs.readFile('/etc/os-release', 'utf8'); + return osRelease; +} + +function isDebian(osReleaseInfo: string): boolean { + return osReleaseInfo.includes('Debian'); +} + +function isUbuntu(osReleaseInfo: string): boolean { + return osReleaseInfo.includes('Ubuntu'); +} + +function isCentOS(osReleaseInfo: string): boolean { + return osReleaseInfo.includes('CentOS') || osReleaseInfo.includes('Red Hat'); +} + +function isAlpine(osReleaseInfo: string): boolean { + return osReleaseInfo.includes('Alpine'); +} + +export async function detectOS(): Promise< + 'Debian' | 'Ubuntu' | 'Alpine' | undefined +> { + if (osType) return osType; + + const envOs = process.env.QL_OS_TYPE?.toLowerCase(); + if (envOs === 'alpine') { + osType = 'Alpine'; + return osType; + } + if (envOs === 'debian') { + osType = 'Debian'; + return osType; + } + if (envOs === 'ubuntu') { + osType = 'Ubuntu'; + return osType; + } + + const platform = os.platform(); + + if (platform === 'linux') { + const osReleaseInfo = await getOSReleaseInfo(); + if (isDebian(osReleaseInfo)) { + osType = 'Debian'; + } else if (isUbuntu(osReleaseInfo)) { + osType = 'Ubuntu'; + } else if (isAlpine(osReleaseInfo)) { + osType = 'Alpine'; + } else { + Logger.error(`Unknown Linux Distribution: ${osReleaseInfo}`); + console.error(`Unknown Linux Distribution: ${osReleaseInfo}`); + } + } else if (platform === 'darwin') { + osType = undefined; + } else { + Logger.error(`Unsupported platform: ${platform}`); + console.error(`Unsupported platform: ${platform}`); + } + + return osType; +} + +async function getCurrentMirrorDomain( + filePath: string, +): Promise { + 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 { + 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( + osType: string, + mirrorDomainWithScheme: string, +): Promise { + let filePath: string, currentDomainWithScheme: string | null; + switch (osType) { + case 'Debian': + filePath = '/etc/apt/sources.list.d/debian.sources'; + currentDomainWithScheme = await getCurrentMirrorDomain(filePath); + if (currentDomainWithScheme) { + await replaceDomainInFile( + filePath, + currentDomainWithScheme, + mirrorDomainWithScheme || 'http://deb.debian.org', + ); + return 'sudo apt-get update'; + } else { + throw Error(`Current mirror domain not found.`); + } + case 'Ubuntu': + filePath = '/etc/apt/sources.list.d/ubuntu.sources'; + currentDomainWithScheme = await getCurrentMirrorDomain(filePath); + if (currentDomainWithScheme) { + await replaceDomainInFile( + filePath, + currentDomainWithScheme, + mirrorDomainWithScheme || 'http://archive.ubuntu.com', + ); + return 'sudo apt-get update'; + } else { + throw Error(`Current mirror domain not found.`); + } + case 'Alpine': + filePath = '/etc/apk/repositories'; + currentDomainWithScheme = await getCurrentMirrorDomain(filePath); + if (currentDomainWithScheme) { + await replaceDomainInFile( + filePath, + currentDomainWithScheme, + mirrorDomainWithScheme || 'http://dl-cdn.alpinelinux.org', + ); + return 'apk update'; + } else { + throw Error(`Current mirror domain not found.`); + } + default: + throw Error('Unsupported OS type for updating mirrors.'); + } +} + +export async function updateLinuxMirrorFile(mirror: string): Promise { + const detectedOS = await detectOS(); + if (!detectedOS) { + throw Error(`Unknown Linux Distribution`); + } + return await _updateLinuxMirror(detectedOS, mirror); +} diff --git a/back/data/env.ts b/back/data/env.ts index bd24f22a..ea238a95 100644 --- a/back/data/env.ts +++ b/back/data/env.ts @@ -10,6 +10,7 @@ export class Env { name?: string; remarks?: string; isPinned?: 1 | 0; + labels?: string[]; constructor(options: Env) { this.value = options.value; @@ -23,6 +24,7 @@ export class Env { this.name = options.name; this.remarks = options.remarks || ''; this.isPinned = options.isPinned || 0; + this.labels = options.labels || []; } } @@ -45,4 +47,5 @@ export const EnvModel = sequelize.define('Env', { name: { type: DataTypes.STRING, unique: 'compositeIndex' }, remarks: DataTypes.STRING, isPinned: DataTypes.NUMBER, + labels: DataTypes.JSON, }); diff --git a/back/data/notify.ts b/back/data/notify.ts index b0899500..1d048dfe 100644 --- a/back/data/notify.ts +++ b/back/data/notify.ts @@ -20,6 +20,7 @@ export enum NotificationMode { 'chronocat' = 'Chronocat', 'ntfy' = 'ntfy', 'wxPusherBot' = 'wxPusherBot', + 'openiLink' = 'openiLink', } abstract class NotificationBaseInfo { @@ -161,6 +162,12 @@ export class WxPusherBotNotification extends NotificationBaseInfo { public wxPusherBotUids = ''; } +export class OpeniLinkNotification extends NotificationBaseInfo { + public openiLinkAppToken = ''; + public openiLinkHubUrl = ''; + public openiLinkContextToken = ''; +} + export interface NotificationInfo extends GoCqHttpBotNotification, GotifyNotification, @@ -182,4 +189,5 @@ export interface NotificationInfo ChronocatNotification, LarkNotification, NtfyNotification, - WxPusherBotNotification {} + WxPusherBotNotification, + OpeniLinkNotification {} diff --git a/back/loaders/db.ts b/back/loaders/db.ts index 49fefb6b..987c6cbe 100644 --- a/back/loaders/db.ts +++ b/back/loaders/db.ts @@ -40,6 +40,7 @@ export default async () => { type: 'NUMBER', }, { table: 'Envs', column: 'isPinned', type: 'NUMBER' }, + { table: 'Envs', column: 'labels', type: 'JSON' }, ]; for (const migration of migrations) { diff --git a/back/loaders/express.ts b/back/loaders/express.ts index 63065a21..b5bf221b 100644 --- a/back/loaders/express.ts +++ b/back/loaders/express.ts @@ -13,9 +13,29 @@ import { isValidToken } from '../shared/auth'; import path from 'path'; export default ({ app }: { app: Application }) => { + // Security: Enable strict routing to prevent case-insensitive path bypass + app.set('case sensitive routing', true); + app.set('strict routing', true); app.set('trust proxy', 'loopback'); app.use(cors()); + // Security: Path normalization middleware to prevent case variation attacks + app.use((req, res, next) => { + const originalPath = req.path; + const normalizedPath = originalPath.toLowerCase(); + + // Block requests with case variations on protected paths + if (originalPath !== normalizedPath && + (normalizedPath.startsWith('/api/') || normalizedPath.startsWith('/open/'))) { + return res.status(400).json({ + code: 400, + message: 'Invalid path format' + }); + } + + next(); + }); + // Rewrite URLs to strip baseUrl prefix if configured // This allows the rest of the app to work without baseUrl awareness if (config.baseUrl) { @@ -36,7 +56,7 @@ export default ({ app }: { app: Application }) => { secret: config.jwt.secret, algorithms: ['HS384'], }).unless({ - path: [...config.apiWhiteList, /^\/(?!api\/).*/], + path: [...config.apiWhiteList, /^(\/(?!api\/).*)$/i], }), ); @@ -51,19 +71,20 @@ export default ({ app }: { app: Application }) => { }); app.use(async (req: Request, res, next) => { - if (!['/open/', '/api/'].some((x) => req.path.startsWith(x))) { + const pathLower = req.path.toLowerCase(); + if (!['/open/', '/api/'].some((x) => pathLower.startsWith(x))) { return next(); } const headerToken = getToken(req); - if (req.path.startsWith('/open/')) { + if (pathLower.startsWith('/open/')) { const apps = await shareStore.getApps(); const doc = apps?.filter((x) => x.tokens?.find((y) => y.value === headerToken), )?.[0]; if (doc && doc.tokens && doc.tokens.length > 0) { const currentToken = doc.tokens.find((x) => x.value === headerToken); - const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/); + const keyMatch = pathLower.match(/\/open\/([a-z]+)\/*/); const key = keyMatch && keyMatch[1]; if ( doc.scopes.includes(key as any) && @@ -98,7 +119,15 @@ export default ({ app }: { app: Application }) => { }); app.use(async (req, res, next) => { - if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) { + const pathLower = req.path.toLowerCase(); + if ( + ![ + '/api/user/init', + '/api/user/notification/init', + '/open/user/init', + '/open/user/notification/init', + ].includes(req.path) + ) { return next(); } const authInfo = diff --git a/back/loaders/initData.ts b/back/loaders/initData.ts index f8b5315a..37bbdd6b 100644 --- a/back/loaders/initData.ts +++ b/back/loaders/initData.ts @@ -13,7 +13,7 @@ import { AuthDataType, SystemModel } from '../data/system'; import SystemService from '../services/system'; import UserService from '../services/user'; import { writeFile, readFile } from 'fs/promises'; -import { createRandomString, fileExist, safeJSONParse } from '../config/util'; +import { createRandomString, fileExist, isDemoEnv, safeJSONParse } from '../config/util'; import OpenService from '../services/open'; import { shareStore } from '../shared/store'; import Logger from './logger'; @@ -50,7 +50,7 @@ export default async () => { const [authConfig] = await SystemModel.findOrCreate({ where: { type: AuthDataType.authConfig }, }); - if (!authConfig?.info) { + if (!authConfig?.info || isDemoEnv()) { let authInfo = { username: 'admin', password: 'admin', diff --git a/back/loaders/initFile.ts b/back/loaders/initFile.ts index 2eab9446..ac629d63 100644 --- a/back/loaders/initFile.ts +++ b/back/loaders/initFile.ts @@ -20,6 +20,7 @@ const uploadPath = path.join(dataPath, 'upload/'); const bakPath = path.join(dataPath, 'bak/'); const samplePath = path.join(rootPath, 'sample/'); const tmpPath = path.join(logPath, '.tmp/'); +const rootTmpPath = path.join(rootPath, '.tmp/'); const confFile = path.join(configPath, 'config.sh'); const sampleConfigFile = path.join(samplePath, 'config.sample.sh'); const sampleTaskShellFile = path.join(samplePath, 'task.sample.sh'); @@ -44,6 +45,7 @@ const directories = [ preloadPath, logPath, tmpPath, + rootTmpPath, uploadPath, sshPath, bakPath, diff --git a/back/schedule/api.ts b/back/schedule/api.ts index 5bcc7b1f..81747958 100644 --- a/back/schedule/api.ts +++ b/back/schedule/api.ts @@ -250,7 +250,7 @@ const normalizeCronData = (data: CronItem | null): CronItem | undefined => { return { ...data, sub_id: data.sub_id ?? undefined, - extra_schedules: data.extra_schedules ?? undefined, + extra_schedules: data.extra_schedules ?? [], pid: data.pid ?? undefined, task_before: data.task_before ?? undefined, task_after: data.task_after ?? undefined, diff --git a/back/schedule/client.ts b/back/schedule/client.ts index 99bf364a..6b086452 100644 --- a/back/schedule/client.ts +++ b/back/schedule/client.ts @@ -10,7 +10,7 @@ import config from '../config'; class Client { private client = new CronClient( - `0.0.0.0:${config.grpcPort}`, + `localhost:${config.grpcPort}`, credentials.createInsecure(), { 'grpc.enable_http_proxy': 0 }, ); diff --git a/back/schedule/health.ts b/back/schedule/health.ts index f77a914b..dc90f7e6 100644 --- a/back/schedule/health.ts +++ b/back/schedule/health.ts @@ -10,7 +10,7 @@ const check = async ( switch (call.request.service) { case 'cron': const res = await promiseExec( - `curl -s --noproxy '*' http://0.0.0.0:${config.port}/api/system`, + `curl -s --noproxy '*' http://localhost:${config.port}/api/system`, ); if (res.includes('200')) { diff --git a/back/services/cron.ts b/back/services/cron.ts index 94cdd95a..def418a4 100644 --- a/back/services/cron.ts +++ b/back/services/cron.ts @@ -39,6 +39,25 @@ export default class CronService { return false; } + private get schedulerMode(): 'system' | 'node' { + const env = process.env.QL_SCHEDULER; + if (env === 'system') return 'system'; + if (env === 'node') return 'node'; + try { + execSync('which crond', { stdio: 'ignore' }); + return 'system'; + } catch { + return 'node'; + } + } + + private shouldUseCronClient(cron: Crontab): boolean { + if (this.schedulerMode === 'node') { + return !this.isSpecialSchedule(cron.schedule); + } + return this.isNodeCron(cron) && !this.isSpecialSchedule(cron.schedule); + } + private isOnceSchedule(schedule?: string) { return schedule?.startsWith(ScheduleType.ONCE); } @@ -80,7 +99,7 @@ export default class CronService { return doc; } - if (this.isNodeCron(doc) && !this.isSpecialSchedule(doc.schedule)) { + if (this.shouldUseCronClient(doc)) { await cronClient.addCron([ { name: doc.name || '', @@ -111,11 +130,9 @@ export default class CronService { return newDoc; } - if (this.isNodeCron(doc)) { - await cronClient.delCron([String(doc.id)]); - } + await cronClient.delCron([String(newDoc.id)]); - if (this.isNodeCron(newDoc) && !this.isSpecialSchedule(newDoc.schedule)) { + if (this.shouldUseCronClient(newDoc)) { await cronClient.addCron([ { name: doc.name || '', @@ -577,8 +594,8 @@ export default class CronService { public async enabled(ids: number[]) { await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } }); const docs = await CrontabModel.findAll({ where: { id: ids } }); - const sixCron = docs - .filter((x) => this.isNodeCron(x) && !this.isSpecialSchedule(x.schedule)) + const crons = docs + .filter((x) => this.shouldUseCronClient(x)) .map((doc) => ({ name: doc.name || '', id: String(doc.id), @@ -590,7 +607,8 @@ export default class CronService { if (isDemoEnv()) { return; } - await cronClient.addCron(sixCron); + + await cronClient.addCron(crons); await this.setCrontab(); } @@ -690,11 +708,13 @@ export default class CronService { await writeFileWithLock(config.crontabFile, crontab_string); - try { - execSync(`crontab ${config.crontabFile}`); - } catch (error: any) { - const errorMsg = error.message || String(error); - this.logger.error('[crontab] Failed to update system crontab:', errorMsg); + if (this.schedulerMode === 'system') { + try { + execSync(`crontab ${config.crontabFile}`); + } catch (error: any) { + const errorMsg = error.message || String(error); + this.logger.error('[crontab] Failed to update system crontab:', errorMsg); + } } await CrontabModel.update({ saved: true }, { where: {} }); @@ -745,8 +765,7 @@ export default class CronService { .filter( (x) => x.isDisabled !== 1 && - this.isNodeCron(x) && - !this.isSpecialSchedule(x.schedule), + this.shouldUseCronClient(x), ) .map((doc) => ({ name: doc.name || '', diff --git a/back/services/dependence.ts b/back/services/dependence.ts index eac054be..c00422b3 100644 --- a/back/services/dependence.ts +++ b/back/services/dependence.ts @@ -22,6 +22,8 @@ import { } from '../config/util'; import dayjs from 'dayjs'; import taskLimit from '../shared/pLimit'; +import { detectOS } from '../config/util'; +import { LINUX_DEPENDENCE_COMMAND } from '../config/const'; @Service() export default class DependenceService { @@ -159,8 +161,19 @@ export default class DependenceService { const docs = await DependenceModel.findAll({ where: { id: ids } }); for (const doc of docs) { taskLimit.removeQueuedDependency(doc); - const depInstallCommand = getInstallCommand(doc.type, doc.name); - const depUnInstallCommand = getUninstallCommand(doc.type, doc.name); + let depInstallCommand = getInstallCommand(doc.type, doc.name); + let depUnInstallCommand = getUninstallCommand(doc.type, doc.name); + const isLinuxDependence = doc.type === DependenceTypes.linux; + + if (isLinuxDependence) { + const osType = await detectOS(); + if (!osType) { + continue; + } + const linuxCommand = LINUX_DEPENDENCE_COMMAND[osType]; + depInstallCommand = `${linuxCommand.install} ${doc.name.trim()}`; + depUnInstallCommand = `${linuxCommand.uninstall} ${doc.name.trim()}`; + } const pids = await Promise.all([ getPid(depInstallCommand), getPid(depUnInstallCommand), @@ -217,23 +230,54 @@ export default class DependenceService { if (taskLimit.firstDependencyId !== dependency.id) { return resolve(null); } - - taskLimit.removeQueuedDependency(dependency); - const depIds = [dependency.id!]; + let depName = dependency.name.trim(); + const actionText = isInstall ? '安装' : '删除'; + const socketMessageType = isInstall + ? 'installDependence' + : 'uninstallDependence'; + const isNodeDependence = dependency.type === DependenceTypes.nodejs; + const isLinuxDependence = dependency.type === DependenceTypes.linux; + const isPythonDependence = dependency.type === DependenceTypes.python3; + const osType = await detectOS(); + let linuxCommand = {} as typeof LINUX_DEPENDENCE_COMMAND.Alpine; + taskLimit.removeQueuedDependency(dependency); + if (isLinuxDependence) { + if (!osType) { + await DependenceModel.update( + { status: DependenceStatus.installFailed }, + { where: { id: depIds } }, + ); + const startTime = dayjs(); + const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format( + 'YYYY-MM-DD HH:mm:ss', + )}\n\n当前系统不支持\n\n依赖${actionText}失败,结束时间 ${startTime.format( + 'YYYY-MM-DD HH:mm:ss', + )},耗时 ${startTime.diff(startTime, 'second')} 秒`; + this.sockService.sendMessage({ + type: socketMessageType, + message, + references: depIds, + }); + this.updateLog(depIds, message); + return resolve(null); + } + linuxCommand = LINUX_DEPENDENCE_COMMAND[osType]; + } + const status = isInstall ? DependenceStatus.installing : DependenceStatus.removing; await DependenceModel.update({ status }, { where: { id: depIds } }); - const socketMessageType = isInstall - ? 'installDependence' - : 'uninstallDependence'; - let depName = dependency.name.trim(); - const command = isInstall + let command = isInstall ? getInstallCommand(dependency.type, depName) : getUninstallCommand(dependency.type, depName); - const actionText = isInstall ? '安装' : '删除'; + if (isLinuxDependence) { + command = isInstall + ? `${linuxCommand.install} ${depName.trim()}` + : `${linuxCommand.uninstall} ${depName.trim()}`; + } const startTime = dayjs(); const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format( @@ -248,8 +292,12 @@ export default class DependenceService { // 判断是否已经安装过依赖 if (isInstall && !force) { - const getCommand = getGetCommand(dependency.type, depName); + let getCommand = getGetCommand(dependency.type, depName); const depVersionStr = versionDependenceCommandTypes[dependency.type]; + if (isLinuxDependence) { + getCommand = `${linuxCommand.info} ${depName}`; + } + let depVersion = ''; if (depName.includes(depVersionStr)) { const symbolRegx = new RegExp( @@ -261,10 +309,6 @@ export default class DependenceService { depVersion = _depVersion; } } - const isNodeDependence = dependency.type === DependenceTypes.nodejs; - const isLinuxDependence = dependency.type === DependenceTypes.linux; - const isPythonDependence = - dependency.type === DependenceTypes.python3; const depInfo = (await promiseExecSuccess(getCommand)) .replace(/\s{2,}/, ' ') .replace(/\s+$/, ''); @@ -273,7 +317,7 @@ export default class DependenceService { depInfo && ((isNodeDependence && depInfo.split(' ')?.[0] === depName) || (isLinuxDependence && - depInfo.toLocaleLowerCase().includes('installed')) || + linuxCommand.check(depInfo.toLocaleLowerCase())) || isPythonDependence) && (!depVersion || depInfo.includes(depVersion)) ) { diff --git a/back/services/env.ts b/back/services/env.ts index cd4d0a84..5efd148b 100644 --- a/back/services/env.ts +++ b/back/services/env.ts @@ -27,7 +27,7 @@ export default class EnvService { envs.length > 0 && typeof envs[envs.length - 1].position === 'number' ) { - position = envs[envs.length - 1].position!; + position = this.getPrecisionPosition(envs[envs.length - 1].position!); } const tabs = payloads.map((x) => { position = position - stepPosition; @@ -100,7 +100,7 @@ export default class EnvService { } private async checkPosition(position: number, edge: number = 0) { - const precisionPosition = parseFloat(position.toPrecision(16)); + const precisionPosition = this.getPrecisionPosition(position); if ( precisionPosition < minPosition || precisionPosition > maxPosition || @@ -116,7 +116,7 @@ export default class EnvService { } private getPrecisionPosition(position: number): number { - return parseFloat(position.toPrecision(16)); + return Math.trunc(parseFloat(position.toPrecision(16))); } public async envs(searchText: string = '', query: any = {}): Promise { @@ -199,6 +199,44 @@ export default class EnvService { await EnvModel.update({ isPinned: 0 }, { where: { id: ids } }); } + public async addLabels(ids: number[], labels: string[]) { + await sequelize.transaction(async (transaction) => { + const docs = await EnvModel.findAll({ + where: { id: ids }, + transaction, + }); + for (const doc of docs) { + const env = doc.get({ plain: true }); + await EnvModel.update( + { labels: Array.from(new Set([...(env.labels || []), ...labels])) }, + { where: { id: env.id }, transaction }, + ); + } + }); + return await this.find({ id: ids }); + } + + public async removeLabels(ids: number[], labels: string[]) { + await sequelize.transaction(async (transaction) => { + const docs = await EnvModel.findAll({ + where: { id: ids }, + transaction, + }); + for (const doc of docs) { + const env = doc.get({ plain: true }); + await EnvModel.update( + { + labels: (env.labels || []).filter( + (label: string) => !labels.includes(label), + ), + }, + { where: { id: env.id }, transaction }, + ); + } + }); + return await this.find({ id: ids }); + } + public async set_envs() { const envs = await this.envs('', { name: { [Op.not]: null }, diff --git a/back/services/grpc.ts b/back/services/grpc.ts index e8ea2282..5edd9d2c 100644 --- a/back/services/grpc.ts +++ b/back/services/grpc.ts @@ -16,6 +16,13 @@ import { Service } from 'typedi'; export class GrpcServerService { private server: Server = new Server({ 'grpc.enable_http_proxy': 0 }); + private formatGrpcAddress(host: string, port: number): string { + if (host === '::') { + return `[::]:${port}`; + } + return `${host}:${port}`; + } + async initialize() { try { this.server.addService(HealthService, { check }); @@ -23,18 +30,32 @@ export class GrpcServerService { this.server.addService(ApiService, Api); const grpcPort = config.grpcPort; + const hostsToTry = [ + config.bindHostGrpc, + ...(config.bindHostGrpc !== '0.0.0.0' ? ['0.0.0.0'] : []) + ]; const bindAsync = promisify(this.server.bindAsync).bind(this.server); - await bindAsync( - `0.0.0.0:${grpcPort}`, - ServerCredentials.createInsecure(), - ); - Logger.debug(`✌️ gRPC service started successfully`); - metricsService.record('grpc_service_start', 1, { - port: grpcPort.toString(), - }); + let lastError: Error | null = null; - return grpcPort; + for (const host of hostsToTry) { + try { + const address = this.formatGrpcAddress(host, grpcPort); + await bindAsync(address, ServerCredentials.createInsecure()); + Logger.debug(`✌️ gRPC service started successfully on ${address}`); + metricsService.record('grpc_service_start', 1, { + port: grpcPort.toString(), + host + }); + return grpcPort; + } catch (err) { + lastError = err as Error; + Logger.warn(`Failed to bind gRPC on ${host}:${grpcPort}, trying next...`, err); + } + } + + Logger.error('Failed to start gRPC service on all hosts'); + throw lastError || new Error('Failed to start gRPC service'); } catch (err) { Logger.error('Failed to start gRPC service:', err); throw err; diff --git a/back/services/http.ts b/back/services/http.ts index 5391c842..bbce8ff7 100644 --- a/back/services/http.ts +++ b/back/services/http.ts @@ -3,31 +3,51 @@ import Logger from '../loaders/logger'; import { metricsService } from './metrics'; import { Service } from 'typedi'; import { Server } from 'http'; +import config from '../config'; @Service() export class HttpServerService { private server?: Server = undefined; async initialize(expressApp: express.Application, port: number) { - try { - return new Promise((resolve, reject) => { - this.server = expressApp.listen(port, '0.0.0.0', () => { - Logger.debug(`✌️ HTTP service started successfully`); - metricsService.record('http_service_start', 1, { - port: port.toString(), - }); - resolve(this.server); - }); + const hostsToTry = [ + config.bindHost, + ...(config.bindHost !== '0.0.0.0' ? ['0.0.0.0'] : []) + ]; - this.server?.on('error', (err: Error) => { - Logger.error('Failed to start HTTP service:', err); - reject(err); + let lastError: Error | null = null; + + for (const host of hostsToTry) { + try { + const server = await this.tryListen(expressApp, port, host); + Logger.debug(`✌️ HTTP service started successfully on ${host}:${port}`); + metricsService.record('http_service_start', 1, { + port: port.toString(), + host }); - }); - } catch (err) { - Logger.error('Failed to start HTTP service:', err); - throw err; + this.server = server; + return server; + } catch (err) { + lastError = err as Error; + Logger.warn(`Failed to bind HTTP on ${host}:${port}, trying next...`, err); + } } + + Logger.error('Failed to start HTTP service on all hosts'); + throw lastError || new Error('Failed to start HTTP service'); + } + + private async tryListen(expressApp: express.Application, port: number, host: string): Promise { + return new Promise((resolve, reject) => { + const server = expressApp.listen(port, host, () => { + resolve(server); + }); + + server.on('error', (err: Error) => { + server.close(); + reject(err); + }); + }); } async shutdown() { diff --git a/back/services/notify.ts b/back/services/notify.ts index 22609748..39a0e47b 100644 --- a/back/services/notify.ts +++ b/back/services/notify.ts @@ -34,6 +34,7 @@ export default class NotificationService { ['chronocat', this.chronocat], ['ntfy', this.ntfy], ['wxPusherBot', this.wxPusherBot], + ['openiLink', this.openiLink], ]); private title = ''; @@ -90,6 +91,14 @@ export default class NotificationService { return true; } + private parseMailRecipients(value?: string) { + const recipients = (value || '') + .split(/[;;]/) + .map((item) => item.trim()) + .filter(Boolean); + return recipients.length > 0 ? recipients : undefined; + } + private async gotify() { const { gotifyUrl, gotifyToken, gotifyPriority = 1 } = this.params; try { @@ -591,6 +600,7 @@ export default class NotificationService { private async email() { const { emailPass, emailService, emailUser, emailTo } = this.params; + const recipients = this.parseMailRecipients(emailTo) || emailUser; try { const transporter = nodemailer.createTransport({ @@ -603,7 +613,7 @@ export default class NotificationService { const info = await transporter.sendMail({ from: `"青龙快讯" <${emailUser}>`, - to: emailTo ? emailTo.split(';') : emailUser, + to: recipients, subject: `${this.title}`, html: `${this.content.replace(/\n/g, '
')}`, }); @@ -858,4 +868,35 @@ export default class NotificationService { } return {}; } + + private async openiLink() { + const { openiLinkAppToken, openiLinkHubUrl, openiLinkContextToken } = + this.params; + const baseUrl = openiLinkHubUrl?.replace(/\/$/, '') || 'https://hub.openilink.com'; + const url = `${baseUrl}/bot/v1/message/send`; + const body: Record = { + type: 'text', + content: `${this.title}\n\n${this.content}`, + }; + if (openiLinkContextToken) { + body.context_token = openiLinkContextToken; + } + try { + const res = await httpClient.post(url, { + ...this.gotOption, + json: body, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${openiLinkAppToken}`, + }, + }); + if (res.ok) { + return true; + } else { + throw new Error(JSON.stringify(res)); + } + } catch (error: any) { + throw new Error(error.response ? error.response.body : error); + } + } } diff --git a/back/services/system.ts b/back/services/system.ts index ecc2a732..97b500f1 100644 --- a/back/services/system.ts +++ b/back/services/system.ts @@ -37,6 +37,7 @@ import ScheduleService, { TaskCallbacks } from './schedule'; import SockService from './sock'; import os from 'os'; import dayjs from 'dayjs'; +import { updateLinuxMirrorFile } from '../config/util'; @Service() export default class SystemService { @@ -214,33 +215,11 @@ export default class SystemService { onEnd?: () => void, ) { const oDoc = await this.getSystemConfig(); - await this.updateAuthDb({ - ...oDoc, - info: { ...oDoc.info, ...info }, - }); - let defaultDomain = 'https://dl-cdn.alpinelinux.org'; - let targetDomain = 'https://dl-cdn.alpinelinux.org'; if (os.platform() !== 'linux') { return; } - const content = await fs.promises.readFile('/etc/apk/repositories', { - encoding: 'utf-8', - }); - const domainMatch = content.match(/(http.*)\/alpine\/.*/); - if (domainMatch) { - defaultDomain = domainMatch[1]; - } - if (info.linuxMirror) { - targetDomain = info.linuxMirror; - } - const command = `sed -i 's/${defaultDomain.replace( - /\//g, - '\\/', - )}/${targetDomain.replace( - /\//g, - '\\/', - )}/g' /etc/apk/repositories && apk update -f`; - + const command = await updateLinuxMirrorFile(info.linuxMirror || ''); + let hasError = false; this.scheduleService.runTask( command, { @@ -254,8 +233,15 @@ export default class SystemService { message: 'update linux mirror end', }); onEnd?.(); + if (!hasError) { + await this.updateAuthDb({ + ...oDoc, + info: { ...oDoc.info, ...info }, + }); + } }, onError: async (message: string) => { + hasError = true; this.sockService.sendMessage({ type: 'updateLinuxMirror', message }); }, onLog: async (message: string) => { diff --git a/back/services/user.ts b/back/services/user.ts index 068b850c..362cb693 100644 --- a/back/services/user.ts +++ b/back/services/user.ts @@ -330,7 +330,7 @@ export default class UserService { } } - public async deactiveTwoFactor() { + public async deactivateTwoFactor() { const authInfo = await this.getAuthInfo(); await this.updateAuthInfo(authInfo, { twoFactorActivated: false, diff --git a/back/shared/pLimit.ts b/back/shared/pLimit.ts index f84763ad..2f04d5be 100644 --- a/back/shared/pLimit.ts +++ b/back/shared/pLimit.ts @@ -37,7 +37,7 @@ class TaskLimit { concurrency: Math.max(os.cpus().length, 4), }); private client = new ApiClient( - `0.0.0.0:${config.grpcPort}`, + `localhost:${config.grpcPort}`, credentials.createInsecure(), { 'grpc.enable_http_proxy': 0 }, ); diff --git a/docker/Dockerfile b/docker/Dockerfile index 6617b4ee..b874649f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ ENV QL_DIR=/ql \ PS1="\u@\h:\w \$ " VOLUME /ql/data - + EXPOSE 5700 COPY --from=builder /usr/local/lib/node_modules/. /usr/local/lib/node_modules/ @@ -69,10 +69,11 @@ RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \ ENV PNPM_HOME=${QL_DIR}/data/dep_cache/node \ PYTHON_HOME=${QL_DIR}/data/dep_cache/python3 \ - PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3 + PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3 \ + HOME=/root -ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin \ - NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules \ +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 \ 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 @@ -80,9 +81,13 @@ RUN pip3 install --prefix ${PYTHON_HOME} requests COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/ +RUN ln -sf ${QL_DIR}/shell/task.sh /usr/local/bin/task \ + && ln -sf ${QL_DIR}/shell/update.sh /usr/local/bin/ql \ + && chmod +x /usr/local/bin/task /usr/local/bin/ql + WORKDIR ${QL_DIR} HEALTHCHECK --interval=5s --timeout=2s --retries=20 \ - CMD curl -sf --noproxy '*' http://127.0.0.1:5700/api/health || exit 1 + CMD curl -sf --noproxy '*' http://localhost:${QlPort:-5700}/api/health || exit 1 ENTRYPOINT ["./docker/docker-entrypoint.sh"] diff --git a/docker/310.Dockerfile b/docker/Dockerfile.310 similarity index 84% rename from docker/310.Dockerfile rename to docker/Dockerfile.310 index 8a092081..6312448b 100644 --- a/docker/310.Dockerfile +++ b/docker/Dockerfile.310 @@ -22,7 +22,7 @@ ENV QL_DIR=/ql \ PS1="\u@\h:\w \$ " VOLUME /ql/data - + EXPOSE 5700 COPY --from=builder /usr/local/lib/node_modules/. /usr/local/lib/node_modules/ @@ -69,10 +69,11 @@ RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \ ENV PNPM_HOME=${QL_DIR}/data/dep_cache/node \ PYTHON_HOME=${QL_DIR}/data/dep_cache/python3 \ - PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3 + PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3 \ + HOME=/root -ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin \ - NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules \ +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 \ 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 @@ -80,9 +81,13 @@ RUN pip3 install --prefix ${PYTHON_HOME} requests COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/ +RUN ln -sf ${QL_DIR}/shell/task.sh /usr/local/bin/task \ + && ln -sf ${QL_DIR}/shell/update.sh /usr/local/bin/ql \ + && chmod +x /usr/local/bin/task /usr/local/bin/ql + WORKDIR ${QL_DIR} HEALTHCHECK --interval=5s --timeout=2s --retries=20 \ - CMD curl -sf --noproxy '*' http://127.0.0.1:5700/api/health || exit 1 + CMD curl -sf --noproxy '*' http://localhost:${QlPort:-5700}/api/health || exit 1 ENTRYPOINT ["./docker/docker-entrypoint.sh"] diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian new file mode 100644 index 00000000..e4d791f3 --- /dev/null +++ b/docker/Dockerfile.debian @@ -0,0 +1,119 @@ +FROM node:22-slim AS nodebuilder + +FROM python:3.11-slim-bookworm AS builder +COPY package.json .npmrc pnpm-lock.yaml /tmp/build/ +COPY --from=nodebuilder /usr/local/bin/node /usr/local/bin/ +COPY --from=nodebuilder /usr/local/lib/node_modules/. /usr/local/lib/node_modules/ +RUN set -x && \ + ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \ + apt-get update && \ + apt-get install --no-install-recommends -y libatomic1 && \ + npm i -g pnpm@8.3.1 && \ + cd /tmp/build && \ + pnpm install --prod + +FROM python:3.11-slim-bookworm + +ARG QL_MAINTAINER="whyour" +LABEL maintainer="${QL_MAINTAINER}" +ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git +ARG QL_BRANCH=develop +ARG PYTHON_SHORT_VERSION=3.11 + +ENV QL_DIR=/ql \ + QL_BRANCH=${QL_BRANCH} \ + LANG=C.UTF-8 \ + SHELL=/bin/bash \ + PS1="\u@\h:\w \$ " + +ARG QL_UID=5432 +ARG QL_GID=5432 +RUN groupadd -g ${QL_GID} qinglong && \ + useradd -m -u ${QL_UID} -g ${QL_GID} -s /bin/bash qinglong && \ + mkdir -p /home/qinglong/bin /home/qinglong/.ssh && \ + chmod 700 /home/qinglong/.ssh && \ + chown -R ${QL_UID}:${QL_GID} /home/qinglong && \ + mkdir -p /etc/sudoers.d && \ + echo 'qinglong ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/qinglong + +ENV QL_USER=qinglong +ENV QL_HOME=/home/$QL_USER + +COPY --from=nodebuilder /usr/local/bin/node /usr/local/bin/ +COPY --from=nodebuilder /usr/local/lib/node_modules/. /usr/local/lib/node_modules/ + +RUN set -x && \ + ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \ + ln -s /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx && \ + apt-get update && \ + apt-get upgrade -y && \ + apt-get install --no-install-recommends -y git \ + curl \ + wget \ + tzdata \ + perl \ + openssl \ + openssh-client \ + jq \ + procps \ + netcat-openbsd \ + sudo \ + unzip \ + libatomic1 && \ + apt-get clean && \ + ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ + echo "Asia/Shanghai" >/etc/timezone && \ + git config --global user.email "qinglong@users.noreply.github.com" && \ + git config --global user.name "qinglong" && \ + git config --global http.postBuffer 524288000 && \ + npm install -g pnpm@8.3.1 pm2 ts-node && \ + rm -rf /root/.cache && \ + rm -rf /root/.npm && \ + rm -rf /etc/apt/apt.conf.d/docker-clean && \ + ulimit -c 0 + +RUN mkdir -p ${QL_DIR} && \ + chown -R ${QL_UID}:${QL_GID} ${QL_DIR} + +USER qinglong +ARG SOURCE_COMMIT +RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} && \ + cd ${QL_DIR} && \ + cp -f .env.example .env && \ + chmod 777 ${QL_DIR}/shell/*.sh && \ + chmod 777 ${QL_DIR}/docker/*.sh && \ + git clone --depth=1 -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /tmp/static && \ + mkdir -p ${QL_DIR}/static && \ + cp -rf /tmp/static/* ${QL_DIR}/static && \ + rm -rf /tmp/static + +ENV PNPM_HOME=${QL_DIR}/data/dep_cache/node \ + PYTHON_HOME=${QL_DIR}/data/dep_cache/python3 \ + PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3 \ + HOME=/home/qinglong + +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 \ + 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 + +RUN pip3 install --prefix ${PYTHON_HOME} requests + +COPY --chown=qinglong:qinglong --from=builder /tmp/build/node_modules/. /ql/node_modules/ + +USER root + +RUN ln -sf ${QL_DIR}/shell/task.sh /usr/local/bin/task \ + && ln -sf ${QL_DIR}/shell/update.sh /usr/local/bin/ql \ + && chmod +x /usr/local/bin/task /usr/local/bin/ql + +WORKDIR ${QL_DIR} + +HEALTHCHECK --interval=5s --timeout=2s --retries=20 \ + CMD curl -sf --noproxy '*' http://localhost:${QlPort:-5700}/api/health || exit 1 + +ENTRYPOINT ["./docker/docker-entrypoint.sh"] + +VOLUME /ql/data + +EXPOSE 5700 diff --git a/docker/Dockerfile.debian310 b/docker/Dockerfile.debian310 new file mode 100644 index 00000000..bc110ed7 --- /dev/null +++ b/docker/Dockerfile.debian310 @@ -0,0 +1,119 @@ +FROM node:22-slim AS nodebuilder + +FROM python:3.10-slim-bookworm AS builder +COPY package.json .npmrc pnpm-lock.yaml /tmp/build/ +COPY --from=nodebuilder /usr/local/bin/node /usr/local/bin/ +COPY --from=nodebuilder /usr/local/lib/node_modules/. /usr/local/lib/node_modules/ +RUN set -x && \ + ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \ + apt-get update && \ + apt-get install --no-install-recommends -y libatomic1 && \ + npm i -g pnpm@8.3.1 && \ + cd /tmp/build && \ + pnpm install --prod + +FROM python:3.10-slim-bookworm + +ARG QL_MAINTAINER="whyour" +LABEL maintainer="${QL_MAINTAINER}" +ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git +ARG QL_BRANCH=develop +ARG PYTHON_SHORT_VERSION=3.10 + +ENV QL_DIR=/ql \ + QL_BRANCH=${QL_BRANCH} \ + LANG=C.UTF-8 \ + SHELL=/bin/bash \ + PS1="\u@\h:\w \$ " + +ARG QL_UID=5432 +ARG QL_GID=5432 +RUN groupadd -g ${QL_GID} qinglong && \ + useradd -m -u ${QL_UID} -g ${QL_GID} -s /bin/bash qinglong && \ + mkdir -p /home/qinglong/bin /home/qinglong/.ssh && \ + chmod 700 /home/qinglong/.ssh && \ + chown -R ${QL_UID}:${QL_GID} /home/qinglong && \ + mkdir -p /etc/sudoers.d && \ + echo 'qinglong ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/qinglong + +ENV QL_USER=qinglong +ENV QL_HOME=/home/$QL_USER + +COPY --from=nodebuilder /usr/local/bin/node /usr/local/bin/ +COPY --from=nodebuilder /usr/local/lib/node_modules/. /usr/local/lib/node_modules/ + +RUN set -x && \ + ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \ + apt-get update && \ + apt-get upgrade -y && \ + apt-get install --no-install-recommends -y git \ + curl \ + wget \ + tzdata \ + perl \ + openssl \ + openssh-client \ + jq \ + procps \ + netcat-openbsd \ + sudo \ + unzip \ + libatomic1 && \ + apt-get clean && \ + ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ + echo "Asia/Shanghai" >/etc/timezone && \ + git config --global user.email "qinglong@users.noreply.github.com" && \ + git config --global user.name "qinglong" && \ + git config --global http.postBuffer 524288000 && \ + npm install -g pnpm@8.3.1 pm2 ts-node && \ + rm -rf /root/.cache && \ + rm -rf /root/.npm && \ + rm -rf /etc/apt/apt.conf.d/docker-clean && \ + ulimit -c 0 + +RUN mkdir -p ${QL_DIR} && \ + chown -R ${QL_UID}:${QL_GID} ${QL_DIR} + +USER qinglong + +ARG SOURCE_COMMIT +RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} && \ + cd ${QL_DIR} && \ + cp -f .env.example .env && \ + chmod 777 ${QL_DIR}/shell/*.sh && \ + chmod 777 ${QL_DIR}/docker/*.sh && \ + git clone --depth=1 -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /tmp/static && \ + mkdir -p ${QL_DIR}/static && \ + cp -rf /tmp/static/* ${QL_DIR}/static && \ + rm -rf /tmp/static + +ENV PNPM_HOME=${QL_DIR}/data/dep_cache/node \ + PYTHON_HOME=${QL_DIR}/data/dep_cache/python3 \ + PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3 \ + HOME=/home/qinglong + +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 \ + 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 + +RUN pip3 install --prefix ${PYTHON_HOME} requests + +COPY --chown=qinglong:qinglong --from=builder /tmp/build/node_modules/. /ql/node_modules/ + +USER root + +RUN ln -sf ${QL_DIR}/shell/task.sh /usr/local/bin/task \ + && ln -sf ${QL_DIR}/shell/update.sh /usr/local/bin/ql \ + && chmod +x /usr/local/bin/task /usr/local/bin/ql + +WORKDIR ${QL_DIR} + +HEALTHCHECK --interval=5s --timeout=2s --retries=20 \ + CMD curl -sf --noproxy '*' http://localhost:${QlPort:-5700}/api/health || exit 1 + +ENTRYPOINT ["./docker/docker-entrypoint.sh"] + +VOLUME /ql/data + +EXPOSE 5700 diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index eb2027f2..978a92cd 100755 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -1,7 +1,5 @@ #!/bin/bash -export PATH="$HOME/bin:$PATH" - dir_shell=/ql/shell . $dir_shell/share.sh @@ -17,9 +15,68 @@ log_with_style() { printf "\n[%s] [%7s] %s\n" "${timestamp}" "${level}" "${message}" } +# ============================================ +# 确保当前用户对 /ql 和 /ql/data 目录有写入权限 +# /ql/data 是 Docker Volume 挂载点,权限可能与 /ql 不同,需单独检测 +# ============================================ +ensure_ql_permissions() { + local current_uid + local current_gid + current_uid=$(id -u) + current_gid=$(id -g) + + if [ "$current_uid" -eq 0 ]; then + return 0 + fi + + # ---- 检查 /ql 目录 ---- + if ! mkdir -p "$QL_DIR/.tmp" 2>/dev/null; then + if chown -R "$current_uid:$current_gid" "$QL_DIR" 2>/dev/null; then + log_with_style "INFO" "已修正 /ql 目录权限: UID=$current_uid GID=$current_gid" + else + local ql_owner + ql_owner=$(stat -c '%u' "$QL_DIR" 2>/dev/null || stat -f '%u' "$QL_DIR" 2>/dev/null) + log_with_style "ERROR" "=============================================" + log_with_style "ERROR" " 权限错误:无法写入 /ql 目录" + log_with_style "ERROR" " 当前用户 UID: $current_uid" + log_with_style "ERROR" " /ql 目录所有者 UID: ${ql_owner:-未知}" + log_with_style "ERROR" "" + log_with_style "ERROR" " 解决方案:" + log_with_style "ERROR" " 1. 使用镜像内置用户: docker run --user ${ql_owner:-5432}:${ql_owner:-5432} ..." + log_with_style "ERROR" " 2. 使用 root 运行: 移除 --user 参数" + log_with_style "ERROR" " 3. 修正宿主机数据目录: chown -R $current_uid:$current_gid /path/to/ql/data" + log_with_style "ERROR" "=============================================" + exit 1 + fi + fi + rmdir "$QL_DIR/.tmp" 2>/dev/null || true + + # ---- 检查 /ql/data 目录(Volume 挂载点,不在用户数据卷内创建临时文件) ---- + if [ ! -w "$QL_DIR/data" ] || [ ! -x "$QL_DIR/data" ]; then + if chown "$current_uid:$current_gid" "$QL_DIR/data" 2>/dev/null; then + log_with_style "INFO" "已修正 /ql/data 目录权限: UID=$current_uid GID=$current_gid" + if [ ! -w "$QL_DIR/data" ] || [ ! -x "$QL_DIR/data" ]; then + log_with_style "ERROR" "修正后仍无法写入 /ql/data,请检查挂载的数据卷权限" + log_with_style "ERROR" "确保宿主机目录: chown -R $current_uid:$current_gid /your/data" + exit 1 + fi + else + local data_owner + data_owner=$(stat -c '%u' "$QL_DIR/data" 2>/dev/null || stat -f '%u' "$QL_DIR/data" 2>/dev/null) + log_with_style "ERROR" "=============================================" + log_with_style "ERROR" " 权限错误:无法写入 /ql/data (Volume 挂载点)" + log_with_style "ERROR" " 当前用户 UID: $current_uid" + log_with_style "ERROR" " /ql/data 所有者 UID: ${data_owner:-未知}" + log_with_style "ERROR" "" + log_with_style "ERROR" " 请修正宿主机数据目录权限:" + log_with_style "ERROR" " chown -R $current_uid:$current_gid /your/ql/data" + log_with_style "ERROR" "=============================================" + exit 1 + fi + fi +} + # Fix DNS resolution issues in Alpine Linux -# Alpine uses musl libc which has known DNS resolver issues with certain domains -# Adding ndots:0 prevents unnecessary search domain appending if [ -f /etc/alpine-release ]; then if ! grep -q "^options ndots:0" /etc/resolv.conf 2>/dev/null; then echo "options ndots:0" >> /etc/resolv.conf @@ -27,6 +84,26 @@ if [ -f /etc/alpine-release ]; then fi fi +# 确保 /etc/hosts 包含 localhost 解析(应对精简镜像或仅 IPv4/IPv6 环境) +if ! grep -qE '^127\.0\.0\.1[[:space:]]+.*localhost' /etc/hosts 2>/dev/null; then + echo "127.0.0.1 localhost" >> /etc/hosts + log_with_style "INFO" "🔧 0. 已添加 IPv4 localhost 解析" +fi +if ! grep -qE '^::1[[:space:]]+.*localhost' /etc/hosts 2>/dev/null; then + echo "::1 localhost ip6-localhost ip6-loopback" >> /etc/hosts + log_with_style "INFO" "🔧 0. 已添加 IPv6 localhost 解析" +fi + +# 自定义用户(非 qinglong/root)可能 HOME 为空或不可写 +# 修正 HOME 确保 npm/pip/pm2 等工具有可用的缓存目录 +if [ ! -w "$HOME" ]; then + mkdir -p "$QL_DIR/.tmp" + export HOME="$QL_DIR/.tmp" +fi + +# 在一切操作之前检查目录权限 +ensure_ql_permissions + log_with_style "INFO" "🚀 1. 检测配置文件..." load_ql_envs export_ql_envs @@ -52,6 +129,19 @@ fi log_with_style "SUCCESS" "🎉 容器启动成功!" -crond -f >/dev/null +# 自动检测调度模式:有 crond 二进制 → system 模式,否则 node 模式 +if [ -z "$QL_SCHEDULER" ]; then + if command -v crond &>/dev/null; then + export QL_SCHEDULER="system" + else + export QL_SCHEDULER="node" + fi +fi + +if [ "$QL_SCHEDULER" = "system" ]; then + crond -f > /dev/null +else + tail -f /dev/null +fi exec "$@" diff --git a/docs/PROJECT_ARCHITECTURE.md b/docs/PROJECT_ARCHITECTURE.md new file mode 100644 index 00000000..8ea6ffac --- /dev/null +++ b/docs/PROJECT_ARCHITECTURE.md @@ -0,0 +1,550 @@ +# Project Architecture Guide + +This document is written for AI coding agents and maintainers who need to understand and modify this project safely. It focuses on where behavior lives, how the application starts, and which files are usually involved for common changes. + +## Project Summary + +Qinglong is a timed task management platform. It provides a web admin panel for managing cron jobs, scripts, environment variables, subscriptions, dependencies, logs, configuration files, and system settings. + +The repository is organized as a full-stack TypeScript application: + +- `src/`: frontend admin panel, built with Umi Max, React, Ant Design, and Ant Design Pro Layout. +- `back/`: backend application, built with Express, TypeScript, typedi, Sequelize, SQLite, gRPC, and worker processes. +- `shell/`: runtime shell scripts used to execute tasks and preload task environments. +- `data/`: local runtime data, including scripts, logs, configs, SQLite database, uploaded files, and cloned repositories. +- `static/`: built frontend and backend artifacts. +- `docker/`: Docker images, compose file, and entrypoint. +- `sample/`: sample scripts and default config templates. + +## High-Level Runtime Flow + +```text +Browser + -> src/pages/* + -> src/utils/http.tsx + -> /api/* + -> back/api/* + -> back/services/* + -> back/data/* Sequelize models + -> data/db/database.sqlite + +Cron/task execution + -> back/services/cron.ts + -> shell/task.sh or shell/otask.sh + -> data/scripts/* + -> data/log/* + +Frontend assets in production + -> static/dist/* + -> served by back/loaders/express.ts +``` + +## Main Startup Path + +Development starts from `package.json`: + +```bash +pnpm start +``` + +This runs: + +- `start:back`: `nodemon ./back/app.ts` +- `start:front`: `max dev` + +Backend startup begins in `back/app.ts`. + +Important details: + +- The backend uses Node `cluster`. +- The primary process initializes the database first. +- A gRPC worker starts before the HTTP worker. +- The HTTP worker starts Express and serves API routes plus frontend static files. +- If the gRPC worker restarts, the HTTP worker is asked to re-register cron jobs. + +Production-style backend output is generated by: + +```bash +pnpm run build:back +``` + +The compiled backend is placed under `static/build`. + +Frontend output is generated by: + +```bash +pnpm run build:front +``` + +The compiled frontend is placed under `static/dist`. + +## Backend Architecture + +### Entry Point + +- `back/app.ts` + +Responsibilities: + +- Creates the Express application. +- Starts primary/worker process logic. +- Initializes database in the primary process. +- Starts gRPC and HTTP workers. +- Handles graceful shutdown. +- Re-registers cron jobs after gRPC worker recovery. + +### Loaders + +- `back/loaders/app.ts` +- `back/loaders/express.ts` +- `back/loaders/db.ts` +- `back/loaders/depInjector.ts` +- `back/loaders/initData.ts` +- `back/loaders/initFile.ts` +- `back/loaders/initTask.ts` +- `back/loaders/server.ts` +- `back/loaders/sock.ts` + +Loader responsibilities: + +- Register dependency injection bindings. +- Sync Sequelize models. +- Initialize files and default data. +- Initialize scheduled tasks. +- Configure Express middleware. +- Register routes. +- Attach socket/server behavior. + +`back/loaders/express.ts` is the main HTTP middleware and routing setup. It handles: + +- CORS. +- Helmet. +- body parser. +- static frontend serving. +- JWT validation. +- token validation against shared auth state. +- `/open/*` rewrite to `/api/*`. +- route mounting through `back/api/index.ts`. +- frontend fallback to `static/dist/index.html`. +- API error handling. + +### API Routes + +- `back/api/index.ts` + +This file registers all API modules: + +- `user.ts`: login, initialization, authentication-related user endpoints. +- `env.ts`: environment variable endpoints. +- `config.ts`: config file endpoints. +- `log.ts`: log endpoints. +- `cron.ts`: cron/task endpoints. +- `script.ts`: script file endpoints. +- `open.ts`: open API/app token endpoints. +- `dependence.ts`: dependency management endpoints. +- `system.ts`: system information/settings endpoints. +- `subscription.ts`: subscription endpoints. +- `update.ts`: update/check endpoints. +- `health.ts`: health check endpoints. + +Route files should stay thin. They should validate input, get a service from `typedi`'s `Container`, call the service, and return `{ code, data, message }` style responses. + +### Services + +- `back/services/*` + +Services contain most business logic. Common examples: + +- `cron.ts`: create/update/delete/run cron jobs, generate crontab data, manage logs, call scheduler client. +- `env.ts`: manage environment variables. +- `config.ts`: read/write config files. +- `script.ts`: manage script files. +- `subscription.ts`: manage script subscriptions and repository pulls. +- `dependence.ts`: install/manage runtime dependencies. +- `system.ts`: system info and settings. +- `notify.ts`: notification behavior. +- `sock.ts`: socket/log stream behavior. +- `grpc.ts`: gRPC server lifecycle. +- `http.ts`: HTTP server lifecycle. + +When changing backend behavior, first find the API route, then follow it into the matching service. In most cases, the service is the right place for behavioral changes. + +### Data Models + +- `back/data/index.ts` +- `back/data/*.ts` + +The backend uses Sequelize with SQLite. Database storage is configured in `back/data/index.ts`: + +```text +data/db/database.sqlite +``` + +Common model files: + +- `cron.ts`: cron job model. +- `cronView.ts`: saved cron table views. +- `env.ts`: environment variable model. +- `dependence.ts`: dependency model. +- `open.ts`: open API app/token model. +- `subscription.ts`: subscription model. +- `system.ts`: system settings model. +- `notify.ts`: notification-related data. + +Model sync and simple column migrations are currently handled in `back/loaders/db.ts`. + +### Configuration + +- `back/config/index.ts` + +This is the central runtime config file. It reads `.env`, establishes `QL_DIR`, and defines important paths: + +- `dataPath`: runtime data root. +- `configPath`: config files. +- `scriptPath`: user scripts. +- `repoPath`: subscription repositories. +- `logPath`: task logs. +- `dbPath`: SQLite database location. +- `uploadPath`: uploaded files. +- `shellPath`: shell runtime scripts. +- `preloadPath`: JS/Python/Shell preload files. + +Before hardcoding paths, check `back/config/index.ts`. + +### Scheduling And gRPC + +- `back/schedule/*` +- `back/protos/*` +- `back/services/grpc.ts` + +The project has two scheduling paths: + +- Standard crontab-style tasks are persisted and written through backend cron logic. +- Node/gRPC scheduler logic handles cases such as second-level cron expressions or additional schedules. + +`back/services/cron.ts` decides whether a task needs the Node scheduler using schedule shape and `extra_schedules`. + +### Shared Backend Utilities + +- `back/shared/*` +- `back/config/util.ts` +- `back/config/share.ts` +- `back/config/http.ts` + +Use these before adding new global helpers. Existing shared code includes: + +- auth helpers. +- shared store. +- log stream manager. +- task runner helpers. +- concurrency limits. +- file locking utilities. +- HTTP/proxy helpers. + +## Frontend Architecture + +### Umi Config + +- `.umirc.ts` + +Important behavior: + +- Dev server proxies API requests to `http://127.0.0.1:5700/`. +- Frontend build output is `static/dist`. +- Runtime env script is loaded from `./api/env.js`. +- `QlBaseUrl` affects frontend public path and routing base. + +### App Initialization + +- `src/app.ts` + +Responsibilities: + +- Load Chinese and English locale JSON. +- Determine locale from URL/cookie/localStorage. +- Set Umi locale. +- Apply `QlBaseUrl` as public path and router basename. + +### Layout And Routes + +- `src/layouts/defaultProps.tsx` +- `src/layouts/index.tsx` + +`defaultProps.tsx` defines the main route/menu list. If adding a new page visible in the sidebar, update this file. + +Current major pages: + +- `src/pages/crontab`: timed task management. +- `src/pages/subscription`: subscription management. +- `src/pages/env`: environment variables. +- `src/pages/config`: config files. +- `src/pages/script`: script management. +- `src/pages/dependence`: dependency management. +- `src/pages/log`: log management. +- `src/pages/diff`: diff tool. +- `src/pages/setting`: system settings. +- `src/pages/login`: login. +- `src/pages/initialization`: first-run initialization. +- `src/pages/error`: error page. + +### Frontend Utilities + +- `src/utils/http.tsx`: API request helper. +- `src/utils/websocket.ts`: socket connection behavior. +- `src/utils/config.ts`: frontend config helpers. +- `src/utils/const.ts`: constants. +- `src/utils/date.ts`: date formatting helpers. +- `src/utils/init.ts`: initialization helpers. +- `src/utils/codemirror/*`: CodeMirror integration. +- `src/utils/monaco/*`: Monaco integration. + +When changing a page's API behavior, inspect both the page file and `src/utils/http.tsx`. + +### Components And Styling + +- `src/components/*`: reusable UI components. +- `src/pages/**/index.less`: page-level styles. +- `src/pages/script/index.module.less` and `src/pages/log/index.module.less`: CSS module styles. +- `src/assets/fonts/*`: bundled fonts. +- `src/locales/*.json`: i18n text. + +Follow the existing Ant Design and Ant Design Pro patterns when modifying UI. + +## Shell Runtime + +- `shell/task.sh`: task execution path. +- `shell/otask.sh`: alternate/manual task execution path. +- `shell/api.sh`: shell-side API helpers. +- `shell/env.sh`: environment setup. +- `shell/check.sh`: runtime check helpers. +- `shell/update.sh`: update helpers. +- `shell/rmlog.sh`: log cleanup. +- `shell/share.sh`: shared shell helpers. +- `shell/preload/*`: preload files injected into JS/Python/Shell task environments. + +The backend often coordinates task execution, but the actual user script process environment is shaped by files in `shell/`. + +## Runtime Data Directory + +- `data/` + +This directory is runtime state, not just source code. Be careful when modifying or deleting files here. + +Important subdirectories: + +- `data/db`: SQLite database. +- `data/config`: generated and user-edited config files. +- `data/scripts`: user scripts. +- `data/repo`: cloned subscription repositories. +- `data/log`: task logs. +- `data/upload`: uploaded files. +- `data/syslog`: system logs. +- `data/ssh.d`: SSH-related runtime data. +- `data/dep_cache`: dependency cache, when present. + +Many bugs that appear as "backend logic" may involve state stored under `data/`. + +## Docker And Release Files + +- `docker/Dockerfile` +- `docker/310.Dockerfile` +- `docker/docker-compose.yml` +- `docker/docker-entrypoint.sh` +- `ecosystem.config.js` +- `version.yaml` + +Use these when changing deployment, container startup, PM2 behavior, or release metadata. + +## Common Modification Map + +### Add Or Modify A Backend API + +Typical files: + +1. Add or update route in `back/api/.ts`. +2. Add or update service logic in `back/services/.ts`. +3. Add or update model in `back/data/.ts` if persistence changes. +4. Add validation with `celebrate`/`Joi` near the route. +5. Update frontend caller in `src/pages/**` or `src/utils/**`. + +### Add A New Frontend Page + +Typical files: + +1. Create `src/pages//index.tsx`. +2. Add styles in `src/pages//index.less` if needed. +3. Register route/menu in `src/layouts/defaultProps.tsx`. +4. Add locale strings in `src/locales/zh-CN.json` and `src/locales/en-US.json`. +5. Add API calls through the existing request helper. + +### Change Cron/Task Behavior + +Start with: + +- `back/api/cron.ts` +- `back/services/cron.ts` +- `back/schedule/*` +- `shell/task.sh` +- `shell/otask.sh` +- `shell/preload/*` + +Also inspect: + +- `back/data/cron.ts` +- `back/validation/schedule.ts` +- `data/config/crontab.list` +- `data/log/*` + +### Change Environment Variable Behavior + +Start with: + +- `back/api/env.ts` +- `back/services/env.ts` +- `back/data/env.ts` +- `src/pages/env/index.tsx` + +Also inspect: + +- `shell/preload/env.sh` +- `shell/preload/env.js` +- `shell/preload/env.py` + +### Change Script Management + +Start with: + +- `back/api/script.ts` +- `back/services/script.ts` +- `src/pages/script/index.tsx` +- `data/scripts/*` + +### Change Login/Auth/Security + +Start with: + +- `back/api/user.ts` +- `back/services/user.ts` +- `back/shared/auth.ts` +- `back/shared/store.ts` +- `back/loaders/express.ts` +- `back/token.ts` +- `src/pages/login/index.tsx` +- `src/pages/initialization/index.tsx` + +Be careful with: + +- JWT behavior. +- open API token behavior. +- first-run initialization. +- platform-specific session limits. + +### Change Subscription Behavior + +Start with: + +- `back/api/subscription.ts` +- `back/services/subscription.ts` +- `back/data/subscription.ts` +- `src/pages/subscription/index.tsx` +- `data/repo/*` + +### Change Dependency Management + +Start with: + +- `back/api/dependence.ts` +- `back/services/dependence.ts` +- `back/data/dependence.ts` +- `src/pages/dependence/index.tsx` +- `data/deps` +- `data/dep_cache` + +### Change Logs Or Live Log Streaming + +Start with: + +- `back/api/log.ts` +- `back/services/log.ts` +- `back/services/sock.ts` +- `back/shared/logStreamManager.ts` +- `src/pages/log/index.tsx` +- `src/components/terminal.tsx` +- `data/log/*` + +## Coding Conventions + +Backend: + +- Prefer adding business logic to services, not route files. +- Use `typedi` services consistently. +- Use existing config paths from `back/config/index.ts`. +- Return API responses in the existing `{ code, data, message }` shape. +- Use existing utilities before adding new helpers. +- Preserve current SQLite/Sequelize style unless doing a larger data-layer refactor. + +Frontend: + +- Follow existing Umi/React/Ant Design patterns. +- Keep route/menu changes in `src/layouts/defaultProps.tsx`. +- Use existing request/WebSocket helpers. +- Add or update locale strings for visible UI text. +- Keep page-specific styles near the page. + +Shell/runtime: + +- Treat `shell/` as part of production behavior. +- Test task execution changes with realistic scripts when possible. +- Be careful with path quoting and environment variable propagation. + +Data: + +- Treat `data/` as mutable runtime state. +- Do not delete runtime state unless explicitly requested. +- Schema changes should account for existing SQLite databases. + +## Suggested First Steps For AI Agents + +When asked to modify behavior: + +1. Identify whether the change is frontend, backend, shell runtime, data model, or deployment. +2. Search by feature name in `src/pages`, `back/api`, and `back/services`. +3. Read the route file and matching service before editing. +4. If persistence is involved, read the matching `back/data` model and `back/loaders/db.ts`. +5. If task execution is involved, inspect `shell/` and `back/services/cron.ts`. +6. Make the smallest scoped change that matches existing patterns. +7. Run the most relevant check: + - `pnpm run build:back` for backend TypeScript changes. + - `pnpm run build:front` for frontend build changes. + - targeted manual task/API checks for shell and scheduler changes. + +## Quick Directory Reference + +```text +. +├── back/ Backend TypeScript application +│ ├── api/ Express route modules +│ ├── config/ Runtime config, paths, constants, helpers +│ ├── data/ Sequelize models and SQLite connection +│ ├── loaders/ Startup initialization and Express setup +│ ├── middlewares/ Express middlewares +│ ├── protos/ gRPC proto files and generated TS +│ ├── schedule/ Scheduler/gRPC client helpers +│ ├── services/ Business logic services +│ ├── shared/ Shared backend utilities +│ └── validation/ Joi validation schemas +├── src/ Frontend Umi/React application +│ ├── assets/ Fonts and static frontend assets +│ ├── components/ Shared UI components +│ ├── hooks/ Frontend hooks +│ ├── layouts/ Main layout and menu route config +│ ├── locales/ i18n JSON +│ ├── pages/ Feature pages +│ └── utils/ HTTP, WebSocket, editor, date, and config utilities +├── shell/ Task runtime shell scripts and preload files +├── data/ Runtime state: db, logs, scripts, repos, configs +├── docker/ Docker build and compose files +├── sample/ Sample scripts and default config templates +├── static/ Built frontend/backend artifacts +└── docs/ Project documentation +``` diff --git a/package.json b/package.json index 038f8c1d..3e942705 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,17 @@ { - "private": true, + "name": "@whyour/qinglong", "packageManager": "pnpm@8.3.1", + "version": "2.20.2-3", + "description": "Timed task management platform supporting Python3, JavaScript, Shell, Typescript", + "repository": { + "type": "git", + "url": "https://github.com/whyour/qinglong.git" + }, + "author": "whyour", + "license": "Apache License 2.0", + "bugs": { + "url": "https://github.com/whyour/qinglong/issues" + }, "scripts": { "start": "concurrently -n w: npm:start:*", "start:back": "nodemon ./back/app.ts", @@ -25,6 +36,11 @@ "prettier --parser=typescript --write" ] }, + "bin": { + "ql": "shell/update.sh", + "task": "shell/task.sh", + "qinglong": "shell/start.sh" + }, "pnpm": { "peerDependencyRules": { "ignoreMissing": [ @@ -51,7 +67,9 @@ } }, "overrides": { - "sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3" + "sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3", + "@codemirror/state": "6.5.4", + "@codemirror/view": "6.39.16" } }, "dependencies": { @@ -77,9 +95,9 @@ "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", - "multer": "1.4.5-lts.1", + "multer": "2.1.1", "node-schedule": "^2.1.0", - "nodemailer": "^6.9.16", + "nodemailer": "^8.0.1", "p-queue-cjs": "7.3.4", "@bufbuild/protobuf": "^2.10.0", "ps-tree": "^1.2.0", @@ -104,8 +122,8 @@ "moment": "2.30.1", "@ant-design/icons": "^5.0.1", "@ant-design/pro-layout": "6.38.22", - "@codemirror/view": "^6.34.1", - "@codemirror/state": "^6.4.1", + "@codemirror/view": "6.39.16", + "@codemirror/state": "6.5.4", "@monaco-editor/react": "4.2.1", "@react-hook/resize-observer": "^2.0.2", "react-router-dom": "6.26.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c8e0b0f..b26696ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,11 +1,9 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - overrides: sqlite3: git+https://github.com/whyour/node-sqlite3.git#v1.0.3 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 dependencies: '@bufbuild/protobuf': @@ -90,14 +88,14 @@ dependencies: specifier: ^4.17.21 version: 4.17.21 multer: - specifier: 1.4.5-lts.1 - version: 1.4.5-lts.1 + specifier: 2.1.1 + version: 2.1.1 node-schedule: specifier: ^2.1.0 version: 2.1.1 nodemailer: - specifier: ^6.9.16 - version: 6.9.16 + specifier: ^8.0.1 + version: 8.0.1 p-queue-cjs: specifier: 7.3.4 version: 7.3.4 @@ -149,11 +147,11 @@ devDependencies: specifier: 6.38.22 version: 6.38.22(prop-types@15.8.1)(react-dom@18.3.1)(react@18.3.1) '@codemirror/state': - specifier: ^6.4.1 - version: 6.4.1 + specifier: 6.5.4 + version: 6.5.4 '@codemirror/view': - specifier: ^6.34.1 - version: 6.35.0 + specifier: 6.39.16 + version: 6.39.16 '@monaco-editor/react': specifier: 4.2.1 version: 4.2.1(monaco-editor@0.33.0)(react-dom@18.3.1)(react@18.3.1) @@ -240,10 +238,10 @@ devDependencies: version: 8.3.4 '@uiw/codemirror-extensions-langs': specifier: ^4.21.9 - version: 4.23.6(@codemirror/autocomplete@6.18.3)(@codemirror/language-data@6.5.1)(@codemirror/language@6.10.6)(@codemirror/legacy-modes@6.4.2)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3)(@lezer/highlight@1.2.1)(@lezer/javascript@1.4.21)(@lezer/lr@1.4.2) + version: 4.23.6(@codemirror/autocomplete@6.20.1)(@codemirror/language-data@6.5.2)(@codemirror/language@6.12.2)(@codemirror/legacy-modes@6.5.2)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(@lezer/common@1.5.1)(@lezer/highlight@1.2.3)(@lezer/javascript@1.5.4)(@lezer/lr@1.4.8) '@uiw/react-codemirror': specifier: ^4.21.9 - version: 4.23.6(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.18.3)(@codemirror/language@6.10.6)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.35.0)(codemirror@6.0.1)(react-dom@18.3.1)(react@18.3.1) + version: 4.23.6(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.39.16)(codemirror@6.0.2)(react-dom@18.3.1)(react@18.3.1) '@umijs/max': specifier: ^4.4.4 version: 4.4.4(@types/node@17.0.45)(@types/react-dom@18.3.1)(@types/react@18.3.12)(prettier@2.8.8)(react-dom@18.3.1)(react@18.3.1)(sockjs-client@1.6.1)(typescript@5.2.2) @@ -934,7 +932,7 @@ packages: '@babel/traverse': 7.25.9 '@babel/types': 7.26.0 convert-source-map: 2.0.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -957,7 +955,7 @@ packages: '@babel/traverse': 7.25.9 '@babel/types': 7.26.0 convert-source-map: 2.0.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -1350,6 +1348,11 @@ packages: regenerator-runtime: 0.14.1 dev: true + /@babel/runtime@7.28.6: + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/template@7.25.9: resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} engines: {node: '>=6.9.0'} @@ -1368,7 +1371,7 @@ packages: '@babel/parser': 7.26.2 '@babel/template': 7.25.9 '@babel/types': 7.26.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.4.3 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -1402,7 +1405,7 @@ packages: tinycolor2: 1.6.0 dev: true - /@codemirror/autocomplete@6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3): + /@codemirror/autocomplete@6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(@lezer/common@1.2.3): resolution: {integrity: sha512-1dNIOmiM0z4BIBwxmxEfA1yoxh1MF/6KPBbh20a5vphGV0ictKlgQsbJs6D6SkR6iJpGbpwRsa6PFMNlg9T9pQ==} peerDependencies: '@codemirror/language': ^6.0.0 @@ -1411,17 +1414,35 @@ packages: '@lezer/common': ^1.0.0 dependencies: '@codemirror/language': 6.10.6 - '@codemirror/state': 6.4.1 - '@codemirror/view': 6.35.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 '@lezer/common': 1.2.3 dev: true + /@codemirror/autocomplete@6.20.1: + resolution: {integrity: sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==} + dependencies: + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + dev: true + + /@codemirror/commands@6.10.2: + resolution: {integrity: sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==} + dependencies: + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + dev: true + /@codemirror/commands@6.7.1: resolution: {integrity: sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==} dependencies: '@codemirror/language': 6.10.6 - '@codemirror/state': 6.4.1 - '@codemirror/view': 6.35.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 '@lezer/common': 1.2.3 dev: true @@ -1436,6 +1457,17 @@ packages: '@lezer/lr': 1.4.2 dev: true + /@codemirror/lang-angular@0.1.4: + resolution: {integrity: sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==} + dependencies: + '@codemirror/lang-html': 6.4.11 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.2 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + dev: true + /@codemirror/lang-cpp@6.0.2: resolution: {integrity: sha512-6oYEYUKHvrnacXxWxYa6t4puTlbN3dgV662BDfSH8+MfjQjVmP697/KYTDOqpxgerkvoNm7q5wlFMBeX8ZMocg==} dependencies: @@ -1443,39 +1475,58 @@ packages: '@lezer/cpp': 1.1.2 dev: true - /@codemirror/lang-css@6.3.1(@codemirror/view@6.35.0): + /@codemirror/lang-cpp@6.0.3: + resolution: {integrity: sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==} + dependencies: + '@codemirror/language': 6.12.2 + '@lezer/cpp': 1.1.5 + dev: true + + /@codemirror/lang-css@6.3.1(@codemirror/view@6.39.16): resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} dependencies: - '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3) + '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(@lezer/common@1.2.3) '@codemirror/language': 6.10.6 - '@codemirror/state': 6.4.1 + '@codemirror/state': 6.5.4 '@lezer/common': 1.2.3 '@lezer/css': 1.1.9 transitivePeerDependencies: - '@codemirror/view' dev: true - /@codemirror/lang-go@6.0.1(@codemirror/view@6.35.0): + /@codemirror/lang-go@6.0.1: resolution: {integrity: sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==} dependencies: - '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3) - '@codemirror/language': 6.10.6 - '@codemirror/state': 6.4.1 - '@lezer/common': 1.2.3 - '@lezer/go': 1.0.0 - transitivePeerDependencies: - - '@codemirror/view' + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.1 + '@lezer/go': 1.0.1 + dev: true + + /@codemirror/lang-html@6.4.11: + resolution: {integrity: sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==} + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/lang-css': 6.3.1(@codemirror/view@6.39.16) + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + '@lezer/css': 1.3.1 + '@lezer/html': 1.3.13 dev: true /@codemirror/lang-html@6.4.9: resolution: {integrity: sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==} dependencies: - '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3) - '@codemirror/lang-css': 6.3.1(@codemirror/view@6.35.0) + '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(@lezer/common@1.2.3) + '@codemirror/lang-css': 6.3.1(@codemirror/view@6.39.16) '@codemirror/lang-javascript': 6.2.2 '@codemirror/language': 6.10.6 - '@codemirror/state': 6.4.1 - '@codemirror/view': 6.35.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 '@lezer/common': 1.2.3 '@lezer/css': 1.1.9 '@lezer/html': 1.3.10 @@ -1488,18 +1539,47 @@ packages: '@lezer/java': 1.1.3 dev: true + /@codemirror/lang-java@6.0.2: + resolution: {integrity: sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==} + dependencies: + '@codemirror/language': 6.12.2 + '@lezer/java': 1.1.3 + dev: true + /@codemirror/lang-javascript@6.2.2: resolution: {integrity: sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==} dependencies: - '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3) + '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(@lezer/common@1.2.3) '@codemirror/language': 6.10.6 '@codemirror/lint': 6.8.4 - '@codemirror/state': 6.4.1 - '@codemirror/view': 6.35.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 '@lezer/common': 1.2.3 '@lezer/javascript': 1.4.21 dev: true + /@codemirror/lang-javascript@6.2.5: + resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==} + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.2 + '@codemirror/lint': 6.9.5 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + '@lezer/javascript': 1.5.4 + dev: true + + /@codemirror/lang-jinja@6.0.0: + resolution: {integrity: sha512-47MFmRcR8UAxd8DReVgj7WJN1WSAMT7OJnewwugZM4XiHWkOjgJQqvEM1NpMj9ALMPyxmlziEI1opH9IaEvmaw==} + dependencies: + '@codemirror/lang-html': 6.4.11 + '@codemirror/language': 6.12.2 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + dev: true + /@codemirror/lang-json@6.0.1: resolution: {integrity: sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==} dependencies: @@ -1507,10 +1587,17 @@ packages: '@lezer/json': 1.0.2 dev: true - /@codemirror/lang-less@6.0.2(@codemirror/view@6.35.0): + /@codemirror/lang-json@6.0.2: + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} + dependencies: + '@codemirror/language': 6.12.2 + '@lezer/json': 1.0.3 + dev: true + + /@codemirror/lang-less@6.0.2(@codemirror/view@6.39.16): resolution: {integrity: sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==} dependencies: - '@codemirror/lang-css': 6.3.1(@codemirror/view@6.35.0) + '@codemirror/lang-css': 6.3.1(@codemirror/view@6.39.16) '@codemirror/language': 6.10.6 '@lezer/common': 1.2.3 '@lezer/highlight': 1.2.1 @@ -1523,7 +1610,7 @@ packages: resolution: {integrity: sha512-WHwjI7OqKFBEfkunohweqA5B/jIlxaZso6Nl3weVckz8EafYbPZldQEKSDb4QQ9H9BUkle4PVELP4sftKoA0uQ==} dependencies: '@codemirror/language': 6.10.6 - '@codemirror/state': 6.4.1 + '@codemirror/state': 6.5.4 '@lezer/common': 1.2.3 '@lezer/lezer': 1.1.2 dev: true @@ -1531,50 +1618,95 @@ packages: /@codemirror/lang-liquid@6.2.2: resolution: {integrity: sha512-7Dm841fk37+JQW6j2rI1/uGkJyESrjzyhiIkaLjbbR0U6aFFQvMrJn35WxQreRMADMhzkyVkZM4467OR7GR8nQ==} dependencies: - '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3) + '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(@lezer/common@1.2.3) '@codemirror/lang-html': 6.4.9 '@codemirror/language': 6.10.6 - '@codemirror/state': 6.4.1 - '@codemirror/view': 6.35.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 '@lezer/common': 1.2.3 '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 dev: true + /@codemirror/lang-liquid@6.3.2: + resolution: {integrity: sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw==} + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/lang-html': 6.4.11 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + dev: true + /@codemirror/lang-markdown@6.3.1: resolution: {integrity: sha512-y3sSPuQjBKZQbQwe3ZJKrSW6Silyl9PnrU/Mf0m2OQgIlPoSYTtOvEL7xs94SVMkb8f4x+SQFnzXPdX4Wk2lsg==} dependencies: - '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3) + '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(@lezer/common@1.2.3) '@codemirror/lang-html': 6.4.9 '@codemirror/language': 6.10.6 - '@codemirror/state': 6.4.1 - '@codemirror/view': 6.35.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 '@lezer/common': 1.2.3 '@lezer/markdown': 1.3.2 dev: true + /@codemirror/lang-markdown@6.5.0: + resolution: {integrity: sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==} + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/lang-html': 6.4.11 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + '@lezer/markdown': 1.6.3 + dev: true + /@codemirror/lang-php@6.0.1: resolution: {integrity: sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA==} dependencies: '@codemirror/lang-html': 6.4.9 '@codemirror/language': 6.10.6 - '@codemirror/state': 6.4.1 + '@codemirror/state': 6.5.4 '@lezer/common': 1.2.3 '@lezer/php': 1.0.2 dev: true - /@codemirror/lang-python@6.1.6(@codemirror/view@6.35.0): + /@codemirror/lang-php@6.0.2: + resolution: {integrity: sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==} + dependencies: + '@codemirror/lang-html': 6.4.11 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.1 + '@lezer/php': 1.0.5 + dev: true + + /@codemirror/lang-python@6.1.6(@codemirror/view@6.39.16): resolution: {integrity: sha512-ai+01WfZhWqM92UqjnvorkxosZ2aq2u28kHvr+N3gu012XqY2CThD67JPMHnGceRfXPDBmn1HnyqowdpF57bNg==} dependencies: - '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3) + '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(@lezer/common@1.2.3) '@codemirror/language': 6.10.6 - '@codemirror/state': 6.4.1 + '@codemirror/state': 6.5.4 '@lezer/common': 1.2.3 '@lezer/python': 1.1.14 transitivePeerDependencies: - '@codemirror/view' dev: true + /@codemirror/lang-python@6.2.1: + resolution: {integrity: sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==} + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.1 + '@lezer/python': 1.1.18 + dev: true + /@codemirror/lang-rust@6.0.1: resolution: {integrity: sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ==} dependencies: @@ -1582,24 +1714,42 @@ packages: '@lezer/rust': 1.0.2 dev: true - /@codemirror/lang-sass@6.0.2(@codemirror/view@6.35.0): + /@codemirror/lang-rust@6.0.2: + resolution: {integrity: sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==} + dependencies: + '@codemirror/language': 6.12.2 + '@lezer/rust': 1.0.2 + dev: true + + /@codemirror/lang-sass@6.0.2(@codemirror/view@6.39.16): resolution: {integrity: sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==} dependencies: - '@codemirror/lang-css': 6.3.1(@codemirror/view@6.35.0) + '@codemirror/lang-css': 6.3.1(@codemirror/view@6.39.16) '@codemirror/language': 6.10.6 - '@codemirror/state': 6.4.1 + '@codemirror/state': 6.5.4 '@lezer/common': 1.2.3 '@lezer/sass': 1.0.7 transitivePeerDependencies: - '@codemirror/view' dev: true - /@codemirror/lang-sql@6.8.0(@codemirror/view@6.35.0): + /@codemirror/lang-sql@6.10.0: + resolution: {integrity: sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==} + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + dev: true + + /@codemirror/lang-sql@6.8.0(@codemirror/view@6.39.16): resolution: {integrity: sha512-aGLmY4OwGqN3TdSx3h6QeA1NrvaYtF7kkoWR/+W7/JzB0gQtJ+VJxewlnE3+VImhA4WVlhmkJr109PefOOhjLg==} dependencies: - '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3) + '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(@lezer/common@1.2.3) '@codemirror/language': 6.10.6 - '@codemirror/state': 6.4.1 + '@codemirror/state': 6.5.4 '@lezer/common': 1.2.3 '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 @@ -1630,52 +1780,52 @@ packages: /@codemirror/lang-xml@6.1.0: resolution: {integrity: sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==} dependencies: - '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3) + '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(@lezer/common@1.2.3) '@codemirror/language': 6.10.6 - '@codemirror/state': 6.4.1 - '@codemirror/view': 6.35.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 '@lezer/common': 1.2.3 '@lezer/xml': 1.0.5 dev: true - /@codemirror/lang-yaml@6.1.1(@codemirror/view@6.35.0): - resolution: {integrity: sha512-HV2NzbK9bbVnjWxwObuZh5FuPCowx51mEfoFT9y3y+M37fA3+pbxx4I7uePuygFzDsAmCTwQSc/kXh/flab4uw==} + /@codemirror/lang-yaml@6.1.2: + resolution: {integrity: sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==} dependencies: - '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3) - '@codemirror/language': 6.10.6 - '@codemirror/state': 6.4.1 - '@lezer/common': 1.2.3 - '@lezer/highlight': 1.2.1 - '@lezer/yaml': 1.0.3 - transitivePeerDependencies: - - '@codemirror/view' + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + '@lezer/yaml': 1.0.4 dev: true - /@codemirror/language-data@6.5.1(@codemirror/view@6.35.0): - resolution: {integrity: sha512-0sWxeUSNlBr6OmkqybUTImADFUP0M3P0IiSde4nc24bz/6jIYzqYSgkOSLS+CBIoW1vU8Q9KUWXscBXeoMVC9w==} + /@codemirror/language-data@6.5.2(@codemirror/view@6.39.16): + resolution: {integrity: sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg==} dependencies: - '@codemirror/lang-angular': 0.1.3 - '@codemirror/lang-cpp': 6.0.2 - '@codemirror/lang-css': 6.3.1(@codemirror/view@6.35.0) - '@codemirror/lang-go': 6.0.1(@codemirror/view@6.35.0) - '@codemirror/lang-html': 6.4.9 - '@codemirror/lang-java': 6.0.1 - '@codemirror/lang-javascript': 6.2.2 - '@codemirror/lang-json': 6.0.1 - '@codemirror/lang-less': 6.0.2(@codemirror/view@6.35.0) - '@codemirror/lang-liquid': 6.2.2 - '@codemirror/lang-markdown': 6.3.1 - '@codemirror/lang-php': 6.0.1 - '@codemirror/lang-python': 6.1.6(@codemirror/view@6.35.0) - '@codemirror/lang-rust': 6.0.1 - '@codemirror/lang-sass': 6.0.2(@codemirror/view@6.35.0) - '@codemirror/lang-sql': 6.8.0(@codemirror/view@6.35.0) + '@codemirror/lang-angular': 0.1.4 + '@codemirror/lang-cpp': 6.0.3 + '@codemirror/lang-css': 6.3.1(@codemirror/view@6.39.16) + '@codemirror/lang-go': 6.0.1 + '@codemirror/lang-html': 6.4.11 + '@codemirror/lang-java': 6.0.2 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/lang-jinja': 6.0.0 + '@codemirror/lang-json': 6.0.2 + '@codemirror/lang-less': 6.0.2(@codemirror/view@6.39.16) + '@codemirror/lang-liquid': 6.3.2 + '@codemirror/lang-markdown': 6.5.0 + '@codemirror/lang-php': 6.0.2 + '@codemirror/lang-python': 6.2.1 + '@codemirror/lang-rust': 6.0.2 + '@codemirror/lang-sass': 6.0.2(@codemirror/view@6.39.16) + '@codemirror/lang-sql': 6.10.0 '@codemirror/lang-vue': 0.1.3 '@codemirror/lang-wast': 6.0.2 '@codemirror/lang-xml': 6.1.0 - '@codemirror/lang-yaml': 6.1.1(@codemirror/view@6.35.0) - '@codemirror/language': 6.10.6 - '@codemirror/legacy-modes': 6.4.2 + '@codemirror/lang-yaml': 6.1.2 + '@codemirror/language': 6.12.2 + '@codemirror/legacy-modes': 6.5.2 transitivePeerDependencies: - '@codemirror/view' dev: true @@ -1683,54 +1833,76 @@ packages: /@codemirror/language@6.10.6: resolution: {integrity: sha512-KrsbdCnxEztLVbB5PycWXFxas4EOyk/fPAfruSOnDDppevQgid2XZ+KbJ9u+fDikP/e7MW7HPBTvTb8JlZK9vA==} dependencies: - '@codemirror/state': 6.4.1 - '@codemirror/view': 6.35.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 '@lezer/common': 1.2.3 '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 - style-mod: 4.1.2 + style-mod: 4.1.3 dev: true - /@codemirror/legacy-modes@6.4.2: - resolution: {integrity: sha512-HsvWu08gOIIk303eZQCal4H4t65O/qp1V4ul4zVa3MHK5FJ0gz3qz3O55FIkm+aQUcshUOjBx38t2hPiJwW5/g==} + /@codemirror/language@6.12.2: + resolution: {integrity: sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==} dependencies: - '@codemirror/language': 6.10.6 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + style-mod: 4.1.3 + dev: true + + /@codemirror/legacy-modes@6.5.2: + resolution: {integrity: sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==} + dependencies: + '@codemirror/language': 6.12.2 dev: true /@codemirror/lint@6.8.4: resolution: {integrity: sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==} dependencies: - '@codemirror/state': 6.4.1 - '@codemirror/view': 6.35.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 crelt: 1.0.6 dev: true - /@codemirror/search@6.5.8: - resolution: {integrity: sha512-PoWtZvo7c1XFeZWmmyaOp2G0XVbOnm+fJzvghqGAktBW3cufwJUWvSCcNG0ppXiBEM05mZu6RhMtXPv2hpllig==} + /@codemirror/lint@6.9.5: + resolution: {integrity: sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==} dependencies: - '@codemirror/state': 6.4.1 - '@codemirror/view': 6.35.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 crelt: 1.0.6 dev: true - /@codemirror/state@6.4.1: - resolution: {integrity: sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==} + /@codemirror/search@6.6.0: + resolution: {integrity: sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==} + dependencies: + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + crelt: 1.0.6 dev: true - /@codemirror/theme-one-dark@6.1.2: - resolution: {integrity: sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==} + /@codemirror/state@6.5.4: + resolution: {integrity: sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==} dependencies: - '@codemirror/language': 6.10.6 - '@codemirror/state': 6.4.1 - '@codemirror/view': 6.35.0 - '@lezer/highlight': 1.2.1 + '@marijn/find-cluster-break': 1.0.2 dev: true - /@codemirror/view@6.35.0: - resolution: {integrity: sha512-I0tYy63q5XkaWsJ8QRv5h6ves7kvtrBWjBcnf/bzohFJQc5c14a1AQRdE8QpPF9eMp5Mq2FMm59TCj1gDfE7kw==} + /@codemirror/theme-one-dark@6.1.3: + resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} dependencies: - '@codemirror/state': 6.4.1 - style-mod: 4.1.2 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/highlight': 1.2.3 + dev: true + + /@codemirror/view@6.39.16: + resolution: {integrity: sha512-m6S22fFpKtOWhq8HuhzsI1WzUP/hB9THbDj0Tl5KX4gbO6Y91hwBl7Yky33NdvB6IffuRFiBxf1R8kJMyXmA4Q==} + dependencies: + '@codemirror/state': 6.5.4 + crelt: 1.0.6 + style-mod: 4.1.3 w3c-keyname: 2.2.8 dev: true @@ -2486,13 +2658,13 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.4.3 espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 import-fresh: 3.3.0 js-yaml: 4.1.0 - minimatch: 3.1.2 + minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color @@ -2617,8 +2789,8 @@ packages: deprecated: Use @eslint/config-array instead dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.7(supports-color@5.5.0) - minimatch: 3.1.2 + debug: 4.4.3 + minimatch: 3.1.5 transitivePeerDependencies: - supports-color dev: true @@ -2643,7 +2815,7 @@ packages: '@antfu/install-pkg': 0.1.1 '@antfu/utils': 0.7.10 '@iconify/types': 2.0.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.4.3 kolorist: 1.8.0 local-pkg: 0.4.3 transitivePeerDependencies: @@ -2656,7 +2828,7 @@ packages: dependencies: string-width: 5.1.2 string-width-cjs: /string-width@4.2.3 - strip-ansi: 7.1.0 + strip-ansi: 7.2.0 strip-ansi-cjs: /strip-ansi@6.0.1 wrap-ansi: 8.1.0 wrap-ansi-cjs: /wrap-ansi@7.0.0 @@ -2809,6 +2981,10 @@ packages: resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} dev: true + /@lezer/common@1.5.1: + resolution: {integrity: sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==} + dev: true + /@lezer/cpp@1.1.2: resolution: {integrity: sha512-macwKtyeUO0EW86r3xWQCzOV9/CF8imJLpJlPv3sDY57cPGeUZ8gXWOWNlJr52TVByMV3PayFQCA5SHEERDmVQ==} dependencies: @@ -2817,6 +2993,14 @@ packages: '@lezer/lr': 1.4.2 dev: true + /@lezer/cpp@1.1.5: + resolution: {integrity: sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw==} + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + dev: true + /@lezer/css@1.1.9: resolution: {integrity: sha512-TYwgljcDv+YrV0MZFFvYFQHCfGgbPMR6nuqLabBdmZoFH3EP1gvw8t0vae326Ne3PszQkbXfVBjCnf3ZVCr0bA==} dependencies: @@ -2825,12 +3009,20 @@ packages: '@lezer/lr': 1.4.2 dev: true - /@lezer/go@1.0.0: - resolution: {integrity: sha512-co9JfT3QqX1YkrMmourYw2Z8meGC50Ko4d54QEcQbEYpvdUvN4yb0NBZdn/9ertgvjsySxHsKzH3lbm3vqJ4Jw==} + /@lezer/css@1.3.1: + resolution: {integrity: sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg==} dependencies: - '@lezer/common': 1.2.3 - '@lezer/highlight': 1.2.1 - '@lezer/lr': 1.4.2 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + dev: true + + /@lezer/go@1.0.1: + resolution: {integrity: sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==} + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 dev: true /@lezer/highlight@1.2.1: @@ -2839,6 +3031,12 @@ packages: '@lezer/common': 1.2.3 dev: true + /@lezer/highlight@1.2.3: + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + dependencies: + '@lezer/common': 1.5.1 + dev: true + /@lezer/html@1.3.10: resolution: {integrity: sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==} dependencies: @@ -2847,6 +3045,14 @@ packages: '@lezer/lr': 1.4.2 dev: true + /@lezer/html@1.3.13: + resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==} + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + dev: true + /@lezer/java@1.1.3: resolution: {integrity: sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==} dependencies: @@ -2863,6 +3069,14 @@ packages: '@lezer/lr': 1.4.2 dev: true + /@lezer/javascript@1.5.4: + resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + dev: true + /@lezer/json@1.0.2: resolution: {integrity: sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==} dependencies: @@ -2871,6 +3085,14 @@ packages: '@lezer/lr': 1.4.2 dev: true + /@lezer/json@1.0.3: + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + dev: true + /@lezer/lezer@1.1.2: resolution: {integrity: sha512-O8yw3CxPhzYHB1hvwbdozjnAslhhR8A5BH7vfEMof0xk3p+/DFDfZkA9Tde6J+88WgtwaHy4Sy6ThZSkaI0Evw==} dependencies: @@ -2884,6 +3106,12 @@ packages: '@lezer/common': 1.2.3 dev: true + /@lezer/lr@1.4.8: + resolution: {integrity: sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==} + dependencies: + '@lezer/common': 1.5.1 + dev: true + /@lezer/markdown@1.3.2: resolution: {integrity: sha512-Wu7B6VnrKTbBEohqa63h5vxXjiC4pO5ZQJ/TDbhJxPQaaIoRD/6UVDhSDtVsCwVZV12vvN9KxuLL3ATMnlG0oQ==} dependencies: @@ -2891,6 +3119,13 @@ packages: '@lezer/highlight': 1.2.1 dev: true + /@lezer/markdown@1.6.3: + resolution: {integrity: sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==} + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + dev: true + /@lezer/php@1.0.2: resolution: {integrity: sha512-GN7BnqtGRpFyeoKSEqxvGvhJQiI4zkgmYnDk/JIyc7H7Ifc1tkPnUn/R2R8meH3h/aBf5rzjvU8ZQoyiNDtDrA==} dependencies: @@ -2899,6 +3134,14 @@ packages: '@lezer/lr': 1.4.2 dev: true + /@lezer/php@1.0.5: + resolution: {integrity: sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==} + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + dev: true + /@lezer/python@1.1.14: resolution: {integrity: sha512-ykDOb2Ti24n76PJsSa4ZoDF0zH12BSw1LGfQXCYJhJyOGiFTfGaX0Du66Ze72R+u/P35U+O6I9m8TFXov1JzsA==} dependencies: @@ -2907,6 +3150,14 @@ packages: '@lezer/lr': 1.4.2 dev: true + /@lezer/python@1.1.18: + resolution: {integrity: sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==} + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + dev: true + /@lezer/rust@1.0.2: resolution: {integrity: sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==} dependencies: @@ -2931,12 +3182,12 @@ packages: '@lezer/lr': 1.4.2 dev: true - /@lezer/yaml@1.0.3: - resolution: {integrity: sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==} + /@lezer/yaml@1.0.4: + resolution: {integrity: sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==} dependencies: - '@lezer/common': 1.2.3 - '@lezer/highlight': 1.2.1 - '@lezer/lr': 1.4.2 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 dev: true /@loadable/component@5.15.2(react@18.3.1): @@ -2958,20 +3209,24 @@ packages: resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true dependencies: - detect-libc: 2.0.3 + detect-libc: 2.1.2 https-proxy-agent: 5.0.1 make-dir: 3.1.0 node-fetch: 2.7.0 nopt: 5.0.0 npmlog: 5.0.1 rimraf: 3.0.2 - semver: 7.6.3 + semver: 7.7.4 tar: 6.2.1 transitivePeerDependencies: - encoding - supports-color dev: false + /@marijn/find-cluster-break@1.0.2: + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + dev: true + /@monaco-editor/loader@1.4.0(monaco-editor@0.33.0): resolution: {integrity: sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==} peerDependencies: @@ -3006,8 +3261,8 @@ packages: state-local: 1.0.7 dev: true - /@napi-rs/nice-android-arm-eabi@1.0.1: - resolution: {integrity: sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==} + /@napi-rs/nice-android-arm-eabi@1.1.1: + resolution: {integrity: sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==} engines: {node: '>= 10'} cpu: [arm] os: [android] @@ -3015,8 +3270,8 @@ packages: dev: true optional: true - /@napi-rs/nice-android-arm64@1.0.1: - resolution: {integrity: sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA==} + /@napi-rs/nice-android-arm64@1.1.1: + resolution: {integrity: sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==} engines: {node: '>= 10'} cpu: [arm64] os: [android] @@ -3024,8 +3279,8 @@ packages: dev: true optional: true - /@napi-rs/nice-darwin-arm64@1.0.1: - resolution: {integrity: sha512-91k3HEqUl2fsrz/sKkuEkscj6EAj3/eZNCLqzD2AA0TtVbkQi8nqxZCZDMkfklULmxLkMxuUdKe7RvG/T6s2AA==} + /@napi-rs/nice-darwin-arm64@1.1.1: + resolution: {integrity: sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -3033,8 +3288,8 @@ packages: dev: true optional: true - /@napi-rs/nice-darwin-x64@1.0.1: - resolution: {integrity: sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ==} + /@napi-rs/nice-darwin-x64@1.1.1: + resolution: {integrity: sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -3042,8 +3297,8 @@ packages: dev: true optional: true - /@napi-rs/nice-freebsd-x64@1.0.1: - resolution: {integrity: sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw==} + /@napi-rs/nice-freebsd-x64@1.1.1: + resolution: {integrity: sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] @@ -3051,8 +3306,8 @@ packages: dev: true optional: true - /@napi-rs/nice-linux-arm-gnueabihf@1.0.1: - resolution: {integrity: sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q==} + /@napi-rs/nice-linux-arm-gnueabihf@1.1.1: + resolution: {integrity: sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==} engines: {node: '>= 10'} cpu: [arm] os: [linux] @@ -3060,8 +3315,8 @@ packages: dev: true optional: true - /@napi-rs/nice-linux-arm64-gnu@1.0.1: - resolution: {integrity: sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA==} + /@napi-rs/nice-linux-arm64-gnu@1.1.1: + resolution: {integrity: sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -3069,8 +3324,8 @@ packages: dev: true optional: true - /@napi-rs/nice-linux-arm64-musl@1.0.1: - resolution: {integrity: sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==} + /@napi-rs/nice-linux-arm64-musl@1.1.1: + resolution: {integrity: sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -3078,8 +3333,8 @@ packages: dev: true optional: true - /@napi-rs/nice-linux-ppc64-gnu@1.0.1: - resolution: {integrity: sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==} + /@napi-rs/nice-linux-ppc64-gnu@1.1.1: + resolution: {integrity: sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==} engines: {node: '>= 10'} cpu: [ppc64] os: [linux] @@ -3087,8 +3342,8 @@ packages: dev: true optional: true - /@napi-rs/nice-linux-riscv64-gnu@1.0.1: - resolution: {integrity: sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==} + /@napi-rs/nice-linux-riscv64-gnu@1.1.1: + resolution: {integrity: sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] @@ -3096,8 +3351,8 @@ packages: dev: true optional: true - /@napi-rs/nice-linux-s390x-gnu@1.0.1: - resolution: {integrity: sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==} + /@napi-rs/nice-linux-s390x-gnu@1.1.1: + resolution: {integrity: sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==} engines: {node: '>= 10'} cpu: [s390x] os: [linux] @@ -3105,8 +3360,8 @@ packages: dev: true optional: true - /@napi-rs/nice-linux-x64-gnu@1.0.1: - resolution: {integrity: sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==} + /@napi-rs/nice-linux-x64-gnu@1.1.1: + resolution: {integrity: sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -3114,8 +3369,8 @@ packages: dev: true optional: true - /@napi-rs/nice-linux-x64-musl@1.0.1: - resolution: {integrity: sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==} + /@napi-rs/nice-linux-x64-musl@1.1.1: + resolution: {integrity: sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -3123,8 +3378,17 @@ packages: dev: true optional: true - /@napi-rs/nice-win32-arm64-msvc@1.0.1: - resolution: {integrity: sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==} + /@napi-rs/nice-openharmony-arm64@1.1.1: + resolution: {integrity: sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [openharmony] + requiresBuild: true + dev: true + optional: true + + /@napi-rs/nice-win32-arm64-msvc@1.1.1: + resolution: {integrity: sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -3132,8 +3396,8 @@ packages: dev: true optional: true - /@napi-rs/nice-win32-ia32-msvc@1.0.1: - resolution: {integrity: sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw==} + /@napi-rs/nice-win32-ia32-msvc@1.1.1: + resolution: {integrity: sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] @@ -3141,8 +3405,8 @@ packages: dev: true optional: true - /@napi-rs/nice-win32-x64-msvc@1.0.1: - resolution: {integrity: sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg==} + /@napi-rs/nice-win32-x64-msvc@1.1.1: + resolution: {integrity: sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -3150,27 +3414,28 @@ packages: dev: true optional: true - /@napi-rs/nice@1.0.1: - resolution: {integrity: sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==} + /@napi-rs/nice@1.1.1: + resolution: {integrity: sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==} engines: {node: '>= 10'} requiresBuild: true optionalDependencies: - '@napi-rs/nice-android-arm-eabi': 1.0.1 - '@napi-rs/nice-android-arm64': 1.0.1 - '@napi-rs/nice-darwin-arm64': 1.0.1 - '@napi-rs/nice-darwin-x64': 1.0.1 - '@napi-rs/nice-freebsd-x64': 1.0.1 - '@napi-rs/nice-linux-arm-gnueabihf': 1.0.1 - '@napi-rs/nice-linux-arm64-gnu': 1.0.1 - '@napi-rs/nice-linux-arm64-musl': 1.0.1 - '@napi-rs/nice-linux-ppc64-gnu': 1.0.1 - '@napi-rs/nice-linux-riscv64-gnu': 1.0.1 - '@napi-rs/nice-linux-s390x-gnu': 1.0.1 - '@napi-rs/nice-linux-x64-gnu': 1.0.1 - '@napi-rs/nice-linux-x64-musl': 1.0.1 - '@napi-rs/nice-win32-arm64-msvc': 1.0.1 - '@napi-rs/nice-win32-ia32-msvc': 1.0.1 - '@napi-rs/nice-win32-x64-msvc': 1.0.1 + '@napi-rs/nice-android-arm-eabi': 1.1.1 + '@napi-rs/nice-android-arm64': 1.1.1 + '@napi-rs/nice-darwin-arm64': 1.1.1 + '@napi-rs/nice-darwin-x64': 1.1.1 + '@napi-rs/nice-freebsd-x64': 1.1.1 + '@napi-rs/nice-linux-arm-gnueabihf': 1.1.1 + '@napi-rs/nice-linux-arm64-gnu': 1.1.1 + '@napi-rs/nice-linux-arm64-musl': 1.1.1 + '@napi-rs/nice-linux-ppc64-gnu': 1.1.1 + '@napi-rs/nice-linux-riscv64-gnu': 1.1.1 + '@napi-rs/nice-linux-s390x-gnu': 1.1.1 + '@napi-rs/nice-linux-x64-gnu': 1.1.1 + '@napi-rs/nice-linux-x64-musl': 1.1.1 + '@napi-rs/nice-openharmony-arm64': 1.1.1 + '@napi-rs/nice-win32-arm64-msvc': 1.1.1 + '@napi-rs/nice-win32-ia32-msvc': 1.1.1 + '@napi-rs/nice-win32-x64-msvc': 1.1.1 dev: true optional: true @@ -3219,7 +3484,7 @@ packages: requiresBuild: true dependencies: '@gar/promisify': 1.1.3 - semver: 7.6.3 + semver: 7.7.4 dev: false optional: true @@ -3396,7 +3661,7 @@ packages: engines: {node: '>=14.0.0'} dev: true - /@replit/codemirror-lang-csharp@6.2.0(@codemirror/autocomplete@6.18.3)(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3)(@lezer/highlight@1.2.1)(@lezer/lr@1.4.2): + /@replit/codemirror-lang-csharp@6.2.0(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.2)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(@lezer/common@1.5.1)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.8): resolution: {integrity: sha512-6utbaWkoymhoAXj051mkRp+VIJlpwUgCX9Toevz3YatiZsz512fw3OVCedXQx+WcR0wb6zVHjChnuxqfCLtFVQ==} peerDependencies: '@codemirror/autocomplete': ^6.0.0 @@ -3407,16 +3672,16 @@ packages: '@lezer/highlight': ^1.0.0 '@lezer/lr': ^1.0.0 dependencies: - '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3) - '@codemirror/language': 6.10.6 - '@codemirror/state': 6.4.1 - '@codemirror/view': 6.35.0 - '@lezer/common': 1.2.3 - '@lezer/highlight': 1.2.1 - '@lezer/lr': 1.4.2 + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 dev: true - /@replit/codemirror-lang-nix@6.0.1(@codemirror/autocomplete@6.18.3)(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3)(@lezer/highlight@1.2.1)(@lezer/lr@1.4.2): + /@replit/codemirror-lang-nix@6.0.1(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.2)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(@lezer/common@1.5.1)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.8): resolution: {integrity: sha512-lvzjoYn9nfJzBD5qdm3Ut6G3+Or2wEacYIDJ49h9+19WSChVnxv4ojf+rNmQ78ncuxIt/bfbMvDLMeMP0xze6g==} peerDependencies: '@codemirror/autocomplete': ^6.0.0 @@ -3427,25 +3692,25 @@ packages: '@lezer/highlight': ^1.0.0 '@lezer/lr': ^1.0.0 dependencies: - '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3) - '@codemirror/language': 6.10.6 - '@codemirror/state': 6.4.1 - '@codemirror/view': 6.35.0 - '@lezer/common': 1.2.3 - '@lezer/highlight': 1.2.1 - '@lezer/lr': 1.4.2 + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 dev: true - /@replit/codemirror-lang-solidity@6.0.2(@codemirror/language@6.10.6): + /@replit/codemirror-lang-solidity@6.0.2(@codemirror/language@6.12.2): resolution: {integrity: sha512-/dpTVH338KFV6SaDYYSadkB4bI/0B0QRF/bkt1XS3t3QtyR49mn6+2k0OUQhvt2ZSO7kt10J+OPilRAtgbmX0w==} peerDependencies: '@codemirror/language': ^6.0.0 dependencies: - '@codemirror/language': 6.10.6 + '@codemirror/language': 6.12.2 '@lezer/highlight': 1.2.1 dev: true - /@replit/codemirror-lang-svelte@6.0.0(@codemirror/autocomplete@6.18.3)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.9)(@codemirror/lang-javascript@6.2.2)(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3)(@lezer/highlight@1.2.1)(@lezer/javascript@1.4.21)(@lezer/lr@1.4.2): + /@replit/codemirror-lang-svelte@6.0.0(@codemirror/autocomplete@6.20.1)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.9)(@codemirror/lang-javascript@6.2.2)(@codemirror/language@6.12.2)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(@lezer/common@1.5.1)(@lezer/highlight@1.2.3)(@lezer/javascript@1.5.4)(@lezer/lr@1.4.8): resolution: {integrity: sha512-U2OqqgMM6jKelL0GNWbAmqlu1S078zZNoBqlJBW+retTc5M4Mha6/Y2cf4SVg6ddgloJvmcSpt4hHrVoM4ePRA==} peerDependencies: '@codemirror/autocomplete': ^6.0.0 @@ -3460,17 +3725,17 @@ packages: '@lezer/javascript': ^1.2.0 '@lezer/lr': ^1.0.0 dependencies: - '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3) - '@codemirror/lang-css': 6.3.1(@codemirror/view@6.35.0) + '@codemirror/autocomplete': 6.20.1 + '@codemirror/lang-css': 6.3.1(@codemirror/view@6.39.16) '@codemirror/lang-html': 6.4.9 '@codemirror/lang-javascript': 6.2.2 - '@codemirror/language': 6.10.6 - '@codemirror/state': 6.4.1 - '@codemirror/view': 6.35.0 - '@lezer/common': 1.2.3 - '@lezer/highlight': 1.2.1 - '@lezer/javascript': 1.4.21 - '@lezer/lr': 1.4.2 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/javascript': 1.5.4 + '@lezer/lr': 1.4.8 dev: true /@sideway/address@4.1.5: @@ -4145,12 +4410,12 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.35.0)(typescript@5.2.2) '@typescript-eslint/utils': 5.62.0(eslint@8.35.0)(typescript@5.2.2) - debug: 4.3.7(supports-color@5.5.0) + debug: 4.4.3 eslint: 8.35.0 graphemer: 1.4.0 ignore: 5.3.2 natural-compare-lite: 1.4.0 - semver: 7.6.3 + semver: 7.7.4 tsutils: 3.21.0(typescript@5.2.2) typescript: 5.2.2 transitivePeerDependencies: @@ -4172,7 +4437,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.2.2) - debug: 4.3.7(supports-color@5.5.0) + debug: 4.4.3 eslint: 8.35.0 typescript: 5.2.2 transitivePeerDependencies: @@ -4201,7 +4466,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.2.2) '@typescript-eslint/utils': 5.62.0(eslint@8.35.0)(typescript@5.2.2) - debug: 4.3.7(supports-color@5.5.0) + debug: 4.4.3 eslint: 8.35.0 tsutils: 3.21.0(typescript@5.2.2) typescript: 5.2.2 @@ -4225,10 +4490,10 @@ packages: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.4.3 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.6.3 + semver: 7.7.4 tsutils: 3.21.0(typescript@5.2.2) typescript: 5.2.2 transitivePeerDependencies: @@ -4252,7 +4517,7 @@ packages: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.2.2) eslint: 8.35.0 eslint-scope: 5.1.1 - semver: 7.6.3 + semver: 7.7.4 transitivePeerDependencies: - supports-color - typescript @@ -4266,7 +4531,7 @@ packages: eslint-visitor-keys: 3.4.3 dev: true - /@uiw/codemirror-extensions-basic-setup@4.23.6(@codemirror/autocomplete@6.18.3)(@codemirror/commands@6.7.1)(@codemirror/language@6.10.6)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0): + /@uiw/codemirror-extensions-basic-setup@4.23.6(@codemirror/autocomplete@6.20.1)(@codemirror/commands@6.7.1)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16): resolution: {integrity: sha512-bvtq8IOvdkLJMhoJBRGPEzU51fMpPDwEhcAHp9xCR05MtbIokQgsnLXrmD1aZm6e7s/3q47H+qdSfAAkR5MkLA==} peerDependencies: '@codemirror/autocomplete': '>=6.0.0' @@ -4277,16 +4542,16 @@ packages: '@codemirror/state': '>=6.0.0' '@codemirror/view': '>=6.0.0' dependencies: - '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3) + '@codemirror/autocomplete': 6.20.1 '@codemirror/commands': 6.7.1 - '@codemirror/language': 6.10.6 - '@codemirror/lint': 6.8.4 - '@codemirror/search': 6.5.8 - '@codemirror/state': 6.4.1 - '@codemirror/view': 6.35.0 + '@codemirror/language': 6.12.2 + '@codemirror/lint': 6.9.5 + '@codemirror/search': 6.6.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 dev: true - /@uiw/codemirror-extensions-langs@4.23.6(@codemirror/autocomplete@6.18.3)(@codemirror/language-data@6.5.1)(@codemirror/language@6.10.6)(@codemirror/legacy-modes@6.4.2)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3)(@lezer/highlight@1.2.1)(@lezer/javascript@1.4.21)(@lezer/lr@1.4.2): + /@uiw/codemirror-extensions-langs@4.23.6(@codemirror/autocomplete@6.20.1)(@codemirror/language-data@6.5.2)(@codemirror/language@6.12.2)(@codemirror/legacy-modes@6.5.2)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(@lezer/common@1.5.1)(@lezer/highlight@1.2.3)(@lezer/javascript@1.5.4)(@lezer/lr@1.4.8): resolution: {integrity: sha512-VKWbEXmVq3EFYrJPWXH4Ei1f92zxuAg6dOlo8suSmwjmEc0qjNEP5Ss2CUi9LlzuWMGMmZgdKw56I3L71wYOog==} peerDependencies: '@codemirror/language-data': '>=6.0.0' @@ -4294,30 +4559,30 @@ packages: dependencies: '@codemirror/lang-angular': 0.1.3 '@codemirror/lang-cpp': 6.0.2 - '@codemirror/lang-css': 6.3.1(@codemirror/view@6.35.0) + '@codemirror/lang-css': 6.3.1(@codemirror/view@6.39.16) '@codemirror/lang-html': 6.4.9 '@codemirror/lang-java': 6.0.1 '@codemirror/lang-javascript': 6.2.2 '@codemirror/lang-json': 6.0.1 - '@codemirror/lang-less': 6.0.2(@codemirror/view@6.35.0) + '@codemirror/lang-less': 6.0.2(@codemirror/view@6.39.16) '@codemirror/lang-lezer': 6.0.1 '@codemirror/lang-liquid': 6.2.2 '@codemirror/lang-markdown': 6.3.1 '@codemirror/lang-php': 6.0.1 - '@codemirror/lang-python': 6.1.6(@codemirror/view@6.35.0) + '@codemirror/lang-python': 6.1.6(@codemirror/view@6.39.16) '@codemirror/lang-rust': 6.0.1 - '@codemirror/lang-sass': 6.0.2(@codemirror/view@6.35.0) - '@codemirror/lang-sql': 6.8.0(@codemirror/view@6.35.0) + '@codemirror/lang-sass': 6.0.2(@codemirror/view@6.39.16) + '@codemirror/lang-sql': 6.8.0(@codemirror/view@6.39.16) '@codemirror/lang-vue': 0.1.3 '@codemirror/lang-wast': 6.0.2 '@codemirror/lang-xml': 6.1.0 - '@codemirror/language-data': 6.5.1(@codemirror/view@6.35.0) - '@codemirror/legacy-modes': 6.4.2 + '@codemirror/language-data': 6.5.2(@codemirror/view@6.39.16) + '@codemirror/legacy-modes': 6.5.2 '@nextjournal/lang-clojure': 1.0.0 - '@replit/codemirror-lang-csharp': 6.2.0(@codemirror/autocomplete@6.18.3)(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3)(@lezer/highlight@1.2.1)(@lezer/lr@1.4.2) - '@replit/codemirror-lang-nix': 6.0.1(@codemirror/autocomplete@6.18.3)(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3)(@lezer/highlight@1.2.1)(@lezer/lr@1.4.2) - '@replit/codemirror-lang-solidity': 6.0.2(@codemirror/language@6.10.6) - '@replit/codemirror-lang-svelte': 6.0.0(@codemirror/autocomplete@6.18.3)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.9)(@codemirror/lang-javascript@6.2.2)(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3)(@lezer/highlight@1.2.1)(@lezer/javascript@1.4.21)(@lezer/lr@1.4.2) + '@replit/codemirror-lang-csharp': 6.2.0(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.2)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(@lezer/common@1.5.1)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.8) + '@replit/codemirror-lang-nix': 6.0.1(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.2)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(@lezer/common@1.5.1)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.8) + '@replit/codemirror-lang-solidity': 6.0.2(@codemirror/language@6.12.2) + '@replit/codemirror-lang-svelte': 6.0.0(@codemirror/autocomplete@6.20.1)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.9)(@codemirror/lang-javascript@6.2.2)(@codemirror/language@6.12.2)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16)(@lezer/common@1.5.1)(@lezer/highlight@1.2.3)(@lezer/javascript@1.5.4)(@lezer/lr@1.4.8) codemirror-lang-mermaid: 0.5.0 transitivePeerDependencies: - '@codemirror/autocomplete' @@ -4330,7 +4595,7 @@ packages: - '@lezer/lr' dev: true - /@uiw/react-codemirror@4.23.6(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.18.3)(@codemirror/language@6.10.6)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.35.0)(codemirror@6.0.1)(react-dom@18.3.1)(react@18.3.1): + /@uiw/react-codemirror@4.23.6(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.39.16)(codemirror@6.0.2)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-caYKGV6TfGLRV1HHD3p0G3FiVzKL1go7wes5XT2nWjB0+dTdyzyb81MKRSacptgZcotujfNO6QXn65uhETRAMw==} peerDependencies: '@babel/runtime': '>=7.11.0' @@ -4346,13 +4611,13 @@ packages: react-dom: optional: true dependencies: - '@babel/runtime': 7.26.0 + '@babel/runtime': 7.28.6 '@codemirror/commands': 6.7.1 - '@codemirror/state': 6.4.1 - '@codemirror/theme-one-dark': 6.1.2 - '@codemirror/view': 6.35.0 - '@uiw/codemirror-extensions-basic-setup': 4.23.6(@codemirror/autocomplete@6.18.3)(@codemirror/commands@6.7.1)(@codemirror/language@6.10.6)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0) - codemirror: 6.0.1(@lezer/common@1.2.3) + '@codemirror/state': 6.5.4 + '@codemirror/theme-one-dark': 6.1.3 + '@codemirror/view': 6.39.16 + '@uiw/codemirror-extensions-basic-setup': 4.23.6(@codemirror/autocomplete@6.20.1)(@codemirror/commands@6.7.1)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/view@6.39.16) + codemirror: 6.0.2 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: @@ -4726,7 +4991,7 @@ packages: react-error-overlay: 6.0.9 react-refresh: 0.14.2 resolve: 1.22.8 - semver: 7.6.3 + semver: 7.7.4 yargs-parser: 21.1.1 optionalDependencies: '@umijs/mako-darwin-arm64': 0.11.1 @@ -5110,7 +5375,7 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.4.3 transitivePeerDependencies: - supports-color dev: false @@ -5120,6 +5385,15 @@ packages: engines: {node: '>= 8.0.0'} dependencies: humanize-ms: 1.2.1 + dev: true + + /agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + dependencies: + humanize-ms: 1.2.1 + dev: false + optional: true /aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} @@ -5224,8 +5498,8 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - /ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + /ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} dev: true @@ -5367,8 +5641,8 @@ packages: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} dev: false - /aproba@2.0.0: - resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + /aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} dev: false /are-we-there-yet@2.0.0: @@ -5809,14 +6083,14 @@ packages: big-integer: 1.6.52 dev: true - /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + /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.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + /brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} dependencies: balanced-match: 1.0.2 dev: true @@ -6198,18 +6472,16 @@ packages: '@lezer/lr': 1.4.2 dev: true - /codemirror@6.0.1(@lezer/common@1.2.3): - resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==} + /codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} dependencies: - '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3) - '@codemirror/commands': 6.7.1 - '@codemirror/language': 6.10.6 - '@codemirror/lint': 6.8.4 - '@codemirror/search': 6.5.8 - '@codemirror/state': 6.4.1 - '@codemirror/view': 6.35.0 - transitivePeerDependencies: - - '@lezer/common' + '@codemirror/autocomplete': 6.20.1 + '@codemirror/commands': 6.10.2 + '@codemirror/language': 6.12.2 + '@codemirror/lint': 6.9.5 + '@codemirror/search': 6.6.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 dev: true /color-convert@1.9.3: @@ -6333,13 +6605,13 @@ packages: /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - /concat-stream@1.6.2: - resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} - engines: {'0': node >= 0.8} + /concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} dependencies: buffer-from: 1.1.2 inherits: 2.0.4 - readable-stream: 2.3.8 + readable-stream: 3.6.2 typedarray: 0.0.6 dev: false @@ -6436,6 +6708,7 @@ packages: /core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + dev: true /cors@2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} @@ -6616,7 +6889,7 @@ packages: postcss-modules-scope: 3.2.1(postcss@8.4.49) postcss-modules-values: 4.0.0(postcss@8.4.49) postcss-value-parser: 4.2.0 - semver: 7.6.3 + semver: 7.7.4 dev: true /css-prefers-color-scheme@6.0.3(postcss@8.4.49): @@ -6795,6 +7068,17 @@ packages: ms: 2.1.3 supports-color: 5.5.0 + /debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + /decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} engines: {node: '>=0.10.0'} @@ -6914,8 +7198,8 @@ packages: hasBin: true dev: true - /detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + /detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} dev: false @@ -7531,7 +7815,7 @@ packages: eslint: 8.35.0 estraverse: 5.3.0 jsx-ast-utils: 3.3.5 - minimatch: 3.1.2 + minimatch: 3.1.5 object.entries: 1.1.8 object.fromentries: 2.0.8 object.hasown: 1.1.4 @@ -8082,10 +8366,10 @@ packages: deepmerge: 4.3.1 fs-extra: 10.1.0 memfs: 3.5.3 - minimatch: 3.1.2 + minimatch: 3.1.5 node-abort-controller: 3.1.1 schema-utils: 3.3.0 - semver: 7.6.3 + semver: 7.7.4 tapable: 2.2.1 typescript: 5.2.2 dev: true @@ -8185,7 +8469,7 @@ packages: engines: {node: '>=10'} deprecated: This package is no longer supported. dependencies: - aproba: 2.0.0 + aproba: 2.1.0 color-support: 1.1.3 console-control-strings: 1.1.0 has-unicode: 2.0.1 @@ -8202,7 +8486,7 @@ packages: deprecated: This package is no longer supported. requiresBuild: true dependencies: - aproba: 2.0.0 + aproba: 2.1.0 color-support: 1.1.3 console-control-strings: 1.1.0 has-unicode: 2.0.1 @@ -8286,26 +8570,27 @@ packages: is-glob: 4.0.3 dev: true - /glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + /glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true dependencies: foreground-child: 3.3.0 jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 + minimatch: 9.0.9 + minipass: 7.1.3 package-json-from-dist: 1.0.1 path-scurry: 1.11.1 dev: true /glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.1.2 + minimatch: 3.1.5 once: 1.4.0 path-is-absolute: 1.0.1 @@ -8575,9 +8860,8 @@ packages: entities: 2.2.0 dev: true - /http-cache-semantics@4.1.1: - resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} - requiresBuild: true + /http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} dev: false optional: true @@ -8605,7 +8889,7 @@ packages: dependencies: '@tootallnate/once': 1.1.2 agent-base: 6.0.2 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.4.3 transitivePeerDependencies: - supports-color dev: false @@ -8645,7 +8929,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.4.3 transitivePeerDependencies: - supports-color dev: false @@ -8822,13 +9106,9 @@ packages: loose-envify: 1.4.0 dev: true - /ip-address@9.0.5: - resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + /ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} - requiresBuild: true - dependencies: - jsbn: 1.1.0 - sprintf-js: 1.1.3 dev: false optional: true @@ -9171,6 +9451,7 @@ packages: /isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + dev: true /isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -9325,12 +9606,6 @@ packages: dependencies: argparse: 2.0.1 - /jsbn@1.1.0: - resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} - requiresBuild: true - dev: false - optional: true - /jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} @@ -9741,7 +10016,7 @@ packages: ansi-escapes: 5.0.0 cli-cursor: 4.0.0 slice-ansi: 5.0.0 - strip-ansi: 7.1.0 + strip-ansi: 7.2.0 wrap-ansi: 8.1.0 dev: true @@ -9830,9 +10105,9 @@ packages: engines: {node: '>= 10'} requiresBuild: true dependencies: - agentkeepalive: 4.5.0 + agentkeepalive: 4.6.0 cacache: 15.3.0 - http-cache-semantics: 4.1.1 + http-cache-semantics: 4.2.0 http-proxy-agent: 4.0.1 https-proxy-agent: 5.0.1 is-lambda: 1.0.1 @@ -10017,13 +10292,19 @@ packages: /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: - brace-expansion: 1.1.11 + brace-expansion: 1.1.12 + dev: true - /minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + /minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + dependencies: + brace-expansion: 1.1.12 + + /minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 dev: true /minimist-options@4.1.0: @@ -10037,6 +10318,7 @@ packages: /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true /minipass-collect@1.0.2: resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} @@ -10099,8 +10381,8 @@ packages: engines: {node: '>=8'} dev: false - /minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + /minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} dev: true @@ -10117,6 +10399,7 @@ packages: hasBin: true dependencies: minimist: 1.2.8 + dev: true /mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} @@ -10151,17 +10434,14 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - /multer@1.4.5-lts.1: - resolution: {integrity: sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==} - engines: {node: '>= 6.0.0'} + /multer@2.1.1: + resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==} + engines: {node: '>= 10.16.0'} dependencies: append-field: 1.0.0 busboy: 1.6.0 - concat-stream: 1.6.2 - mkdirp: 0.5.6 - object-assign: 4.1.1 + concat-stream: 2.0.0 type-is: 1.6.18 - xtend: 4.0.2 dev: false /mz@2.7.0: @@ -10263,7 +10543,7 @@ packages: nopt: 5.0.0 npmlog: 6.0.2 rimraf: 3.0.2 - semver: 7.6.3 + semver: 7.7.4 tar: 6.2.1 which: 2.0.2 transitivePeerDependencies: @@ -10350,8 +10630,8 @@ packages: sorted-array-functions: 1.3.0 dev: false - /nodemailer@6.9.16: - resolution: {integrity: sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==} + /nodemailer@8.0.1: + resolution: {integrity: sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==} engines: {node: '>=6.0.0'} dev: false @@ -10395,7 +10675,7 @@ packages: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.15.1 - semver: 7.6.3 + semver: 7.7.4 validate-npm-package-license: 3.0.4 dev: true @@ -10781,7 +11061,7 @@ packages: engines: {node: '>=16 || 14 >=14.18'} dependencies: lru-cache: 10.4.3 - minipass: 7.1.2 + minipass: 7.1.3 dev: true /path-to-regexp@0.1.12: @@ -10892,7 +11172,7 @@ packages: /piscina@4.7.0: resolution: {integrity: sha512-b8hvkpp9zS0zsfa939b/jXbe64Z2gZv0Ha7FYPNUiDIB1y2AtxcOZdfP8xN8HFjUaqQiT9gRlfjAsoL8vdJ1Iw==} optionalDependencies: - '@napi-rs/nice': 1.0.1 + '@napi-rs/nice': 1.1.1 dev: true /point-in-polygon@1.1.0: @@ -11530,6 +11810,7 @@ packages: /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + dev: true /process-okam@0.11.10: resolution: {integrity: sha512-p8e5nl6/OCeMalVb9dSojND5B9m/nq64WsyUfRmrTdLMKcNYcDN++/2I8WV1mTQDqrh2PQ6tIIb2A7/A38eSvw==} @@ -12951,6 +13232,7 @@ packages: safe-buffer: 5.1.2 string_decoder: 1.1.1 util-deprecate: 1.0.2 + dev: true /readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} @@ -13156,7 +13438,7 @@ packages: engines: {node: '>=14'} hasBin: true dependencies: - glob: 10.4.5 + glob: 10.5.0 dev: true /ripemd160@2.0.2: @@ -13221,6 +13503,7 @@ packages: /safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: true /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -13301,6 +13584,11 @@ packages: engines: {node: '>=10'} hasBin: true + /semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + /send@0.19.0: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} @@ -13487,7 +13775,7 @@ packages: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} engines: {node: '>=10'} dependencies: - semver: 7.6.3 + semver: 7.7.4 dev: true /single-spa@5.9.5: @@ -13555,19 +13843,18 @@ packages: requiresBuild: true dependencies: agent-base: 6.0.2 - debug: 4.3.7(supports-color@5.5.0) - socks: 2.8.3 + debug: 4.4.3 + socks: 2.8.7 transitivePeerDependencies: - supports-color dev: false optional: true - /socks@2.8.3: - resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==} + /socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} - requiresBuild: true dependencies: - ip-address: 9.0.5 + ip-address: 10.1.0 smart-buffer: 4.2.0 dev: false optional: true @@ -13654,7 +13941,7 @@ packages: /spdy-transport@3.0.0: resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==} dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.4.3 detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 @@ -13668,7 +13955,7 @@ packages: resolution: {integrity: sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==} engines: {node: '>=6.0.0'} dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.4.3 handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 @@ -13702,12 +13989,6 @@ packages: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} dev: true - /sprintf-js@1.1.3: - resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} - requiresBuild: true - dev: false - optional: true - /ssri@8.0.1: resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} engines: {node: '>= 8'} @@ -13810,7 +14091,7 @@ packages: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.1.0 + strip-ansi: 7.2.0 dev: true /string.prototype.matchall@4.0.11: @@ -13868,6 +14149,7 @@ packages: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} dependencies: safe-buffer: 5.1.2 + dev: true /string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -13880,11 +14162,11 @@ packages: dependencies: ansi-regex: 5.0.1 - /strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + /strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} dependencies: - ansi-regex: 6.1.0 + ansi-regex: 6.2.2 dev: true /strip-final-newline@2.0.0: @@ -13909,8 +14191,8 @@ packages: engines: {node: '>=8'} dev: true - /style-mod@4.1.2: - resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==} + /style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} dev: true /style-search@0.1.0: @@ -14152,6 +14434,7 @@ packages: /tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me dependencies: chownr: 2.0.0 fs-minipass: 2.1.0 @@ -14178,7 +14461,7 @@ packages: dependencies: '@istanbuljs/schema': 0.1.3 glob: 7.2.3 - minimatch: 3.1.2 + minimatch: 3.1.5 dev: true /text-hex@1.0.0: @@ -15123,7 +15406,7 @@ packages: dependencies: ansi-styles: 6.2.1 string-width: 5.1.2 - strip-ansi: 7.1.0 + strip-ansi: 7.2.0 dev: true /wrappy@1.0.2: @@ -15140,6 +15423,7 @@ packages: /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + dev: true /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} @@ -15225,3 +15509,7 @@ packages: - encoding - supports-color dev: false + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false diff --git a/sample/config.sample.sh b/sample/config.sample.sh index 734f4ae5..9f653e56 100644 --- a/sample/config.sample.sh +++ b/sample/config.sample.sh @@ -195,12 +195,14 @@ export SMTP_SERVER="" ## SMTP 发送邮件服务器是否使用 SSL,填写 true 或 false export SMTP_SSL="" -## smtp_email 填写 SMTP 收发件邮箱,通知将会由自己发给自己 +## smtp_email 填写 SMTP 发件邮箱 export SMTP_EMAIL="" ## smtp_password 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定 export SMTP_PASSWORD="" ## smtp_name 填写 SMTP 收发件人姓名,可随意填写 export SMTP_NAME="" +## smtp_email_to 填写 SMTP 收件邮箱,多个用英文;分隔,不填默认发给发件邮箱 +export SMTP_EMAIL_TO="" ## 17. PushMe ## 官方说明文档:https://push.i-i.me/ @@ -259,4 +261,13 @@ export WEBHOOK_METHOD="" ## 支持 text/plain、application/json、multipart/form-data、application/x-www-form-urlencoded export WEBHOOK_CONTENT_TYPE="" +## 23. OpeniLink +## 官方文档: https://openilink.com/docs/hub/apps +## 在 OpeniLink Hub 后台安装 App 后获取 app_token +export OPENILINK_APP_TOKEN="" +## OpeniLink Hub 地址,默认为 https://hub.openilink.com,自建 Hub 时填写自己的地址 +export OPENILINK_HUB_URL="" +## OpeniLink 的 context_token,用于标识消息会话上下文,可从消息事件中获取 +export OPENILINK_CONTEXT_TOKEN="" + ## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可 diff --git a/sample/notify.js b/sample/notify.js index 59d38e86..355e17f1 100644 --- a/sample/notify.js +++ b/sample/notify.js @@ -108,8 +108,8 @@ const push_config = { QYWX_KEY: '', // 企业微信机器人的 webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770),例如:693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa - TG_BOT_TOKEN: '', // tg 机器人的 TG_BOT_TOKEN,例:1407203283:AAG9rt-6RDaaX0HBLZQq0laNOh898iFYaRQ - TG_USER_ID: '', // tg 机器人的 TG_USER_ID,例:1434078534 + TG_BOT_TOKEN: '', // tg 机器人的 TG_BOT_TOKEN,例:1234567890:ABCdefGHIjklMNOpqrsTUVwxyz + TG_USER_ID: '', // tg 机器人的 TG_USER_ID,例:1234567890 TG_API_HOST: 'https://api.telegram.org', // tg 代理 api TG_PROXY_AUTH: '', // tg 代理认证参数 TG_PROXY_HOST: '', // tg 机器人的 TG_PROXY_HOST @@ -121,7 +121,8 @@ const push_config = { SMTP_SERVICE: '', // 邮箱服务名称,比如 126、163、Gmail、QQ 等,支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json SMTP_EMAIL: '', // SMTP 发件邮箱 - SMTP_TO: '', // SMTP 收件邮箱,默认通知将会发给发件邮箱 + SMTP_TO: '', // SMTP 收件邮箱,兼容旧参数名,默认通知将会发给发件邮箱 + SMTP_EMAIL_TO: '', // SMTP 收件邮箱,多个分号分隔,默认发给发件邮箱 SMTP_PASSWORD: '', // SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定 SMTP_NAME: '', // SMTP 收发件人姓名,可随意填写 @@ -151,6 +152,11 @@ const push_config = { WXPUSHER_APP_TOKEN: '', // wxpusher 的 appToken WXPUSHER_TOPIC_IDS: '', // wxpusher 的 主题ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行 WXPUSHER_UIDS: '', // wxpusher 的 用户ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行 + + // 官方文档: https://openilink.com/docs/hub/apps + OPENILINK_APP_TOKEN: '', // OpeniLink 的 app_token,在 OpeniLink Hub 后台安装 App 后获取 + OPENILINK_HUB_URL: '', // OpeniLink Hub 地址,默认为 https://hub.openilink.com,自建 Hub 时填写自己的地址 + OPENILINK_CONTEXT_TOKEN: '', // OpeniLink 的 context_token,用于标识消息会话上下文,可从消息事件中获取 }; for (const key in push_config) { @@ -1046,8 +1052,14 @@ function fsBotNotify(text, desp) { } async function smtpNotify(text, desp) { - const { SMTP_EMAIL, SMTP_TO, SMTP_PASSWORD, SMTP_SERVICE, SMTP_NAME } = - push_config; + const { + SMTP_EMAIL, + SMTP_TO, + SMTP_EMAIL_TO, + SMTP_PASSWORD, + SMTP_SERVICE, + SMTP_NAME, + } = push_config; if (![SMTP_EMAIL, SMTP_PASSWORD].every(Boolean) || !SMTP_SERVICE) { return; } @@ -1063,9 +1075,20 @@ async function smtpNotify(text, desp) { }); const addr = SMTP_NAME ? `"${SMTP_NAME}" <${SMTP_EMAIL}>` : SMTP_EMAIL; + const recipients = [SMTP_EMAIL_TO, SMTP_TO].reduce((list, value) => { + if (!value) { + return list; + } + return list.concat( + value + .split(/[;;]/) + .map((item) => item.trim()) + .filter(Boolean), + ); + }, []); const info = await transporter.sendMail({ from: addr, - to: SMTP_TO ? SMTP_TO.split(';') : addr, + to: recipients.length ? recipients : SMTP_EMAIL, subject: text, html: `${desp.replace(/\n/g, '
')}`, }); @@ -1408,6 +1431,54 @@ function wxPusherNotify(text, desp) { }); } +function openiLinkNotify(text, desp) { + return new Promise((resolve) => { + const { OPENILINK_APP_TOKEN, OPENILINK_HUB_URL, OPENILINK_CONTEXT_TOKEN } = + push_config; + if (OPENILINK_APP_TOKEN) { + const baseUrl = OPENILINK_HUB_URL + ? OPENILINK_HUB_URL.replace(/\/$/, '') + : 'https://hub.openilink.com'; + const body = { + type: 'text', + content: `${text}\n\n${desp}`, + }; + if (OPENILINK_CONTEXT_TOKEN) { + body.context_token = OPENILINK_CONTEXT_TOKEN; + } + const options = { + url: `${baseUrl}/bot/v1/message/send`, + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${OPENILINK_APP_TOKEN}`, + }, + timeout, + }; + + $.post(options, (err, resp, data) => { + try { + if (err) { + console.log('OpeniLink 发送通知消息失败!\n', err); + } else { + if (data.ok) { + console.log('OpeniLink 发送通知消息成功!'); + } else { + console.log(`OpeniLink 发送通知消息异常:${data.error}`); + } + } + } catch (e) { + $.logErr(e, resp); + } finally { + resolve(data); + } + }); + } else { + resolve(); + } + }); +} + function parseString(input, valueFormatFn) { const regex = /(\w+):\s*((?:(?!\n\w+:).)*)/g; const matches = {}; @@ -1527,7 +1598,7 @@ async function sendNotify(text, desp, params = {}) { iGotNotify(text, desp, params), // iGot gobotNotify(text, desp), // go-cqhttp gotifyNotify(text, desp), // gotify - chatNotify(text, desp), // synolog chat + chatNotify(text, desp), // synology chat pushDeerNotify(text, desp), // PushDeer aibotkNotify(text, desp), // 智能微秘书 fsBotNotify(text, desp), // 飞书机器人 @@ -1538,6 +1609,7 @@ async function sendNotify(text, desp, params = {}) { qmsgNotify(text, desp), // 自定义通知 ntfyNotify(text, desp), // Ntfy wxPusherNotify(text, desp), // wxpusher + openiLinkNotify(text, desp), // OpeniLink ]); } diff --git a/sample/notify.py b/sample/notify.py index 6b82a572..0bc81ffd 100644 --- a/sample/notify.py +++ b/sample/notify.py @@ -94,8 +94,8 @@ push_config = { 'QYWX_KEY': '', # 企业微信机器人 - 'TG_BOT_TOKEN': '', # tg 机器人的 TG_BOT_TOKEN,例:1407203283:AAG9rt-6RDaaX0HBLZQq0laNOh898iFYaRQ - 'TG_USER_ID': '', # tg 机器人的 TG_USER_ID,例:1434078534 + 'TG_BOT_TOKEN': '', # tg 机器人的 TG_BOT_TOKEN,例:1234567890:ABCdefGHIjklMNOpqrsTUVwxyz + 'TG_USER_ID': '', # tg 机器人的 TG_USER_ID,例:1234567890 'TG_API_HOST': '', # tg 代理 api 'TG_PROXY_AUTH': '', # tg 代理认证参数 'TG_PROXY_HOST': '', # tg 机器人的 TG_PROXY_HOST @@ -107,7 +107,8 @@ push_config = { 'SMTP_SERVER': '', # SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465 'SMTP_SSL': 'false', # SMTP 发送邮件服务器是否使用 SSL,填写 true 或 false - 'SMTP_EMAIL': '', # SMTP 收发件邮箱,通知将会由自己发给自己 + 'SMTP_EMAIL': '', # SMTP 发件邮箱 + 'SMTP_EMAIL_TO': '', # SMTP 收件邮箱,多个分号分隔,默认发给发件邮箱 'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定 'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写 @@ -135,6 +136,10 @@ push_config = { 'WXPUSHER_APP_TOKEN': '', # wxpusher 的 appToken 官方文档: https://wxpusher.zjiecode.com/docs/ 管理后台: https://wxpusher.zjiecode.com/admin/ 'WXPUSHER_TOPIC_IDS': '', # wxpusher 的 主题ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行 'WXPUSHER_UIDS': '', # wxpusher 的 用户ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行 + + 'OPENILINK_APP_TOKEN': '', # OpeniLink 的 app_token,在 OpeniLink Hub 后台安装 App 后获取 官方文档: https://openilink.com/docs/hub/apps + 'OPENILINK_HUB_URL': '', # OpeniLink Hub 地址,默认为 https://hub.openilink.com,自建 Hub 时填写自己的地址 + 'OPENILINK_CONTEXT_TOKEN': '', # OpeniLink 的 context_token,用于标识消息会话上下文,可从消息事件中获取 } # fmt: on @@ -690,6 +695,10 @@ def smtp(title: str, content: str) -> None: return print("SMTP 邮件 服务启动") + email_to = push_config.get("SMTP_EMAIL_TO") or push_config.get("SMTP_EMAIL") + email_to_list = [ + item.strip() for item in re.split(r"[;;]", email_to) if item.strip() + ] message = MIMEText(content, "plain", "utf-8") message["From"] = formataddr( ( @@ -697,12 +706,7 @@ def smtp(title: str, content: str) -> None: push_config.get("SMTP_EMAIL"), ) ) - message["To"] = formataddr( - ( - Header(push_config.get("SMTP_NAME"), "utf-8").encode(), - push_config.get("SMTP_EMAIL"), - ) - ) + message["To"] = ",".join(email_to_list) message["Subject"] = Header(title, "utf-8") try: @@ -716,7 +720,7 @@ def smtp(title: str, content: str) -> None: ) smtp_server.sendmail( push_config.get("SMTP_EMAIL"), - push_config.get("SMTP_EMAIL"), + email_to_list, message.as_bytes(), ) smtp_server.close() @@ -898,6 +902,43 @@ def wxpusher_bot(title: str, content: str) -> None: print(f"wxpusher 推送失败!错误信息:{response.get('msg')}") +def openilink(title: str, content: str) -> None: + """ + 通过 OpeniLink 推送消息。 + 支持的环境变量: + - OPENILINK_APP_TOKEN: 在 OpeniLink Hub 后台安装 App 后获取的 app_token + - OPENILINK_HUB_URL: OpeniLink Hub 地址,默认为 https://hub.openilink.com + - OPENILINK_CONTEXT_TOKEN: 消息会话上下文 token,可从消息事件中获取 + """ + if not push_config.get("OPENILINK_APP_TOKEN"): + return + + print("OpeniLink 服务启动") + + base_url = ( + push_config.get("OPENILINK_HUB_URL", "").rstrip("/") + or "https://hub.openilink.com" + ) + url = f"{base_url}/bot/v1/message/send" + headers = { + "Content-Type": "application/json", + "Authorization": f'Bearer {push_config.get("OPENILINK_APP_TOKEN")}', + } + data = { + "type": "text", + "content": f"{title}\n\n{content}", + } + if push_config.get("OPENILINK_CONTEXT_TOKEN"): + data["context_token"] = push_config.get("OPENILINK_CONTEXT_TOKEN") + + response = requests.post(url=url, json=data, headers=headers).json() + + if response.get("ok"): + print("OpeniLink 推送成功!") + else: + print(f'OpeniLink 推送失败!错误信息:{response.get("error")}') + + def parse_headers(headers): if not headers: return {} @@ -1063,6 +1104,8 @@ def add_notify_function(): push_config.get("WXPUSHER_TOPIC_IDS") or push_config.get("WXPUSHER_UIDS") ): notify_function.append(wxpusher_bot) + if push_config.get("OPENILINK_APP_TOKEN"): + notify_function.append(openilink) if not notify_function: print(f"无推送渠道,请检查通知变量是否正确") return notify_function diff --git a/sample/notify.py.save b/sample/notify.py.save deleted file mode 100644 index cda60ef3..00000000 --- a/sample/notify.py.save +++ /dev/null @@ -1,1010 +0,0 @@ -#!/usr/bin/env python3 -# _*_ coding:utf-8 _*_ -import base64 -import hashlib -import hmac -import json -import os -import re -import threading -import time -import urllib.parse -import smtplib -from email.mime.text import MIMEText -from email.header import Header -from email.utils import formataddr - -import requests - -# 原先的 print 函数和主线程的锁 -_print = print -mutex = threading.Lock() - - -# 定义新的 print 函数 -def print(text, *args, **kw): - """ - 使输出有序进行,不出现多线程同一时间输出导致错乱的问题。 - """ - with mutex: - _print(text, *args, **kw) - - -# 通知服务 -# fmt: off -push_config = { - 'HITOKOTO': True, # 启用一言(随机句子) - - 'BARK_PUSH': '', # bark IP 或设备码,例:https://api.day.app/DxHcxxxxxRxxxxxxcm/ - 'BARK_ARCHIVE': '', # bark 推送是否存档 - 'BARK_GROUP': '', # bark 推送分组 - 'BARK_SOUND': '', # bark 推送声音 - 'BARK_ICON': '', # bark 推送图标 - 'BARK_LEVEL': '', # bark 推送时效性 - 'BARK_URL': '', # bark 推送跳转URL - - 'CONSOLE': False, # 控制台输出 - - 'DD_BOT_SECRET': '', # 钉钉机器人的 DD_BOT_SECRET - 'DD_BOT_TOKEN': '', # 钉钉机器人的 DD_BOT_TOKEN - - 'FSKEY': '', # 飞书机器人的 FSKEY - - 'GOBOT_URL': '', # go-cqhttp - # 推送到个人QQ:http://127.0.0.1/send_private_msg - # 群:http://127.0.0.1/send_group_msg - 'GOBOT_QQ': '', # go-cqhttp 的推送群或用户 - # GOBOT_URL 设置 /send_private_msg 时填入 user_id=个人QQ - # /send_group_msg 时填入 group_id=QQ群 - 'GOBOT_TOKEN': '', # go-cqhttp 的 access_token - - 'GOTIFY_URL': '', # gotify地址,如https://push.example.de:8080 - 'GOTIFY_TOKEN': '', # gotify的消息应用token - 'GOTIFY_PRIORITY': 0, # 推送消息优先级,默认为0 - - 'IGOT_PUSH_KEY': '', # iGot 聚合推送的 IGOT_PUSH_KEY - - 'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版 - - 'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY - 'DEER_URL': '', # PushDeer 的 PUSHDEER_URL - - 'CHAT_URL': '', # synology chat url - 'CHAT_TOKEN': '', # synology chat token - - 'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌 - 'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码 - - 'WE_PLUS_BOT_TOKEN': '', # 微加机器人的用户令牌 - 'WE_PLUS_BOT_RECEIVER': '', # 微加机器人的消息接收者 - 'WE_PLUS_BOT_VERSION': 'pro', # 微加机器人的调用版本 - - 'QMSG_KEY': '', # qmsg 酱的 QMSG_KEY - 'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE - - 'QYWX_ORIGIN': '', # 企业微信代理地址 - - 'QYWX_AM': '', # 企业微信应用 - - 'QYWX_KEY': '', # 企业微信机器人 - - 'TG_BOT_TOKEN': '', # tg 机器人的 TG_BOT_TOKEN,例:1407203283:AAG9rt-6RDaaX0HBLZQq0laNOh898iFYaRQ - 'TG_USER_ID': '', # tg 机器人的 TG_USER_ID,例:1434078534 - 'TG_API_HOST': '', # tg 代理 api - 'TG_PROXY_AUTH': '', # tg 代理认证参数 - 'TG_PROXY_HOST': '', # tg 机器人的 TG_PROXY_HOST - 'TG_PROXY_PORT': '', # tg 机器人的 TG_PROXY_PORT - - 'AIBOTK_KEY': '', # 智能微秘书 个人中心的apikey 文档地址:http://wechat.aibotk.com/docs/about - 'AIBOTK_TYPE': '', # 智能微秘书 发送目标 room 或 contact - 'AIBOTK_NAME': '', # 智能微秘书 发送群名 或者好友昵称和type要对应好 - - 'SMTP_SERVER': '', # SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465 - 'SMTP_SSL': 'false', # SMTP 发送邮件服务器是否使用 SSL,填写 true 或 false - 'SMTP_EMAIL': '', # SMTP 收发件邮箱,通知将会由自己发给自己 - 'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定 - 'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写 - - 'PUSHME_KEY': '', # PushMe 的 PUSHME_KEY - 'PUSHME_URL': '', # PushMe 的 PUSHME_URL - - 'CHRONOCAT_QQ': '', # qq号 - 'CHRONOCAT_TOKEN': '', # CHRONOCAT 的token - 'CHRONOCAT_URL': '', # CHRONOCAT的url地址 - - 'WEBHOOK_URL': '', # 自定义通知 请求地址 - 'WEBHOOK_BODY': '', # 自定义通知 请求体 - 'WEBHOOK_HEADERS': '', # 自定义通知 请求头 - 'WEBHOOK_METHOD': '', # 自定义通知 请求方法 - 'WEBHOOK_CONTENT_TYPE': '' # 自定义通知 content-type - - 'NTFY_URL': '', # ntfy地址,如https://ntfy.sh - 'NTFY_TOPIC': '', # ntfy的消息应用topic - 'NTFY_PRIORITY': '3', # 推送消息优先级,默认为3 -} -# fmt: on - -for k in push_config: - if os.getenv(k): - v = os.getenv(k) - push_config[k] = v - - -def bark(title: str, content: str) -> None: - """ - 使用 bark 推送消息。 - """ - if not push_config.get("BARK_PUSH"): - print("bark 服务的 BARK_PUSH 未设置!!\n取消推送") - return - print("bark 服务启动") - - if push_config.get("BARK_PUSH").startswith("http"): - url = f'{push_config.get("BARK_PUSH")}' - else: - url = f'https://api.day.app/{push_config.get("BARK_PUSH")}' - - bark_params = { - "BARK_ARCHIVE": "isArchive", - "BARK_GROUP": "group", - "BARK_SOUND": "sound", - "BARK_ICON": "icon", - "BARK_LEVEL": "level", - "BARK_URL": "url", - } - data = { - "title": title, - "body": content, - } - for pair in filter( - lambda pairs: pairs[0].startswith("BARK_") - and pairs[0] != "BARK_PUSH" - and pairs[1] - and bark_params.get(pairs[0]), - push_config.items(), - ): - data[bark_params.get(pair[0])] = pair[1] - headers = {"Content-Type": "application/json;charset=utf-8"} - response = requests.post( - url=url, data=json.dumps(data), headers=headers, timeout=15 - ).json() - - if response["code"] == 200: - print("bark 推送成功!") - else: - print("bark 推送失败!") - - -def console(title: str, content: str) -> None: - """ - 使用 控制台 推送消息。 - """ - print(f"{title}\n\n{content}") - - -def dingding_bot(title: str, content: str) -> None: - """ - 使用 钉钉机器人 推送消息。 - """ - if not push_config.get("DD_BOT_SECRET") or not push_config.get("DD_BOT_TOKEN"): - print("钉钉机器人 服务的 DD_BOT_SECRET 或者 DD_BOT_TOKEN 未设置!!\n取消推送") - return - print("钉钉机器人 服务启动") - - timestamp = str(round(time.time() * 1000)) - secret_enc = push_config.get("DD_BOT_SECRET").encode("utf-8") - string_to_sign = "{}\n{}".format(timestamp, push_config.get("DD_BOT_SECRET")) - string_to_sign_enc = string_to_sign.encode("utf-8") - hmac_code = hmac.new( - secret_enc, string_to_sign_enc, digestmod=hashlib.sha256 - ).digest() - sign = urllib.parse.quote_plus(base64.b64encode(hmac_code)) - url = f'https://oapi.dingtalk.com/robot/send?access_token={push_config.get("DD_BOT_TOKEN")}×tamp={timestamp}&sign={sign}' - headers = {"Content-Type": "application/json;charset=utf-8"} - data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}} - response = requests.post( - url=url, data=json.dumps(data), headers=headers, timeout=15 - ).json() - - if not response["errcode"]: - print("钉钉机器人 推送成功!") - else: - print("钉钉机器人 推送失败!") - - -def feishu_bot(title: str, content: str) -> None: - """ - 使用 飞书机器人 推送消息。 - """ - if not push_config.get("FSKEY"): - print("飞书 服务的 FSKEY 未设置!!\n取消推送") - return - print("飞书 服务启动") - - url = f'https://open.feishu.cn/open-apis/bot/v2/hook/{push_config.get("FSKEY")}' - data = {"msg_type": "text", "content": {"text": f"{title}\n\n{content}"}} - response = requests.post(url, data=json.dumps(data)).json() - - if response.get("StatusCode") == 0 or response.get("code") == 0: - print("飞书 推送成功!") - else: - print("飞书 推送失败!错误信息如下:\n", response) - - -def go_cqhttp(title: str, content: str) -> None: - """ - 使用 go_cqhttp 推送消息。 - """ - if not push_config.get("GOBOT_URL") or not push_config.get("GOBOT_QQ"): - print("go-cqhttp 服务的 GOBOT_URL 或 GOBOT_QQ 未设置!!\n取消推送") - return - 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}' - response = requests.get(url).json() - - if response["status"] == "ok": - print("go-cqhttp 推送成功!") - else: - print("go-cqhttp 推送失败!") - - -def gotify(title: str, content: str) -> None: - """ - 使用 gotify 推送消息。 - """ - if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"): - print("gotify 服务的 GOTIFY_URL 或 GOTIFY_TOKEN 未设置!!\n取消推送") - return - print("gotify 服务启动") - - url = f'{push_config.get("GOTIFY_URL")}/message?token={push_config.get("GOTIFY_TOKEN")}' - data = { - "title": title, - "message": content, - "priority": push_config.get("GOTIFY_PRIORITY"), - } - response = requests.post(url, data=data).json() - - if response.get("id"): - print("gotify 推送成功!") - else: - print("gotify 推送失败!") - - -def iGot(title: str, content: str) -> None: - """ - 使用 iGot 推送消息。 - """ - if not push_config.get("IGOT_PUSH_KEY"): - print("iGot 服务的 IGOT_PUSH_KEY 未设置!!\n取消推送") - return - print("iGot 服务启动") - - url = f'https://push.hellyw.com/{push_config.get("IGOT_PUSH_KEY")}' - data = {"title": title, "content": content} - headers = {"Content-Type": "application/x-www-form-urlencoded"} - response = requests.post(url, data=data, headers=headers).json() - - if response["ret"] == 0: - print("iGot 推送成功!") - else: - print(f'iGot 推送失败!{response["errMsg"]}') - - -def serverJ(title: str, content: str) -> None: - """ - 通过 serverJ 推送消息。 - """ - if not push_config.get("PUSH_KEY"): - print("serverJ 服务的 PUSH_KEY 未设置!!\n取消推送") - return - print("serverJ 服务启动") - - data = {"text": title, "desp": content.replace("\n", "\n\n")} - if push_config.get("PUSH_KEY").startswith("sctp"): - url = f'https://{push_config.get("PUSH_KEY")}.push.ft07.com/send' - else: - url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send' - response = requests.post(url, data=data).json() - - if response.get("errno") == 0 or response.get("code") == 0: - print("serverJ 推送成功!") - else: - print(f'serverJ 推送失败!错误码:{response["message"]}') - - -def pushdeer(title: str, content: str) -> None: - """ - 通过PushDeer 推送消息 - """ - if not push_config.get("DEER_KEY"): - print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送") - return - print("PushDeer 服务启动") - data = { - "text": title, - "desp": content, - "type": "markdown", - "pushkey": push_config.get("DEER_KEY"), - } - url = "https://api2.pushdeer.com/message/push" - if push_config.get("DEER_URL"): - url = push_config.get("DEER_URL") - - response = requests.post(url, data=data).json() - - if len(response.get("content").get("result")) > 0: - print("PushDeer 推送成功!") - else: - print("PushDeer 推送失败!错误信息:", response) - - -def chat(title: str, content: str) -> None: - """ - 通过Chat 推送消息 - """ - if not push_config.get("CHAT_URL") or not push_config.get("CHAT_TOKEN"): - print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送") - return - print("chat 服务启动") - data = "payload=" + json.dumps({"text": title + "\n" + content}) - url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN") - response = requests.post(url, data=data) - - if response.status_code == 200: - print("Chat 推送成功!") - else: - print("Chat 推送失败!错误信息:", response) - - -def pushplus_bot(title: str, content: str) -> None: - """ - 通过 push+ 推送消息。 - """ - if not push_config.get("PUSH_PLUS_TOKEN"): - print("PUSHPLUS 服务的 PUSH_PLUS_TOKEN 未设置!!\n取消推送") - return - print("PUSHPLUS 服务启动") - - url = "http://www.pushplus.plus/send" - data = { - "token": push_config.get("PUSH_PLUS_TOKEN"), - "title": title, - "content": content, - "topic": push_config.get("PUSH_PLUS_USER"), - } - body = json.dumps(data).encode(encoding="utf-8") - headers = {"Content-Type": "application/json"} - response = requests.post(url=url, data=body, headers=headers).json() - - if response["code"] == 200: - print("PUSHPLUS 推送成功!") - - else: - url_old = "http://pushplus.hxtrip.com/send" - headers["Accept"] = "application/json" - response = requests.post(url=url_old, data=body, headers=headers).json() - - if response["code"] == 200: - print("PUSHPLUS(hxtrip) 推送成功!") - - else: - print("PUSHPLUS 推送失败!") - - -def weplus_bot(title: str, content: str) -> None: - """ - 通过 微加机器人 推送消息。 - """ - if not push_config.get("WE_PLUS_BOT_TOKEN"): - print("微加机器人 服务的 WE_PLUS_BOT_TOKEN 未设置!!\n取消推送") - return - print("微加机器人 服务启动") - - template = "txt" - if len(content) > 800: - template = "html" - - url = "https://www.weplusbot.com/send" - data = { - "token": push_config.get("WE_PLUS_BOT_TOKEN"), - "title": title, - "content": content, - "template": template, - "receiver": push_config.get("WE_PLUS_BOT_RECEIVER"), - "version": push_config.get("WE_PLUS_BOT_VERSION"), - } - body = json.dumps(data).encode(encoding="utf-8") - headers = {"Content-Type": "application/json"} - response = requests.post(url=url, data=body, headers=headers).json() - - if response["code"] == 200: - print("微加机器人 推送成功!") - else: - print("微加机器人 推送失败!") - - -def qmsg_bot(title: str, content: str) -> None: - """ - 使用 qmsg 推送消息。 - """ - if not push_config.get("QMSG_KEY") or not push_config.get("QMSG_TYPE"): - print("qmsg 的 QMSG_KEY 或者 QMSG_TYPE 未设置!!\n取消推送") - return - print("qmsg 服务启动") - - 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")} - response = requests.post(url=url, params=payload).json() - - if response["code"] == 0: - print("qmsg 推送成功!") - else: - print(f'qmsg 推送失败!{response["reason"]}') - - -def wecom_app(title: str, content: str) -> None: - """ - 通过 企业微信 APP 推送消息。 - """ - if not push_config.get("QYWX_AM"): - print("QYWX_AM 未设置!!\n取消推送") - return - QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM")) - if 4 < len(QYWX_AM_AY) > 5: - print("QYWX_AM 设置错误!!\n取消推送") - return - print("企业微信 APP 服务启动") - - corpid = QYWX_AM_AY[0] - corpsecret = QYWX_AM_AY[1] - touser = QYWX_AM_AY[2] - agentid = QYWX_AM_AY[3] - try: - media_id = QYWX_AM_AY[4] - except IndexError: - media_id = "" - wx = WeCom(corpid, corpsecret, agentid) - # 如果没有配置 media_id 默认就以 text 方式发送 - if not media_id: - message = title + "\n\n" + content - response = wx.send_text(message, touser) - else: - response = wx.send_mpnews(title, content, media_id, touser) - - if response == "ok": - print("企业微信推送成功!") - else: - print("企业微信推送失败!错误信息如下:\n", response) - - -class WeCom: - def __init__(self, corpid, corpsecret, agentid): - self.CORPID = corpid - self.CORPSECRET = corpsecret - self.AGENTID = agentid - self.ORIGIN = "https://qyapi.weixin.qq.com" - if push_config.get("QYWX_ORIGIN"): - self.ORIGIN = push_config.get("QYWX_ORIGIN") - - def get_access_token(self): - url = f"{self.ORIGIN}/cgi-bin/gettoken" - values = { - "corpid": self.CORPID, - "corpsecret": self.CORPSECRET, - } - req = requests.post(url, params=values) - data = json.loads(req.text) - return data["access_token"] - - def send_text(self, message, touser="@all"): - send_url = ( - f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}" - ) - send_values = { - "touser": touser, - "msgtype": "text", - "agentid": self.AGENTID, - "text": {"content": message}, - "safe": "0", - } - send_msges = bytes(json.dumps(send_values), "utf-8") - respone = requests.post(send_url, send_msges) - respone = respone.json() - return respone["errmsg"] - - def send_mpnews(self, title, message, media_id, touser="@all"): - send_url = ( - f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}" - ) - send_values = { - "touser": touser, - "msgtype": "mpnews", - "agentid": self.AGENTID, - "mpnews": { - "articles": [ - { - "title": title, - "thumb_media_id": media_id, - "author": "Author", - "content_source_url": "", - "content": message.replace("\n", "
"), - "digest": message, - } - ] - }, - } - send_msges = bytes(json.dumps(send_values), "utf-8") - respone = requests.post(send_url, send_msges) - respone = respone.json() - return respone["errmsg"] - - -def wecom_bot(title: str, content: str) -> None: - """ - 通过 企业微信机器人 推送消息。 - """ - if not push_config.get("QYWX_KEY"): - print("企业微信机器人 服务的 QYWX_KEY 未设置!!\n取消推送") - return - print("企业微信机器人服务启动") - - origin = "https://qyapi.weixin.qq.com" - if push_config.get("QYWX_ORIGIN"): - origin = push_config.get("QYWX_ORIGIN") - - url = f"{origin}/cgi-bin/webhook/send?key={push_config.get('QYWX_KEY')}" - headers = {"Content-Type": "application/json;charset=utf-8"} - data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}} - response = requests.post( - url=url, data=json.dumps(data), headers=headers, timeout=15 - ).json() - - if response["errcode"] == 0: - print("企业微信机器人推送成功!") - else: - print("企业微信机器人推送失败!") - - -def telegram_bot(title: str, content: str) -> None: - """ - 使用 telegram 机器人 推送消息。 - """ - if not push_config.get("TG_BOT_TOKEN") or not push_config.get("TG_USER_ID"): - print("tg 服务的 bot_token 或者 user_id 未设置!!\n取消推送") - return - print("tg 服务启动") - - if push_config.get("TG_API_HOST"): - url = f"{push_config.get('TG_API_HOST')}/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage" - else: - url = ( - f"https://api.telegram.org/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage" - ) - headers = {"Content-Type": "application/x-www-form-urlencoded"} - payload = { - "chat_id": str(push_config.get("TG_USER_ID")), - "text": f"{title}\n\n{content}", - "disable_web_page_preview": "true", - } - proxies = None - if push_config.get("TG_PROXY_HOST") and push_config.get("TG_PROXY_PORT"): - if push_config.get("TG_PROXY_AUTH") is not None and "@" not in push_config.get( - "TG_PROXY_HOST" - ): - push_config["TG_PROXY_HOST"] = ( - push_config.get("TG_PROXY_AUTH") - + "@" - + push_config.get("TG_PROXY_HOST") - ) - proxyStr = "http://{}:{}".format( - push_config.get("TG_PROXY_HOST"), push_config.get("TG_PROXY_PORT") - ) - proxies = {"http": proxyStr, "https": proxyStr} - response = requests.post( - url=url, headers=headers, params=payload, proxies=proxies - ).json() - - if response["ok"]: - print("tg 推送成功!") - else: - print("tg 推送失败!") - - -def aibotk(title: str, content: str) -> None: - """ - 使用 智能微秘书 推送消息。 - """ - if ( - not push_config.get("AIBOTK_KEY") - or not push_config.get("AIBOTK_TYPE") - or not push_config.get("AIBOTK_NAME") - ): - print( - "智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送" - ) - return - print("智能微秘书 服务启动") - - if push_config.get("AIBOTK_TYPE") == "room": - url = "https://api-bot.aibotk.com/openapi/v1/chat/room" - data = { - "apiKey": push_config.get("AIBOTK_KEY"), - "roomName": push_config.get("AIBOTK_NAME"), - "message": {"type": 1, "content": f"【青龙快讯】\n\n{title}\n{content}"}, - } - else: - url = "https://api-bot.aibotk.com/openapi/v1/chat/contact" - data = { - "apiKey": push_config.get("AIBOTK_KEY"), - "name": push_config.get("AIBOTK_NAME"), - "message": {"type": 1, "content": f"【青龙快讯】\n\n{title}\n{content}"}, - } - body = json.dumps(data).encode(encoding="utf-8") - headers = {"Content-Type": "application/json"} - response = requests.post(url=url, data=body, headers=headers).json() - print(response) - if response["code"] == 0: - print("智能微秘书 推送成功!") - else: - print(f'智能微秘书 推送失败!{response["error"]}') - - -def smtp(title: str, content: str) -> None: - """ - 使用 SMTP 邮件 推送消息。 - """ - if ( - not push_config.get("SMTP_SERVER") - or not push_config.get("SMTP_SSL") - or not push_config.get("SMTP_EMAIL") - or not push_config.get("SMTP_PASSWORD") - or not push_config.get("SMTP_NAME") - ): - print( - "SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送" - ) - return - print("SMTP 邮件 服务启动") - - message = MIMEText(content, "plain", "utf-8") - message["From"] = formataddr( - ( - Header(push_config.get("SMTP_NAME"), "utf-8").encode(), - push_config.get("SMTP_EMAIL"), - ) - ) - message["To"] = formataddr( - ( - Header(push_config.get("SMTP_NAME"), "utf-8").encode(), - push_config.get("SMTP_EMAIL"), - ) - ) - message["Subject"] = Header(title, "utf-8") - - try: - smtp_server = ( - smtplib.SMTP_SSL(push_config.get("SMTP_SERVER")) - if push_config.get("SMTP_SSL") == "true" - else smtplib.SMTP(push_config.get("SMTP_SERVER")) - ) - smtp_server.login( - push_config.get("SMTP_EMAIL"), push_config.get("SMTP_PASSWORD") - ) - smtp_server.sendmail( - push_config.get("SMTP_EMAIL"), - push_config.get("SMTP_EMAIL"), - message.as_bytes(), - ) - smtp_server.close() - print("SMTP 邮件 推送成功!") - except Exception as e: - print(f"SMTP 邮件 推送失败!{e}") - - -def pushme(title: str, content: str) -> None: - """ - 使用 PushMe 推送消息。 - """ - if not push_config.get("PUSHME_KEY"): - print("PushMe 服务的 PUSHME_KEY 未设置!!\n取消推送") - return - print("PushMe 服务启动") - - url = ( - push_config.get("PUSHME_URL") - if push_config.get("PUSHME_URL") - else "https://push.i-i.me/" - ) - data = { - "push_key": push_config.get("PUSHME_KEY"), - "title": title, - "content": content, - "date": push_config.get("date") if push_config.get("date") else "", - "type": push_config.get("type") if push_config.get("type") else "", - } - response = requests.post(url, data=data) - - if response.status_code == 200 and response.text == "success": - print("PushMe 推送成功!") - else: - print(f"PushMe 推送失败!{response.status_code} {response.text}") - - -def chronocat(title: str, content: str) -> None: - """ - 使用 CHRONOCAT 推送消息。 - """ - if ( - not push_config.get("CHRONOCAT_URL") - or not push_config.get("CHRONOCAT_QQ") - or not push_config.get("CHRONOCAT_TOKEN") - ): - print("CHRONOCAT 服务的 CHRONOCAT_URL 或 CHRONOCAT_QQ 未设置!!\n取消推送") - return - - print("CHRONOCAT 服务启动") - - user_ids = re.findall(r"user_id=(\d+)", push_config.get("CHRONOCAT_QQ")) - group_ids = re.findall(r"group_id=(\d+)", push_config.get("CHRONOCAT_QQ")) - - url = f'{push_config.get("CHRONOCAT_URL")}/api/message/send' - headers = { - "Content-Type": "application/json", - "Authorization": f'Bearer {push_config.get("CHRONOCAT_TOKEN")}', - } - - for chat_type, ids in [(1, user_ids), (2, group_ids)]: - if not ids: - continue - for chat_id in ids: - data = { - "peer": {"chatType": chat_type, "peerUin": chat_id}, - "elements": [ - { - "elementType": 1, - "textElement": {"content": f"{title}\n\n{content}"}, - } - ], - } - response = requests.post(url, headers=headers, data=json.dumps(data)) - if response.status_code == 200: - if chat_type == 1: - print(f"QQ个人消息:{ids}推送成功!") - else: - print(f"QQ群消息:{ids}推送成功!") - else: - if chat_type == 1: - print(f"QQ个人消息:{ids}推送失败!") - else: - print(f"QQ群消息:{ids}推送失败!") - -def ntfy(title: str, content: str) -> None: - """ - 使用 Ntfy 推送消息。 - """ - if not push_config.get("Ntfy_T"): - print("PushMe 服务的 PUSHME_KEY 未设置!!\n取消推送") - return - print("PushMe 服务启动") - - url = push_config.get("PUSHME_URL") if push_config.get("PUSHME_URL") else "https://push.i-i.me/" - data = { - "push_key": push_config.get("PUSHME_KEY"), - "title": title, - "content": content, - "date": push_config.get("date") if push_config.get("date") else "", - "type": push_config.get("type") if push_config.get("type") else "", - } - response = requests.post(url, data=data) - - if response.status_code == 200 and response.text == "success": - print("PushMe 推送成功!") - else: - print(f"PushMe 推送失败!{response.status_code} {response.text}") - - -def parse_headers(headers): - if not headers: - return {} - - parsed = {} - lines = headers.split("\n") - - for line in lines: - i = line.find(":") - if i == -1: - continue - - key = line[:i].strip().lower() - val = line[i + 1 :].strip() - parsed[key] = parsed.get(key, "") + ", " + val if key in parsed else val - - return parsed - - -def parse_string(input_string, value_format_fn=None): - matches = {} - pattern = r"(\w+):\s*((?:(?!\n\w+:).)*)" - regex = re.compile(pattern) - for match in regex.finditer(input_string): - key, value = match.group(1).strip(), match.group(2).strip() - try: - value = value_format_fn(value) if value_format_fn else value - json_value = json.loads(value) - matches[key] = json_value - except: - matches[key] = value - return matches - - -def parse_body(body, content_type, value_format_fn=None): - if not body or content_type == "text/plain": - return value_format_fn(body) if value_format_fn and body else body - - parsed = parse_string(body, value_format_fn) - - if content_type == "application/x-www-form-urlencoded": - data = urllib.parse.urlencode(parsed, doseq=True) - return data - - if content_type == "application/json": - data = json.dumps(parsed) - return data - - return parsed - - -def custom_notify(title: str, content: str) -> None: - """ - 通过 自定义通知 推送消息。 - """ - if not push_config.get("WEBHOOK_URL") or not push_config.get("WEBHOOK_METHOD"): - print("自定义通知的 WEBHOOK_URL 或 WEBHOOK_METHOD 未设置!!\n取消推送") - return - - print("自定义通知服务启动") - - WEBHOOK_URL = push_config.get("WEBHOOK_URL") - WEBHOOK_METHOD = push_config.get("WEBHOOK_METHOD") - WEBHOOK_CONTENT_TYPE = push_config.get("WEBHOOK_CONTENT_TYPE") - WEBHOOK_BODY = push_config.get("WEBHOOK_BODY") - WEBHOOK_HEADERS = push_config.get("WEBHOOK_HEADERS") - - if "$title" not in WEBHOOK_URL and "$title" not in WEBHOOK_BODY: - print("请求头或者请求体中必须包含 $title 和 $content") - return - - headers = parse_headers(WEBHOOK_HEADERS) - body = parse_body( - WEBHOOK_BODY, - WEBHOOK_CONTENT_TYPE, - lambda v: v.replace("$title", title.replace("\n", "\\n")).replace( - "$content", content.replace("\n", "\\n") - ), - ) - formatted_url = WEBHOOK_URL.replace( - "$title", urllib.parse.quote_plus(title) - ).replace("$content", urllib.parse.quote_plus(content)) - response = requests.request( - method=WEBHOOK_METHOD, url=formatted_url, headers=headers, timeout=15, data=body - ) - - if response.status_code == 200: - print("自定义通知推送成功!") - else: - print(f"自定义通知推送失败!{response.status_code} {response.text}") - - -def one() -> str: - """ - 获取一条一言。 - :return: - """ - url = "https://v1.hitokoto.cn/" - res = requests.get(url).json() - return res["hitokoto"] + " ----" + res["from"] - - -def add_notify_function(): - notify_function = [] - if push_config.get("BARK_PUSH"): - notify_function.append(bark) - if push_config.get("CONSOLE"): - notify_function.append(console) - if push_config.get("DD_BOT_TOKEN") and push_config.get("DD_BOT_SECRET"): - notify_function.append(dingding_bot) - if push_config.get("FSKEY"): - notify_function.append(feishu_bot) - if push_config.get("GOBOT_URL") and push_config.get("GOBOT_QQ"): - notify_function.append(go_cqhttp) - if push_config.get("GOTIFY_URL") and push_config.get("GOTIFY_TOKEN"): - notify_function.append(gotify) - if push_config.get("IGOT_PUSH_KEY"): - notify_function.append(iGot) - if push_config.get("PUSH_KEY"): - notify_function.append(serverJ) - if push_config.get("DEER_KEY"): - notify_function.append(pushdeer) - if push_config.get("CHAT_URL") and push_config.get("CHAT_TOKEN"): - notify_function.append(chat) - if push_config.get("PUSH_PLUS_TOKEN"): - notify_function.append(pushplus_bot) - if push_config.get("WE_PLUS_BOT_TOKEN"): - notify_function.append(weplus_bot) - if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"): - notify_function.append(qmsg_bot) - if push_config.get("QYWX_AM"): - notify_function.append(wecom_app) - if push_config.get("QYWX_KEY"): - notify_function.append(wecom_bot) - if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"): - notify_function.append(telegram_bot) - if ( - push_config.get("AIBOTK_KEY") - and push_config.get("AIBOTK_TYPE") - and push_config.get("AIBOTK_NAME") - ): - notify_function.append(aibotk) - if ( - push_config.get("SMTP_SERVER") - and push_config.get("SMTP_SSL") - and push_config.get("SMTP_EMAIL") - and push_config.get("SMTP_PASSWORD") - and push_config.get("SMTP_NAME") - ): - notify_function.append(smtp) - if push_config.get("PUSHME_KEY"): - notify_function.append(pushme) - if ( - push_config.get("CHRONOCAT_URL") - and push_config.get("CHRONOCAT_QQ") - and push_config.get("CHRONOCAT_TOKEN") - ): - notify_function.append(chronocat) - if push_config.get("WEBHOOK_URL") and push_config.get("WEBHOOK_METHOD"): - notify_function.append(custom_notify) - - if not notify_function: - print(f"无推送渠道,请检查通知变量是否正确") - return notify_function - - -def send(title: str, content: str, ignore_default_config: bool = False, **kwargs): - if kwargs: - global push_config - if ignore_default_config: - push_config = kwargs # 清空从环境变量获取的配置 - else: - push_config.update(kwargs) - - if not content: - print(f"{title} 推送内容为空!") - return - - # 根据标题跳过一些消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔 - skipTitle = os.getenv("SKIP_PUSH_TITLE") - if skipTitle: - if title in re.split("\n", skipTitle): - print(f"{title} 在SKIP_PUSH_TITLE环境变量内,跳过推送!") - return - - hitokoto = push_config.get("HITOKOTO") - content += "\n\n" + one() if hitokoto not in [False, "false"] else "" - - notify_function = add_notify_function() - ts = [ - threading.Thread(target=mode, args=(title, content), name=mode.__name__) - for mode in notify_function - ] - [t.start() for t in ts] - [t.join() for t in ts] - - -def main(): - send("title", "content") - - -if __name__ == "__main__": - main() diff --git a/shell/bot.sh b/shell/bot.sh index f30cad26..3d8e799d 100755 --- a/shell/bot.sh +++ b/shell/bot.sh @@ -9,7 +9,29 @@ else fi echo -e "\n1、安装bot依赖...\n" -apk --no-cache add -f zlib-dev gcc jpeg-dev python3-dev musl-dev freetype-dev +os_name="${QL_OS_TYPE:-}" +if [ -z "$os_name" ]; then + os_name=$(source /etc/os-release && echo "$ID") +fi + +# 非 root 用户使用 sudo +SUDO="" +if [ "$(id -u)" -ne 0 ]; then + SUDO="sudo" +fi + +case "$os_name" in + alpine) + $SUDO apk --no-cache add -f zlib-dev gcc jpeg-dev python3-dev musl-dev freetype-dev + ;; + debian|ubuntu) + $SUDO apt-get install -y gcc python3-dev musl-dev zlib1g-dev libjpeg-dev libfreetype-dev + ;; + *) + echo -e "暂不支持此系统 $os_name" + exit 1 + ;; +esac echo -e "\nbot依赖安装成功...\n" echo -e "2、下载bot所需文件...\n" diff --git a/shell/otask.sh b/shell/otask.sh index 516a9439..c6612bd1 100755 --- a/shell/otask.sh +++ b/shell/otask.sh @@ -83,6 +83,35 @@ clear_non_sh_env() { fi } +append_node_dependency_path() { + export PREV_NODE_PATH="${NODE_PATH:=}" + + local pnpm_global_path=$(pnpm root -g 2>/dev/null) + if [[ -n "$pnpm_global_path" ]]; then + export QL_NODE_GLOBAL_PATH="$pnpm_global_path" + export NODE_PATH="${NODE_PATH:+${NODE_PATH}:}${pnpm_global_path}" + fi +} + +enter_script_workdir() { + local use_dot_prefix="$1" + + cd $dir_scripts + if [[ ${file_param} =~ "/" ]]; then + local script_dir="${file_param%/*}" + local script_name="${file_param##*/}" + + if [[ -d ${script_dir} ]]; then + cd ${script_dir} + if [[ "${use_dot_prefix}" == "true" ]]; then + file_param="./${script_name}" + else + file_param="${script_name}" + fi + fi + fi +} + ## 正常运行单个脚本,$1:传入参数 run_normal() { local file_param=$1 @@ -90,12 +119,7 @@ run_normal() { random_delay "$file_param" fi - cd $dir_scripts - local relative_path="${file_param%/*}" - if [[ ${file_param} != /* ]] && [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then - cd ${relative_path} - file_param=${file_param/$relative_path\//} - fi + enter_script_workdir if [[ $isJsOrPythonFile == 'false' ]]; then clear_non_sh_env @@ -128,12 +152,7 @@ run_concurrent() { time=$(date "+$mtime_format") single_log_time=$(format_log_time "$mtime_format" "$time") - cd $dir_scripts - local relative_path="${file_param%/*}" - if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then - cd ${relative_path} - file_param=${file_param/$relative_path\//} - fi + enter_script_workdir local j=0 for i in ${array_run[@]}; do @@ -182,12 +201,7 @@ run_designated() { clear_non_sh_env fi - cd $dir_scripts - local relative_path="${file_param%/*}" - if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then - cd ${relative_path} - file_param=${file_param/$relative_path\//} - fi + enter_script_workdir envParam="${env_param}" numParam="${num_param}" $timeoutCmd $which_program $file_param "${script_params[@]}" } @@ -196,12 +210,7 @@ run_designated() { run_else() { local file_param="$1" - cd $dir_scripts - local relative_path="${file_param%/*}" - if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then - cd ${relative_path} - file_param=${file_param/$relative_path\//.\/} - fi + enter_script_workdir true shift @@ -242,7 +251,7 @@ check_nounset() { } main() { - if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then + if [[ $1 == *.js ]] || [[ $1 == *.mjs ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then if [[ $1 == *.sh ]]; then timeoutCmd="" fi @@ -278,6 +287,7 @@ main() { handle_task_start "${task_shell_params[@]}" check_file "${task_shell_params[@]}" +append_node_dependency_path if [[ $isJsOrPythonFile == 'false' ]]; then run_task_before "${task_shell_params[@]}" fi @@ -287,6 +297,8 @@ main "${task_shell_params[@]}" if [[ "$set_u_on" == 'true' ]]; then set -u fi +export NODE_PATH="${PREV_NODE_PATH}" +unset QL_NODE_GLOBAL_PATH if [[ $isJsOrPythonFile == 'true' ]]; then export NODE_OPTIONS="${PREV_NODE_OPTIONS}" export PYTHONPATH="${PREV_PYTHONPATH}" diff --git a/shell/preload/sitecustomize.js b/shell/preload/sitecustomize.js index dbb9198f..44dc1bea 100644 --- a/shell/preload/sitecustomize.js +++ b/shell/preload/sitecustomize.js @@ -1,7 +1,36 @@ const { execSync } = require('child_process'); +const Module = require('module'); +const path = require('path'); const client = require('./client.js'); require(`./env.js`); +function preferGlobalNodeModules() { + const { QL_NODE_GLOBAL_PATH } = process.env; + if (!QL_NODE_GLOBAL_PATH || Module._qlGlobalPathPatched) { + return; + } + + const originalResolveFilename = Module._resolveFilename; + Module._resolveFilename = function (request, parent, isMain, options) { + if ( + !Module.builtinModules.includes(request) && + !request.startsWith('node:') && + !request.startsWith('.') && + !path.isAbsolute(request) + ) { + try { + return originalResolveFilename.call(this, request, parent, isMain, { + ...options, + paths: [QL_NODE_GLOBAL_PATH], + }); + } catch (error) {} + } + + return originalResolveFilename.call(this, request, parent, isMain, options); + }; + Module._qlGlobalPathPatched = true; +} + function expandRange(rangeStr, max) { const tempRangeStr = rangeStr .trim() @@ -113,6 +142,8 @@ try { return; } + preferGlobalNodeModules(); + process.on('SIGTERM', (code) => { process.exit(15); }); diff --git a/shell/pub.sh b/shell/pub.sh index 1d126bc6..1d5794f4 100755 --- a/shell/pub.sh +++ b/shell/pub.sh @@ -1,26 +1,26 @@ #!/usr/bin/env bash echo -e "开始发布" -echo -e "切换master分支" -git branch -D master -git checkout -b master -git push --set-upstream origin master -f +echo -e "切换 debian 分支" +git branch -D debian +git checkout -b debian +git push --set-upstream origin debian -f echo -e "更新cdn文件" ts-node-transpile-only sample/tool.ts string=$(cat version.yaml | grep "version" | egrep "[^ ]*" -o | egrep "\d\.*") version="v$string" -echo -e "当前版本$version" +echo -e "当前版本$version-debian" echo -e "删除已经存在的本地tag" -git tag -d "$version" &>/dev/null +git tag -d "$version-debian" &>/dev/null echo -e "删除已经存在的远程tag" -git push origin :refs/tags/$version &>/dev/null +git push origin :refs/tags/$version-debian &>/dev/null echo -e "创建新tag" -git tag -a "$version" -m "release $version" +git tag -a "$version-debian" -m "release $version-debian" echo -e "提交tag" git push --tags diff --git a/shell/share.sh b/shell/share.sh index cad30659..585d885d 100755 --- a/shell/share.sh +++ b/shell/share.sh @@ -65,17 +65,7 @@ link_name=( ) init_env() { - local pnpm_global_path=$(pnpm root -g 2>/dev/null) - export NODE_PATH="/usr/local/bin:/usr/local/lib/node_modules${pnpm_global_path:+:${pnpm_global_path}}" - - # 如果存在 pnpm 全局路径,创建软链接 - if [[ -n "$pnpm_global_path" ]]; then - # 确保目标目录存在 - mkdir -p "${dir_root}/node_modules" - # 链接全局模块到项目的 node_modules - ln -sf "${pnpm_global_path}/"* "${dir_root}/node_modules/" 2>/dev/null || true - fi - + export NODE_PATH="/usr/local/bin:/usr/local/lib/node_modules" export PYTHONUNBUFFERED=1 } diff --git a/shell/start.sh b/shell/start.sh new file mode 100644 index 00000000..3f4bb9cd --- /dev/null +++ b/shell/start.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash + +# 前置依赖 nodejs、npm、python3 +set -e +set -x + +if [[ ! $QL_DIR ]]; then + npm_dir=$(npm root -g) + pnpm_dir=$(pnpm root -g) + if [[ -d "$npm_dir/@whyour/qinglong" ]]; then + QL_DIR="$npm_dir/@whyour/qinglong" + elif [[ -d "$pnpm_dir/@whyour/qinglong" ]]; then + QL_DIR="$pnpm_dir/@whyour/qinglong" + else + echo -e "未找到 qinglong 模块,请先执行 npm i -g @whyour/qinglong 安装" + fi + + if [[ $QL_DIR ]]; then + echo -e "请先手动设置 export QL_DIR=$QL_DIR,环境变量,并手动添加到系统环境变量,然后再次执行命令 qinglong 启动服务" + fi + + exit 1 +fi + +if [[ ! $QL_DATA_DIR ]]; then + echo -e "请先手动设置数据存储目录 export QL_DATA_DIR 环境变量,目录必须以斜杠开头的绝对路径,并且以 /data 结尾,例如 /ql/data 并手动添加到系统环境变量" + exit 1 +fi + +if [[ $QL_DATA_DIR != */data ]]; then + echo -e "QL_DATA_DIR 必须以 /data 结尾,例如 /ql/data,如果有历史数据,请新建 data 目录,把历史数据放到 data 目录中" + exit 1 +fi + +command="$1" + +if [[ $command != "reload" ]]; then + # 安装依赖 + os_name="${QL_OS_TYPE:-}" + if [ -z "$os_name" ]; then + os_name=$(source /etc/os-release && echo "$ID") + fi + + # 非 root 用户使用 sudo + SUDO="" + if [ "$(id -u)" -ne 0 ]; then + SUDO="sudo" + fi + + case "$os_name" in + alpine) + $SUDO apk update + $SUDO apk add -f bash \ + coreutils \ + git \ + curl \ + wget \ + tzdata \ + perl \ + openssl \ + jq \ + nginx \ + openssh \ + procps \ + netcat-openbsd + ;; + debian|ubuntu) + $SUDO apt-get update + $SUDO apt-get install -y git curl wget tzdata perl openssl jq nginx procps netcat-openbsd openssh-client + ;; + *) + echo -e "暂不支持此系统部署 $os_name" + exit 1 + ;; + esac + + npm install -g pnpm@8.3.1 pm2 ts-node +fi + +export PYTHON_SHORT_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') +export PNPM_HOME=${QL_DIR}/data/dep_cache/node +export PYTHON_HOME=${QL_DIR}/data/dep_cache/python3 +export PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3 + +export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin +export NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules +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} +cp -f .env.example .env +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 + + 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" "🎉 启动成功!" diff --git a/src/components/tag.tsx b/src/components/tag.tsx index f9b5db51..2df5cb50 100644 --- a/src/components/tag.tsx +++ b/src/components/tag.tsx @@ -66,9 +66,7 @@ const EditableTagGroup = ({ }, [inputVisible]); useEffect(() => { - if (value) { - setTags(value); - } + setTags(value || []); }, [value]); return ( diff --git a/src/components/terminal.tsx b/src/components/terminal.tsx index 0492595b..8b4d1804 100644 --- a/src/components/terminal.tsx +++ b/src/components/terminal.tsx @@ -29,10 +29,10 @@ const Terminal = ({ const lastLineRef = useRef(null); // An effect that handles scrolling into view the last line of terminal input or output - const performScrolldown = useRef(false); + const performScrollDown = useRef(false); useEffect(() => { - if (performScrolldown.current) { - // skip scrolldown when the component first loads + if (performScrollDown.current) { + // skip scrollDown when the component first loads setTimeout( () => lastLineRef?.current?.scrollIntoView({ @@ -42,7 +42,7 @@ const Terminal = ({ 500, ); } - performScrolldown.current = true; + performScrollDown.current = true; }, [lineData.length]); const renderedLineData = lineData.map((ld, i) => { diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 0a1a2a2a..4662e9b7 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -357,8 +357,8 @@ "BARK推送图标,自定义推送图标 (需iOS15或以上才能显示)": "BARK push icon, custom push icon (requires iOS 15 or above to display)", "BARK推送铃声,铃声列表去APP查看复制填写": "BARK push ringtone, check and copy from the APP's ringtone list", "BARK推送消息的分组,默认为qinglong": "BARK push message grouping, default is qinglong", - "BARK推送消息的时效性,默认为active": "BARK push message redirecting URL", - "BARK推送消息的跳转URL": "BARK push message grouping, default is qinglong", + "BARK推送消息的时效性,默认为active": "BARK push message timeliness, default is active", + "BARK推送消息的跳转URL": "BARK push message redirecting URL", "BARK是否保存推送消息": "Does BARK save push messages", "telegram机器人的token,例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw": "Telegram Bot token, e.g., 1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw", "telegram用户的id,例如:129xxx206": "Telegram user ID, e.g., 129xxx206", @@ -437,6 +437,9 @@ "Cron表达式格式有误": "Incorrect Cron Expression Format", "添加Labels成功": "Labels added successfully", "删除Labels成功": "Labels deleted successfully", + "添加标签成功": "Tags added successfully", + "删除标签成功": "Tags deleted successfully", + "请至少输入一个标签": "Please enter at least one tag", "编辑视图": "Edit View", "排序方式": "Sort Order", "开始时间": "Start Time", diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 448e140c..01cdf4ab 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -437,6 +437,9 @@ "Cron表达式格式有误": "Cron表达式格式有误", "添加Labels成功": "添加Labels成功", "删除Labels成功": "删除Labels成功", + "添加标签成功": "添加标签成功", + "删除标签成功": "删除标签成功", + "请至少输入一个标签": "请至少输入一个标签", "编辑视图": "编辑视图", "排序方式": "排序方式", "开始时间": "开始时间", diff --git a/src/pages/diff/index.tsx b/src/pages/diff/index.tsx index 5897cb2e..3b42d91e 100644 --- a/src/pages/diff/index.tsx +++ b/src/pages/diff/index.tsx @@ -64,7 +64,7 @@ const Diff = () => { const getFiles = () => { setLoading(true); request - .get(`${config.apiPrefix}configs/sample`) + .get(`${config.apiPrefix}configs/samples`) .then(({ code, data }) => { if (code === 200) { setFiles(data); diff --git a/src/pages/env/index.tsx b/src/pages/env/index.tsx index fde76fda..0cb2c610 100644 --- a/src/pages/env/index.tsx +++ b/src/pages/env/index.tsx @@ -36,7 +36,7 @@ import { useVT } from 'virtualizedtableforantd4'; import Copy from '../../components/copy'; import EditNameModal from './editNameModal'; import './index.less'; -import EnvModal from './modal'; +import EnvModal, { EnvLabelModal } from './modal'; const { Paragraph } = Typography; const { Search } = Input; @@ -121,6 +121,25 @@ const Env = () => { ); }, }, + { + title: intl.get('标签'), + dataIndex: 'labels', + key: 'labels', + render: (labels: string[] | null) => { + const envLabels = Array.isArray(labels) ? labels : []; + return ( + + {envLabels + .filter((label) => label) + .map((label) => ( + + {label} + + ))} + + ); + }, + }, { title: intl.get('更新时间'), dataIndex: 'timestamp', @@ -238,6 +257,7 @@ const Env = () => { const [loading, setLoading] = useState(true); const [isModalVisible, setIsModalVisible] = useState(false); const [isEditNameModalVisible, setIsEditNameModalVisible] = useState(false); + const [isLabelModalVisible, setIsLabelModalVisible] = useState(false); const [editedEnv, setEditedEnv] = useState(); const [selectedRowIds, setSelectedRowIds] = useState([]); const [searchText, setSearchText] = useState(''); @@ -408,6 +428,13 @@ const Env = () => { getEnvs(); }; + const handleLabelCancel = (needUpdate?: boolean) => { + setIsLabelModalVisible(false); + if (needUpdate) { + getEnvs(); + } + }; + const [vt, setVT] = useVT( () => ({ scroll: { y: tableScrollHeight } }), [tableScrollHeight], @@ -542,7 +569,12 @@ const Env = () => { const exportEnvs = () => { const envs = value .filter((x) => selectedRowIds.includes(x.id)) - .map((x) => ({ value: x.value, name: x.name, remarks: x.remarks })); + .map((x) => ({ + value: x.value, + name: x.name, + remarks: x.remarks, + labels: x.labels, + })); exportJson('env.json', JSON.stringify(envs)); }; @@ -550,6 +582,10 @@ const Env = () => { setIsEditNameModalVisible(true); }; + const modifyLabels = () => { + setIsLabelModalVisible(true); + }; + const onSearch = (value: string) => { setSearchText(value.trim()); }; @@ -622,6 +658,13 @@ const Env = () => { > {intl.get('批量修改变量名称')} + , + , + , + ]} + centered + maskClosable={false} + forceRender + onCancel={() => handleCancel(false)} + > +
+ + + +
+ + ); +}; diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index be88e737..60755c0d 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -50,7 +50,7 @@ const Login = () => { }); }; - const completeTowFactor = (values: any) => { + const completeTwoFactor = (values: any) => { setVerifying(true); request .put(`${config.apiPrefix}user/two-factor/login`, { @@ -129,7 +129,7 @@ const Login = () => { const { value } = e.target as any; const regx = /^[0-9]{6}$/; if (regx.test(value)) { - completeTowFactor({ code: value }); + completeTwoFactor({ code: value }); } }; @@ -156,7 +156,7 @@ const Login = () => {
{twoFactor ? ( -
+ { { setSystemConfig({ diff --git a/src/pages/setting/security.tsx b/src/pages/setting/security.tsx index 19d98d09..29b92f00 100644 --- a/src/pages/setting/security.tsx +++ b/src/pages/setting/security.tsx @@ -37,18 +37,18 @@ const SecuritySettings = ({ user, userChange }: any) => { }); }; - const activeOrDeactiveTwoFactor = () => { + const activeOrDeactivateTwoFactor = () => { if (twoFactorActivated) { - deactiveTowFactor(); + deactivateTwoFactor(); } else { getTwoFactorInfo(); setTwoFactoring(true); } }; - const deactiveTowFactor = () => { + const deactivateTwoFactor = () => { request - .put(`${config.apiPrefix}user/two-factor/deactive`) + .put(`${config.apiPrefix}user/two-factor/deactivate`) .then(({ code, data }) => { if (code === 200 && data) { setTwoFactorActivated(false); @@ -60,7 +60,7 @@ const SecuritySettings = ({ user, userChange }: any) => { }); }; - const completeTowFactor = () => { + const completeTwoFactor = () => { setLoading(true); request .put(`${config.apiPrefix}user/two-factor/active`, { code }) @@ -162,7 +162,7 @@ const SecuritySettings = ({ user, userChange }: any) => { onChange={(e) => setCode(e.target.value)} placeholder="123456" /> -
@@ -230,7 +230,7 @@ const SecuritySettings = ({ user, userChange }: any) => { diff --git a/src/utils/config.ts b/src/utils/config.ts index b529a7d0..3249f67c 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -15,29 +15,6 @@ export default { exclude: [/(\/(en|zh))*\/login/], }, ], - - /* I18n configuration, `languages` and `defaultLanguage` are required currently. */ - i18n: { - /* Countrys flags: https://www.flaticon.com/packs/countrys-flags */ - languages: [ - { - key: 'pt-br', - title: 'Português', - flag: '/portugal.svg', - }, - { - key: 'en', - title: 'English', - flag: '/america.svg', - }, - { - key: 'zh', - title: intl.get('中文'), - flag: '/china.svg', - }, - ], - defaultLanguage: 'en', - }, scopes: [ { name: intl.get('定时任务'), @@ -98,6 +75,7 @@ export default { { value: 'pushPlus', label: 'PushPlus' }, { value: 'wePlusBot', label: intl.get('微加机器人') }, { value: 'wxPusherBot', label: 'wxPusher' }, + { value: 'openiLink', label: 'OpeniLink' }, { value: 'chat', label: intl.get('群晖chat') }, { value: 'email', label: intl.get('邮箱') }, { value: 'lark', label: intl.get('飞书机器人') }, @@ -387,6 +365,27 @@ export default { required: false, }, ], + openiLink: [ + { + label: 'openiLinkAppToken', + tip: intl.get( + 'OpeniLink的app_token,在OpeniLink Hub后台安装App后获取,参考 https://openilink.com/docs/hub/apps', + ), + required: true, + }, + { + label: 'openiLinkHubUrl', + tip: intl.get( + 'OpeniLink Hub地址,默认为 https://hub.openilink.com,自建Hub时填写自己的地址', + ), + }, + { + label: 'openiLinkContextToken', + tip: intl.get( + 'OpeniLink的context_token,用于标识消息会话上下文,可从消息事件中获取', + ), + }, + ], lark: [ { label: 'larkKey', diff --git a/src/utils/index.ts b/src/utils/index.ts index 9260a100..d393df5c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -179,7 +179,7 @@ export default function browserType() { /** * 获取第一个表格的可视化高度 * @param {*} extraHeight 额外的高度(表格底部的内容高度 Number类型,默认为74) - * @param {*} id 当前页面中有多个table时需要制定table的id + * @param {*} id 当前页面中有多个table时需要指定table的id */ export function getTableScroll({ extraHeight, @@ -208,7 +208,7 @@ export function getTableScroll({ } // 自动触发点击事件 -function automaticClick(elment: HTMLElement) { +function automaticClick(element: HTMLElement) { const ev = document.createEvent('MouseEvents'); ev.initMouseEvent( 'click', @@ -227,7 +227,7 @@ function automaticClick(elment: HTMLElement) { 0, null, ); - elment.dispatchEvent(ev); + element.dispatchEvent(ev); } // 导出文件 diff --git a/version.yaml b/version.yaml index 2ab325e8..65648012 100644 --- a/version.yaml +++ b/version.yaml @@ -1,11 +1,6 @@ -version: 2.20.1 -changeLogLink: https://t.me/jiao_long/433 -publishTime: 2025-12-26 22:00 +version: 2.20.2 +changeLogLink: https://t.me/jiao_long/434 +publishTime: 2026-03-01 1800 changeLog: | - 1. 修复获取依赖管理列表 - 2. notify.js 修复 TG_PROXY_AUTH 参数拼接 - 3. QLAPI.notify larkSecret 参数 - 4. 修复 cron parser 定时规则校验 - 5. 修复设置 baseUrl 后无法访问 - 6. 修复环境变量排序 - 7. 修复定时任务无法停止 \ No newline at end of file + 1. 修复 path 安全漏洞(重要) + \ No newline at end of file