koishi-plugin-docker-control 0.0.2 → 0.0.4
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/control.d.ts +1 -1
- package/lib/commands/control.js +45 -6
- package/lib/commands/index.js +48 -60
- package/lib/commands/list.js +13 -155
- package/lib/commands/logs.d.ts +1 -1
- package/lib/commands/logs.js +29 -39
- package/lib/index.js +1 -1
- package/lib/service/connector.d.ts +11 -1
- package/lib/service/connector.js +38 -1
- package/lib/service/index.d.ts +7 -0
- package/lib/service/index.js +17 -0
- package/lib/service/node.d.ts +38 -1
- package/lib/service/node.js +97 -11
- package/lib/utils/render.d.ts +48 -0
- package/lib/utils/render.js +682 -0
- package/package.json +2 -2
package/lib/service/connector.js
CHANGED
|
@@ -77,6 +77,43 @@ class DockerConnector {
|
|
|
77
77
|
});
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* 执行命令并返回输出和退出码
|
|
82
|
+
*/
|
|
83
|
+
async execWithExitCode(command) {
|
|
84
|
+
const client = await this.getConnection();
|
|
85
|
+
logger_1.connectorLogger.debug(`[${this.config.name}] 执行命令: ${command}`);
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
client.exec(command, (err, stream) => {
|
|
88
|
+
if (err) {
|
|
89
|
+
logger_1.connectorLogger.debug(`[${this.config.name}] 命令执行错误: ${err.message}`);
|
|
90
|
+
reject(err);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
let stdout = '';
|
|
94
|
+
let stderr = '';
|
|
95
|
+
let exitCode = null;
|
|
96
|
+
stream.on('close', (code, signal) => {
|
|
97
|
+
logger_1.connectorLogger.debug(`[${this.config.name}] 命令完成: code=${code}, signal=${signal}`);
|
|
98
|
+
exitCode = code ?? 0;
|
|
99
|
+
// 显式结束 stream 防止 channel 泄露
|
|
100
|
+
try {
|
|
101
|
+
stream.end();
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
// 可能已经关闭,忽略错误
|
|
105
|
+
}
|
|
106
|
+
resolve({ output: stdout.trim(), exitCode });
|
|
107
|
+
});
|
|
108
|
+
stream.on('data', (data) => {
|
|
109
|
+
stdout += data.toString();
|
|
110
|
+
});
|
|
111
|
+
stream.on('err', (data) => {
|
|
112
|
+
stderr += data.toString();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
80
117
|
/**
|
|
81
118
|
* 执行 docker ps 获取容器列表
|
|
82
119
|
*/
|
|
@@ -115,7 +152,7 @@ class DockerConnector {
|
|
|
115
152
|
async execContainer(containerId, cmd) {
|
|
116
153
|
// 使用 docker exec 需要处理引号
|
|
117
154
|
const escapedCmd = cmd.replace(/'/g, "'\\''");
|
|
118
|
-
return this.
|
|
155
|
+
return this.execWithExitCode(`docker exec ${containerId} sh -c '${escapedCmd}'`);
|
|
119
156
|
}
|
|
120
157
|
/**
|
|
121
158
|
* 监听 Docker 事件流
|
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
|
@@ -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,43 @@ 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
|
+
}>;
|
|
68
|
+
/**
|
|
69
|
+
* 获取 Docker 版本信息
|
|
70
|
+
*/
|
|
71
|
+
getVersion(): Promise<{
|
|
72
|
+
Version: string;
|
|
73
|
+
ApiVersion: string;
|
|
74
|
+
Os: string;
|
|
75
|
+
Arch: string;
|
|
76
|
+
KernelVersion: string;
|
|
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 inspect)
|
|
99
|
+
*/
|
|
100
|
+
getContainer(containerId: string): Promise<any>;
|
|
64
101
|
/**
|
|
65
102
|
* 解析 docker ps 输出
|
|
66
103
|
*/
|
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
|
* 断开连接
|
|
@@ -157,6 +162,87 @@ class DockerNode {
|
|
|
157
162
|
throw new Error('未连接');
|
|
158
163
|
return this.connector.execContainer(containerId, cmd);
|
|
159
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* 获取 Docker 版本信息
|
|
167
|
+
*/
|
|
168
|
+
async getVersion() {
|
|
169
|
+
if (!this.connector)
|
|
170
|
+
throw new Error('未连接');
|
|
171
|
+
const output = await this.connector.exec('docker version --format "{{json .Server}}"');
|
|
172
|
+
const info = JSON.parse(output);
|
|
173
|
+
return {
|
|
174
|
+
Version: info.Version || 'unknown',
|
|
175
|
+
ApiVersion: info.ApiVersion || 'unknown',
|
|
176
|
+
Os: info.Os || 'unknown',
|
|
177
|
+
Arch: info.Arch || 'unknown',
|
|
178
|
+
KernelVersion: info.KernelVersion || 'unknown',
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* 获取系统信息 (CPU、内存)
|
|
183
|
+
*/
|
|
184
|
+
async getSystemInfo() {
|
|
185
|
+
if (!this.connector)
|
|
186
|
+
return null;
|
|
187
|
+
try {
|
|
188
|
+
// 使用更可靠的格式
|
|
189
|
+
const output = await this.connector.exec('docker info --format "{{.NCPU}} {{.MemTotal}} {{.MemAvailable}}"');
|
|
190
|
+
const parts = output.trim().split(/\s+/);
|
|
191
|
+
if (parts.length >= 2) {
|
|
192
|
+
return {
|
|
193
|
+
NCPU: parseInt(parts[0]) || 0,
|
|
194
|
+
MemTotal: parseInt(parts[1]) || 0,
|
|
195
|
+
MemAvailable: parts[2] ? parseInt(parts[2]) : undefined,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* 获取容器数量
|
|
206
|
+
*/
|
|
207
|
+
async getContainerCount() {
|
|
208
|
+
if (!this.connector)
|
|
209
|
+
throw new Error('未连接');
|
|
210
|
+
try {
|
|
211
|
+
const running = await this.connector.exec('docker ps -q | wc -l');
|
|
212
|
+
const total = await this.connector.exec('docker ps -aq | wc -l');
|
|
213
|
+
return {
|
|
214
|
+
running: parseInt(running.trim()) || 0,
|
|
215
|
+
total: parseInt(total.trim()) || 0,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return { running: 0, total: 0 };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* 获取镜像数量
|
|
224
|
+
*/
|
|
225
|
+
async getImageCount() {
|
|
226
|
+
if (!this.connector)
|
|
227
|
+
throw new Error('未连接');
|
|
228
|
+
try {
|
|
229
|
+
const output = await this.connector.exec('docker images -q | wc -l');
|
|
230
|
+
return parseInt(output.trim()) || 0;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return 0;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* 获取容器详细信息 (docker inspect)
|
|
238
|
+
*/
|
|
239
|
+
async getContainer(containerId) {
|
|
240
|
+
if (!this.connector)
|
|
241
|
+
throw new Error('未连接');
|
|
242
|
+
const output = await this.connector.exec(`docker inspect ${containerId}`);
|
|
243
|
+
const info = JSON.parse(output);
|
|
244
|
+
return Array.isArray(info) ? info[0] : info;
|
|
245
|
+
}
|
|
160
246
|
/**
|
|
161
247
|
* 解析 docker ps 输出
|
|
162
248
|
*/
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import type { ContainerInfo } from '../types';
|
|
3
|
+
interface RenderOptions {
|
|
4
|
+
title?: string;
|
|
5
|
+
width?: number;
|
|
6
|
+
height?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* 通用渲染函数:将 HTML 转换为图片
|
|
10
|
+
*/
|
|
11
|
+
export declare function renderToImage(ctx: Context, html: string, options?: RenderOptions): Promise<string>;
|
|
12
|
+
/**
|
|
13
|
+
* 生成容器列表 HTML
|
|
14
|
+
*/
|
|
15
|
+
export declare function generateListHtml(data: Array<{
|
|
16
|
+
node: any;
|
|
17
|
+
containers: ContainerInfo[];
|
|
18
|
+
}>, title?: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* 生成操作结果 HTML (启动/停止/重启)
|
|
21
|
+
*/
|
|
22
|
+
export declare function generateResultHtml(results: Array<{
|
|
23
|
+
node: any;
|
|
24
|
+
container?: any;
|
|
25
|
+
success: boolean;
|
|
26
|
+
error?: string;
|
|
27
|
+
}>, title: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* 生成详情 HTML
|
|
30
|
+
*/
|
|
31
|
+
export declare function generateInspectHtml(nodeName: string, info: any): string;
|
|
32
|
+
/**
|
|
33
|
+
* 生成节点列表 HTML
|
|
34
|
+
*/
|
|
35
|
+
export declare function generateNodesHtml(nodes: any[]): string;
|
|
36
|
+
/**
|
|
37
|
+
* 生成节点详情 HTML
|
|
38
|
+
*/
|
|
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
|
+
export {};
|