#!/usr/bin/env node const { spawn, exec } = require('child_process') const fs = require('fs') const path = require('path') const process = require('process') const PID_FILE = path.join(__dirname, '..', 'claude-relay-service.pid') const LOG_FILE = path.join(__dirname, '..', 'logs', 'service.log') const ERROR_LOG_FILE = path.join(__dirname, '..', 'logs', 'service-error.log') const APP_FILE = path.join(__dirname, '..', 'src', 'app.js') class ServiceManager { constructor() { this.ensureLogDir() } ensureLogDir() { const logDir = path.dirname(LOG_FILE) if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir, { recursive: true }) } } getPid() { try { if (fs.existsSync(PID_FILE)) { const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim()) return pid } } catch (error) { console.error('读取PID文件失败:', error.message) } return null } isProcessRunning(pid) { try { process.kill(pid, 0) return true } catch (error) { return false } } writePid(pid) { try { fs.writeFileSync(PID_FILE, pid.toString()) console.log(`✅ PID ${pid} 已保存到 ${PID_FILE}`) } catch (error) { console.error('写入PID文件失败:', error.message) } } removePidFile() { try { if (fs.existsSync(PID_FILE)) { fs.unlinkSync(PID_FILE) console.log('🗑️ 已清理PID文件') } } catch (error) { console.error('清理PID文件失败:', error.message) } } getStatus() { const pid = this.getPid() if (pid && this.isProcessRunning(pid)) { return { running: true, pid } } return { running: false, pid: null } } start(daemon = false) { const status = this.getStatus() if (status.running) { console.log(`⚠️ 服务已在运行中 (PID: ${status.pid})`) return false } console.log('🚀 启动 Claude Relay Service...') if (daemon) { // 后台运行模式 - 使用nohup实现真正的后台运行 const { exec: execChild } = require('child_process') const command = `nohup node "${APP_FILE}" > "${LOG_FILE}" 2> "${ERROR_LOG_FILE}" & echo $!` execChild(command, (error, stdout) => { if (error) { console.error('❌ 后台启动失败:', error.message) return } const pid = parseInt(stdout.trim()) if (pid && !isNaN(pid)) { this.writePid(pid) console.log(`🔄 服务已在后台启动 (PID: ${pid})`) console.log(`📝 日志文件: ${LOG_FILE}`) console.log(`❌ 错误日志: ${ERROR_LOG_FILE}`) console.log('✅ 终端现在可以安全关闭') } else { console.error('❌ 无法获取进程ID') } }) // 给exec一点时间执行 setTimeout(() => { process.exit(0) }, 1000) } else { // 前台运行模式 const child = spawn('node', [APP_FILE], { stdio: 'inherit' }) console.log(`🔄 服务已启动 (PID: ${child.pid})`) this.writePid(child.pid) // 监听进程退出 child.on('exit', (code, signal) => { this.removePidFile() if (code !== 0) { console.log(`💥 进程退出 (代码: ${code}, 信号: ${signal})`) } }) child.on('error', (error) => { console.error('❌ 启动失败:', error.message) this.removePidFile() }) } return true } stop() { const status = this.getStatus() if (!status.running) { console.log('⚠️ 服务未在运行') this.removePidFile() // 清理可能存在的过期PID文件 return false } console.log(`🛑 停止服务 (PID: ${status.pid})...`) try { // 优雅关闭:先发送SIGTERM process.kill(status.pid, 'SIGTERM') // 等待进程退出 let attempts = 0 const maxAttempts = 30 // 30秒超时 const checkExit = setInterval(() => { attempts++ if (!this.isProcessRunning(status.pid)) { clearInterval(checkExit) console.log('✅ 服务已停止') this.removePidFile() return } if (attempts >= maxAttempts) { clearInterval(checkExit) console.log('⚠️ 优雅关闭超时,强制终止进程...') try { process.kill(status.pid, 'SIGKILL') console.log('✅ 服务已强制停止') } catch (error) { console.error('❌ 强制停止失败:', error.message) } this.removePidFile() } }, 1000) } catch (error) { console.error('❌ 停止服务失败:', error.message) this.removePidFile() return false } return true } restart(daemon = false) { console.log('🔄 重启服务...') this.stop() // 等待停止完成 setTimeout(() => { this.start(daemon) }, 2000) return true } status() { const status = this.getStatus() if (status.running) { console.log(`✅ 服务正在运行 (PID: ${status.pid})`) // 显示进程信息 exec(`ps -p ${status.pid} -o pid,ppid,pcpu,pmem,etime,cmd --no-headers`, (error, stdout) => { if (!error && stdout.trim()) { console.log('\n📊 进程信息:') console.log('PID\tPPID\tCPU%\tMEM%\tTIME\t\tCOMMAND') console.log(stdout.trim()) } }) } else { console.log('❌ 服务未运行') } return status.running } logs(lines = 50) { console.log(`📖 最近 ${lines} 行日志:\n`) exec(`tail -n ${lines} ${LOG_FILE}`, (error, stdout) => { if (error) { console.error('读取日志失败:', error.message) return } console.log(stdout) }) } help() { console.log(` 🔧 Claude Relay Service 进程管理器 用法: npm run service [options] 重要提示: 如果要传递参数,请在npm run命令中使用 -- 分隔符 npm run service -- [options] 命令: start [-d|--daemon] 启动服务 (-d: 后台运行) stop 停止服务 restart [-d|--daemon] 重启服务 (-d: 后台运行) status 查看服务状态 logs [lines] 查看日志 (默认50行) help 显示帮助信息 命令缩写: s, start 启动服务 r, restart 重启服务 st, status 查看状态 l, log, logs 查看日志 halt, stop 停止服务 h, help 显示帮助 示例: npm run service start # 前台启动 npm run service -- start -d # 后台启动(正确方式) npm run service:start:d # 后台启动(推荐快捷方式) npm run service:daemon # 后台启动(推荐快捷方式) npm run service stop # 停止服务 npm run service -- restart -d # 后台重启(正确方式) npm run service:restart:d # 后台重启(推荐快捷方式) npm run service status # 查看状态 npm run service logs # 查看日志 npm run service -- logs 100 # 查看最近100行日志 推荐的快捷方式(无需 -- 分隔符): npm run service:start:d # 等同于 npm run service -- start -d npm run service:restart:d # 等同于 npm run service -- restart -d npm run service:daemon # 等同于 npm run service -- start -d 直接使用脚本(推荐): node scripts/manage.js start -d # 后台启动 node scripts/manage.js restart -d # 后台重启 node scripts/manage.js status # 查看状态 node scripts/manage.js logs 100 # 查看最近100行日志 文件位置: PID文件: ${PID_FILE} 日志文件: ${LOG_FILE} 错误日志: ${ERROR_LOG_FILE} `) } } // 主程序 function main() { const manager = new ServiceManager() const args = process.argv.slice(2) const command = args[0] const isDaemon = args.includes('-d') || args.includes('--daemon') switch (command) { case 'start': case 's': manager.start(isDaemon) break case 'stop': case 'halt': manager.stop() break case 'restart': case 'r': manager.restart(isDaemon) break case 'status': case 'st': manager.status() break case 'logs': case 'log': case 'l': { const lines = parseInt(args[1]) || 50 manager.logs(lines) break } case 'help': case '--help': case '-h': case 'h': manager.help() break default: console.log('❌ 未知命令:', command) manager.help() process.exit(1) } } if (require.main === module) { main() } module.exports = ServiceManager