按 TODO.md 的优先级把待办全部落地为代码,详见 CHANGELOG 的 [1.7.0]。 - 「走不动」攻关:地面状态修正(syncGroundToPhysics / trustSetbackOnGround)、 出站包类型分布、备选执行器 manualMover(只借 A* 规划、自己按键执行) - 原点自救分两轮并统计成败;回城前评估「原地是否更差」并记录前后坐标 - 新增目标区块租约表(targetLease)避免多机器人撞同一块 - 定时消息支持 cron 与「期望回复」确认;新增人工介入 Webhook 通知(notifier) - 大地图后台预热与视野裁剪;控制台日志按帧批量写入、配置面板支持搜索 顺带修掉两个影响部署的细节: - 部署脚本(deploy.js / ssh-helper.js)改为也能从 .env 读 SSH 凭据 —— 在命令行传密码会留在 shell 历史与进程列表里,写进已被忽略的 .env 更安全; 显式环境变量仍然优先。.env.example 补上占位符并说明这些只在本机用。 - package.json / package-lock.json 版本对齐到 1.7.0(此前停留在 1.1.0, 与 CHANGELOG 的语义化版本声明脱节)。 测试:368 → 436 个用例,`npm test` 与 `npm run check` 全绿。
136 行
4.2 KiB
JavaScript
136 行
4.2 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* 部署辅助:用 ssh2 做密码登录的 SSH 执行与 SFTP 上传。
|
||
*
|
||
* 为什么要自己写一个:Windows 自带 OpenSSH 但**没有 sshpass / plink**,
|
||
* 无法在非交互模式下传密码,而部署脚本必须能无人值守地跑完。
|
||
* 用 ssh2 直接实现协议层就绕开了这个问题。
|
||
*
|
||
* **脚本本身不含任何凭据**,全部走环境变量,避免密钥进仓库。
|
||
*
|
||
* 用法:
|
||
* node scripts/ssh-helper.js exec "<命令>"
|
||
* node scripts/ssh-helper.js put <本地文件> <远程路径>
|
||
*
|
||
* 环境变量(也可以写进项目根目录的 .env,那里已被 .gitignore 忽略):
|
||
* SSH_HOST 必填
|
||
* SSH_PASS 必填
|
||
* SSH_PORT 可选,默认 22
|
||
* SSH_USER 可选,默认 root
|
||
*
|
||
* 若未安装 ssh2(devDependencies,可选),会给出安装提示而不是裸崩。
|
||
*/
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
// 与 deploy.js 一致:凭据可以从 .env 读;dotenv 不覆盖已存在的环境变量,显式传参仍然优先
|
||
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
||
|
||
let Client;
|
||
try {
|
||
({ Client } = require('ssh2'));
|
||
} catch {
|
||
console.error('缺少可选依赖 ssh2。部署相关脚本需要它:\n\n npm install --save-dev ssh2\n');
|
||
process.exit(2);
|
||
}
|
||
|
||
const HOST = process.env.SSH_HOST;
|
||
const PORT = Number(process.env.SSH_PORT || 22);
|
||
const USER = process.env.SSH_USER || 'root';
|
||
const PASS = process.env.SSH_PASS;
|
||
|
||
if (!HOST || !PASS) {
|
||
console.error('缺少 SSH_HOST / SSH_PASS。请写进项目根目录的 .env(参考 .env.example),'
|
||
+ '或用环境变量临时传入。');
|
||
process.exit(2);
|
||
}
|
||
|
||
function connect() {
|
||
return new Promise((resolve, reject) => {
|
||
const client = new Client();
|
||
client.on('ready', () => resolve(client));
|
||
client.on('error', reject);
|
||
client.connect({
|
||
host: HOST,
|
||
port: PORT,
|
||
username: USER,
|
||
password: PASS,
|
||
readyTimeout: 30000,
|
||
algorithms: {
|
||
// 老版本 OpenSSH 服务器可能只提供这些算法,放宽列表避免握手失败
|
||
kex: [
|
||
'curve25519-sha256', 'curve25519-sha256@libssh.org',
|
||
'ecdh-sha2-nistp256', 'ecdh-sha2-nistp384', 'ecdh-sha2-nistp521',
|
||
'diffie-hellman-group-exchange-sha256',
|
||
'diffie-hellman-group14-sha256', 'diffie-hellman-group14-sha1',
|
||
'diffie-hellman-group1-sha1'
|
||
],
|
||
serverHostKey: [
|
||
'ssh-ed25519', 'ecdsa-sha2-nistp256', 'rsa-sha2-512', 'rsa-sha2-256', 'ssh-rsa'
|
||
]
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/** @returns {Promise<{code: number, stdout: string, stderr: string}>} */
|
||
function exec(client, command) {
|
||
return new Promise((resolve, reject) => {
|
||
client.exec(command, { pty: false }, (err, stream) => {
|
||
if (err) {
|
||
reject(err);
|
||
return;
|
||
}
|
||
let stdout = '';
|
||
let stderr = '';
|
||
stream.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
|
||
stream.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
|
||
stream.on('close', (code) => resolve({ code, stdout, stderr }));
|
||
});
|
||
});
|
||
}
|
||
|
||
function put(client, localFile, remotePath) {
|
||
return new Promise((resolve, reject) => {
|
||
client.sftp((err, sftp) => {
|
||
if (err) {
|
||
reject(err);
|
||
return;
|
||
}
|
||
sftp.fastPut(localFile, remotePath, (putErr) => {
|
||
if (putErr) reject(putErr);
|
||
else resolve(remotePath);
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
async function main() {
|
||
const [action, ...rest] = process.argv.slice(2);
|
||
const client = await connect();
|
||
try {
|
||
if (action === 'exec') {
|
||
const result = await exec(client, rest.join(' '));
|
||
if (result.stdout) process.stdout.write(result.stdout);
|
||
if (result.stderr) process.stderr.write(result.stderr);
|
||
process.exitCode = result.code || 0;
|
||
} else if (action === 'put') {
|
||
const local = path.resolve(rest[0]);
|
||
await put(client, local, rest[1]);
|
||
console.log(`uploaded ${path.basename(local)} -> ${rest[1]} (${fs.statSync(local).size} bytes)`);
|
||
} else {
|
||
console.error('未知动作:' + action);
|
||
process.exitCode = 2;
|
||
}
|
||
} finally {
|
||
client.end();
|
||
}
|
||
}
|
||
|
||
main().catch((err) => {
|
||
console.error('SSH 失败:' + (err && err.message ? err.message : err));
|
||
process.exit(1);
|
||
});
|