2026-03-12 12:47:56 +08:00
|
|
|
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` },
|
2026-03-13 13:06:46 +08:00
|
|
|
{ local: "server\\package.json", remote: `${PROJECT}/server/package.json` },
|
|
|
|
|
{ local: "server\\package-lock.json", remote: `${PROJECT}/server/package-lock.json` },
|
2026-03-12 12:47:56 +08:00
|
|
|
{ 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` },
|
2026-03-13 13:06:46 +08:00
|
|
|
{ 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` },
|
2026-03-12 12:47:56 +08:00
|
|
|
{ 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` },
|
|
|
|
|
];
|
2026-03-13 13:06:46 +08:00
|
|
|
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}`,
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-03-12 12:47:56 +08:00
|
|
|
|
|
|
|
|
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`,
|
2026-03-13 13:06:46 +08:00
|
|
|
`${PROJECT}/server/package.json`,
|
|
|
|
|
`${PROJECT}/server/package-lock.json`,
|
2026-03-12 12:47:56 +08:00
|
|
|
`${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`,
|
2026-03-13 13:06:46 +08:00
|
|
|
`${PROJECT}/server/services/nativeVoiceGateway.js`,
|
|
|
|
|
`${PROJECT}/server/services/realtimeDialogProtocol.js`,
|
|
|
|
|
`${PROJECT}/server/services/realtimeDialogRouting.js`,
|
2026-03-12 12:47:56 +08:00
|
|
|
`${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("备份完成");
|
|
|
|
|
|
2026-03-13 13:06:46 +08:00
|
|
|
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 残留文件");
|
|
|
|
|
|
2026-03-12 12:47:56 +08:00
|
|
|
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. 同步前端构建产物 ===");
|
2026-03-13 13:06:46 +08:00
|
|
|
await sshExec(`mkdir -p ${PROJECT}/client/dist/assets && find ${PROJECT}/client/dist/assets -maxdepth 1 -type f -delete`);
|
2026-03-12 12:47:56 +08:00
|
|
|
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 服务 ===");
|
2026-03-13 13:06:46 +08:00
|
|
|
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 服务 ===");
|
2026-03-12 12:47:56 +08:00
|
|
|
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); });
|