@sunnoy/wecom 1.1.2 → 1.3.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/README.md +465 -144
- package/crypto.js +110 -83
- package/dynamic-agent.js +70 -87
- package/image-processor.js +86 -93
- package/index.js +16 -1068
- package/logger.js +48 -49
- package/package.json +5 -6
- package/stream-manager.js +316 -265
- package/utils.js +76 -238
- package/webhook.js +434 -287
- package/wecom/agent-api.js +251 -0
- package/wecom/agent-inbound.js +433 -0
- package/wecom/allow-from.js +58 -0
- package/wecom/channel-plugin.js +638 -0
- package/wecom/commands.js +85 -0
- package/wecom/constants.js +58 -0
- package/wecom/http-handler.js +315 -0
- package/wecom/inbound-processor.js +519 -0
- package/wecom/media.js +118 -0
- package/wecom/outbound-delivery.js +175 -0
- package/wecom/response-url.js +33 -0
- package/wecom/state.js +82 -0
- package/wecom/stream-utils.js +124 -0
- package/wecom/target.js +57 -0
- package/wecom/webhook-bot.js +155 -0
- package/wecom/webhook-targets.js +28 -0
- package/wecom/workspace-template.js +165 -0
- package/wecom/xml-parser.js +126 -0
- package/README_ZH.md +0 -289
- package/client.js +0 -127
package/utils.js
CHANGED
|
@@ -1,251 +1,89 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Utility
|
|
2
|
+
* Utility helpers for the WeCom plugin.
|
|
3
3
|
*/
|
|
4
4
|
export class TTLCache {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
5
|
+
options;
|
|
6
|
+
cache = new Map();
|
|
7
|
+
checkPeriod;
|
|
8
|
+
cleanupTimer;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
this.checkPeriod = options.checkPeriod || options.ttl;
|
|
12
|
+
this.startCleanup();
|
|
13
|
+
}
|
|
14
|
+
set(key, value, ttl) {
|
|
15
|
+
const expiresAt = Date.now() + (ttl || this.options.ttl);
|
|
16
|
+
this.cache.set(key, { value, expiresAt });
|
|
17
|
+
}
|
|
18
|
+
get(key) {
|
|
19
|
+
const entry = this.cache.get(key);
|
|
20
|
+
if (!entry) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
if (Date.now() > entry.expiresAt) {
|
|
24
|
+
this.cache.delete(key);
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
return entry.value;
|
|
28
|
+
}
|
|
29
|
+
has(key) {
|
|
30
|
+
return this.get(key) !== undefined;
|
|
31
|
+
}
|
|
32
|
+
delete(key) {
|
|
33
|
+
return this.cache.delete(key);
|
|
34
|
+
}
|
|
35
|
+
clear() {
|
|
36
|
+
this.cache.clear();
|
|
37
|
+
}
|
|
38
|
+
size() {
|
|
39
|
+
this.cleanup();
|
|
40
|
+
return this.cache.size;
|
|
41
|
+
}
|
|
42
|
+
cleanup() {
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
45
|
+
if (now > entry.expiresAt) {
|
|
46
|
+
this.cache.delete(key);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
startCleanup() {
|
|
51
|
+
this.cleanupTimer = setInterval(() => {
|
|
52
|
+
this.cleanup();
|
|
53
|
+
}, this.checkPeriod);
|
|
54
|
+
// Don't prevent process from exiting
|
|
55
|
+
if (this.cleanupTimer.unref) {
|
|
56
|
+
this.cleanupTimer.unref();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
destroy() {
|
|
60
|
+
if (this.cleanupTimer) {
|
|
61
|
+
clearInterval(this.cleanupTimer);
|
|
62
|
+
}
|
|
63
|
+
this.cache.clear();
|
|
64
|
+
}
|
|
64
65
|
}
|
|
65
66
|
// ============================================================================
|
|
66
|
-
//
|
|
67
|
-
// ============================================================================
|
|
68
|
-
export class PromiseLock {
|
|
69
|
-
pending = new Map();
|
|
70
|
-
async acquire(key, fn) {
|
|
71
|
-
// If there's already a pending operation, wait for it
|
|
72
|
-
const existing = this.pending.get(key);
|
|
73
|
-
if (existing) {
|
|
74
|
-
return existing;
|
|
75
|
-
}
|
|
76
|
-
// Start new operation
|
|
77
|
-
const promise = fn();
|
|
78
|
-
this.pending.set(key, promise);
|
|
79
|
-
try {
|
|
80
|
-
const result = await promise;
|
|
81
|
-
return result;
|
|
82
|
-
}
|
|
83
|
-
finally {
|
|
84
|
-
this.pending.delete(key);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
clear(key) {
|
|
88
|
-
if (key) {
|
|
89
|
-
this.pending.delete(key);
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
this.pending.clear();
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
export async function withRetry(fn, options = {}) {
|
|
97
|
-
const { retries = 3, minTimeout = 1000, maxTimeout = 10000, factor = 2, randomize = true, onRetry, } = options;
|
|
98
|
-
let lastError;
|
|
99
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
100
|
-
try {
|
|
101
|
-
return await fn();
|
|
102
|
-
}
|
|
103
|
-
catch (error) {
|
|
104
|
-
lastError = error;
|
|
105
|
-
if (attempt === retries) {
|
|
106
|
-
throw lastError;
|
|
107
|
-
}
|
|
108
|
-
// Calculate backoff delay
|
|
109
|
-
let delay = Math.min(minTimeout * Math.pow(factor, attempt), maxTimeout);
|
|
110
|
-
if (randomize) {
|
|
111
|
-
delay = delay * (0.5 + Math.random() * 0.5);
|
|
112
|
-
}
|
|
113
|
-
if (onRetry) {
|
|
114
|
-
onRetry(lastError, attempt + 1);
|
|
115
|
-
}
|
|
116
|
-
await sleep(delay);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
throw lastError;
|
|
120
|
-
}
|
|
121
|
-
export function sleep(ms) {
|
|
122
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
123
|
-
}
|
|
124
|
-
// ============================================================================
|
|
125
|
-
// Message Deduplication
|
|
67
|
+
// Message deduplication
|
|
126
68
|
// ============================================================================
|
|
127
69
|
export class MessageDeduplicator {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
export function parseWecomError(errcode, errmsg) {
|
|
141
|
-
// Reference: https://developer.work.weixin.qq.com/document/path/96213
|
|
142
|
-
switch (errcode) {
|
|
143
|
-
case -1:
|
|
144
|
-
return {
|
|
145
|
-
code: errcode,
|
|
146
|
-
message: "System busy, retry later",
|
|
147
|
-
retryable: true,
|
|
148
|
-
category: "system",
|
|
149
|
-
};
|
|
150
|
-
case 0:
|
|
151
|
-
return {
|
|
152
|
-
code: errcode,
|
|
153
|
-
message: "Success",
|
|
154
|
-
retryable: false,
|
|
155
|
-
category: "system",
|
|
156
|
-
};
|
|
157
|
-
case 40001:
|
|
158
|
-
case 40014:
|
|
159
|
-
case 42001:
|
|
160
|
-
case 42007:
|
|
161
|
-
case 42009:
|
|
162
|
-
return {
|
|
163
|
-
code: errcode,
|
|
164
|
-
message: `Invalid or expired access_token: ${errmsg}`,
|
|
165
|
-
retryable: true,
|
|
166
|
-
category: "auth",
|
|
167
|
-
};
|
|
168
|
-
case 45009:
|
|
169
|
-
return {
|
|
170
|
-
code: errcode,
|
|
171
|
-
message: "API rate limit exceeded",
|
|
172
|
-
retryable: true,
|
|
173
|
-
category: "rate_limit",
|
|
174
|
-
};
|
|
175
|
-
case 48002:
|
|
176
|
-
return {
|
|
177
|
-
code: errcode,
|
|
178
|
-
message: "API concurrent call limit exceeded",
|
|
179
|
-
retryable: true,
|
|
180
|
-
category: "rate_limit",
|
|
181
|
-
};
|
|
182
|
-
case 40003:
|
|
183
|
-
case 40013:
|
|
184
|
-
case 40035:
|
|
185
|
-
return {
|
|
186
|
-
code: errcode,
|
|
187
|
-
message: `Invalid parameter: ${errmsg}`,
|
|
188
|
-
retryable: false,
|
|
189
|
-
category: "invalid_input",
|
|
190
|
-
};
|
|
191
|
-
default:
|
|
192
|
-
return {
|
|
193
|
-
code: errcode,
|
|
194
|
-
message: errmsg || "Unknown error",
|
|
195
|
-
retryable: errcode >= 50000 && errcode < 60000, // System errors are retryable
|
|
196
|
-
category: "unknown",
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
export function shouldRetryError(errcode) {
|
|
201
|
-
const info = parseWecomError(errcode, "");
|
|
202
|
-
return info.retryable;
|
|
203
|
-
}
|
|
204
|
-
// ============================================================================
|
|
205
|
-
// URL Helpers
|
|
206
|
-
// ============================================================================
|
|
207
|
-
export function buildApiUrl(path, params) {
|
|
208
|
-
const base = `https://qyapi.weixin.qq.com${path}`;
|
|
209
|
-
if (!params || Object.keys(params).length === 0) {
|
|
210
|
-
return base;
|
|
211
|
-
}
|
|
212
|
-
const query = new URLSearchParams(params).toString();
|
|
213
|
-
return `${base}?${query}`;
|
|
214
|
-
}
|
|
215
|
-
// ============================================================================
|
|
216
|
-
// Random String Generation
|
|
217
|
-
// ============================================================================
|
|
218
|
-
export function randomString(length) {
|
|
219
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
220
|
-
let result = "";
|
|
221
|
-
for (let i = 0; i < length; i++) {
|
|
222
|
-
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
223
|
-
}
|
|
224
|
-
return result;
|
|
70
|
+
seen = new TTLCache({ ttl: 300000 }); // 5 minutes
|
|
71
|
+
isDuplicate(msgId) {
|
|
72
|
+
if (this.seen.has(msgId)) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
this.seen.set(msgId, true);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
markAsSeen(msgId) {
|
|
79
|
+
this.seen.set(msgId, true);
|
|
80
|
+
}
|
|
225
81
|
}
|
|
226
82
|
// ============================================================================
|
|
227
83
|
// Constants
|
|
228
84
|
// ============================================================================
|
|
229
85
|
export const CONSTANTS = {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
// Response URL settings
|
|
234
|
-
RESPONSE_URL_TTL_MS: 3600000, // 1 hour
|
|
235
|
-
RESPONSE_URL_MAX_USES: 1,
|
|
236
|
-
// Media settings
|
|
237
|
-
MEDIA_ID_TTL_MS: 259200000, // 3 days
|
|
238
|
-
// Rate limiting
|
|
239
|
-
MESSAGE_RATE_LIMIT_PER_MINUTE: 20,
|
|
240
|
-
// Timeouts
|
|
241
|
-
API_TIMEOUT_MS: 10000, // 10 seconds
|
|
242
|
-
WEBHOOK_RESPONSE_TIMEOUT_MS: 5000, // 5 seconds
|
|
243
|
-
// Retry settings
|
|
244
|
-
DEFAULT_RETRY_COUNT: 3,
|
|
245
|
-
DEFAULT_RETRY_MIN_DELAY_MS: 1000,
|
|
246
|
-
DEFAULT_RETRY_MAX_DELAY_MS: 10000,
|
|
247
|
-
// AES/Crypto
|
|
248
|
-
AES_BLOCK_SIZE: 32,
|
|
249
|
-
AES_KEY_LENGTH: 43,
|
|
86
|
+
// AES/Crypto
|
|
87
|
+
AES_BLOCK_SIZE: 32,
|
|
88
|
+
AES_KEY_LENGTH: 43,
|
|
250
89
|
};
|
|
251
|
-
//# sourceMappingURL=utils.js.map
|