"添加前端模板和运行代码模块"

This commit is contained in:
Codex
2026-06-01 13:43:09 +08:00
parent 24045274ad
commit 8092c4b1f8
587 changed files with 99761 additions and 0 deletions

View 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
```

View 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('🎉 所有模板构建完成!');

View 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.')

View 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 "=========================================="

View File

@@ -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`)
}
}

View 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}`);

View 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)
}

View 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('')

View 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 重启流程执行完成。"

View 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

View 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('🎉 所有模板测试完成!');

View 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"