diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index 6b399d28..420dc9f4 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -89,9 +89,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 @@ -135,16 +132,13 @@ jobs: 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@v6 @@ -160,11 +154,9 @@ jobs: run: | VERSION=$(grep '^version:' version.yaml | awk '{print $2}') echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Version: $VERSION" - name: Setup timezone - run: | - sudo timedatectl set-timezone Asia/Shanghai + run: sudo timedatectl set-timezone Asia/Shanghai - name: Login to DockerHub uses: docker/login-action@v4 @@ -190,7 +182,7 @@ 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}} @@ -200,8 +192,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - - name: Build and push - id: docker_build + - name: Build and push (Alpine) uses: docker/build-push-action@v7 with: build-args: | @@ -209,30 +200,22 @@ jobs: 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@v6 @@ -248,11 +231,85 @@ jobs: run: | VERSION=$(grep '^version:' version.yaml | awk '{print $2}') echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Version: $VERSION" - 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: 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@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - 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: | - sudo timedatectl set-timezone Asia/Shanghai + 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 @@ -273,8 +330,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - - name: Build and push python3.10 - id: docker_build_310 + - name: Build and push (Alpine Python 3.10) uses: docker/build-push-action@v7 with: build-args: | @@ -282,7 +338,6 @@ jobs: 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 @@ -290,9 +345,93 @@ jobs: 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/310.Dockerfile.debian + 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/.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/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/config/const.ts b/back/config/const.ts index e5083f43..74be9a93 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: 'apt-get install -y', + uninstall: 'apt-get remove -y', + info: 'dpkg-query -s', + check(info: string) { + return info.includes('install ok installed'); + }, + }, + Ubuntu: { + install: 'apt-get install -y', + uninstall: 'apt-get remove -y', + info: '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/util.ts b/back/config/util.ts index 7d61f74f..0f050193 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}` + : `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' + : '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' + : '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 '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 '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/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/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/docker/310.Dockerfile b/docker/310.Dockerfile index 6cbbc127..6bc02e53 100644 --- a/docker/310.Dockerfile +++ b/docker/310.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/ diff --git a/docker/310.Dockerfile.debian b/docker/310.Dockerfile.debian new file mode 100644 index 00000000..194a6faa --- /dev/null +++ b/docker/310.Dockerfile.debian @@ -0,0 +1,112 @@ +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 + +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 \ + 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} ${QL_DIR}/data && \ + 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=/root + +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 + +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 b/docker/Dockerfile index 405260ac..352a6cb6 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/ diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian new file mode 100644 index 00000000..45ba5104 --- /dev/null +++ b/docker/Dockerfile.debian @@ -0,0 +1,112 @@ +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 + +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 \ + 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} ${QL_DIR}/data && \ + 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=/root + +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 + +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 b0693fe3..3015073a 100755 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -15,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 @@ -35,6 +94,15 @@ if ! grep -qE '^::1[[:space:]]+.*localhost' /etc/hosts 2>/dev/null; then log_with_style "INFO" "🔧 0. 已添加 IPv6 localhost 解析" fi +# 在一切操作之前检查目录权限 +ensure_ql_permissions + +# Dockerfile 中 HOME=/root,非 root 用户无法写入 +# 将 HOME 修正为临时目录,PM2/npm/pip 等工具的运行时数据无需持久化 +if [ ! -w "$HOME" ]; then + export HOME="$QL_DIR/.tmp" +fi + log_with_style "INFO" "🚀 1. 检测配置文件..." load_ql_envs export_ql_envs @@ -60,6 +128,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/package.json b/package.json index eb0b5af4..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": [ diff --git a/shell/bot.sh b/shell/bot.sh index f30cad26..524a44e9 100755 --- a/shell/bot.sh +++ b/shell/bot.sh @@ -9,7 +9,22 @@ 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 +case "$os_name" in + alpine) + apk --no-cache add -f zlib-dev gcc jpeg-dev python3-dev musl-dev freetype-dev + ;; + debian|ubuntu) + 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/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/start.sh b/shell/start.sh new file mode 100644 index 00000000..1f015f2b --- /dev/null +++ b/shell/start.sh @@ -0,0 +1,132 @@ +#!/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 + + case "$os_name" in + alpine) + apk update + apk add -f bash \ + coreutils \ + git \ + curl \ + wget \ + tzdata \ + perl \ + openssl \ + jq \ + nginx \ + openssh \ + procps \ + netcat-openbsd + ;; + debian|ubuntu) + apt-get update + 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/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/setting/dependence.tsx b/src/pages/setting/dependence.tsx index 536c7861..9a0974aa 100644 --- a/src/pages/setting/dependence.tsx +++ b/src/pages/setting/dependence.tsx @@ -215,12 +215,12 @@ const Dependence = () => { { setSystemConfig({