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.
- package/lib/commands/compose.js +93 -0
- package/lib/commands/index.js +43 -4
- package/lib/constants.d.ts +2 -0
- package/lib/constants.js +6 -2
- package/lib/index.d.ts +11 -0
- package/lib/index.js +40 -0
- package/lib/service/connector.d.ts +17 -0
- package/lib/service/connector.js +65 -12
- package/lib/service/index.d.ts +4 -0
- package/lib/service/index.js +34 -1
- package/lib/service/node.d.ts +78 -5
- package/lib/service/node.js +864 -99
- package/package.json +1 -1
package/lib/commands/compose.js
CHANGED
|
@@ -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
|
}
|
package/lib/commands/index.js
CHANGED
|
@@ -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
|
-
`内存: ${
|
|
150
|
+
`内存: ${memoryDisplay}`,
|
|
112
151
|
`容器: ${containerCount.running}/${containerCount.total} 运行中`,
|
|
113
152
|
`镜像: ${imageCount} 个`,
|
|
114
153
|
`Docker 版本: ${version.Version}`,
|
package/lib/constants.d.ts
CHANGED
|
@@ -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
|
-
// 容器状态轮询间隔 (毫秒) -
|
|
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
|
}
|
package/lib/service/connector.js
CHANGED
|
@@ -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
|
|
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}]
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
251
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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;
|
package/lib/service/index.d.ts
CHANGED
package/lib/service/index.js
CHANGED
|
@@ -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
|
-
|
|
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) => {
|