koishi-plugin-docker-control 0.0.7 → 0.1.0

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.
Files changed (40) hide show
  1. package/lib/commands/audit.d.ts +9 -0
  2. package/lib/commands/audit.js +136 -0
  3. package/lib/commands/cluster.d.ts +5 -0
  4. package/lib/commands/cluster.js +294 -0
  5. package/lib/commands/control.js +166 -92
  6. package/lib/commands/index.js +27 -0
  7. package/lib/commands/permission.d.ts +9 -0
  8. package/lib/commands/permission.js +204 -0
  9. package/lib/config.d.ts +154 -0
  10. package/lib/config.js +55 -0
  11. package/lib/constants/enhanced.d.ts +202 -0
  12. package/lib/constants/enhanced.js +105 -0
  13. package/lib/enhanced/index.d.ts +12 -0
  14. package/lib/enhanced/index.js +43 -0
  15. package/lib/index.d.ts +185 -64
  16. package/lib/index.js +226 -30
  17. package/lib/service/audit-logger.d.ts +55 -0
  18. package/lib/service/audit-logger.js +242 -0
  19. package/lib/service/cache-manager.d.ts +95 -0
  20. package/lib/service/cache-manager.js +285 -0
  21. package/lib/service/connection-pool.d.ts +71 -0
  22. package/lib/service/connection-pool.js +284 -0
  23. package/lib/service/index.d.ts +14 -0
  24. package/lib/service/index.js +41 -0
  25. package/lib/service/node.d.ts +53 -0
  26. package/lib/service/node.js +148 -0
  27. package/lib/service/permission-manager.d.ts +62 -0
  28. package/lib/service/permission-manager.js +206 -0
  29. package/lib/service/reconnect-manager.d.ts +39 -0
  30. package/lib/service/reconnect-manager.js +108 -0
  31. package/lib/types/enhanced.d.ts +122 -0
  32. package/lib/types/enhanced.js +6 -0
  33. package/lib/types.d.ts +36 -0
  34. package/lib/utils/permission-check.d.ts +54 -0
  35. package/lib/utils/permission-check.js +120 -0
  36. package/lib/utils/render.d.ts +59 -0
  37. package/lib/utils/render.js +261 -0
  38. package/lib/utils/retry-decorator.d.ts +14 -0
  39. package/lib/utils/retry-decorator.js +91 -0
  40. package/package.json +8 -3
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 审计日志指令
3
+ * 查询和管理审计日志
4
+ */
5
+ import { Context } from 'koishi';
6
+ /**
7
+ * 注册审计日志指令
8
+ */
9
+ export declare function registerAuditCommands(ctx: Context, getService: () => any): void;
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerAuditCommands = registerAuditCommands;
4
+ const logger_1 = require("../utils/logger");
5
+ /**
6
+ * 注册审计日志指令
7
+ */
8
+ function registerAuditCommands(ctx, getService) {
9
+ /**
10
+ * 查询审计日志
11
+ */
12
+ ctx
13
+ .command('docker.audit.log', '查看审计日志')
14
+ .alias('审计日志', '日志审计')
15
+ .option('user', '-u <userId> 按用户ID筛选')
16
+ .option('action', '-a <action> 按操作类型筛选')
17
+ .option('result', '-r <result> 按结果筛选 (success/failure)')
18
+ .option('limit', '-l <count> 限制返回条数', { fallback: 20 })
19
+ .action(async ({ options }) => {
20
+ const service = getService();
21
+ if (!service) {
22
+ return 'Docker 服务未初始化';
23
+ }
24
+ if (!service.auditLogger) {
25
+ return '审计日志功能未启用';
26
+ }
27
+ try {
28
+ const filter = {
29
+ limit: options.limit || 20
30
+ };
31
+ if (options.user)
32
+ filter.userId = options.user;
33
+ if (options.action)
34
+ filter.action = options.action;
35
+ if (options.result)
36
+ filter.result = options.result;
37
+ const logs = await service.auditLogger.query(filter);
38
+ if (logs.length === 0) {
39
+ return '没有找到符合条件的审计日志';
40
+ }
41
+ const lines = [`=== 审计日志 (最近 ${logs.length} 条) ===`, ''];
42
+ for (const log of logs) {
43
+ const status = log.result === 'success' ? '✅' : '❌';
44
+ const time = new Date(log.timestamp).toLocaleString();
45
+ const duration = log.duration ? `${log.duration}ms` : '-';
46
+ lines.push(`${status} ${time}`, ` 用户: ${log.userName || log.userId} (${log.platform})`, ` 操作: ${log.action}`, ` 结果: ${log.result}`, ` 耗时: ${duration}`, log.nodeId ? ` 节点: ${log.nodeId}` : '', log.containerId ? ` 容器: ${log.containerId.slice(0, 12)}` : '', log.errorMessage ? ` 错误: ${log.errorMessage}` : '', '');
47
+ }
48
+ return lines.filter(Boolean).join('\n');
49
+ }
50
+ catch (e) {
51
+ logger_1.commandLogger.error(`查询审计日志失败: ${e.message}`);
52
+ return `❌ 查询失败: ${e.message}`;
53
+ }
54
+ });
55
+ /**
56
+ * 获取审计日志统计
57
+ */
58
+ ctx
59
+ .command('docker.audit.stats', '审计日志统计')
60
+ .alias('审计统计', '日志统计')
61
+ .action(async () => {
62
+ const service = getService();
63
+ if (!service) {
64
+ return 'Docker 服务未初始化';
65
+ }
66
+ if (!service.auditLogger) {
67
+ return '审计日志功能未启用';
68
+ }
69
+ try {
70
+ const stats = await service.auditLogger.getStats();
71
+ return [
72
+ '=== 审计日志统计 ===',
73
+ `总操作数: ${stats.total}`,
74
+ `成功: ${stats.success}`,
75
+ `失败: ${stats.failure}`,
76
+ `平均耗时: ${stats.avgDuration}ms`
77
+ ].join('\n');
78
+ }
79
+ catch (e) {
80
+ logger_1.commandLogger.error(`获取审计统计失败: ${e.message}`);
81
+ return `❌ 获取失败: ${e.message}`;
82
+ }
83
+ });
84
+ /**
85
+ * 清理旧日志
86
+ */
87
+ ctx
88
+ .command('docker.audit.cleanup', '清理旧审计日志')
89
+ .alias('清理审计日志', '清理日志')
90
+ .option('days', '-d <days> 保留天数', { fallback: null })
91
+ .action(async ({ options }) => {
92
+ const service = getService();
93
+ if (!service) {
94
+ return 'Docker 服务未初始化';
95
+ }
96
+ if (!service.auditLogger) {
97
+ return '审计日志功能未启用';
98
+ }
99
+ try {
100
+ const deletedCount = await service.auditLogger.cleanup(options.days);
101
+ return `✅ 已清理 ${deletedCount} 条旧日志`;
102
+ }
103
+ catch (e) {
104
+ logger_1.commandLogger.error(`清理审计日志失败: ${e.message}`);
105
+ return `❌ 清理失败: ${e.message}`;
106
+ }
107
+ });
108
+ /**
109
+ * 导出审计日志
110
+ */
111
+ ctx
112
+ .command('docker.audit.export', '导出审计日志')
113
+ .alias('导出审计日志', '导出日志')
114
+ .action(async () => {
115
+ const service = getService();
116
+ if (!service) {
117
+ return 'Docker 服务未初始化';
118
+ }
119
+ if (!service.auditLogger) {
120
+ return '审计日志功能未启用';
121
+ }
122
+ if (!ctx.assets) {
123
+ return '❌ 需要assets插件支持才能导出文件';
124
+ }
125
+ try {
126
+ const csv = await service.auditLogger.export();
127
+ const filename = `audit-logs-${Date.now()}.csv`;
128
+ const url = await ctx.assets.upload(csv, filename);
129
+ return `✅ 审计日志已导出: ${url}`;
130
+ }
131
+ catch (e) {
132
+ logger_1.commandLogger.error(`导出审计日志失败: ${e.message}`);
133
+ return `❌ 导出失败: ${e.message}`;
134
+ }
135
+ });
136
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Docker Swarm 集群管理指令
3
+ */
4
+ import { Context } from 'koishi';
5
+ export declare function registerClusterCommands(ctx: Context, getService: () => any, config?: any): void;
@@ -0,0 +1,294 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerClusterCommands = registerClusterCommands;
4
+ const logger_1 = require("../utils/logger");
5
+ const render_1 = require("../utils/render");
6
+ function registerClusterCommands(ctx, getService, config) {
7
+ const useImageOutput = config?.imageOutput === true;
8
+ /**
9
+ * 查看集群信息
10
+ */
11
+ ctx.command('docker.cluster [selector]', '查看 Swarm 集群信息')
12
+ .alias('集群', 'swarm', 'docker集群')
13
+ .action(async (_, selector) => {
14
+ logger_1.commandLogger.debug(`docker.cluster 被调用: selector=${selector}`);
15
+ const service = getService();
16
+ if (!service)
17
+ return '❌ 服务未初始化';
18
+ const nodes = service.getNodesBySelector(selector || '');
19
+ if (nodes.length === 0)
20
+ return `❌ 未找到节点: ${selector}`;
21
+ const lines = [];
22
+ for (const node of nodes) {
23
+ if (node.status !== 'connected') {
24
+ lines.push(`=== ${node.name} ===`);
25
+ lines.push(' (未连接)');
26
+ lines.push('');
27
+ continue;
28
+ }
29
+ // 检查是否在 Swarm 模式
30
+ const isSwarm = await node.isSwarmMode();
31
+ if (!isSwarm) {
32
+ lines.push(`=== ${node.name} ===`);
33
+ lines.push(' (不在 Swarm 模式)');
34
+ lines.push('');
35
+ continue;
36
+ }
37
+ const swarmInfo = await node.getSwarmInfo();
38
+ if (!swarmInfo) {
39
+ lines.push(`=== ${node.name} ===`);
40
+ lines.push(' (无法获取集群信息)');
41
+ lines.push('');
42
+ continue;
43
+ }
44
+ lines.push(`=== ${node.name} ===`);
45
+ lines.push(` 集群 ID: ${swarmInfo.id}`);
46
+ lines.push(` 集群名称: ${swarmInfo.name}`);
47
+ lines.push(` 创建时间: ${swarmInfo.createdAt}`);
48
+ lines.push(` 更新时间: ${swarmInfo.updatedAt}`);
49
+ lines.push('');
50
+ }
51
+ return lines.join('\n').trim();
52
+ });
53
+ /**
54
+ * 查看集群节点列表
55
+ */
56
+ ctx.command('docker.cluster.nodes [selector]', '查看 Swarm 集群节点')
57
+ .alias('集群节点', 'swarm节点', 'swarm节点')
58
+ .option('format', '-f <format> 输出格式: simple|image', { fallback: null })
59
+ .action(async ({ options }, selector) => {
60
+ logger_1.commandLogger.debug(`docker.cluster.nodes 被调用: selector=${selector}, format=${options.format}`);
61
+ const service = getService();
62
+ if (!service)
63
+ return '❌ 服务未初始化';
64
+ const nodes = service.getNodesBySelector(selector || '');
65
+ if (nodes.length === 0)
66
+ return `❌ 未找到节点: ${selector}`;
67
+ const format = options.format || (useImageOutput ? 'image' : 'simple');
68
+ // 图片渲染模式
69
+ if (format === 'image') {
70
+ if (!ctx.puppeteer)
71
+ return '❌ 未安装 puppeteer 插件';
72
+ try {
73
+ const results = [];
74
+ for (const node of nodes) {
75
+ if (node.status !== 'connected')
76
+ continue;
77
+ const isSwarm = await node.isSwarmMode();
78
+ if (!isSwarm)
79
+ continue;
80
+ const swarmNodes = await node.getSwarmNodes();
81
+ if (swarmNodes.length > 0) {
82
+ results.push({ node, swarmNodes });
83
+ }
84
+ }
85
+ if (results.length === 0)
86
+ return '❌ 未找到任何 Swarm 集群节点';
87
+ const html = (0, render_1.generateSwarmNodesHtml)(results, '集群节点');
88
+ return await (0, render_1.renderToImage)(ctx, html);
89
+ }
90
+ catch (e) {
91
+ logger_1.commandLogger.error(`获取集群节点失败: ${e.message}`);
92
+ return `❌ 错误: ${e.message}`;
93
+ }
94
+ }
95
+ // 文字模式
96
+ const lines = [];
97
+ for (const node of nodes) {
98
+ if (node.status !== 'connected') {
99
+ lines.push(`=== ${node.name} ===`);
100
+ lines.push(' (未连接)');
101
+ lines.push('');
102
+ continue;
103
+ }
104
+ const isSwarm = await node.isSwarmMode();
105
+ if (!isSwarm) {
106
+ lines.push(`=== ${node.name} ===`);
107
+ lines.push(' (不在 Swarm 模式)');
108
+ lines.push('');
109
+ continue;
110
+ }
111
+ const swarmNodes = await node.getSwarmNodes();
112
+ lines.push(`=== ${node.name} (${swarmNodes.length} 个节点) ===`);
113
+ if (swarmNodes.length === 0) {
114
+ lines.push(' (无节点)');
115
+ }
116
+ else {
117
+ for (const n of swarmNodes) {
118
+ const shortId = n.ID.slice(0, 12);
119
+ const isLeader = n.ManagerStatus?.Leader ? ' 👑' : '';
120
+ const statusIcon = n.Status.State === 'ready' ? '🟢' : '🔴';
121
+ lines.push(` ${isLeader}${n.Hostname} (${n.Role})`);
122
+ lines.push(` ID: ${shortId}`);
123
+ lines.push(` 状态: ${statusIcon} ${n.Status.State} | 可用性: ${n.Availability}`);
124
+ lines.push(` 地址: ${n.Status.Addr}`);
125
+ if (n.ManagerStatus?.Reachability) {
126
+ lines.push(` 管理可达性: ${n.ManagerStatus.Reachability}`);
127
+ }
128
+ }
129
+ }
130
+ lines.push('');
131
+ }
132
+ return lines.join('\n').trim();
133
+ });
134
+ /**
135
+ * 查看集群服务列表
136
+ */
137
+ ctx.command('docker.cluster.services [selector]', '查看 Swarm 集群服务')
138
+ .alias('集群服务', 'swarm服务', '集群services')
139
+ .option('format', '-f <format> 输出格式: simple|image', { fallback: null })
140
+ .action(async ({ options }, selector) => {
141
+ logger_1.commandLogger.debug(`docker.cluster.services 被调用: selector=${selector}, format=${options.format}`);
142
+ const service = getService();
143
+ if (!service)
144
+ return '❌ 服务未初始化';
145
+ const nodes = service.getNodesBySelector(selector || '');
146
+ if (nodes.length === 0)
147
+ return `❌ 未找到节点: ${selector}`;
148
+ const format = options.format || (useImageOutput ? 'image' : 'simple');
149
+ // 图片渲染模式
150
+ if (format === 'image') {
151
+ if (!ctx.puppeteer)
152
+ return '❌ 未安装 puppeteer 插件';
153
+ try {
154
+ const results = [];
155
+ for (const node of nodes) {
156
+ if (node.status !== 'connected')
157
+ continue;
158
+ const isSwarm = await node.isSwarmMode();
159
+ if (!isSwarm)
160
+ continue;
161
+ const services = await node.getSwarmServices();
162
+ if (services.length > 0) {
163
+ results.push({ node, services });
164
+ }
165
+ }
166
+ if (results.length === 0)
167
+ return '❌ 未找到任何 Swarm 服务';
168
+ const html = (0, render_1.generateSwarmServicesHtml)(results, '集群服务');
169
+ return await (0, render_1.renderToImage)(ctx, html);
170
+ }
171
+ catch (e) {
172
+ logger_1.commandLogger.error(`获取集群服务失败: ${e.message}`);
173
+ return `❌ 错误: ${e.message}`;
174
+ }
175
+ }
176
+ // 文字模式
177
+ const lines = [];
178
+ for (const node of nodes) {
179
+ if (node.status !== 'connected') {
180
+ lines.push(`=== ${node.name} ===`);
181
+ lines.push(' (未连接)');
182
+ lines.push('');
183
+ continue;
184
+ }
185
+ const isSwarm = await node.isSwarmMode();
186
+ if (!isSwarm) {
187
+ lines.push(`=== ${node.name} ===`);
188
+ lines.push(' (不在 Swarm 模式)');
189
+ lines.push('');
190
+ continue;
191
+ }
192
+ const services = await node.getSwarmServices();
193
+ lines.push(`=== ${node.name} (${services.length} 个服务) ===`);
194
+ if (services.length === 0) {
195
+ lines.push(' (无服务)');
196
+ }
197
+ else {
198
+ for (const s of services) {
199
+ const shortId = s.ID.slice(0, 12);
200
+ const imageName = s.Image.split('@')[0];
201
+ lines.push(` ${s.Name}`);
202
+ lines.push(` ID: ${shortId} | 副本: ${s.Replicas} | 镜像: ${imageName}`);
203
+ if (s.Ports !== '-') {
204
+ lines.push(` 端口: ${s.Ports}`);
205
+ }
206
+ }
207
+ }
208
+ lines.push('');
209
+ }
210
+ return lines.join('\n').trim();
211
+ });
212
+ /**
213
+ * 查看集群服务任务
214
+ */
215
+ ctx.command('docker.cluster.ps <selector> <service>', '查看 Swarm 服务任务')
216
+ .alias('集群任务', 'swarm任务', 'swarmps', '集群ps')
217
+ .option('format', '-f <format> 输出格式: simple|image', { fallback: null })
218
+ .action(async ({ options }, selector, serviceName) => {
219
+ logger_1.commandLogger.debug(`docker.cluster.ps 被调用: selector=${selector}, service=${serviceName}`);
220
+ const service = getService();
221
+ if (!service)
222
+ return '❌ 服务未初始化';
223
+ if (!serviceName) {
224
+ return '⚠️ 请指定服务名称\n例如: 集群任务 yun my-service';
225
+ }
226
+ const nodes = service.getNodesBySelector(selector || '');
227
+ if (nodes.length === 0)
228
+ return `❌ 未找到节点: ${selector}`;
229
+ const format = options.format || (useImageOutput ? 'image' : 'simple');
230
+ // 图片渲染模式
231
+ if (format === 'image') {
232
+ if (!ctx.puppeteer)
233
+ return '❌ 未安装 puppeteer 插件';
234
+ try {
235
+ const results = [];
236
+ for (const node of nodes) {
237
+ if (node.status !== 'connected')
238
+ continue;
239
+ const isSwarm = await node.isSwarmMode();
240
+ if (!isSwarm)
241
+ continue;
242
+ const tasks = await node.getSwarmTasks(serviceName);
243
+ if (tasks.length > 0) {
244
+ results.push({ node, serviceName, tasks });
245
+ }
246
+ }
247
+ if (results.length === 0)
248
+ return `❌ 未找到服务 "${serviceName}" 的任务`;
249
+ const html = (0, render_1.generateSwarmTasksHtml)(results, `集群任务 - ${serviceName}`);
250
+ return await (0, render_1.renderToImage)(ctx, html);
251
+ }
252
+ catch (e) {
253
+ logger_1.commandLogger.error(`获取集群任务失败: ${e.message}`);
254
+ return `❌ 错误: ${e.message}`;
255
+ }
256
+ }
257
+ // 文字模式
258
+ const lines = [];
259
+ for (const node of nodes) {
260
+ if (node.status !== 'connected') {
261
+ lines.push(`=== ${node.name} ===`);
262
+ lines.push(' (未连接)');
263
+ lines.push('');
264
+ continue;
265
+ }
266
+ const isSwarm = await node.isSwarmMode();
267
+ if (!isSwarm) {
268
+ lines.push(`=== ${node.name} ===`);
269
+ lines.push(' (不在 Swarm 模式)');
270
+ lines.push('');
271
+ continue;
272
+ }
273
+ const tasks = await node.getSwarmTasks(serviceName);
274
+ lines.push(`=== ${node.name} (${tasks.length} 个任务) ===`);
275
+ if (tasks.length === 0) {
276
+ lines.push(` (服务 "${serviceName}" 无任务或不存在)`);
277
+ }
278
+ else {
279
+ for (const t of tasks) {
280
+ const shortId = t.ID.slice(0, 12);
281
+ const statusIcon = t.Status.State === 'running' ? '🟢' :
282
+ t.Status.State === 'pending' ? '⏳' :
283
+ t.Status.State === 'failed' ? '❌' : '⚪';
284
+ lines.push(` ${statusIcon} Slot ${t.Slot} | ${t.Status.State}`);
285
+ lines.push(` ID: ${shortId}`);
286
+ lines.push(` 节点: ${t.NodeID} | 期望状态: ${t.DesiredState}`);
287
+ lines.push(` 时间: ${t.Status.Since}`);
288
+ }
289
+ }
290
+ lines.push('');
291
+ }
292
+ return lines.join('\n').trim();
293
+ });
294
+ }