import { Client } from "ssh2"; import { readFileSync, readdirSync, statSync } from "fs"; import { join, relative } from "path"; const SSH_CONFIG = { host: "119.45.10.34", port: 22, username: "root", password: "#xyzh%CS#2512@28", readyTimeout: 10000, }; function sshExec(command, timeout = 60000) { return new Promise((resolve, reject) => { const conn = new Client(); let stdout = ""; let stderr = ""; let timer = setTimeout(() => { conn.end(); resolve({ stdout, stderr: stderr + "\n[TIMEOUT]", code: -1 }); }, timeout); conn.on("ready", () => { conn.exec(command, (err, stream) => { if (err) { clearTimeout(timer); conn.end(); return reject(err); } stream.on("close", (code) => { clearTimeout(timer); conn.end(); resolve({ stdout, stderr, code }); }); stream.on("data", (d) => { stdout += d.toString(); }); stream.stderr.on("data", (d) => { stderr += d.toString(); }); }); }).on("error", (err) => { clearTimeout(timer); reject(err); }).connect(SSH_CONFIG); }); } function sshUpload(localPath, remotePath) { return new Promise((resolve, reject) => { const conn = new Client(); conn.on("ready", () => { conn.sftp((err, sftp) => { if (err) { conn.end(); return reject(err); } const content = readFileSync(localPath); const ws = sftp.createWriteStream(remotePath); ws.on("close", () => { conn.end(); resolve(); }); ws.on("error", (e) => { conn.end(); reject(e); }); ws.write(content); ws.end(); }); }).on("error", (err) => reject(err)).connect(SSH_CONFIG); }); } function sshUploadDir(localDir, remoteDir) { return new Promise((resolve, reject) => { const conn = new Client(); conn.on("ready", () => { conn.sftp((err, sftp) => { if (err) { conn.end(); return reject(err); } const files = []; function walkDir(dir, baseDir) { const items = readdirSync(dir); for (const item of items) { const fullPath = join(dir, item); const stat = statSync(fullPath); if (stat.isFile()) { files.push({ local: fullPath, remote: join(remoteDir, relative(baseDir, fullPath)), }); } } } walkDir(localDir, localDir); let uploaded = 0; const total = files.length; if (total === 0) { conn.end(); resolve([]); return; } const results = []; for (const file of files) { const content = readFileSync(file.local); const ws = sftp.createWriteStream(file.remote); ws.on("close", () => { results.push({ local: file.local, remote: file.remote, success: true }); uploaded++; if (uploaded === total) { conn.end(); resolve(results); } }); ws.on("error", (e) => { results.push({ local: file.local, remote: file.remote, success: false, error: e.message }); uploaded++; if (uploaded === total) { conn.end(); resolve(results); } }); ws.write(content); ws.end(); } }); }).on("error", (err) => reject(err)).connect(SSH_CONFIG); }); } const PROJECT = "/www/wwwroot/demo.tensorgrove.com.cn"; const LOCAL_BASE = "C:\\Users\\UI\\Desktop\\bigwo\\test2"; async function main() { console.log("========================================"); console.log(" BigWo Test2 同步部署脚本"); console.log(" 目标服务器: " + SSH_CONFIG.host); console.log(" 项目路径: " + PROJECT); console.log("========================================\n"); const serverFiles = [ { local: "server\\app.js", remote: `${PROJECT}/server/app.js` }, { local: "server\\package.json", remote: `${PROJECT}/server/package.json` }, { local: "server\\package-lock.json", remote: `${PROJECT}/server/package-lock.json` }, { local: "server\\routes\\chat.js", remote: `${PROJECT}/server/routes/chat.js` }, { local: "server\\routes\\session.js", remote: `${PROJECT}/server/routes/session.js` }, { local: "server\\routes\\voice.js", remote: `${PROJECT}/server/routes/voice.js` }, { local: "server\\services\\arkChatService.js", remote: `${PROJECT}/server/services/arkChatService.js` }, { local: "server\\services\\cozeChatService.js", remote: `${PROJECT}/server/services/cozeChatService.js` }, { local: "server\\services\\nativeVoiceGateway.js", remote: `${PROJECT}/server/services/nativeVoiceGateway.js` }, { local: "server\\services\\realtimeDialogProtocol.js", remote: `${PROJECT}/server/services/realtimeDialogProtocol.js` }, { local: "server\\services\\realtimeDialogRouting.js", remote: `${PROJECT}/server/services/realtimeDialogRouting.js` }, { local: "server\\services\\toolExecutor.js", remote: `${PROJECT}/server/services/toolExecutor.js` }, { local: "server\\config\\tools.js", remote: `${PROJECT}/server/config/tools.js` }, { local: "server\\db\\index.js", remote: `${PROJECT}/server/db/index.js` }, ]; const clientFiles = [ { local: "client\\dist\\index.html", remote: `${PROJECT}/client/dist/index.html` }, ]; const localAssetsDir = join(LOCAL_BASE, "client", "dist", "assets"); const assetNames = readdirSync(localAssetsDir).filter((name) => statSync(join(localAssetsDir, name)).isFile()); assetNames.forEach((name) => { clientFiles.push({ local: `client\\dist\\assets\\${name}`, remote: `${PROJECT}/client/dist/assets/${name}`, }); }); console.log("=== 1. 检查服务器状态 ==="); const pm2Check = await sshExec("pm2 list 2>&1 | head -20"); console.log(pm2Check.stdout); console.log("\n=== 2. 创建备份 ==="); const backupDir = `${PROJECT}/_backup_${Date.now()}`; const backupResult = await sshExec(`mkdir -p ${backupDir}/server ${backupDir}/client/dist/assets`); console.log(`备份目录: ${backupDir}`); const backupFiles = [ `${PROJECT}/server/app.js`, `${PROJECT}/server/package.json`, `${PROJECT}/server/package-lock.json`, `${PROJECT}/server/routes/chat.js`, `${PROJECT}/server/routes/session.js`, `${PROJECT}/server/routes/voice.js`, `${PROJECT}/server/services/arkChatService.js`, `${PROJECT}/server/services/cozeChatService.js`, `${PROJECT}/server/services/nativeVoiceGateway.js`, `${PROJECT}/server/services/realtimeDialogProtocol.js`, `${PROJECT}/server/services/realtimeDialogRouting.js`, `${PROJECT}/server/services/toolExecutor.js`, `${PROJECT}/server/config/tools.js`, `${PROJECT}/server/db/index.js`, `${PROJECT}/client/dist/index.html`, ]; for (const file of backupFiles) { const backupCmd = `cp ${file} ${backupDir}/ 2>/dev/null || echo "skip"`; await sshExec(backupCmd); } console.log("备份完成"); console.log("\n=== 2b. 删除已废弃的 RTC 文件 ==="); const removeRtc = await sshExec(`rm -f ${PROJECT}/server/services/volcengine.js ${PROJECT}/server/config/voiceChatConfig.js ${PROJECT}/server/lib/token.js 2>&1`); console.log("已清理远程 RTC 残留文件"); console.log("\n=== 3. 同步服务端代码 ==="); for (const { local, remote } of serverFiles) { const localPath = join(LOCAL_BASE, local); try { await sshUpload(localPath, remote); console.log(` ✅ ${local} → ${remote}`); } catch (e) { console.error(` ❌ ${local}: ${e.message}`); } } console.log("\n=== 4. 同步前端构建产物 ==="); await sshExec(`mkdir -p ${PROJECT}/client/dist/assets && find ${PROJECT}/client/dist/assets -maxdepth 1 -type f -delete`); for (const { local, remote } of clientFiles) { const localPath = join(LOCAL_BASE, local); try { await sshUpload(localPath, remote); console.log(` ✅ ${local} → ${remote}`); } catch (e) { console.error(` ❌ ${local}: ${e.message}`); } } console.log("\n=== 5. 重启 PM2 服务 ==="); console.log("\n=== 5a. 安装服务端依赖 ==="); const install = await sshExec(`cd ${PROJECT}/server && npm install --production`, 180000); console.log(install.stdout || install.stderr); console.log("\n=== 5b. 重启 PM2 服务 ==="); const restart = await sshExec("pm2 restart bigwo-server 2>&1"); console.log(restart.stdout); console.log("\n=== 6. 等待服务启动 ==="); await new Promise(r => setTimeout(r, 3000)); console.log("\n=== 7. 健康检查 ==="); const health = await sshExec(`curl -s http://127.0.0.1:3012/api/health 2>&1`); console.log("Health Check:", health.stdout); console.log("\n=== 8. PM2 状态 ==="); const pm2Status = await sshExec("pm2 list 2>&1"); console.log(pm2Status.stdout); console.log("\n=== 9. 最新日志 ==="); const logs = await sshExec("pm2 logs bigwo-server --nostream --lines 20 2>&1"); console.log(logs.stdout); console.log("\n========================================"); console.log(" 部署完成!"); console.log(" 访问: https://demo.tensorgrove.com.cn"); console.log("========================================"); } main().catch(e => { console.error("Fatal:", e.message); process.exit(1); });