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/channel.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { ChannelPlugin, ClawdbotConfig } from "clawdbot/plugin-sdk";
|
|
2
|
+
import type { ResolvedNimAccount, NimConfig } from "./types.js";
|
|
3
|
+
import { resolveNimAccount, resolveNimCredentials, DEFAULT_NIM_ACCOUNT_ID } from "./accounts.js";
|
|
4
|
+
import { normalizeNimTarget, looksLikeNimId } from "./targets.js";
|
|
5
|
+
import { sendMessageNim } from "./send.js";
|
|
6
|
+
import { probeNim } from "./probe.js";
|
|
7
|
+
import { nimOutboundConfig } from "./outbound.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Channel plugin metadata.
|
|
11
|
+
*/
|
|
12
|
+
const meta = {
|
|
13
|
+
id: "nim",
|
|
14
|
+
label: "NIM",
|
|
15
|
+
selectionLabel: "NetEase IM (网易云信)",
|
|
16
|
+
docsPath: "/channels/nim",
|
|
17
|
+
docsLabel: "nim",
|
|
18
|
+
blurb: "网易云信 IM 即时通讯。",
|
|
19
|
+
aliases: ["netease", "yunxin"],
|
|
20
|
+
order: 80,
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* NIM channel plugin implementation.
|
|
25
|
+
*/
|
|
26
|
+
export const nimPlugin: ChannelPlugin<ResolvedNimAccount> = {
|
|
27
|
+
id: "nim",
|
|
28
|
+
meta: {
|
|
29
|
+
...meta,
|
|
30
|
+
},
|
|
31
|
+
pairing: {
|
|
32
|
+
idLabel: "nimAccountId",
|
|
33
|
+
normalizeAllowEntry: (entry) => entry.replace(/^(nim|user|account):/i, ""),
|
|
34
|
+
notifyApproval: async ({ cfg, id }) => {
|
|
35
|
+
await sendMessageNim({
|
|
36
|
+
cfg,
|
|
37
|
+
to: id,
|
|
38
|
+
text: "Your account has been approved to chat with this bot.",
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
capabilities: {
|
|
43
|
+
chatTypes: ["direct"],
|
|
44
|
+
polls: false,
|
|
45
|
+
threads: false,
|
|
46
|
+
media: true,
|
|
47
|
+
reactions: false,
|
|
48
|
+
edit: false,
|
|
49
|
+
reply: false,
|
|
50
|
+
},
|
|
51
|
+
agentPrompt: {
|
|
52
|
+
messageToolHints: () => [
|
|
53
|
+
"- NIM targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `user:<accountId>` or `nim:<accountId>`.",
|
|
54
|
+
"- NIM supports text, image, file, audio, and video messages.",
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
reload: { configPrefixes: ["channels.nim"] },
|
|
58
|
+
configSchema: {
|
|
59
|
+
schema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
additionalProperties: false,
|
|
62
|
+
properties: {
|
|
63
|
+
enabled: { type: "boolean" },
|
|
64
|
+
appKey: { type: "string" },
|
|
65
|
+
account: { type: "string" },
|
|
66
|
+
token: { type: "string" },
|
|
67
|
+
dmPolicy: { type: "string", enum: ["open", "allowlist"] },
|
|
68
|
+
allowFrom: { type: "array", items: { oneOf: [{ type: "string" }, { type: "number" }] } },
|
|
69
|
+
mediaMaxMb: { type: "number", minimum: 0 },
|
|
70
|
+
textChunkLimit: { type: "integer", minimum: 1 },
|
|
71
|
+
lbsUrl: { type: "string" },
|
|
72
|
+
linkUrl: { type: "string" },
|
|
73
|
+
debug: { type: "boolean" },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
config: {
|
|
78
|
+
listAccountIds: () => [DEFAULT_NIM_ACCOUNT_ID],
|
|
79
|
+
resolveAccount: (cfg) => resolveNimAccount({ cfg }),
|
|
80
|
+
defaultAccountId: () => DEFAULT_NIM_ACCOUNT_ID,
|
|
81
|
+
setAccountEnabled: ({ cfg, enabled }) => ({
|
|
82
|
+
...cfg,
|
|
83
|
+
channels: {
|
|
84
|
+
...cfg.channels,
|
|
85
|
+
nim: {
|
|
86
|
+
...cfg.channels?.nim,
|
|
87
|
+
enabled,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
deleteAccount: ({ cfg }) => {
|
|
92
|
+
const next = { ...cfg } as ClawdbotConfig;
|
|
93
|
+
const nextChannels = { ...cfg.channels };
|
|
94
|
+
delete (nextChannels as Record<string, unknown>).nim;
|
|
95
|
+
if (Object.keys(nextChannels).length > 0) {
|
|
96
|
+
next.channels = nextChannels;
|
|
97
|
+
} else {
|
|
98
|
+
delete next.channels;
|
|
99
|
+
}
|
|
100
|
+
return next;
|
|
101
|
+
},
|
|
102
|
+
isConfigured: (_account, cfg) =>
|
|
103
|
+
Boolean(resolveNimCredentials(cfg.channels?.nim as NimConfig | undefined)),
|
|
104
|
+
describeAccount: (account) => ({
|
|
105
|
+
accountId: account.accountId,
|
|
106
|
+
enabled: account.enabled,
|
|
107
|
+
configured: account.configured,
|
|
108
|
+
}),
|
|
109
|
+
resolveAllowFrom: ({ cfg }) =>
|
|
110
|
+
(cfg.channels?.nim as NimConfig | undefined)?.allowFrom ?? [],
|
|
111
|
+
formatAllowFrom: ({ allowFrom }) =>
|
|
112
|
+
allowFrom
|
|
113
|
+
.map((entry) => String(entry).trim())
|
|
114
|
+
.filter(Boolean)
|
|
115
|
+
.map((entry) => entry.toLowerCase()),
|
|
116
|
+
},
|
|
117
|
+
security: {
|
|
118
|
+
collectWarnings: ({ cfg }) => {
|
|
119
|
+
const nimCfg = cfg.channels?.nim as NimConfig | undefined;
|
|
120
|
+
const dmPolicy = nimCfg?.dmPolicy ?? "open";
|
|
121
|
+
if (dmPolicy !== "open") return [];
|
|
122
|
+
return [
|
|
123
|
+
`- NIM DMs: dmPolicy="open" allows any user to message. Set channels.nim.dmPolicy="allowlist" + channels.nim.allowFrom to restrict senders.`,
|
|
124
|
+
];
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
setup: {
|
|
128
|
+
resolveAccountId: () => DEFAULT_NIM_ACCOUNT_ID,
|
|
129
|
+
applyAccountConfig: ({ cfg }) => ({
|
|
130
|
+
...cfg,
|
|
131
|
+
channels: {
|
|
132
|
+
...cfg.channels,
|
|
133
|
+
nim: {
|
|
134
|
+
...cfg.channels?.nim,
|
|
135
|
+
enabled: true,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
}),
|
|
139
|
+
},
|
|
140
|
+
messaging: {
|
|
141
|
+
normalizeTarget: normalizeNimTarget,
|
|
142
|
+
targetResolver: {
|
|
143
|
+
looksLikeId: looksLikeNimId,
|
|
144
|
+
hint: "<accountId|user:accountId|nim:accountId>",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
outbound: nimOutboundConfig,
|
|
148
|
+
status: {
|
|
149
|
+
defaultRuntime: {
|
|
150
|
+
accountId: DEFAULT_NIM_ACCOUNT_ID,
|
|
151
|
+
running: false,
|
|
152
|
+
lastStartAt: null,
|
|
153
|
+
lastStopAt: null,
|
|
154
|
+
lastError: null,
|
|
155
|
+
},
|
|
156
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
157
|
+
configured: snapshot.configured ?? false,
|
|
158
|
+
running: snapshot.running ?? false,
|
|
159
|
+
lastStartAt: snapshot.lastStartAt ?? null,
|
|
160
|
+
lastStopAt: snapshot.lastStopAt ?? null,
|
|
161
|
+
lastError: snapshot.lastError ?? null,
|
|
162
|
+
probe: snapshot.probe,
|
|
163
|
+
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
164
|
+
}),
|
|
165
|
+
probeAccount: async ({ cfg }) =>
|
|
166
|
+
await probeNim(cfg.channels?.nim as NimConfig | undefined),
|
|
167
|
+
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
|
168
|
+
accountId: account.accountId,
|
|
169
|
+
enabled: account.enabled,
|
|
170
|
+
configured: account.configured,
|
|
171
|
+
running: runtime?.running ?? false,
|
|
172
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
173
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
174
|
+
lastError: runtime?.lastError ?? null,
|
|
175
|
+
probe,
|
|
176
|
+
}),
|
|
177
|
+
},
|
|
178
|
+
gateway: {
|
|
179
|
+
startAccount: async (ctx) => {
|
|
180
|
+
const { monitorNimProvider } = await import("./monitor.js");
|
|
181
|
+
const nimCfg = ctx.cfg.channels?.nim as NimConfig | undefined;
|
|
182
|
+
ctx.setStatus({ accountId: ctx.accountId });
|
|
183
|
+
ctx.log?.info(`starting NIM provider for account ${nimCfg?.account ?? "unknown"}`);
|
|
184
|
+
return monitorNimProvider({
|
|
185
|
+
cfg: ctx.cfg,
|
|
186
|
+
runtime: ctx.runtime,
|
|
187
|
+
abortSignal: ctx.abortSignal,
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
};
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NIM Client - node-nim SDK V2 API 封装
|
|
3
|
+
*
|
|
4
|
+
* 使用网易云信官方 Node.js SDK (node-nim) V2 版本
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
NimConfig,
|
|
9
|
+
NimClientInstance,
|
|
10
|
+
NimMessageEvent,
|
|
11
|
+
NimSendResult,
|
|
12
|
+
NimSessionType,
|
|
13
|
+
NimMessageType,
|
|
14
|
+
NimAttachment,
|
|
15
|
+
} from "./types.js";
|
|
16
|
+
import { resolveNimCredentials } from "./accounts.js";
|
|
17
|
+
import * as path from "path";
|
|
18
|
+
import * as os from "os";
|
|
19
|
+
import * as fs from "fs";
|
|
20
|
+
|
|
21
|
+
// 客户端缓存
|
|
22
|
+
const clientCache = new Map<string, NimClientInstance>();
|
|
23
|
+
|
|
24
|
+
// 消息回调管理
|
|
25
|
+
const messageCallbacks = new Map<string, Set<(msg: NimMessageEvent) => void>>();
|
|
26
|
+
const connectionCallbacks = new Map<string, Set<(state: string) => void>>();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 获取 SDK 数据目录
|
|
30
|
+
*/
|
|
31
|
+
function getSdkDataPath(account: string): string {
|
|
32
|
+
const dataDir = path.join(os.homedir(), ".openclaw-nim", account);
|
|
33
|
+
if (!fs.existsSync(dataDir)) {
|
|
34
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
return dataDir;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 将 V2 消息类型转换为我们的类型
|
|
41
|
+
*/
|
|
42
|
+
function convertMessageType(v2Type: number): NimMessageType {
|
|
43
|
+
// V2NIMMessageType 枚举
|
|
44
|
+
const typeMap: Record<number, NimMessageType> = {
|
|
45
|
+
0: "text",
|
|
46
|
+
1: "image",
|
|
47
|
+
2: "audio",
|
|
48
|
+
3: "video",
|
|
49
|
+
4: "geo",
|
|
50
|
+
5: "notification",
|
|
51
|
+
6: "file",
|
|
52
|
+
10: "tip",
|
|
53
|
+
11: "robot",
|
|
54
|
+
100: "custom",
|
|
55
|
+
};
|
|
56
|
+
return typeMap[v2Type] || "unknown";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 从 conversationId 解析会话类型
|
|
61
|
+
* conversationId 格式: {appId}|{type}|{targetId}
|
|
62
|
+
*/
|
|
63
|
+
function parseConversationId(conversationId: string): { sessionType: NimSessionType; targetId: string } {
|
|
64
|
+
const parts = conversationId.split("|");
|
|
65
|
+
if (parts.length >= 3) {
|
|
66
|
+
const typeNum = parseInt(parts[1], 10);
|
|
67
|
+
const sessionType: NimSessionType = typeNum === 1 ? "p2p" : typeNum === 2 ? "team" : typeNum === 3 ? "superTeam" : "p2p";
|
|
68
|
+
return { sessionType, targetId: parts[2] };
|
|
69
|
+
}
|
|
70
|
+
return { sessionType: "p2p", targetId: "" };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 构建 conversationId
|
|
75
|
+
*/
|
|
76
|
+
function buildConversationId(conversationIdUtil: any, accountId: string, sessionType: NimSessionType): string {
|
|
77
|
+
if (conversationIdUtil) {
|
|
78
|
+
switch (sessionType) {
|
|
79
|
+
case "p2p":
|
|
80
|
+
return conversationIdUtil.p2pConversationId(accountId) || "";
|
|
81
|
+
case "team":
|
|
82
|
+
return conversationIdUtil.teamConversationId(accountId) || "";
|
|
83
|
+
case "superTeam":
|
|
84
|
+
return conversationIdUtil.superTeamConversationId(accountId) || "";
|
|
85
|
+
default:
|
|
86
|
+
return conversationIdUtil.p2pConversationId(accountId) || "";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// fallback: 手动构建
|
|
90
|
+
const typeNum = sessionType === "p2p" ? 1 : sessionType === "team" ? 2 : 3;
|
|
91
|
+
return `0|${typeNum}|${accountId}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 解析 V2 消息附件
|
|
96
|
+
*/
|
|
97
|
+
function parseV2Attachment(msg: any): NimAttachment | undefined {
|
|
98
|
+
const attachment = msg.attachment;
|
|
99
|
+
if (!attachment) return undefined;
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
name: attachment.name,
|
|
103
|
+
size: attachment.size,
|
|
104
|
+
url: attachment.url,
|
|
105
|
+
ext: attachment.ext,
|
|
106
|
+
md5: attachment.md5,
|
|
107
|
+
w: attachment.width,
|
|
108
|
+
h: attachment.height,
|
|
109
|
+
dur: attachment.duration,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 将 V2 消息转换为我们的消息事件格式
|
|
115
|
+
*/
|
|
116
|
+
function convertV2ToMessageEvent(msg: any): NimMessageEvent {
|
|
117
|
+
const { sessionType } = parseConversationId(msg.conversationId || "");
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
msgId: String(msg.messageServerId || msg.messageClientId || ""),
|
|
121
|
+
clientMsgId: String(msg.messageClientId || ""),
|
|
122
|
+
sessionType,
|
|
123
|
+
from: String(msg.senderId || ""),
|
|
124
|
+
to: String(msg.receiverId || ""),
|
|
125
|
+
type: convertMessageType(msg.messageType),
|
|
126
|
+
text: msg.text || "",
|
|
127
|
+
time: msg.createTime || Date.now(),
|
|
128
|
+
attach: parseV2Attachment(msg),
|
|
129
|
+
ext: msg.serverExtension ? JSON.parse(msg.serverExtension) : undefined,
|
|
130
|
+
rawMsg: msg,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 创建 NIM 客户端实例 (V2 API)
|
|
136
|
+
*/
|
|
137
|
+
export async function createNimClient(cfg: NimConfig): Promise<NimClientInstance> {
|
|
138
|
+
const creds = resolveNimCredentials(cfg);
|
|
139
|
+
if (!creds) {
|
|
140
|
+
throw new Error("NIM credentials not configured");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const cacheKey = `${creds.appKey}:${creds.account}`;
|
|
144
|
+
|
|
145
|
+
// 检查缓存
|
|
146
|
+
const cached = clientCache.get(cacheKey);
|
|
147
|
+
if (cached && cached.initialized) {
|
|
148
|
+
return cached;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 动态导入 node-nim
|
|
152
|
+
const nodenim = await import("node-nim");
|
|
153
|
+
|
|
154
|
+
// 使用 V2 API
|
|
155
|
+
const v2Client = new nodenim.V2NIMClient();
|
|
156
|
+
|
|
157
|
+
const dataPath = getSdkDataPath(creds.account);
|
|
158
|
+
|
|
159
|
+
// 初始化 SDK (V2)
|
|
160
|
+
const initError = v2Client.init({
|
|
161
|
+
appkey: creds.appKey,
|
|
162
|
+
appDataPath: dataPath,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (initError) {
|
|
166
|
+
throw new Error(`NIM SDK V2 initialization failed: ${initError.desc}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log("[NIM V2] SDK initialized, dataPath:", dataPath);
|
|
170
|
+
|
|
171
|
+
let loggedIn = false;
|
|
172
|
+
const msgCallbackSet = new Set<(msg: NimMessageEvent) => void>();
|
|
173
|
+
const connCallbackSet = new Set<(state: string) => void>();
|
|
174
|
+
|
|
175
|
+
messageCallbacks.set(cacheKey, msgCallbackSet);
|
|
176
|
+
connectionCallbacks.set(cacheKey, connCallbackSet);
|
|
177
|
+
|
|
178
|
+
// 获取服务
|
|
179
|
+
const loginService = v2Client.getLoginService();
|
|
180
|
+
const messageService = v2Client.getMessageService();
|
|
181
|
+
const messageCreator = v2Client.messageCreator;
|
|
182
|
+
const conversationIdUtil = v2Client.conversationIdUtil;
|
|
183
|
+
|
|
184
|
+
if (!loginService || !messageService) {
|
|
185
|
+
throw new Error("NIM SDK V2 services not available");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 注册消息接收回调
|
|
189
|
+
messageService.on("receiveMessages", (messages: any[]) => {
|
|
190
|
+
console.log("[NIM V2] Received messages:", messages.length);
|
|
191
|
+
for (const msg of messages) {
|
|
192
|
+
console.log("[NIM V2] Message:", JSON.stringify(msg, null, 2));
|
|
193
|
+
const event = convertV2ToMessageEvent(msg);
|
|
194
|
+
msgCallbackSet.forEach((cb) => cb(event));
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// 注册发送消息状态回调
|
|
199
|
+
messageService.on("sendMessage", (msg: any) => {
|
|
200
|
+
console.log("[NIM V2] Send message status:", msg.messageClientId, msg.sendingState);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// 注册登录状态回调
|
|
204
|
+
loginService.on("loginStatus", (status: number) => {
|
|
205
|
+
console.log("[NIM V2] Login status changed:", status);
|
|
206
|
+
// V2NIMLoginStatus: 0=LOGOUT, 1=LOGINED, 2=LOGINING
|
|
207
|
+
if (status === 1) {
|
|
208
|
+
loggedIn = true;
|
|
209
|
+
connCallbackSet.forEach((cb) => cb("connected"));
|
|
210
|
+
} else if (status === 0) {
|
|
211
|
+
loggedIn = false;
|
|
212
|
+
connCallbackSet.forEach((cb) => cb("logout"));
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
loginService.on("kickedOffline", (detail: any) => {
|
|
217
|
+
console.log("[NIM V2] Kicked offline:", detail);
|
|
218
|
+
loggedIn = false;
|
|
219
|
+
connCallbackSet.forEach((cb) => cb("kickout"));
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
loginService.on("disconnected", (error: any) => {
|
|
223
|
+
console.log("[NIM V2] Disconnected:", error);
|
|
224
|
+
connCallbackSet.forEach((cb) => cb("disconnected"));
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const instance: NimClientInstance = {
|
|
228
|
+
initialized: true,
|
|
229
|
+
loggedIn: false,
|
|
230
|
+
account: creds.account,
|
|
231
|
+
|
|
232
|
+
async login(): Promise<boolean> {
|
|
233
|
+
try {
|
|
234
|
+
console.log("[NIM V2] Logging in...", creds.account);
|
|
235
|
+
await loginService.login(creds.account, creds.token, {});
|
|
236
|
+
loggedIn = true;
|
|
237
|
+
instance.loggedIn = true;
|
|
238
|
+
console.log("[NIM V2] Login successful");
|
|
239
|
+
return true;
|
|
240
|
+
} catch (error: any) {
|
|
241
|
+
console.error("[NIM V2] Login failed:", error);
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
async logout(): Promise<void> {
|
|
247
|
+
try {
|
|
248
|
+
await loginService.logout();
|
|
249
|
+
loggedIn = false;
|
|
250
|
+
instance.loggedIn = false;
|
|
251
|
+
console.log("[NIM V2] Logged out");
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error("[NIM V2] Logout error:", error);
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
async sendText(to: string, text: string, sessionType: NimSessionType = "p2p"): Promise<NimSendResult> {
|
|
258
|
+
try {
|
|
259
|
+
const message = messageCreator?.createTextMessage(text);
|
|
260
|
+
if (!message) {
|
|
261
|
+
return { success: false, error: "Failed to create text message" };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const conversationId = buildConversationId(conversationIdUtil, to, sessionType);
|
|
265
|
+
console.log("[NIM V2] Sending text to:", conversationId, "text:", text.substring(0, 50));
|
|
266
|
+
|
|
267
|
+
const result = await messageService.sendMessage(message, conversationId, {}, () => {});
|
|
268
|
+
|
|
269
|
+
console.log("[NIM V2] Send result:", result);
|
|
270
|
+
return {
|
|
271
|
+
success: true,
|
|
272
|
+
msgId: result.message?.messageServerId,
|
|
273
|
+
clientMsgId: result.message?.messageClientId,
|
|
274
|
+
};
|
|
275
|
+
} catch (error: any) {
|
|
276
|
+
console.error("[NIM V2] Send text failed:", error);
|
|
277
|
+
return {
|
|
278
|
+
success: false,
|
|
279
|
+
error: error.message || String(error),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
async sendImage(to: string, filePath: string, sessionType: NimSessionType = "p2p"): Promise<NimSendResult> {
|
|
285
|
+
try {
|
|
286
|
+
const message = messageCreator?.createImageMessage(filePath, path.basename(filePath), "", 0, 0);
|
|
287
|
+
if (!message) {
|
|
288
|
+
return { success: false, error: "Failed to create image message" };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const conversationId = buildConversationId(conversationIdUtil, to, sessionType);
|
|
292
|
+
console.log("[NIM V2] Sending image to:", conversationId);
|
|
293
|
+
|
|
294
|
+
const result = await messageService.sendMessage(message, conversationId, {}, () => {});
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
success: true,
|
|
298
|
+
msgId: result.message?.messageServerId,
|
|
299
|
+
clientMsgId: result.message?.messageClientId,
|
|
300
|
+
};
|
|
301
|
+
} catch (error: any) {
|
|
302
|
+
console.error("[NIM V2] Send image failed:", error);
|
|
303
|
+
return { success: false, error: error.message || String(error) };
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
async sendFile(to: string, filePath: string, sessionType: NimSessionType = "p2p"): Promise<NimSendResult> {
|
|
308
|
+
try {
|
|
309
|
+
const message = messageCreator?.createFileMessage(filePath, path.basename(filePath), "");
|
|
310
|
+
if (!message) {
|
|
311
|
+
return { success: false, error: "Failed to create file message" };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const conversationId = buildConversationId(conversationIdUtil, to, sessionType);
|
|
315
|
+
console.log("[NIM V2] Sending file to:", conversationId);
|
|
316
|
+
|
|
317
|
+
const result = await messageService.sendMessage(message, conversationId, {}, () => {});
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
success: true,
|
|
321
|
+
msgId: result.message?.messageServerId,
|
|
322
|
+
clientMsgId: result.message?.messageClientId,
|
|
323
|
+
};
|
|
324
|
+
} catch (error: any) {
|
|
325
|
+
console.error("[NIM V2] Send file failed:", error);
|
|
326
|
+
return { success: false, error: error.message || String(error) };
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
async sendAudio(to: string, filePath: string, duration: number, sessionType: NimSessionType = "p2p"): Promise<NimSendResult> {
|
|
331
|
+
try {
|
|
332
|
+
const message = messageCreator?.createAudioMessage?.(filePath, path.basename(filePath), "", duration);
|
|
333
|
+
if (!message) {
|
|
334
|
+
return { success: false, error: "Failed to create audio message" };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const conversationId = buildConversationId(conversationIdUtil, to, sessionType);
|
|
338
|
+
const result = await messageService.sendMessage(message, conversationId, {}, () => {});
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
success: true,
|
|
342
|
+
msgId: result.message?.messageServerId,
|
|
343
|
+
clientMsgId: result.message?.messageClientId,
|
|
344
|
+
};
|
|
345
|
+
} catch (error: any) {
|
|
346
|
+
console.error("[NIM V2] Send audio failed:", error);
|
|
347
|
+
return { success: false, error: error.message || String(error) };
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
async sendVideo(to: string, filePath: string, duration: number, width: number, height: number, sessionType: NimSessionType = "p2p"): Promise<NimSendResult> {
|
|
352
|
+
try {
|
|
353
|
+
const message = messageCreator?.createVideoMessage?.(filePath, path.basename(filePath), "", duration, width, height);
|
|
354
|
+
if (!message) {
|
|
355
|
+
return { success: false, error: "Failed to create video message" };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const conversationId = buildConversationId(conversationIdUtil, to, sessionType);
|
|
359
|
+
const result = await messageService.sendMessage(message, conversationId, {}, () => {});
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
success: true,
|
|
363
|
+
msgId: result.message?.messageServerId,
|
|
364
|
+
clientMsgId: result.message?.messageClientId,
|
|
365
|
+
};
|
|
366
|
+
} catch (error: any) {
|
|
367
|
+
console.error("[NIM V2] Send video failed:", error);
|
|
368
|
+
return { success: false, error: error.message || String(error) };
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
onMessage(callback: (msg: NimMessageEvent) => void): void {
|
|
373
|
+
msgCallbackSet.add(callback);
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
offMessage(callback: (msg: NimMessageEvent) => void): void {
|
|
377
|
+
msgCallbackSet.delete(callback);
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
onConnectionChange(callback: (state: string) => void): void {
|
|
381
|
+
connCallbackSet.add(callback);
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
async destroy(): Promise<void> {
|
|
385
|
+
await instance.logout();
|
|
386
|
+
v2Client.uninit();
|
|
387
|
+
clientCache.delete(cacheKey);
|
|
388
|
+
messageCallbacks.delete(cacheKey);
|
|
389
|
+
connectionCallbacks.delete(cacheKey);
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
clientCache.set(cacheKey, instance);
|
|
394
|
+
return instance;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* 获取缓存的客户端
|
|
399
|
+
*/
|
|
400
|
+
export function getCachedNimClient(cfg: NimConfig): NimClientInstance | undefined {
|
|
401
|
+
const creds = resolveNimCredentials(cfg);
|
|
402
|
+
if (!creds) return undefined;
|
|
403
|
+
const cacheKey = `${creds.appKey}:${creds.account}`;
|
|
404
|
+
return clientCache.get(cacheKey);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* 清除客户端缓存
|
|
409
|
+
*/
|
|
410
|
+
export async function clearNimClientCache(cfg?: NimConfig): Promise<void> {
|
|
411
|
+
if (cfg) {
|
|
412
|
+
const creds = resolveNimCredentials(cfg);
|
|
413
|
+
if (!creds) return;
|
|
414
|
+
const cacheKey = `${creds.appKey}:${creds.account}`;
|
|
415
|
+
const client = clientCache.get(cacheKey);
|
|
416
|
+
if (client) {
|
|
417
|
+
await client.destroy();
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
for (const client of clientCache.values()) {
|
|
421
|
+
await client.destroy();
|
|
422
|
+
}
|
|
423
|
+
clientCache.clear();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Coerce value to string (handles number inputs from YAML).
|
|
5
|
+
* YAML may parse values like `account: 123456` as numbers.
|
|
6
|
+
*/
|
|
7
|
+
const coerceToString = z.preprocess(
|
|
8
|
+
(val) => (typeof val === "number" ? String(val) : val),
|
|
9
|
+
z.string()
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* NIM channel configuration schema.
|
|
14
|
+
*/
|
|
15
|
+
export const NimConfigSchema = z.object({
|
|
16
|
+
/** Whether the NIM channel is enabled */
|
|
17
|
+
enabled: z.boolean().optional().default(false),
|
|
18
|
+
|
|
19
|
+
/** NIM App Key (coerced from number if needed) */
|
|
20
|
+
appKey: coerceToString.optional(),
|
|
21
|
+
|
|
22
|
+
/** Bot account ID (coerced from number if needed) */
|
|
23
|
+
account: coerceToString.optional(),
|
|
24
|
+
|
|
25
|
+
/** Authentication token (coerced from number if needed) */
|
|
26
|
+
token: coerceToString.optional(),
|
|
27
|
+
|
|
28
|
+
/** DM access policy: open (allow all), allowlist (only allowed users) */
|
|
29
|
+
dmPolicy: z.enum(["open", "allowlist"]).optional().default("open"),
|
|
30
|
+
|
|
31
|
+
/** List of allowed sender IDs when dmPolicy is "allowlist" */
|
|
32
|
+
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
33
|
+
|
|
34
|
+
/** Maximum media file size in MB */
|
|
35
|
+
mediaMaxMb: z.number().min(0).optional().default(30),
|
|
36
|
+
|
|
37
|
+
/** Text chunk limit for splitting long messages */
|
|
38
|
+
textChunkLimit: z.number().min(1).optional().default(4000),
|
|
39
|
+
|
|
40
|
+
/** NIM server configuration (optional, for private deployment) */
|
|
41
|
+
lbsUrl: z.string().optional(),
|
|
42
|
+
|
|
43
|
+
/** Link server URL (optional, for private deployment) */
|
|
44
|
+
linkUrl: z.string().optional(),
|
|
45
|
+
|
|
46
|
+
/** Enable debug logging */
|
|
47
|
+
debug: z.boolean().optional().default(false),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export type { z };
|