moltbot-wecom 1.0.2 → 2.0.0
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/clawdbot.plugin.json +42 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/src/accounts.d.ts +15 -0
- package/dist/src/accounts.d.ts.map +1 -0
- package/dist/src/accounts.js +90 -0
- package/dist/src/channel.d.ts +8 -0
- package/dist/src/channel.d.ts.map +1 -0
- package/dist/src/channel.js +365 -0
- package/dist/src/config-schema.d.ts +211 -0
- package/dist/src/config-schema.d.ts.map +1 -0
- package/dist/src/config-schema.js +21 -0
- package/dist/src/dedup.d.ts +8 -0
- package/dist/src/dedup.d.ts.map +1 -0
- package/dist/src/dedup.js +24 -0
- package/dist/src/group-filter.d.ts +15 -0
- package/dist/src/group-filter.d.ts.map +1 -0
- package/dist/src/group-filter.js +43 -0
- package/dist/src/probe.d.ts +10 -0
- package/dist/src/probe.d.ts.map +1 -0
- package/dist/src/probe.js +70 -0
- package/dist/src/receive.d.ts +26 -0
- package/dist/src/receive.d.ts.map +1 -0
- package/dist/src/receive.js +282 -0
- package/dist/src/runtime.d.ts +7 -0
- package/dist/src/runtime.d.ts.map +1 -0
- package/dist/src/runtime.js +13 -0
- package/dist/src/send.d.ts +26 -0
- package/dist/src/send.d.ts.map +1 -0
- package/dist/src/send.js +75 -0
- package/dist/src/status-issues.d.ts +7 -0
- package/dist/src/status-issues.d.ts.map +1 -0
- package/dist/src/status-issues.js +47 -0
- package/dist/src/types.d.ts +79 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +4 -0
- package/package.json +28 -11
- package/moltbot.plugin.json +0 -27
- package/plugin.js +0 -243
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeCom WebSocket message receive handler.
|
|
3
|
+
*
|
|
4
|
+
* Connects to WeCom via a WebSocket proxy, dispatches incoming
|
|
5
|
+
* messages to Clawdbot's channel reply infrastructure.
|
|
6
|
+
*/
|
|
7
|
+
import { isDuplicate } from "./dedup.js";
|
|
8
|
+
import { shouldRespondInGroup } from "./group-filter.js";
|
|
9
|
+
import { sendReplyMessage, sendPing, registerConnection, unregisterConnection } from "./send.js";
|
|
10
|
+
import { getWeComRuntime } from "./runtime.js";
|
|
11
|
+
/** Start the WeCom WebSocket provider. Returns a stop function. */
|
|
12
|
+
export function startWeComProvider(options) {
|
|
13
|
+
const { account, config, log, statusSink, abortSignal } = options;
|
|
14
|
+
const { proxyUrl, proxyToken, accountId } = account;
|
|
15
|
+
const thinkingThresholdMs = account.config.thinkingThresholdMs ?? 0;
|
|
16
|
+
const botNames = account.config.botNames;
|
|
17
|
+
const pingIntervalMs = account.config.pingIntervalMs ?? 30000;
|
|
18
|
+
const reconnectDelayMs = account.config.reconnectDelayMs ?? 5000;
|
|
19
|
+
log.info(`[wecom:${accountId}] Starting WebSocket provider (proxy=${proxyUrl})`);
|
|
20
|
+
const state = {
|
|
21
|
+
ws: null,
|
|
22
|
+
pingTimer: null,
|
|
23
|
+
reconnectTimer: null,
|
|
24
|
+
isAlive: false,
|
|
25
|
+
stopped: false,
|
|
26
|
+
};
|
|
27
|
+
// Handle abort signal
|
|
28
|
+
if (abortSignal) {
|
|
29
|
+
abortSignal.addEventListener("abort", () => {
|
|
30
|
+
state.stopped = true;
|
|
31
|
+
cleanup();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function cleanup() {
|
|
35
|
+
if (state.pingTimer) {
|
|
36
|
+
clearInterval(state.pingTimer);
|
|
37
|
+
state.pingTimer = null;
|
|
38
|
+
}
|
|
39
|
+
if (state.reconnectTimer) {
|
|
40
|
+
clearTimeout(state.reconnectTimer);
|
|
41
|
+
state.reconnectTimer = null;
|
|
42
|
+
}
|
|
43
|
+
if (state.ws) {
|
|
44
|
+
unregisterConnection(accountId);
|
|
45
|
+
try {
|
|
46
|
+
state.ws.terminate();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Ignore
|
|
50
|
+
}
|
|
51
|
+
state.ws = null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function startHeartbeat() {
|
|
55
|
+
if (state.pingTimer) {
|
|
56
|
+
clearInterval(state.pingTimer);
|
|
57
|
+
}
|
|
58
|
+
state.pingTimer = setInterval(() => {
|
|
59
|
+
if (!state.ws || state.ws.readyState !== 1 /* OPEN */) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!state.isAlive) {
|
|
63
|
+
log.warn?.(`[wecom:${accountId}] Heartbeat timeout, reconnecting...`);
|
|
64
|
+
state.ws.terminate();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
state.isAlive = false;
|
|
68
|
+
sendPing(state.ws);
|
|
69
|
+
}, pingIntervalMs);
|
|
70
|
+
}
|
|
71
|
+
function scheduleReconnect() {
|
|
72
|
+
if (state.stopped || state.reconnectTimer)
|
|
73
|
+
return;
|
|
74
|
+
state.reconnectTimer = setTimeout(() => {
|
|
75
|
+
state.reconnectTimer = null;
|
|
76
|
+
if (!state.stopped) {
|
|
77
|
+
connect();
|
|
78
|
+
}
|
|
79
|
+
}, reconnectDelayMs);
|
|
80
|
+
}
|
|
81
|
+
async function connect() {
|
|
82
|
+
if (state.stopped)
|
|
83
|
+
return;
|
|
84
|
+
try {
|
|
85
|
+
const { default: WebSocket } = await import("ws");
|
|
86
|
+
const url = proxyToken
|
|
87
|
+
? `${proxyUrl}?token=${encodeURIComponent(proxyToken)}`
|
|
88
|
+
: proxyUrl;
|
|
89
|
+
log.info(`[wecom:${accountId}] Connecting to proxy: ${proxyUrl}`);
|
|
90
|
+
const ws = new WebSocket(url);
|
|
91
|
+
state.ws = ws;
|
|
92
|
+
ws.on("open", () => {
|
|
93
|
+
log.info(`[wecom:${accountId}] WebSocket connected`);
|
|
94
|
+
state.isAlive = true;
|
|
95
|
+
registerConnection(accountId, ws);
|
|
96
|
+
statusSink?.({ running: true, lastStartAt: Date.now() });
|
|
97
|
+
if (state.reconnectTimer) {
|
|
98
|
+
clearTimeout(state.reconnectTimer);
|
|
99
|
+
state.reconnectTimer = null;
|
|
100
|
+
}
|
|
101
|
+
startHeartbeat();
|
|
102
|
+
});
|
|
103
|
+
ws.on("message", async (data) => {
|
|
104
|
+
try {
|
|
105
|
+
const event = JSON.parse(data.toString());
|
|
106
|
+
await handleIncomingEvent(event, {
|
|
107
|
+
ws,
|
|
108
|
+
account,
|
|
109
|
+
config,
|
|
110
|
+
log,
|
|
111
|
+
thinkingThresholdMs,
|
|
112
|
+
botNames,
|
|
113
|
+
statusSink,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
log.error(`[wecom:${accountId}] Message handler error: ${err instanceof Error ? err.message : String(err)}`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
ws.on("close", () => {
|
|
121
|
+
log.warn?.(`[wecom:${accountId}] WebSocket disconnected`);
|
|
122
|
+
state.isAlive = false;
|
|
123
|
+
unregisterConnection(accountId);
|
|
124
|
+
statusSink?.({ running: false, lastStopAt: Date.now() });
|
|
125
|
+
if (state.pingTimer) {
|
|
126
|
+
clearInterval(state.pingTimer);
|
|
127
|
+
state.pingTimer = null;
|
|
128
|
+
}
|
|
129
|
+
scheduleReconnect();
|
|
130
|
+
});
|
|
131
|
+
ws.on("error", (err) => {
|
|
132
|
+
log.error(`[wecom:${accountId}] WebSocket error: ${err.message}`);
|
|
133
|
+
statusSink?.({ lastError: err.message });
|
|
134
|
+
ws.terminate();
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
log.error(`[wecom:${accountId}] Connection failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
139
|
+
scheduleReconnect();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Start initial connection
|
|
143
|
+
connect();
|
|
144
|
+
const stop = () => {
|
|
145
|
+
log.info(`[wecom:${accountId}] Stopping WebSocket provider`);
|
|
146
|
+
state.stopped = true;
|
|
147
|
+
cleanup();
|
|
148
|
+
statusSink?.({ running: false, lastStopAt: Date.now() });
|
|
149
|
+
};
|
|
150
|
+
return { stop };
|
|
151
|
+
}
|
|
152
|
+
/** Handle a single incoming WeCom event. */
|
|
153
|
+
async function handleIncomingEvent(event, ctx) {
|
|
154
|
+
// Handle pong response
|
|
155
|
+
if (event.kind === "pong") {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// Handle connection confirmation
|
|
159
|
+
if (event.kind === "connected") {
|
|
160
|
+
ctx.log.info(`[wecom:${ctx.account.accountId}] Connection confirmed by proxy`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// Only process message events
|
|
164
|
+
if (event.kind !== "message") {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const payload = event.payload;
|
|
168
|
+
if (!payload)
|
|
169
|
+
return;
|
|
170
|
+
const msgId = event.msgId;
|
|
171
|
+
const senderId = payload.FromUserName;
|
|
172
|
+
const text = payload.Content?.trim();
|
|
173
|
+
// Basic validation
|
|
174
|
+
if (!senderId || !text) {
|
|
175
|
+
// Send empty reply for non-text messages
|
|
176
|
+
await sendReplyMessage(ctx.ws, msgId ?? "", "");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Dedup check
|
|
180
|
+
if (isDuplicate(msgId)) {
|
|
181
|
+
ctx.log.debug?.(`[wecom:${ctx.account.accountId}] Duplicate message ignored: ${msgId}`);
|
|
182
|
+
await sendReplyMessage(ctx.ws, msgId ?? "", "");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
ctx.statusSink?.({ lastInboundAt: Date.now() });
|
|
186
|
+
// WeCom Smart Bot messages are typically direct; group logic kept for future use
|
|
187
|
+
const isGroup = false;
|
|
188
|
+
if (isGroup && !shouldRespondInGroup(text, [], ctx.botNames)) {
|
|
189
|
+
ctx.log.debug?.(`[wecom:${ctx.account.accountId}] Ignoring group message (no trigger)`);
|
|
190
|
+
await sendReplyMessage(ctx.ws, msgId ?? "", "");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const sessionKey = `wecom:${senderId}`;
|
|
194
|
+
ctx.log.info(`[wecom:${ctx.account.accountId}] Rx [${senderId}]: ${text.slice(0, 80)}`);
|
|
195
|
+
// Dispatch via Clawdbot runtime
|
|
196
|
+
const runtime = getWeComRuntime();
|
|
197
|
+
const channel = runtime.channel;
|
|
198
|
+
if (!channel?.reply?.dispatchReplyWithBufferedBlockDispatcher) {
|
|
199
|
+
ctx.log.error(`[wecom:${ctx.account.accountId}] dispatchReplyWithBufferedBlockDispatcher not available`);
|
|
200
|
+
await sendReplyMessage(ctx.ws, msgId ?? "", "");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
// Build inbound context
|
|
204
|
+
const inboundCtx = {
|
|
205
|
+
Body: text,
|
|
206
|
+
RawBody: text,
|
|
207
|
+
CommandBody: text,
|
|
208
|
+
From: senderId,
|
|
209
|
+
To: payload.ToUserName ?? "",
|
|
210
|
+
SessionKey: sessionKey,
|
|
211
|
+
AccountId: ctx.account.accountId,
|
|
212
|
+
MessageSid: msgId,
|
|
213
|
+
ChatType: "direct",
|
|
214
|
+
ConversationLabel: senderId,
|
|
215
|
+
SenderId: senderId,
|
|
216
|
+
CommandAuthorized: true,
|
|
217
|
+
Provider: "wecom",
|
|
218
|
+
Surface: "wecom",
|
|
219
|
+
OriginatingChannel: "wecom",
|
|
220
|
+
OriginatingTo: senderId,
|
|
221
|
+
DeliveryContext: {
|
|
222
|
+
channel: "wecom",
|
|
223
|
+
to: senderId,
|
|
224
|
+
accountId: ctx.account.accountId,
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
// Timeout for passive reply (WeCom has strict 5s limit)
|
|
228
|
+
const REPLY_TIMEOUT_MS = 4500;
|
|
229
|
+
let replied = false;
|
|
230
|
+
// Promise to handle the reply
|
|
231
|
+
const replyPromise = new Promise((resolve) => {
|
|
232
|
+
const timeout = setTimeout(() => {
|
|
233
|
+
if (!replied) {
|
|
234
|
+
replied = true;
|
|
235
|
+
ctx.log.warn?.(`[wecom:${ctx.account.accountId}] Reply timeout, sending empty`);
|
|
236
|
+
sendReplyMessage(ctx.ws, msgId ?? "", "").finally(resolve);
|
|
237
|
+
}
|
|
238
|
+
}, REPLY_TIMEOUT_MS);
|
|
239
|
+
// Create reply handler
|
|
240
|
+
const replyHandler = async (replyText) => {
|
|
241
|
+
if (replied)
|
|
242
|
+
return;
|
|
243
|
+
replied = true;
|
|
244
|
+
clearTimeout(timeout);
|
|
245
|
+
if (!replyText || replyText === "NO_REPLY") {
|
|
246
|
+
await sendReplyMessage(ctx.ws, msgId ?? "", "");
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
ctx.log.info(`[wecom:${ctx.account.accountId}] Tx [${senderId}]: ${replyText.slice(0, 50)}...`);
|
|
250
|
+
ctx.statusSink?.({ lastOutboundAt: Date.now() });
|
|
251
|
+
await sendReplyMessage(ctx.ws, msgId ?? "", replyText);
|
|
252
|
+
}
|
|
253
|
+
resolve();
|
|
254
|
+
};
|
|
255
|
+
// Dispatch to Clawdbot pipeline
|
|
256
|
+
channel.reply?.dispatchReplyWithBufferedBlockDispatcher?.({
|
|
257
|
+
ctx: inboundCtx,
|
|
258
|
+
replyFn: replyHandler,
|
|
259
|
+
blockHandler: {
|
|
260
|
+
onBlock: async (block) => {
|
|
261
|
+
// For buffered responses, we collect and send at end
|
|
262
|
+
if (block.text && !replied) {
|
|
263
|
+
await replyHandler(block.text);
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
onComplete: async () => {
|
|
267
|
+
if (!replied) {
|
|
268
|
+
await replyHandler("");
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
}).catch((err) => {
|
|
273
|
+
ctx.log.error(`[wecom:${ctx.account.accountId}] Pipeline error: ${err.message}`);
|
|
274
|
+
if (!replied) {
|
|
275
|
+
replied = true;
|
|
276
|
+
clearTimeout(timeout);
|
|
277
|
+
sendReplyMessage(ctx.ws, msgId ?? "", "").finally(resolve);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
await replyPromise;
|
|
282
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global runtime reference for the WeCom plugin.
|
|
3
|
+
*/
|
|
4
|
+
import type { PluginRuntime } from "clawdbot/plugin-sdk";
|
|
5
|
+
export declare function setWeComRuntime(next: PluginRuntime): void;
|
|
6
|
+
export declare function getWeComRuntime(): PluginRuntime;
|
|
7
|
+
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIzD,wBAAgB,eAAe,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAEzD;AAED,wBAAgB,eAAe,IAAI,aAAa,CAK/C"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global runtime reference for the WeCom plugin.
|
|
3
|
+
*/
|
|
4
|
+
let runtime = null;
|
|
5
|
+
export function setWeComRuntime(next) {
|
|
6
|
+
runtime = next;
|
|
7
|
+
}
|
|
8
|
+
export function getWeComRuntime() {
|
|
9
|
+
if (!runtime) {
|
|
10
|
+
throw new Error("WeCom runtime not initialized");
|
|
11
|
+
}
|
|
12
|
+
return runtime;
|
|
13
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send messages to WeCom via WebSocket proxy.
|
|
3
|
+
*/
|
|
4
|
+
import type { WeComSendResult } from "./types.js";
|
|
5
|
+
import type WebSocket from "ws";
|
|
6
|
+
/** Register an active WebSocket connection for an account. */
|
|
7
|
+
export declare function registerConnection(accountId: string, ws: WebSocket): void;
|
|
8
|
+
/** Unregister a WebSocket connection for an account. */
|
|
9
|
+
export declare function unregisterConnection(accountId: string): void;
|
|
10
|
+
/** Get active WebSocket connection for an account. */
|
|
11
|
+
export declare function getConnection(accountId: string): WebSocket | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Send a reply to a specific message via the WeCom proxy.
|
|
14
|
+
*/
|
|
15
|
+
export declare function sendReplyMessage(ws: WebSocket | undefined, msgId: string, text: string): Promise<WeComSendResult>;
|
|
16
|
+
/**
|
|
17
|
+
* Send a text message to a WeCom chat.
|
|
18
|
+
* Note: For passive reply mode, messages go through sendReplyMessage.
|
|
19
|
+
* This is a placeholder for potential active messaging support.
|
|
20
|
+
*/
|
|
21
|
+
export declare function sendTextMessage(accountId: string, chatId: string, text: string): Promise<WeComSendResult>;
|
|
22
|
+
/**
|
|
23
|
+
* Send a ping to keep the connection alive.
|
|
24
|
+
*/
|
|
25
|
+
export declare function sendPing(ws: WebSocket | undefined): boolean;
|
|
26
|
+
//# sourceMappingURL=send.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"send.d.ts","sourceRoot":"","sources":["../../src/send.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,KAAK,SAAS,MAAM,IAAI,CAAC;AAKhC,8DAA8D;AAC9D,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,GAAG,IAAI,CAEzE;AAED,wDAAwD;AACxD,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAE5D;AAED,sDAAsD;AACtD,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAEtE;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,SAAS,GAAG,SAAS,EACzB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,eAAe,CAAC,CAwB1B;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,eAAe,CAAC,CAc1B;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,CAW3D"}
|
package/dist/src/send.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send messages to WeCom via WebSocket proxy.
|
|
3
|
+
*/
|
|
4
|
+
/** Active WebSocket connections by account. */
|
|
5
|
+
const activeConnections = new Map();
|
|
6
|
+
/** Register an active WebSocket connection for an account. */
|
|
7
|
+
export function registerConnection(accountId, ws) {
|
|
8
|
+
activeConnections.set(accountId, ws);
|
|
9
|
+
}
|
|
10
|
+
/** Unregister a WebSocket connection for an account. */
|
|
11
|
+
export function unregisterConnection(accountId) {
|
|
12
|
+
activeConnections.delete(accountId);
|
|
13
|
+
}
|
|
14
|
+
/** Get active WebSocket connection for an account. */
|
|
15
|
+
export function getConnection(accountId) {
|
|
16
|
+
return activeConnections.get(accountId);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Send a reply to a specific message via the WeCom proxy.
|
|
20
|
+
*/
|
|
21
|
+
export async function sendReplyMessage(ws, msgId, text) {
|
|
22
|
+
if (!ws || ws.readyState !== 1 /* OPEN */) {
|
|
23
|
+
return { ok: false, error: "WebSocket not connected" };
|
|
24
|
+
}
|
|
25
|
+
if (!msgId) {
|
|
26
|
+
return { ok: false, error: "No message ID provided" };
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
ws.send(JSON.stringify({
|
|
30
|
+
kind: "reply",
|
|
31
|
+
msgId,
|
|
32
|
+
text: text ?? "",
|
|
33
|
+
}));
|
|
34
|
+
return { ok: true, messageId: msgId };
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
error: err instanceof Error ? err.message : String(err),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Send a text message to a WeCom chat.
|
|
45
|
+
* Note: For passive reply mode, messages go through sendReplyMessage.
|
|
46
|
+
* This is a placeholder for potential active messaging support.
|
|
47
|
+
*/
|
|
48
|
+
export async function sendTextMessage(accountId, chatId, text) {
|
|
49
|
+
const ws = activeConnections.get(accountId);
|
|
50
|
+
if (!ws || ws.readyState !== 1 /* OPEN */) {
|
|
51
|
+
return { ok: false, error: "WebSocket not connected" };
|
|
52
|
+
}
|
|
53
|
+
// WeCom Smart Bot uses passive reply mode; active messaging
|
|
54
|
+
// would require calling WeCom APIs directly (future enhancement).
|
|
55
|
+
// For now, return an error indicating this limitation.
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
error: "Active messaging not supported in proxy mode. Use reply flow.",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Send a ping to keep the connection alive.
|
|
63
|
+
*/
|
|
64
|
+
export function sendPing(ws) {
|
|
65
|
+
if (!ws || ws.readyState !== 1 /* OPEN */) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
ws.send(JSON.stringify({ kind: "ping", ts: Date.now() }));
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostic issues collector for WeCom channel.
|
|
3
|
+
*/
|
|
4
|
+
import type { ChannelAccountSnapshot, ChannelStatusIssue } from "clawdbot/plugin-sdk";
|
|
5
|
+
/** Collect configuration issues for all WeCom accounts. */
|
|
6
|
+
export declare function collectWeComStatusIssues(accounts: ChannelAccountSnapshot[]): ChannelStatusIssue[];
|
|
7
|
+
//# sourceMappingURL=status-issues.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status-issues.d.ts","sourceRoot":"","sources":["../../src/status-issues.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AA2BtF,2DAA2D;AAC3D,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,sBAAsB,EAAE,GACjC,kBAAkB,EAAE,CAiCtB"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostic issues collector for WeCom channel.
|
|
3
|
+
*/
|
|
4
|
+
const isRecord = (value) => Boolean(value && typeof value === "object");
|
|
5
|
+
const asString = (value) => typeof value === "string" ? value : typeof value === "number" ? String(value) : undefined;
|
|
6
|
+
function readWeComAccountStatus(value) {
|
|
7
|
+
if (!isRecord(value))
|
|
8
|
+
return null;
|
|
9
|
+
return {
|
|
10
|
+
accountId: value.accountId,
|
|
11
|
+
enabled: value.enabled,
|
|
12
|
+
configured: value.configured,
|
|
13
|
+
dmPolicy: value.dmPolicy,
|
|
14
|
+
proxyUrl: value.proxyUrl,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Collect configuration issues for all WeCom accounts. */
|
|
18
|
+
export function collectWeComStatusIssues(accounts) {
|
|
19
|
+
const issues = [];
|
|
20
|
+
for (const entry of accounts) {
|
|
21
|
+
const account = readWeComAccountStatus(entry);
|
|
22
|
+
if (!account)
|
|
23
|
+
continue;
|
|
24
|
+
const accountId = asString(account.accountId) ?? "default";
|
|
25
|
+
const enabled = account.enabled !== false;
|
|
26
|
+
const configured = account.configured === true;
|
|
27
|
+
if (enabled && !configured) {
|
|
28
|
+
issues.push({
|
|
29
|
+
channel: "wecom",
|
|
30
|
+
accountId,
|
|
31
|
+
kind: "config",
|
|
32
|
+
message: "WeCom account is enabled but not configured (missing proxyUrl).",
|
|
33
|
+
fix: "Set channels.wecom.proxyUrl in clawdbot.json or set WECOM_PROXY_URL environment variable.",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (enabled && configured && account.dmPolicy === "open") {
|
|
37
|
+
issues.push({
|
|
38
|
+
channel: "wecom",
|
|
39
|
+
accountId,
|
|
40
|
+
kind: "config",
|
|
41
|
+
message: 'WeCom dmPolicy is "open", allowing any user to message the bot without pairing.',
|
|
42
|
+
fix: 'Set channels.wecom.dmPolicy to "pairing" or "allowlist" to restrict access.',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return issues;
|
|
47
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeCom channel plugin — type definitions.
|
|
3
|
+
*/
|
|
4
|
+
/** Per-account configuration stored in clawdbot.json channels.wecom */
|
|
5
|
+
export type WeComAccountConfig = {
|
|
6
|
+
/** Optional display name for this account. */
|
|
7
|
+
name?: string;
|
|
8
|
+
/** If false, do not start this WeCom account. Default: true. */
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
/** WebSocket proxy URL for WeCom Smart Bot. */
|
|
11
|
+
proxyUrl?: string;
|
|
12
|
+
/** Token for authenticating with the WebSocket proxy. */
|
|
13
|
+
proxyToken?: string;
|
|
14
|
+
/** Direct message access policy (default: pairing). */
|
|
15
|
+
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
|
16
|
+
/** Allowlist for DM senders (WeCom user IDs). */
|
|
17
|
+
allowFrom?: Array<string | number>;
|
|
18
|
+
/** "Thinking…" placeholder threshold in milliseconds. 0 to disable. */
|
|
19
|
+
thinkingThresholdMs?: number;
|
|
20
|
+
/** Bot name aliases used for group-chat address detection. */
|
|
21
|
+
botNames?: string[];
|
|
22
|
+
/** Heartbeat ping interval in milliseconds. Default: 30000. */
|
|
23
|
+
pingIntervalMs?: number;
|
|
24
|
+
/** Reconnect delay in milliseconds. Default: 5000. */
|
|
25
|
+
reconnectDelayMs?: number;
|
|
26
|
+
};
|
|
27
|
+
/** Top-level WeCom config section (channels.wecom). */
|
|
28
|
+
export type WeComConfig = {
|
|
29
|
+
/** Multi-account map. */
|
|
30
|
+
accounts?: Record<string, WeComAccountConfig>;
|
|
31
|
+
/** Default account ID when multiple accounts exist. */
|
|
32
|
+
defaultAccount?: string;
|
|
33
|
+
} & WeComAccountConfig;
|
|
34
|
+
/** How the credentials were resolved. */
|
|
35
|
+
export type WeComTokenSource = "config" | "plugin" | "env" | "none";
|
|
36
|
+
/** Resolved account ready for use. */
|
|
37
|
+
export type ResolvedWeComAccount = {
|
|
38
|
+
accountId: string;
|
|
39
|
+
name?: string;
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
proxyUrl: string;
|
|
42
|
+
proxyToken: string;
|
|
43
|
+
tokenSource: WeComTokenSource;
|
|
44
|
+
config: WeComAccountConfig;
|
|
45
|
+
};
|
|
46
|
+
/** Result of sending a message via WeCom proxy. */
|
|
47
|
+
export type WeComSendResult = {
|
|
48
|
+
ok: boolean;
|
|
49
|
+
messageId?: string;
|
|
50
|
+
error?: string;
|
|
51
|
+
};
|
|
52
|
+
/** WeCom probe result. */
|
|
53
|
+
export type WeComProbeResult = {
|
|
54
|
+
ok: boolean;
|
|
55
|
+
error?: string;
|
|
56
|
+
elapsedMs: number;
|
|
57
|
+
};
|
|
58
|
+
/** Incoming message from WeCom proxy. */
|
|
59
|
+
export type WeComIncomingMessage = {
|
|
60
|
+
kind: "message" | "pong" | "connected" | "error";
|
|
61
|
+
msgId?: string;
|
|
62
|
+
payload?: {
|
|
63
|
+
FromUserName?: string;
|
|
64
|
+
ToUserName?: string;
|
|
65
|
+
Content?: string;
|
|
66
|
+
MsgType?: string;
|
|
67
|
+
CreateTime?: number;
|
|
68
|
+
};
|
|
69
|
+
ts?: number;
|
|
70
|
+
error?: string;
|
|
71
|
+
};
|
|
72
|
+
/** Outgoing reply to WeCom proxy. */
|
|
73
|
+
export type WeComOutgoingReply = {
|
|
74
|
+
kind: "reply" | "ping";
|
|
75
|
+
msgId?: string;
|
|
76
|
+
text?: string;
|
|
77
|
+
ts?: number;
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,uEAAuE;AACvE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,SAAS,GAAG,WAAW,GAAG,MAAM,GAAG,UAAU,CAAC;IACzD,iDAAiD;IACjD,SAAS,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;IACnC,uEAAuE;IACvE,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,+DAA+D;IAC/D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sDAAsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,uDAAuD;AACvD,MAAM,MAAM,WAAW,GAAG;IACxB,yBAAyB;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAC9C,uDAAuD;IACvD,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GAAG,kBAAkB,CAAC;AAEvB,yCAAyC;AACzC,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;AAEpE,sCAAsC;AACtC,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,gBAAgB,CAAC;IAC9B,MAAM,EAAE,kBAAkB,CAAC;CAC5B,CAAC;AAEF,mDAAmD;AACnD,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,OAAO,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,0BAA0B;AAC1B,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,yCAAyC;AACzC,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,WAAW,GAAG,OAAO,CAAC;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE;QACR,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,qCAAqC;AACrC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moltbot-wecom",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "企业微信机器人插件 - 让 AI 助手接入企业微信智能机器人 | WeCom Smart Bot channel plugin for Moltbot",
|
|
6
6
|
"author": "xiajingan",
|
|
@@ -20,21 +20,30 @@
|
|
|
20
20
|
"websocket",
|
|
21
21
|
"smart-bot"
|
|
22
22
|
],
|
|
23
|
-
"main": "
|
|
23
|
+
"main": "dist/index.js",
|
|
24
|
+
"types": "dist/index.d.ts",
|
|
24
25
|
"exports": {
|
|
25
26
|
".": {
|
|
26
|
-
"import": "./
|
|
27
|
+
"import": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts"
|
|
27
29
|
}
|
|
28
30
|
},
|
|
29
31
|
"files": [
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
+
"dist",
|
|
33
|
+
"clawdbot.plugin.json",
|
|
32
34
|
"README.md",
|
|
33
35
|
"LICENSE"
|
|
34
36
|
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc",
|
|
39
|
+
"clean": "rm -rf dist",
|
|
40
|
+
"prepublishOnly": "npm run clean && npm run build",
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"dev": "tsc --watch"
|
|
43
|
+
},
|
|
35
44
|
"moltbot": {
|
|
36
45
|
"extensions": [
|
|
37
|
-
"./
|
|
46
|
+
"./dist/index.js"
|
|
38
47
|
],
|
|
39
48
|
"channel": {
|
|
40
49
|
"id": "wecom",
|
|
@@ -45,9 +54,11 @@
|
|
|
45
54
|
"blurb": "WeCom (Enterprise WeChat) Smart Bot with WebSocket proxy.",
|
|
46
55
|
"aliases": [
|
|
47
56
|
"企微",
|
|
48
|
-
"qw"
|
|
57
|
+
"qw",
|
|
58
|
+
"wechat-work",
|
|
59
|
+
"qywx"
|
|
49
60
|
],
|
|
50
|
-
"order":
|
|
61
|
+
"order": 86,
|
|
51
62
|
"quickstartAllowFrom": true
|
|
52
63
|
},
|
|
53
64
|
"install": {
|
|
@@ -56,13 +67,19 @@
|
|
|
56
67
|
}
|
|
57
68
|
},
|
|
58
69
|
"dependencies": {
|
|
59
|
-
"ws": "^8.16.0"
|
|
70
|
+
"ws": "^8.16.0",
|
|
71
|
+
"zod": "^3.22.0"
|
|
72
|
+
},
|
|
73
|
+
"devDependencies": {
|
|
74
|
+
"@types/node": "^20.0.0",
|
|
75
|
+
"@types/ws": "^8.5.10",
|
|
76
|
+
"typescript": "^5.3.0"
|
|
60
77
|
},
|
|
61
78
|
"peerDependencies": {
|
|
62
|
-
"
|
|
79
|
+
"clawdbot": ">=1.0.0"
|
|
63
80
|
},
|
|
64
81
|
"peerDependenciesMeta": {
|
|
65
|
-
"
|
|
82
|
+
"clawdbot": {
|
|
66
83
|
"optional": false
|
|
67
84
|
}
|
|
68
85
|
},
|
package/moltbot.plugin.json
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "wecom",
|
|
3
|
-
"name": "WeCom",
|
|
4
|
-
"description": "WeCom (企业微信) channel plugin — WebSocket proxy connection for Smart Bot",
|
|
5
|
-
"version": "1.0.1",
|
|
6
|
-
"channels": ["wecom"],
|
|
7
|
-
"configSchema": {
|
|
8
|
-
"type": "object",
|
|
9
|
-
"properties": {
|
|
10
|
-
"proxyUrl": {
|
|
11
|
-
"type": "string",
|
|
12
|
-
"description": "WeCom Proxy WebSocket URL (wss://your-domain.com)"
|
|
13
|
-
},
|
|
14
|
-
"proxyToken": {
|
|
15
|
-
"type": "string",
|
|
16
|
-
"description": "Authentication token for wecom-proxy connection"
|
|
17
|
-
},
|
|
18
|
-
"pingInterval": {
|
|
19
|
-
"type": "number",
|
|
20
|
-
"default": 30000,
|
|
21
|
-
"description": "WebSocket heartbeat interval in milliseconds"
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
"required": ["proxyUrl", "proxyToken"],
|
|
25
|
-
"additionalProperties": true
|
|
26
|
-
}
|
|
27
|
-
}
|