openclaw-nim 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/README.md +202 -0
- package/index.ts +64 -0
- package/package.json +58 -0
- package/src/accounts.ts +115 -0
- package/src/bot.ts +240 -0
- package/src/channel.ts +191 -0
- package/src/client.ts +425 -0
- package/src/config-schema.ts +50 -0
- package/src/media.ts +315 -0
- package/src/monitor.ts +196 -0
- package/src/outbound.ts +322 -0
- package/src/probe.ts +82 -0
- package/src/reply-dispatcher.ts +111 -0
- package/src/runtime.ts +38 -0
- package/src/send.ts +159 -0
- package/src/targets.ts +94 -0
- package/src/types.ts +203 -0
package/src/media.ts
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NIM Media - 媒体消息处理模块 (node-nim 版本)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
|
6
|
+
import type { NimConfig, NimSendResult, NimMediaInfo, NimMessageEvent, NimSessionType } from "./types.js";
|
|
7
|
+
import { createNimClient, getCachedNimClient } from "./client.js";
|
|
8
|
+
import { normalizeNimTarget } from "./targets.js";
|
|
9
|
+
import { getNimRuntime } from "./runtime.js";
|
|
10
|
+
import { extname, join } from "path";
|
|
11
|
+
import * as os from "os";
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as https from "https";
|
|
14
|
+
import * as http from "http";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 发送图片消息
|
|
18
|
+
*/
|
|
19
|
+
export async function sendImageNim(params: {
|
|
20
|
+
cfg: ClawdbotConfig;
|
|
21
|
+
to: string;
|
|
22
|
+
imagePath: string;
|
|
23
|
+
sessionType?: NimSessionType;
|
|
24
|
+
}): Promise<NimSendResult> {
|
|
25
|
+
const { cfg, to, imagePath, sessionType = "p2p" } = params;
|
|
26
|
+
const nimCfg = cfg.channels?.nim as NimConfig;
|
|
27
|
+
|
|
28
|
+
if (!nimCfg) {
|
|
29
|
+
return { success: false, error: "NIM channel not configured" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const targetId = normalizeNimTarget(to);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
let client = getCachedNimClient(nimCfg);
|
|
36
|
+
if (!client || !client.loggedIn) {
|
|
37
|
+
client = await createNimClient(nimCfg);
|
|
38
|
+
await client.login();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return await client.sendImage(targetId, imagePath, sessionType);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return {
|
|
44
|
+
success: false,
|
|
45
|
+
error: error instanceof Error ? error.message : String(error),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 发送文件消息
|
|
52
|
+
*/
|
|
53
|
+
export async function sendFileNim(params: {
|
|
54
|
+
cfg: ClawdbotConfig;
|
|
55
|
+
to: string;
|
|
56
|
+
filePath: string;
|
|
57
|
+
sessionType?: NimSessionType;
|
|
58
|
+
}): Promise<NimSendResult> {
|
|
59
|
+
const { cfg, to, filePath, sessionType = "p2p" } = params;
|
|
60
|
+
const nimCfg = cfg.channels?.nim as NimConfig;
|
|
61
|
+
|
|
62
|
+
if (!nimCfg) {
|
|
63
|
+
return { success: false, error: "NIM channel not configured" };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const targetId = normalizeNimTarget(to);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
let client = getCachedNimClient(nimCfg);
|
|
70
|
+
if (!client || !client.loggedIn) {
|
|
71
|
+
client = await createNimClient(nimCfg);
|
|
72
|
+
await client.login();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return await client.sendFile(targetId, filePath, sessionType);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
error: error instanceof Error ? error.message : String(error),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 发送音频消息
|
|
86
|
+
*/
|
|
87
|
+
export async function sendAudioNim(params: {
|
|
88
|
+
cfg: ClawdbotConfig;
|
|
89
|
+
to: string;
|
|
90
|
+
audioPath: string;
|
|
91
|
+
duration: number;
|
|
92
|
+
sessionType?: NimSessionType;
|
|
93
|
+
}): Promise<NimSendResult> {
|
|
94
|
+
const { cfg, to, audioPath, duration, sessionType = "p2p" } = params;
|
|
95
|
+
const nimCfg = cfg.channels?.nim as NimConfig;
|
|
96
|
+
|
|
97
|
+
if (!nimCfg) {
|
|
98
|
+
return { success: false, error: "NIM channel not configured" };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const targetId = normalizeNimTarget(to);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
let client = getCachedNimClient(nimCfg);
|
|
105
|
+
if (!client || !client.loggedIn) {
|
|
106
|
+
client = await createNimClient(nimCfg);
|
|
107
|
+
await client.login();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return await client.sendAudio(targetId, audioPath, duration, sessionType);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
error: error instanceof Error ? error.message : String(error),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 发送视频消息
|
|
121
|
+
*/
|
|
122
|
+
export async function sendVideoNim(params: {
|
|
123
|
+
cfg: ClawdbotConfig;
|
|
124
|
+
to: string;
|
|
125
|
+
videoPath: string;
|
|
126
|
+
duration: number;
|
|
127
|
+
width: number;
|
|
128
|
+
height: number;
|
|
129
|
+
sessionType?: NimSessionType;
|
|
130
|
+
}): Promise<NimSendResult> {
|
|
131
|
+
const { cfg, to, videoPath, duration, width, height, sessionType = "p2p" } = params;
|
|
132
|
+
const nimCfg = cfg.channels?.nim as NimConfig;
|
|
133
|
+
|
|
134
|
+
if (!nimCfg) {
|
|
135
|
+
return { success: false, error: "NIM channel not configured" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const targetId = normalizeNimTarget(to);
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
let client = getCachedNimClient(nimCfg);
|
|
142
|
+
if (!client || !client.loggedIn) {
|
|
143
|
+
client = await createNimClient(nimCfg);
|
|
144
|
+
await client.login();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return await client.sendVideo(targetId, videoPath, duration, width, height, sessionType);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
return {
|
|
150
|
+
success: false,
|
|
151
|
+
error: error instanceof Error ? error.message : String(error),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 下载媒体文件
|
|
158
|
+
*/
|
|
159
|
+
export async function downloadNimMedia(params: {
|
|
160
|
+
cfg: ClawdbotConfig;
|
|
161
|
+
url: string;
|
|
162
|
+
filename?: string;
|
|
163
|
+
maxBytes?: number;
|
|
164
|
+
log?: (msg: string) => void;
|
|
165
|
+
}): Promise<NimMediaInfo | null> {
|
|
166
|
+
const { cfg, url, filename, maxBytes = 30 * 1024 * 1024, log = console.log } = params;
|
|
167
|
+
|
|
168
|
+
if (!url) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const runtime = getNimRuntime();
|
|
174
|
+
const tempDir = (runtime as any)?.tempDir || os.tmpdir();
|
|
175
|
+
|
|
176
|
+
// 生成文件名
|
|
177
|
+
const ext = extname(url.split("?")[0]) || ".bin";
|
|
178
|
+
const name = filename || `nim_${Date.now()}${ext}`;
|
|
179
|
+
const localPath = join(tempDir, name);
|
|
180
|
+
|
|
181
|
+
// 下载文件
|
|
182
|
+
await downloadFile(url, localPath, maxBytes);
|
|
183
|
+
|
|
184
|
+
// 获取文件大小
|
|
185
|
+
const stats = fs.statSync(localPath);
|
|
186
|
+
|
|
187
|
+
// 推断媒体类型
|
|
188
|
+
const mediaType = inferMessageType(localPath);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
type: mediaType,
|
|
192
|
+
url,
|
|
193
|
+
name,
|
|
194
|
+
size: stats.size,
|
|
195
|
+
localPath,
|
|
196
|
+
};
|
|
197
|
+
} catch (error) {
|
|
198
|
+
log(`nim: failed to download media: ${error instanceof Error ? error.message : String(error)}`);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 从媒体信息列表构建 payload
|
|
205
|
+
*/
|
|
206
|
+
export function buildNimMediaPayload(mediaList: NimMediaInfo[]): Record<string, unknown> {
|
|
207
|
+
if (!mediaList || mediaList.length === 0) {
|
|
208
|
+
return {};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
MediaAttachments: mediaList.map((m) => ({
|
|
213
|
+
type: m.type,
|
|
214
|
+
url: m.url,
|
|
215
|
+
name: m.name,
|
|
216
|
+
size: m.size,
|
|
217
|
+
localPath: m.localPath,
|
|
218
|
+
})),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 推断消息的媒体类型占位符(用于 AI 显示)
|
|
224
|
+
*/
|
|
225
|
+
export function inferMediaPlaceholder(messageType: string): string {
|
|
226
|
+
switch (messageType) {
|
|
227
|
+
case "image":
|
|
228
|
+
return "[图片]";
|
|
229
|
+
case "audio":
|
|
230
|
+
return "[语音消息]";
|
|
231
|
+
case "video":
|
|
232
|
+
return "[视频]";
|
|
233
|
+
case "file":
|
|
234
|
+
return "[文件]";
|
|
235
|
+
case "geo":
|
|
236
|
+
case "location":
|
|
237
|
+
return "[位置]";
|
|
238
|
+
default:
|
|
239
|
+
return "[多媒体消息]";
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* 根据文件扩展名推断消息类型
|
|
245
|
+
*/
|
|
246
|
+
export function inferMessageType(filePath: string): "image" | "file" | "audio" | "video" {
|
|
247
|
+
const ext = extname(filePath).toLowerCase();
|
|
248
|
+
|
|
249
|
+
const imageExts = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"];
|
|
250
|
+
const audioExts = [".mp3", ".wav", ".aac", ".m4a", ".ogg", ".amr"];
|
|
251
|
+
const videoExts = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv"];
|
|
252
|
+
|
|
253
|
+
if (imageExts.includes(ext)) return "image";
|
|
254
|
+
if (audioExts.includes(ext)) return "audio";
|
|
255
|
+
if (videoExts.includes(ext)) return "video";
|
|
256
|
+
return "file";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 辅助函数:下载文件
|
|
261
|
+
*/
|
|
262
|
+
function downloadFile(url: string, destPath: string, maxBytes: number = 30 * 1024 * 1024): Promise<void> {
|
|
263
|
+
return new Promise((resolve, reject) => {
|
|
264
|
+
const protocol = url.startsWith("https") ? https : http;
|
|
265
|
+
const file = fs.createWriteStream(destPath);
|
|
266
|
+
let downloadedBytes = 0;
|
|
267
|
+
|
|
268
|
+
protocol.get(url, (response) => {
|
|
269
|
+
if (response.statusCode === 301 || response.statusCode === 302) {
|
|
270
|
+
// 处理重定向
|
|
271
|
+
const redirectUrl = response.headers.location;
|
|
272
|
+
if (redirectUrl) {
|
|
273
|
+
file.close();
|
|
274
|
+
try { fs.unlinkSync(destPath); } catch {}
|
|
275
|
+
downloadFile(redirectUrl, destPath, maxBytes).then(resolve).catch(reject);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (response.statusCode !== 200) {
|
|
281
|
+
file.close();
|
|
282
|
+
try { fs.unlinkSync(destPath); } catch {}
|
|
283
|
+
reject(new Error(`Download failed with status ${response.statusCode}`));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
response.on("data", (chunk) => {
|
|
288
|
+
downloadedBytes += chunk.length;
|
|
289
|
+
if (downloadedBytes > maxBytes) {
|
|
290
|
+
response.destroy();
|
|
291
|
+
file.close();
|
|
292
|
+
try { fs.unlinkSync(destPath); } catch {}
|
|
293
|
+
reject(new Error(`File too large (>${maxBytes} bytes)`));
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
response.pipe(file);
|
|
298
|
+
|
|
299
|
+
file.on("finish", () => {
|
|
300
|
+
file.close();
|
|
301
|
+
resolve();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
file.on("error", (err) => {
|
|
305
|
+
file.close();
|
|
306
|
+
try { fs.unlinkSync(destPath); } catch {}
|
|
307
|
+
reject(err);
|
|
308
|
+
});
|
|
309
|
+
}).on("error", (err) => {
|
|
310
|
+
file.close();
|
|
311
|
+
try { fs.unlinkSync(destPath); } catch {}
|
|
312
|
+
reject(err);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
}
|
package/src/monitor.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NIM Monitor - 消息监听模块 (node-nim 版本)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ClawdbotConfig, RuntimeEnv } from "clawdbot/plugin-sdk";
|
|
6
|
+
import type { NimConfig, NimClientInstance, NimMessageEvent } from "./types.js";
|
|
7
|
+
import { createNimClient, clearNimClientCache } from "./client.js";
|
|
8
|
+
import { resolveNimCredentials } from "./accounts.js";
|
|
9
|
+
import { handleNimMessage } from "./bot.js";
|
|
10
|
+
|
|
11
|
+
/** 监控状态 */
|
|
12
|
+
interface MonitorState {
|
|
13
|
+
client: NimClientInstance;
|
|
14
|
+
running: boolean;
|
|
15
|
+
abortController: AbortController;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 监控状态缓存 */
|
|
19
|
+
const monitorStates = new Map<string, MonitorState>();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 启动 NIM 消息监听
|
|
23
|
+
*/
|
|
24
|
+
export async function monitorNimProvider(params: {
|
|
25
|
+
cfg: ClawdbotConfig;
|
|
26
|
+
runtime: RuntimeEnv;
|
|
27
|
+
abortSignal?: AbortSignal;
|
|
28
|
+
}): Promise<void> {
|
|
29
|
+
const { cfg, runtime, abortSignal } = params;
|
|
30
|
+
const nimCfg = cfg.channels?.nim as NimConfig;
|
|
31
|
+
|
|
32
|
+
if (!nimCfg) {
|
|
33
|
+
console.error("[NIM] Channel not configured");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const creds = resolveNimCredentials(nimCfg);
|
|
38
|
+
if (!creds) {
|
|
39
|
+
console.error("[NIM] Credentials not configured");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const monitorKey = `${creds.appKey}:${creds.account}`;
|
|
43
|
+
|
|
44
|
+
// 检查是否已有监控在运行
|
|
45
|
+
if (monitorStates.has(monitorKey)) {
|
|
46
|
+
console.log("[NIM] Monitor already running for", creds.account);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log("[NIM] Starting monitor for account:", creds.account);
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// 创建客户端并登录
|
|
54
|
+
const client = await createNimClient(nimCfg);
|
|
55
|
+
const loginSuccess = await client.login();
|
|
56
|
+
|
|
57
|
+
if (!loginSuccess) {
|
|
58
|
+
console.error("[NIM] Login failed, cannot start monitor");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log("[NIM] Login successful, starting message listener");
|
|
63
|
+
|
|
64
|
+
// 创建 AbortController 用于停止监控
|
|
65
|
+
const abortController = new AbortController();
|
|
66
|
+
|
|
67
|
+
// 保存监控状态
|
|
68
|
+
const state: MonitorState = {
|
|
69
|
+
client,
|
|
70
|
+
running: true,
|
|
71
|
+
abortController,
|
|
72
|
+
};
|
|
73
|
+
monitorStates.set(monitorKey, state);
|
|
74
|
+
|
|
75
|
+
// 注册消息处理回调
|
|
76
|
+
const messageHandler = async (msg: NimMessageEvent) => {
|
|
77
|
+
if (!state.running) return;
|
|
78
|
+
|
|
79
|
+
// 忽略自己发送的消息
|
|
80
|
+
if (msg.from === creds.account) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log("[NIM] Received message from:", msg.from, "type:", msg.type);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
await handleNimMessage({
|
|
88
|
+
cfg,
|
|
89
|
+
runtime,
|
|
90
|
+
message: msg,
|
|
91
|
+
});
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error("[NIM] Error handling message:", error);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
client.onMessage(messageHandler);
|
|
98
|
+
|
|
99
|
+
// 注册连接状态回调
|
|
100
|
+
client.onConnectionChange((status) => {
|
|
101
|
+
console.log("[NIM] Connection status changed:", status);
|
|
102
|
+
|
|
103
|
+
if (status === "kickout") {
|
|
104
|
+
console.warn("[NIM] Account kicked out, stopping monitor");
|
|
105
|
+
stopNimMonitor(nimCfg);
|
|
106
|
+
} else if (status === "disconnected") {
|
|
107
|
+
console.warn("[NIM] Disconnected, will try to reconnect");
|
|
108
|
+
// SDK 会自动重连
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// 监听外部中止信号
|
|
113
|
+
if (abortSignal) {
|
|
114
|
+
abortSignal.addEventListener("abort", () => {
|
|
115
|
+
console.log("[NIM] Received abort signal, stopping monitor");
|
|
116
|
+
stopNimMonitor(nimCfg);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 监听内部中止信号
|
|
121
|
+
abortController.signal.addEventListener("abort", () => {
|
|
122
|
+
state.running = false;
|
|
123
|
+
client.offMessage(messageHandler);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
console.log("[NIM] Monitor started successfully");
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error("[NIM] Failed to start monitor:", error);
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 停止 NIM 消息监听
|
|
135
|
+
*/
|
|
136
|
+
export async function stopNimMonitor(cfg: NimConfig): Promise<void> {
|
|
137
|
+
const creds = resolveNimCredentials(cfg);
|
|
138
|
+
if (!creds) {
|
|
139
|
+
console.log("[NIM] No credentials to stop monitor");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const monitorKey = `${creds.appKey}:${creds.account}`;
|
|
143
|
+
|
|
144
|
+
const state = monitorStates.get(monitorKey);
|
|
145
|
+
if (!state) {
|
|
146
|
+
console.log("[NIM] No monitor running for", creds.account);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log("[NIM] Stopping monitor for account:", creds.account);
|
|
151
|
+
|
|
152
|
+
state.running = false;
|
|
153
|
+
state.abortController.abort();
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await state.client.logout();
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error("[NIM] Error during logout:", error);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
monitorStates.delete(monitorKey);
|
|
162
|
+
console.log("[NIM] Monitor stopped");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 检查监控是否在运行
|
|
167
|
+
*/
|
|
168
|
+
export function isNimMonitorRunning(cfg: NimConfig): boolean {
|
|
169
|
+
const creds = resolveNimCredentials(cfg);
|
|
170
|
+
if (!creds) return false;
|
|
171
|
+
const monitorKey = `${creds.appKey}:${creds.account}`;
|
|
172
|
+
const state = monitorStates.get(monitorKey);
|
|
173
|
+
return state?.running ?? false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 停止所有监控
|
|
178
|
+
*/
|
|
179
|
+
export async function stopAllNimMonitors(): Promise<void> {
|
|
180
|
+
console.log("[NIM] Stopping all monitors...");
|
|
181
|
+
|
|
182
|
+
for (const [key, state] of monitorStates.entries()) {
|
|
183
|
+
state.running = false;
|
|
184
|
+
state.abortController.abort();
|
|
185
|
+
try {
|
|
186
|
+
await state.client.logout();
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.error("[NIM] Error stopping monitor:", key, error);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
monitorStates.clear();
|
|
193
|
+
await clearNimClientCache();
|
|
194
|
+
|
|
195
|
+
console.log("[NIM] All monitors stopped");
|
|
196
|
+
}
|