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/outbound.ts
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
|
2
|
+
import type { NimConfig } from "./types.js";
|
|
3
|
+
import { sendMessageNim, splitMessageIntoChunks } from "./send.js";
|
|
4
|
+
import { sendImageNim, sendFileNim, inferMessageType } from "./media.js";
|
|
5
|
+
import { normalizeNimTarget } from "./targets.js";
|
|
6
|
+
|
|
7
|
+
/** Default text chunk limit for NIM messages */
|
|
8
|
+
const DEFAULT_TEXT_CHUNK_LIMIT = 5000;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Outbound send result type (matching Clawdbot SDK expectations)
|
|
12
|
+
*/
|
|
13
|
+
export type NimOutboundResult = {
|
|
14
|
+
channel: "nim";
|
|
15
|
+
ok: boolean;
|
|
16
|
+
msgId?: string;
|
|
17
|
+
clientMsgId?: string;
|
|
18
|
+
error?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Outbound message options (legacy, for backward compatibility)
|
|
23
|
+
*/
|
|
24
|
+
export type NimOutboundOptions = {
|
|
25
|
+
cfg: ClawdbotConfig;
|
|
26
|
+
to: string;
|
|
27
|
+
text?: string;
|
|
28
|
+
mediaPath?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Target resolution result
|
|
33
|
+
*/
|
|
34
|
+
type TargetResolveResult =
|
|
35
|
+
| { ok: true; to: string }
|
|
36
|
+
| { ok: false; error: string };
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve NIM target from various input formats.
|
|
40
|
+
* Implements the standard outbound.resolveTarget interface.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveNimOutboundTarget(params: {
|
|
43
|
+
to?: string;
|
|
44
|
+
allowFrom?: (string | number)[];
|
|
45
|
+
mode?: "explicit" | "implicit" | "heartbeat";
|
|
46
|
+
}): TargetResolveResult {
|
|
47
|
+
const { to, allowFrom, mode } = params;
|
|
48
|
+
const trimmed = to?.trim() ?? "";
|
|
49
|
+
|
|
50
|
+
// Normalize allowFrom list
|
|
51
|
+
const allowListRaw = (allowFrom ?? [])
|
|
52
|
+
.map((entry) => String(entry).trim())
|
|
53
|
+
.filter(Boolean);
|
|
54
|
+
const hasWildcard = allowListRaw.includes("*");
|
|
55
|
+
const allowList = allowListRaw
|
|
56
|
+
.filter((entry) => entry !== "*")
|
|
57
|
+
.map((entry) => normalizeNimTarget(entry))
|
|
58
|
+
.filter((entry): entry is string => Boolean(entry));
|
|
59
|
+
|
|
60
|
+
// If explicit target provided
|
|
61
|
+
if (trimmed) {
|
|
62
|
+
const normalizedTo = normalizeNimTarget(trimmed);
|
|
63
|
+
if (!normalizedTo) {
|
|
64
|
+
// Fallback to allowFrom if target is invalid
|
|
65
|
+
if ((mode === "implicit" || mode === "heartbeat") && allowList.length > 0) {
|
|
66
|
+
return { ok: true, to: allowList[0] };
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
error: `Invalid NIM target: ${trimmed}. Provide a valid NIM account ID or configure channels.nim.allowFrom.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// For implicit/heartbeat mode, verify target is in allowlist
|
|
75
|
+
if (mode === "implicit" || mode === "heartbeat") {
|
|
76
|
+
if (hasWildcard || allowList.length === 0) {
|
|
77
|
+
return { ok: true, to: normalizedTo };
|
|
78
|
+
}
|
|
79
|
+
if (allowList.includes(normalizedTo)) {
|
|
80
|
+
return { ok: true, to: normalizedTo };
|
|
81
|
+
}
|
|
82
|
+
// Fallback to first allowlist entry
|
|
83
|
+
return { ok: true, to: allowList[0] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { ok: true, to: normalizedTo };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// No explicit target - use allowFrom
|
|
90
|
+
if (allowList.length > 0) {
|
|
91
|
+
return { ok: true, to: allowList[0] };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
error: `Missing NIM target. Provide a target ID or configure channels.nim.allowFrom.`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Send text message through NIM channel.
|
|
102
|
+
* Implements the standard outbound.sendText interface.
|
|
103
|
+
*/
|
|
104
|
+
export async function sendNimOutboundText(params: {
|
|
105
|
+
to: string;
|
|
106
|
+
text: string;
|
|
107
|
+
cfg: ClawdbotConfig;
|
|
108
|
+
accountId?: string;
|
|
109
|
+
}): Promise<NimOutboundResult> {
|
|
110
|
+
const { to, text, cfg } = params;
|
|
111
|
+
|
|
112
|
+
console.log(`[NIM Outbound] sendText to=${to}, textLen=${text.length}`);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const result = await sendMessageNim({ cfg, to, text });
|
|
116
|
+
|
|
117
|
+
if (result.success) {
|
|
118
|
+
console.log(`[NIM Outbound] sendText success, msgId=${result.msgId}`);
|
|
119
|
+
return {
|
|
120
|
+
channel: "nim",
|
|
121
|
+
ok: true,
|
|
122
|
+
msgId: result.msgId,
|
|
123
|
+
clientMsgId: result.clientMsgId,
|
|
124
|
+
};
|
|
125
|
+
} else {
|
|
126
|
+
console.error(`[NIM Outbound] sendText failed: ${result.error}`);
|
|
127
|
+
return {
|
|
128
|
+
channel: "nim",
|
|
129
|
+
ok: false,
|
|
130
|
+
error: result.error,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
135
|
+
console.error(`[NIM Outbound] sendText exception: ${errorMsg}`);
|
|
136
|
+
return {
|
|
137
|
+
channel: "nim",
|
|
138
|
+
ok: false,
|
|
139
|
+
error: errorMsg,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Send media message through NIM channel.
|
|
146
|
+
* Implements the standard outbound.sendMedia interface.
|
|
147
|
+
*/
|
|
148
|
+
export async function sendNimOutboundMedia(params: {
|
|
149
|
+
to: string;
|
|
150
|
+
text?: string;
|
|
151
|
+
mediaUrl?: string;
|
|
152
|
+
mediaPath?: string;
|
|
153
|
+
cfg: ClawdbotConfig;
|
|
154
|
+
accountId?: string;
|
|
155
|
+
}): Promise<NimOutboundResult> {
|
|
156
|
+
const { to, text, mediaUrl, mediaPath, cfg } = params;
|
|
157
|
+
const media = mediaPath || mediaUrl;
|
|
158
|
+
|
|
159
|
+
console.log(`[NIM Outbound] sendMedia to=${to}, media=${media}, hasText=${Boolean(text)}`);
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// Send media if provided
|
|
163
|
+
if (media) {
|
|
164
|
+
const mediaType = inferMessageType(media);
|
|
165
|
+
let mediaResult;
|
|
166
|
+
|
|
167
|
+
if (mediaType === "image") {
|
|
168
|
+
mediaResult = await sendImageNim({ cfg, to, imagePath: media });
|
|
169
|
+
} else {
|
|
170
|
+
mediaResult = await sendFileNim({ cfg, to, filePath: media });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!mediaResult.success) {
|
|
174
|
+
console.error(`[NIM Outbound] sendMedia failed: ${mediaResult.error}`);
|
|
175
|
+
return {
|
|
176
|
+
channel: "nim",
|
|
177
|
+
ok: false,
|
|
178
|
+
error: mediaResult.error,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log(`[NIM Outbound] sendMedia success, msgId=${mediaResult.msgId}`);
|
|
183
|
+
|
|
184
|
+
// If no text, return media result
|
|
185
|
+
if (!text) {
|
|
186
|
+
return {
|
|
187
|
+
channel: "nim",
|
|
188
|
+
ok: true,
|
|
189
|
+
msgId: mediaResult.msgId,
|
|
190
|
+
clientMsgId: mediaResult.clientMsgId,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Send text if provided
|
|
196
|
+
if (text) {
|
|
197
|
+
return await sendNimOutboundText({ to, text, cfg });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Nothing to send
|
|
201
|
+
return {
|
|
202
|
+
channel: "nim",
|
|
203
|
+
ok: true,
|
|
204
|
+
};
|
|
205
|
+
} catch (error) {
|
|
206
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
207
|
+
console.error(`[NIM Outbound] sendMedia exception: ${errorMsg}`);
|
|
208
|
+
return {
|
|
209
|
+
channel: "nim",
|
|
210
|
+
ok: false,
|
|
211
|
+
error: errorMsg,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* NIM outbound configuration object.
|
|
218
|
+
* Conforms to Clawdbot ChannelPlugin outbound interface.
|
|
219
|
+
*/
|
|
220
|
+
export const nimOutboundConfig = {
|
|
221
|
+
/**
|
|
222
|
+
* Delivery mode - "gateway" means messages go through the gateway process
|
|
223
|
+
*/
|
|
224
|
+
deliveryMode: "gateway" as const,
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Text chunker function for splitting long messages
|
|
228
|
+
*/
|
|
229
|
+
chunker: splitMessageIntoChunks,
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Maximum characters per text chunk
|
|
233
|
+
*/
|
|
234
|
+
textChunkLimit: DEFAULT_TEXT_CHUNK_LIMIT,
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Resolve target address from various input formats
|
|
238
|
+
*/
|
|
239
|
+
resolveTarget: resolveNimOutboundTarget,
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Send a text message
|
|
243
|
+
*/
|
|
244
|
+
sendText: async (params: {
|
|
245
|
+
to: string;
|
|
246
|
+
text: string;
|
|
247
|
+
cfg: ClawdbotConfig;
|
|
248
|
+
accountId?: string;
|
|
249
|
+
deps?: unknown;
|
|
250
|
+
}): Promise<NimOutboundResult> => {
|
|
251
|
+
return sendNimOutboundText(params);
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Send a media message (with optional text caption)
|
|
256
|
+
*/
|
|
257
|
+
sendMedia: async (params: {
|
|
258
|
+
to: string;
|
|
259
|
+
text?: string;
|
|
260
|
+
mediaUrl?: string;
|
|
261
|
+
cfg: ClawdbotConfig;
|
|
262
|
+
accountId?: string;
|
|
263
|
+
deps?: unknown;
|
|
264
|
+
}): Promise<NimOutboundResult> => {
|
|
265
|
+
return sendNimOutboundMedia({ ...params, mediaPath: params.mediaUrl });
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// ============================================================================
|
|
270
|
+
// Legacy functions for backward compatibility
|
|
271
|
+
// ============================================================================
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Handle outbound messages for the NIM channel.
|
|
275
|
+
* @deprecated Use nimOutboundConfig.sendText/sendMedia instead
|
|
276
|
+
*/
|
|
277
|
+
export async function nimOutbound(params: NimOutboundOptions): Promise<void> {
|
|
278
|
+
const { cfg, to, text, mediaPath } = params;
|
|
279
|
+
const nimCfg = cfg.channels?.nim as NimConfig | undefined;
|
|
280
|
+
|
|
281
|
+
const targetId = normalizeNimTarget(to);
|
|
282
|
+
if (!targetId) {
|
|
283
|
+
throw new Error(`Invalid NIM target: ${to}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Send media if provided
|
|
287
|
+
if (mediaPath) {
|
|
288
|
+
const result = await sendNimOutboundMedia({
|
|
289
|
+
cfg,
|
|
290
|
+
to: targetId,
|
|
291
|
+
mediaPath,
|
|
292
|
+
text,
|
|
293
|
+
});
|
|
294
|
+
if (!result.ok) {
|
|
295
|
+
throw new Error(result.error || "Failed to send media");
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Send text if provided
|
|
301
|
+
if (text) {
|
|
302
|
+
const chunkLimit = nimCfg?.textChunkLimit ?? DEFAULT_TEXT_CHUNK_LIMIT;
|
|
303
|
+
const chunks = splitMessageIntoChunks(text, chunkLimit);
|
|
304
|
+
|
|
305
|
+
for (const chunk of chunks) {
|
|
306
|
+
const result = await sendNimOutboundText({ cfg, to: targetId, text: chunk });
|
|
307
|
+
if (!result.ok) {
|
|
308
|
+
throw new Error(result.error || "Failed to send text");
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Create an outbound handler function for the NIM channel.
|
|
316
|
+
* @deprecated Use nimOutboundConfig instead
|
|
317
|
+
*/
|
|
318
|
+
export function createNimOutboundHandler(cfg: ClawdbotConfig) {
|
|
319
|
+
return async (params: { to: string; text?: string; mediaPath?: string }) => {
|
|
320
|
+
await nimOutbound({ cfg, ...params });
|
|
321
|
+
};
|
|
322
|
+
}
|
package/src/probe.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NIM Probe - 连接探测模块 (node-nim 版本)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { NimConfig, NimProbeResult } from "./types.js";
|
|
6
|
+
import { resolveNimCredentials } from "./accounts.js";
|
|
7
|
+
import { createNimClient, getCachedNimClient } from "./client.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 探测 NIM 连接状态(使用缓存的客户端)
|
|
11
|
+
*/
|
|
12
|
+
export async function probeNim(cfg: NimConfig): Promise<NimProbeResult> {
|
|
13
|
+
try {
|
|
14
|
+
const creds = resolveNimCredentials(cfg);
|
|
15
|
+
const client = getCachedNimClient(cfg);
|
|
16
|
+
|
|
17
|
+
if (client && client.loggedIn) {
|
|
18
|
+
return {
|
|
19
|
+
connected: true,
|
|
20
|
+
account: creds.account,
|
|
21
|
+
loginState: "connected",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
connected: false,
|
|
27
|
+
account: creds.account,
|
|
28
|
+
loginState: "not_connected",
|
|
29
|
+
};
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return {
|
|
32
|
+
connected: false,
|
|
33
|
+
error: error instanceof Error ? error.message : String(error),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 探测 NIM 连接状态(尝试建立连接)
|
|
40
|
+
*/
|
|
41
|
+
export async function probeNimWithConnect(cfg: NimConfig): Promise<NimProbeResult> {
|
|
42
|
+
try {
|
|
43
|
+
const creds = resolveNimCredentials(cfg);
|
|
44
|
+
|
|
45
|
+
// 尝试创建客户端并登录
|
|
46
|
+
const client = await createNimClient(cfg);
|
|
47
|
+
const loginSuccess = await client.login();
|
|
48
|
+
|
|
49
|
+
if (loginSuccess) {
|
|
50
|
+
return {
|
|
51
|
+
connected: true,
|
|
52
|
+
account: creds.account,
|
|
53
|
+
loginState: "connected",
|
|
54
|
+
};
|
|
55
|
+
} else {
|
|
56
|
+
return {
|
|
57
|
+
connected: false,
|
|
58
|
+
account: creds.account,
|
|
59
|
+
error: "Login failed",
|
|
60
|
+
loginState: "login_failed",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return {
|
|
65
|
+
connected: false,
|
|
66
|
+
error: error instanceof Error ? error.message : String(error),
|
|
67
|
+
loginState: "error",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 快速检查配置是否完整
|
|
74
|
+
*/
|
|
75
|
+
export function isNimConfigComplete(cfg: NimConfig): boolean {
|
|
76
|
+
try {
|
|
77
|
+
const creds = resolveNimCredentials(cfg);
|
|
78
|
+
return !!(creds.appKey && creds.account && creds.token);
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { ClawdbotConfig, RuntimeEnv } from "clawdbot/plugin-sdk";
|
|
2
|
+
import type { NimConfig } from "./types.js";
|
|
3
|
+
import { sendMessageNim, splitMessageIntoChunks } from "./send.js";
|
|
4
|
+
import { sendImageNim, sendFileNim, inferMessageType } from "./media.js";
|
|
5
|
+
import { getNimRuntime } from "./runtime.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Reply payload type from Clawdbot SDK
|
|
9
|
+
*/
|
|
10
|
+
type ReplyPayload = {
|
|
11
|
+
text?: string;
|
|
12
|
+
mediaUrl?: string;
|
|
13
|
+
mediaUrls?: string[];
|
|
14
|
+
channelData?: Record<string, unknown>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a reply dispatcher for NIM messages.
|
|
19
|
+
* Uses the Clawdbot SDK's createReplyDispatcherWithTyping for proper integration.
|
|
20
|
+
*/
|
|
21
|
+
export function createNimReplyDispatcher(params: {
|
|
22
|
+
cfg: ClawdbotConfig;
|
|
23
|
+
agentId: string;
|
|
24
|
+
runtime: RuntimeEnv;
|
|
25
|
+
senderId: string;
|
|
26
|
+
}) {
|
|
27
|
+
const { cfg, runtime, senderId } = params;
|
|
28
|
+
const nimCfg = cfg.channels?.nim as NimConfig | undefined;
|
|
29
|
+
const log = runtime?.log ?? console.log;
|
|
30
|
+
const chunkLimit = nimCfg?.textChunkLimit ?? 4000;
|
|
31
|
+
|
|
32
|
+
// Get the core runtime which has the full channel.reply interface
|
|
33
|
+
const core = getNimRuntime();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Deliver function that sends a reply message to NIM.
|
|
37
|
+
* Called by the SDK dispatcher for each block/tool/final reply.
|
|
38
|
+
* @param payload - The reply payload containing text and/or media
|
|
39
|
+
*/
|
|
40
|
+
const deliver = async (payload: ReplyPayload): Promise<void> => {
|
|
41
|
+
const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
|
42
|
+
const text = payload.text ?? "";
|
|
43
|
+
|
|
44
|
+
log(`nim: deliver called with text=${text.length} chars, media=${mediaList.length} items`);
|
|
45
|
+
|
|
46
|
+
// If no content, skip
|
|
47
|
+
if (!text && mediaList.length === 0) {
|
|
48
|
+
log(`nim: skipping empty payload`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// Send media first if present
|
|
54
|
+
if (mediaList.length > 0) {
|
|
55
|
+
for (const mediaUrl of mediaList) {
|
|
56
|
+
const mediaType = inferMessageType(mediaUrl);
|
|
57
|
+
log(`nim: sending media to ${senderId}, type=${mediaType}, url=${mediaUrl}`);
|
|
58
|
+
|
|
59
|
+
if (mediaType === "image") {
|
|
60
|
+
await sendImageNim({ cfg, to: senderId, imagePath: mediaUrl });
|
|
61
|
+
} else {
|
|
62
|
+
await sendFileNim({ cfg, to: senderId, filePath: mediaUrl });
|
|
63
|
+
}
|
|
64
|
+
log(`nim: sent media to ${senderId}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Send text if present
|
|
69
|
+
if (text) {
|
|
70
|
+
const chunks = splitMessageIntoChunks(text, chunkLimit);
|
|
71
|
+
for (const chunk of chunks) {
|
|
72
|
+
await sendMessageNim({
|
|
73
|
+
cfg,
|
|
74
|
+
to: senderId,
|
|
75
|
+
text: chunk,
|
|
76
|
+
});
|
|
77
|
+
log(`nim: sent reply chunk (${chunk.length} chars) to ${senderId}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
log(`nim: failed to send reply: ${String(err)}`);
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Use the SDK's createReplyDispatcherWithTyping for proper dispatcher structure
|
|
87
|
+
const { dispatcher, replyOptions: sdkReplyOptions, markDispatchIdle } =
|
|
88
|
+
core.channel.reply.createReplyDispatcherWithTyping({
|
|
89
|
+
deliver,
|
|
90
|
+
humanDelay: { mode: "off" },
|
|
91
|
+
onIdle: () => {
|
|
92
|
+
log(`nim: reply dispatcher is idle`);
|
|
93
|
+
},
|
|
94
|
+
onError: (err: Error) => {
|
|
95
|
+
log(`nim: reply dispatcher error: ${String(err)}`);
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const replyOptions = {
|
|
100
|
+
channel: "nim" as const,
|
|
101
|
+
targetId: senderId,
|
|
102
|
+
...sdkReplyOptions,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
dispatcher,
|
|
107
|
+
replyOptions,
|
|
108
|
+
markDispatchIdle,
|
|
109
|
+
isIdle: () => false, // The SDK dispatcher handles idle state internally
|
|
110
|
+
};
|
|
111
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { RuntimeEnv } from "clawdbot/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Global runtime environment reference.
|
|
5
|
+
*/
|
|
6
|
+
let nimRuntime: RuntimeEnv | null = null;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Set the NIM runtime environment.
|
|
10
|
+
*/
|
|
11
|
+
export function setNimRuntime(runtime: RuntimeEnv): void {
|
|
12
|
+
nimRuntime = runtime;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get the NIM runtime environment.
|
|
17
|
+
* Throws if runtime is not set.
|
|
18
|
+
*/
|
|
19
|
+
export function getNimRuntime(): RuntimeEnv {
|
|
20
|
+
if (!nimRuntime) {
|
|
21
|
+
throw new Error("NIM runtime not initialized. Call setNimRuntime first.");
|
|
22
|
+
}
|
|
23
|
+
return nimRuntime;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if NIM runtime is initialized.
|
|
28
|
+
*/
|
|
29
|
+
export function isNimRuntimeInitialized(): boolean {
|
|
30
|
+
return nimRuntime !== null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Clear the NIM runtime reference.
|
|
35
|
+
*/
|
|
36
|
+
export function clearNimRuntime(): void {
|
|
37
|
+
nimRuntime = null;
|
|
38
|
+
}
|
package/src/send.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NIM Send - 消息发送模块 (node-nim 版本)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
|
6
|
+
import type { NimConfig, NimSendResult, NimSessionType } from "./types.js";
|
|
7
|
+
import { createNimClient, getCachedNimClient } from "./client.js";
|
|
8
|
+
import { normalizeNimTarget } from "./targets.js";
|
|
9
|
+
|
|
10
|
+
/** 单条消息最大字符数 */
|
|
11
|
+
const MAX_MESSAGE_LENGTH = 5000;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 发送文本消息
|
|
15
|
+
*/
|
|
16
|
+
export async function sendMessageNim(params: {
|
|
17
|
+
cfg: ClawdbotConfig;
|
|
18
|
+
to: string;
|
|
19
|
+
text: string;
|
|
20
|
+
sessionType?: NimSessionType;
|
|
21
|
+
}): Promise<NimSendResult> {
|
|
22
|
+
const { cfg, to, text, sessionType = "p2p" } = params;
|
|
23
|
+
const nimCfg = cfg.channels?.nim as NimConfig;
|
|
24
|
+
|
|
25
|
+
if (!nimCfg) {
|
|
26
|
+
return { success: false, error: "NIM channel not configured" };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const targetId = normalizeNimTarget(to);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
let client = getCachedNimClient(nimCfg);
|
|
33
|
+
if (!client || !client.loggedIn) {
|
|
34
|
+
client = await createNimClient(nimCfg);
|
|
35
|
+
await client.login();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return await client.sendText(targetId, text, sessionType);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return {
|
|
41
|
+
success: false,
|
|
42
|
+
error: error instanceof Error ? error.message : String(error),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 发送长消息(自动分割)
|
|
49
|
+
*/
|
|
50
|
+
export async function sendLongMessageNim(params: {
|
|
51
|
+
cfg: ClawdbotConfig;
|
|
52
|
+
to: string;
|
|
53
|
+
text: string;
|
|
54
|
+
sessionType?: NimSessionType;
|
|
55
|
+
}): Promise<NimSendResult[]> {
|
|
56
|
+
const { cfg, to, text, sessionType = "p2p" } = params;
|
|
57
|
+
const chunks = splitMessageIntoChunks(text, MAX_MESSAGE_LENGTH);
|
|
58
|
+
const results: NimSendResult[] = [];
|
|
59
|
+
|
|
60
|
+
for (const chunk of chunks) {
|
|
61
|
+
const result = await sendMessageNim({
|
|
62
|
+
cfg,
|
|
63
|
+
to,
|
|
64
|
+
text: chunk,
|
|
65
|
+
sessionType,
|
|
66
|
+
});
|
|
67
|
+
results.push(result);
|
|
68
|
+
|
|
69
|
+
// 如果发送失败,停止发送后续消息
|
|
70
|
+
if (!result.success) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 避免发送过快
|
|
75
|
+
if (chunks.length > 1) {
|
|
76
|
+
await sleep(100);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return results;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 编辑消息(NIM 不支持真正的编辑,这里通过撤回+重发模拟)
|
|
85
|
+
* 注意:这个功能可能需要根据实际 SDK 能力调整
|
|
86
|
+
*/
|
|
87
|
+
export async function editMessageNim(params: {
|
|
88
|
+
cfg: ClawdbotConfig;
|
|
89
|
+
msgId: string;
|
|
90
|
+
to: string;
|
|
91
|
+
newText: string;
|
|
92
|
+
sessionType?: NimSessionType;
|
|
93
|
+
}): Promise<NimSendResult> {
|
|
94
|
+
// NIM 不支持编辑消息,直接发送新消息
|
|
95
|
+
const { cfg, to, newText, sessionType = "p2p" } = params;
|
|
96
|
+
return sendMessageNim({ cfg, to, text: newText, sessionType });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 获取消息(根据消息ID)
|
|
101
|
+
* 注意:需要根据实际 node-nim SDK 能力实现
|
|
102
|
+
*/
|
|
103
|
+
export async function getMessageNim(params: {
|
|
104
|
+
cfg: ClawdbotConfig;
|
|
105
|
+
msgId: string;
|
|
106
|
+
}): Promise<{ success: boolean; message?: unknown; error?: string }> {
|
|
107
|
+
// 暂时返回不支持
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
error: "Get message by ID is not implemented yet",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 将长文本分割成多条消息
|
|
116
|
+
*/
|
|
117
|
+
export function splitMessageIntoChunks(
|
|
118
|
+
text: string,
|
|
119
|
+
maxLength: number = MAX_MESSAGE_LENGTH
|
|
120
|
+
): string[] {
|
|
121
|
+
if (text.length <= maxLength) {
|
|
122
|
+
return [text];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const chunks: string[] = [];
|
|
126
|
+
let remaining = text;
|
|
127
|
+
|
|
128
|
+
while (remaining.length > 0) {
|
|
129
|
+
if (remaining.length <= maxLength) {
|
|
130
|
+
chunks.push(remaining);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 尝试在换行符处分割
|
|
135
|
+
let splitIndex = remaining.lastIndexOf("\n", maxLength);
|
|
136
|
+
|
|
137
|
+
// 如果没有换行符,尝试在空格处分割
|
|
138
|
+
if (splitIndex === -1 || splitIndex < maxLength * 0.5) {
|
|
139
|
+
splitIndex = remaining.lastIndexOf(" ", maxLength);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 如果还是找不到合适的分割点,强制在 maxLength 处分割
|
|
143
|
+
if (splitIndex === -1 || splitIndex < maxLength * 0.5) {
|
|
144
|
+
splitIndex = maxLength;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
chunks.push(remaining.slice(0, splitIndex));
|
|
148
|
+
remaining = remaining.slice(splitIndex).trimStart();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return chunks;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 辅助函数:延时
|
|
156
|
+
*/
|
|
157
|
+
function sleep(ms: number): Promise<void> {
|
|
158
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
159
|
+
}
|