koishi-plugin-docker-control 0.0.1
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 +9 -0
- package/lib/commands/control.js +202 -0
- package/lib/commands/index.d.ts +14 -0
- package/lib/commands/index.js +224 -0
- package/lib/commands/list.d.ts +6 -0
- package/lib/commands/list.js +269 -0
- package/lib/commands/logs.d.ts +10 -0
- package/lib/commands/logs.js +85 -0
- package/lib/config.d.ts +55 -0
- package/lib/config.js +90 -0
- package/lib/constants.d.ts +26 -0
- package/lib/constants.js +37 -0
- package/lib/index.d.ts +125 -0
- package/lib/index.js +312 -0
- package/lib/service/agent.d.ts +28 -0
- package/lib/service/agent.js +64 -0
- package/lib/service/connector.d.ts +67 -0
- package/lib/service/connector.js +267 -0
- package/lib/service/index.d.ts +65 -0
- package/lib/service/index.js +202 -0
- package/lib/service/monitor.d.ts +38 -0
- package/lib/service/monitor.js +139 -0
- package/lib/service/node.d.ts +119 -0
- package/lib/service/node.js +509 -0
- package/lib/service/notifier.d.ts +33 -0
- package/lib/service/notifier.js +189 -0
- package/lib/types.d.ts +100 -0
- package/lib/types.js +5 -0
- package/lib/utils/format.d.ts +39 -0
- package/lib/utils/format.js +142 -0
- package/lib/utils/logger.d.ts +65 -0
- package/lib/utils/logger.js +89 -0
- package/lib/utils/stream.d.ts +28 -0
- package/lib/utils/stream.js +156 -0
- package/package.json +38 -0
- package/readme.md +96 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerListCommand = registerListCommand;
|
|
4
|
+
/**
|
|
5
|
+
* 列出容器指令
|
|
6
|
+
* docker.ls - 支持集群视图和图片渲染
|
|
7
|
+
*/
|
|
8
|
+
const koishi_1 = require("koishi");
|
|
9
|
+
const logger_1 = require("../utils/logger");
|
|
10
|
+
function registerListCommand(ctx, getService, config) {
|
|
11
|
+
// 检查是否启用了图片输出
|
|
12
|
+
const useImageOutput = config?.imageOutput === true;
|
|
13
|
+
ctx
|
|
14
|
+
.command('docker.ls [selector]', '列出容器')
|
|
15
|
+
.alias('docker列表', '容器列表', 'dockercs', '容器查看', 'docker查看')
|
|
16
|
+
.option('all', '-a 列出所有容器,包括已停止', { fallback: false })
|
|
17
|
+
.option('format', '-f <format> 输出格式: simple|detail|json|image', {
|
|
18
|
+
fallback: null, // 由 config.imageOutput 决定
|
|
19
|
+
})
|
|
20
|
+
.action(async ({ options }, selector) => {
|
|
21
|
+
logger_1.commandLogger.debug(`docker.ls 被调用: selector=${selector}, all=${options.all}, format=${options.format}`);
|
|
22
|
+
const service = getService();
|
|
23
|
+
if (!service) {
|
|
24
|
+
logger_1.commandLogger.debug('服务未初始化');
|
|
25
|
+
return 'Docker 服务未初始化';
|
|
26
|
+
}
|
|
27
|
+
const all = options.all ?? false;
|
|
28
|
+
// 如果未指定 format,使用配置的 imageOutput 设置
|
|
29
|
+
const format = options.format || (useImageOutput ? 'image' : 'simple');
|
|
30
|
+
logger_1.commandLogger.debug(`列表参数: all=${all}, format=${format}`);
|
|
31
|
+
// 图片渲染模式
|
|
32
|
+
if (format === 'image') {
|
|
33
|
+
logger_1.commandLogger.debug('使用图片渲染模式');
|
|
34
|
+
if (!ctx.puppeteer) {
|
|
35
|
+
return '错误: 未安装 koishi-plugin-puppeteer 插件,无法使用图片渲染';
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
// 获取容器数据
|
|
39
|
+
logger_1.commandLogger.debug('获取容器数据...');
|
|
40
|
+
const results = await getContainerResults(service, selector, all);
|
|
41
|
+
logger_1.commandLogger.debug(`获取到 ${results.length} 个节点`);
|
|
42
|
+
if (results.length === 0) {
|
|
43
|
+
return '未发现任何容器';
|
|
44
|
+
}
|
|
45
|
+
// 生成 HTML
|
|
46
|
+
const html = generateHtml(results);
|
|
47
|
+
// 渲染图片 (puppeteer.render 返回的是 h.image() 元素的字符串)
|
|
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;
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
logger_1.commandLogger.error(`图片渲染失败: ${e.message}`);
|
|
60
|
+
return `错误: ${e.message}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// 文字模式
|
|
64
|
+
try {
|
|
65
|
+
const results = await getContainerResults(service, selector, all);
|
|
66
|
+
if (results.length === 0) {
|
|
67
|
+
return selector ? '所有指定节点均未连接' : '未发现任何容器';
|
|
68
|
+
}
|
|
69
|
+
const lines = [];
|
|
70
|
+
for (const { node, containers } of results) {
|
|
71
|
+
lines.push(`=== ${node.name} ===`);
|
|
72
|
+
if (containers.length === 0) {
|
|
73
|
+
lines.push(' (无容器)');
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
for (const c of containers) {
|
|
77
|
+
lines.push(formatContainerLine(c, format));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
lines.push('');
|
|
81
|
+
}
|
|
82
|
+
return lines.join('\n');
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
logger_1.commandLogger.error(`列出容器失败: ${e.message}`);
|
|
86
|
+
return `错误: ${e.message}`;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 获取容器数据
|
|
92
|
+
*/
|
|
93
|
+
async function getContainerResults(service, selector, all) {
|
|
94
|
+
const results = [];
|
|
95
|
+
if (selector) {
|
|
96
|
+
const nodes = service.getNodesBySelector(selector);
|
|
97
|
+
for (const node of nodes) {
|
|
98
|
+
if (node.status !== 'connected')
|
|
99
|
+
continue;
|
|
100
|
+
const containers = await node.listContainers(all);
|
|
101
|
+
results.push({ node, containers });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const aggregated = await service.getAggregatedContainers(all);
|
|
106
|
+
for (const { node, containers } of aggregated) {
|
|
107
|
+
if (node.status !== 'connected')
|
|
108
|
+
continue;
|
|
109
|
+
results.push({ node, containers: containers || [] });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return results;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 格式化输出单行容器信息
|
|
116
|
+
*/
|
|
117
|
+
function formatContainerLine(container, format) {
|
|
118
|
+
const status = container.State;
|
|
119
|
+
const emoji = status === 'running' ? '🟢' : (status === 'stopped' ? '🔴' : '⚪');
|
|
120
|
+
const name = container.Names[0]?.replace('/', '') || 'Unknown';
|
|
121
|
+
const shortId = container.Id.slice(0, 8);
|
|
122
|
+
let image = container.Image;
|
|
123
|
+
const parts = image.split('/');
|
|
124
|
+
if (parts.length > 1) {
|
|
125
|
+
image = parts[parts.length - 1];
|
|
126
|
+
}
|
|
127
|
+
if (format === 'detail') {
|
|
128
|
+
return `${emoji} **${name}**\n ID: ${shortId}\n Image: ${container.Image}\n State: ${container.Status}`;
|
|
129
|
+
}
|
|
130
|
+
// simple 模式:双行显示
|
|
131
|
+
return `${emoji} ${name}\n └ ${shortId} | ${image}`;
|
|
132
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 日志指令
|
|
3
|
+
* docker.logs <container> [node]
|
|
4
|
+
*/
|
|
5
|
+
import { Context } from 'koishi';
|
|
6
|
+
import type { DockerControlConfig } from '../types';
|
|
7
|
+
/**
|
|
8
|
+
* 注册日志指令
|
|
9
|
+
*/
|
|
10
|
+
export declare function registerLogsCommand(ctx: Context, getService: () => any, config?: DockerControlConfig): void;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerLogsCommand = registerLogsCommand;
|
|
4
|
+
const logger_1 = require("../utils/logger");
|
|
5
|
+
/**
|
|
6
|
+
* 获取容器名称
|
|
7
|
+
*/
|
|
8
|
+
function getContainerName(c) {
|
|
9
|
+
return c.Names[0]?.replace('/', '') || c.Id.slice(0, 8);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 注册日志指令
|
|
13
|
+
*/
|
|
14
|
+
function registerLogsCommand(ctx, getService, config) {
|
|
15
|
+
ctx
|
|
16
|
+
.command('docker.logs <container> [node]', '查看容器日志')
|
|
17
|
+
.alias('docker日志', '容器日志', 'docker查看日志', '容器查看日志', 'dockerlogs')
|
|
18
|
+
.option('lines', '-n <lines:number> 显示最后 N 行')
|
|
19
|
+
.option('timestamp', '-t 显示时间戳')
|
|
20
|
+
.action(async ({ options }, container, node) => {
|
|
21
|
+
logger_1.commandLogger.debug(`docker.logs 被调用: container=${container}, node=${node}, lines=${options.lines}, timestamp=${options.timestamp}`);
|
|
22
|
+
const service = getService();
|
|
23
|
+
if (!service) {
|
|
24
|
+
logger_1.commandLogger.debug('服务未初始化');
|
|
25
|
+
return 'Docker 服务未初始化';
|
|
26
|
+
}
|
|
27
|
+
// 参数校验
|
|
28
|
+
if (!container) {
|
|
29
|
+
return '请指定容器名或ID\n用法示例:\n docker.logs my-app\n docker.logs my-app node-1 -n 50';
|
|
30
|
+
}
|
|
31
|
+
// 确定日志行数 (优先级: 命令行参数 > 全局配置 > 默认值)
|
|
32
|
+
const tail = options.lines || config?.defaultLogLines || 100;
|
|
33
|
+
const showTimestamp = options.timestamp || false;
|
|
34
|
+
logger_1.commandLogger.debug(`日志参数: tail=${tail}, showTimestamp=${showTimestamp}`);
|
|
35
|
+
try {
|
|
36
|
+
let targetNode = null;
|
|
37
|
+
let containerInfo = null;
|
|
38
|
+
// 查找容器逻辑
|
|
39
|
+
if (node) {
|
|
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;
|
|
64
|
+
}
|
|
65
|
+
if (!targetNode || !containerInfo) {
|
|
66
|
+
return '❌ 未能获取容器信息';
|
|
67
|
+
}
|
|
68
|
+
// 获取日志
|
|
69
|
+
const logs = await targetNode.getContainerLogs(containerInfo.Id, tail);
|
|
70
|
+
if (!logs || !logs.trim()) {
|
|
71
|
+
return `${targetNode.name}: ${getContainerName(containerInfo)} - 无日志`;
|
|
72
|
+
}
|
|
73
|
+
// 格式化输出
|
|
74
|
+
const lines = logs.split('\n').filter(l => l.length > 0);
|
|
75
|
+
const displayLines = lines.length > tail ? lines.slice(-tail) : lines;
|
|
76
|
+
// 移除 ANSI 颜色代码
|
|
77
|
+
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
|
+
return `=== ${targetNode.name}: ${getContainerName(containerInfo)} (最后 ${displayLines.length} 行) ===\n${cleanLogs}`;
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
logger_1.commandLogger.error(e);
|
|
82
|
+
return `❌ 获取日志失败: ${e.message}`;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
package/lib/config.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置定义 - 简化版,只支持 SSH 直连模式
|
|
3
|
+
*/
|
|
4
|
+
import { Schema } from 'koishi';
|
|
5
|
+
import type { DockerControlConfig, CredentialConfig, NodeConfig } from './types';
|
|
6
|
+
export declare const CredentialSchema: Schema<CredentialConfig>;
|
|
7
|
+
export declare const NotificationSchema: Schema<Schemastery.ObjectS<{
|
|
8
|
+
enabled: Schema<boolean, boolean>;
|
|
9
|
+
level: Schema<"error" | "all" | "none", "error" | "all" | "none">;
|
|
10
|
+
targetGroups: Schema<string[], string[]>;
|
|
11
|
+
events: Schema<string[], string[]>;
|
|
12
|
+
}>, Schemastery.ObjectT<{
|
|
13
|
+
enabled: Schema<boolean, boolean>;
|
|
14
|
+
level: Schema<"error" | "all" | "none", "error" | "all" | "none">;
|
|
15
|
+
targetGroups: Schema<string[], string[]>;
|
|
16
|
+
events: Schema<string[], string[]>;
|
|
17
|
+
}>>;
|
|
18
|
+
export declare const NodeSchema: Schema<NodeConfig>;
|
|
19
|
+
export declare const ConfigSchema: Schema<Schemastery.ObjectS<{
|
|
20
|
+
requestTimeout: Schema<number, number>;
|
|
21
|
+
debug: Schema<boolean, boolean>;
|
|
22
|
+
credentials: Schema<CredentialConfig[], CredentialConfig[]>;
|
|
23
|
+
nodes: Schema<NodeConfig[], NodeConfig[]>;
|
|
24
|
+
notification: Schema<Schemastery.ObjectS<{
|
|
25
|
+
enabled: Schema<boolean, boolean>;
|
|
26
|
+
level: Schema<"error" | "all" | "none", "error" | "all" | "none">;
|
|
27
|
+
targetGroups: Schema<string[], string[]>;
|
|
28
|
+
events: Schema<string[], string[]>;
|
|
29
|
+
}>, Schemastery.ObjectT<{
|
|
30
|
+
enabled: Schema<boolean, boolean>;
|
|
31
|
+
level: Schema<"error" | "all" | "none", "error" | "all" | "none">;
|
|
32
|
+
targetGroups: Schema<string[], string[]>;
|
|
33
|
+
events: Schema<string[], string[]>;
|
|
34
|
+
}>>;
|
|
35
|
+
}>, Schemastery.ObjectT<{
|
|
36
|
+
requestTimeout: Schema<number, number>;
|
|
37
|
+
debug: Schema<boolean, boolean>;
|
|
38
|
+
credentials: Schema<CredentialConfig[], CredentialConfig[]>;
|
|
39
|
+
nodes: Schema<NodeConfig[], NodeConfig[]>;
|
|
40
|
+
notification: Schema<Schemastery.ObjectS<{
|
|
41
|
+
enabled: Schema<boolean, boolean>;
|
|
42
|
+
level: Schema<"error" | "all" | "none", "error" | "all" | "none">;
|
|
43
|
+
targetGroups: Schema<string[], string[]>;
|
|
44
|
+
events: Schema<string[], string[]>;
|
|
45
|
+
}>, Schemastery.ObjectT<{
|
|
46
|
+
enabled: Schema<boolean, boolean>;
|
|
47
|
+
level: Schema<"error" | "all" | "none", "error" | "all" | "none">;
|
|
48
|
+
targetGroups: Schema<string[], string[]>;
|
|
49
|
+
events: Schema<string[], string[]>;
|
|
50
|
+
}>>;
|
|
51
|
+
}>>;
|
|
52
|
+
export declare function getCredentialById(config: DockerControlConfig, id: string): CredentialConfig | undefined;
|
|
53
|
+
export declare function getNodeById(config: DockerControlConfig, id: string): NodeConfig | undefined;
|
|
54
|
+
export declare function getNodesByTag(config: DockerControlConfig, tag: string): NodeConfig[];
|
|
55
|
+
export declare function validateConfig(config: DockerControlConfig): string[];
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConfigSchema = exports.NodeSchema = exports.NotificationSchema = exports.CredentialSchema = void 0;
|
|
4
|
+
exports.getCredentialById = getCredentialById;
|
|
5
|
+
exports.getNodeById = getNodeById;
|
|
6
|
+
exports.getNodesByTag = getNodesByTag;
|
|
7
|
+
exports.validateConfig = validateConfig;
|
|
8
|
+
/**
|
|
9
|
+
* 配置定义 - 简化版,只支持 SSH 直连模式
|
|
10
|
+
*/
|
|
11
|
+
const koishi_1 = require("koishi");
|
|
12
|
+
// ==================== 凭证 Schema ====================
|
|
13
|
+
exports.CredentialSchema = koishi_1.Schema.object({
|
|
14
|
+
id: koishi_1.Schema.string().required().description('凭证 ID (唯一标识)'),
|
|
15
|
+
name: koishi_1.Schema.string().required().description('凭证名称 (用于显示)'),
|
|
16
|
+
username: koishi_1.Schema.string().default('root').description('SSH 用户名'),
|
|
17
|
+
authType: koishi_1.Schema.union(['key', 'password'])
|
|
18
|
+
.role('radio')
|
|
19
|
+
.default('key')
|
|
20
|
+
.description('认证方式'),
|
|
21
|
+
password: koishi_1.Schema.string().role('secret').description('SSH 密码'),
|
|
22
|
+
privateKey: koishi_1.Schema.string().role('textarea').description('私钥 (PEM 格式)'),
|
|
23
|
+
passphrase: koishi_1.Schema.string().role('secret').description('私钥密码'),
|
|
24
|
+
});
|
|
25
|
+
// ==================== 通知 Schema ====================
|
|
26
|
+
const NotificationEventSchema = koishi_1.Schema.array(koishi_1.Schema.string())
|
|
27
|
+
.default([
|
|
28
|
+
'container.start',
|
|
29
|
+
'container.stop',
|
|
30
|
+
'container.restart',
|
|
31
|
+
'container.die',
|
|
32
|
+
]);
|
|
33
|
+
exports.NotificationSchema = koishi_1.Schema.object({
|
|
34
|
+
enabled: koishi_1.Schema.boolean().default(false).description('是否启用通知'),
|
|
35
|
+
level: koishi_1.Schema.union(['all', 'error', 'none'])
|
|
36
|
+
.default('all')
|
|
37
|
+
.description('通知级别'),
|
|
38
|
+
targetGroups: koishi_1.Schema.array(koishi_1.Schema.string())
|
|
39
|
+
.default([])
|
|
40
|
+
.description('通知的群组 ID 列表'),
|
|
41
|
+
events: NotificationEventSchema
|
|
42
|
+
.description('通知的事件类型'),
|
|
43
|
+
});
|
|
44
|
+
// ==================== 节点 Schema ====================
|
|
45
|
+
exports.NodeSchema = koishi_1.Schema.object({
|
|
46
|
+
id: koishi_1.Schema.string().required().description('节点 ID (唯一标识)'),
|
|
47
|
+
name: koishi_1.Schema.string().required().description('节点名称 (用于显示)'),
|
|
48
|
+
tags: koishi_1.Schema.array(koishi_1.Schema.string())
|
|
49
|
+
.default([])
|
|
50
|
+
.description('标签 (用于集群操作)'),
|
|
51
|
+
host: koishi_1.Schema.string().required().description('SSH 主机地址'),
|
|
52
|
+
port: koishi_1.Schema.number().default(22).description('SSH 端口'),
|
|
53
|
+
credentialId: koishi_1.Schema.string().required().description('SSH 凭证 ID'),
|
|
54
|
+
});
|
|
55
|
+
// ==================== 完整配置 Schema ====================
|
|
56
|
+
exports.ConfigSchema = koishi_1.Schema.object({
|
|
57
|
+
requestTimeout: koishi_1.Schema.number()
|
|
58
|
+
.default(30000)
|
|
59
|
+
.description('全局请求超时 (毫秒)'),
|
|
60
|
+
debug: koishi_1.Schema.boolean().default(false).description('调试模式'),
|
|
61
|
+
credentials: koishi_1.Schema.array(exports.CredentialSchema)
|
|
62
|
+
.default([])
|
|
63
|
+
.description('SSH 凭证列表'),
|
|
64
|
+
nodes: koishi_1.Schema.array(exports.NodeSchema)
|
|
65
|
+
.default([])
|
|
66
|
+
.description('Docker 节点列表'),
|
|
67
|
+
notification: exports.NotificationSchema
|
|
68
|
+
.description('通知配置'),
|
|
69
|
+
});
|
|
70
|
+
// ==================== 辅助函数 ====================
|
|
71
|
+
function getCredentialById(config, id) {
|
|
72
|
+
return config.credentials?.find((c) => c.id === id);
|
|
73
|
+
}
|
|
74
|
+
function getNodeById(config, id) {
|
|
75
|
+
return config.nodes?.find((n) => n.id === id);
|
|
76
|
+
}
|
|
77
|
+
function getNodesByTag(config, tag) {
|
|
78
|
+
return config.nodes?.filter((n) => n.tags.includes(tag)) || [];
|
|
79
|
+
}
|
|
80
|
+
function validateConfig(config) {
|
|
81
|
+
const errors = [];
|
|
82
|
+
if (!config.nodes)
|
|
83
|
+
return errors;
|
|
84
|
+
for (const node of config.nodes) {
|
|
85
|
+
if (!getCredentialById(config, node.credentialId)) {
|
|
86
|
+
errors.push(`节点 ${node.name} 引用的凭证 ${node.credentialId} 不存在`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return errors;
|
|
90
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 常量定义
|
|
3
|
+
*/
|
|
4
|
+
export declare const DEFAULT_TIMEOUT = 5000;
|
|
5
|
+
export declare const RETRY_INTERVAL = 5000;
|
|
6
|
+
export declare const SSH_TIMEOUT = 5000;
|
|
7
|
+
export declare const MAX_RETRY_COUNT = 3;
|
|
8
|
+
export declare const MONITOR_RETRY_INTERVAL = 30000;
|
|
9
|
+
export declare const EVENTS_POLL_INTERVAL = 60000;
|
|
10
|
+
export declare const CONTAINER_POLL_INTERVAL: number;
|
|
11
|
+
export declare const DEFAULT_LOG_LINES = 100;
|
|
12
|
+
export declare const NodeStatus: {
|
|
13
|
+
readonly DISCONNECTED: "disconnected";
|
|
14
|
+
readonly CONNECTING: "connecting";
|
|
15
|
+
readonly CONNECTED: "connected";
|
|
16
|
+
readonly ERROR: "error";
|
|
17
|
+
};
|
|
18
|
+
export type NodeStatusType = typeof NodeStatus[keyof typeof NodeStatus];
|
|
19
|
+
export declare const ContainerAction: {
|
|
20
|
+
readonly START: "start";
|
|
21
|
+
readonly STOP: "stop";
|
|
22
|
+
readonly RESTART: "restart";
|
|
23
|
+
readonly CREATE: "create";
|
|
24
|
+
readonly DIE: "die";
|
|
25
|
+
};
|
|
26
|
+
export type ContainerActionType = typeof ContainerAction[keyof typeof ContainerAction];
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 常量定义
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ContainerAction = exports.NodeStatus = exports.DEFAULT_LOG_LINES = exports.CONTAINER_POLL_INTERVAL = exports.EVENTS_POLL_INTERVAL = exports.MONITOR_RETRY_INTERVAL = exports.MAX_RETRY_COUNT = exports.SSH_TIMEOUT = exports.RETRY_INTERVAL = exports.DEFAULT_TIMEOUT = void 0;
|
|
7
|
+
// 默认请求超时时间 (毫秒)
|
|
8
|
+
exports.DEFAULT_TIMEOUT = 5000;
|
|
9
|
+
// 默认重试间隔 (毫秒)
|
|
10
|
+
exports.RETRY_INTERVAL = 5000;
|
|
11
|
+
// SSH 连接超时 (毫秒)
|
|
12
|
+
exports.SSH_TIMEOUT = 5000;
|
|
13
|
+
// 最大重试次数
|
|
14
|
+
exports.MAX_RETRY_COUNT = 3;
|
|
15
|
+
// 监控重连间隔 (毫秒)
|
|
16
|
+
exports.MONITOR_RETRY_INTERVAL = 30000;
|
|
17
|
+
// Docker Events 监听间隔 (毫秒) - 已改为流式监听,此常量仅作备用
|
|
18
|
+
exports.EVENTS_POLL_INTERVAL = 60000;
|
|
19
|
+
// 容器状态轮询间隔 (毫秒) - 改为5分钟仅作兜底同步
|
|
20
|
+
exports.CONTAINER_POLL_INTERVAL = 5 * 60 * 1000;
|
|
21
|
+
// 日志行数默认
|
|
22
|
+
exports.DEFAULT_LOG_LINES = 100;
|
|
23
|
+
// 节点状态枚举
|
|
24
|
+
exports.NodeStatus = {
|
|
25
|
+
DISCONNECTED: 'disconnected',
|
|
26
|
+
CONNECTING: 'connecting',
|
|
27
|
+
CONNECTED: 'connected',
|
|
28
|
+
ERROR: 'error',
|
|
29
|
+
};
|
|
30
|
+
// 容器操作类型
|
|
31
|
+
exports.ContainerAction = {
|
|
32
|
+
START: 'start',
|
|
33
|
+
STOP: 'stop',
|
|
34
|
+
RESTART: 'restart',
|
|
35
|
+
CREATE: 'create',
|
|
36
|
+
DIE: 'die',
|
|
37
|
+
};
|