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/commands/control.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerControlCommands = registerControlCommands;
|
|
4
4
|
const logger_1 = require("../utils/logger");
|
|
5
|
+
const render_1 = require("../utils/render");
|
|
5
6
|
/**
|
|
6
7
|
* 格式化容器搜索结果
|
|
7
8
|
*/
|
|
@@ -27,7 +28,8 @@ function formatSearchResults(results, operation) {
|
|
|
27
28
|
/**
|
|
28
29
|
* 注册控制指令
|
|
29
30
|
*/
|
|
30
|
-
function registerControlCommands(ctx, getService) {
|
|
31
|
+
function registerControlCommands(ctx, getService, config) {
|
|
32
|
+
const useImageOutput = config?.imageOutput === true;
|
|
31
33
|
/**
|
|
32
34
|
* 启动容器
|
|
33
35
|
*/
|
|
@@ -47,6 +49,10 @@ function registerControlCommands(ctx, getService) {
|
|
|
47
49
|
logger_1.commandLogger.debug('批量启动容器');
|
|
48
50
|
// 批量操作
|
|
49
51
|
const results = await service.operateContainers(selector, container, 'start');
|
|
52
|
+
if (useImageOutput && ctx.puppeteer) {
|
|
53
|
+
const html = (0, render_1.generateResultHtml)(results, '批量启动结果');
|
|
54
|
+
return await (0, render_1.renderToImage)(ctx, html);
|
|
55
|
+
}
|
|
50
56
|
return formatSearchResults(results, '启动');
|
|
51
57
|
}
|
|
52
58
|
// 单个容器
|
|
@@ -55,11 +61,21 @@ function registerControlCommands(ctx, getService) {
|
|
|
55
61
|
logger_1.commandLogger.debug(`找到容器: ${found.Names[0]} 在节点 ${node.name}`);
|
|
56
62
|
await node.startContainer(found.Id);
|
|
57
63
|
logger_1.commandLogger.debug(`容器已启动: ${found.Id}`);
|
|
64
|
+
if (useImageOutput && ctx.puppeteer) {
|
|
65
|
+
const results = [{ node, container: found, success: true }];
|
|
66
|
+
const html = (0, render_1.generateResultHtml)(results, '启动成功');
|
|
67
|
+
return await (0, render_1.renderToImage)(ctx, html);
|
|
68
|
+
}
|
|
58
69
|
const name = found.Names[0]?.replace('/', '') || found.Id.slice(0, 8);
|
|
59
70
|
return `✅ ${node.name}: ${name} 已启动`;
|
|
60
71
|
}
|
|
61
72
|
catch (e) {
|
|
62
73
|
logger_1.commandLogger.error(`启动容器失败: ${e.message}`);
|
|
74
|
+
if (useImageOutput && ctx.puppeteer) {
|
|
75
|
+
// 尝试构造一个失败的结果用于渲染,虽然这里可能没有 node/container 信息
|
|
76
|
+
// 如果找不到容器,e 可能是 "找不到容器"
|
|
77
|
+
return `❌ 启动失败: ${e.message}`;
|
|
78
|
+
}
|
|
63
79
|
return `❌ 启动失败: ${e.message}`;
|
|
64
80
|
}
|
|
65
81
|
});
|
|
@@ -82,6 +98,10 @@ function registerControlCommands(ctx, getService) {
|
|
|
82
98
|
logger_1.commandLogger.debug('批量停止容器');
|
|
83
99
|
// 批量操作
|
|
84
100
|
const results = await service.operateContainers(selector, container, 'stop');
|
|
101
|
+
if (useImageOutput && ctx.puppeteer) {
|
|
102
|
+
const html = (0, render_1.generateResultHtml)(results, '批量停止结果');
|
|
103
|
+
return await (0, render_1.renderToImage)(ctx, html);
|
|
104
|
+
}
|
|
85
105
|
return formatSearchResults(results, '停止');
|
|
86
106
|
}
|
|
87
107
|
logger_1.commandLogger.debug(`查找容器: ${container} 在 ${selector}`);
|
|
@@ -89,6 +109,11 @@ function registerControlCommands(ctx, getService) {
|
|
|
89
109
|
logger_1.commandLogger.debug(`找到容器: ${found.Names[0]} 在节点 ${node.name}`);
|
|
90
110
|
await node.stopContainer(found.Id);
|
|
91
111
|
logger_1.commandLogger.debug(`容器已停止: ${found.Id}`);
|
|
112
|
+
if (useImageOutput && ctx.puppeteer) {
|
|
113
|
+
const results = [{ node, container: found, success: true }];
|
|
114
|
+
const html = (0, render_1.generateResultHtml)(results, '停止成功');
|
|
115
|
+
return await (0, render_1.renderToImage)(ctx, html);
|
|
116
|
+
}
|
|
92
117
|
const name = found.Names[0]?.replace('/', '') || found.Id.slice(0, 8);
|
|
93
118
|
return `✅ ${node.name}: ${name} 已停止`;
|
|
94
119
|
}
|
|
@@ -116,6 +141,10 @@ function registerControlCommands(ctx, getService) {
|
|
|
116
141
|
logger_1.commandLogger.debug('批量重启容器');
|
|
117
142
|
// 批量操作
|
|
118
143
|
const results = await service.operateContainers(selector, container, 'restart');
|
|
144
|
+
if (useImageOutput && ctx.puppeteer) {
|
|
145
|
+
const html = (0, render_1.generateResultHtml)(results, '批量重启结果');
|
|
146
|
+
return await (0, render_1.renderToImage)(ctx, html);
|
|
147
|
+
}
|
|
119
148
|
return formatSearchResults(results, '重启');
|
|
120
149
|
}
|
|
121
150
|
logger_1.commandLogger.debug(`查找容器: ${container} 在 ${selector}`);
|
|
@@ -123,6 +152,11 @@ function registerControlCommands(ctx, getService) {
|
|
|
123
152
|
logger_1.commandLogger.debug(`找到容器: ${found.Names[0]} 在节点 ${node.name}`);
|
|
124
153
|
await node.restartContainer(found.Id);
|
|
125
154
|
logger_1.commandLogger.debug(`容器已重启: ${found.Id}`);
|
|
155
|
+
if (useImageOutput && ctx.puppeteer) {
|
|
156
|
+
const results = [{ node, container: found, success: true }];
|
|
157
|
+
const html = (0, render_1.generateResultHtml)(results, '重启成功');
|
|
158
|
+
return await (0, render_1.renderToImage)(ctx, html);
|
|
159
|
+
}
|
|
126
160
|
const name = found.Names[0]?.replace('/', '') || found.Id.slice(0, 8);
|
|
127
161
|
return `✅ ${node.name}: ${name} 已重启`;
|
|
128
162
|
}
|
|
@@ -145,6 +179,10 @@ function registerControlCommands(ctx, getService) {
|
|
|
145
179
|
try {
|
|
146
180
|
const { node, container: found } = await service.findContainer(selector, container);
|
|
147
181
|
const info = await node.getContainer(found.Id);
|
|
182
|
+
if (useImageOutput && ctx.puppeteer) {
|
|
183
|
+
const html = (0, render_1.generateInspectHtml)(node.name, info);
|
|
184
|
+
return await (0, render_1.renderToImage)(ctx, html);
|
|
185
|
+
}
|
|
148
186
|
const lines = [
|
|
149
187
|
`名称: ${info.Name.replace('/', '')}`,
|
|
150
188
|
`ID: ${info.Id.slice(0, 12)}`,
|
|
@@ -181,12 +219,13 @@ function registerControlCommands(ctx, getService) {
|
|
|
181
219
|
logger_1.commandLogger.info(`[${selector}] 执行命令: "${cmd}"`);
|
|
182
220
|
try {
|
|
183
221
|
const { node, container: found } = await service.findContainer(selector, container);
|
|
184
|
-
const result = await node.execContainer(found.Id,
|
|
185
|
-
'/bin/sh',
|
|
186
|
-
'-c',
|
|
187
|
-
cmd,
|
|
188
|
-
]);
|
|
222
|
+
const result = await node.execContainer(found.Id, cmd);
|
|
189
223
|
const name = found.Names[0]?.replace('/', '') || found.Id.slice(0, 8);
|
|
224
|
+
// 图片渲染模式
|
|
225
|
+
if (useImageOutput && ctx.puppeteer) {
|
|
226
|
+
const html = (0, render_1.generateExecHtml)(node.name, name, cmd, result.output, result.exitCode);
|
|
227
|
+
return await (0, render_1.renderToImage)(ctx, html);
|
|
228
|
+
}
|
|
190
229
|
if (result.output.trim()) {
|
|
191
230
|
return `=== ${node.name}: ${name} ===\n${result.output}`;
|
|
192
231
|
}
|
package/lib/commands/index.js
CHANGED
|
@@ -4,21 +4,23 @@ exports.registerCommands = registerCommands;
|
|
|
4
4
|
const list_1 = require("./list");
|
|
5
5
|
const control_1 = require("./control");
|
|
6
6
|
const logs_1 = require("./logs");
|
|
7
|
+
const render_1 = require("../utils/render");
|
|
7
8
|
/**
|
|
8
9
|
* 注册所有指令
|
|
9
10
|
*/
|
|
10
11
|
function registerCommands(ctx, getService, config) {
|
|
11
12
|
// 注册各模块指令
|
|
12
13
|
(0, list_1.registerListCommand)(ctx, getService, config);
|
|
13
|
-
(0, control_1.registerControlCommands)(ctx, getService);
|
|
14
|
+
(0, control_1.registerControlCommands)(ctx, getService, config);
|
|
14
15
|
(0, logs_1.registerLogsCommand)(ctx, getService, config);
|
|
15
16
|
// 注册辅助指令
|
|
16
|
-
registerHelperCommands(ctx, getService);
|
|
17
|
+
registerHelperCommands(ctx, getService, config);
|
|
17
18
|
}
|
|
18
19
|
/**
|
|
19
20
|
* 注册辅助指令
|
|
20
21
|
*/
|
|
21
|
-
function registerHelperCommands(ctx, getService) {
|
|
22
|
+
function registerHelperCommands(ctx, getService, config) {
|
|
23
|
+
const useImageOutput = config?.imageOutput === true;
|
|
22
24
|
/**
|
|
23
25
|
* 查看节点列表
|
|
24
26
|
*/
|
|
@@ -31,6 +33,11 @@ function registerHelperCommands(ctx, getService) {
|
|
|
31
33
|
if (nodes.length === 0) {
|
|
32
34
|
return '未配置任何节点';
|
|
33
35
|
}
|
|
36
|
+
const online = nodes.filter((n) => n.status === 'connected').length;
|
|
37
|
+
if (useImageOutput && ctx.puppeteer) {
|
|
38
|
+
const html = (0, render_1.generateNodesHtml)(nodes);
|
|
39
|
+
return await (0, render_1.renderToImage)(ctx, html);
|
|
40
|
+
}
|
|
34
41
|
const lines = ['=== Docker 节点 ==='];
|
|
35
42
|
for (const node of nodes) {
|
|
36
43
|
const statusIcon = node.status === 'connected'
|
|
@@ -41,7 +48,6 @@ function registerHelperCommands(ctx, getService) {
|
|
|
41
48
|
const tags = node.tags.length > 0 ? ` [@${node.tags.join(' @')}]` : '';
|
|
42
49
|
lines.push(`${statusIcon} ${node.name} (${node.id})${tags} - ${node.status}`);
|
|
43
50
|
}
|
|
44
|
-
const online = nodes.filter((n) => n.status === 'connected').length;
|
|
45
51
|
lines.push(`\n总计: ${nodes.length} 个节点,${online} 个在线`);
|
|
46
52
|
return lines.join('\n');
|
|
47
53
|
});
|
|
@@ -62,12 +68,36 @@ function registerHelperCommands(ctx, getService) {
|
|
|
62
68
|
}
|
|
63
69
|
const node = nodes[0];
|
|
64
70
|
try {
|
|
65
|
-
const version = await
|
|
71
|
+
const [version, systemInfo, containerCount, imageCount] = await Promise.all([
|
|
72
|
+
node.getVersion(),
|
|
73
|
+
node.getSystemInfo(),
|
|
74
|
+
node.getContainerCount(),
|
|
75
|
+
node.getImageCount(),
|
|
76
|
+
]);
|
|
77
|
+
// 将容器和镜像数量添加到节点对象
|
|
78
|
+
const nodeData = {
|
|
79
|
+
...node,
|
|
80
|
+
containerCount: containerCount.total,
|
|
81
|
+
imageCount: imageCount,
|
|
82
|
+
};
|
|
83
|
+
if (useImageOutput && ctx.puppeteer) {
|
|
84
|
+
const html = (0, render_1.generateNodeDetailHtml)(nodeData, version, systemInfo);
|
|
85
|
+
return await (0, render_1.renderToImage)(ctx, html);
|
|
86
|
+
}
|
|
87
|
+
const memoryUsed = systemInfo?.MemTotal && systemInfo?.MemAvailable !== undefined
|
|
88
|
+
? `${Math.round((1 - systemInfo.MemAvailable / systemInfo.MemTotal) * 100)}%`
|
|
89
|
+
: '-';
|
|
90
|
+
const nodeName = node.config?.name || node.name || node.Name || 'Unknown';
|
|
91
|
+
const nodeId = node.id || node.ID || node.Id || node.config?.id || '-';
|
|
66
92
|
const lines = [
|
|
67
|
-
`=== ${
|
|
68
|
-
`ID: ${
|
|
69
|
-
`状态: ${node.status}`,
|
|
70
|
-
`标签: ${node.tags.join(', ') || '无'}`,
|
|
93
|
+
`=== ${nodeName} ===`,
|
|
94
|
+
`ID: ${nodeId}`,
|
|
95
|
+
`状态: ${node.status || node.Status || 'unknown'}`,
|
|
96
|
+
`标签: ${node.tags?.join(', ') || node.config?.tags?.join(', ') || '无'}`,
|
|
97
|
+
`CPU: ${systemInfo?.NCPU || '-'} 核心`,
|
|
98
|
+
`内存: ${memoryUsed} (可用: ${systemInfo?.MemAvailable ? Math.round(systemInfo.MemAvailable / 1024 / 1024) + ' MB' : '-'})`,
|
|
99
|
+
`容器: ${containerCount.running}/${containerCount.total} 运行中`,
|
|
100
|
+
`镜像: ${imageCount} 个`,
|
|
71
101
|
`Docker 版本: ${version.Version}`,
|
|
72
102
|
`API 版本: ${version.ApiVersion}`,
|
|
73
103
|
`操作系统: ${version.Os} (${version.Arch})`,
|
|
@@ -136,7 +166,7 @@ function registerHelperCommands(ctx, getService) {
|
|
|
136
166
|
if (c.State !== 'running') {
|
|
137
167
|
return `容器 ${container} 未运行`;
|
|
138
168
|
}
|
|
139
|
-
const result = await node.execContainer(c.Id, cmd
|
|
169
|
+
const result = await node.execContainer(c.Id, cmd);
|
|
140
170
|
return [
|
|
141
171
|
`=== 执行结果 ===`,
|
|
142
172
|
`退出码: ${result.exitCode}`,
|
|
@@ -148,47 +178,6 @@ function registerHelperCommands(ctx, getService) {
|
|
|
148
178
|
return `执行失败: ${e.message}`;
|
|
149
179
|
}
|
|
150
180
|
});
|
|
151
|
-
/**
|
|
152
|
-
* 交互式执行 (返回结果,不支持实时交互)
|
|
153
|
-
*/
|
|
154
|
-
ctx
|
|
155
|
-
.command('docker.shell <container> <cmd>', '在容器中执行命令(交互式)')
|
|
156
|
-
.alias('dockershell', '容器shell')
|
|
157
|
-
.option('node', '-n <node> 指定节点', { fallback: '' })
|
|
158
|
-
.option('timeout', '-t <seconds> 超时时间', { fallback: 30 })
|
|
159
|
-
.action(async ({ options }, container, cmd) => {
|
|
160
|
-
const service = getService();
|
|
161
|
-
if (!service) {
|
|
162
|
-
return 'Docker 服务未初始化';
|
|
163
|
-
}
|
|
164
|
-
const nodeSelector = options.node || 'all';
|
|
165
|
-
try {
|
|
166
|
-
const nodes = service.getNodesBySelector(nodeSelector);
|
|
167
|
-
if (nodes.length === 0) {
|
|
168
|
-
return `未找到节点: ${nodeSelector}`;
|
|
169
|
-
}
|
|
170
|
-
const results = await service.findContainerGlobal(container);
|
|
171
|
-
if (results.length === 0) {
|
|
172
|
-
return `未找到容器: ${container}`;
|
|
173
|
-
}
|
|
174
|
-
const { node, container: c } = results[0];
|
|
175
|
-
if (c.State !== 'running') {
|
|
176
|
-
return `容器 ${container} 未运行`;
|
|
177
|
-
}
|
|
178
|
-
const result = await node.execContainer(c.Id, cmd.split(' '));
|
|
179
|
-
return [
|
|
180
|
-
`=== ${node.name}/${c.Names[0]?.replace('/', '') || c.Id.slice(0, 8)} ===`,
|
|
181
|
-
`> ${cmd}`,
|
|
182
|
-
``,
|
|
183
|
-
result.output || '(无输出)',
|
|
184
|
-
``,
|
|
185
|
-
`[退出码: ${result.exitCode}]`,
|
|
186
|
-
].join('\n');
|
|
187
|
-
}
|
|
188
|
-
catch (e) {
|
|
189
|
-
return `执行失败: ${e.message}`;
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
181
|
/**
|
|
193
182
|
* 查看帮助
|
|
194
183
|
*/
|
|
@@ -200,15 +189,14 @@ function registerHelperCommands(ctx, getService) {
|
|
|
200
189
|
' docker.nodes - 查看节点列表',
|
|
201
190
|
' docker.node <节点> - 查看节点详情',
|
|
202
191
|
'',
|
|
203
|
-
'【容器操作】',
|
|
204
|
-
' docker.ls
|
|
205
|
-
' docker.start <容器>
|
|
206
|
-
' docker.stop <容器>
|
|
207
|
-
' docker.restart <容器>
|
|
208
|
-
' docker.logs <容器> [-
|
|
209
|
-
' docker.
|
|
210
|
-
' docker.exec <容器> <命令>
|
|
211
|
-
' docker.shell <容器> <命令> - 交互式执行',
|
|
192
|
+
'【容器操作】(参数顺序: 节点 容器)',
|
|
193
|
+
' docker.ls <节点> - 列出容器',
|
|
194
|
+
' docker.start <节点> <容器> - 启动容器',
|
|
195
|
+
' docker.stop <节点> <容器> - 停止容器',
|
|
196
|
+
' docker.restart <节点> <容器> - 重启容器',
|
|
197
|
+
' docker.logs <节点> <容器> [-n 行数] - 查看日志',
|
|
198
|
+
' docker.inspect <节点> <容器> - 查看容器详情',
|
|
199
|
+
' docker.exec <节点> <容器> <命令> - 在容器内执行命令',
|
|
212
200
|
'',
|
|
213
201
|
'【节点选择器】',
|
|
214
202
|
' all - 所有节点',
|
package/lib/commands/list.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerListCommand = registerListCommand;
|
|
4
|
-
/**
|
|
5
|
-
* 列出容器指令
|
|
6
|
-
* docker.ls - 支持集群视图和图片渲染
|
|
7
|
-
*/
|
|
8
|
-
const koishi_1 = require("koishi");
|
|
9
4
|
const logger_1 = require("../utils/logger");
|
|
5
|
+
const render_1 = require("../utils/render");
|
|
10
6
|
function registerListCommand(ctx, getService, config) {
|
|
11
7
|
// 检查是否启用了图片输出
|
|
12
8
|
const useImageOutput = config?.imageOutput === true;
|
|
@@ -34,6 +30,10 @@ function registerListCommand(ctx, getService, config) {
|
|
|
34
30
|
if (!ctx.puppeteer) {
|
|
35
31
|
return '错误: 未安装 koishi-plugin-puppeteer 插件,无法使用图片渲染';
|
|
36
32
|
}
|
|
33
|
+
// 如果未指定节点,提示用户
|
|
34
|
+
if (!selector) {
|
|
35
|
+
return '请指定节点名称、ID 或标签,或使用 "all" 列出全部容器\n例如: docker.ls @web -f image 或 docker.ls all -f image';
|
|
36
|
+
}
|
|
37
37
|
try {
|
|
38
38
|
// 获取容器数据
|
|
39
39
|
logger_1.commandLogger.debug('获取容器数据...');
|
|
@@ -42,18 +42,9 @@ function registerListCommand(ctx, getService, config) {
|
|
|
42
42
|
if (results.length === 0) {
|
|
43
43
|
return '未发现任何容器';
|
|
44
44
|
}
|
|
45
|
-
//
|
|
46
|
-
const html =
|
|
47
|
-
|
|
48
|
-
logger_1.commandLogger.debug('渲染图片中...');
|
|
49
|
-
const imageElement = await ctx.puppeteer.render(html, async (page, next) => {
|
|
50
|
-
await page.setViewport({ width: 600, height: 800 });
|
|
51
|
-
const body = await page.$('body');
|
|
52
|
-
const clip = await body.boundingBox();
|
|
53
|
-
const buffer = await page.screenshot({ clip });
|
|
54
|
-
return koishi_1.h.image(buffer, 'image/png').toString();
|
|
55
|
-
});
|
|
56
|
-
return imageElement;
|
|
45
|
+
// 生成并渲染
|
|
46
|
+
const html = (0, render_1.generateListHtml)(results, `容器列表 (${selector})`);
|
|
47
|
+
return await (0, render_1.renderToImage)(ctx, html);
|
|
57
48
|
}
|
|
58
49
|
catch (e) {
|
|
59
50
|
logger_1.commandLogger.error(`图片渲染失败: ${e.message}`);
|
|
@@ -62,9 +53,13 @@ function registerListCommand(ctx, getService, config) {
|
|
|
62
53
|
}
|
|
63
54
|
// 文字模式
|
|
64
55
|
try {
|
|
56
|
+
// 如果未指定节点,提示用户
|
|
57
|
+
if (!selector) {
|
|
58
|
+
return '请指定节点名称、ID 或标签,或使用 "all" 列出全部容器\n例如: docker.ls @web 或 docker.ls all';
|
|
59
|
+
}
|
|
65
60
|
const results = await getContainerResults(service, selector, all);
|
|
66
61
|
if (results.length === 0) {
|
|
67
|
-
return
|
|
62
|
+
return '所有指定节点均未连接';
|
|
68
63
|
}
|
|
69
64
|
const lines = [];
|
|
70
65
|
for (const { node, containers } of results) {
|
|
@@ -130,140 +125,3 @@ function formatContainerLine(container, format) {
|
|
|
130
125
|
// simple 模式:双行显示
|
|
131
126
|
return `${emoji} ${name}\n └ ${shortId} | ${image}`;
|
|
132
127
|
}
|
|
133
|
-
/**
|
|
134
|
-
* 生成 HTML 模板
|
|
135
|
-
*/
|
|
136
|
-
function generateHtml(results) {
|
|
137
|
-
const styles = `
|
|
138
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
139
|
-
body {
|
|
140
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
141
|
-
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
142
|
-
min-height: 100vh;
|
|
143
|
-
padding: 20px;
|
|
144
|
-
color: #fff;
|
|
145
|
-
}
|
|
146
|
-
.container {
|
|
147
|
-
max-width: 700px;
|
|
148
|
-
margin: 0 auto;
|
|
149
|
-
}
|
|
150
|
-
.node-section {
|
|
151
|
-
background: rgba(255, 255, 255, 0.1);
|
|
152
|
-
border-radius: 12px;
|
|
153
|
-
margin-bottom: 20px;
|
|
154
|
-
overflow: hidden;
|
|
155
|
-
}
|
|
156
|
-
.node-header {
|
|
157
|
-
background: rgba(79, 172, 254, 0.3);
|
|
158
|
-
padding: 12px 16px;
|
|
159
|
-
font-size: 16px;
|
|
160
|
-
font-weight: 600;
|
|
161
|
-
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
162
|
-
}
|
|
163
|
-
.table-header {
|
|
164
|
-
display: grid;
|
|
165
|
-
grid-template-columns: 40px 1fr 100px 1fr;
|
|
166
|
-
gap: 10px;
|
|
167
|
-
padding: 10px 16px;
|
|
168
|
-
background: rgba(0, 0, 0, 0.2);
|
|
169
|
-
font-size: 12px;
|
|
170
|
-
color: rgba(255, 255, 255, 0.6);
|
|
171
|
-
text-transform: uppercase;
|
|
172
|
-
letter-spacing: 0.5px;
|
|
173
|
-
}
|
|
174
|
-
.row {
|
|
175
|
-
display: grid;
|
|
176
|
-
grid-template-columns: 40px 1fr 100px 1fr;
|
|
177
|
-
gap: 10px;
|
|
178
|
-
padding: 10px 16px;
|
|
179
|
-
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
180
|
-
align-items: center;
|
|
181
|
-
transition: background 0.2s;
|
|
182
|
-
}
|
|
183
|
-
.row:hover {
|
|
184
|
-
background: rgba(255, 255, 255, 0.05);
|
|
185
|
-
}
|
|
186
|
-
.row:last-child {
|
|
187
|
-
border-bottom: none;
|
|
188
|
-
}
|
|
189
|
-
.status {
|
|
190
|
-
font-size: 18px;
|
|
191
|
-
text-align: center;
|
|
192
|
-
}
|
|
193
|
-
.name {
|
|
194
|
-
font-weight: 500;
|
|
195
|
-
white-space: nowrap;
|
|
196
|
-
overflow: hidden;
|
|
197
|
-
text-overflow: ellipsis;
|
|
198
|
-
}
|
|
199
|
-
.id {
|
|
200
|
-
font-family: 'SF Mono', Monaco, monospace;
|
|
201
|
-
font-size: 12px;
|
|
202
|
-
color: rgba(255, 255, 255, 0.7);
|
|
203
|
-
}
|
|
204
|
-
.image {
|
|
205
|
-
font-size: 12px;
|
|
206
|
-
color: rgba(255, 255, 255, 0.7);
|
|
207
|
-
white-space: nowrap;
|
|
208
|
-
overflow: hidden;
|
|
209
|
-
text-overflow: ellipsis;
|
|
210
|
-
}
|
|
211
|
-
.running { color: #4ade80; }
|
|
212
|
-
.stopped { color: #f87171; }
|
|
213
|
-
.other { color: #94a3b8; }
|
|
214
|
-
.stats {
|
|
215
|
-
display: flex;
|
|
216
|
-
justify-content: center;
|
|
217
|
-
gap: 20px;
|
|
218
|
-
padding: 16px;
|
|
219
|
-
color: rgba(255, 255, 255, 0.6);
|
|
220
|
-
font-size: 13px;
|
|
221
|
-
}
|
|
222
|
-
`;
|
|
223
|
-
let html = `<!DOCTYPE html><html><head><meta charset="UTF-8"><style>${styles}</style></head><body>`;
|
|
224
|
-
html += `<div class="container">`;
|
|
225
|
-
let totalRunning = 0;
|
|
226
|
-
let totalStopped = 0;
|
|
227
|
-
for (const { node, containers } of results) {
|
|
228
|
-
const running = containers.filter(c => c.State === 'running').length;
|
|
229
|
-
const stopped = containers.length - running;
|
|
230
|
-
totalRunning += running;
|
|
231
|
-
totalStopped += stopped;
|
|
232
|
-
html += `<div class="node-section">`;
|
|
233
|
-
html += `<div class="node-header">${node.name}</div>`;
|
|
234
|
-
// 表头
|
|
235
|
-
html += `<div class="table-header">
|
|
236
|
-
<span></span>
|
|
237
|
-
<span>容器</span>
|
|
238
|
-
<span>ID</span>
|
|
239
|
-
<span>镜像</span>
|
|
240
|
-
</div>`;
|
|
241
|
-
// 容器列表
|
|
242
|
-
for (const c of containers) {
|
|
243
|
-
const status = c.State;
|
|
244
|
-
const emoji = status === 'running' ? '🟢' : (status === 'stopped' ? '🔴' : '⚪');
|
|
245
|
-
const name = c.Names[0]?.replace('/', '') || 'Unknown';
|
|
246
|
-
const shortId = c.Id.slice(0, 8);
|
|
247
|
-
let image = c.Image;
|
|
248
|
-
const parts = image.split('/');
|
|
249
|
-
if (parts.length > 1) {
|
|
250
|
-
image = parts[parts.length - 1];
|
|
251
|
-
}
|
|
252
|
-
html += `<div class="row">
|
|
253
|
-
<span class="status">${emoji}</span>
|
|
254
|
-
<span class="name" title="${name}">${name}</span>
|
|
255
|
-
<span class="id">${shortId}</span>
|
|
256
|
-
<span class="image" title="${image}">${image}</span>
|
|
257
|
-
</div>`;
|
|
258
|
-
}
|
|
259
|
-
// 统计
|
|
260
|
-
html += `<div class="stats">运行中: ${running} | 已停止: ${stopped}</div>`;
|
|
261
|
-
html += `</div>`;
|
|
262
|
-
}
|
|
263
|
-
// 总体统计
|
|
264
|
-
html += `<div class="node-section">`;
|
|
265
|
-
html += `<div class="stats"><strong>总计:</strong> ${totalRunning} 运行中, ${totalStopped} 已停止</div>`;
|
|
266
|
-
html += `</div>`;
|
|
267
|
-
html += `</div></body></html>`;
|
|
268
|
-
return html;
|
|
269
|
-
}
|
package/lib/commands/logs.d.ts
CHANGED
package/lib/commands/logs.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerLogsCommand = registerLogsCommand;
|
|
4
4
|
const logger_1 = require("../utils/logger");
|
|
5
|
+
const render_1 = require("../utils/render");
|
|
5
6
|
/**
|
|
6
7
|
* 获取容器名称
|
|
7
8
|
*/
|
|
@@ -12,74 +13,63 @@ function getContainerName(c) {
|
|
|
12
13
|
* 注册日志指令
|
|
13
14
|
*/
|
|
14
15
|
function registerLogsCommand(ctx, getService, config) {
|
|
16
|
+
const useImageOutput = config?.imageOutput === true;
|
|
15
17
|
ctx
|
|
16
|
-
.command('docker.logs <container>
|
|
18
|
+
.command('docker.logs <node> <container>', '查看容器日志')
|
|
17
19
|
.alias('docker日志', '容器日志', 'docker查看日志', '容器查看日志', 'dockerlogs')
|
|
18
20
|
.option('lines', '-n <lines:number> 显示最后 N 行')
|
|
19
21
|
.option('timestamp', '-t 显示时间戳')
|
|
20
|
-
.
|
|
21
|
-
|
|
22
|
+
.option('all', '-a 显示全部(不截断)')
|
|
23
|
+
.action(async ({ options }, node, container) => {
|
|
24
|
+
logger_1.commandLogger.debug(`docker.logs 被调用: node=${node}, container=${container}, lines=${options.lines}, timestamp=${options.timestamp}`);
|
|
22
25
|
const service = getService();
|
|
23
26
|
if (!service) {
|
|
24
27
|
logger_1.commandLogger.debug('服务未初始化');
|
|
25
28
|
return 'Docker 服务未初始化';
|
|
26
29
|
}
|
|
27
30
|
// 参数校验
|
|
28
|
-
if (!container) {
|
|
29
|
-
return '
|
|
31
|
+
if (!node || !container) {
|
|
32
|
+
return '请指定节点和容器\n用法示例:\n docker.logs @web my-app -n 50\n docker.logs all my-app';
|
|
30
33
|
}
|
|
31
34
|
// 确定日志行数 (优先级: 命令行参数 > 全局配置 > 默认值)
|
|
32
35
|
const tail = options.lines || config?.defaultLogLines || 100;
|
|
33
36
|
const showTimestamp = options.timestamp || false;
|
|
34
|
-
|
|
37
|
+
const showAll = options.all || false;
|
|
38
|
+
logger_1.commandLogger.debug(`日志参数: tail=${tail}, showTimestamp=${showTimestamp}, showAll=${showAll}`);
|
|
35
39
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const nodes = service.getNodesBySelector(node);
|
|
42
|
-
if (nodes.length === 0) {
|
|
43
|
-
logger_1.commandLogger.debug(`找不到节点: ${node}`);
|
|
44
|
-
return `❌ 找不到节点: ${node}`;
|
|
45
|
-
}
|
|
46
|
-
targetNode = nodes[0];
|
|
47
|
-
logger_1.commandLogger.debug(`在节点 ${targetNode.name} 中查找容器...`);
|
|
48
|
-
const result = await service.findContainer(targetNode.id, container);
|
|
49
|
-
containerInfo = result.container;
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
// 全局模糊查找
|
|
53
|
-
logger_1.commandLogger.debug(`全局搜索容器: ${container}`);
|
|
54
|
-
const results = await service.findContainerGlobal(container);
|
|
55
|
-
if (results.length === 0) {
|
|
56
|
-
logger_1.commandLogger.debug(`找不到容器: ${container}`);
|
|
57
|
-
return `❌ 找不到容器: ${container}`;
|
|
58
|
-
}
|
|
59
|
-
// 优先返回 Running 的,如果没有则返回第一个
|
|
60
|
-
const running = results.find(r => r.container.State === 'running');
|
|
61
|
-
const target = running || results[0];
|
|
62
|
-
targetNode = target.node;
|
|
63
|
-
containerInfo = target.container;
|
|
40
|
+
// 查找节点
|
|
41
|
+
const nodes = service.getNodesBySelector(node);
|
|
42
|
+
if (nodes.length === 0) {
|
|
43
|
+
logger_1.commandLogger.debug(`找不到节点: ${node}`);
|
|
44
|
+
return `找不到节点: ${node}`;
|
|
64
45
|
}
|
|
46
|
+
// 在节点上查找容器
|
|
47
|
+
const { node: targetNode, container: containerInfo } = await service.findContainer(nodes[0].id, container);
|
|
65
48
|
if (!targetNode || !containerInfo) {
|
|
66
|
-
return '
|
|
49
|
+
return '未能获取容器信息';
|
|
67
50
|
}
|
|
68
51
|
// 获取日志
|
|
69
|
-
const logs = await targetNode.getContainerLogs(containerInfo.Id, tail);
|
|
52
|
+
const logs = await targetNode.getContainerLogs(containerInfo.Id, showAll ? 10000 : tail);
|
|
70
53
|
if (!logs || !logs.trim()) {
|
|
71
54
|
return `${targetNode.name}: ${getContainerName(containerInfo)} - 无日志`;
|
|
72
55
|
}
|
|
73
56
|
// 格式化输出
|
|
74
57
|
const lines = logs.split('\n').filter(l => l.length > 0);
|
|
75
|
-
const displayLines = lines.length > tail ? lines.slice(-tail) : lines;
|
|
58
|
+
const displayLines = !showAll && lines.length > tail ? lines.slice(-tail) : lines;
|
|
76
59
|
// 移除 ANSI 颜色代码
|
|
77
60
|
const cleanLogs = displayLines.map(line => line.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '')).join('\n');
|
|
78
|
-
|
|
61
|
+
// 图片渲染模式
|
|
62
|
+
if (useImageOutput && ctx.puppeteer && !showAll) {
|
|
63
|
+
const html = (0, render_1.generateLogsHtml)(targetNode.name, getContainerName(containerInfo), cleanLogs, displayLines.length);
|
|
64
|
+
// 根据行数动态计算高度:header 80px + header 80px + 每行 25px + padding
|
|
65
|
+
const estimatedHeight = 200 + displayLines.length * 25;
|
|
66
|
+
return await (0, render_1.renderToImage)(ctx, html, { height: Math.max(estimatedHeight, 800) });
|
|
67
|
+
}
|
|
68
|
+
return `=== ${targetNode.name}: ${getContainerName(containerInfo)} (${showAll ? '全部' : `最后 ${displayLines.length} 行`}) ===\n${cleanLogs}`;
|
|
79
69
|
}
|
|
80
70
|
catch (e) {
|
|
81
71
|
logger_1.commandLogger.error(e);
|
|
82
|
-
return
|
|
72
|
+
return `获取日志失败: ${e.message}`;
|
|
83
73
|
}
|
|
84
74
|
});
|
|
85
75
|
}
|
package/lib/index.js
CHANGED
|
@@ -19,7 +19,7 @@ exports.inject = {
|
|
|
19
19
|
exports.Config = koishi_1.Schema.object({
|
|
20
20
|
requestTimeout: koishi_1.Schema.number().default(30000).description('请求超时 (毫秒)'),
|
|
21
21
|
debug: koishi_1.Schema.boolean().default(false).description('调试模式'),
|
|
22
|
-
imageOutput: koishi_1.Schema.boolean().default(false).description('
|
|
22
|
+
imageOutput: koishi_1.Schema.boolean().default(false).description('使用图片格式输出容器列表和操作结果'),
|
|
23
23
|
defaultLogLines: koishi_1.Schema.number().default(100).description('默认日志显示的行数'),
|
|
24
24
|
// 监控策略
|
|
25
25
|
monitor: koishi_1.Schema.object({
|
|
@@ -9,6 +9,13 @@ export declare class DockerConnector {
|
|
|
9
9
|
*/
|
|
10
10
|
exec(command: string): Promise<string>;
|
|
11
11
|
private execInternal;
|
|
12
|
+
/**
|
|
13
|
+
* 执行命令并返回输出和退出码
|
|
14
|
+
*/
|
|
15
|
+
execWithExitCode(command: string): Promise<{
|
|
16
|
+
output: string;
|
|
17
|
+
exitCode: number;
|
|
18
|
+
}>;
|
|
12
19
|
/**
|
|
13
20
|
* 执行 docker ps 获取容器列表
|
|
14
21
|
*/
|
|
@@ -32,7 +39,10 @@ export declare class DockerConnector {
|
|
|
32
39
|
/**
|
|
33
40
|
* 执行容器内命令
|
|
34
41
|
*/
|
|
35
|
-
execContainer(containerId: string, cmd: string): Promise<
|
|
42
|
+
execContainer(containerId: string, cmd: string): Promise<{
|
|
43
|
+
output: string;
|
|
44
|
+
exitCode: number;
|
|
45
|
+
}>;
|
|
36
46
|
/**
|
|
37
47
|
* 监听 Docker 事件流
|
|
38
48
|
* @param callback 每行事件数据的回调
|