ssh-mcp-devops 2.0.2 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/ssh-manager-v2.js +107 -45
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ssh-mcp-devops",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "SSH MCP Server for AI-powered DevOps with Docker and System Monitoring",
5
5
  "type": "module",
6
6
  "main": "index-v2.js",
package/ssh-manager-v2.js CHANGED
@@ -19,7 +19,7 @@ export class SSHManager {
19
19
  config.privateKey = await fs.readFile(config.privateKeyPath, 'utf-8');
20
20
  } catch (error) {
21
21
  throw new Error(`无法读取私钥文件: ${error.message}`);
22
- }
22
+ }
23
23
  }
24
24
  return this.connect(config);
25
25
  }
@@ -38,7 +38,8 @@ export class SSHManager {
38
38
  port: config.port || 22,
39
39
  username: config.username,
40
40
  readyTimeout: 30000, // 30秒连接超时
41
- keepaliveInterval: 10000, // 10秒心跳
41
+ keepaliveInterval: 5000, // 5秒心跳(更频繁)
42
+ keepaliveCountMax: 10, // 最多10次心跳失败
42
43
  };
43
44
 
44
45
  if (config.password) {
@@ -70,6 +71,10 @@ export class SSHManager {
70
71
  this.connected = false;
71
72
  console.error('SSH连接已关闭');
72
73
  })
74
+ .on('end', () => {
75
+ this.connected = false;
76
+ console.error('SSH连接已结束');
77
+ })
73
78
  .connect(connectionConfig);
74
79
  });
75
80
  }
@@ -129,57 +134,114 @@ export class SSHManager {
129
134
  });
130
135
  }
131
136
 
