mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 19:42:50 +00:00
first commit
This commit is contained in:
335
scripts/manage.js
Normal file
335
scripts/manage.js
Normal file
@@ -0,0 +1,335 @@
|
||||
#!/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 } = require('child_process');
|
||||
|
||||
const command = `nohup node "${APP_FILE}" > "${LOG_FILE}" 2> "${ERROR_LOG_FILE}" & echo $!`;
|
||||
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
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('🔄 重启服务...');
|
||||
const stopResult = 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 <command> [options]
|
||||
|
||||
重要提示:
|
||||
如果要传递参数,请在npm run命令中使用 -- 分隔符
|
||||
npm run service <command> -- [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;
|
||||
107
scripts/setup.js
Normal file
107
scripts/setup.js
Normal file
@@ -0,0 +1,107 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const chalk = require('chalk');
|
||||
const ora = require('ora');
|
||||
|
||||
const config = require('../config/config');
|
||||
|
||||
async function setup() {
|
||||
console.log(chalk.blue.bold('\n🚀 Claude Relay Service 初始化设置\n'));
|
||||
|
||||
const spinner = ora('正在进行初始化设置...').start();
|
||||
|
||||
try {
|
||||
// 1. 创建必要目录
|
||||
const directories = [
|
||||
'logs',
|
||||
'data',
|
||||
'temp'
|
||||
];
|
||||
|
||||
directories.forEach(dir => {
|
||||
const dirPath = path.join(__dirname, '..', dir);
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 生成环境配置文件
|
||||
if (!fs.existsSync(path.join(__dirname, '..', '.env'))) {
|
||||
const envTemplate = fs.readFileSync(path.join(__dirname, '..', '.env.example'), 'utf8');
|
||||
|
||||
// 生成随机密钥
|
||||
const jwtSecret = crypto.randomBytes(64).toString('hex');
|
||||
const encryptionKey = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
const envContent = envTemplate
|
||||
.replace('your-jwt-secret-here', jwtSecret)
|
||||
.replace('your-encryption-key-here', encryptionKey);
|
||||
|
||||
fs.writeFileSync(path.join(__dirname, '..', '.env'), envContent);
|
||||
}
|
||||
|
||||
// 3. 生成随机管理员凭据
|
||||
const adminUsername = `cr_admin_${crypto.randomBytes(4).toString('hex')}`;
|
||||
const adminPassword = crypto.randomBytes(16).toString('base64').replace(/[^a-zA-Z0-9]/g, '').substring(0, 16);
|
||||
|
||||
// 4. 创建初始化完成标记文件
|
||||
const initData = {
|
||||
initializedAt: new Date().toISOString(),
|
||||
adminUsername,
|
||||
adminPassword,
|
||||
version: '1.0.0'
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, '..', 'data', 'init.json'),
|
||||
JSON.stringify(initData, null, 2)
|
||||
);
|
||||
|
||||
spinner.succeed('初始化设置完成');
|
||||
|
||||
console.log(chalk.green('\n✅ 设置完成!\n'));
|
||||
console.log(chalk.yellow('📋 重要信息:\n'));
|
||||
console.log(` 管理员用户名: ${chalk.cyan(adminUsername)}`);
|
||||
console.log(` 管理员密码: ${chalk.cyan(adminPassword)}`);
|
||||
console.log(chalk.red('\n⚠️ 请立即保存这些凭据!首次登录后建议修改密码。\n'));
|
||||
|
||||
console.log(chalk.blue('🚀 启动服务:\n'));
|
||||
console.log(' npm start - 启动生产服务');
|
||||
console.log(' npm run dev - 启动开发服务');
|
||||
console.log(' npm run cli admin - 管理员CLI工具\n');
|
||||
|
||||
console.log(chalk.blue('🌐 访问地址:\n'));
|
||||
console.log(` Web管理界面: http://localhost:${config.server.port}/web`);
|
||||
console.log(` API端点: http://localhost:${config.server.port}/api/v1/messages`);
|
||||
console.log(` 健康检查: http://localhost:${config.server.port}/health\n`);
|
||||
|
||||
} catch (error) {
|
||||
spinner.fail('初始化设置失败');
|
||||
console.error(chalk.red('❌ 错误:'), error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否已初始化
|
||||
function checkInitialized() {
|
||||
const initFile = path.join(__dirname, '..', 'data', 'init.json');
|
||||
if (fs.existsSync(initFile)) {
|
||||
const initData = JSON.parse(fs.readFileSync(initFile, 'utf8'));
|
||||
console.log(chalk.yellow('⚠️ 服务已经初始化过了!'));
|
||||
console.log(` 初始化时间: ${new Date(initData.initializedAt).toLocaleString()}`);
|
||||
console.log(` 管理员用户名: ${initData.adminUsername}`);
|
||||
console.log('\n如需重新初始化,请删除 data/init.json 文件。');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
if (!checkInitialized()) {
|
||||
setup();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { setup, checkInitialized };
|
||||
Reference in New Issue
Block a user