koishi-plugin-docker-control 0.0.1 → 0.0.3
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 +39 -1
- package/lib/commands/index.js +14 -4
- package/lib/commands/list.js +4 -154
- package/lib/index.js +1 -1
- package/lib/service/node.d.ts +14 -0
- package/lib/service/node.js +26 -0
- package/lib/utils/render.d.ts +40 -0
- package/lib/utils/render.js +416 -0
- package/package.json +4 -6
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)}`,
|
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
|
});
|
|
@@ -63,6 +69,10 @@ function registerHelperCommands(ctx, getService) {
|
|
|
63
69
|
const node = nodes[0];
|
|
64
70
|
try {
|
|
65
71
|
const version = await node.getVersion();
|
|
72
|
+
if (useImageOutput && ctx.puppeteer) {
|
|
73
|
+
const html = (0, render_1.generateNodeDetailHtml)(node, version);
|
|
74
|
+
return await (0, render_1.renderToImage)(ctx, html);
|
|
75
|
+
}
|
|
66
76
|
const lines = [
|
|
67
77
|
`=== ${node.name} ===`,
|
|
68
78
|
`ID: ${node.id}`,
|
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;
|
|
@@ -42,18 +38,9 @@ function registerListCommand(ctx, getService, config) {
|
|
|
42
38
|
if (results.length === 0) {
|
|
43
39
|
return '未发现任何容器';
|
|
44
40
|
}
|
|
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;
|
|
41
|
+
// 生成并渲染
|
|
42
|
+
const html = (0, render_1.generateListHtml)(results, selector ? `容器列表 (${selector})` : '容器列表');
|
|
43
|
+
return await (0, render_1.renderToImage)(ctx, html);
|
|
57
44
|
}
|
|
58
45
|
catch (e) {
|
|
59
46
|
logger_1.commandLogger.error(`图片渲染失败: ${e.message}`);
|
|
@@ -130,140 +117,3 @@ function formatContainerLine(container, format) {
|
|
|
130
117
|
// simple 模式:双行显示
|
|
131
118
|
return `${emoji} ${name}\n └ ${shortId} | ${image}`;
|
|
132
119
|
}
|
|
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/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({
|
package/lib/service/node.d.ts
CHANGED
|
@@ -61,6 +61,20 @@ export declare class DockerNode {
|
|
|
61
61
|
* 执行容器内命令
|
|
62
62
|
*/
|
|
63
63
|
execContainer(containerId: string, cmd: string): Promise<string>;
|
|
64
|
+
/**
|
|
65
|
+
* 获取 Docker 版本信息
|
|
66
|
+
*/
|
|
67
|
+
getVersion(): Promise<{
|
|
68
|
+
Version: string;
|
|
69
|
+
ApiVersion: string;
|
|
70
|
+
Os: string;
|
|
71
|
+
Arch: string;
|
|
72
|
+
KernelVersion: string;
|
|
73
|
+
}>;
|
|
74
|
+
/**
|
|
75
|
+
* 获取容器详细信息 (docker inspect)
|
|
76
|
+
*/
|
|
77
|
+
getContainer(containerId: string): Promise<any>;
|
|
64
78
|
/**
|
|
65
79
|
* 解析 docker ps 输出
|
|
66
80
|
*/
|
package/lib/service/node.js
CHANGED
|
@@ -157,6 +157,32 @@ class DockerNode {
|
|
|
157
157
|
throw new Error('未连接');
|
|
158
158
|
return this.connector.execContainer(containerId, cmd);
|
|
159
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* 获取 Docker 版本信息
|
|
162
|
+
*/
|
|
163
|
+
async getVersion() {
|
|
164
|
+
if (!this.connector)
|
|
165
|
+
throw new Error('未连接');
|
|
166
|
+
const output = await this.connector.exec('docker version --format "{{json .Server}}"');
|
|
167
|
+
const info = JSON.parse(output);
|
|
168
|
+
return {
|
|
169
|
+
Version: info.Version || 'unknown',
|
|
170
|
+
ApiVersion: info.ApiVersion || 'unknown',
|
|
171
|
+
Os: info.Os || 'unknown',
|
|
172
|
+
Arch: info.Arch || 'unknown',
|
|
173
|
+
KernelVersion: info.KernelVersion || 'unknown',
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* 获取容器详细信息 (docker inspect)
|
|
178
|
+
*/
|
|
179
|
+
async getContainer(containerId) {
|
|
180
|
+
if (!this.connector)
|
|
181
|
+
throw new Error('未连接');
|
|
182
|
+
const output = await this.connector.exec(`docker inspect ${containerId}`);
|
|
183
|
+
const info = JSON.parse(output);
|
|
184
|
+
return Array.isArray(info) ? info[0] : info;
|
|
185
|
+
}
|
|
160
186
|
/**
|
|
161
187
|
* 解析 docker ps 输出
|
|
162
188
|
*/
|
|
@@ -0,0 +1,40 @@
|
|
|
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): string;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderToImage = renderToImage;
|
|
4
|
+
exports.generateListHtml = generateListHtml;
|
|
5
|
+
exports.generateResultHtml = generateResultHtml;
|
|
6
|
+
exports.generateInspectHtml = generateInspectHtml;
|
|
7
|
+
exports.generateNodesHtml = generateNodesHtml;
|
|
8
|
+
exports.generateNodeDetailHtml = generateNodeDetailHtml;
|
|
9
|
+
const koishi_1 = require("koishi");
|
|
10
|
+
// 基础样式
|
|
11
|
+
const STYLE = `
|
|
12
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
13
|
+
body {
|
|
14
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
15
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
16
|
+
min-height: 100vh;
|
|
17
|
+
padding: 24px;
|
|
18
|
+
color: #e2e8f0;
|
|
19
|
+
line-height: 1.5;
|
|
20
|
+
}
|
|
21
|
+
.wrapper {
|
|
22
|
+
max-width: 800px;
|
|
23
|
+
margin: 0 auto;
|
|
24
|
+
background: rgba(30, 41, 59, 0.7);
|
|
25
|
+
backdrop-filter: blur(12px);
|
|
26
|
+
border-radius: 16px;
|
|
27
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
30
|
+
}
|
|
31
|
+
.header {
|
|
32
|
+
background: rgba(51, 65, 85, 0.5);
|
|
33
|
+
padding: 16px 24px;
|
|
34
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
justify-content: space-between;
|
|
38
|
+
}
|
|
39
|
+
.header-title {
|
|
40
|
+
font-size: 18px;
|
|
41
|
+
font-weight: 600;
|
|
42
|
+
color: #f8fafc;
|
|
43
|
+
}
|
|
44
|
+
.header-badge {
|
|
45
|
+
font-size: 12px;
|
|
46
|
+
padding: 4px 12px;
|
|
47
|
+
border-radius: 9999px;
|
|
48
|
+
background: rgba(255, 255, 255, 0.1);
|
|
49
|
+
color: #cbd5e1;
|
|
50
|
+
}
|
|
51
|
+
.content {
|
|
52
|
+
padding: 24px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* 表格/列表样式 */
|
|
56
|
+
.list-item {
|
|
57
|
+
display: grid;
|
|
58
|
+
grid-template-columns: 48px 2fr 1.5fr 1fr;
|
|
59
|
+
gap: 16px;
|
|
60
|
+
padding: 16px;
|
|
61
|
+
border-radius: 8px;
|
|
62
|
+
align-items: center;
|
|
63
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
64
|
+
transition: background 0.2s;
|
|
65
|
+
}
|
|
66
|
+
.list-item:last-child {
|
|
67
|
+
border-bottom: none;
|
|
68
|
+
}
|
|
69
|
+
.list-item:hover {
|
|
70
|
+
background: rgba(255, 255, 255, 0.05);
|
|
71
|
+
}
|
|
72
|
+
.list-header {
|
|
73
|
+
font-size: 13px;
|
|
74
|
+
color: #94a3b8;
|
|
75
|
+
text-transform: uppercase;
|
|
76
|
+
letter-spacing: 0.05em;
|
|
77
|
+
padding: 0 16px 12px;
|
|
78
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
79
|
+
margin-bottom: 8px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* 状态样式 */
|
|
83
|
+
.status-icon { font-size: 20px; }
|
|
84
|
+
.name-col { font-weight: 500; color: #fff; }
|
|
85
|
+
.meta-col { font-size: 13px; color: #94a3b8; font-family: 'SF Mono', Monaco, monospace; }
|
|
86
|
+
.tag {
|
|
87
|
+
display: inline-block;
|
|
88
|
+
padding: 2px 8px;
|
|
89
|
+
border-radius: 4px;
|
|
90
|
+
font-size: 12px;
|
|
91
|
+
background: rgba(255, 255, 255, 0.1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* Inspect 详情样式 */
|
|
95
|
+
.detail-grid {
|
|
96
|
+
display: grid;
|
|
97
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
98
|
+
gap: 20px;
|
|
99
|
+
}
|
|
100
|
+
.detail-card {
|
|
101
|
+
background: rgba(0, 0, 0, 0.2);
|
|
102
|
+
border-radius: 12px;
|
|
103
|
+
padding: 20px;
|
|
104
|
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
105
|
+
}
|
|
106
|
+
.detail-item {
|
|
107
|
+
margin-bottom: 12px;
|
|
108
|
+
}
|
|
109
|
+
.detail-item:last-child { margin-bottom: 0; }
|
|
110
|
+
.detail-label {
|
|
111
|
+
font-size: 13px;
|
|
112
|
+
color: #94a3b8;
|
|
113
|
+
margin-bottom: 4px;
|
|
114
|
+
}
|
|
115
|
+
.detail-value {
|
|
116
|
+
font-size: 15px;
|
|
117
|
+
color: #e2e8f0;
|
|
118
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
119
|
+
word-break: break-all;
|
|
120
|
+
}
|
|
121
|
+
.detail-value.highlight {
|
|
122
|
+
color: #60a5fa;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* 操作结果样式 */
|
|
126
|
+
.result-card {
|
|
127
|
+
display: flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
padding: 16px;
|
|
130
|
+
background: rgba(255, 255, 255, 0.03);
|
|
131
|
+
border-radius: 8px;
|
|
132
|
+
margin-bottom: 12px;
|
|
133
|
+
border-left: 4px solid #64748b;
|
|
134
|
+
}
|
|
135
|
+
.result-card.success { border-left-color: #4ade80; background: rgba(74, 222, 128, 0.1); }
|
|
136
|
+
.result-card.error { border-left-color: #f87171; background: rgba(248, 113, 113, 0.1); }
|
|
137
|
+
.result-icon {
|
|
138
|
+
font-size: 24px;
|
|
139
|
+
margin-right: 16px;
|
|
140
|
+
}
|
|
141
|
+
.result-info { flex: 1; }
|
|
142
|
+
.result-title { font-weight: 600; margin-bottom: 4px; }
|
|
143
|
+
.result-msg { font-size: 13px; color: #cbd5e1; }
|
|
144
|
+
`;
|
|
145
|
+
/**
|
|
146
|
+
* 包装 HTML
|
|
147
|
+
*/
|
|
148
|
+
function wrapHtml(content, style = STYLE) {
|
|
149
|
+
return `<!DOCTYPE html>
|
|
150
|
+
<html>
|
|
151
|
+
<head>
|
|
152
|
+
<meta charset="UTF-8">
|
|
153
|
+
<style>${style}</style>
|
|
154
|
+
</head>
|
|
155
|
+
<body>
|
|
156
|
+
<div class="wrapper">
|
|
157
|
+
${content}
|
|
158
|
+
</div>
|
|
159
|
+
</body>
|
|
160
|
+
</html>`;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* 通用渲染函数:将 HTML 转换为图片
|
|
164
|
+
*/
|
|
165
|
+
async function renderToImage(ctx, html, options = {}) {
|
|
166
|
+
if (!ctx.puppeteer) {
|
|
167
|
+
throw new Error('未安装 koishi-plugin-puppeteer 插件');
|
|
168
|
+
}
|
|
169
|
+
return ctx.puppeteer.render(html, async (page, next) => {
|
|
170
|
+
// 设置适当的视口,高度设大一点以便 content 自适应,然后截图 clip
|
|
171
|
+
await page.setViewport({
|
|
172
|
+
width: options.width || 700,
|
|
173
|
+
height: options.height || 1000,
|
|
174
|
+
deviceScaleFactor: 2 // 高清渲染
|
|
175
|
+
});
|
|
176
|
+
// 等待内容渲染
|
|
177
|
+
const body = await page.$('body');
|
|
178
|
+
const wrapper = await page.$('.wrapper');
|
|
179
|
+
// 获取 wrapper 的实际大小
|
|
180
|
+
const clip = await wrapper?.boundingBox() || await body?.boundingBox();
|
|
181
|
+
if (clip) {
|
|
182
|
+
// 增加一点 padding 截图
|
|
183
|
+
// clip.x -= 10
|
|
184
|
+
// clip.y -= 10
|
|
185
|
+
// clip.width += 20
|
|
186
|
+
// clip.height += 20
|
|
187
|
+
// 直接截取 content
|
|
188
|
+
const buffer = await page.screenshot({ clip });
|
|
189
|
+
return koishi_1.h.image(buffer, 'image/png').toString();
|
|
190
|
+
}
|
|
191
|
+
// Fallback
|
|
192
|
+
const buffer = await page.screenshot({ fullPage: true });
|
|
193
|
+
return koishi_1.h.image(buffer, 'image/png').toString();
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* 生成容器列表 HTML
|
|
198
|
+
*/
|
|
199
|
+
function generateListHtml(data, title = '容器列表') {
|
|
200
|
+
let stats = { running: 0, stopped: 0, total: 0 };
|
|
201
|
+
const content = data.map(({ node, containers }) => {
|
|
202
|
+
const nodeStats = {
|
|
203
|
+
running: containers.filter(c => c.State === 'running').length,
|
|
204
|
+
total: containers.length
|
|
205
|
+
};
|
|
206
|
+
stats.running += nodeStats.running;
|
|
207
|
+
stats.total += nodeStats.total;
|
|
208
|
+
stats.stopped += (nodeStats.total - nodeStats.running);
|
|
209
|
+
const listItems = containers.length === 0
|
|
210
|
+
? `<div style="padding: 20px; text-align: center; color: #64748b;">(暂无容器)</div>`
|
|
211
|
+
: containers.map(c => {
|
|
212
|
+
const isRunning = c.State === 'running';
|
|
213
|
+
const icon = isRunning ? '🟢' : (c.State === 'stopped' ? '🔴' : '⚪');
|
|
214
|
+
const name = c.Names[0]?.replace('/', '') || 'Unknown';
|
|
215
|
+
const shortId = c.Id.slice(0, 12);
|
|
216
|
+
const image = c.Image.split('/').pop() || c.Image;
|
|
217
|
+
return `
|
|
218
|
+
<div class="list-item">
|
|
219
|
+
<div class="status-icon">${icon}</div>
|
|
220
|
+
<div class="name-col">
|
|
221
|
+
<div>${name}</div>
|
|
222
|
+
<div style="font-size:12px; opacity:0.6; margin-top:2px;">${c.Status}</div>
|
|
223
|
+
</div>
|
|
224
|
+
<div class="meta-col">
|
|
225
|
+
<div>ID: ${shortId}</div>
|
|
226
|
+
<div style="color: #64748b; margin-top:2px;">${image}</div>
|
|
227
|
+
</div>
|
|
228
|
+
<div style="text-align: right;">
|
|
229
|
+
<span class="tag" style="background: ${isRunning ? 'rgba(74, 222, 128, 0.1); color: #4ade80' : 'rgba(248, 113, 113, 0.1); color: #f87171'}">${c.State}</span>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
`;
|
|
233
|
+
}).join('');
|
|
234
|
+
return `
|
|
235
|
+
<div style="margin-bottom: 24px;">
|
|
236
|
+
<div style="padding: 12px 16px; background: rgba(0,0,0,0.2); border-radius: 8px 8px 0 0; font-weight: 500; border-bottom: 1px solid rgba(255,255,255,0.05); display: flex; justify-content: space-between;">
|
|
237
|
+
<span>📦 ${node.name}</span>
|
|
238
|
+
<span style="font-size: 13px; opacity: 0.7;">${nodeStats.running} / ${nodeStats.total} 运行中</span>
|
|
239
|
+
</div>
|
|
240
|
+
<div style="background: rgba(0,0,0,0.1); border-radius: 0 0 8px 8px;">
|
|
241
|
+
${listItems}
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
`;
|
|
245
|
+
}).join('');
|
|
246
|
+
const header = `
|
|
247
|
+
<div class="header">
|
|
248
|
+
<div class="header-title">${title}</div>
|
|
249
|
+
<div class="header-badge">Total: ${stats.running} running / ${stats.total} total</div>
|
|
250
|
+
</div>
|
|
251
|
+
`;
|
|
252
|
+
return wrapHtml(header + '<div class="content">' + content + '</div>');
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* 生成操作结果 HTML (启动/停止/重启)
|
|
256
|
+
*/
|
|
257
|
+
function generateResultHtml(results, title) {
|
|
258
|
+
const successCount = results.filter(r => r.success).length;
|
|
259
|
+
const failCount = results.length - successCount;
|
|
260
|
+
const items = results.map(r => {
|
|
261
|
+
const isSuccess = r.success;
|
|
262
|
+
const icon = isSuccess ? '✅' : '❌';
|
|
263
|
+
const name = r.container?.Names?.[0]?.replace('/', '') || r.container?.Id?.slice(0, 8) || 'Unknown';
|
|
264
|
+
const message = r.error || (isSuccess ? '操作成功' : '操作失败');
|
|
265
|
+
return `
|
|
266
|
+
<div class="result-card ${isSuccess ? 'success' : 'error'}">
|
|
267
|
+
<div class="result-icon">${icon}</div>
|
|
268
|
+
<div class="result-info">
|
|
269
|
+
<div class="result-title">${r.node.name}: ${name}</div>
|
|
270
|
+
<div class="result-msg">${message}</div>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
`;
|
|
274
|
+
}).join('');
|
|
275
|
+
const header = `
|
|
276
|
+
<div class="header">
|
|
277
|
+
<div class="header-title">${title}</div>
|
|
278
|
+
<div class="header-badge" style="background: ${failCount > 0 ? 'rgba(248, 113, 113, 0.2); color: #fca5a5' : 'rgba(74, 222, 128, 0.2); color: #86efac'}">
|
|
279
|
+
成功: ${successCount} | 失败: ${failCount}
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
`;
|
|
283
|
+
return wrapHtml(header + '<div class="content">' + items + '</div>');
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* 生成详情 HTML
|
|
287
|
+
*/
|
|
288
|
+
function generateInspectHtml(nodeName, info) {
|
|
289
|
+
const name = info.Name.replace('/', '');
|
|
290
|
+
const shortId = info.Id.slice(0, 12);
|
|
291
|
+
const isRunning = info.State.Running;
|
|
292
|
+
const items = [
|
|
293
|
+
{ label: '容器名称', value: name },
|
|
294
|
+
{ label: '容器 ID', value: info.Id },
|
|
295
|
+
{ label: '镜像', value: info.Config.Image },
|
|
296
|
+
{ label: '状态', value: info.State.Status, highlight: true },
|
|
297
|
+
{ label: '创建时间', value: new Date(info.Created).toLocaleString() },
|
|
298
|
+
{ label: '启动时间', value: new Date(info.State.StartedAt).toLocaleString() },
|
|
299
|
+
{ label: '重启次数', value: info.RestartCount },
|
|
300
|
+
{ label: 'IP 地址', value: info.NetworkSettings?.IPAddress || '-' },
|
|
301
|
+
{ label: '平台', value: info.Platform || 'linux' },
|
|
302
|
+
{ label: '驱动', value: info.Driver },
|
|
303
|
+
];
|
|
304
|
+
if (info.State.Health) {
|
|
305
|
+
items.push({ label: '健康状态', value: info.State.Health.Status, highlight: true });
|
|
306
|
+
}
|
|
307
|
+
const gridItems = items.map(item => `
|
|
308
|
+
<div class="detail-item">
|
|
309
|
+
<div class="detail-label">${item.label}</div>
|
|
310
|
+
<div class="detail-value ${item.highlight ? 'highlight' : ''}">${item.value}</div>
|
|
311
|
+
</div>
|
|
312
|
+
`).join('');
|
|
313
|
+
const header = `
|
|
314
|
+
<div class="header">
|
|
315
|
+
<div class="header-title">容器详情</div>
|
|
316
|
+
<div class="header-badge">${nodeName}</div>
|
|
317
|
+
</div>
|
|
318
|
+
`;
|
|
319
|
+
const body = `
|
|
320
|
+
<div class="content">
|
|
321
|
+
<div class="detail-card">
|
|
322
|
+
<div style="display: flex; align-items: center; margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid rgba(255,255,255,0.1);">
|
|
323
|
+
<div style="font-size: 32px; margin-right: 16px;">${isRunning ? '🟢' : '🔴'}</div>
|
|
324
|
+
<div>
|
|
325
|
+
<div style="font-size: 20px; font-weight: 600;">${name}</div>
|
|
326
|
+
<div style="font-size: 13px; color: #94a3b8; font-family: monospace;">${shortId}</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
<div class="detail-grid">
|
|
330
|
+
${gridItems}
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
<!--Mounts/Ports could be added here-->
|
|
334
|
+
</div>
|
|
335
|
+
`;
|
|
336
|
+
return wrapHtml(header + body);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* 生成节点列表 HTML
|
|
340
|
+
*/
|
|
341
|
+
function generateNodesHtml(nodes) {
|
|
342
|
+
const onlineCount = nodes.filter(n => n.status === 'connected').length;
|
|
343
|
+
const totalCount = nodes.length;
|
|
344
|
+
const listItems = nodes.map(n => {
|
|
345
|
+
const isOnline = n.status === 'connected';
|
|
346
|
+
const isConnecting = n.status === 'connecting';
|
|
347
|
+
const icon = isOnline ? '🟢' : (isConnecting ? '🟡' : '🔴');
|
|
348
|
+
const tags = n.tags.map((t) => `<span class="tag">@${t}</span>`).join(' ');
|
|
349
|
+
return `
|
|
350
|
+
<div class="list-item">
|
|
351
|
+
<div class="status-icon">${icon}</div>
|
|
352
|
+
<div class="name-col">
|
|
353
|
+
<div>${n.name}</div>
|
|
354
|
+
<div style="font-size:12px; opacity:0.6; margin-top:2px;">${n.id}</div>
|
|
355
|
+
</div>
|
|
356
|
+
<div class="meta-col">
|
|
357
|
+
<div style="color: ${isOnline ? '#4ade80' : (isConnecting ? '#facc15' : '#f87171')}">${n.status}</div>
|
|
358
|
+
</div>
|
|
359
|
+
<div>${tags}</div>
|
|
360
|
+
</div>
|
|
361
|
+
`;
|
|
362
|
+
}).join('');
|
|
363
|
+
const header = `
|
|
364
|
+
<div class="header">
|
|
365
|
+
<div class="header-title">节点列表</div>
|
|
366
|
+
<div class="header-badge" style="background: rgba(74, 222, 128, 0.1); color: #4ade80">在线: ${onlineCount} / ${totalCount}</div>
|
|
367
|
+
</div>
|
|
368
|
+
`;
|
|
369
|
+
return wrapHtml(header + '<div class="content"><div style="background: rgba(0,0,0,0.2); border-radius: 8px;">' + listItems + '</div></div>');
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* 生成节点详情 HTML
|
|
373
|
+
*/
|
|
374
|
+
function generateNodeDetailHtml(node, version) {
|
|
375
|
+
const isOnline = node.status === 'connected';
|
|
376
|
+
// 基础信息
|
|
377
|
+
const items = [
|
|
378
|
+
{ label: '节点名称', value: node.name },
|
|
379
|
+
{ label: '节点 ID', value: node.id },
|
|
380
|
+
{ label: '状态', value: node.status, highlight: isOnline },
|
|
381
|
+
{ label: '标签', value: node.tags.join(', ') || '(无)' },
|
|
382
|
+
];
|
|
383
|
+
// 版本信息
|
|
384
|
+
if (version) {
|
|
385
|
+
items.push({ label: 'Docker 版本', value: version.Version }, { label: 'API 版本', value: version.ApiVersion }, { label: '操作系统', value: `${version.Os} (${version.Arch})` }, { label: '内核版本', value: version.KernelVersion }, { label: 'Go 版本', value: version.GoVersion }, { label: 'Git Commit', value: version.GitCommit }, { label: '构建时间', value: version.BuildTime });
|
|
386
|
+
}
|
|
387
|
+
const gridItems = items.map(item => `
|
|
388
|
+
<div class="detail-item">
|
|
389
|
+
<div class="detail-label">${item.label}</div>
|
|
390
|
+
<div class="detail-value ${item.highlight ? 'highlight' : ''}">${item.value}</div>
|
|
391
|
+
</div>
|
|
392
|
+
`).join('');
|
|
393
|
+
const header = `
|
|
394
|
+
<div class="header">
|
|
395
|
+
<div class="header-title">节点详情</div>
|
|
396
|
+
<div class="header-badge">${node.name}</div>
|
|
397
|
+
</div>
|
|
398
|
+
`;
|
|
399
|
+
const body = `
|
|
400
|
+
<div class="content">
|
|
401
|
+
<div class="detail-card">
|
|
402
|
+
<div style="display: flex; align-items: center; margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid rgba(255,255,255,0.1);">
|
|
403
|
+
<div style="font-size: 32px; margin-right: 16px;">${isOnline ? '🟢' : '🔴'}</div>
|
|
404
|
+
<div>
|
|
405
|
+
<div style="font-size: 20px; font-weight: 600;">${node.name}</div>
|
|
406
|
+
<div style="font-size: 13px; color: #94a3b8; font-family: monospace;">${node.id}</div>
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
<div class="detail-grid">
|
|
410
|
+
${gridItems}
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
`;
|
|
415
|
+
return wrapHtml(header + body);
|
|
416
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-docker-control",
|
|
3
3
|
"description": "Koishi 插件 - 通过 SSH 控制 Docker 容器",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.3",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -22,17 +22,15 @@
|
|
|
22
22
|
"ssh"
|
|
23
23
|
],
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"
|
|
26
|
-
"koishi": "4.18.10",
|
|
27
|
-
"ssh2": "^1.17.0"
|
|
25
|
+
"koishi": "4.18.10"
|
|
28
26
|
},
|
|
29
27
|
"dependencies": {
|
|
28
|
+
"ssh2": "^1.17.0",
|
|
30
29
|
"puppeteer": "^21.0.0"
|
|
31
30
|
},
|
|
32
31
|
"devDependencies": {
|
|
33
|
-
"@types/dockerode": "^3.3.47",
|
|
34
32
|
"@types/node": "^20.19.27",
|
|
35
33
|
"@types/ssh2": "^1.15.5",
|
|
36
34
|
"typescript": "^5.9.3"
|
|
37
35
|
}
|
|
38
|
-
}
|
|
36
|
+
}
|