初次提交
Build VoiceHub FPK / build-fpk (push) Failing after 56m5s
Build and Push Docker Image / build-and-push (push) Canceled after 1h9m35s

This commit is contained in:
2026-09-06 08:19:02 +08:00
commit 4ce4649aaf
1061 changed files with 337258 additions and 0 deletions
+245
View File
@@ -0,0 +1,245 @@
#!/usr/bin/env node
import fs from 'fs'
import path from 'path'
// 颜色输出函数
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
cyan: '\x1b[36m'
}
function log(message, color = 'reset') {
console.log(`${colors[color]}${message}${colors.reset}`)
}
function logSuccess(message) {
log(`✅ ${message}`, 'green')
}
function logWarning(message) {
log(`⚠️ ${message}`, 'yellow')
}
function logError(message) {
log(`❌ ${message}`, 'red')
}
function logInfo(message) {
log(`ℹ️ ${message}`, 'blue')
}
// 检查文件是否存在
function fileExists(filePath) {
try {
return fs.existsSync(filePath)
} catch {
return false
}
}
// 检查目录大小
function getDirectorySize(dirPath) {
try {
const stats = fs.statSync(dirPath)
if (stats.isFile()) {
return stats.size
} else if (stats.isDirectory()) {
let totalSize = 0
const files = fs.readdirSync(dirPath)
for (const file of files) {
const filePath = path.join(dirPath, file)
totalSize += getDirectorySize(filePath)
}
return totalSize
}
} catch {
return 0
}
return 0
}
// 格式化文件大小
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
// 检查 Drizzle 配置
function checkDrizzleConfig() {
log('🔍 检查 Drizzle 配置...', 'cyan')
const drizzleConfigPath = 'drizzle.config.ts'
const drizzleSchemaPath = 'app/drizzle/schema.ts'
if (!fileExists(drizzleConfigPath)) {
logError('Drizzle 配置文件不存在')
return false
}
if (!fileExists(drizzleSchemaPath)) {
logError('Drizzle schema 文件不存在')
return false
}
logSuccess('Drizzle 配置检查通过')
return true
}
// 检查环境变量
function checkEnvironmentVariables() {
log('🔍 检查环境变量...', 'cyan')
const requiredVars = ['DATABASE_URL', 'JWT_SECRET']
const optionalVars = ['NODE_ENV']
let allRequired = true
// 检查必需的环境变量
requiredVars.forEach((varName) => {
if (process.env[varName]) {
logSuccess(`${varName} 已设置`)
} else {
logError(`缺少必需的环境变量: ${varName}`)
allRequired = false
}
})
// 检查可选的环境变量
optionalVars.forEach((varName) => {
if (process.env[varName]) {
logInfo(`${varName} 已设置`)
} else {
logWarning(`可选环境变量未设置: ${varName}`)
}
})
return allRequired
}
// 检查数据库连接
async function checkDatabaseConnection() {
log('🔍 检查数据库连接...', 'cyan')
try {
// 检查数据库连接文件是否存在
const dbPath = 'app/drizzle/db.ts'
if (!fileExists(dbPath)) {
logError('数据库连接文件不存在')
return false
}
logSuccess('数据库配置检查通过')
return true
} catch (error) {
logWarning('数据库连接测试失败(这在某些部署环境中是正常的)')
return false
}
}
// 检查关键文件
function checkCriticalFiles() {
log('🔍 检查关键文件...', 'cyan')
const criticalFiles = [
'nuxt.config.ts',
'drizzle.config.ts',
'app/drizzle/schema.ts',
'package.json'
]
let allExists = true
criticalFiles.forEach((file) => {
if (fileExists(file)) {
logSuccess(`${file} 存在`)
} else {
logError(`关键文件缺失: ${file}`)
allExists = false
}
})
return allExists
}
// 生成部署报告
function generateDeploymentReport(checks) {
log('\n📊 部署检查报告', 'bright')
log('='.repeat(50), 'cyan')
const passed = checks.filter((check) => check.passed).length
const total = checks.length
checks.forEach((check) => {
const status = check.passed ? '✅' : '❌'
const color = check.passed ? 'green' : 'red'
log(`${status} ${check.name}`, color)
if (check.details) {
log(` ${check.details}`, 'reset')
}
})
log(`\n总计: ${passed}/${total} 项检查通过`, passed === total ? 'green' : 'yellow')
if (passed === total) {
log('🎉 所有检查都通过了!部署应该可以正常工作。', 'green')
} else {
log('⚠️ 部分检查未通过,可能会影响应用运行。', 'yellow')
}
}
// 主检查函数
async function checkDeployment() {
log('🔍 开始部署后检查...', 'bright')
const checks = [
{
name: '关键文件检查',
passed: checkCriticalFiles(),
details: null
},
{
name: 'Drizzle 配置检查',
passed: checkDrizzleConfig(),
details: null
},
{
name: '环境变量检查',
passed: checkEnvironmentVariables(),
details: null
}
]
// 数据库连接检查(异步)
const dbConnected = await checkDatabaseConnection()
checks.push({
name: '数据库连接检查',
passed: dbConnected,
details: dbConnected ? null : '在某些部署环境中这是正常的'
})
generateDeploymentReport(checks)
const criticalFailed = checks.filter(
(check) => !check.passed && !check.name.includes('数据库连接')
).length
if (criticalFailed > 0) {
process.exit(1)
}
}
// 运行检查
checkDeployment().catch((error) => {
logError(`检查过程中发生错误: ${error.message}`)
process.exit(1)
})
+122
View File
@@ -0,0 +1,122 @@
/**
* 数据库清空脚本
* 这个脚本会清空所有表的数据,但保留表结构
*/
import {
apiKeyPermissions,
apiKeys,
apiLogs,
db,
emailTemplates,
notifications,
notificationSettings,
playTimes,
schedules,
semesters,
songBlacklists,
songs,
systemSettings,
users,
userStatusLogs,
votes
} from '../app/drizzle/db.ts'
import bcrypt from 'bcryptjs'
// 重置所有表的自增序列
async function resetAutoIncrementSequences() {
const tables = [
'User',
'Song',
'Vote',
'Schedule',
'Notification',
'NotificationSettings',
'PlayTime',
'Semester',
'SystemSettings',
'SongBlacklist',
'EmailTemplate',
'user_status_logs'
]
console.log('重置自增序列...')
for (const table of tables) {
try {
// PostgreSQL 重置序列的 SQL 命令
const sequenceName = `"${table}_id_seq"`
await db.execute(`ALTER SEQUENCE ${sequenceName} RESTART WITH 1`)
} catch (error) {
console.warn(`重置 ${table} 表序列失败: ${error.message}`)
}
}
}
async function main() {
console.log('开始清空数据库...')
// 清空所有表数据
try {
// 按照关联关系顺序删除数据
await db.delete(notifications)
await db.delete(notificationSettings)
await db.delete(schedules)
await db.delete(votes)
await db.delete(songs)
await db.delete(playTimes)
await db.delete(semesters)
await db.delete(systemSettings)
await db.delete(songBlacklists)
await db.delete(apiLogs)
await db.delete(apiKeyPermissions)
await db.delete(apiKeys)
await db.delete(userStatusLogs)
await db.delete(emailTemplates)
await db.delete(users)
console.log('数据库已清空,开始重置自增序列...')
// 先重置所有自增序列
await resetAutoIncrementSequences()
console.log('开始创建默认超级管理员账户...')
// 创建默认管理员账户
const hashedPassword = await bcrypt.hash('admin123', 10)
// 创建超级管理员用户
const [admin] = await db
.insert(users)
.values({
name: '超级管理员',
username: 'admin',
password: hashedPassword,
role: 'SUPER_ADMIN',
forcePasswordChange: false
})
.returning()
console.log('默认超级管理员账户已创建:')
console.log('账号名: admin')
console.log('密码: admin123')
console.log(`管理员ID: ${admin.id}`)
// 调整User表的自增序列,确保下一个用户从admin.id + 1开始
try {
const nextId = admin.id + 1
await db.execute(`ALTER SEQUENCE "User_id_seq" RESTART WITH ${nextId}`)
} catch (error) {
console.warn(`调整User表序列失败: ${error.message}`)
}
console.log('🎉 数据库清空、初始化和序列重置全部完成!')
} catch (error) {
console.error('清空数据库时出错:', error)
process.exit(1)
}
}
main().catch((e) => {
console.error(e)
process.exit(1)
})
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env node
import { config } from 'dotenv'
import path from 'path'
import { fileURLToPath } from 'url'
import { client, db, notificationSettings, systemSettings, users } from '../app/drizzle/db.ts'
import bcrypt from 'bcryptjs'
import { eq } from 'drizzle-orm'
const currentDir = path.dirname(fileURLToPath(import.meta.url))
config({ path: path.resolve(process.cwd(), '.env') })
if (!process.env.DATABASE_URL) {
console.error('❌ DATABASE_URL 环境变量未设置')
process.exit(1)
}
async function main() {
try {
// 检查是否已有超级管理员
const existingSuperAdmin = await db
.select()
.from(users)
.where(eq(users.role, 'SUPER_ADMIN'))
.limit(1)
if (existingSuperAdmin.length > 0) {
console.log('✅ 管理员已存在')
return existingSuperAdmin[0]
}
// 创建管理员
const hashedPassword = await bcrypt.hash('admin123', 10)
const [admin] = await db
.insert(users)
.values({
username: 'admin',
name: '超级管理员',
password: hashedPassword,
role: 'SUPER_ADMIN',
forcePasswordChange: false
})
.returning()
// 创建通知设置
await db
.insert(notificationSettings)
.values({
userId: admin.id,
enabled: true,
songRequestEnabled: true,
songVotedEnabled: true,
songPlayedEnabled: true,
refreshInterval: 60,
songVotedThreshold: 1
})
.onConflictDoNothing()
// 创建系统设置
await db
.insert(systemSettings)
.values({
id: 1,
enablePlayTimeSelection: false
})
.onConflictDoNothing()
console.log('✅ 管理员创建成功 (admin/admin123)')
return admin
} catch (error) {
console.error('❌ 创建管理员失败:', error)
throw error
}
}
main()
.then(async () => {
await client.end()
process.exit(0)
})
.catch(async (error) => {
console.error('❌ 初始化失败:', error)
await client.end()
process.exit(1)
})
+177
View File
@@ -0,0 +1,177 @@
#!/usr/bin/env node
import { execSync } from 'child_process'
import fs from 'fs'
import path from 'path'
import { config } from 'dotenv'
config({ path: path.resolve(process.cwd(), '.env') })
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
cyan: '\x1b[36m'
}
const log = (msg, color = 'reset') => console.log(`${colors[color]}${msg}${colors.reset}`)
const ok = (msg) => log(`✅ ${msg}`, 'green')
const warn = (msg) => log(`⚠️ ${msg}`, 'yellow')
const err = (msg) => log(`❌ ${msg}`, 'red')
const NON_INTERACTIVE_ENV = {
...process.env,
CI: 'true',
DRIZZLE_KIT_FORCE: 'true',
DRIZZLE_KIT_NON_INTERACTIVE: 'true',
NODE_ENV: process.env.NODE_ENV || 'production'
}
function safeExec(command, options = {}) {
try {
execSync(command, { stdio: 'inherit', ...options })
return true
} catch (e) {
return false
}
}
function fileExists(p) {
try {
return fs.existsSync(p)
} catch {
return false
}
}
function ensureDrizzleFiles() {
if (!fileExists('drizzle.config.ts')) throw new Error('Drizzle 配置文件不存在')
if (!fileExists('app/drizzle/schema.ts')) throw new Error('Schema 文件不存在')
}
function isEmptyDatabase() {
try {
const output = execSync('npx drizzle-kit introspect --config=drizzle.config.ts', {
stdio: 'pipe',
env: NON_INTERACTIVE_ENV,
encoding: 'utf8'
})
const tablesMatch = output.match(/(\d+)\s+tables/i)
const hasTablesCount = tablesMatch && Number(tablesMatch[1]) > 0
const listsTables = /\bcolumns\b|\bindexes\b|\bfks\b/i.test(output)
return !(hasTablesCount || listsTables)
} catch {
warn('无法检测数据库状态,按非空库处理')
return false
}
}
// 检查数据库schema是否与代码中的schema一致
function checkSchemaConsistency() {
try {
// 使用 drizzle-kit introspect 获取当前数据库schema
const output = execSync('npx drizzle-kit introspect --config=drizzle.config.ts', {
stdio: 'pipe',
env: NON_INTERACTIVE_ENV,
encoding: 'utf8'
})
// 检查关键表和字段是否存在
const checks = [
{ pattern: /"status".*user_status/i, name: 'user_status enum type' },
{ pattern: /"api_keys"/i, name: 'api_keys table' }
]
const missing = []
for (const check of checks) {
if (!check.pattern.test(output)) {
missing.push(check.name)
}
}
if (missing.length > 0) {
warn(`检测到数据库schema不完整,缺少: ${missing.join(', ')}`)
return false
}
return true
} catch (e) {
warn('无法检测数据库schema一致性')
return true // 出错时不阻止继续
}
}
function main() {
log('🔄 数据库同步', 'cyan')
if (!process.env.DATABASE_URL) {
warn('未设置 DATABASE_URL')
process.exit(0)
}
ensureDrizzleFiles()
const emptyDb = isEmptyDatabase()
if (emptyDb) {
log('🆕 检测到空库,执行迁移 (migrate)...', 'cyan')
if (!safeExec('npx drizzle-kit migrate', { env: NON_INTERACTIVE_ENV })) {
err('数据库迁移失败')
process.exit(1)
}
ok('空库迁移完成')
} else {
log('🔁 检测到非空库,检查schema一致性...', 'cyan')
// 检查数据库schema是否与代码一致
const schemaConsistent = checkSchemaConsistency()
if (!schemaConsistent) {
warn('数据库schema不完整,尝试使用 push --force 进行修复...', 'cyan')
const pushCommand = 'npx drizzle-kit push --force --config=drizzle.config.ts'
if (
!safeExec(pushCommand, {
env: { ...NON_INTERACTIVE_ENV, DRIZZLE_KIT_NON_INTERACTIVE: 'true' }
})
) {
err('数据库schema修复失败')
process.exit(1)
}
ok('schema修复成功')
} else {
log('🔁 数据库schema一致,尝试执行 migrate 同步...', 'cyan')
// 尝试执行 migrate
const migrateSuccess = safeExec('npx drizzle-kit migrate', {
env: { ...NON_INTERACTIVE_ENV, DRIZZLE_KIT_NON_INTERACTIVE: 'true' }
})
if (migrateSuccess) {
ok('migrate 同步成功')
} else {
warn('migrate 同步失败,可能是由于数据库结构与迁移记录不一致。')
log('🔄 尝试使用 push --force 进行强制同步...', 'cyan')
// 在 CI 环境下,push 命令如果遇到重命名等歧义可能会弹出交互式提示
// 确保 DRIZZLE_KIT_NON_INTERACTIVE 已设置
const pushCommand = 'npx drizzle-kit push --force --config=drizzle.config.ts'
if (
!safeExec(pushCommand, {
env: { ...NON_INTERACTIVE_ENV, DRIZZLE_KIT_NON_INTERACTIVE: 'true' }
})
) {
err('数据库同步完全失败。请检查数据库连接或手动运行 npx drizzle-kit push 以解决歧义。')
process.exit(1)
}
ok('强制同步 (push) 成功')
}
}
}
ok('数据库同步流程完成')
}
try {
main()
} catch (e) {
err(`同步异常: ${e.message || e}`)
process.exit(1)
}
+198
View File
@@ -0,0 +1,198 @@
#!/usr/bin/env node
import { execSync } from 'child_process'
import fs from 'fs'
import { config } from 'dotenv'
import path from 'path'
// 加载环境变量(从项目根目录)
config({ path: path.resolve(process.cwd(), '.env') })
// 如果设置了 PREBUILT=true,则自动跳过安装和构建
if (process.env.PREBUILT === 'true') {
process.env.SKIP_INSTALL = 'true'
process.env.SKIP_BUILD = 'true'
}
// 颜色输出函数
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
cyan: '\x1b[36m'
}
const BUILD_MEMORY_MB = 1024
const DEFAULT_NODE_OPTIONS = `--max-old-space-size=${BUILD_MEMORY_MB}`
function log(message, color = 'reset') {
console.log(`${colors[color]}${message}${colors.reset}`)
}
function logStep(step, message) {
log(`${step} ${message}`, 'cyan')
}
function logSuccess(message) {
log(`✅ ${message}`, 'green')
}
function logWarning(message) {
log(`⚠️ ${message}`, 'yellow')
}
function logError(message) {
log(`❌ ${message}`, 'red')
}
// 检查文件是否存在
function fileExists(filePath) {
try {
return fs.existsSync(filePath)
} catch {
return false
}
}
// 安全执行命令
function safeExec(command, options = {}) {
try {
execSync(command, { stdio: 'inherit', ...options })
return true
} catch (error) {
return false
}
}
// 检查环境变量
function checkEnvironment() {
logStep('🔍', '检查环境配置...')
const requiredEnvVars = ['DATABASE_URL']
const missingVars = []
requiredEnvVars.forEach((varName) => {
if (!process.env[varName]) {
missingVars.push(varName)
}
})
if (missingVars.length > 0) {
logError(`缺少必需的环境变量: ${missingVars.join(', ')}`)
logError('请通过以下方式之一设置环境变量:')
logError('1. 创建 .env 文件并配置 DATABASE_URL')
logError('2. 使用 docker run -e DATABASE_URL=xxx 传递环境变量')
logError('3. 在 docker-compose.yml 中配置 environment')
throw new Error('环境变量配置不完整')
} else {
logSuccess('环境变量检查通过')
}
return true
}
// 主部署流程
async function deploy() {
log('🚀 开始部署...', 'cyan')
try {
// 0. 检查环境(必须通过)
checkEnvironment()
// 1. 安装依赖
if (process.env.SKIP_INSTALL === 'true') {
logStep('📦', '跳过依赖安装 (SKIP_INSTALL=true)...')
} else {
logStep('📦', '安装依赖...')
let installed = false
// 优先尝试 pnpm install --frozen-lockfile
if (fileExists('pnpm-lock.yaml')) {
log('尝试使用 pnpm install --frozen-lockfile 安装...', 'cyan')
if (safeExec('pnpm install --frozen-lockfile')) {
installed = true
logSuccess('依赖安装完成 (pnpm install --frozen-lockfile)')
} else {
logWarning('pnpm install --frozen-lockfile 安装失败,准备回退到 pnpm install...')
}
} else {
logWarning('未检测到 pnpm-lock.yaml,跳过冻结锁文件安装,直接使用 pnpm install...')
}
// 如果冻结锁文件安装没运行或失败,使用 pnpm install
if (!installed) {
log('正在使用 pnpm install 安装...', 'cyan')
if (!safeExec('pnpm install')) {
throw new Error('依赖安装失败')
}
logSuccess('依赖安装完成 (pnpm install)')
}
}
// 2. 检查 Drizzle 配置
if (
!fileExists('drizzle.config.ts') ||
!fileExists('app/drizzle/schema.ts') ||
!fileExists('app/drizzle/db.ts')
) {
throw new Error('Drizzle 配置文件不完整')
}
// 2.1. 确保迁移目录存在
if (!fileExists('app/drizzle/migrations')) {
fs.mkdirSync('app/drizzle/migrations', { recursive: true })
}
// 3. 数据库同步
logStep('🗄️', '同步数据库...')
let dbSyncSuccess = false
if (process.env.DATABASE_URL) {
const nonInteractiveEnv = {
...process.env,
DRIZZLE_KIT_FORCE: 'true',
CI: 'true',
NODE_ENV: 'production'
}
if (safeExec('node scripts/db-sync.js', { env: nonInteractiveEnv })) {
logSuccess('数据库同步成功')
dbSyncSuccess = true
} else {
logWarning('数据库同步失败')
}
} else {
logWarning('未设置 DATABASE_URL,跳过数据库迁移')
}
// 4. 创建管理员账户
if (fileExists('scripts/create-admin.js') && dbSyncSuccess) {
logStep('👤', '检查管理员账户...')
safeExec('pnpm run create-admin')
}
// 5. 构建应用
if (process.env.SKIP_BUILD === 'true') {
logStep('🔨', '跳过应用构建 (SKIP_BUILD=true)...')
} else {
logStep('🔨', '构建应用...')
const buildEnv = {
...process.env,
NODE_OPTIONS: process.env.NODE_OPTIONS || DEFAULT_NODE_OPTIONS
}
if (!safeExec('pnpm exec nuxt build', { env: buildEnv })) {
throw new Error('应用构建失败')
}
logSuccess('应用构建完成')
}
log('🎉 部署完成!', 'green')
} catch (error) {
logError(`部署失败: ${error.message}`)
process.exit(1)
}
}
// 运行部署
deploy().catch((error) => {
logError(`未预期的错误: ${error.message}`)
process.exit(1)
})
@@ -0,0 +1 @@
{ "version": "7", "dialect": "postgresql", "entries": [] }
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env node
import { spawn } from 'child_process';
import os from 'os';
// 内存限制配置(4GB总内存)
const memoryLimits = [
{ nodeOptions: '--max-old-space-size=1536', viteThreads: 1, name: '第一次尝试' },
{ nodeOptions: '--max-old-space-size=1280', viteThreads: 1, name: '第二次尝试' },
{ nodeOptions: '--max-old-space-size=1024', viteThreads: 1, name: '第三次尝试' }
];
async function runBuild(config) {
console.log(`🚀 ${config.name}: 内存限制 ${config.nodeOptions}`);
return new Promise((resolve, reject) => {
const env = {
...process.env,
NODE_OPTIONS: config.nodeOptions,
VITE_BUILD_THREADS: config.viteThreads,
NUXT_BUILD_PARALLEL: 'false',
NUXT_SSR: 'true',
VITE_PLUGIN_VUE_MAX_PARALLEL: '1',
VITE_BUILD_MINIFY: 'esbuild',
VITE_SSR_MANIFEST: 'false',
VITE_SOURCEMAP: 'false'
};
const buildProcess = spawn('npm', ['run', 'build'], {
env,
stdio: 'inherit',
shell: true
});
buildProcess.on('close', (code) => {
if (code === 0) {
console.log(`✅ ${config.name}: 构建成功!`);
resolve(true);
} else {
console.log(`❌ ${config.name}: 构建失败,退出码 ${code}`);
resolve(false);
}
});
buildProcess.on('error', (err) => {
console.log(`❌ ${config.name}: 构建错误`, err.message);
resolve(false);
});
// 10分钟超时
setTimeout(() => {
buildProcess.kill();
console.log(`⏰ ${config.name}: 构建超时`);
resolve(false);
}, 600000);
});
}
async function main() {
console.log('🎯 开始内存优化构建(4GB限制)');
console.log(`💻 系统内存: ${Math.round(os.totalmem() / 1024 / 1024 / 1024)}GB`);
for (let i = 0; i < memoryLimits.length; i++) {
const success = await runBuild(memoryLimits[i]);
if (success) {
console.log('🎉 构建完成!');
process.exit(0);
}
if (i < memoryLimits.length - 1) {
console.log('🔄 尝试更保守的设置...');
await new Promise(resolve => setTimeout(resolve, 5000)); // 等待5秒
}
}
console.log('💥 所有构建尝试都失败了');
console.log('💡 建议:');
console.log(' 1. 增加Docker内存限制');
console.log(' 2. 使用更强大的构建机器');
console.log(' 3. 考虑在外部构建后复制结果');
process.exit(1);
}
main().catch(console.error);
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env node
import { execSync } from 'child_process'
import fs from 'fs'
// 颜色输出
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
cyan: '\x1b[36m'
}
const BUILD_MEMORY_MB = 6144
const DEFAULT_NODE_OPTIONS = `--max-old-space-size=${BUILD_MEMORY_MB}`
function log(message, color = 'reset') {
console.log(`${colors[color]}${message}${colors.reset}`)
}
function logStep(step, message) {
log(`${step} ${message}`, 'cyan')
}
function logSuccess(message) {
log(`✅ ${message}`, 'green')
}
function logWarning(message) {
log(`⚠️ ${message}`, 'yellow')
}
function logError(message) {
log(`❌ ${message}`, 'red')
}
// 安全执行命令
function safeExec(command, options = {}) {
try {
execSync(command, { stdio: 'inherit', ...options })
return true
} catch (error) {
logError(`命令执行失败: ${command}`)
logError(error.message)
return false
}
}
// 检查文件是否存在
function fileExists(filePath) {
try {
return fs.existsSync(filePath)
} catch {
return false
}
}
// Netlify 构建流程
async function netlifyBuild() {
log('🚀 Netlify 构建', 'cyan')
try {
// 1. 设置环境变量
process.env.NETLIFY = 'true'
process.env.NITRO_PRESET = 'netlify'
// 2. 清理构建目录
logStep('🧹', '清理构建目录...')
if (fileExists('dist')) safeExec('rm -rf dist')
if (fileExists('.netlify')) safeExec('rm -rf .netlify')
if (fileExists('.nuxt')) safeExec('rm -rf .nuxt')
logSuccess('清理完成')
// 3. 安装依赖
logStep('📦', '安装依赖...')
let installed = false
if (fileExists('node_modules')) {
safeExec('rm -rf node_modules')
}
if (fileExists('pnpm-lock.yaml')) {
if (safeExec('pnpm install --frozen-lockfile')) {
installed = true
logSuccess('依赖安装完成 (pnpm install --frozen-lockfile)')
} else {
logWarning('pnpm install --frozen-lockfile 安装失败,准备回退到 pnpm install...')
}
} else {
logWarning('未检测到 pnpm-lock.yaml,跳过冻结锁文件安装,直接使用 pnpm install...')
}
if (!installed) {
if (!safeExec('pnpm install')) {
throw new Error('依赖安装失败')
}
logSuccess('依赖安装完成 (pnpm install)')
}
// 验证 Drizzle 依赖
if (!safeExec('pnpm list drizzle-orm drizzle-kit')) {
if (!safeExec('pnpm add drizzle-orm drizzle-kit')) {
throw new Error('Drizzle 依赖安装失败')
}
}
// 4. 检查 Drizzle 配置
if (!fileExists('drizzle.config.ts') || !fileExists('app/drizzle/schema.ts')) {
throw new Error('Drizzle 配置文件不完整')
}
// 5. 确保迁移目录存在
if (!fileExists('app/drizzle/migrations')) {
fs.mkdirSync('app/drizzle/migrations', { recursive: true })
}
// 6. 数据库同步
if (process.env.DATABASE_URL) {
logStep('�️', '同步数据库...')
const env = { ...process.env, CI: 'true', DRIZZLE_KIT_FORCE: 'true', NODE_ENV: 'production' }
if (safeExec('node scripts/db-sync.js', { env })) {
logSuccess('数据库同步成功')
} else {
logWarning('数据库同步失败')
}
// 检查管理员账户
if (fileExists('scripts/create-admin.js')) {
logStep('👤', '检查管理员账户...')
safeExec('pnpm run create-admin', { env })
}
} else {
logWarning('未设置 DATABASE_URL')
}
// 7. 构建应用
logStep('🔨', '构建应用...')
const buildEnv = {
...process.env,
NODE_OPTIONS: process.env.NODE_OPTIONS || DEFAULT_NODE_OPTIONS
}
if (!safeExec('pnpm exec nuxt build', { env: buildEnv })) {
throw new Error('构建失败')
}
logSuccess('构建完成')
// 8. 验证构建输出
const hasNetlifyFunctions = fileExists('.netlify/functions-internal/server')
const hasOutputPublic = fileExists('.output/public')
if (hasNetlifyFunctions) {
logSuccess('Netlify Functions 生成成功')
}
if (hasOutputPublic) {
logSuccess('静态资源生成成功')
}
if (!hasNetlifyFunctions && !hasOutputPublic) {
throw new Error('构建输出目录不存在')
}
log('🎉 构建完成!', 'green')
} catch (error) {
logError(`构建失败: ${error.message}`)
process.exit(1)
}
}
// 运行构建
netlifyBuild().catch((error) => {
logError(`未预期的错误: ${error.message}`)
process.exit(1)
})
+23
View File
@@ -0,0 +1,23 @@
{
"name": "voicehub-scripts",
"version": "1.0.0",
"description": "VoiceHub管理脚本",
"packageManager": "pnpm@10.29.3",
"type": "module",
"scripts": {
"create-admin": "pnpm exec tsx create-admin.js",
"clear-db": "pnpm exec tsx clear-database.js",
"safe-migrate": "pnpm exec tsx safe-migrate.js",
"db:generate": "cd .. && pnpm exec drizzle-kit generate --config=drizzle.config.ts",
"db:migrate": "cd .. && pnpm exec drizzle-kit migrate --config=drizzle.config.ts"
},
"dependencies": {
"bcrypt": "^5.1.1",
"chalk": "^5.3.0",
"dotenv": "^16.4.5",
"prompts": "^2.4.2",
"drizzle-orm": "^0.36.4",
"postgres": "^3.4.5",
"tsx": "^4.7.0"
}
}
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env node
import fs from 'fs'
const isCI = process.env.CI || process.env.NETLIFY || process.env.VERCEL
if (isCI) {
console.log('🔧 CI 环境检测')
try {
if (fs.existsSync('drizzle.config.ts') && fs.existsSync('app/drizzle/schema.ts')) {
console.log('✅ Drizzle 配置正常')
} else {
console.warn('⚠️ Drizzle 配置不完整')
}
} catch (error) {
console.error('❌ 配置检查失败:', error.message)
}
} else {
console.log('📝 本地开发环境')
}
+241
View File
@@ -0,0 +1,241 @@
#!/usr/bin/env node
import { execSync } from 'child_process'
import fs from 'fs'
import path from 'path'
import { config } from 'dotenv'
// 加载环境变量
config({ path: path.resolve(process.cwd(), '.env') })
// 颜色输出函数
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
cyan: '\x1b[36m'
}
function log(message, color = 'reset') {
console.log(`${colors[color]}${message}${colors.reset}`)
}
function logSuccess(message) {
log(`✅ ${message}`, 'green')
}
function logWarning(message) {
log(`⚠️ ${message}`, 'yellow')
}
function logError(message) {
log(`❌ ${message}`, 'red')
}
// 检查环境变量
if (!process.env.DATABASE_URL) {
logError('DATABASE_URL 环境变量未设置')
process.exit(1)
}
// 安全执行命令
function safeExec(command, options = {}) {
try {
execSync(command, { stdio: 'inherit', ...options })
return true
} catch (error) {
logError(`命令执行失败: ${command}`)
logError(error.message)
return false
}
}
// 检查文件是否存在
function fileExists(filePath) {
try {
return fs.existsSync(filePath)
} catch {
return false
}
}
// 处理数据冲突的函数
async function handleDataConflicts() {
try {
// 在Vercel环境中,我们主要关注schema同步而不是数据冲突处理
log('检查数据库连接...', 'cyan')
// 简单的连接测试
if (!safeExec('cd .. && pnpm exec drizzle-kit check --config=drizzle.config.ts', { stdio: 'pipe' })) {
logWarning('数据库schema检查失败,将尝试强制同步')
}
logSuccess('数据冲突检查完成')
} catch (error) {
logWarning(`数据冲突处理警告: ${error.message}`)
}
}
async function safeMigrate() {
log('🔄 开始安全数据库迁移流程...', 'bright')
try {
// 获取项目根目录路径
const projectRoot = path.resolve(process.cwd(), '..')
const drizzleConfigPath = path.join(projectRoot, 'drizzle.config.ts')
const schemaPath = path.join(projectRoot, 'app/drizzle/schema.ts')
const migrationsPath = path.join(projectRoot, 'app/drizzle/migrations')
// 1. 确保drizzle配置存在
if (!fileExists(drizzleConfigPath)) {
throw new Error(`drizzle.config.ts 配置文件不存在: ${drizzleConfigPath}`)
}
// 2. 检查schema文件
if (!fileExists(schemaPath)) {
throw new Error(`app/drizzle/schema.ts 文件不存在: ${schemaPath}`)
}
// 3. 创建迁移目录(如果不存在)
if (!fileExists(migrationsPath)) {
log('创建迁移目录...', 'cyan')
fs.mkdirSync(migrationsPath, { recursive: true })
}
// 自动执行 generate 命令并处理交互
async function runGenerateWithAutoConfirm(env) {
return new Promise((resolve) => {
const { spawn } = require('child_process')
const child = spawn('pnpm', ['run', 'db:generate'], {
env,
shell: true
})
// 监听输出,透传给用户,并自动响应提示
child.stdout.on('data', (data) => {
process.stdout.write(data)
const str = data.toString()
// 如果检测到交互提示(通常包含问号或选项),自动发送回车
if (str.includes('?') || str.includes('renamed') || str.includes('created')) {
try {
// 发送回车以选择默认选项(通常是 Created)
child.stdin.write('\n')
} catch (e) {
// 忽略写入错误
}
}
})
child.stderr.on('data', (data) => {
process.stderr.write(data)
})
child.on('close', (code) => {
resolve(code === 0)
})
child.on('error', () => {
resolve(false)
})
})
}
// 4. 生成迁移文件(如果需要)
log('生成数据库迁移文件...', 'cyan')
// 设置非交互式环境变量
const nonInteractiveEnv = {
...process.env,
DRIZZLE_KIT_FORCE: 'true',
CI: 'true',
NODE_ENV: 'production'
}
// 使用新的自动确认函数
const generateSuccess = await runGenerateWithAutoConfirm(nonInteractiveEnv)
if (!generateSuccess) {
logWarning('迁移文件生成可能遇到问题,尝试继续...')
} else {
logSuccess('迁移文件生成完成')
}
// 5. 预处理数据冲突
log('🔍 检查并处理数据冲突...', 'cyan')
await handleDataConflicts()
// 6. 检查是否为全新部署(数据库为空)
log('📋 检查数据库状态...', 'cyan')
// 设置非交互式环境变量
const env = {
...process.env,
DRIZZLE_KIT_FORCE: 'true',
CI: 'true',
NODE_ENV: 'production'
}
// 检查数据库是否有任何表(基于 introspect 输出更稳健的判断)
let isEmptyDatabase = false
try {
const checkResult = execSync(
'cd .. && pnpm exec drizzle-kit introspect --config=drizzle.config.ts',
{
stdio: 'pipe',
env,
encoding: 'utf8'
}
)
// 优先解析 "<n> tables" 提示,其次检查是否包含 columns 索引等摘要
const tablesMatch = checkResult.match(/(\d+)\s+tables/i)
const hasTablesCount = tablesMatch && Number(tablesMatch[1]) > 0
const listsTables = /\bcolumns\b|\bindexes\b|\bfks\b/i.test(checkResult)
// 数据库为空当且仅当:统计为0且没有任何表摘要
isEmptyDatabase = !(hasTablesCount || listsTables)
} catch (error) {
// introspect 失败时不轻易判定为空库,改为保守:视为非空库,走 push 路径
logWarning('数据库状态检查失败,按非空库处理以避免重复建表')
isEmptyDatabase = false
}
if (isEmptyDatabase) {
log('🆕 检测到全新部署,执行标准迁移...', 'cyan')
// 对于全新部署,直接使用migrate避免交互式提示
if (!safeExec('cd .. && pnpm run db:migrate', { env })) {
throw new Error('数据库迁移失败')
}
logSuccess('全新数据库迁移成功')
} else {
log('🔄 检测到现有数据库,执行schema同步...', 'cyan')
// 对于现有数据库,使用push进行增量更新
if (safeExec('cd .. && pnpm exec drizzle-kit push --force --config=drizzle.config.ts', { env })) {
logSuccess('数据库schema同步成功')
} else {
logWarning('schema同步失败,尝试标准迁移...')
// 7. 执行迁移(作为后备)
if (!safeExec('cd .. && pnpm run db:migrate', { env })) {
throw new Error('数据库迁移完全失败')
}
logSuccess('数据库迁移成功')
}
}
// 8. 验证迁移结果
log('✅ 数据库迁移流程完成!', 'green')
} catch (error) {
logError(`迁移失败: ${error.message}`)
logError('请检查数据库连接和迁移文件')
process.exit(1)
}
}
// 运行迁移
safeMigrate().catch((error) => {
logError(`未预期的错误: ${error.message}`)
process.exit(1)
})