132
- async uploadFile(localPath, remotePath, options = {}) {
133
- await this.ensureConnected();
134
-
135
- // 检查本地文件是否存在
137
+ async uploadFile(localPath, remotePath, options = {}, retryCount = 0) {
138
+ const maxRetries = 3;
139
+
136
140
  try {
137
- await fs.access(localPath);
138
- } catch (error) {
139
- throw new Error(`本地文件不存在: ${localPath}`);
140
- }
141
+ await this.ensureConnected();
141
142
 
142
- // 获取文件大小
143
- const stats = await fs.stat(localPath);
144
- const fileSizeMB = (stats.size / 1024 / 1024).toFixed(2);
145
- console.error(`开始上传文件: ${localPath} (${fileSizeMB} MB)`);
143
+ // 检查本地文件是否存在
144
+ try {
145
+ await fs.access(localPath);
146
+ } catch (error) {
147
+ throw new Error(`本地文件不存在: ${localPath}`);
148
+ }
146
149
 
147
- return new Promise((resolve, reject) => {
148
- // 根据文件大小设置超时(每 MB 10 秒,最少 60 秒)
149
- const timeout = Math.max(60000, stats.size / 1024 / 1024 * 10000);
150
- let uploadTimer;
150
+ // 获取文件大小
151
+ const stats = await fs.stat(localPath);
152
+ const fileSizeMB = (stats.size / 1024 / 1024).toFixed(2);
153
+ console.error(`开始上传文件: ${localPath} (${fileSizeMB} MB)${retryCount > 0 ? ` [重试 ${retryCount}/${maxRetries}]` : ''}`);
151
154
 
152
- this.client.sftp((err, sftp) => {
153
- if (err) return reject(err);
155
+ return await new Promise((resolve, reject) => {
156
+ // 根据文件大小设置超时(每 MB 20 秒,最少 180 秒)
157
+ const timeout = Math.max(180000, stats.size / 1024 / 1024 * 20000);
158
+ let uploadTimer;
159
+ let lastProgressTime = 0;
160
+ let lastProgressBytes = 0;
161
+ let startTime = Date.now();
154
162
 
155
- // 设置超时
156
- uploadTimer = setTimeout(() => {
157
- sftp.end();
158
- reject(new Error(`上传超时 (${(timeout / 1000).toFixed(0)}秒): 文件可能太大`));
159
- }, timeout);
160
-
161
- const transferOptions = {
162
- concurrency: 64,
163
- chunkSize: 32768,
164
- step: (total, nb, fsize) => {
165
- // 进度回调
166
- const percent = ((nb / fsize) * 100).toFixed(1);
167
- console.error(`上传进度: ${percent}% (${(nb / 1024 / 1024).toFixed(2)} MB / ${fileSizeMB} MB)`);
168
- if (options.onProgress) {
169
- options.onProgress(total, nb, fsize);
170
- }
171
- },
172
- };
163
+ this.client.sftp((err, sftp) => {
164
+ if (err) {
165
+ return reject(err);
166
+ }
173
167
 
174
- sftp.fastPut(localPath, remotePath, transferOptions, (err) => {
175
- clearTimeout(uploadTimer);
176
- sftp.end();
177
- if (err) return reject(new Error(`上传失败: ${err.message}`));
178
- console.error(`✅ 上传完成: ${fileSizeMB} MB`);
179
- resolve(`✅ 文件上传成功: ${localPath} -> ${remotePath} (${fileSizeMB} MB)`);
168
+ // 设置超时
169
+ uploadTimer = setTimeout(() => {
170
+ sftp.end();
171
+ reject(new Error(`上传超时 (${(timeout / 1000).toFixed(0)}): 文件可能太大或网络太慢`));
172
+ }, timeout);
173
+
174
+ const transferOptions = {
175
+ concurrency: 64,
176
+ chunkSize: 32768,
177
+ step: (total_transferred, chunk, total) => {
178
+ const now = Date.now();
179
+
180
+ // 每 2 秒输出一次进度(降低输出频率)
181
+ if (now - lastProgressTime >= 2000) {
182
+ const percent = ((total_transferred / total) * 100).toFixed(1);
183
+ const transferredMB = (total_transferred / 1024 / 1024).toFixed(2);
184
+
185
+ // 计算速度
186
+ const timeDiff = (now - (lastProgressTime || startTime)) / 1000;
187
+ const bytesDiff = total_transferred - lastProgressBytes;
188
+ const speedMBps = timeDiff > 0 ? (bytesDiff / 1024 / 1024 / timeDiff).toFixed(2) : 0;
189
+
190
+ // 估算剩余时间
191
+ const remainingBytes = total - total_transferred;
192
+ const eta = speedMBps > 0 ? (remainingBytes / 1024 / 1024 / speedMBps).toFixed(0) : '?';
193
+
194
+ console.error(`上传进度: ${percent}% (${transferredMB}/${fileSizeMB} MB) 速度: ${speedMBps} MB/s ETA: ${eta}s`);
195
+
196
+ lastProgressTime = now;
197
+ lastProgressBytes = total_transferred;
198
+ }
199
+
200
+ if (options.onProgress) {
201
+ options.onProgress(total_transferred, chunk, total);
202
+ }
203
+ },
204
+ };
205
+
206
+ sftp.fastPut(localPath, remotePath, transferOptions, (err) => {
207
+ clearTimeout(uploadTimer);
208
+ sftp.end();
209
+
210
+ if (err) {
211
+ return reject(new Error(`上传失败: ${err.message}`));
212
+ }
213
+
214
+ const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
215
+ const avgSpeed = (stats.size / 1024 / 1024 / totalTime).toFixed(2);
216
+ console.error(`✅ 上传完成: ${fileSizeMB} MB,耗时 ${totalTime}s,平均速度 ${avgSpeed} MB/s`);
217
+ resolve(`✅ 文件上传成功: ${localPath} -> ${remotePath} (${fileSizeMB} MB, ${totalTime}s)`);
218
+ });
180
219
  });
181
220
  });
182
- });
221
+ } catch (error) {
222
+ // 如果是连接错误且还有重试次数,尝试重连并重试
223
+ if (retryCount < maxRetries && (
224
+ error.message.includes('连接') ||
225
+ error.message.includes('Connection') ||
226
+ error.message.includes('ECONNRESET') ||
227
+ error.message.includes('超时')
228
+ )) {
229
+ console.error(`上传失败: ${error.message}`);
230
+ console.error(`尝试重新连接... (${retryCount + 1}/${maxRetries})`);
231
+
232
+ // 重新连接
233
+ try {
234
+ await this.connect(this.config);
235
+ // 等待 2 秒后重试
236
+ await new Promise(resolve => setTimeout(resolve, 2000));
237
+ return this.uploadFile(localPath, remotePath, options, retryCount + 1);
238
+ } catch (reconnectError) {
239
+ throw new Error(`重连失败: ${reconnectError.message}`);
240
+ }
241
+ }
242
+
243
+ throw error;
244
+ }
183
245
  }
184
246
 
185
247
  async downloadFile(remotePath, localPath, options = {}) {