diff --git a/.github/workflows/build_docker_image.yml b/.github/workflows/build-docker-image.yml similarity index 100% rename from .github/workflows/build_docker_image.yml rename to .github/workflows/build-docker-image.yml diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 00000000..3915d91e --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,52 @@ +name: Build Release Packages + +on: + push: + paths-ignore: + - "*.md" + tags: + - "v*" + workflow_dispatch: + +jobs: + build-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: pnpm/action-setup@v3 + with: + version: "8.3.1" + + - uses: actions/setup-node@v4 + with: + node-version: "18" + cache: "pnpm" + + - name: Install dependencies + run: | + pnpm install + pnpm install -g pkg + + - name: Install zip tool + run: sudo apt-get install -y zip + + - name: Build release packages + run: node build-release.js + + - name: Upload release artifacts + uses: actions/upload-artifact@v5 + with: + name: release-packages + path: dist/*.zip + retention-days: 7 + + - name: Create GitHub Release + if: startsWith(github.ref, 'refs/tags/v') + uses: ncipollo/release-action@v1 + with: + artifacts: "dist/*.zip" + token: ${{ secrets.GITHUB_TOKEN }} + generateReleaseNotes: true diff --git a/.gitignore b/.gitignore index 709791ed..0739200b 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ __pycache__ /shell/preload/notify.* /shell/preload/*-notify.json /shell/preload/__ql_notify__.* +/dist +/.pkg-temp diff --git a/back/app.ts b/back/app.ts index 35042d0c..0859140c 100644 --- a/back/app.ts +++ b/back/app.ts @@ -96,7 +96,9 @@ class Application { } private setupMiddlewares() { - this.app.use(helmet()); + this.app.use(helmet({ + contentSecurityPolicy: false, + })); this.app.use(cors(config.cors)); this.app.use(compression()); this.app.use(monitoringMiddleware); diff --git a/back/loaders/express.ts b/back/loaders/express.ts index aa3d0ef6..b677489a 100644 --- a/back/loaders/express.ts +++ b/back/loaders/express.ts @@ -9,6 +9,7 @@ import rewrite from 'express-urlrewrite'; import { errors } from 'celebrate'; import { serveEnv } from '../config/serverEnv'; import { IKeyvStore, shareStore } from '../shared/store'; +import path from 'path'; export default ({ app }: { app: Application }) => { app.set('trust proxy', 'loopback'); @@ -19,12 +20,16 @@ export default ({ app }: { app: Application }) => { app.use(bodyParser.json({ limit: '50mb' })); app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); + const frontendPath = path.join(config.rootPath, 'static/dist'); + app.use(express.static(frontendPath)); + app.use( expressjwt({ secret: config.jwt.secret, algorithms: ['HS384'], }).unless({ - path: [...config.apiWhiteList, /^\/open\//], + // 使用正则表达式排除非API路径,只对/api/和/open/路径应用JWT验证 + path: [...config.apiWhiteList, /^\/$/, /^\/(?!api\/)(?!open\/).*/] }), ); @@ -39,6 +44,10 @@ export default ({ app }: { app: Application }) => { }); app.use(async (req: Request, res, next) => { + if (!['/open/', '/api/'].some((x) => req.path.startsWith(x))) { + return next(); + } + const headerToken = getToken(req); if (req.path.startsWith('/open/')) { const apps = await shareStore.getApps(); @@ -110,10 +119,15 @@ export default ({ app }: { app: Application }) => { app.use(rewrite('/open/*', '/api/$1')); app.use(config.api.prefix, routes()); - app.use((req, res, next) => { - const err: any = new Error('Not Found'); - err['status'] = 404; - next(err); + app.get('*', (req, res, next) => { + const indexPath = path.join(frontendPath, 'index.html'); + res.sendFile(indexPath, (err) => { + if (err) { + const err: any = new Error('Not Found'); + err['status'] = 404; + next(err); + } + }); }); app.use(errors()); diff --git a/build-release.js b/build-release.js new file mode 100755 index 00000000..e914ea03 --- /dev/null +++ b/build-release.js @@ -0,0 +1,478 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +// 获取当前平台信息 +const getCurrentPlatform = () => { + const platform = process.platform === 'darwin' ? 'macos' : process.platform === 'win32' ? 'win' : 'linux'; + const arch = process.arch === 'x64' ? 'x64' : process.arch === 'arm64' || process.arch === 'aarch64' ? 'arm64' : 'x64'; + return { platform, arch }; +}; + +// 配置 +const CONFIG = { + outputDir: path.join(__dirname, 'dist'), + platforms: [ + { nodeVersion: '18', platform: 'linux', arch: 'x64' }, + { nodeVersion: '18', platform: 'linux', arch: 'arm64' }, + { nodeVersion: '18', platform: 'win', arch: 'x64' }, + { nodeVersion: '18', platform: 'macos', arch: 'x64' }, + { nodeVersion: '18', platform: 'macos', arch: 'arm64' }, + ], +}; + +// 解析命令行参数 +const args = process.argv.slice(2); +const options = { + localOnly: args.includes('--local-only'), + platform: args.find(arg => arg.startsWith('--platform='))?.split('=')[1], + arch: args.find(arg => arg.startsWith('--arch='))?.split('=')[1], +}; + +// 根据参数过滤平台 +function getTargetPlatforms() { + if (options.localOnly) { + const current = getCurrentPlatform(); + console.log(`只构建当前平台: ${current.platform}-${current.arch}`); + return [{ + nodeVersion: '18', + platform: current.platform, + arch: current.arch + }]; + } + + if (options.platform && options.arch) { + console.log(`只构建指定平台: ${options.platform}-${options.arch}`); + return [{ + nodeVersion: '18', + platform: options.platform, + arch: options.arch + }]; + } + + return CONFIG.platforms; +} + +// 确保输出目录存在 +function ensureDir(dir) { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } +} + +// 执行命令 +function runCommand(command, cwd = __dirname) { + console.log(`执行命令: ${command}`); + execSync(command, { cwd, stdio: 'inherit' }); +} + +// 清理之前的构建 +function cleanBuild() { + console.log('清理之前的构建...'); + if (fs.existsSync(CONFIG.outputDir)) { + fs.rmSync(CONFIG.outputDir, { recursive: true, force: true }); + } + if (fs.existsSync(path.join(__dirname, 'back/dist'))) { + fs.rmSync(path.join(__dirname, 'back/dist'), { recursive: true, force: true }); + } + if (fs.existsSync(path.join(__dirname, 'static/build'))) { + fs.rmSync(path.join(__dirname, 'static/build'), { recursive: true, force: true }); + } + ensureDir(CONFIG.outputDir); +} + +// 安装依赖 +function installDependencies() { + console.log('安装依赖...'); + runCommand('pnpm install'); +} + +// 构建前端 +function buildFrontend() { + console.log('构建前端...'); + runCommand('npm run build:front'); +} + +// 构建后端 +function buildBackend() { + console.log('构建后端...'); + runCommand('npm run build:back'); +} + +// 准备打包所需文件 +function preparePackageFiles() { + console.log('准备打包所需文件...'); + + // 创建临时目录用于打包 + const tempDir = path.join(__dirname, '.pkg-temp'); + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + ensureDir(tempDir); + + // 复制必要的文件和目录 + const copyFiles = [ + // 直接使用编译后的JavaScript文件,不再依赖TypeScript源文件 + { from: path.join(__dirname, 'static/build'), to: path.join(tempDir, 'static/build') }, + { from: path.join(__dirname, 'shell'), to: path.join(tempDir, 'shell') }, + { from: path.join(__dirname, 'sample'), to: path.join(tempDir, 'sample') }, + { from: path.join(__dirname, '.env.example'), to: path.join(tempDir, '.env.example') }, + // 复制整个static目录以确保前端文件完整(包含dist目录中的前端文件) + { from: path.join(__dirname, 'static'), to: path.join(tempDir, 'static') }, + ]; + + // 额外复制前端构建产物到更明确的位置 + if (fs.existsSync(path.join(__dirname, 'static/dist'))) { + copyFiles.push({ + from: path.join(__dirname, 'static/dist'), + to: path.join(tempDir, 'static/dist') + }); + } + + copyFiles.forEach(({ from, to }) => { + if (fs.existsSync(from)) { + console.log(`复制文件: ${from} -> ${to}`); + fs.cpSync(from, to, { recursive: true }); + } + }); + + // 创建入口文件 + const entryContent = ` +require('dotenv').config(); +const path = require('path'); +const fs = require('fs'); + +// 设置QL_DIR环境变量 +process.env.QL_DIR = process.env.QL_DIR || path.dirname(process.execPath); +const qlDir = process.env.QL_DIR; +console.log('🚀 QL_DIR is set to: ' + qlDir); + +// 设置NODE_PATH以支持模块解析 +process.env.NODE_PATH = path.resolve(qlDir, 'node_modules'); +require('module').Module._initPaths(); + +// 配置静态文件服务路径,确保前端可以正确加载 +process.env.STATIC_PATH = path.resolve(qlDir, 'static'); +console.log('📁 Static files path: ' + process.env.STATIC_PATH); + +// 只使用编译后的JavaScript文件作为入口,不再依赖TypeScript和ts-node +let appPath; +const possiblePaths = [ + // 主要使用编译后的app.js + path.join(qlDir, 'static', 'build', 'app.js'), + // 备用路径 + path.join(process.cwd(), 'static', 'build', 'app.js') +]; + +console.log('🔍 Looking for app entry in:'); +for (const p of possiblePaths) { + console.log(' - ' + p); + if (fs.existsSync(p)) { + appPath = p; + console.log('✅ Found app entry file: ' + appPath); + break; + } +} + +if (!appPath) { + console.error('❌ Error: Cannot find compiled app.js file'); + console.error('Please make sure you have properly built the project with: npm run build:back'); + console.error('Expected path: static/build/app.js'); + process.exit(1); +} + +// 验证前端文件是否存在 - 检查多个可能的位置 +const frontendIndexPaths = [ + path.join(qlDir, 'static', 'dist', 'index.html'), + path.join(qlDir, 'static', 'index.html') +]; +let frontendFound = false; + +for (const indexPath of frontendIndexPaths) { + if (fs.existsSync(indexPath)) { + console.log('✅ Frontend files found at: ' + path.dirname(indexPath)); + // 如果在dist子目录找到,设置正确的静态文件路径 + if (indexPath.includes('dist')) { + process.env.STATIC_PATH = path.dirname(path.dirname(indexPath)); + console.log('📁 Updated static path to: ' + process.env.STATIC_PATH); + } + frontendFound = true; + break; + } +} + +if (!frontendFound) { + console.warn('⚠️ Frontend index.html not found in expected locations:'); + frontendIndexPaths.forEach(path => console.log(' - ' + path)); +} + +// 加载应用 +try { + console.log('🎯 Loading application from:', appPath); + require(appPath); +} catch (error) { + console.error('❌ Error loading app:', error.message); + console.error('Error stack:', error.stack); + process.exit(1); +} +`; + fs.writeFileSync(path.join(tempDir, 'entry.js'), entryContent); + + return tempDir; +} + +// 使用pkg打包 +function packageWithPkg(tempDir, platformConfig) { + const { nodeVersion, platform, arch } = platformConfig; + const outputName = `qinglong-${platform}-${arch}`; + const outputPath = path.join(CONFIG.outputDir, outputName); + + console.log(`打包 ${platform}-${arch} 版本...`); + + try { + runCommand( + `npx pkg@5.8.1 --targets node${nodeVersion}-${platform}-${arch} --output ${outputPath} ${tempDir}/entry.js`, + __dirname + ); + + // 创建发布包 + const releaseDir = path.join(CONFIG.outputDir, `${outputName}-release`); + ensureDir(releaseDir); + + // 复制二进制文件到发布目录 + if (platform === 'win') { + if (fs.existsSync(`${outputPath}.exe`)) { + fs.cpSync(`${outputPath}.exe`, path.join(releaseDir, `${outputName}.exe`)); + } + } else { + if (fs.existsSync(outputPath)) { + fs.cpSync(outputPath, path.join(releaseDir, outputName)); + } + } + + // 复制其他必要文件 + const copyFiles = [ + { from: path.join(tempDir, 'static'), to: path.join(releaseDir, 'static') }, + { from: path.join(tempDir, 'shell'), to: path.join(releaseDir, 'shell') }, + { from: path.join(tempDir, 'sample'), to: path.join(releaseDir, 'sample') }, + { from: path.join(tempDir, '.env.example'), to: path.join(releaseDir, '.env.example') }, + ]; + + copyFiles.forEach(({ from, to }) => { + if (fs.existsSync(from)) { + fs.cpSync(from, to, { recursive: true }); + } + }); + + // 创建启动脚本 + if (platform === 'win') { + const startScript = `@echo off +set QL_DIR=%~dp0 +${outputName}.exe +`; + fs.writeFileSync(path.join(releaseDir, 'start.bat'), startScript); + } else { + const startScript = `#!/bin/bash + +# 设置QL_DIR环境变量 +QL_DIR=$(cd "$(dirname "$0")" && pwd) +export QL_DIR + +echo "🚀 QL_DIR is set to: $QL_DIR" +echo "📁 Static files path: $QL_DIR/static" + +# 检查是否存在.env文件,如果不存在则从.env.example复制 +if [ ! -f ".env" ]; then + if [ -f ".env.example" ]; then + cp .env.example .env + echo "✅ Created .env file from .env.example" + else + echo "❌ .env.example not found, please create .env file manually" + fi +fi + +# 更新.env文件设置正确的端口 +echo "✅ .env file updated with correct ports" +cat > .env << 'EOF2' +# 数据库配置 +DB_TYPE=sqlite +DB_HOST= +DB_PORT= +DB_USERNAME= +DB_PASSWORD= +DB_NAME=go.db + +# 服务器配置 +PORT=5700 +BACK_PORT=5700 +GRPC_PORT=5500 +GOTTY_PORT=5600 + +# 环境配置 +NODE_ENV=production +QL_DIR=$QL_DIR +EOF2 + +# 设置执行权限 +chmod +x ./${outputName} + +# 添加信号处理,支持Ctrl+C正常退出 +cleanup() { + echo "\n🛑 正在停止服务..." + # 找到并终止所有相关进程 + if [ -n "$PID" ]; then + kill -INT $PID 2>/dev/null + wait $PID 2>/dev/null + fi + echo "✅ 服务已停止" + exit 0 +} + +# 捕获SIGINT信号(Ctrl+C) +trap cleanup SIGINT SIGTERM + +# 启动二进制文件并保存PID +echo "🚀 正在启动服务..." +./${outputName} & +PID=$! + +# 等待进程结束或信号 +echo "🎯 服务启动成功,PID: $PID" +echo "💡 按 Ctrl+C 停止服务" +wait $PID +`; + fs.writeFileSync(path.join(releaseDir, 'start.sh'), startScript); + try { + execSync(`chmod +x ${path.join(releaseDir, 'start.sh')}`); + } catch (e) { + console.log('警告: 设置执行权限失败,但不影响打包'); + } + } + + // 复制version.yaml文件到发布目录 + const versionYamlPath = path.join(__dirname, 'version.yaml'); + if (fs.existsSync(versionYamlPath)) { + try { + fs.cpSync(versionYamlPath, path.join(releaseDir, 'version.yaml')); + console.log(`✅ version.yaml已复制到发布目录`); + } catch (e) { + console.log('警告: 复制version.yaml失败,但不影响打包'); + } + } + + // 创建zip包 + console.log(`创建 ${outputName}.zip...`); + const zipCommand = platform === 'win' + ? `powershell Compress-Archive -Path "${releaseDir}\*" -DestinationPath "${CONFIG.outputDir}\${outputName}.zip" -Force` + : `cd "${CONFIG.outputDir}" && zip -r "${outputName}.zip" "${outputName}-release"`; + + try { + runCommand(zipCommand); + console.log(`${platform}-${arch} 版本打包完成: ${CONFIG.outputDir}/${outputName}.zip`); + } catch (e) { + console.error(`创建zip包失败,可能需要安装zip工具:`, e.message); + } + } catch (error) { + console.error(`打包 ${platform}-${arch} 版本失败:`, error.message); + } +} + +// 生成GitHub Release脚本 +function generateGithubReleaseScript() { + const releaseScriptPath = path.join(__dirname, 'github-release.js'); + const releaseScript = `#!/usr/bin/env node +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +// 读取版本信息 +const version = require('./package.json').version; +const releaseTag = \`v\${version}\`; +const releaseTitle = \`Release v\${version}\`; +const releaseBody = \`## v\${version}\n\n自动生成的发布版本。\`; +const assetsDir = path.join(__dirname, 'dist'); + +// 获取所有zip文件 +const assets = fs.readdirSync(assetsDir) + .filter(file => file.endsWith('.zip')) + .map(file => path.join(assetsDir, file)); + +console.log(\`创建GitHub Release: \${releaseTag}\`); + +// 构建命令 +let command = \`gh release create \${releaseTag} --title "\${releaseTitle}" --body "\${releaseBody}"\`; + +// 添加资产文件 +assets.forEach(asset => { + command += \` "\${asset}"\`; +}); + +// 执行命令 +console.log('执行GitHub Release创建命令...'); +try { + execSync(command, { stdio: 'inherit' }); + console.log('GitHub Release创建完成!'); +} catch (error) { + console.error('创建GitHub Release失败,请确保已安装GitHub CLI并登录。'); + console.error('错误信息:', error.message); +} +`; + + fs.writeFileSync(releaseScriptPath, releaseScript); + try { + fs.chmodSync(releaseScriptPath, '755'); + } catch (e) { + console.log('警告: 设置GitHub Release脚本执行权限失败'); + } + console.log(`GitHub Release脚本已生成: ${releaseScriptPath}`); +} + +// 主函数 +async function main() { + try { + // 清理构建 + cleanBuild(); + + // 安装依赖 + installDependencies(); + + // 构建前端和后端 + buildFrontend(); + buildBackend(); + + // 准备打包文件 + const tempDir = preparePackageFiles(); + + // 获取目标平台 + const targetPlatforms = getTargetPlatforms(); + + // 为每个平台打包 + for (const platform of targetPlatforms) { + packageWithPkg(tempDir, platform); + } + + // 清理临时目录 + if (fs.existsSync(tempDir)) { + try { + fs.rmSync(tempDir, { recursive: true, force: true }); + } catch (e) { + console.log('警告: 清理临时目录失败'); + } + } + + // 生成GitHub Release脚本 + generateGithubReleaseScript(); + + console.log('\n🎉 所有平台的二进制包打包完成!'); + console.log('📁 输出目录:', CONFIG.outputDir); + console.log('\n要创建GitHub Release,请运行:'); + console.log(' node github-release.js'); + } catch (error) { + console.error('\n❌ 构建过程中出现错误:', error.message); + process.exit(1); + } +} + +// 执行主函数 +main(); \ No newline at end of file diff --git a/github-release.js b/github-release.js new file mode 100755 index 00000000..1a79e17f --- /dev/null +++ b/github-release.js @@ -0,0 +1,38 @@ +#!/usr/bin/env node +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +// 读取版本信息 +const version = require('./package.json').version; +const releaseTag = `v${version}`; +const releaseTitle = `Release v${version}`; +const releaseBody = `## v${version} + +自动生成的发布版本。`; +const assetsDir = path.join(__dirname, 'dist'); + +// 获取所有zip文件 +const assets = fs.readdirSync(assetsDir) + .filter(file => file.endsWith('.zip')) + .map(file => path.join(assetsDir, file)); + +console.log(`创建GitHub Release: ${releaseTag}`); + +// 构建命令 +let command = `gh release create ${releaseTag} --title "${releaseTitle}" --body "${releaseBody}"`; + +// 添加资产文件 +assets.forEach(asset => { + command += ` "${asset}"`; +}); + +// 执行命令 +console.log('执行GitHub Release创建命令...'); +try { + execSync(command, { stdio: 'inherit' }); + console.log('GitHub Release创建完成!'); +} catch (error) { + console.error('创建GitHub Release失败,请确保已安装GitHub CLI并登录。'); + console.error('错误信息:', error.message); +} diff --git a/package.json b/package.json index 9bec3428..3f2365a6 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", "postinstall": "max setup 2>/dev/null || true", "test": "umi-test", - "test:coverage": "umi-test --coverage" + "test:coverage": "umi-test --coverage", + "build-release": "node build-release.js" }, "gitHooks": { "pre-commit": "lint-staged" @@ -97,7 +98,7 @@ "@keyv/sqlite": "^4.0.1", "proper-lockfile": "^4.1.2", "compression": "^1.7.4", - "helmet": "^6.0.1" + "helmet": "^8.1.0" }, "devDependencies": { "moment": "2.30.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5177ec06..50cce37f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,8 +63,8 @@ dependencies: specifier: ^2.0.3 version: 2.0.3 helmet: - specifier: ^6.0.1 - version: 6.2.0 + specifier: ^8.1.0 + version: 8.1.0 hpagent: specifier: ^1.2.0 version: 1.2.0 @@ -3873,7 +3873,7 @@ packages: resolution: {integrity: sha512-ONIn/nSNQA57yRge3oaMQESef/6QhoeX7llWeDli0UZIfz8TQMkfNPTXA8VnnyeA1WUjG2pGqdjEIueYonMdfQ==} deprecated: This is a stub types definition. helmet provides its own type definitions, so you do not need this installed. dependencies: - helmet: 6.2.0 + helmet: 8.1.0 dev: true /@types/hoist-non-react-statics@3.3.5: @@ -8467,9 +8467,9 @@ packages: hasBin: true dev: true - /helmet@6.2.0: - resolution: {integrity: sha512-DWlwuXLLqbrIOltR6tFQXShj/+7Cyp0gLi6uAb8qMdFh/YBBFbKSgQ6nbXmScYd8emMctuthmgIa7tUfo9Rtyg==} - engines: {node: '>=14.0.0'} + /helmet@8.1.0: + resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==} + engines: {node: '>=18.0.0'} /history@5.3.0: resolution: {integrity: sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==}