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
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件入口 - 支持订阅机制的 Docker 管理插件
|
|
3
|
+
*/
|
|
4
|
+
import { Context, Schema } from 'koishi';
|
|
5
|
+
import type { DockerControlConfig } from './types';
|
|
6
|
+
export declare const name = "docker-control";
|
|
7
|
+
export declare const inject: {
|
|
8
|
+
required: string[];
|
|
9
|
+
optional: string[];
|
|
10
|
+
};
|
|
11
|
+
interface DockerControlSubscription {
|
|
12
|
+
id: number;
|
|
13
|
+
platform: string;
|
|
14
|
+
channelId: string;
|
|
15
|
+
nodeId: string;
|
|
16
|
+
containerPattern: string;
|
|
17
|
+
eventTypes: string;
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
createdAt: number;
|
|
20
|
+
}
|
|
21
|
+
declare module 'koishi' {
|
|
22
|
+
interface Context {
|
|
23
|
+
puppeteer?: {
|
|
24
|
+
render: (html: string, callback?: (page: any, next: (handle?: any) => Promise<string>) => Promise<string>) => Promise<string>;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
interface Tables {
|
|
28
|
+
'docker_control_subscriptions': DockerControlSubscription;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export declare const Config: Schema<Schemastery.ObjectS<{
|
|
32
|
+
requestTimeout: Schema<number, number>;
|
|
33
|
+
debug: Schema<boolean, boolean>;
|
|
34
|
+
imageOutput: Schema<boolean, boolean>;
|
|
35
|
+
defaultLogLines: Schema<number, number>;
|
|
36
|
+
monitor: Schema<Schemastery.ObjectS<{
|
|
37
|
+
debounceWait: Schema<number, number>;
|
|
38
|
+
flappingWindow: Schema<number, number>;
|
|
39
|
+
flappingThreshold: Schema<number, number>;
|
|
40
|
+
}>, Schemastery.ObjectT<{
|
|
41
|
+
debounceWait: Schema<number, number>;
|
|
42
|
+
flappingWindow: Schema<number, number>;
|
|
43
|
+
flappingThreshold: Schema<number, number>;
|
|
44
|
+
}>>;
|
|
45
|
+
credentials: Schema<Schemastery.ObjectS<{
|
|
46
|
+
id: Schema<string, string>;
|
|
47
|
+
name: Schema<string, string>;
|
|
48
|
+
username: Schema<string, string>;
|
|
49
|
+
authType: Schema<"key" | "password", "key" | "password">;
|
|
50
|
+
password: Schema<string, string>;
|
|
51
|
+
privateKey: Schema<string, string>;
|
|
52
|
+
passphrase: Schema<string, string>;
|
|
53
|
+
}>[], Schemastery.ObjectT<{
|
|
54
|
+
id: Schema<string, string>;
|
|
55
|
+
name: Schema<string, string>;
|
|
56
|
+
username: Schema<string, string>;
|
|
57
|
+
authType: Schema<"key" | "password", "key" | "password">;
|
|
58
|
+
password: Schema<string, string>;
|
|
59
|
+
privateKey: Schema<string, string>;
|
|
60
|
+
passphrase: Schema<string, string>;
|
|
61
|
+
}>[]>;
|
|
62
|
+
nodes: Schema<Schemastery.ObjectS<{
|
|
63
|
+
id: Schema<string, string>;
|
|
64
|
+
name: Schema<string, string>;
|
|
65
|
+
tags: Schema<string[], string[]>;
|
|
66
|
+
host: Schema<string, string>;
|
|
67
|
+
port: Schema<number, number>;
|
|
68
|
+
credentialId: Schema<string, string>;
|
|
69
|
+
}>[], Schemastery.ObjectT<{
|
|
70
|
+
id: Schema<string, string>;
|
|
71
|
+
name: Schema<string, string>;
|
|
72
|
+
tags: Schema<string[], string[]>;
|
|
73
|
+
host: Schema<string, string>;
|
|
74
|
+
port: Schema<number, number>;
|
|
75
|
+
credentialId: Schema<string, string>;
|
|
76
|
+
}>[]>;
|
|
77
|
+
}>, Schemastery.ObjectT<{
|
|
78
|
+
requestTimeout: Schema<number, number>;
|
|
79
|
+
debug: Schema<boolean, boolean>;
|
|
80
|
+
imageOutput: Schema<boolean, boolean>;
|
|
81
|
+
defaultLogLines: Schema<number, number>;
|
|
82
|
+
monitor: Schema<Schemastery.ObjectS<{
|
|
83
|
+
debounceWait: Schema<number, number>;
|
|
84
|
+
flappingWindow: Schema<number, number>;
|
|
85
|
+
flappingThreshold: Schema<number, number>;
|
|
86
|
+
}>, Schemastery.ObjectT<{
|
|
87
|
+
debounceWait: Schema<number, number>;
|
|
88
|
+
flappingWindow: Schema<number, number>;
|
|
89
|
+
flappingThreshold: Schema<number, number>;
|
|
90
|
+
}>>;
|
|
91
|
+
credentials: Schema<Schemastery.ObjectS<{
|
|
92
|
+
id: Schema<string, string>;
|
|
93
|
+
name: Schema<string, string>;
|
|
94
|
+
username: Schema<string, string>;
|
|
95
|
+
authType: Schema<"key" | "password", "key" | "password">;
|
|
96
|
+
password: Schema<string, string>;
|
|
97
|
+
privateKey: Schema<string, string>;
|
|
98
|
+
passphrase: Schema<string, string>;
|
|
99
|
+
}>[], Schemastery.ObjectT<{
|
|
100
|
+
id: Schema<string, string>;
|
|
101
|
+
name: Schema<string, string>;
|
|
102
|
+
username: Schema<string, string>;
|
|
103
|
+
authType: Schema<"key" | "password", "key" | "password">;
|
|
104
|
+
password: Schema<string, string>;
|
|
105
|
+
privateKey: Schema<string, string>;
|
|
106
|
+
passphrase: Schema<string, string>;
|
|
107
|
+
}>[]>;
|
|
108
|
+
nodes: Schema<Schemastery.ObjectS<{
|
|
109
|
+
id: Schema<string, string>;
|
|
110
|
+
name: Schema<string, string>;
|
|
111
|
+
tags: Schema<string[], string[]>;
|
|
112
|
+
host: Schema<string, string>;
|
|
113
|
+
port: Schema<number, number>;
|
|
114
|
+
credentialId: Schema<string, string>;
|
|
115
|
+
}>[], Schemastery.ObjectT<{
|
|
116
|
+
id: Schema<string, string>;
|
|
117
|
+
name: Schema<string, string>;
|
|
118
|
+
tags: Schema<string[], string[]>;
|
|
119
|
+
host: Schema<string, string>;
|
|
120
|
+
port: Schema<number, number>;
|
|
121
|
+
credentialId: Schema<string, string>;
|
|
122
|
+
}>[]>;
|
|
123
|
+
}>>;
|
|
124
|
+
export declare function apply(ctx: Context, config: DockerControlConfig): void;
|
|
125
|
+
export {};
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Config = exports.inject = exports.name = void 0;
|
|
4
|
+
exports.apply = apply;
|
|
5
|
+
/**
|
|
6
|
+
* 插件入口 - 支持订阅机制的 Docker 管理插件
|
|
7
|
+
*/
|
|
8
|
+
const koishi_1 = require("koishi");
|
|
9
|
+
const logger_1 = require("./utils/logger");
|
|
10
|
+
const service_1 = require("./service");
|
|
11
|
+
const monitor_1 = require("./service/monitor");
|
|
12
|
+
const commands_1 = require("./commands");
|
|
13
|
+
exports.name = 'docker-control';
|
|
14
|
+
exports.inject = {
|
|
15
|
+
required: ['database'],
|
|
16
|
+
optional: ['puppeteer'],
|
|
17
|
+
};
|
|
18
|
+
// 导出配置 Schema
|
|
19
|
+
exports.Config = koishi_1.Schema.object({
|
|
20
|
+
requestTimeout: koishi_1.Schema.number().default(30000).description('请求超时 (毫秒)'),
|
|
21
|
+
debug: koishi_1.Schema.boolean().default(false).description('调试模式'),
|
|
22
|
+
imageOutput: koishi_1.Schema.boolean().default(false).description('使用图片格式输出容器列表'),
|
|
23
|
+
defaultLogLines: koishi_1.Schema.number().default(100).description('默认日志显示的行数'),
|
|
24
|
+
// 监控策略
|
|
25
|
+
monitor: koishi_1.Schema.object({
|
|
26
|
+
debounceWait: koishi_1.Schema.number().default(60000).description('容器意外停止后等待重启的时间 (ms),在此期间恢复不发送通知'),
|
|
27
|
+
flappingWindow: koishi_1.Schema.number().default(300000).description('检测抖动/频繁重启的时间窗口 (ms)'),
|
|
28
|
+
flappingThreshold: koishi_1.Schema.number().default(3).description('时间窗口内允许的最大状态变更次数,超过则报警'),
|
|
29
|
+
}).description('监控策略设置'),
|
|
30
|
+
credentials: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
31
|
+
id: koishi_1.Schema.string().required(),
|
|
32
|
+
name: koishi_1.Schema.string().required(),
|
|
33
|
+
username: koishi_1.Schema.string().default('root'),
|
|
34
|
+
authType: koishi_1.Schema.union(['key', 'password']).default('key'),
|
|
35
|
+
password: koishi_1.Schema.string().role('secret'),
|
|
36
|
+
privateKey: koishi_1.Schema.string().role('textarea'),
|
|
37
|
+
passphrase: koishi_1.Schema.string().role('secret'),
|
|
38
|
+
})).description('SSH 凭证列表'),
|
|
39
|
+
nodes: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
40
|
+
id: koishi_1.Schema.string().required(),
|
|
41
|
+
name: koishi_1.Schema.string().required(),
|
|
42
|
+
tags: koishi_1.Schema.array(koishi_1.Schema.string()).default([]),
|
|
43
|
+
host: koishi_1.Schema.string().required().description('SSH 主机地址'),
|
|
44
|
+
port: koishi_1.Schema.number().default(22).description('SSH 端口'),
|
|
45
|
+
credentialId: koishi_1.Schema.string().required().description('SSH 凭证 ID'),
|
|
46
|
+
})).description('Docker 节点列表'),
|
|
47
|
+
});
|
|
48
|
+
// 事件消息模板
|
|
49
|
+
const EVENT_MESSAGES = {
|
|
50
|
+
'container.start': '已启动',
|
|
51
|
+
'container.stop': '已停止',
|
|
52
|
+
'container.restart': '已重启',
|
|
53
|
+
'container.die': '已异常退出',
|
|
54
|
+
'container.flapping': '运行状态不稳定 (频繁重启)',
|
|
55
|
+
};
|
|
56
|
+
function apply(ctx, config) {
|
|
57
|
+
// 表名
|
|
58
|
+
const TABLE_NAME = 'docker_control_subscriptions';
|
|
59
|
+
// 注册表结构
|
|
60
|
+
ctx.model.extend(TABLE_NAME, {
|
|
61
|
+
id: 'unsigned',
|
|
62
|
+
platform: 'string',
|
|
63
|
+
channelId: 'string',
|
|
64
|
+
nodeId: 'string',
|
|
65
|
+
containerPattern: 'string',
|
|
66
|
+
eventTypes: 'text',
|
|
67
|
+
enabled: 'boolean',
|
|
68
|
+
createdAt: 'integer',
|
|
69
|
+
}, {
|
|
70
|
+
autoInc: true,
|
|
71
|
+
primary: 'id',
|
|
72
|
+
});
|
|
73
|
+
// 安全检查
|
|
74
|
+
if (!config) {
|
|
75
|
+
logger_1.logger.info('Docker Control 配置未定义,跳过加载');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// 验证配置
|
|
79
|
+
const errors = [];
|
|
80
|
+
const credentialIds = new Set(config.credentials?.map(c => c.id) || []);
|
|
81
|
+
for (const node of config.nodes || []) {
|
|
82
|
+
if (!credentialIds.has(node.credentialId)) {
|
|
83
|
+
errors.push(`节点 ${node.name} 引用的凭证 ${node.credentialId} 不存在`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (errors.length > 0) {
|
|
87
|
+
logger_1.logger.warn('配置验证失败:');
|
|
88
|
+
for (const error of errors) {
|
|
89
|
+
logger_1.logger.warn(` - ${error}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// 如果没有配置节点,直接跳过初始化
|
|
93
|
+
if (!config.nodes || config.nodes.length === 0) {
|
|
94
|
+
logger_1.logger.info('Docker Control 未配置任何节点,跳过初始化');
|
|
95
|
+
(0, commands_1.registerCommands)(ctx, () => null);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// 创建服务实例
|
|
99
|
+
const dockerService = new service_1.DockerService(ctx, config);
|
|
100
|
+
// 传入监控配置
|
|
101
|
+
const monitorManager = new monitor_1.MonitorManager(config.monitor || {});
|
|
102
|
+
// 插件就绪时初始化(异步,不阻塞 Koishi 启动)
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
dockerService.initialize().catch((e) => {
|
|
105
|
+
logger_1.logger.error(`初始化失败: ${e?.message || e}`);
|
|
106
|
+
});
|
|
107
|
+
}, 0);
|
|
108
|
+
// 注册基础指令
|
|
109
|
+
(0, commands_1.registerCommands)(ctx, () => dockerService, config);
|
|
110
|
+
// ==================== 订阅指令 ====================
|
|
111
|
+
ctx.command('docker.subscribe <node> <container>', '订阅容器状态变更通知')
|
|
112
|
+
.alias('docker订阅', '订阅', '容器订阅')
|
|
113
|
+
.option('events', '-e <events> 监听的事件类型,默认全部', { fallback: 'start,stop,restart,die' })
|
|
114
|
+
.action(async ({ options, session }, nodeSelector, containerPattern) => {
|
|
115
|
+
const { platform, channelId } = session;
|
|
116
|
+
// 检查服务是否可用
|
|
117
|
+
if (!dockerService) {
|
|
118
|
+
return '❌ Docker 服务未初始化';
|
|
119
|
+
}
|
|
120
|
+
// 验证必填参数
|
|
121
|
+
if (!nodeSelector || !containerPattern) {
|
|
122
|
+
return '❌ 缺少参数,用法: docker.subscribe <节点> <容器>\n 示例: docker.subscribe yun myapp\n 示例: docker.subscribe all all';
|
|
123
|
+
}
|
|
124
|
+
// 验证节点
|
|
125
|
+
const nodes = dockerService.getNodesBySelector(nodeSelector);
|
|
126
|
+
if (nodes.length === 0) {
|
|
127
|
+
return `❌ 找不到节点: ${nodeSelector}`;
|
|
128
|
+
}
|
|
129
|
+
const nodeId = nodeSelector === 'all' ? '' : nodes[0].id;
|
|
130
|
+
const eventTypes = options.events.split(',').map(e => e.trim()).filter(Boolean);
|
|
131
|
+
const targetContainerPattern = containerPattern === 'all' ? '' : containerPattern;
|
|
132
|
+
// 查询是否已存在相同订阅
|
|
133
|
+
const existing = await ctx.model.get(TABLE_NAME, {
|
|
134
|
+
platform,
|
|
135
|
+
channelId,
|
|
136
|
+
nodeId,
|
|
137
|
+
containerPattern: targetContainerPattern,
|
|
138
|
+
});
|
|
139
|
+
if (existing.length > 0) {
|
|
140
|
+
// 更新已有订阅
|
|
141
|
+
await ctx.model.set(TABLE_NAME, { id: existing[0].id }, {
|
|
142
|
+
eventTypes: JSON.stringify(eventTypes),
|
|
143
|
+
enabled: true,
|
|
144
|
+
});
|
|
145
|
+
logger_1.logger.info(`更新订阅: ${platform}:${channelId} ${nodeId || '*'} ${targetContainerPattern || '*'}`);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
// 创建新订阅
|
|
149
|
+
await ctx.database.create(TABLE_NAME, {
|
|
150
|
+
platform,
|
|
151
|
+
channelId,
|
|
152
|
+
nodeId,
|
|
153
|
+
containerPattern: targetContainerPattern,
|
|
154
|
+
eventTypes: JSON.stringify(eventTypes),
|
|
155
|
+
enabled: true,
|
|
156
|
+
createdAt: Date.now(),
|
|
157
|
+
});
|
|
158
|
+
logger_1.logger.info(`创建订阅: ${platform}:${channelId} ${nodeId || '*'} ${targetContainerPattern || '*'}`);
|
|
159
|
+
}
|
|
160
|
+
const nodeDesc = nodeSelector === 'all' ? '所有节点' : nodes[0].name;
|
|
161
|
+
const containerDesc = containerPattern === 'all' ? '所有容器' : containerPattern;
|
|
162
|
+
return `✅ 已更新订阅\n 节点: ${nodeDesc}\n 容器: ${containerDesc}\n 事件: ${eventTypes.join(', ')}`;
|
|
163
|
+
});
|
|
164
|
+
// 取消订阅
|
|
165
|
+
ctx.command('docker.unsubscribe <id>', '取消订阅')
|
|
166
|
+
.alias('docker取消订阅', '取消订阅')
|
|
167
|
+
.action(async (_, id) => {
|
|
168
|
+
const subId = Number(id);
|
|
169
|
+
if (isNaN(subId) || subId <= 0) {
|
|
170
|
+
return '❌ 请提供有效的订阅 ID,使用 docker订阅列表 查看';
|
|
171
|
+
}
|
|
172
|
+
await ctx.model.remove(TABLE_NAME, { id: subId });
|
|
173
|
+
return `✅ 已取消订阅 ${subId}`;
|
|
174
|
+
});
|
|
175
|
+
// 查看订阅列表
|
|
176
|
+
ctx.command('docker.subscriptions', '查看当前订阅')
|
|
177
|
+
.alias('docker订阅列表', '订阅列表')
|
|
178
|
+
.action(async ({ session }) => {
|
|
179
|
+
const { platform, channelId } = session;
|
|
180
|
+
const rows = await ctx.model.get(TABLE_NAME, { platform, channelId });
|
|
181
|
+
if (rows.length === 0) {
|
|
182
|
+
return '暂无订阅,使用 docker.subscribe <节点> <容器> 添加订阅';
|
|
183
|
+
}
|
|
184
|
+
const lines = ['=== 我的订阅 ==='];
|
|
185
|
+
for (const row of rows) {
|
|
186
|
+
const nodeDesc = row.nodeId ? `(节点: ${row.nodeId})` : '(所有节点)';
|
|
187
|
+
const containerDesc = row.containerPattern || '(所有容器)';
|
|
188
|
+
const eventTypes = JSON.parse(row.eventTypes || '[]');
|
|
189
|
+
lines.push(`[${row.id}] ${nodeDesc} ${containerDesc}`);
|
|
190
|
+
lines.push(` 事件: ${eventTypes.join(', ')}`);
|
|
191
|
+
}
|
|
192
|
+
return lines.join('\n');
|
|
193
|
+
});
|
|
194
|
+
// ==================== 事件监听 ====================
|
|
195
|
+
// 1. 将 DockerService 的原始事件喂给 MonitorManager
|
|
196
|
+
dockerService.onNodeEvent((event, nodeId) => {
|
|
197
|
+
const node = dockerService.getNode(nodeId);
|
|
198
|
+
if (node) {
|
|
199
|
+
monitorManager.processEvent(node, event);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
// 2. 监听 MonitorManager 处理后的"智能"事件
|
|
203
|
+
const eventUnsub = monitorManager.onProcessedEvent(async (processedEvent) => {
|
|
204
|
+
const { eventType, action, nodeName, containerName, nodeId } = processedEvent;
|
|
205
|
+
// [调试日志]
|
|
206
|
+
logger_1.commandLogger.debug(`[推送] 准备发送通知: [${nodeName}] ${containerName} -> ${action}`);
|
|
207
|
+
// 获取所有订阅并发送通知
|
|
208
|
+
const subs = await ctx.model.get(TABLE_NAME, {});
|
|
209
|
+
if (subs.length === 0) {
|
|
210
|
+
logger_1.commandLogger.debug(`[推送] 无订阅`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
for (const sub of subs) {
|
|
214
|
+
if (!sub.enabled)
|
|
215
|
+
continue;
|
|
216
|
+
// 1. 检查事件类型
|
|
217
|
+
const eventTypes = JSON.parse(sub.eventTypes || '[]');
|
|
218
|
+
// 特殊逻辑:如果订阅了 'restart' 或 'die',通常也希望能收到 'flapping' 报警
|
|
219
|
+
const effectiveEventTypes = [...eventTypes];
|
|
220
|
+
if (effectiveEventTypes.includes('die') || effectiveEventTypes.includes('restart')) {
|
|
221
|
+
effectiveEventTypes.push('flapping');
|
|
222
|
+
}
|
|
223
|
+
if (!effectiveEventTypes.includes(action)) {
|
|
224
|
+
logger_1.commandLogger.debug(` - 订阅[${sub.id}] 忽略: 事件类型不匹配 (订阅: ${eventTypes.join(', ')}, 收到: ${action})`);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
// 2. 检查节点匹配
|
|
228
|
+
if (sub.nodeId && sub.nodeId !== nodeId) {
|
|
229
|
+
logger_1.commandLogger.debug(` - 订阅[${sub.id}] 忽略: 节点不匹配`);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
// 3. 检查容器名称匹配
|
|
233
|
+
if (sub.containerPattern) {
|
|
234
|
+
const pattern = sub.containerPattern
|
|
235
|
+
.replace(/\*/g, '.*')
|
|
236
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
237
|
+
const regex = new RegExp(`^${pattern}$`, 'i');
|
|
238
|
+
if (!regex.test(containerName)) {
|
|
239
|
+
logger_1.commandLogger.debug(` - 订阅[${sub.id}] 忽略: 容器名不匹配`);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// 构建消息
|
|
244
|
+
const emoji = {
|
|
245
|
+
start: '🟢',
|
|
246
|
+
stop: '🔴',
|
|
247
|
+
restart: '🟡',
|
|
248
|
+
die: '⚠️',
|
|
249
|
+
flapping: '💥',
|
|
250
|
+
kill: '💀',
|
|
251
|
+
health_status: '💚',
|
|
252
|
+
};
|
|
253
|
+
const actionText = EVENT_MESSAGES[eventType] || action;
|
|
254
|
+
const emojiChar = emoji[action] || '📦';
|
|
255
|
+
const message = `${emojiChar} 【${nodeName}】${containerName} ${actionText}`;
|
|
256
|
+
// 发送
|
|
257
|
+
try {
|
|
258
|
+
const bots = ctx.bots.filter(b => b.platform === sub.platform);
|
|
259
|
+
if (bots.length === 0) {
|
|
260
|
+
logger_1.commandLogger.warn(` - 订阅[${sub.id}] 失败: 找不到平台 ${sub.platform} 的 Bot`);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
for (const bot of bots) {
|
|
264
|
+
await bot.sendMessage(sub.channelId, message);
|
|
265
|
+
logger_1.commandLogger.info(`[通知] 已推送到 ${sub.channelId}: ${message}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch (e) {
|
|
269
|
+
logger_1.commandLogger.error(`通知发送失败: ${e}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
// ==================== 调试指令 ====================
|
|
274
|
+
if (config.debug) {
|
|
275
|
+
const debugLevel = koishi_1.Logger.DEBUG || 4;
|
|
276
|
+
logger_1.logger.level = debugLevel;
|
|
277
|
+
logger_1.nodeLogger.level = debugLevel;
|
|
278
|
+
logger_1.commandLogger.level = debugLevel;
|
|
279
|
+
logger_1.logger.info(`[DEBUG] 调试模式已启用 (Level: ${debugLevel})`);
|
|
280
|
+
ctx.command('docker.debug', '调试指令').action(async () => {
|
|
281
|
+
const nodes = dockerService.getAllNodes();
|
|
282
|
+
const online = dockerService.getOnlineNodes();
|
|
283
|
+
const subs = await ctx.model.get(TABLE_NAME, {});
|
|
284
|
+
const lines = [
|
|
285
|
+
'=== Docker Control 调试信息 ===',
|
|
286
|
+
`节点总数: ${nodes.length}`,
|
|
287
|
+
`在线节点: ${online.length}`,
|
|
288
|
+
`离线节点: ${nodes.length - online.length}`,
|
|
289
|
+
`订阅总数: ${subs.length}`,
|
|
290
|
+
'',
|
|
291
|
+
];
|
|
292
|
+
lines.push('--- 节点详情 ---');
|
|
293
|
+
for (const n of nodes) {
|
|
294
|
+
const status = n.status === 'connected' ? '🟢' : n.status === 'connecting' ? '🟡' : '🔴';
|
|
295
|
+
lines.push(`${status} ${n.name} (${n.id})`);
|
|
296
|
+
}
|
|
297
|
+
lines.push('');
|
|
298
|
+
lines.push('--- 订阅列表 ---');
|
|
299
|
+
for (const sub of subs) {
|
|
300
|
+
lines.push(`[${sub.id}] ${sub.platform}:${sub.channelId} ${sub.nodeId || '*'} ${sub.containerPattern || '*'}`);
|
|
301
|
+
}
|
|
302
|
+
return lines.join('\n');
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
logger_1.logger.info('Docker Control 插件已加载');
|
|
306
|
+
// 插件卸载时清理
|
|
307
|
+
ctx.on('dispose', async () => {
|
|
308
|
+
logger_1.logger.info('Docker Control 插件正在卸载...');
|
|
309
|
+
eventUnsub();
|
|
310
|
+
await dockerService.stopAll();
|
|
311
|
+
});
|
|
312
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
/**
|
|
3
|
+
* SSH Agent Option Interface
|
|
4
|
+
*/
|
|
5
|
+
export interface SSHAgentOptions extends http.AgentOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Provides the raw stream (SSH Channel)
|
|
8
|
+
* MUST return a Promise that resolves to a stream
|
|
9
|
+
*/
|
|
10
|
+
getStream: () => Promise<any>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Custom SSH Agent
|
|
14
|
+
* Adapts an SSH Stream (ClientChannel) to work with Node.js http.Agent
|
|
15
|
+
* Overrides createConnection to use the provided stream factory.
|
|
16
|
+
*/
|
|
17
|
+
export declare class SSHAgent extends http.Agent {
|
|
18
|
+
private getStream;
|
|
19
|
+
constructor(options: SSHAgentOptions);
|
|
20
|
+
/**
|
|
21
|
+
* Override createConnection to inject our SSH stream
|
|
22
|
+
*/
|
|
23
|
+
createConnection(options: any, callback: (err: Error | null, stream: any) => void): any;
|
|
24
|
+
/**
|
|
25
|
+
* Patch the stream with missing methods (e.g., setTimeout)
|
|
26
|
+
*/
|
|
27
|
+
private patchStream;
|
|
28
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SSHAgent = void 0;
|
|
7
|
+
const http_1 = __importDefault(require("http"));
|
|
8
|
+
const koishi_1 = require("koishi");
|
|
9
|
+
const logger = new koishi_1.Logger('docker-control-agent');
|
|
10
|
+
/**
|
|
11
|
+
* Custom SSH Agent
|
|
12
|
+
* Adapts an SSH Stream (ClientChannel) to work with Node.js http.Agent
|
|
13
|
+
* Overrides createConnection to use the provided stream factory.
|
|
14
|
+
*/
|
|
15
|
+
class SSHAgent extends http_1.default.Agent {
|
|
16
|
+
constructor(options) {
|
|
17
|
+
super({
|
|
18
|
+
keepAlive: true,
|
|
19
|
+
timeout: 30000, // Agent level timeout
|
|
20
|
+
...options
|
|
21
|
+
});
|
|
22
|
+
this.getStream = options.getStream;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Override createConnection to inject our SSH stream
|
|
26
|
+
*/
|
|
27
|
+
createConnection(options, callback) {
|
|
28
|
+
this.getStream()
|
|
29
|
+
.then(stream => {
|
|
30
|
+
// Patch the stream to satisfy net.Socket interface required by http.ClientRequest
|
|
31
|
+
this.patchStream(stream);
|
|
32
|
+
// Emit 'connect' event as expected by http.Agent
|
|
33
|
+
setImmediate(() => {
|
|
34
|
+
stream.emit('connect');
|
|
35
|
+
});
|
|
36
|
+
callback(null, stream);
|
|
37
|
+
})
|
|
38
|
+
.catch(err => {
|
|
39
|
+
callback(err, null);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Patch the stream with missing methods (e.g., setTimeout)
|
|
44
|
+
*/
|
|
45
|
+
patchStream(stream) {
|
|
46
|
+
// ssh2 stream lacks setTimeout, which is required by http module
|
|
47
|
+
if (typeof stream.setTimeout !== 'function') {
|
|
48
|
+
stream.setTimeout = (msecs, callback) => {
|
|
49
|
+
if (callback) {
|
|
50
|
+
setTimeout(callback, msecs);
|
|
51
|
+
}
|
|
52
|
+
return stream;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Some libraries might check for ref/unref
|
|
56
|
+
if (typeof stream.ref !== 'function') {
|
|
57
|
+
stream.ref = () => stream;
|
|
58
|
+
}
|
|
59
|
+
if (typeof stream.unref !== 'function') {
|
|
60
|
+
stream.unref = () => stream;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.SSHAgent = SSHAgent;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { NodeConfig, DockerControlConfig } from '../types';
|
|
2
|
+
export declare class DockerConnector {
|
|
3
|
+
private config;
|
|
4
|
+
private fullConfig;
|
|
5
|
+
private sshClient;
|
|
6
|
+
constructor(config: NodeConfig, fullConfig: DockerControlConfig);
|
|
7
|
+
/**
|
|
8
|
+
* 执行 SSH 命令
|
|
9
|
+
*/
|
|
10
|
+
exec(command: string): Promise<string>;
|
|
11
|
+
private execInternal;
|
|
12
|
+
/**
|
|
13
|
+
* 执行 docker ps 获取容器列表
|
|
14
|
+
*/
|
|
15
|
+
listContainers(all?: boolean): Promise<string>;
|
|
16
|
+
/**
|
|
17
|
+
* 执行 docker start
|
|
18
|
+
*/
|
|
19
|
+
startContainer(containerId: string): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* 执行 docker stop
|
|
22
|
+
*/
|
|
23
|
+
stopContainer(containerId: string, timeout?: number): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* 执行 docker restart
|
|
26
|
+
*/
|
|
27
|
+
restartContainer(containerId: string, timeout?: number): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* 获取容器日志
|
|
30
|
+
*/
|
|
31
|
+
getLogs(containerId: string, tail?: number): Promise<string>;
|
|
32
|
+
/**
|
|
33
|
+
* 执行容器内命令
|
|
34
|
+
*/
|
|
35
|
+
execContainer(containerId: string, cmd: string): Promise<string>;
|
|
36
|
+
/**
|
|
37
|
+
* 监听 Docker 事件流
|
|
38
|
+
* @param callback 每行事件数据的回调
|
|
39
|
+
* @returns 停止监听的方法
|
|
40
|
+
*/
|
|
41
|
+
startEventStream(callback: (line: string) => void): Promise<() => void>;
|
|
42
|
+
private connected;
|
|
43
|
+
/**
|
|
44
|
+
* 标记连接状态
|
|
45
|
+
*/
|
|
46
|
+
setConnected(status: boolean): void;
|
|
47
|
+
/**
|
|
48
|
+
* 销毁连接
|
|
49
|
+
*/
|
|
50
|
+
dispose(): void;
|
|
51
|
+
/**
|
|
52
|
+
* 获取 SSH 连接
|
|
53
|
+
*/
|
|
54
|
+
private getConnection;
|
|
55
|
+
/**
|
|
56
|
+
* 创建 SSH 连接
|
|
57
|
+
*/
|
|
58
|
+
private createConnection;
|
|
59
|
+
/**
|
|
60
|
+
* 构建认证选项
|
|
61
|
+
*/
|
|
62
|
+
private buildAuthOptions;
|
|
63
|
+
/**
|
|
64
|
+
* 获取凭证配置
|
|
65
|
+
*/
|
|
66
|
+
private getCredential;
|
|
67
|
+
}
|