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.
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MonitorManager = void 0;
4
+ const logger_1 = require("../utils/logger");
5
+ class MonitorManager {
6
+ constructor(config = {}) {
7
+ this.config = config;
8
+ /** 容器状态映射: nodeId -> containerId -> State */
9
+ this.states = new Map();
10
+ }
11
+ /**
12
+ * 处理原始 Docker 事件
13
+ */
14
+ processEvent(node, event) {
15
+ if (event.Type !== 'container')
16
+ return;
17
+ const action = event.Action;
18
+ // 只关注核心生命周期事件
19
+ if (!['start', 'die', 'stop', 'restart'].includes(action))
20
+ return;
21
+ const containerId = event.Actor.ID;
22
+ const containerName = event.Actor.Attributes?.name || 'unknown';
23
+ // 获取容器状态存储
24
+ const state = this.getContainerState(node.id, containerId);
25
+ const now = Date.now();
26
+ // ---------------------------------------------------------
27
+ // 0. 屏蔽 restart 冗余事件
28
+ // 如果收到 restart,且距离上次 start 只有不到 3秒,说明是连在一起的,忽略 restart
29
+ // ---------------------------------------------------------
30
+ if (action === 'restart') {
31
+ if (state.lastStartTime && (now - state.lastStartTime < 3000)) {
32
+ logger_1.monitorLogger.debug(`[${node.name}] ${containerName} 忽略冗余 restart (刚启动)`);
33
+ return;
34
+ }
35
+ }
36
+ // 1. 记录历史用于抖动检测
37
+ state.history.push(now);
38
+ this.cleanHistory(state, now);
39
+ // 2. 抖动检测 (Flapping)
40
+ const threshold = this.config.flappingThreshold || 3;
41
+ if (state.history.length > threshold) {
42
+ logger_1.monitorLogger.warn(`[${node.name}] ${containerName} 频繁重启 (Flapping)`);
43
+ // 如果正在防抖等待中,立即清除定时器
44
+ if (state.stopTimer) {
45
+ clearTimeout(state.stopTimer);
46
+ state.stopTimer = undefined;
47
+ }
48
+ // 清空历史,避免重复触发 Flapping 报警
49
+ state.history = [];
50
+ // 发出 Flapping 事件
51
+ this.emit({
52
+ eventType: 'container.flapping',
53
+ action: 'flapping',
54
+ nodeId: node.id,
55
+ nodeName: node.name,
56
+ containerId,
57
+ containerName,
58
+ timestamp: now
59
+ });
60
+ return;
61
+ }
62
+ // 3. 防抖逻辑
63
+ const debounceWait = this.config.debounceWait || 60000;
64
+ if (action === 'die' || action === 'stop') {
65
+ // [关键修复] 如果已经有一个定时器在跑了,说明已经处理了 stop/die,
66
+ // 不要再重复打印日志或重置定时器了。
67
+ if (state.stopTimer) {
68
+ logger_1.monitorLogger.debug(`[${node.name}] ${containerName} 收到 ${action},但已在等待停止确认中 (忽略)`);
69
+ return;
70
+ }
71
+ logger_1.monitorLogger.debug(`[${node.name}] ${containerName} 已停止 (${action}),等待 ${debounceWait}ms...`);
72
+ state.stopTimer = setTimeout(() => {
73
+ state.stopTimer = undefined;
74
+ // 只有定时器真正走完了,才发送通知
75
+ this.emit({
76
+ eventType: `container.die`, // 统一使用 die
77
+ action: 'die',
78
+ nodeId: node.id,
79
+ nodeName: node.name,
80
+ containerId,
81
+ containerName,
82
+ timestamp: Date.now()
83
+ });
84
+ }, debounceWait);
85
+ }
86
+ else if (action === 'start' || action === 'restart') {
87
+ // [关键] 记录启动时间,用于屏蔽后续的 restart
88
+ state.lastStartTime = now;
89
+ if (state.stopTimer) {
90
+ // 在防抖时间内恢复:取消报警
91
+ clearTimeout(state.stopTimer);
92
+ state.stopTimer = undefined;
93
+ logger_1.monitorLogger.info(`[${node.name}] ${containerName} 在防抖时间内恢复,通知已抑制`);
94
+ }
95
+ else {
96
+ // 这是一个"干净"的启动(比如手动启动一个已停止很久的容器)
97
+ this.emit({
98
+ eventType: `container.${action}`,
99
+ action: action,
100
+ nodeId: node.id,
101
+ nodeName: node.name,
102
+ containerId,
103
+ containerName,
104
+ timestamp: now
105
+ });
106
+ }
107
+ }
108
+ }
109
+ /**
110
+ * 注册处理后事件的回调
111
+ */
112
+ onProcessedEvent(callback) {
113
+ this.callback = callback;
114
+ return () => { this.callback = undefined; };
115
+ }
116
+ emit(event) {
117
+ if (this.callback) {
118
+ this.callback(event);
119
+ }
120
+ }
121
+ getContainerState(nodeId, containerId) {
122
+ if (!this.states.has(nodeId)) {
123
+ this.states.set(nodeId, new Map());
124
+ }
125
+ const nodeStates = this.states.get(nodeId);
126
+ if (!nodeStates.has(containerId)) {
127
+ nodeStates.set(containerId, {
128
+ history: []
129
+ });
130
+ }
131
+ return nodeStates.get(containerId);
132
+ }
133
+ cleanHistory(state, now) {
134
+ const window = this.config.flappingWindow || 300000; // 默认 5分钟
135
+ // 移除超出时间窗口的记录
136
+ state.history = state.history.filter(t => now - t <= window);
137
+ }
138
+ }
139
+ exports.MonitorManager = MonitorManager;
@@ -0,0 +1,119 @@
1
+ import type { NodeConfig, ContainerInfo, DockerEvent, NodeStatusType, CredentialConfig } from '../types';
2
+ export declare class DockerNode {
3
+ /** 节点配置 */
4
+ readonly config: NodeConfig;
5
+ /** 节点状态 */
6
+ status: NodeStatusType;
7
+ /** SSH 连接器 */
8
+ private connector;
9
+ /** 监控定时器 (容器状态轮询) */
10
+ private monitorTimer;
11
+ /** 事件监控定时器 (docker events) */
12
+ private eventTimer;
13
+ /** 上次事件查询时间 */
14
+ private lastEventTime;
15
+ /** 上次容器状态快照 */
16
+ private lastContainerStates;
17
+ /** 事件回调 */
18
+ private eventCallbacks;
19
+ /** Debug 模式 */
20
+ private debug;
21
+ /** 凭证配置 */
22
+ private credential;
23
+ /** 用于事件去重: 记录 "ID:Action:Time" -> Timestamp */
24
+ private eventDedupMap;
25
+ /** [新增] 实例唯一标识,用于判断是否存在多实例冲突 */
26
+ private instanceId;
27
+ constructor(config: NodeConfig, credential: CredentialConfig, debug?: boolean);
28
+ /**
29
+ * 连接到 Docker (带重试)
30
+ */
31
+ connect(): Promise<void>;
32
+ /**
33
+ * 断开连接
34
+ */
35
+ disconnect(): Promise<void>;
36
+ /**
37
+ * 重新连接
38
+ */
39
+ reconnect(): Promise<void>;
40
+ /**
41
+ * 列出容器
42
+ */
43
+ listContainers(all?: boolean): Promise<ContainerInfo[]>;
44
+ /**
45
+ * 启动容器
46
+ */
47
+ startContainer(containerId: string): Promise<void>;
48
+ /**
49
+ * 停止容器
50
+ */
51
+ stopContainer(containerId: string, timeout?: number): Promise<void>;
52
+ /**
53
+ * 重启容器
54
+ */
55
+ restartContainer(containerId: string, timeout?: number): Promise<void>;
56
+ /**
57
+ * 获取容器日志
58
+ */
59
+ getContainerLogs(containerId: string, tail?: number): Promise<string>;
60
+ /**
61
+ * 执行容器内命令
62
+ */
63
+ execContainer(containerId: string, cmd: string): Promise<string>;
64
+ /**
65
+ * 解析 docker ps 输出
66
+ */
67
+ private parseContainerList;
68
+ /**
69
+ * 映射容器状态
70
+ */
71
+ private mapState;
72
+ /**
73
+ * 启动监控 (容器状态轮询 + 事件流监听)
74
+ */
75
+ private startMonitoring;
76
+ /**
77
+ * 启动 Docker 事件流监听
78
+ */
79
+ private startEventStream;
80
+ /**
81
+ * 处理事件流中的一行数据
82
+ */
83
+ private handleEventLine;
84
+ /**
85
+ * 初始化容器状态快照
86
+ */
87
+ private initializeContainerStates;
88
+ /**
89
+ * 检测容器状态变更并发送通知
90
+ */
91
+ private checkContainerStateChanges;
92
+ /**
93
+ * 轮询 Docker 事件
94
+ */
95
+ private pollEvents;
96
+ /**
97
+ * 停止监控
98
+ */
99
+ private stopMonitoring;
100
+ /**
101
+ * 订阅事件
102
+ */
103
+ onEvent(callback: (event: DockerEvent) => void): () => void;
104
+ /**
105
+ * 触发事件
106
+ */
107
+ private emitEvent;
108
+ /**
109
+ * 清理定时器
110
+ */
111
+ private clearTimers;
112
+ /**
113
+ * 销毁节点
114
+ */
115
+ dispose(): Promise<void>;
116
+ get name(): string;
117
+ get id(): string;
118
+ get tags(): string[];
119
+ }