koishi-plugin-docker-control 0.1.0 → 0.1.2

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.
@@ -135,4 +135,97 @@ function registerComposeCommand(ctx, getService, config) {
135
135
  return `获取 compose 配置失败: ${e.message}`;
136
136
  }
137
137
  });
138
+ /**
139
+ * 更新 compose 缓存命令
140
+ */
141
+ ctx
142
+ .command('docker.compose.update <node> <container>', '手动更新 compose 文件缓存')
143
+ .alias('compose.update')
144
+ .action(async (_, nodeSelector, container) => {
145
+ const service = getService();
146
+ if (!service) {
147
+ return 'Docker 服务未初始化';
148
+ }
149
+ if (!nodeSelector || !container) {
150
+ return '请指定节点和容器: docker.compose.update <节点> <容器>';
151
+ }
152
+ // 获取节点
153
+ const nodes = service.getNodesBySelector(nodeSelector);
154
+ if (nodes.length === 0) {
155
+ return `未找到节点: ${nodeSelector}`;
156
+ }
157
+ const node = nodes[0];
158
+ if (node.status !== 'connected') {
159
+ return `节点 ${node.name} 未连接`;
160
+ }
161
+ // 查找容器
162
+ let containers;
163
+ try {
164
+ containers = await node.listContainers(true);
165
+ }
166
+ catch (e) {
167
+ return `获取容器列表失败: ${e.message}`;
168
+ }
169
+ // 支持容器名称或 ID 前缀匹配
170
+ const targetContainer = containers.find(c => c.Names[0]?.replace('/', '') === container ||
171
+ c.Id.startsWith(container));
172
+ if (!targetContainer) {
173
+ return `未找到容器: ${container}`;
174
+ }
175
+ // 更新缓存
176
+ const result = await node.updateComposeCache(targetContainer.Id);
177
+ return result.message;
178
+ });
179
+ /**
180
+ * 清除 compose 缓存命令
181
+ */
182
+ ctx
183
+ .command('docker.compose.clear [node] [container]', '清除 compose 文件缓存')
184
+ .alias('compose.clear')
185
+ .action(async (_, nodeSelector, container) => {
186
+ const service = getService();
187
+ if (!service) {
188
+ return 'Docker 服务未初始化';
189
+ }
190
+ if (!nodeSelector) {
191
+ // 清除所有缓存
192
+ let totalCleared = 0;
193
+ const nodes = service.getAllNodes();
194
+ for (const node of nodes) {
195
+ const result = await node.clearComposeCache();
196
+ totalCleared += result.cleared;
197
+ }
198
+ return totalCleared > 0
199
+ ? `已清除所有节点的 compose 缓存 (共 ${totalCleared} 条)`
200
+ : '没有需要清除的缓存';
201
+ }
202
+ // 获取节点
203
+ const nodes = service.getNodesBySelector(nodeSelector);
204
+ if (nodes.length === 0) {
205
+ return `未找到节点: ${nodeSelector}`;
206
+ }
207
+ const node = nodes[0];
208
+ if (!container) {
209
+ // 清除指定节点的所有缓存
210
+ const result = await node.clearComposeCache();
211
+ return result.message;
212
+ }
213
+ // 查找容器
214
+ let containers;
215
+ try {
216
+ containers = await node.listContainers(true);
217
+ }
218
+ catch (e) {
219
+ return `获取容器列表失败: ${e.message}`;
220
+ }
221
+ // 支持容器名称或 ID 前缀匹配
222
+ const targetContainer = containers.find(c => c.Names[0]?.replace('/', '') === container ||
223
+ c.Id.startsWith(container));
224
+ if (!targetContainer) {
225
+ return `未找到容器: ${container}`;
226
+ }
227
+ // 清除指定容器的缓存
228
+ const result = await node.clearComposeCache(targetContainer.Id);
229
+ return result.message;
230
+ });
138
231
  }
