"添加前端模板和运行代码模块"
This commit is contained in:
210
qiming-xagi-frontend-templates/scripts/README.md
Normal file
210
qiming-xagi-frontend-templates/scripts/README.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# 模板上传脚本使用说明
|
||||
|
||||
## 功能说明
|
||||
|
||||
提供四个脚本用于模板部署与重启:
|
||||
|
||||
1. **`deploy-templates.sh`** - 完整部署流程(推荐)
|
||||
- 打包所有模板
|
||||
- 上传到远程服务器
|
||||
- 重启 rcoder 容器内 `nuwax-file-server` 进程(不重启整个容器)
|
||||
|
||||
2. **`upload-templates.sh`** - 仅上传已有的 zip 文件
|
||||
- 适用于已经打包好的情况
|
||||
- 仅上传文件,不执行任何重启
|
||||
|
||||
3. **`restart-file-server.sh`** - 仅重启 `nuwax-file-server` 进程
|
||||
- 在 `rcoder` 容器内先停止旧进程,再拉起新进程
|
||||
- 内置健康检查
|
||||
|
||||
4. **`restart-rcoder.sh`** - 仅重启 `rcoder` 容器
|
||||
- 与 file-server 进程重启完全解耦
|
||||
|
||||
## 前置要求
|
||||
|
||||
### 安装 sshpass
|
||||
|
||||
macOS 上需要安装 sshpass 工具来实现自动化 SSH 登录:
|
||||
|
||||
```bash
|
||||
brew install hudochenkov/sshpass/sshpass
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 1. 创建 .env 文件
|
||||
|
||||
在项目根目录创建 `.env` 文件(已在 .gitignore 中,不会提交到 git):
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
### 2. 配置环境变量
|
||||
|
||||
编辑 `.env` 文件,设置远程服务器信息:
|
||||
|
||||
```bash
|
||||
REMOTE_HOST=192.168.1.34
|
||||
REMOTE_USER=swufe
|
||||
REMOTE_PASSWORD=Swufe@2024
|
||||
REMOTE_PATH=/home/swufe/nuwax/docker/config/rcoder/template
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 方式一: 完整部署(推荐)
|
||||
|
||||
一键完成打包、上传并重启容器内 file-server:
|
||||
|
||||
```bash
|
||||
# 使用 npm script(推荐)
|
||||
pnpm deploy:templates
|
||||
|
||||
# 或直接运行脚本
|
||||
./scripts/deploy-templates.sh
|
||||
```
|
||||
|
||||
这个脚本会:
|
||||
1. 使用 `pnpm pack:all` 打包所有模板
|
||||
2. 上传 react-vite 和 vue3-vite 到远程服务器
|
||||
3. 重启 rcoder 容器内 `nuwax-file-server` 进程
|
||||
|
||||
### 方式二: 仅上传
|
||||
|
||||
如果已经打包好了,只需要上传:
|
||||
|
||||
```bash
|
||||
# 使用 npm script(推荐)
|
||||
pnpm upload:templates
|
||||
|
||||
# 或直接运行脚本
|
||||
./scripts/upload-templates.sh
|
||||
```
|
||||
|
||||
### 方式三: 仅重启 file-server
|
||||
|
||||
```bash
|
||||
# 使用 npm script(推荐)
|
||||
pnpm restart:file-server
|
||||
|
||||
# 或直接运行脚本
|
||||
./scripts/restart-file-server.sh
|
||||
```
|
||||
|
||||
### 方式四: 仅重启 rcoder 容器
|
||||
|
||||
```bash
|
||||
# 使用 npm script(推荐)
|
||||
pnpm restart:rcoder
|
||||
|
||||
# 或直接运行脚本
|
||||
./scripts/restart-rcoder.sh
|
||||
```
|
||||
|
||||
### 脚本执行流程
|
||||
|
||||
1. 加载 `.env` 配置
|
||||
2. 检查必需的环境变量
|
||||
3. 查找 `zip/` 目录下最新的模板文件:
|
||||
- `react-vite-template_*.zip`
|
||||
- `vue3-vite-template_*.zip`
|
||||
4. 重命名为标准名称:
|
||||
- `react-vite-template.zip`
|
||||
- `vue3-vite-template.zip`
|
||||
5. 上传到远程服务器
|
||||
|
||||
### 上传的文件
|
||||
|
||||
脚本会自动上传以下两个模板:
|
||||
|
||||
- `react-vite-template.zip` - React + Vite 模板
|
||||
- `vue3-vite-template.zip` - Vue3 + Vite 模板
|
||||
|
||||
注意:`react-next-template` 不会被上传(根据需求只上传 vite 模板)
|
||||
|
||||
## 远程服务器路径
|
||||
|
||||
上传目标路径:`/home/swufe/nuwax/docker/config/rcoder/template`
|
||||
|
||||
上传后的文件结构:
|
||||
|
||||
```
|
||||
/home/swufe/nuwax/docker/config/rcoder/template/
|
||||
├── react-vite-template.zip
|
||||
├── react-vite-template/ # 自动解压后的目录
|
||||
├── vue3-vite-template.zip
|
||||
└── vue3-vite-template/ # 自动解压后的目录
|
||||
```
|
||||
|
||||
## 自动化流程
|
||||
|
||||
脚本会自动完成以下操作:
|
||||
|
||||
1. **上传模板文件** - 上传最新的 react-vite 和 vue3-vite 模板
|
||||
2. **重启 file-server 进程** - 在 rcoder 容器内重启 `nuwax-file-server`
|
||||
3. **容器重启(可选)** - 如有需要,手动执行 `restart-rcoder.sh`
|
||||
|
||||
无需手动干预,一键完成整个部署流程。
|
||||
|
||||
## 安全说明
|
||||
|
||||
- `.env` 文件包含敏感信息(密码),已添加到 `.gitignore`
|
||||
- 不要将 `.env` 文件提交到 git 仓库
|
||||
- 可以使用 `.env.example` 作为配置模板分享
|
||||
|
||||
## 故障排除
|
||||
|
||||
### sshpass 未安装
|
||||
|
||||
```
|
||||
错误: sshpass 未安装
|
||||
请运行: brew install hudochenkov/sshpass/sshpass
|
||||
```
|
||||
|
||||
解决方法:安装 sshpass
|
||||
|
||||
### .env 文件不存在
|
||||
|
||||
```
|
||||
错误: .env 文件不存在,请先创建 .env 文件
|
||||
```
|
||||
|
||||
解决方法:复制 `.env.example` 并修改配置
|
||||
|
||||
### 未找到模板文件
|
||||
|
||||
```
|
||||
错误: 未找到 react-vite-template zip 文件
|
||||
```
|
||||
|
||||
解决方法:确保 `zip/` 目录下存在对应的模板 zip 文件
|
||||
|
||||
### SSH 连接失败
|
||||
|
||||
检查:
|
||||
1. 远程服务器 IP 是否正确
|
||||
2. 用户名和密码是否正确
|
||||
3. 网络连接是否正常
|
||||
4. 远程服务器是否开启 SSH 服务
|
||||
|
||||
## 手动上传方式
|
||||
|
||||
如果自动脚本无法使用,可以手动上传:
|
||||
|
||||
```bash
|
||||
# 1. 进入 zip 目录
|
||||
cd zip
|
||||
|
||||
# 2. 使用 scp 上传
|
||||
scp react-vite-template_*.zip swufe@192.168.1.34:/home/swufe/nuwax/docker/config/rcoder/template/react-vite-template.zip
|
||||
scp vue3-vite-template_*.zip swufe@192.168.1.34:/home/swufe/nuwax/docker/config/rcoder/template/vue3-vite-template.zip
|
||||
|
||||
# 3. SSH 登录到远程服务器
|
||||
ssh swufe@192.168.1.34
|
||||
|
||||
# 4. 解压文件
|
||||
cd /home/swufe/nuwax/docker/config/rcoder/template
|
||||
unzip -o react-vite-template.zip -d react-vite-template
|
||||
unzip -o vue3-vite-template.zip -d vue3-vite-template
|
||||
```
|
||||
33
qiming-xagi-frontend-templates/scripts/build-all.js
Normal file
33
qiming-xagi-frontend-templates/scripts/build-all.js
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const packages = [
|
||||
'packages/react-vite',
|
||||
'packages/vue3-vite',
|
||||
'packages/react-next'
|
||||
];
|
||||
|
||||
console.log('🚀 开始构建所有模板...\n');
|
||||
|
||||
packages.forEach((pkg, index) => {
|
||||
console.log(`[${index + 1}/${packages.length}] 构建 ${pkg}...`);
|
||||
|
||||
try {
|
||||
// 清理之前的构建
|
||||
if (fs.existsSync(path.join(pkg, 'dist')) || fs.existsSync(path.join(pkg, '.next'))) {
|
||||
execSync(`cd ${pkg} && npm run clean`, { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
// 执行构建
|
||||
execSync(`cd ${pkg} && npm run build`, { stdio: 'inherit' });
|
||||
console.log(`✅ ${pkg} 构建成功\n`);
|
||||
} catch (error) {
|
||||
console.error(`❌ ${pkg} 构建失败:`, error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('🎉 所有模板构建完成!');
|
||||
82
qiming-xagi-frontend-templates/scripts/check-versions.js
Normal file
82
qiming-xagi-frontend-templates/scripts/check-versions.js
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const rootDir = path.resolve(__dirname, '..')
|
||||
|
||||
function readJson(relativePath) {
|
||||
const filePath = path.join(rootDir, relativePath)
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
||||
}
|
||||
|
||||
const templateSpecs = [
|
||||
{
|
||||
id: 'vue3-vite',
|
||||
packagePath: 'packages/vue3-vite/package.json',
|
||||
metaPath: 'packages/vue3-vite/meta.json',
|
||||
},
|
||||
{
|
||||
id: 'react-vite',
|
||||
packagePath: 'packages/react-vite/package.json',
|
||||
metaPath: 'packages/react-vite/meta.json',
|
||||
},
|
||||
{
|
||||
id: 'react-next',
|
||||
packagePath: 'packages/react-next/package.json',
|
||||
metaPath: 'packages/react-next/meta.json',
|
||||
},
|
||||
]
|
||||
|
||||
const rootPkg = readJson('package.json')
|
||||
const templatesRegistry = readJson('templates.json')
|
||||
let hasError = false
|
||||
|
||||
// Check root package.json version vs templates.json root version
|
||||
if (rootPkg.version !== templatesRegistry.version) {
|
||||
console.error(`\n[ERROR] Root version mismatch`)
|
||||
console.error(` package.json: ${rootPkg.version}`)
|
||||
console.error(` templates.json: ${templatesRegistry.version}`)
|
||||
hasError = true
|
||||
} else {
|
||||
console.log(`[OK] Root version aligned: ${rootPkg.version}`)
|
||||
}
|
||||
|
||||
// Check each template
|
||||
for (const spec of templateSpecs) {
|
||||
const pkg = readJson(spec.packagePath)
|
||||
const meta = readJson(spec.metaPath)
|
||||
const registryItem = templatesRegistry.templates.find(template => template.id === spec.id)
|
||||
|
||||
if (!registryItem) {
|
||||
console.error(`[ERROR] templates.json missing entry for id: ${spec.id}`)
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
|
||||
const expectedVersion = rootPkg.version
|
||||
const actualVersions = {
|
||||
'package.json': pkg.version,
|
||||
'meta.json': meta.version,
|
||||
'templates.json': registryItem.version,
|
||||
}
|
||||
|
||||
const mismatches = Object.entries(actualVersions).filter(([, value]) => value !== expectedVersion)
|
||||
|
||||
if (mismatches.length > 0) {
|
||||
hasError = true
|
||||
console.error(`\n[ERROR] Version mismatch for ${spec.id}`)
|
||||
console.error(` expected (root): ${expectedVersion}`)
|
||||
for (const [source, value] of mismatches) {
|
||||
console.error(` actual ${source}: ${value}`)
|
||||
}
|
||||
} else {
|
||||
console.log(`[OK] ${spec.id} version aligned: ${expectedVersion} (package/meta/templates)`)
|
||||
}
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('\n✅ Version consistency check passed.')
|
||||
47
qiming-xagi-frontend-templates/scripts/deploy-templates.sh
Normal file
47
qiming-xagi-frontend-templates/scripts/deploy-templates.sh
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 完整部署脚本
|
||||
# 用途: 打包模板 -> 上传到远程服务器 -> 重启 rcoder 容器内 nuwax-file-server 进程
|
||||
|
||||
set -e
|
||||
|
||||
# 脚本所在目录
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
echo "=========================================="
|
||||
echo " 前端模板完整部署流程"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# 步骤 1: 打包模板
|
||||
echo "步骤 1/3: 打包模板"
|
||||
echo "------------------------------------------"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
echo "打包所有模板 (react-vite, vue3-vite, react-next)..."
|
||||
pnpm pack:all
|
||||
|
||||
echo "✓ 模板打包完成"
|
||||
echo ""
|
||||
|
||||
# 显示打包结果
|
||||
echo "打包文件列表:"
|
||||
ls -lh "$PROJECT_ROOT/zip/"
|
||||
echo ""
|
||||
|
||||
# 步骤 2: 上传模板文件
|
||||
echo "步骤 2/3: 上传模板文件"
|
||||
echo "------------------------------------------"
|
||||
"$SCRIPT_DIR/upload-templates.sh"
|
||||
|
||||
# 步骤 3: 重启容器内 file-server 进程(与 rcoder 容器重启分离)
|
||||
echo ""
|
||||
echo "步骤 3/3: 重启 rcoder 容器内 nuwax-file-server"
|
||||
echo "------------------------------------------"
|
||||
"$SCRIPT_DIR/restart-file-server.sh"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " 部署完成!"
|
||||
echo "=========================================="
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const crypto = require('crypto')
|
||||
|
||||
const rootDir = path.resolve(__dirname, '..')
|
||||
|
||||
const REPO = 'nuwax-ai/xagi-frontend-templates'
|
||||
const BASE_URL = `https://github.com/${REPO}/releases/download`
|
||||
|
||||
const templateSpecs = [
|
||||
{ id: 'react-vite', outputName: 'react-vite-template', dir: 'packages/react-vite' },
|
||||
{ id: 'vue3-vite', outputName: 'vue3-vite-template', dir: 'packages/vue3-vite' },
|
||||
{ id: 'react-next', outputName: 'react-next-template', dir: 'packages/react-next' },
|
||||
]
|
||||
|
||||
function readJson(relativePath) {
|
||||
return JSON.parse(fs.readFileSync(path.join(rootDir, relativePath), 'utf8'))
|
||||
}
|
||||
|
||||
function computeSha256(filePath) {
|
||||
if (!fs.existsSync(filePath)) return null
|
||||
const data = fs.readFileSync(filePath)
|
||||
return crypto.createHash('sha256').update(data).digest('hex')
|
||||
}
|
||||
|
||||
const version = process.argv[2]
|
||||
|
||||
if (!version) {
|
||||
const rootPkg = readJson('package.json')
|
||||
console.error(`Usage: node scripts/generate-latest-json.js <version>`)
|
||||
console.error(` Current root version: ${rootPkg.version}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const tag = `v${version}`
|
||||
const zipDir = path.join(rootDir, 'zip')
|
||||
|
||||
const templates = {}
|
||||
|
||||
for (const spec of templateSpecs) {
|
||||
const pkg = readJson(`${spec.dir}/package.json`)
|
||||
const fileName = `${spec.outputName}_${pkg.version}.zip`
|
||||
const zipPath = path.join(zipDir, fileName)
|
||||
const sha256 = computeSha256(zipPath)
|
||||
|
||||
templates[spec.id] = {
|
||||
version: pkg.version,
|
||||
url: `${BASE_URL}/${tag}/${fileName}`,
|
||||
...(sha256 && { sha256 }),
|
||||
}
|
||||
}
|
||||
|
||||
const latestJson = {
|
||||
version,
|
||||
tag,
|
||||
updatedAt: new Date().toISOString(),
|
||||
repository: `https://github.com/${REPO}`,
|
||||
downloadUrlPattern: `${BASE_URL}/{tag}/{template}_{version}.zip`,
|
||||
templates,
|
||||
}
|
||||
|
||||
const outputPath = path.join(rootDir, 'latest.json')
|
||||
fs.writeFileSync(outputPath, JSON.stringify(latestJson, null, 2) + '\n')
|
||||
|
||||
console.log(`✅ latest.json generated at ${outputPath}`)
|
||||
console.log(` version: ${version}`)
|
||||
console.log(` tag: ${tag}`)
|
||||
for (const [id, info] of Object.entries(templates)) {
|
||||
console.log(` ${id}: ${info.url}`)
|
||||
if (info.sha256) {
|
||||
console.log(` sha256: ${info.sha256}`)
|
||||
} else {
|
||||
console.log(` ⚠️ zip file not found, sha256 skipped`)
|
||||
}
|
||||
}
|
||||
38
qiming-xagi-frontend-templates/scripts/list-templates.js
Normal file
38
qiming-xagi-frontend-templates/scripts/list-templates.js
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 读取 templates.json
|
||||
const templatesPath = path.join(__dirname, '../templates.json');
|
||||
const templates = JSON.parse(fs.readFileSync(templatesPath, 'utf8'));
|
||||
|
||||
console.log('📦 XAGI Frontend Templates\n');
|
||||
console.log(`版本: ${templates.version}`);
|
||||
console.log(`更新时间: ${templates.updatedAt}`);
|
||||
console.log(`模板数量: ${templates.templates.length}\n`);
|
||||
|
||||
// 显示所有模板
|
||||
templates.templates.forEach((template, index) => {
|
||||
console.log(`${index + 1}. ${template.name}`);
|
||||
console.log(` 包名: ${template.packageName}`);
|
||||
console.log(` 版本: ${template.version}`);
|
||||
console.log(` 框架: ${template.framework} + ${template.buildTool}`);
|
||||
console.log(` 端口: ${template.port}`);
|
||||
console.log(` 路径: ${template.path}`);
|
||||
console.log(` 描述: ${template.description}`);
|
||||
console.log();
|
||||
});
|
||||
|
||||
console.log('🔧 公共特性:');
|
||||
templates.commonFeatures.forEach(feature => {
|
||||
console.log(` • ${feature}`);
|
||||
});
|
||||
|
||||
console.log('\n📋 使用方法:');
|
||||
console.log(' Monorepo 模式: pnpm install && pnpm dev');
|
||||
console.log(' 单独使用: cd packages/<template-name> && pnpm install && pnpm dev');
|
||||
|
||||
console.log('\n📚 更多信息:');
|
||||
console.log(` 文档: ${templates.support.documentation}`);
|
||||
console.log(` 问题反馈: ${templates.support.issues}`);
|
||||
211
qiming-xagi-frontend-templates/scripts/pack-template.js
Normal file
211
qiming-xagi-frontend-templates/scripts/pack-template.js
Normal file
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
/**
|
||||
* 模板打包脚本
|
||||
* 用于将指定的模板项目打包成 zip 文件,排除 .gitignore 中指定的文件
|
||||
*/
|
||||
|
||||
// 获取命令行参数
|
||||
const templateName = process.argv[2]
|
||||
const isRelease = process.argv.includes('--release')
|
||||
|
||||
if (!templateName) {
|
||||
console.error('❌ 请指定模板名称')
|
||||
console.log('用法: node scripts/pack-template.js <template-name> [--release]')
|
||||
console.log('可用模板: react-vite, react-next, vue3-vite')
|
||||
console.log(' --release 生成稳定文件名(无时间戳),用于 GitHub Release')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// 模板配置
|
||||
const templates = {
|
||||
'react-vite': {
|
||||
name: 'React + Vite + TypeScript',
|
||||
dir: 'packages/react-vite',
|
||||
outputName: 'react-vite-template',
|
||||
},
|
||||
'react-next': {
|
||||
name: 'React + Next.js + TypeScript',
|
||||
dir: 'packages/react-next',
|
||||
outputName: 'react-next-template',
|
||||
},
|
||||
'vue3-vite': {
|
||||
name: 'Vue 3 + Vite + TypeScript',
|
||||
dir: 'packages/vue3-vite',
|
||||
outputName: 'vue3-vite-template',
|
||||
},
|
||||
}
|
||||
|
||||
const template = templates[templateName]
|
||||
|
||||
if (!template) {
|
||||
console.error(`❌ 未找到模板: ${templateName}`)
|
||||
console.log('可用模板:', Object.keys(templates).join(', '))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const templateDir = path.resolve(__dirname, '..', template.dir)
|
||||
const outputDir = path.resolve(__dirname, '..', 'zip')
|
||||
|
||||
// 检查模板目录是否存在
|
||||
if (!fs.existsSync(templateDir)) {
|
||||
console.error(`❌ 模板目录不存在: ${templateDir}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// 创建输出目录
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true })
|
||||
}
|
||||
|
||||
// 读取 package.json 获取版本号
|
||||
let version = null
|
||||
const packageJsonPath = path.join(templateDir, 'package.json')
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
||||
if (packageJson.version) {
|
||||
version = packageJson.version
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ 读取 package.json 失败: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成时间戳:YYYY_MM_DD_HH_MM_SS
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(now.getDate()).padStart(2, '0')
|
||||
const hours = String(now.getHours()).padStart(2, '0')
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0')
|
||||
const timestamp = `${year}_${month}_${day}_${hours}_${minutes}_${seconds}`
|
||||
|
||||
let fileName
|
||||
if (isRelease) {
|
||||
if (!version) {
|
||||
console.error('❌ Release 模式需要 package.json 中有 version 字段')
|
||||
process.exit(1)
|
||||
}
|
||||
fileName = `${template.outputName}_${version}.zip`
|
||||
} else {
|
||||
fileName = version
|
||||
? `${template.outputName}_${version}_${timestamp}.zip`
|
||||
: `${template.outputName}_${timestamp}.zip`
|
||||
}
|
||||
const outputFile = path.join(outputDir, fileName)
|
||||
|
||||
// 清除之前的包(匹配该模板的所有旧文件)
|
||||
console.log(`🧹 清除之前的包...`)
|
||||
try {
|
||||
const files = fs.readdirSync(outputDir)
|
||||
// 匹配该模板的所有旧文件(可能包含版本号,也可能不包含)
|
||||
const oldFiles = files.filter(file => {
|
||||
const baseName = file.replace(/\.zip$/, '')
|
||||
// 匹配格式:templateName_* 或 templateName_version_*
|
||||
return (
|
||||
file.endsWith('.zip') &&
|
||||
(baseName === template.outputName ||
|
||||
baseName.startsWith(`${template.outputName}_`))
|
||||
)
|
||||
})
|
||||
|
||||
if (oldFiles.length > 0) {
|
||||
oldFiles.forEach(file => {
|
||||
const filePath = path.join(outputDir, file)
|
||||
fs.unlinkSync(filePath)
|
||||
console.log(` ✓ 已删除: ${file}`)
|
||||
})
|
||||
console.log(`✅ 已清除 ${oldFiles.length} 个旧文件\n`)
|
||||
} else {
|
||||
console.log(` ℹ️ 没有找到旧文件\n`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ 清除旧文件时出错: ${error.message}\n`)
|
||||
}
|
||||
|
||||
console.log(`🚀 开始打包模板: ${template.name}`)
|
||||
if (version) {
|
||||
console.log(`📌 版本号: ${version}`)
|
||||
}
|
||||
console.log(`📁 源目录: ${templateDir}`)
|
||||
console.log(`📦 输出文件: ${outputFile}`)
|
||||
|
||||
try {
|
||||
// 读取 .gitignore 文件
|
||||
const gitignorePath = path.join(templateDir, '.gitignore')
|
||||
let ignorePatterns = []
|
||||
|
||||
// 强制排除的目录和文件
|
||||
const forceExclude = [
|
||||
'node_modules',
|
||||
'dist',
|
||||
'dist-ssr',
|
||||
'build',
|
||||
'.next',
|
||||
'.nuxt',
|
||||
'.cache',
|
||||
'.parcel-cache',
|
||||
'coverage',
|
||||
'.nyc_output',
|
||||
'*.log',
|
||||
'.DS_Store',
|
||||
'Thumbs.db',
|
||||
'playground',
|
||||
]
|
||||
|
||||
if (fs.existsSync(gitignorePath)) {
|
||||
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8')
|
||||
ignorePatterns = gitignoreContent
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#'))
|
||||
.map(pattern => {
|
||||
// 处理 .gitignore 模式
|
||||
if (pattern.endsWith('/')) {
|
||||
return pattern.slice(0, -1) // 移除末尾的 /
|
||||
}
|
||||
return pattern
|
||||
})
|
||||
}
|
||||
|
||||
// 合并强制排除和 .gitignore 中的模式
|
||||
ignorePatterns = [...new Set([...forceExclude, ...ignorePatterns])]
|
||||
|
||||
// 构建 zip 命令 - 使用更可靠的排除方式
|
||||
let zipCommand = `cd "${templateDir}" && zip -r "${outputFile}" .`
|
||||
|
||||
// 添加排除模式 - 使用多个 -x 参数,确保正确排除
|
||||
if (ignorePatterns.length > 0) {
|
||||
ignorePatterns.forEach(pattern => {
|
||||
zipCommand += ` -x "${pattern}/*" -x "${pattern}"`
|
||||
})
|
||||
}
|
||||
|
||||
console.log('📋 排除模式:', ignorePatterns.length > 0 ? ignorePatterns.join(', ') : '无')
|
||||
|
||||
// 执行打包命令
|
||||
console.log('⏳ 正在打包...')
|
||||
execSync(zipCommand, { stdio: 'inherit' })
|
||||
|
||||
// 检查输出文件
|
||||
if (fs.existsSync(outputFile)) {
|
||||
const stats = fs.statSync(outputFile)
|
||||
const fileSizeInMB = (stats.size / (1024 * 1024)).toFixed(2)
|
||||
|
||||
console.log('✅ 打包完成!')
|
||||
console.log(`📦 输出文件: ${outputFile}`)
|
||||
console.log(`📊 文件大小: ${fileSizeInMB} MB`)
|
||||
} else {
|
||||
console.error('❌ 打包失败: 输出文件未生成')
|
||||
process.exit(1)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 打包过程中发生错误:', error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
191
qiming-xagi-frontend-templates/scripts/release.js
Normal file
191
qiming-xagi-frontend-templates/scripts/release.js
Normal file
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
const rootDir = path.resolve(__dirname, '..')
|
||||
|
||||
function readJson(relPath) {
|
||||
return JSON.parse(fs.readFileSync(path.join(rootDir, relPath), 'utf8'))
|
||||
}
|
||||
|
||||
function writeJson(relPath, data) {
|
||||
fs.writeFileSync(path.join(rootDir, relPath), JSON.stringify(data, null, 2) + '\n')
|
||||
}
|
||||
|
||||
function run(cmd, options = {}) {
|
||||
execSync(cmd, { stdio: 'inherit', cwd: rootDir, ...options })
|
||||
}
|
||||
|
||||
function runQuiet(cmd) {
|
||||
return execSync(cmd, { encoding: 'utf8', cwd: rootDir }).trim()
|
||||
}
|
||||
|
||||
const SEMVER_RE = /^\d+\.\d+\.\d+$/
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
let newVersion = null
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg === '--patch' || arg === '--minor' || arg === '--major') {
|
||||
const rootPkg = readJson('package.json')
|
||||
const parts = rootPkg.version.split('.').map(Number)
|
||||
if (arg === '--patch') { parts[2]++; }
|
||||
else if (arg === '--minor') { parts[1]++; parts[2] = 0; }
|
||||
else if (arg === '--major') { parts[0]++; parts[1] = 0; parts[2] = 0; }
|
||||
newVersion = parts.join('.')
|
||||
} else if (SEMVER_RE.test(arg)) {
|
||||
newVersion = arg
|
||||
}
|
||||
}
|
||||
|
||||
if (!newVersion) {
|
||||
console.error('❌ 请指定版本号')
|
||||
console.log('')
|
||||
console.log('用法:')
|
||||
console.log(' pnpm release <version> 指定版本号,如 pnpm release 1.3.0')
|
||||
console.log(' pnpm release --patch 递增补丁版本 1.2.1 → 1.2.2')
|
||||
console.log(' pnpm release --minor 递增次版本 1.2.1 → 1.3.0')
|
||||
console.log(' pnpm release --major 递增主版本 1.2.1 → 2.0.0')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const tag = `v${newVersion}`
|
||||
|
||||
console.log('==========================================')
|
||||
console.log(` XAGI Frontend Templates Release`)
|
||||
console.log(` Target version: ${newVersion}`)
|
||||
console.log(` Tag: ${tag}`)
|
||||
console.log('==========================================')
|
||||
console.log('')
|
||||
|
||||
// 1. Check working tree clean
|
||||
try {
|
||||
const status = runQuiet('git status --porcelain')
|
||||
if (status) {
|
||||
console.error('❌ 工作区有未提交的变更,请先 commit 或 stash')
|
||||
console.log(status)
|
||||
process.exit(1)
|
||||
}
|
||||
} catch {
|
||||
console.error('❌ 无法检查 git 状态')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// 2. Check tag doesn't already exist
|
||||
const existingTags = runQuiet('git tag -l').split('\n').filter(Boolean)
|
||||
if (existingTags.includes(tag)) {
|
||||
console.error(`❌ Tag ${tag} 已存在`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// 3. Update all version numbers
|
||||
console.log('📝 更新版本号...')
|
||||
const templateSpecs = [
|
||||
{ id: 'react-vite', packagePath: 'packages/react-vite/package.json', metaPath: 'packages/react-vite/meta.json' },
|
||||
{ id: 'vue3-vite', packagePath: 'packages/vue3-vite/package.json', metaPath: 'packages/vue3-vite/meta.json' },
|
||||
{ id: 'react-next', packagePath: 'packages/react-next/package.json', metaPath: 'packages/react-next/meta.json' },
|
||||
]
|
||||
|
||||
// Update root package.json
|
||||
const rootPkg = readJson('package.json')
|
||||
rootPkg.version = newVersion
|
||||
writeJson('package.json', rootPkg)
|
||||
console.log(` ✓ root package.json → ${newVersion}`)
|
||||
|
||||
// Update templates.json
|
||||
const templatesRegistry = readJson('templates.json')
|
||||
templatesRegistry.version = newVersion
|
||||
templatesRegistry.updatedAt = new Date().toISOString()
|
||||
for (const spec of templateSpecs) {
|
||||
const entry = templatesRegistry.templates.find(t => t.id === spec.id)
|
||||
if (entry) {
|
||||
entry.version = newVersion
|
||||
console.log(` ✓ templates.json[${spec.id}] → ${newVersion}`)
|
||||
}
|
||||
}
|
||||
writeJson('templates.json', templatesRegistry)
|
||||
|
||||
// Update each template package.json and meta.json
|
||||
for (const spec of templateSpecs) {
|
||||
const pkg = readJson(spec.packagePath)
|
||||
pkg.version = newVersion
|
||||
writeJson(spec.packagePath, pkg)
|
||||
console.log(` ✓ ${spec.packagePath} → ${newVersion}`)
|
||||
|
||||
const meta = readJson(spec.metaPath)
|
||||
meta.version = newVersion
|
||||
writeJson(spec.metaPath, meta)
|
||||
console.log(` ✓ ${spec.metaPath} → ${newVersion}`)
|
||||
}
|
||||
|
||||
console.log('')
|
||||
|
||||
// 4. Run version consistency check
|
||||
console.log('🔍 校验版本一致性...')
|
||||
try {
|
||||
run('node scripts/check-versions.js')
|
||||
console.log('✅ 版本一致性校验通过')
|
||||
} catch {
|
||||
console.error('❌ 版本一致性校验失败')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('')
|
||||
|
||||
// 5. Git commit
|
||||
console.log('📦 创建 release commit...')
|
||||
try {
|
||||
run('git add package.json templates.json latest.json packages/*/package.json packages/*/meta.json')
|
||||
run(`git commit -m "release: ${tag}"`)
|
||||
console.log(` ✓ Committed: release: ${tag}`)
|
||||
} catch {
|
||||
console.error('❌ Git commit 失败')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('')
|
||||
|
||||
// 6. Git tag
|
||||
console.log(`🏷️ 创建 tag: ${tag}...`)
|
||||
try {
|
||||
run(`git tag -a ${tag} -m "Release ${tag}"`)
|
||||
console.log(` ✓ Tag created: ${tag}`)
|
||||
} catch {
|
||||
console.error('❌ Git tag 创建失败')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('')
|
||||
|
||||
// 7. Push
|
||||
console.log('🚀 推送到远程...')
|
||||
try {
|
||||
run('git push')
|
||||
run(`git push origin ${tag}`)
|
||||
console.log(' ✓ Pushed commit and tag')
|
||||
} catch {
|
||||
console.error('❌ Push 失败')
|
||||
console.log('')
|
||||
console.log('你可以手动执行:')
|
||||
console.log(` git push`)
|
||||
console.log(` git push origin ${tag}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('')
|
||||
console.log('==========================================')
|
||||
console.log(' ✅ Release 流程完成!')
|
||||
console.log('==========================================')
|
||||
console.log('')
|
||||
console.log(` 版本: ${newVersion}`)
|
||||
console.log(` Tag: ${tag}`)
|
||||
console.log('')
|
||||
console.log(' GitHub Actions 将自动:')
|
||||
console.log(' 1. 打包所有模板')
|
||||
console.log(' 2. 创建 GitHub Release')
|
||||
console.log(' 3. 上传 zip 文件')
|
||||
console.log(' 4. 更新 latest.json')
|
||||
console.log('')
|
||||
console.log(' 下载地址:')
|
||||
console.log(` https://github.com/nuwax-ai/xagi-frontend-templates/releases/tag/${tag}`)
|
||||
console.log(` https://raw.githubusercontent.com/nuwax-ai/xagi-frontend-templates/main/latest.json`)
|
||||
console.log('')
|
||||
203
qiming-xagi-frontend-templates/scripts/restart-file-server.sh
Normal file
203
qiming-xagi-frontend-templates/scripts/restart-file-server.sh
Normal file
@@ -0,0 +1,203 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 重启 rcoder 容器内 nuwax-file-server 进程
|
||||
# 用途: 与 rcoder 容器重启解耦,单独重启 file-server
|
||||
|
||||
set -e
|
||||
|
||||
# 脚本所在目录
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
# 加载环境变量
|
||||
if [ -f "$PROJECT_ROOT/.env" ]; then
|
||||
source "$PROJECT_ROOT/.env"
|
||||
else
|
||||
echo "错误: .env 文件不存在,请先创建 .env 文件"
|
||||
echo "可以复制 .env.example 并修改配置"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查必需的环境变量
|
||||
if [ -z "$REMOTE_HOST" ] || [ -z "$REMOTE_USER" ] || [ -z "$REMOTE_PASSWORD" ]; then
|
||||
echo "错误: 缺少必需的环境变量"
|
||||
echo "请确保 .env 文件中包含: REMOTE_HOST, REMOTE_USER, REMOTE_PASSWORD"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查 sshpass 是否安装
|
||||
if ! command -v sshpass &> /dev/null; then
|
||||
echo "错误: sshpass 未安装"
|
||||
echo "请运行: brew install hudochenkov/sshpass/sshpass"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "步骤: 重启 rcoder 容器内 nuwax-file-server"
|
||||
echo "------------------------------------------"
|
||||
echo "正在连接远程服务器执行 file-server 重启..."
|
||||
|
||||
sshpass -p "$REMOTE_PASSWORD" ssh -o StrictHostKeyChecking=no \
|
||||
"$REMOTE_USER@$REMOTE_HOST" << 'EOF'
|
||||
set -e
|
||||
|
||||
# 目标容器名和镜像(与你当前环境保持一致)
|
||||
TARGET_CONTAINER_NAME="docker-rcoder-1"
|
||||
TARGET_IMAGE="nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/nuwax-test/rcoder:latest"
|
||||
|
||||
# 保存命中的容器 ID 和命中策略,方便排查
|
||||
RCODER_CONTAINER=""
|
||||
MATCH_STRATEGY=""
|
||||
|
||||
# 1) 优先按容器名精确匹配
|
||||
RCODER_CONTAINER=$(docker ps -a --filter "name=^${TARGET_CONTAINER_NAME}$" --format "{{.ID}}" | head -1)
|
||||
if [ -n "$RCODER_CONTAINER" ]; then
|
||||
MATCH_STRATEGY="容器名精确匹配: ${TARGET_CONTAINER_NAME}"
|
||||
fi
|
||||
|
||||
# 2) 容器名未命中时,按镜像精确匹配
|
||||
if [ -z "$RCODER_CONTAINER" ]; then
|
||||
RCODER_CONTAINER=$(docker ps -a --filter "ancestor=${TARGET_IMAGE}" --format "{{.ID}}" | head -1)
|
||||
if [ -n "$RCODER_CONTAINER" ]; then
|
||||
MATCH_STRATEGY="镜像精确匹配: ${TARGET_IMAGE}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3) 最后兜底模糊匹配,兼容历史命名
|
||||
if [ -z "$RCODER_CONTAINER" ]; then
|
||||
RCODER_CONTAINER=$(docker ps -a --filter "name=rcoder" --format "{{.ID}}" | head -1)
|
||||
if [ -n "$RCODER_CONTAINER" ]; then
|
||||
MATCH_STRATEGY="兜底模糊匹配: name=rcoder"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$RCODER_CONTAINER" ]; then
|
||||
echo "✗ 未找到 rcoder 容器,无法执行 file-server 重启"
|
||||
echo "候选容器(名称或镜像包含 rcoder):"
|
||||
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}" | grep -i "rcoder" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "命中策略: $MATCH_STRATEGY"
|
||||
echo "目标容器详情:"
|
||||
docker ps -a --filter "id=${RCODER_CONTAINER}" --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}"
|
||||
echo ""
|
||||
echo "进入容器执行 nuwax-file-server 重启..."
|
||||
|
||||
# 在容器内执行完整重启流程(使用单条 bash -lc,避免 SSH 嵌套 heredoc 丢失 stdin)
|
||||
docker exec "$RCODER_CONTAINER" /bin/bash -lc '
|
||||
set -e
|
||||
|
||||
# 按 PID 列表查找目标进程,并排除当前 shell 自身,避免统计到检查命令进程
|
||||
list_target_pids() {
|
||||
local self_pid="$1"
|
||||
ps -eo pid=,comm=,args= | awk -v self="$self_pid" "\$1 != self && (\$2 == \"node\" || \$2 == \"sh\" || \$2 == \"pnpm\") && (\$0 ~ /nuwax-file-server\\/dist\\/server\\.js/ || \$0 ~ /scripts\\/start-prod\\.js/ || \$0 ~ /\\/usr\\/local\\/bin\\/pnpm prod/ || \$0 ~ /pnpm prod/) { print \$1 }" | sort -u
|
||||
}
|
||||
|
||||
print_target_processes() {
|
||||
local self_pid="$1"
|
||||
ps -eo pid=,comm=,args= | awk -v self="$self_pid" "\$1 != self && (\$2 == \"node\" || \$2 == \"sh\" || \$2 == \"pnpm\") && (\$0 ~ /nuwax-file-server\\/dist\\/server\\.js/ || \$0 ~ /scripts\\/start-prod\\.js/ || \$0 ~ /\\/usr\\/local\\/bin\\/pnpm prod/ || \$0 ~ /pnpm prod/) { print }"
|
||||
}
|
||||
|
||||
echo "[容器内] 当前 file-server 相关进程:"
|
||||
print_target_processes "$$" || true
|
||||
echo ""
|
||||
|
||||
TARGET_PIDS="$(list_target_pids "$$")"
|
||||
if [ -n "$TARGET_PIDS" ]; then
|
||||
echo "[容器内] 发现旧进程 PID: $TARGET_PIDS"
|
||||
echo "[容器内] 第一步: 发送 TERM,优雅停止旧进程"
|
||||
for pid in $TARGET_PIDS; do
|
||||
kill "$pid" 2>/dev/null || true
|
||||
done
|
||||
|
||||
WAIT_SECONDS=10
|
||||
for _ in $(seq 1 "$WAIT_SECONDS"); do
|
||||
REMAINING_PIDS=""
|
||||
for pid in $TARGET_PIDS; do
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
REMAINING_PIDS="$REMAINING_PIDS $pid"
|
||||
fi
|
||||
done
|
||||
if [ -z "$REMAINING_PIDS" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [ -n "$REMAINING_PIDS" ]; then
|
||||
echo "[容器内] 以下 PID 在 TERM 后仍存活,执行 KILL:$REMAINING_PIDS"
|
||||
for pid in $REMAINING_PIDS; do
|
||||
kill -9 "$pid" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
else
|
||||
echo "[容器内] 未发现旧的 nuwax-file-server 相关进程"
|
||||
fi
|
||||
|
||||
POST_KILL_PIDS="$(list_target_pids "$$")"
|
||||
if [ -n "$POST_KILL_PIDS" ]; then
|
||||
POST_KILL_COUNT="$(echo "$POST_KILL_PIDS" | wc -w | tr -d " ")"
|
||||
echo "[容器内] ✗ 旧进程未清理干净,当前残留进程数: $POST_KILL_COUNT"
|
||||
echo "[容器内] 为避免双实例冲突,本次停止启动新进程"
|
||||
print_target_processes "$$" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[容器内] 旧进程已清理完成,准备启动新进程"
|
||||
echo "[容器内] 启动新进程: pnpm prod"
|
||||
mkdir -p /app/logs
|
||||
cd /app/nuwax-file-server
|
||||
pnpm prod > /app/logs/nuwax-file-server.log 2>&1 &
|
||||
sleep 2
|
||||
|
||||
echo "[容器内] 重启后 file-server 相关进程:"
|
||||
print_target_processes "$$" || true
|
||||
echo "[容器内] 日志路径: /app/logs/nuwax-file-server.log"
|
||||
'
|
||||
|
||||
# 重启完成后执行健康检查,尽量在脚本阶段提前发现异常
|
||||
echo ""
|
||||
echo "执行健康检查..."
|
||||
|
||||
# 1) 检查容器运行状态和健康状态
|
||||
# - State.Status 常见值: running/exited
|
||||
# - Health.Status 常见值: healthy/unhealthy/starting(若镜像未配置 healthcheck 则为空)
|
||||
CONTAINER_STATE=$(docker inspect -f '{{.State.Status}}' "$RCODER_CONTAINER")
|
||||
HEALTH_STATE=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$RCODER_CONTAINER")
|
||||
echo "容器状态: $CONTAINER_STATE"
|
||||
echo "健康状态: $HEALTH_STATE"
|
||||
|
||||
if [ "$CONTAINER_STATE" != "running" ]; then
|
||||
echo "✗ 健康检查失败: 容器未运行"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 若镜像配置了 healthcheck,则要求至少不是 unhealthy
|
||||
if [ "$HEALTH_STATE" = "unhealthy" ]; then
|
||||
echo "✗ 健康检查失败: 容器健康状态为 unhealthy"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2) 检查容器内 nuwax-file-server 关键进程是否存在(排除当前 shell 自身)
|
||||
PROCESS_COUNT=$(docker exec "$RCODER_CONTAINER" /bin/bash -lc '
|
||||
SELF_PID="$$"
|
||||
ps -eo pid=,comm=,args= | awk -v self="$SELF_PID" '\''$1 != self && ($2 == "node" || $2 == "sh" || $2 == "pnpm") && ($0 ~ /nuwax-file-server\/dist\/server\.js/ || $0 ~ /scripts\/start-prod\.js/ || $0 ~ /\/usr\/local\/bin\/pnpm prod/ || $0 ~ /pnpm prod/) {count++} END {print count+0}'\''')
|
||||
echo "nuwax-file-server 相关进程数: $PROCESS_COUNT"
|
||||
if [ "$PROCESS_COUNT" -lt 1 ]; then
|
||||
echo "✗ 健康检查失败: 未检测到 nuwax-file-server 相关进程"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 3) 检查日志文件是否存在且非空,作为服务启动后的基础信号
|
||||
LOG_CHECK=$(docker exec "$RCODER_CONTAINER" /bin/bash -lc \
|
||||
"if [ -s /app/logs/nuwax-file-server.log ]; then echo ok; else echo fail; fi")
|
||||
if [ "$LOG_CHECK" != "ok" ]; then
|
||||
echo "✗ 健康检查失败: /app/logs/nuwax-file-server.log 不存在或为空"
|
||||
exit 1
|
||||
fi
|
||||
echo "日志文件检查: ok (/app/logs/nuwax-file-server.log)"
|
||||
echo "✓ 健康检查通过"
|
||||
EOF
|
||||
|
||||
echo ""
|
||||
echo "nuwax-file-server 重启流程执行完成。"
|
||||
93
qiming-xagi-frontend-templates/scripts/restart-rcoder.sh
Normal file
93
qiming-xagi-frontend-templates/scripts/restart-rcoder.sh
Normal file
@@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 重启 rcoder 容器
|
||||
# 用途: 与 file-server 进程重启解耦,单独执行容器重启
|
||||
|
||||
set -e
|
||||
|
||||
# 脚本所在目录
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
# 加载环境变量
|
||||
if [ -f "$PROJECT_ROOT/.env" ]; then
|
||||
source "$PROJECT_ROOT/.env"
|
||||
else
|
||||
echo "错误: .env 文件不存在,请先创建 .env 文件"
|
||||
echo "可以复制 .env.example 并修改配置"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查必需的环境变量
|
||||
if [ -z "$REMOTE_HOST" ] || [ -z "$REMOTE_USER" ] || [ -z "$REMOTE_PASSWORD" ]; then
|
||||
echo "错误: 缺少必需的环境变量"
|
||||
echo "请确保 .env 文件中包含: REMOTE_HOST, REMOTE_USER, REMOTE_PASSWORD"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查 sshpass 是否安装
|
||||
if ! command -v sshpass &> /dev/null; then
|
||||
echo "错误: sshpass 未安装"
|
||||
echo "请运行: brew install hudochenkov/sshpass/sshpass"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "步骤: 重启 rcoder 容器"
|
||||
echo "------------------------------------------"
|
||||
echo "正在连接远程服务器重启容器..."
|
||||
|
||||
sshpass -p "$REMOTE_PASSWORD" ssh -o StrictHostKeyChecking=no \
|
||||
"$REMOTE_USER@$REMOTE_HOST" << 'EOF'
|
||||
echo "查找 rcoder 容器..."
|
||||
|
||||
# 目标容器名与镜像名(固定值),优先精确匹配,避免误重启同类容器
|
||||
TARGET_CONTAINER_NAME="docker-rcoder-1"
|
||||
TARGET_IMAGE="nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/nuwax-test/rcoder:latest"
|
||||
|
||||
# 记录最终命中的容器 ID 与命中策略,便于日志排查
|
||||
RCODER_CONTAINER=""
|
||||
MATCH_STRATEGY=""
|
||||
|
||||
# 1) 先按容器名精确匹配(最稳定,与你当前线上容器名一致)
|
||||
RCODER_CONTAINER=$(docker ps -a --filter "name=^${TARGET_CONTAINER_NAME}$" --format "{{.ID}}" | head -1)
|
||||
if [ -n "$RCODER_CONTAINER" ]; then
|
||||
MATCH_STRATEGY="容器名精确匹配: ${TARGET_CONTAINER_NAME}"
|
||||
fi
|
||||
|
||||
# 2) 若容器名未命中,再按镜像精确匹配(适合容器名变化但镜像固定的场景)
|
||||
if [ -z "$RCODER_CONTAINER" ]; then
|
||||
RCODER_CONTAINER=$(docker ps -a --filter "ancestor=${TARGET_IMAGE}" --format "{{.ID}}" | head -1)
|
||||
if [ -n "$RCODER_CONTAINER" ]; then
|
||||
MATCH_STRATEGY="镜像精确匹配: ${TARGET_IMAGE}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3) 最后兜底模糊匹配 name=rcoder,兼容历史命名方式
|
||||
if [ -z "$RCODER_CONTAINER" ]; then
|
||||
RCODER_CONTAINER=$(docker ps -a --filter "name=rcoder" --format "{{.ID}}" | head -1)
|
||||
if [ -n "$RCODER_CONTAINER" ]; then
|
||||
MATCH_STRATEGY="兜底模糊匹配: name=rcoder"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$RCODER_CONTAINER" ]; then
|
||||
echo "✗ 未找到可重启的 rcoder 容器"
|
||||
echo "目标容器名: $TARGET_CONTAINER_NAME"
|
||||
echo "目标镜像: $TARGET_IMAGE"
|
||||
echo ""
|
||||
echo "候选容器(名称或镜像包含 rcoder):"
|
||||
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}" | grep -i "rcoder" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "命中策略: $MATCH_STRATEGY"
|
||||
echo "重启前容器详情:"
|
||||
docker ps -a --filter "id=${RCODER_CONTAINER}" --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}"
|
||||
|
||||
echo "正在重启容器..."
|
||||
docker restart "$RCODER_CONTAINER"
|
||||
|
||||
echo "重启后容器详情:"
|
||||
docker ps -a --filter "id=${RCODER_CONTAINER}" --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}"
|
||||
echo "✓ rcoder 容器重启成功"
|
||||
EOF
|
||||
29
qiming-xagi-frontend-templates/scripts/test-all.js
Normal file
29
qiming-xagi-frontend-templates/scripts/test-all.js
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
const packages = [
|
||||
{ name: 'React Vite', path: 'packages/react-vite', port: 3000 },
|
||||
{ name: 'Vue3 Vite', path: 'packages/vue3-vite', port: 4000 },
|
||||
{ name: 'React Next.js', path: 'packages/react-next', port: 3000 }
|
||||
];
|
||||
|
||||
console.log('🧪 开始测试所有模板...\n');
|
||||
|
||||
packages.forEach((pkg, index) => {
|
||||
console.log(`[${index + 1}/${packages.length}] 测试 ${pkg.name} (${pkg.path})...`);
|
||||
|
||||
try {
|
||||
// 由于已经使用workspace管理,只需要测试构建
|
||||
console.log(' 🔨 测试构建...');
|
||||
execSync(`cd ${pkg.path} && pnpm build`, { stdio: 'inherit' });
|
||||
|
||||
console.log(`✅ ${pkg.name} 测试通过\n`);
|
||||
} catch (error) {
|
||||
console.error(`❌ ${pkg.name} 测试失败:`, error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('🎉 所有模板测试完成!');
|
||||
104
qiming-xagi-frontend-templates/scripts/upload-templates.sh
Normal file
104
qiming-xagi-frontend-templates/scripts/upload-templates.sh
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 模板上传脚本
|
||||
# 用途: 自动上传 react-vite 和 vue3-vite 模板到远程服务器
|
||||
|
||||
set -e
|
||||
|
||||
# 脚本所在目录
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
# 加载环境变量
|
||||
if [ -f "$PROJECT_ROOT/.env" ]; then
|
||||
source "$PROJECT_ROOT/.env"
|
||||
else
|
||||
echo "错误: .env 文件不存在,请先创建 .env 文件"
|
||||
echo "可以复制 .env.example 并修改配置"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查必需的环境变量
|
||||
if [ -z "$REMOTE_HOST" ] || [ -z "$REMOTE_USER" ] || [ -z "$REMOTE_PASSWORD" ] || [ -z "$REMOTE_PATH" ]; then
|
||||
echo "错误: 缺少必需的环境变量"
|
||||
echo "请确保 .env 文件中包含: REMOTE_HOST, REMOTE_USER, REMOTE_PASSWORD, REMOTE_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查 sshpass 是否安装
|
||||
if ! command -v sshpass &> /dev/null; then
|
||||
echo "错误: sshpass 未安装"
|
||||
echo "请运行: brew install hudochenkov/sshpass/sshpass"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# zip 文件目录
|
||||
ZIP_DIR="$PROJECT_ROOT/zip"
|
||||
|
||||
# 查找最新的模板文件
|
||||
echo "查找最新的模板文件..."
|
||||
|
||||
REACT_VITE_ZIP=$(ls -t "$ZIP_DIR"/react-vite-template_*.zip 2>/dev/null | head -1)
|
||||
VUE3_VITE_ZIP=$(ls -t "$ZIP_DIR"/vue3-vite-template_*.zip 2>/dev/null | head -1)
|
||||
|
||||
if [ -z "$REACT_VITE_ZIP" ]; then
|
||||
echo "错误: 未找到 react-vite-template zip 文件"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$VUE3_VITE_ZIP" ]; then
|
||||
echo "错误: 未找到 vue3-vite-template zip 文件"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "找到模板文件:"
|
||||
echo " React Vite: $(basename "$REACT_VITE_ZIP")"
|
||||
echo " Vue3 Vite: $(basename "$VUE3_VITE_ZIP")"
|
||||
echo ""
|
||||
|
||||
# 创建临时目录
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
trap "rm -rf $TEMP_DIR" EXIT
|
||||
|
||||
# 复制并重命名文件
|
||||
echo "准备上传文件..."
|
||||
cp "$REACT_VITE_ZIP" "$TEMP_DIR/react-vite-template.zip"
|
||||
cp "$VUE3_VITE_ZIP" "$TEMP_DIR/vue3-vite-template.zip"
|
||||
|
||||
# 上传文件
|
||||
echo ""
|
||||
echo "开始上传到 $REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH"
|
||||
echo ""
|
||||
|
||||
# 上传 react-vite-template.zip
|
||||
echo "上传 react-vite-template.zip..."
|
||||
sshpass -p "$REMOTE_PASSWORD" scp -o StrictHostKeyChecking=no \
|
||||
"$TEMP_DIR/react-vite-template.zip" \
|
||||
"$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/react-vite-template.zip"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✓ react-vite-template.zip 上传成功"
|
||||
else
|
||||
echo "✗ react-vite-template.zip 上传失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 上传 vue3-vite-template.zip
|
||||
echo "上传 vue3-vite-template.zip..."
|
||||
sshpass -p "$REMOTE_PASSWORD" scp -o StrictHostKeyChecking=no \
|
||||
"$TEMP_DIR/vue3-vite-template.zip" \
|
||||
"$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/vue3-vite-template.zip"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✓ vue3-vite-template.zip 上传成功"
|
||||
else
|
||||
echo "✗ vue3-vite-template.zip 上传失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "所有模板上传完成!"
|
||||
echo ""
|
||||
echo "提示: file-server 重启与 rcoder 容器重启已拆分为独立脚本"
|
||||
echo "如需重启 file-server: ./scripts/restart-file-server.sh"
|
||||
echo "如需重启 rcoder 容器: ./scripts/restart-rcoder.sh"
|
||||
Reference in New Issue
Block a user