koishi-plugin-docker-control 0.0.3 → 0.0.5
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.d.ts +11 -0
- package/lib/commands/compose.js +138 -0
- package/lib/commands/control.js +6 -5
- package/lib/commands/index.js +37 -57
- package/lib/commands/list.js +10 -2
- package/lib/commands/logs.d.ts +1 -1
- package/lib/commands/logs.js +29 -39
- package/lib/index.d.ts +3 -0
- package/lib/index.js +1 -1
- package/lib/service/connector.d.ts +30 -1
- package/lib/service/connector.js +97 -3
- package/lib/service/index.d.ts +7 -0
- package/lib/service/index.js +17 -0
- package/lib/service/node.d.ts +39 -2
- package/lib/service/node.js +151 -11
- package/lib/types.d.ts +24 -0
- package/lib/utils/render.d.ts +13 -1
- package/lib/utils/render.js +391 -47
- package/package.json +1 -1
package/lib/service/index.d.ts
CHANGED
|
@@ -62,4 +62,11 @@ export declare class DockerService {
|
|
|
62
62
|
*/
|
|
63
63
|
onNodeEvent(callback: (event: DockerEvent, nodeId: string) => void): () => void;
|
|
64
64
|
stopAll(): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* 获取所有在线节点的容器聚合
|
|
67
|
+
*/
|
|
68
|
+
getAggregatedContainers(all?: boolean): Promise<Array<{
|
|
69
|
+
node: DockerNode;
|
|
70
|
+
containers: ContainerInfo[];
|
|
71
|
+
}>>;
|
|
65
72
|
}
|
package/lib/service/index.js
CHANGED
|
@@ -198,5 +198,22 @@ class DockerService {
|
|
|
198
198
|
this.eventCallbacks.clear();
|
|
199
199
|
logger_1.logger.info('Docker 服务已停止');
|
|
200
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* 获取所有在线节点的容器聚合
|
|
203
|
+
*/
|
|
204
|
+
async getAggregatedContainers(all = true) {
|
|
205
|
+
const results = [];
|
|
206
|
+
for (const node of this.getOnlineNodes()) {
|
|
207
|
+
try {
|
|
208
|
+
const containers = await node.listContainers(all);
|
|
209
|
+
results.push({ node, containers });
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
logger_1.logger.warn(`[${node.name}] 获取容器列表失败: ${e}`);
|
|
213
|
+
results.push({ node, containers: [] });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return results;
|
|
217
|
+
}
|
|
201
218
|
}
|
|
202
219
|
exports.DockerService = DockerService;
|
package/lib/service/node.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NodeConfig, ContainerInfo, DockerEvent, NodeStatusType, CredentialConfig } from '../types';
|
|
1
|
+
import type { NodeConfig, ContainerInfo, DockerEvent, NodeStatusType, CredentialConfig, ComposeFileInfo, ContainerComposeInfo } from '../types';
|
|
2
2
|
export declare class DockerNode {
|
|
3
3
|
/** 节点配置 */
|
|
4
4
|
readonly config: NodeConfig;
|
|
@@ -27,6 +27,7 @@ export declare class DockerNode {
|
|
|
27
27
|
constructor(config: NodeConfig, credential: CredentialConfig, debug?: boolean);
|
|
28
28
|
/**
|
|
29
29
|
* 连接到 Docker (带重试)
|
|
30
|
+
* 前 3 次失败后每 1 分钟重试一次,直到成功
|
|
30
31
|
*/
|
|
31
32
|
connect(): Promise<void>;
|
|
32
33
|
/**
|
|
@@ -60,7 +61,10 @@ export declare class DockerNode {
|
|
|
60
61
|
/**
|
|
61
62
|
* 执行容器内命令
|
|
62
63
|
*/
|
|
63
|
-
execContainer(containerId: string, cmd: string): Promise<
|
|
64
|
+
execContainer(containerId: string, cmd: string): Promise<{
|
|
65
|
+
output: string;
|
|
66
|
+
exitCode: number;
|
|
67
|
+
}>;
|
|
64
68
|
/**
|
|
65
69
|
* 获取 Docker 版本信息
|
|
66
70
|
*/
|
|
@@ -71,6 +75,39 @@ export declare class DockerNode {
|
|
|
71
75
|
Arch: string;
|
|
72
76
|
KernelVersion: string;
|
|
73
77
|
}>;
|
|
78
|
+
/**
|
|
79
|
+
* 获取系统信息 (CPU、内存)
|
|
80
|
+
*/
|
|
81
|
+
getSystemInfo(): Promise<{
|
|
82
|
+
NCPU: number;
|
|
83
|
+
MemTotal: number;
|
|
84
|
+
MemAvailable?: number;
|
|
85
|
+
} | null>;
|
|
86
|
+
/**
|
|
87
|
+
* 获取容器数量
|
|
88
|
+
*/
|
|
89
|
+
getContainerCount(): Promise<{
|
|
90
|
+
running: number;
|
|
91
|
+
total: number;
|
|
92
|
+
}>;
|
|
93
|
+
/**
|
|
94
|
+
* 获取镜像数量
|
|
95
|
+
*/
|
|
96
|
+
getImageCount(): Promise<number>;
|
|
97
|
+
/**
|
|
98
|
+
* 获取容器的 Docker Compose 信息
|
|
99
|
+
* 通过标签 com.docker.compose.project.config_files 获取 compose 文件路径
|
|
100
|
+
*/
|
|
101
|
+
getContainerComposeInfo(containerId: string): Promise<ContainerComposeInfo | null>;
|
|
102
|
+
/**
|
|
103
|
+
* 获取容器的 Docker Compose 文件信息
|
|
104
|
+
* 读取并解析 compose 文件
|
|
105
|
+
*/
|
|
106
|
+
getComposeFileInfo(containerId: string): Promise<ComposeFileInfo | null>;
|
|
107
|
+
/**
|
|
108
|
+
* 统计 compose 文件中的服务数量
|
|
109
|
+
*/
|
|
110
|
+
private countServices;
|
|
74
111
|
/**
|
|
75
112
|
* 获取容器详细信息 (docker inspect)
|
|
76
113
|
*/
|
package/lib/service/node.js
CHANGED
|
@@ -38,6 +38,7 @@ class DockerNode {
|
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
40
|
* 连接到 Docker (带重试)
|
|
41
|
+
* 前 3 次失败后每 1 分钟重试一次,直到成功
|
|
41
42
|
*/
|
|
42
43
|
async connect() {
|
|
43
44
|
if (this.status === constants_1.NodeStatus.CONNECTING) {
|
|
@@ -46,10 +47,18 @@ class DockerNode {
|
|
|
46
47
|
}
|
|
47
48
|
this.status = constants_1.NodeStatus.CONNECTING;
|
|
48
49
|
let attempt = 0;
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
const MAX_INITIAL_ATTEMPTS = 3; // 前 3 次快速重试
|
|
51
|
+
const LONG_RETRY_INTERVAL = 60000; // 1 分钟
|
|
52
|
+
while (true) {
|
|
51
53
|
attempt++;
|
|
52
|
-
|
|
54
|
+
const isInitialAttempts = attempt <= MAX_INITIAL_ATTEMPTS;
|
|
55
|
+
const currentInterval = isInitialAttempts ? constants_1.RETRY_INTERVAL : LONG_RETRY_INTERVAL;
|
|
56
|
+
if (isInitialAttempts) {
|
|
57
|
+
logger_1.nodeLogger.info(`[${this.name}] 连接尝试 ${attempt}/${MAX_INITIAL_ATTEMPTS}...`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
logger_1.nodeLogger.info(`[${this.name}] 连接尝试 ${attempt} (每 ${LONG_RETRY_INTERVAL / 1000} 秒重试)...`);
|
|
61
|
+
}
|
|
53
62
|
try {
|
|
54
63
|
// 创建 connector
|
|
55
64
|
const connector = new connector_1.DockerConnector(this.config, { credentials: [this.credential], nodes: [this.config] });
|
|
@@ -74,20 +83,16 @@ class DockerNode {
|
|
|
74
83
|
return;
|
|
75
84
|
}
|
|
76
85
|
catch (error) {
|
|
77
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
86
|
+
const lastError = error instanceof Error ? error : new Error(String(error));
|
|
78
87
|
logger_1.nodeLogger.warn(`[${this.name}] 连接失败: ${lastError.message}`);
|
|
79
88
|
// 清理连接
|
|
80
89
|
this.connector?.dispose();
|
|
81
90
|
this.connector = null;
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
91
|
+
// 等待后重试
|
|
92
|
+
logger_1.nodeLogger.info(`[${this.name}] ${currentInterval / 1000} 秒后重试...`);
|
|
93
|
+
await new Promise(resolve => setTimeout(resolve, currentInterval));
|
|
86
94
|
}
|
|
87
95
|
}
|
|
88
|
-
// 所有重试都失败
|
|
89
|
-
this.status = constants_1.NodeStatus.ERROR;
|
|
90
|
-
logger_1.nodeLogger.error(`[${this.name}] 连接失败,已重试 ${constants_1.MAX_RETRY_COUNT} 次`);
|
|
91
96
|
}
|
|
92
97
|
/**
|
|
93
98
|
* 断开连接
|
|
@@ -173,6 +178,141 @@ class DockerNode {
|
|
|
173
178
|
KernelVersion: info.KernelVersion || 'unknown',
|
|
174
179
|
};
|
|
175
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* 获取系统信息 (CPU、内存)
|
|
183
|
+
*/
|
|
184
|
+
async getSystemInfo() {
|
|
185
|
+
if (!this.connector)
|
|
186
|
+
return null;
|
|
187
|
+
try {
|
|
188
|
+
// 使用 execWithExitCode 避免非零退出码抛出异常
|
|
189
|
+
const result = await this.connector.execWithExitCode('docker info --format "{{.NCPU}} {{.MemTotal}} {{.MemAvailable}}"');
|
|
190
|
+
logger_1.nodeLogger.debug(`[${this.name}] docker info 输出: "${result.output}", 退出码: ${result.exitCode}`);
|
|
191
|
+
// docker info 可能返回退出码 1 但仍有输出(权限问题),只要有输出就解析
|
|
192
|
+
if (!result.output.trim()) {
|
|
193
|
+
logger_1.nodeLogger.warn(`[${this.name}] docker info 输出为空`);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const parts = result.output.trim().split(/\s+/);
|
|
197
|
+
if (parts.length >= 2) {
|
|
198
|
+
return {
|
|
199
|
+
NCPU: parseInt(parts[0]) || 0,
|
|
200
|
+
MemTotal: parseInt(parts[1]) || 0,
|
|
201
|
+
MemAvailable: parts[2] ? parseInt(parts[2]) : undefined,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
logger_1.nodeLogger.warn(`[${this.name}] 获取系统信息异常: ${e}`);
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 获取容器数量
|
|
213
|
+
*/
|
|
214
|
+
async getContainerCount() {
|
|
215
|
+
if (!this.connector)
|
|
216
|
+
throw new Error('未连接');
|
|
217
|
+
try {
|
|
218
|
+
const running = await this.connector.exec('docker ps -q | wc -l');
|
|
219
|
+
const total = await this.connector.exec('docker ps -aq | wc -l');
|
|
220
|
+
return {
|
|
221
|
+
running: parseInt(running.trim()) || 0,
|
|
222
|
+
total: parseInt(total.trim()) || 0,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return { running: 0, total: 0 };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* 获取镜像数量
|
|
231
|
+
*/
|
|
232
|
+
async getImageCount() {
|
|
233
|
+
if (!this.connector)
|
|
234
|
+
throw new Error('未连接');
|
|
235
|
+
try {
|
|
236
|
+
const output = await this.connector.exec('docker images -q | wc -l');
|
|
237
|
+
return parseInt(output.trim()) || 0;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* 获取容器的 Docker Compose 信息
|
|
245
|
+
* 通过标签 com.docker.compose.project.config_files 获取 compose 文件路径
|
|
246
|
+
*/
|
|
247
|
+
async getContainerComposeInfo(containerId) {
|
|
248
|
+
if (!this.connector)
|
|
249
|
+
throw new Error('未连接');
|
|
250
|
+
try {
|
|
251
|
+
// 使用 docker inspect 获取容器标签
|
|
252
|
+
const output = await this.connector.exec(`docker inspect ${containerId} --format "{{json .Config.Labels}}"`);
|
|
253
|
+
if (!output.trim()) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const labels = JSON.parse(output);
|
|
257
|
+
// 获取 compose 项目名称和配置文件路径
|
|
258
|
+
const projectName = labels['com.docker.compose.project'] || '';
|
|
259
|
+
const configFiles = labels['com.docker.compose.project.config_files'] || '';
|
|
260
|
+
if (!projectName || !configFiles) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
containerId,
|
|
265
|
+
containerName: labels['com.docker.compose.container-number'] || '',
|
|
266
|
+
projectName,
|
|
267
|
+
composeFilePath: configFiles,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
catch (e) {
|
|
271
|
+
logger_1.nodeLogger.warn(`[${this.name}] 获取容器 ${containerId} 的 compose 信息失败: ${e}`);
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* 获取容器的 Docker Compose 文件信息
|
|
277
|
+
* 读取并解析 compose 文件
|
|
278
|
+
*/
|
|
279
|
+
async getComposeFileInfo(containerId) {
|
|
280
|
+
if (!this.connector)
|
|
281
|
+
throw new Error('未连接');
|
|
282
|
+
try {
|
|
283
|
+
const composeInfo = await this.getContainerComposeInfo(containerId);
|
|
284
|
+
if (!composeInfo) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
const filePath = composeInfo.composeFilePath;
|
|
288
|
+
const originalPath = filePath;
|
|
289
|
+
// 尝试读取文件 (支持 Windows 路径自动转换)
|
|
290
|
+
const content = await this.connector.readFile(filePath);
|
|
291
|
+
// 统计服务数量 (简单的 yaml 解析)
|
|
292
|
+
const serviceCount = this.countServices(content);
|
|
293
|
+
return {
|
|
294
|
+
originalPath,
|
|
295
|
+
effectivePath: filePath,
|
|
296
|
+
usedWslPath: false, // 实际是否使用了 WSL 路径在内部处理
|
|
297
|
+
content,
|
|
298
|
+
projectName: composeInfo.projectName,
|
|
299
|
+
serviceCount,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
catch (e) {
|
|
303
|
+
logger_1.nodeLogger.warn(`[${this.name}] 获取 compose 文件信息失败: ${e.message}`);
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* 统计 compose 文件中的服务数量
|
|
309
|
+
*/
|
|
310
|
+
countServices(content) {
|
|
311
|
+
// 简单的正则匹配 services: 下面的服务名
|
|
312
|
+
const servicePattern = /^[a-zA-Z0-9_-]+:\s*$/gm;
|
|
313
|
+
const matches = content.match(servicePattern);
|
|
314
|
+
return matches ? matches.length : 0;
|
|
315
|
+
}
|
|
176
316
|
/**
|
|
177
317
|
* 获取容器详细信息 (docker inspect)
|
|
178
318
|
*/
|
package/lib/types.d.ts
CHANGED
|
@@ -98,3 +98,27 @@ export interface SubscriptionConfig {
|
|
|
98
98
|
/** 创建时间 */
|
|
99
99
|
createdAt?: number;
|
|
100
100
|
}
|
|
101
|
+
export interface ComposeFileInfo {
|
|
102
|
+
/** 原始路径 (Windows 路径或 WSL 路径) */
|
|
103
|
+
originalPath: string;
|
|
104
|
+
/** 实际使用的路径 (尝试转换后的路径) */
|
|
105
|
+
effectivePath: string;
|
|
106
|
+
/** 是否使用了 WSL 路径转换 */
|
|
107
|
+
usedWslPath: boolean;
|
|
108
|
+
/** Compose 文件内容 */
|
|
109
|
+
content: string;
|
|
110
|
+
/** 所属项目名称 */
|
|
111
|
+
projectName: string;
|
|
112
|
+
/** 容器数量 */
|
|
113
|
+
serviceCount: number;
|
|
114
|
+
}
|
|
115
|
+
export interface ContainerComposeInfo {
|
|
116
|
+
/** 容器 ID */
|
|
117
|
+
containerId: string;
|
|
118
|
+
/** 容器名称 */
|
|
119
|
+
containerName: string;
|
|
120
|
+
/** 所属 Docker Compose 项目 */
|
|
121
|
+
projectName: string;
|
|
122
|
+
/** Compose 文件路径 */
|
|
123
|
+
composeFilePath: string;
|
|
124
|
+
}
|
package/lib/utils/render.d.ts
CHANGED
|
@@ -36,5 +36,17 @@ export declare function generateNodesHtml(nodes: any[]): string;
|
|
|
36
36
|
/**
|
|
37
37
|
* 生成节点详情 HTML
|
|
38
38
|
*/
|
|
39
|
-
export declare function generateNodeDetailHtml(node: any, version: any): string;
|
|
39
|
+
export declare function generateNodeDetailHtml(node: any, version: any, systemInfo?: any): string;
|
|
40
|
+
/**
|
|
41
|
+
* 生成日志 HTML
|
|
42
|
+
*/
|
|
43
|
+
export declare function generateLogsHtml(nodeName: string, containerName: string, logs: string, lineCount: number): string;
|
|
44
|
+
/**
|
|
45
|
+
* 生成执行结果 HTML
|
|
46
|
+
*/
|
|
47
|
+
export declare function generateExecHtml(nodeName: string, containerName: string, command: string, output: string, exitCode: number): string;
|
|
48
|
+
/**
|
|
49
|
+
* 生成 Docker Compose 配置 HTML
|
|
50
|
+
*/
|
|
51
|
+
export declare function generateComposeHtml(nodeName: string, containerName: string, projectName: string, filePath: string, serviceCount: number, composeContent: string): string;
|
|
40
52
|
export {};
|