@@ -34,6 +34,42 @@ function registerCommands(ctx, getService, config) {
34
34
  */
35
35
  function registerHelperCommands(ctx, getService, config) {
36
36
  const useImageOutput = config?.imageOutput === true;
37
+ /**
38
+ * 诊断:查看节点原始配置
39
+ */
40
+ ctx.command('docker.debug.config', '查看节点原始配置(诊断用)')
41
+ .action(async () => {
42
+ const service = getService();
43
+ if (!service) {
44
+ return 'Docker 服务未初始化';
45
+ }
46
+ const nodes = service.getAllNodes();
47
+ if (nodes.length === 0) {
48
+ return '未配置任何节点';
49
+ }
50
+ const lines = [];
51
+ lines.push('=== 节点原始配置诊断 ===\n');
52
+ for (const node of nodes) {
53
+ const config = node.config;
54
+ lines.push(`【${config.name}】`);
55
+ lines.push(` ID: ${config.id}`);
56
+ lines.push(` Host: ${config.host}`);
57
+ lines.push(` Port: ${config.port} (类型: ${typeof config.port})`);
58
+ lines.push(` Credential: ${config.credentialId}`);
59
+ lines.push(` Tags: ${config.tags.join(', ') || '(无)'}`);
60
+ // 检测异常
61
+ if (typeof config.port === 'string') {
62
+ if (config.port.includes('.') || config.port.includes(':')) {
63
+ lines.push(` ⚠️ 检测到异常端口配置: 包含IP地址或特殊字符`);
64
+ }
65
+ }
66
+ else if (typeof config.port !== 'number') {
67
+ lines.push(` ⚠️ 检测到异常端口类型: ${typeof config.port}`);
68
+ }
69
+ lines.push('');
70
+ }
71
+ return lines.join('\n');
72
+ });
37
73
  /**
38
74
  * 查看节点列表
39
75
  */
@@ -97,18 +133,21 @@ function registerHelperCommands(ctx, getService, config) {
97
133
  const html = (0, render_1.generateNodeDetailHtml)(nodeData, version, systemInfo);
98
134
  return await (0, render_1.renderToImage)(ctx, html);
99
135
  }
100
- const memoryUsed = systemInfo?.MemTotal && systemInfo?.MemAvailable !== undefined
101
- ? `${Math.round((1 - systemInfo.MemAvailable / systemInfo.MemTotal) * 100)}%`
102
- : '-';
103
136
  const nodeName = node.config?.name || node.name || node.Name || 'Unknown';
104
137
  const nodeId = node.id || node.ID || node.Id || node.config?.id || '-';
138
+ // 格式化内存显示:总内存量
139
+ let memoryDisplay = '-';
140
+ if (systemInfo?.MemTotal) {
141
+ const memGB = (systemInfo.MemTotal / 1024 / 1024 / 1024).toFixed(2);
142
+ memoryDisplay = `${memGB} GB`;
143
+ }
105
144
  const lines = [
106
145
  `=== ${nodeName} ===`,
107
146
  `ID: ${nodeId}`,
108
147
  `状态: ${node.status || node.Status || 'unknown'}`,
109
148
  `标签: ${node.tags?.join(', ') || node.config?.tags?.join(', ') || '无'}`,
110
149
  `CPU: ${systemInfo?.NCPU || '-'} 核心`,
111
- `内存: ${memoryUsed} (可用: ${systemInfo?.MemAvailable ? Math.round(systemInfo.MemAvailable / 1024 / 1024) + ' MB' : '-'})`,
150
+ `内存: ${memoryDisplay}`,
112
151
  `容器: ${containerCount.running}/${containerCount.total} 运行中`,
113
152
  `镜像: ${imageCount} 个`,
114
153
  `Docker 版本: ${version.Version}`,
@@ -8,6 +8,8 @@ export declare const MAX_RETRY_COUNT = 3;
8
8
  export declare const MONITOR_RETRY_INTERVAL = 30000;
9
9
  export declare const EVENTS_POLL_INTERVAL = 60000;
10
10
  export declare const CONTAINER_POLL_INTERVAL: number;
11
+ export declare const API_HEALTH_CHECK_INTERVAL: number;
12
+ export declare const DEGRADED_POLL_INTERVAL: number;
11
13
  export declare const DEFAULT_LOG_LINES = 100;
12
14
  export declare const NodeStatus: {
13
15
  readonly DISCONNECTED: "disconnected";
package/lib/constants.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * 常量定义
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.ContainerAction = exports.NodeStatus = exports.DEFAULT_LOG_LINES = exports.CONTAINER_POLL_INTERVAL = exports.EVENTS_POLL_INTERVAL = exports.MONITOR_RETRY_INTERVAL = exports.MAX_RETRY_COUNT = exports.SSH_TIMEOUT = exports.RETRY_INTERVAL = exports.DEFAULT_TIMEOUT = void 0;
6
+ exports.ContainerAction = exports.NodeStatus = exports.DEFAULT_LOG_LINES = exports.DEGRADED_POLL_INTERVAL = exports.API_HEALTH_CHECK_INTERVAL = exports.CONTAINER_POLL_INTERVAL = exports.EVENTS_POLL_INTERVAL = exports.MONITOR_RETRY_INTERVAL = exports.MAX_RETRY_COUNT = exports.SSH_TIMEOUT = exports.RETRY_INTERVAL = exports.DEFAULT_TIMEOUT = void 0;
7
7
  // 默认请求超时时间 (毫秒)
8
8
  exports.DEFAULT_TIMEOUT = 5000;
9
9
  // 默认重试间隔 (毫秒)
@@ -16,8 +16,12 @@ exports.MAX_RETRY_COUNT = 3;
16
16
  exports.MONITOR_RETRY_INTERVAL = 30000;
17
17
  // Docker Events 监听间隔 (毫秒) - 已改为流式监听,此常量仅作备用
18
18
  exports.EVENTS_POLL_INTERVAL = 60000;
19
- // 容器状态轮询间隔 (毫秒) - 改为5分钟仅作兜底同步
19
+ // 容器状态轮询间隔 (毫秒) - 仅在API降级时使用
20
20
  exports.CONTAINER_POLL_INTERVAL = 5 * 60 * 1000;
21
+ // Docker API 健康检查间隔 (毫秒) - 定期检测API是否恢复
22
+ exports.API_HEALTH_CHECK_INTERVAL = 60 * 1000;
23
+ // 降级轮询间隔 (毫秒) - API不可用时的轮询频率
24
+ exports.DEGRADED_POLL_INTERVAL = 30 * 1000;
21
25
  // 日志行数默认
22
26
  exports.DEFAULT_LOG_LINES = 100;
23
27
  // 节点状态枚举
package/lib/index.d.ts CHANGED
@@ -43,6 +43,16 @@ interface AuditLogRecord {
43
43
  containerId: string;
44
44
  metadata: Record<string, any>;
45
45
  }
46
+ interface ComposeFileCache {
47
+ id: string;
48
+ containerId: string;
49
+ filePath: string;
50
+ content: string;
51
+ projectName: string;
52
+ serviceCount: number;
53
+ mtime: number;
54
+ updatedAt: number;
55
+ }
46
56
  declare module 'koishi' {
47
57
  interface Context {
48
58
  puppeteer?: {
@@ -56,6 +66,7 @@ declare module 'koishi' {
56
66
  'docker_control_subscriptions': DockerControlSubscription;
57
67
  'docker_user_permissions': UserPermissionRecord;
58
68
  'docker_audit_logs': AuditLogRecord;
69
+ 'docker_compose_cache': ComposeFileCache;
59
70
  }
60
71
  }
61
72
  export declare const Config: Schema<Schemastery.ObjectS<{
package/lib/index.js CHANGED
@@ -86,6 +86,46 @@ function apply(ctx, config) {
86
86
  logger_1.logger.info('Docker Control 配置未定义,跳过加载');
87
87
  return;
88
88
  }
89
+ // 🔍 诊断:打印原始配置信息
90
+ logger_1.logger.info('=== 配置诊断 ===');
91
+ logger_1.logger.info(`配置中的节点数量: ${config.nodes?.length || 0}`);
92
+ if (config.nodes && config.nodes.length > 0) {
93
+ for (const node of config.nodes) {
94
+ logger_1.logger.info(`节点 [${node.name}]:`);
95
+ logger_1.logger.info(` ID: ${node.id}`);
96
+ logger_1.logger.info(` Host: ${node.host}`);
97
+ logger_1.logger.info(` Port: ${node.port} (类型: ${typeof node.port})`);
98
+ logger_1.logger.info(` Credential: ${node.credentialId}`);
99
+ }
100
+ }
101
+ logger_1.logger.info('================');
102
+ // 🔧 第一道防线:在插件入口处清理配置
103
+ if (config.nodes) {
104
+ for (const node of config.nodes) {
105
+ if (typeof node.port === 'string') {
106
+ const portStr = node.port;
107
+ if (portStr.includes('.') || portStr.includes(':')) {
108
+ logger_1.logger.warn(`[配置清理] 节点 ${node.name} 检测到异常端口: "${portStr}",已修正为 22`);
109
+ node.port = 22;
110
+ }
111
+ else {
112
+ const parsed = parseInt(portStr, 10);
113
+ if (!isNaN(parsed) && parsed >= 1 && parsed <= 65535) {
114
+ ;
115
+ node.port = parsed;
116
+ }
117
+ else {
118
+ logger_1.logger.error(`[配置清理] 节点 ${node.name} 端口值无效: "${portStr}",已修正为 22`);
119
+ node.port = 22;
120
+ }
121
+ }
122
+ }
123
+ else if (typeof node.port !== 'number' || node.port < 1 || node.port > 65535) {
124
+ logger_1.logger.error(`[配置清理] 节点 ${node.name} 端口异常: ${node.port} (${typeof node.port}),已修正为 22`);
125
+ node.port = 22;
126
+ }
127
+ }
128
+ }
89
129
  // 验证配置
90
130
  const errors = [];
91
131
  const credentialIds = new Set(config.credentials?.map(c => c.id) || []);
@@ -1,9 +1,22 @@
1
+ /**
2
+ * SSH 连接器 - 通过 SSH 执行 docker 命令
3
+ */
4
+ import { Client } from 'ssh2';
1
5
  import type { NodeConfig, DockerControlConfig } from '../types';
2
6
  export declare class DockerConnector {
3
7
  private config;
4
8
  private fullConfig;
5
9
  private sshClient;
6
10
  constructor(config: NodeConfig, fullConfig: DockerControlConfig);
11
+ /**
12
+ * 获取内部 SSH Client(用于连接复用)
13
+ * 如果尚未连接,会触发连接建立
14
+ */
15
+ getSshClient(): Promise<Client>;
16
+ /**
17
+ * 验证并修正配置
18
+ */
19
+ private validateConfig;
7
20
  /**
8
21
  * 执行 SSH 命令
9
22
  */
@@ -93,4 +106,8 @@ export declare class DockerConnector {
93
106
  * 检查文件是否存在
94
107
  */
95
108
  fileExists(filePath: string): Promise<boolean>;
109
+ /**
110
+ * 获取文件修改时间 (Unix 时间戳,秒)
111
+ */
112
+ getFileModTime(filePath: string): Promise<number>;
96
113
  }
@@ -14,6 +14,38 @@ class DockerConnector {
14
14
  this.fullConfig = fullConfig;
15
15
  this.sshClient = null;
16
16
  this.connected = true;
17
+ // 立即验证并修正配置
18
+ this.validateConfig();
19
+ }
20
+ /**
21
+ * 获取内部 SSH Client(用于连接复用)
22
+ * 如果尚未连接,会触发连接建立
23
+ */
24
+ async getSshClient() {
25
+ return await this.getConnection();
26
+ }
27
+ /**
28
+ * 验证并修正配置
29
+ */
30
+ validateConfig() {
31
+ if (typeof this.config.port === 'string') {
32
+ const portStr = this.config.port;
33
+ if (portStr.includes('.') || portStr.includes(':')) {
34
+ logger_1.connectorLogger.warn(`[${this.config.name}] 检测到异常端口配置: "${portStr}",已自动修正为 22`);
35
+ this.config.port = 22;
36
+ }
37
+ else {
38
+ const parsed = parseInt(portStr, 10);
39
+ if (!isNaN(parsed) && parsed >= 1 && parsed <= 65535) {
40
+ ;
41
+ this.config.port = parsed;
42
+ }
43
+ else {
44
+ logger_1.connectorLogger.error(`[${this.config.name}] 端口值无效: "${portStr}",已自动修正为 22`);
45
+ this.config.port = 22;
46
+ }
47
+ }
48
+ }
17
49
  }
18
50
  /**
19
51
  * 执行 SSH 命令
@@ -30,7 +62,8 @@ class DockerConnector {
30
62
  const msg = err.message || '';
31
63
  // 如果是 SSH 通道打开失败,或者是连接已结束,则强制重连
32
64
  if (msg.includes('Channel open failure') || msg.includes('Client ended') || msg.includes('Socket ended')) {
33
- logger_1.connectorLogger.warn(`[${this.config.name}] SSH 连接异常 (${msg}),尝试重连...`);
65
+ logger_1.connectorLogger.warn(`[${this.config.name}] SSH连接异常: ${msg},尝试重连...`);
66
+ logger_1.connectorLogger.debug(`[${this.config.name}] 重连将产生新的SSH登录记录`);
34
67
  this.dispose(); // 强制销毁当前连接
35
68
  continue; // 重试
36
69
  }
@@ -42,11 +75,11 @@ class DockerConnector {
42
75
  }
43
76
  async execInternal(command) {
44
77
  const client = await this.getConnection();
45
- logger_1.connectorLogger.debug(`[${this.config.name}] 执行命令: ${command}`);
78
+ logger_1.connectorLogger.debug(`[${this.config.name}] 🔧 执行SSH命令: ${command}`);
46
79
  return new Promise((resolve, reject) => {
47
80
  client.exec(command, (err, stream) => {
48
81
  if (err) {
49
- logger_1.connectorLogger.debug(`[${this.config.name}] 命令执行错误: ${err.message}`);
82
+ logger_1.connectorLogger.warn(`[${this.config.name}] SSH命令执行失败: ${err.message}`);
50
83
  reject(err);
51
84
  return;
52
85
  }
@@ -171,7 +204,7 @@ class DockerConnector {
171
204
  reject(err);
172
205
  return;
173
206
  }
174
- logger_1.connectorLogger.info(`[${this.config.name}] Docker 事件流已连接`);
207
+ logger_1.connectorLogger.info(`[${this.config.name}] Docker 事件流已建立长连接 (docker events --format json --filter type=container)`);
175
208
  let buffer = '';
176
209
  let closed = false;
177
210
  const stop = () => {
@@ -186,13 +219,14 @@ class DockerConnector {
186
219
  catch (e) {
187
220
  // 可能已经关闭,忽略错误
188
221
  }
189
- logger_1.connectorLogger.debug(`[${this.config.name}] 主动停止事件流`);
222
+ logger_1.connectorLogger.info(`[${this.config.name}] 🔒 主动停止事件流`);
190
223
  }
191
224
  };
192
225
  stream.on('close', (code, signal) => {
193
226
  if (!closed) {
194
227
  closed = true;
195
- logger_1.connectorLogger.warn(`[${this.config.name}] 事件流意外断开 (Code: ${code}, Signal: ${signal})`);
228
+ logger_1.connectorLogger.error(`[${this.config.name}] 事件流意外断开!Code: ${code}, Signal: ${signal}`);
229
+ logger_1.connectorLogger.error(`[${this.config.name}] ⚠ 事件流断开后,node.ts 会自动重连 (将产生新的SSH登录记录)`);
196
230
  }
197
231
  });
198
232
  stream.on('data', (data) => {
@@ -226,6 +260,7 @@ class DockerConnector {
226
260
  */
227
261
  dispose() {
228
262
  if (this.sshClient) {
263
+ logger_1.connectorLogger.info(`[${this.config.name}] 主动销毁 SSH 连接`);
229
264
  this.sshClient.end();
230
265
  this.sshClient = null;
231
266
  }
@@ -247,32 +282,42 @@ class DockerConnector {
247
282
  if (!credential) {
248
283
  throw new Error(`凭证不存在: ${this.config.credentialId}`);
249
284
  }
250
- logger_1.connectorLogger.info(`[${this.config.name}] 正在连接到 ${this.config.host}:${this.config.port}`);
251
- logger_1.connectorLogger.debug(`[${this.config.name}] 用户名: ${credential.username}, 认证方式: ${credential.authType}`);
285
+ const port = typeof this.config.port === 'string'
286
+ ? parseInt(this.config.port, 10)
287
+ : (this.config.port || 22);
288
+ logger_1.connectorLogger.info(`[${this.config.name}] 🔗 建立新的SSH连接...`);
289
+ logger_1.connectorLogger.info(`[${this.config.name}] 目标: ${credential.username}@${this.config.host}:${port}`);
290
+ logger_1.connectorLogger.info(`[${this.config.name}] 认证方式: ${credential.authType}`);
252
291
  return new Promise((resolve, reject) => {
253
292
  const conn = new ssh2_1.Client();
254
293
  conn.on('ready', () => {
255
- logger_1.connectorLogger.info(`[${this.config.name}] SSH 连接成功`);
294
+ logger_1.connectorLogger.info(`[${this.config.name}] SSH连接成功 (user=${credential.username}, host=${this.config.host}, port=${port})`);
256
295
  resolve(conn);
257
296
  });
258
297
  conn.on('error', (err) => {
259
- logger_1.connectorLogger.error(`[${this.config.name}] SSH 连接失败: ${err.message}`);
298
+ logger_1.connectorLogger.error(`[${this.config.name}] SSH连接失败: ${err.message} (host=${this.config.host}, port=${port})`);
299
+ logger_1.connectorLogger.error(`[${this.config.name}] ⚠ 连接失败后将在片刻重试 (重试会产生新的SSH登录记录)`);
260
300
  conn.end();
261
301
  reject(err);
262
302
  });
263
303
  conn.on('close', () => {
264
- logger_1.connectorLogger.debug(`[${this.config.name}] SSH 连接关闭`);
304
+ const reason = this.connected ? 'SSH连接意外断开' : 'SSH连接已关闭';
305
+ logger_1.connectorLogger.warn(`[${this.config.name}] ${reason} (host=${this.config.host}, port=${this.config.port})`);
306
+ this.connected = false;
265
307
  });
266
308
  conn.on('banner', (msg) => {
267
309
  logger_1.connectorLogger.debug(`[${this.config.name}] SSH Banner: ${msg.trim()}`);
268
310
  });
269
311
  const connectConfig = {
270
312
  host: this.config.host,
271
- port: this.config.port,
313
+ port: port,
272
314
  username: credential.username,
273
315
  readyTimeout: constants_1.SSH_TIMEOUT,
274
316
  timeout: constants_1.SSH_TIMEOUT,
275
317
  tryKeyboard: true,
318
+ // === 保持连接活跃,防止被服务器踢掉 ===
319
+ keepaliveInterval: 15000, // 每15秒发送一次心跳
320
+ keepaliveCountMax: 3, // 失败3次认为断开
276
321
  ...this.buildAuthOptions(credential),
277
322
  };
278
323
  conn.connect(connectConfig);
@@ -357,5 +402,13 @@ class DockerConnector {
357
402
  const result = await this.execWithExitCode(`test -f "${escapedPath}" && echo "exists" || echo "not exists"`);
358
403
  return result.output.trim() === 'exists';
359
404
  }
405
+ /**
406
+ * 获取文件修改时间 (Unix 时间戳,秒)
407
+ */
408
+ async getFileModTime(filePath) {
409
+ const escapedPath = filePath.replace(/"/g, '\\"');
410
+ const output = await this.exec(`stat -c %Y "${escapedPath}" 2>/dev/null || echo "0"`);
411
+ return parseInt(output.trim(), 10);
412
+ }
360
413
  }
361
414
  exports.DockerConnector = DockerConnector;
@@ -21,6 +21,10 @@ export declare class DockerService {
21
21
  auditLogger?: AuditLogger;
22
22
  reconnectManager?: ReconnectManager;
23
23
  constructor(ctx: Context, config: DockerControlConfig);
24
+ /**
25
+ * 清理节点配置
26
+ */
27
+ private cleanNodeConfig;
24
28
  /**
25
29
  * 初始化所有节点
26
30
  */
@@ -12,6 +12,37 @@ class DockerService {
12
12
  this.ctx = ctx;
13
13
  this.config = config;
14
14
  }
15
+ /**
16
+ * 清理节点配置
17
+ */
18
+ cleanNodeConfig(nodeConfig) {
19
+ // 创建配置副本以避免修改原始配置
20
+ const cleaned = { ...nodeConfig };
21
+ // 验证并清理端口
22
+ if (typeof cleaned.port === 'string') {
23
+ const portStr = cleaned.port;
24
+ if (portStr.includes('.') || portStr.includes(':')) {
25
+ logger_1.logger.warn(`节点 ${cleaned.name} 检测到异常端口配置: "${portStr}",已自动修正为 22`);
26
+ cleaned.port = 22;
27
+ }
28
+ else {
29
+ const parsed = parseInt(portStr, 10);
30
+ if (!isNaN(parsed) && parsed >= 1 && parsed <= 65535) {
31
+ ;
32
+ cleaned.port = parsed;
33
+ }
34
+ else {
35
+ logger_1.logger.error(`节点 ${cleaned.name} 端口值无效: "${portStr}",已自动修正为 22`);
36
+ cleaned.port = 22;
37
+ }
38
+ }
39
+ }
40
+ else if (typeof cleaned.port !== 'number' || cleaned.port < 1 || cleaned.port > 65535) {
41
+ logger_1.logger.error(`节点 ${cleaned.name} 端口类型或值异常: ${cleaned.port},已自动修正为 22`);
42
+ cleaned.port = 22;
43
+ }
44
+ return cleaned;
45
+ }
15
46
  /**
16
47
  * 初始化所有节点
17
48
  */
@@ -26,7 +57,9 @@ class DockerService {
26
57
  logger_1.logger.warn(`节点 ${nodeConfig.name} 找不到凭证 ${nodeConfig.credentialId},跳过`);
27
58
  continue;
28
59
  }
29
- const node = new node_1.DockerNode(nodeConfig, credential, this.config.debug);
60
+ // 清理和验证端口配置
61
+ const cleanedNodeConfig = this.cleanNodeConfig(nodeConfig);
62
+ const node = new node_1.DockerNode(this.ctx, cleanedNodeConfig, credential, this.config.debug);
30
63
  // 【关键修复】创建节点时,立即绑定事件转发
31
64
  // 无论 index.ts 何时调用 onNodeEvent,这里都会把事件转发给 eventCallbacks
32
65
  node.onEvent((event) => {