@yanhaidao/wecom 2.4.160 → 2.5.110
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/dist/index.js +68 -0
- package/dist/src/accounts.js +20 -0
- package/dist/src/agent/handler.js +895 -0
- package/dist/src/agent/index.js +5 -0
- package/dist/src/app/account-runtime.js +216 -0
- package/dist/src/app/bootstrap.js +19 -0
- package/dist/src/app/index.js +118 -0
- package/dist/src/capability/agent/delivery-service.js +63 -0
- package/dist/src/capability/agent/fallback-policy.js +6 -0
- package/dist/src/capability/agent/ingress-service.js +33 -0
- package/dist/src/capability/agent/upstream-delivery-service.js +71 -0
- package/dist/src/capability/bot/dispatch-config.js +45 -0
- package/dist/src/capability/bot/fallback-delivery.js +147 -0
- package/dist/src/capability/bot/local-path-delivery.js +178 -0
- package/dist/src/capability/bot/sandbox-media.js +138 -0
- package/dist/src/capability/bot/service.js +49 -0
- package/dist/src/capability/bot/stream-delivery.js +321 -0
- package/dist/src/capability/bot/stream-finalizer.js +81 -0
- package/dist/src/capability/bot/stream-orchestrator.js +318 -0
- package/dist/src/capability/bot/types.js +1 -0
- package/{src/capability/calendar/client.ts → dist/src/capability/calendar/client.js} +118 -241
- package/{src/capability/calendar/schema.ts → dist/src/capability/calendar/schema.js} +0 -38
- package/dist/src/capability/calendar/tool.js +365 -0
- package/dist/src/capability/calendar/types.js +12 -0
- package/{src/capability/doc/client.ts → dist/src/capability/doc/client.js} +370 -605
- package/{src/capability/doc/schema.ts → dist/src/capability/doc/schema.js} +345 -394
- package/dist/src/capability/doc/tool.js +1556 -0
- package/dist/src/capability/doc/types.js +113 -0
- package/dist/src/capability/mcp/index.js +3 -0
- package/dist/src/capability/mcp/schema.js +102 -0
- package/dist/src/capability/mcp/tool.js +146 -0
- package/dist/src/capability/mcp/transport.js +293 -0
- package/dist/src/channel.js +224 -0
- package/dist/src/config/accounts.js +236 -0
- package/dist/src/config/derived-paths.js +31 -0
- package/dist/src/config/index.js +7 -0
- package/dist/src/config/media.js +110 -0
- package/dist/src/config/network.js +32 -0
- package/dist/src/config/routing.js +20 -0
- package/dist/src/config/runtime-config.js +25 -0
- package/dist/src/config/schema.js +4 -0
- package/{src/config-schema.ts → dist/src/config-schema.js} +1 -1
- package/dist/src/context-store.js +219 -0
- package/{src/crypto/aes.ts → dist/src/crypto/aes.js} +11 -28
- package/dist/src/crypto/index.js +9 -0
- package/{src/crypto/signature.ts → dist/src/crypto/signature.js} +3 -18
- package/{src/crypto/xml.ts → dist/src/crypto/xml.js} +3 -11
- package/dist/src/crypto.js +145 -0
- package/dist/src/domain/models.js +1 -0
- package/dist/src/domain/policies.js +32 -0
- package/{src/dynamic-agent.ts → dist/src/dynamic-agent.js} +36 -73
- package/dist/src/gateway-monitor.js +139 -0
- package/dist/src/http.js +114 -0
- package/{src/media.ts → dist/src/media.js} +21 -40
- package/dist/src/monitor/limits.js +7 -0
- package/dist/src/monitor/state.js +28 -0
- package/dist/src/monitor.js +84 -0
- package/dist/src/observability/audit-log.js +30 -0
- package/dist/src/observability/legacy-operational-event-store.js +22 -0
- package/dist/src/observability/raw-envelope-log.js +24 -0
- package/dist/src/observability/status-registry.js +9 -0
- package/dist/src/observability/transport-session-view.js +14 -0
- package/dist/src/onboarding.js +546 -0
- package/dist/src/outbound.js +557 -0
- package/dist/src/runtime/dispatcher.js +57 -0
- package/{src/runtime/index.ts → dist/src/runtime/index.js} +0 -1
- package/dist/src/runtime/outbound-intent.js +1 -0
- package/dist/src/runtime/reply-orchestrator.js +38 -0
- package/dist/src/runtime/routing-bridge.js +26 -0
- package/dist/src/runtime/session-manager.js +112 -0
- package/dist/src/runtime/source-registry.js +174 -0
- package/dist/src/runtime.js +1 -0
- package/dist/src/shared/command-auth.js +57 -0
- package/{src/shared/index.ts → dist/src/shared/index.js} +0 -1
- package/dist/src/shared/media-asset.js +65 -0
- package/dist/src/shared/media-service.js +59 -0
- package/dist/src/shared/media-types.js +1 -0
- package/{src/shared/xml-parser.ts → dist/src/shared/xml-parser.js} +72 -63
- package/dist/src/store/active-reply-store.js +41 -0
- package/dist/src/store/interfaces.js +1 -0
- package/dist/src/store/memory-store.js +33 -0
- package/dist/src/store/stream-batch-store.js +319 -0
- package/{src/target.ts → dist/src/target.js} +15 -48
- package/dist/src/transport/agent-api/client.js +168 -0
- package/dist/src/transport/agent-api/core.js +337 -0
- package/dist/src/transport/agent-api/delivery.js +28 -0
- package/dist/src/transport/agent-api/media-upload.js +4 -0
- package/dist/src/transport/agent-api/reply.js +24 -0
- package/dist/src/transport/agent-api/upstream-delivery.js +30 -0
- package/dist/src/transport/agent-api/upstream-media-upload.js +46 -0
- package/dist/src/transport/agent-api/upstream-reply.js +26 -0
- package/dist/src/transport/agent-callback/http-handler.js +30 -0
- package/dist/src/transport/agent-callback/inbound.js +4 -0
- package/dist/src/transport/agent-callback/reply.js +8 -0
- package/dist/src/transport/agent-callback/request-handler.js +189 -0
- package/dist/src/transport/agent-callback/session.js +15 -0
- package/dist/src/transport/bot-webhook/active-reply.js +27 -0
- package/dist/src/transport/bot-webhook/http-handler.js +31 -0
- package/dist/src/transport/bot-webhook/inbound-normalizer.js +496 -0
- package/dist/src/transport/bot-webhook/inbound.js +4 -0
- package/dist/src/transport/bot-webhook/message-shape.js +98 -0
- package/dist/src/transport/bot-webhook/protocol.js +124 -0
- package/dist/src/transport/bot-webhook/reply.js +9 -0
- package/dist/src/transport/bot-webhook/request-handler.js +285 -0
- package/dist/src/transport/bot-webhook/session.js +15 -0
- package/dist/src/transport/bot-ws/inbound.js +147 -0
- package/dist/src/transport/bot-ws/media.js +236 -0
- package/dist/src/transport/bot-ws/reply.js +310 -0
- package/dist/src/transport/bot-ws/sdk-adapter.js +257 -0
- package/dist/src/transport/bot-ws/session.js +15 -0
- package/dist/src/transport/http/common.js +78 -0
- package/dist/src/transport/http/registry.js +71 -0
- package/dist/src/transport/http/request-handler.js +51 -0
- package/{src/transport/index.ts → dist/src/transport/index.js} +2 -10
- package/dist/src/types/account.js +1 -0
- package/dist/src/types/config.js +1 -0
- package/dist/src/types/constants.js +28 -0
- package/dist/src/types/events.js +1 -0
- package/dist/src/types/index.js +1 -0
- package/dist/src/types/legacy-stream.js +1 -0
- package/dist/src/types/message.js +5 -0
- package/dist/src/types/runtime-context.js +1 -0
- package/dist/src/types/runtime.js +1 -0
- package/dist/src/types.js +1 -0
- package/dist/src/upstream/index.js +111 -0
- package/dist/src/wecom_msg_adapter/markdown_adapter.js +280 -0
- package/openclaw.plugin.json +15 -0
- package/package.json +18 -1
- package/.github/workflows/release.yml +0 -143
- package/GOVERNANCE.md +0 -26
- package/SKILLS_CAL.md +0 -895
- package/SKILLS_DOC.md +0 -2288
- package/UPSTREAM_CONFIG.md +0 -170
- package/UPSTREAM_PLAN.md +0 -175
- package/assets/01.bot-add.png +0 -0
- package/assets/01.bot-setp2.png +0 -0
- package/assets/01.image.jpg +0 -0
- package/assets/02.agent.add.png +0 -0
- package/assets/02.agent.api-set.png +0 -0
- package/assets/02.image.jpg +0 -0
- package/assets/03.agent.page.png +0 -0
- package/assets/03.bot.page.png +0 -0
- package/assets/link-me.jpg +0 -0
- package/assets/register.png +0 -0
- package/changelog/v2.2.28.md +0 -70
- package/changelog/v2.3.10.md +0 -17
- package/changelog/v2.3.11.md +0 -19
- package/changelog/v2.3.12.md +0 -25
- package/changelog/v2.3.13.md +0 -19
- package/changelog/v2.3.14.md +0 -48
- package/changelog/v2.3.15.md +0 -15
- package/changelog/v2.3.16.md +0 -11
- package/changelog/v2.3.18.md +0 -22
- package/changelog/v2.3.19.md +0 -73
- package/changelog/v2.3.2.md +0 -28
- package/changelog/v2.3.26.md +0 -21
- package/changelog/v2.3.27.md +0 -33
- package/changelog/v2.3.4.md +0 -20
- package/changelog/v2.3.9.md +0 -22
- package/changelog/v2.4.12.md +0 -37
- package/changelog/v2.4.16.md +0 -19
- package/compat-single-account.md +0 -148
- package/index.test.ts +0 -38
- package/scripts/test-proxy.ts +0 -70
- package/src/accounts.ts +0 -34
- package/src/agent/api-client.upload.test.ts +0 -109
- package/src/agent/handler.event-filter.test.ts +0 -100
- package/src/agent/handler.ts +0 -1105
- package/src/agent/index.ts +0 -12
- package/src/app/account-runtime.ts +0 -276
- package/src/app/bootstrap.ts +0 -29
- package/src/app/index.ts +0 -192
- package/src/capability/agent/delivery-service.ts +0 -87
- package/src/capability/agent/fallback-policy.ts +0 -13
- package/src/capability/agent/ingress-service.ts +0 -38
- package/src/capability/agent/upstream-delivery-service.ts +0 -96
- package/src/capability/bot/dispatch-config.ts +0 -47
- package/src/capability/bot/fallback-delivery.ts +0 -178
- package/src/capability/bot/local-path-delivery.ts +0 -215
- package/src/capability/bot/sandbox-media.test.ts +0 -221
- package/src/capability/bot/sandbox-media.ts +0 -176
- package/src/capability/bot/service.ts +0 -56
- package/src/capability/bot/stream-delivery.ts +0 -379
- package/src/capability/bot/stream-finalizer.ts +0 -120
- package/src/capability/bot/stream-orchestrator.ts +0 -371
- package/src/capability/bot/types.ts +0 -8
- package/src/capability/calendar/SKILLS_CHECKLIST.md +0 -251
- package/src/capability/calendar/tool.ts +0 -417
- package/src/capability/calendar/types.ts +0 -309
- package/src/capability/doc/tool.ts +0 -1629
- package/src/capability/doc/types.ts +0 -792
- package/src/capability/mcp/index.ts +0 -10
- package/src/capability/mcp/schema.ts +0 -107
- package/src/capability/mcp/tool.ts +0 -174
- package/src/capability/mcp/transport.ts +0 -394
- package/src/channel.config.test.ts +0 -147
- package/src/channel.lifecycle.test.ts +0 -255
- package/src/channel.meta.test.ts +0 -26
- package/src/channel.ts +0 -256
- package/src/config/accounts.resolve.test.ts +0 -75
- package/src/config/accounts.ts +0 -296
- package/src/config/derived-paths.test.ts +0 -111
- package/src/config/derived-paths.ts +0 -41
- package/src/config/index.ts +0 -26
- package/src/config/media.test.ts +0 -113
- package/src/config/media.ts +0 -139
- package/src/config/network.ts +0 -53
- package/src/config/routing.test.ts +0 -88
- package/src/config/routing.ts +0 -26
- package/src/config/runtime-config.ts +0 -46
- package/src/config/schema.ts +0 -90
- package/src/context-store.ts +0 -297
- package/src/crypto/index.ts +0 -24
- package/src/crypto.test.ts +0 -32
- package/src/crypto.ts +0 -176
- package/src/domain/models.ts +0 -7
- package/src/domain/policies.ts +0 -36
- package/src/dynamic-agent.account-scope.test.ts +0 -17
- package/src/gateway-monitor.ts +0 -181
- package/src/http.ts +0 -145
- package/src/media.test.ts +0 -82
- package/src/monitor/limits.ts +0 -7
- package/src/monitor/state.queue.test.ts +0 -185
- package/src/monitor/state.ts +0 -34
- package/src/monitor.active.test.ts +0 -245
- package/src/monitor.inbound-filter.test.ts +0 -63
- package/src/monitor.integration.test.ts +0 -208
- package/src/monitor.ts +0 -121
- package/src/monitor.webhook.test.ts +0 -774
- package/src/observability/audit-log.ts +0 -48
- package/src/observability/legacy-operational-event-store.ts +0 -36
- package/src/observability/raw-envelope-log.ts +0 -28
- package/src/observability/status-registry.ts +0 -13
- package/src/observability/transport-session-view.ts +0 -14
- package/src/onboarding.test.ts +0 -336
- package/src/onboarding.ts +0 -704
- package/src/outbound.test.ts +0 -1271
- package/src/outbound.ts +0 -746
- package/src/runtime/dispatcher.ts +0 -71
- package/src/runtime/outbound-intent.ts +0 -4
- package/src/runtime/reply-orchestrator.test.ts +0 -71
- package/src/runtime/reply-orchestrator.ts +0 -67
- package/src/runtime/routing-bridge.test.ts +0 -115
- package/src/runtime/routing-bridge.ts +0 -44
- package/src/runtime/session-manager.test.ts +0 -174
- package/src/runtime/session-manager.ts +0 -139
- package/src/runtime/source-registry.ts +0 -249
- package/src/runtime.ts +0 -14
- package/src/shared/command-auth.ts +0 -87
- package/src/shared/media-asset.ts +0 -78
- package/src/shared/media-service.test.ts +0 -111
- package/src/shared/media-service.ts +0 -84
- package/src/shared/media-types.ts +0 -5
- package/src/shared/xml-parser.test.ts +0 -50
- package/src/store/active-reply-store.ts +0 -42
- package/src/store/interfaces.ts +0 -11
- package/src/store/memory-store.ts +0 -43
- package/src/store/stream-batch-store.ts +0 -350
- package/src/transport/agent-api/client.ts +0 -277
- package/src/transport/agent-api/core.ts +0 -463
- package/src/transport/agent-api/delivery.ts +0 -41
- package/src/transport/agent-api/media-upload.ts +0 -11
- package/src/transport/agent-api/reply.ts +0 -39
- package/src/transport/agent-api/upstream-delivery.ts +0 -45
- package/src/transport/agent-api/upstream-media-upload.ts +0 -70
- package/src/transport/agent-api/upstream-reply.ts +0 -43
- package/src/transport/agent-callback/http-handler.ts +0 -47
- package/src/transport/agent-callback/inbound.ts +0 -5
- package/src/transport/agent-callback/reply.ts +0 -13
- package/src/transport/agent-callback/request-handler.ts +0 -244
- package/src/transport/agent-callback/session.ts +0 -23
- package/src/transport/bot-webhook/active-reply.ts +0 -39
- package/src/transport/bot-webhook/http-handler.ts +0 -48
- package/src/transport/bot-webhook/inbound-normalizer.test.ts +0 -433
- package/src/transport/bot-webhook/inbound-normalizer.ts +0 -558
- package/src/transport/bot-webhook/inbound.ts +0 -5
- package/src/transport/bot-webhook/message-shape.ts +0 -92
- package/src/transport/bot-webhook/protocol.ts +0 -148
- package/src/transport/bot-webhook/reply.ts +0 -15
- package/src/transport/bot-webhook/request-handler.ts +0 -394
- package/src/transport/bot-webhook/session.ts +0 -23
- package/src/transport/bot-ws/inbound.test.ts +0 -290
- package/src/transport/bot-ws/inbound.ts +0 -163
- package/src/transport/bot-ws/media.test.ts +0 -44
- package/src/transport/bot-ws/media.ts +0 -321
- package/src/transport/bot-ws/reply.test.ts +0 -450
- package/src/transport/bot-ws/reply.ts +0 -365
- package/src/transport/bot-ws/sdk-adapter.test.ts +0 -187
- package/src/transport/bot-ws/sdk-adapter.ts +0 -314
- package/src/transport/bot-ws/session.ts +0 -28
- package/src/transport/http/common.ts +0 -109
- package/src/transport/http/registry.ts +0 -92
- package/src/transport/http/request-handler.ts +0 -84
- package/src/types/account.ts +0 -70
- package/src/types/config.ts +0 -114
- package/src/types/constants.ts +0 -31
- package/src/types/events.ts +0 -21
- package/src/types/global.d.ts +0 -9
- package/src/types/index.ts +0 -17
- package/src/types/legacy-stream.ts +0 -50
- package/src/types/message.ts +0 -189
- package/src/types/runtime-context.ts +0 -28
- package/src/types/runtime.ts +0 -165
- package/src/types.ts +0 -41
- package/src/upstream/index.ts +0 -150
- package/src/upstream.test.ts +0 -84
- package/src/wecom_msg_adapter/markdown_adapter.ts +0 -331
- package/tsconfig.json +0 -22
- package/vitest.config.ts +0 -26
- /package/{src/capability/agent/index.ts → dist/src/capability/agent/index.js} +0 -0
- /package/{src/capability/bot/index.ts → dist/src/capability/bot/index.js} +0 -0
- /package/{src/capability/calendar/index.ts → dist/src/capability/calendar/index.js} +0 -0
- /package/{src/capability/index.ts → dist/src/capability/index.js} +0 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context store for WeCom Bot WS proactive push.
|
|
3
|
+
*
|
|
4
|
+
* Similar to Weixin's contextToken mechanism, we need to track:
|
|
5
|
+
* - Which accountId has active sessions with which peerId
|
|
6
|
+
* - The contextToken for routing outbound messages
|
|
7
|
+
*/
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
// Simple logger
|
|
12
|
+
const logger = {
|
|
13
|
+
info: (...args) => console.log('[wecom-context]', ...args),
|
|
14
|
+
warn: (...args) => console.warn('[wecom-context]', ...args),
|
|
15
|
+
debug: (...args) => process.env.DEBUG && console.log('[wecom-context]', ...args),
|
|
16
|
+
};
|
|
17
|
+
// In-memory store: accountId -> peerId -> context info
|
|
18
|
+
const peerContextStore = new Map();
|
|
19
|
+
// Reverse lookup: peerId -> accountId (for routing outbound)
|
|
20
|
+
const peerToAccountMap = new Map();
|
|
21
|
+
const contextTokenToPeerMap = new Map();
|
|
22
|
+
function resolveStateDir() {
|
|
23
|
+
return process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME || "/tmp", ".openclaw");
|
|
24
|
+
}
|
|
25
|
+
function resolveContextFilePath(accountId) {
|
|
26
|
+
return path.join(resolveStateDir(), "wecom", "context", `${accountId}.json`);
|
|
27
|
+
}
|
|
28
|
+
/** Persist peer contexts for an account to disk */
|
|
29
|
+
function persistContexts(accountId) {
|
|
30
|
+
const peerMap = peerContextStore.get(accountId);
|
|
31
|
+
if (!peerMap)
|
|
32
|
+
return;
|
|
33
|
+
const data = {};
|
|
34
|
+
for (const [peerId, info] of peerMap) {
|
|
35
|
+
data[peerId] = info;
|
|
36
|
+
}
|
|
37
|
+
const filePath = resolveContextFilePath(accountId);
|
|
38
|
+
try {
|
|
39
|
+
const dir = path.dirname(filePath);
|
|
40
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
41
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 0), "utf-8");
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
logger.warn?.(`persistContexts: failed to write ${filePath}: ${String(err)}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function normalizeContextToken(value) {
|
|
48
|
+
const token = typeof value === "string" ? value.trim() : "";
|
|
49
|
+
return token || undefined;
|
|
50
|
+
}
|
|
51
|
+
function normalizePeerKind(value) {
|
|
52
|
+
return value === "group" ? "group" : "direct";
|
|
53
|
+
}
|
|
54
|
+
function normalizeOptional(value) {
|
|
55
|
+
const normalized = typeof value === "string" ? value.trim() : "";
|
|
56
|
+
return normalized || undefined;
|
|
57
|
+
}
|
|
58
|
+
function findStoredPeerContext(accountId, peerId) {
|
|
59
|
+
const peerMap = peerContextStore.get(accountId);
|
|
60
|
+
if (!peerMap)
|
|
61
|
+
return undefined;
|
|
62
|
+
const exact = peerMap.get(peerId);
|
|
63
|
+
if (exact)
|
|
64
|
+
return exact;
|
|
65
|
+
const normalizedPeerId = peerId.trim().toLowerCase();
|
|
66
|
+
for (const [storedPeerId, info] of peerMap) {
|
|
67
|
+
if (storedPeerId.trim().toLowerCase() === normalizedPeerId) {
|
|
68
|
+
return info;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
function registerPeerContext(accountId, peerId, info) {
|
|
74
|
+
let peerMap = peerContextStore.get(accountId);
|
|
75
|
+
if (!peerMap) {
|
|
76
|
+
peerMap = new Map();
|
|
77
|
+
peerContextStore.set(accountId, peerMap);
|
|
78
|
+
}
|
|
79
|
+
const previous = peerMap.get(peerId);
|
|
80
|
+
if (previous?.contextToken && previous.contextToken !== info.contextToken) {
|
|
81
|
+
contextTokenToPeerMap.delete(previous.contextToken);
|
|
82
|
+
}
|
|
83
|
+
peerMap.set(peerId, info);
|
|
84
|
+
peerToAccountMap.set(peerId, accountId);
|
|
85
|
+
contextTokenToPeerMap.set(info.contextToken, {
|
|
86
|
+
accountId,
|
|
87
|
+
peerId,
|
|
88
|
+
...info,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function resolveStoredPeerContext(accountId, peerId, params) {
|
|
92
|
+
const existing = findStoredPeerContext(accountId, peerId);
|
|
93
|
+
return {
|
|
94
|
+
contextToken: normalizeContextToken(params.contextToken) ??
|
|
95
|
+
existing?.contextToken ??
|
|
96
|
+
randomUUID(),
|
|
97
|
+
peerKind: params.peerKind ?? existing?.peerKind ?? "direct",
|
|
98
|
+
lastSeen: params.lastSeen ?? Date.now(),
|
|
99
|
+
...(normalizeOptional(params.upstreamCorpId) || existing?.upstreamCorpId
|
|
100
|
+
? { upstreamCorpId: normalizeOptional(params.upstreamCorpId) ?? existing?.upstreamCorpId }
|
|
101
|
+
: {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** Restore persisted peer contexts for an account */
|
|
105
|
+
export function restorePeerContexts(accountId) {
|
|
106
|
+
const filePath = resolveContextFilePath(accountId);
|
|
107
|
+
try {
|
|
108
|
+
if (!fs.existsSync(filePath))
|
|
109
|
+
return;
|
|
110
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
111
|
+
const data = JSON.parse(raw);
|
|
112
|
+
const peerMap = new Map();
|
|
113
|
+
let count = 0;
|
|
114
|
+
let mutated = false;
|
|
115
|
+
for (const [peerId, info] of Object.entries(data)) {
|
|
116
|
+
const normalized = {
|
|
117
|
+
contextToken: normalizeContextToken(info?.contextToken) ?? randomUUID(),
|
|
118
|
+
peerKind: normalizePeerKind(info?.peerKind),
|
|
119
|
+
lastSeen: typeof info?.lastSeen === "number" && Number.isFinite(info.lastSeen)
|
|
120
|
+
? info.lastSeen
|
|
121
|
+
: Date.now(),
|
|
122
|
+
...(normalizeOptional(info?.upstreamCorpId)
|
|
123
|
+
? { upstreamCorpId: normalizeOptional(info?.upstreamCorpId) }
|
|
124
|
+
: {}),
|
|
125
|
+
};
|
|
126
|
+
peerMap.set(peerId, normalized);
|
|
127
|
+
peerToAccountMap.set(peerId, accountId);
|
|
128
|
+
contextTokenToPeerMap.set(normalized.contextToken, {
|
|
129
|
+
accountId,
|
|
130
|
+
peerId,
|
|
131
|
+
...normalized,
|
|
132
|
+
});
|
|
133
|
+
if (normalized.contextToken !== info?.contextToken ||
|
|
134
|
+
normalized.peerKind !== info?.peerKind ||
|
|
135
|
+
normalized.lastSeen !== info?.lastSeen ||
|
|
136
|
+
normalized.upstreamCorpId !== normalizeOptional(info?.upstreamCorpId)) {
|
|
137
|
+
mutated = true;
|
|
138
|
+
}
|
|
139
|
+
count++;
|
|
140
|
+
}
|
|
141
|
+
peerContextStore.set(accountId, peerMap);
|
|
142
|
+
if (mutated) {
|
|
143
|
+
persistContexts(accountId);
|
|
144
|
+
}
|
|
145
|
+
logger.info?.(`restorePeerContexts: restored ${count} peers for account=${accountId}`);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
logger.warn?.(`restorePeerContexts: failed to read ${filePath}: ${String(err)}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** Store context for a peer (called on inbound message) */
|
|
152
|
+
export function setPeerContext(accountId, peerId, options) {
|
|
153
|
+
const resolved = resolveStoredPeerContext(accountId, peerId, options ?? {});
|
|
154
|
+
registerPeerContext(accountId, peerId, resolved);
|
|
155
|
+
// Persist to disk (debounced would be better, but simple for now)
|
|
156
|
+
persistContexts(accountId);
|
|
157
|
+
logger.debug?.(`setPeerContext: accountId=${accountId} peerId=${peerId} token=${resolved.contextToken} kind=${resolved.peerKind}`);
|
|
158
|
+
return resolved.contextToken;
|
|
159
|
+
}
|
|
160
|
+
/** Get the accountId that has an active session with a peer */
|
|
161
|
+
export function getAccountIdByPeer(peerId) {
|
|
162
|
+
return peerToAccountMap.get(peerId);
|
|
163
|
+
}
|
|
164
|
+
/** Get the most recent peerId for an account (for proactive push) */
|
|
165
|
+
export function getRecentPeerForAccount(accountId, maxAgeMs = 30 * 60 * 1000) {
|
|
166
|
+
const peerMap = peerContextStore.get(accountId);
|
|
167
|
+
if (!peerMap)
|
|
168
|
+
return undefined;
|
|
169
|
+
let mostRecent;
|
|
170
|
+
for (const [peerId, info] of peerMap) {
|
|
171
|
+
if (Date.now() - info.lastSeen > maxAgeMs)
|
|
172
|
+
continue;
|
|
173
|
+
if (!mostRecent || info.lastSeen > mostRecent.lastSeen) {
|
|
174
|
+
mostRecent = { peerId, lastSeen: info.lastSeen };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return mostRecent?.peerId;
|
|
178
|
+
}
|
|
179
|
+
/** Get context token for a peer */
|
|
180
|
+
export function getPeerContextToken(accountId, peerId) {
|
|
181
|
+
return findStoredPeerContext(accountId, peerId)?.contextToken;
|
|
182
|
+
}
|
|
183
|
+
export function getPeerUpstreamCorpId(accountId, peerId) {
|
|
184
|
+
return findStoredPeerContext(accountId, peerId)?.upstreamCorpId;
|
|
185
|
+
}
|
|
186
|
+
/** Resolve a peer context from a context token. */
|
|
187
|
+
export function getPeerContextByToken(contextToken) {
|
|
188
|
+
return contextTokenToPeerMap.get(contextToken);
|
|
189
|
+
}
|
|
190
|
+
/** Resolve accountId from a context token. */
|
|
191
|
+
export function getAccountIdByContextToken(contextToken) {
|
|
192
|
+
return contextTokenToPeerMap.get(contextToken)?.accountId;
|
|
193
|
+
}
|
|
194
|
+
/** Check if we have an active session for routing */
|
|
195
|
+
export function hasActiveSession(accountId, peerId, maxAgeMs = 30 * 60 * 1000) {
|
|
196
|
+
const info = findStoredPeerContext(accountId, peerId);
|
|
197
|
+
if (!info)
|
|
198
|
+
return false;
|
|
199
|
+
return Date.now() - info.lastSeen < maxAgeMs;
|
|
200
|
+
}
|
|
201
|
+
/** Clear all contexts for an account */
|
|
202
|
+
export function clearPeerContexts(accountId) {
|
|
203
|
+
const peerMap = peerContextStore.get(accountId);
|
|
204
|
+
if (peerMap) {
|
|
205
|
+
for (const [peerId, info] of peerMap) {
|
|
206
|
+
peerToAccountMap.delete(peerId);
|
|
207
|
+
contextTokenToPeerMap.delete(info.contextToken);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
peerContextStore.delete(accountId);
|
|
211
|
+
const filePath = resolveContextFilePath(accountId);
|
|
212
|
+
try {
|
|
213
|
+
if (fs.existsSync(filePath))
|
|
214
|
+
fs.unlinkSync(filePath);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
logger.warn?.(`clearPeerContexts: failed to remove ${filePath}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
* WeCom AES-256-CBC 加解密核心
|
|
3
3
|
* Bot 和 Agent 模式共用
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
5
|
import crypto from "node:crypto";
|
|
7
6
|
import { CRYPTO } from "../types/constants.js";
|
|
8
|
-
|
|
9
|
-
export function decodeEncodingAESKey(encodingAESKey: string): Buffer {
|
|
7
|
+
export function decodeEncodingAESKey(encodingAESKey) {
|
|
10
8
|
const trimmed = encodingAESKey.trim();
|
|
11
|
-
if (!trimmed)
|
|
9
|
+
if (!trimmed)
|
|
10
|
+
throw new Error("encodingAESKey missing");
|
|
12
11
|
const withPadding = trimmed.endsWith("=") ? trimmed : `${trimmed}=`;
|
|
13
12
|
const key = Buffer.from(withPadding, "base64");
|
|
14
13
|
if (key.length !== CRYPTO.AES_KEY_LENGTH) {
|
|
@@ -16,17 +15,16 @@ export function decodeEncodingAESKey(encodingAESKey: string): Buffer {
|
|
|
16
15
|
}
|
|
17
16
|
return key;
|
|
18
17
|
}
|
|
19
|
-
|
|
20
|
-
function pkcs7Pad(buf: Buffer, blockSize: number): Buffer {
|
|
18
|
+
function pkcs7Pad(buf, blockSize) {
|
|
21
19
|
const mod = buf.length % blockSize;
|
|
22
20
|
const pad = mod === 0 ? blockSize : blockSize - mod;
|
|
23
21
|
const padByte = Buffer.from([pad]);
|
|
24
|
-
return Buffer.concat([buf, Buffer.alloc(pad, padByte[0]
|
|
22
|
+
return Buffer.concat([buf, Buffer.alloc(pad, padByte[0])]);
|
|
25
23
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const pad = buf[buf.length - 1]
|
|
24
|
+
export function pkcs7Unpad(buf, blockSize) {
|
|
25
|
+
if (buf.length === 0)
|
|
26
|
+
throw new Error("invalid pkcs7 payload");
|
|
27
|
+
const pad = buf[buf.length - 1];
|
|
30
28
|
if (pad < 1 || pad > blockSize) {
|
|
31
29
|
throw new Error("invalid pkcs7 padding");
|
|
32
30
|
}
|
|
@@ -40,15 +38,10 @@ export function pkcs7Unpad(buf: Buffer, blockSize: number): Buffer {
|
|
|
40
38
|
}
|
|
41
39
|
return buf.subarray(0, buf.length - pad);
|
|
42
40
|
}
|
|
43
|
-
|
|
44
41
|
/**
|
|
45
42
|
* 解密 WeCom 加密消息
|
|
46
43
|
*/
|
|
47
|
-
export function decryptWecomEncrypted(params
|
|
48
|
-
encodingAESKey: string;
|
|
49
|
-
receiveId?: string;
|
|
50
|
-
encrypt: string;
|
|
51
|
-
}): string {
|
|
44
|
+
export function decryptWecomEncrypted(params) {
|
|
52
45
|
const aesKey = decodeEncodingAESKey(params.encodingAESKey);
|
|
53
46
|
const iv = aesKey.subarray(0, 16);
|
|
54
47
|
const decipher = crypto.createDecipheriv("aes-256-cbc", aesKey, iv);
|
|
@@ -58,11 +51,9 @@ export function decryptWecomEncrypted(params: {
|
|
|
58
51
|
decipher.final(),
|
|
59
52
|
]);
|
|
60
53
|
const decrypted = pkcs7Unpad(decryptedPadded, CRYPTO.PKCS7_BLOCK_SIZE);
|
|
61
|
-
|
|
62
54
|
if (decrypted.length < 20) {
|
|
63
55
|
throw new Error(`invalid payload (expected >=20 bytes, got ${decrypted.length})`);
|
|
64
56
|
}
|
|
65
|
-
|
|
66
57
|
// 16 bytes random + 4 bytes length + msg + receiveId
|
|
67
58
|
const msgLen = decrypted.readUInt32BE(16);
|
|
68
59
|
const msgStart = 20;
|
|
@@ -71,7 +62,6 @@ export function decryptWecomEncrypted(params: {
|
|
|
71
62
|
throw new Error(`invalid msg length (msgEnd=${msgEnd}, total=${decrypted.length})`);
|
|
72
63
|
}
|
|
73
64
|
const msg = decrypted.subarray(msgStart, msgEnd).toString("utf8");
|
|
74
|
-
|
|
75
65
|
const receiveId = params.receiveId ?? "";
|
|
76
66
|
if (receiveId) {
|
|
77
67
|
const trailing = decrypted.subarray(msgEnd).toString("utf8");
|
|
@@ -79,18 +69,12 @@ export function decryptWecomEncrypted(params: {
|
|
|
79
69
|
throw new Error(`receiveId mismatch (expected "${receiveId}", got "${trailing}")`);
|
|
80
70
|
}
|
|
81
71
|
}
|
|
82
|
-
|
|
83
72
|
return msg;
|
|
84
73
|
}
|
|
85
|
-
|
|
86
74
|
/**
|
|
87
75
|
* 加密明文为 WeCom 格式
|
|
88
76
|
*/
|
|
89
|
-
export function encryptWecomPlaintext(params
|
|
90
|
-
encodingAESKey: string;
|
|
91
|
-
receiveId?: string;
|
|
92
|
-
plaintext: string;
|
|
93
|
-
}): string {
|
|
77
|
+
export function encryptWecomPlaintext(params) {
|
|
94
78
|
const aesKey = decodeEncodingAESKey(params.encodingAESKey);
|
|
95
79
|
const iv = aesKey.subarray(0, 16);
|
|
96
80
|
const random16 = crypto.randomBytes(16);
|
|
@@ -98,7 +82,6 @@ export function encryptWecomPlaintext(params: {
|
|
|
98
82
|
const msgLen = Buffer.alloc(4);
|
|
99
83
|
msgLen.writeUInt32BE(msg.length, 0);
|
|
100
84
|
const receiveId = Buffer.from(params.receiveId ?? "", "utf8");
|
|
101
|
-
|
|
102
85
|
const raw = Buffer.concat([random16, msgLen, msg, receiveId]);
|
|
103
86
|
const padded = pkcs7Pad(raw, CRYPTO.PKCS7_BLOCK_SIZE);
|
|
104
87
|
const cipher = crypto.createCipheriv("aes-256-cbc", aesKey, iv);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeCom 加解密模块导出
|
|
3
|
+
*/
|
|
4
|
+
// AES 加解密
|
|
5
|
+
export { decodeEncodingAESKey, pkcs7Unpad, decryptWecomEncrypted, encryptWecomPlaintext, } from "./aes.js";
|
|
6
|
+
// 签名验证
|
|
7
|
+
export { computeWecomMsgSignature, verifyWecomSignature, } from "./signature.js";
|
|
8
|
+
// XML 辅助
|
|
9
|
+
export { extractEncryptFromXml, extractToUserNameFromXml, buildEncryptedXmlResponse, } from "./xml.js";
|
|
@@ -1,38 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WeCom 签名计算与验证
|
|
3
3
|
*/
|
|
4
|
-
|
|
5
4
|
import crypto from "node:crypto";
|
|
6
|
-
|
|
7
|
-
function sha1Hex(input: string): string {
|
|
5
|
+
function sha1Hex(input) {
|
|
8
6
|
return crypto.createHash("sha1").update(input).digest("hex");
|
|
9
7
|
}
|
|
10
|
-
|
|
11
8
|
/**
|
|
12
9
|
* 计算 WeCom 消息签名
|
|
13
10
|
*/
|
|
14
|
-
export function computeWecomMsgSignature(params
|
|
15
|
-
token: string;
|
|
16
|
-
timestamp: string;
|
|
17
|
-
nonce: string;
|
|
18
|
-
encrypt: string;
|
|
19
|
-
}): string {
|
|
11
|
+
export function computeWecomMsgSignature(params) {
|
|
20
12
|
const parts = [params.token, params.timestamp, params.nonce, params.encrypt]
|
|
21
13
|
.map((v) => String(v ?? ""))
|
|
22
14
|
.sort();
|
|
23
15
|
return sha1Hex(parts.join(""));
|
|
24
16
|
}
|
|
25
|
-
|
|
26
17
|
/**
|
|
27
18
|
* 验证 WeCom 消息签名
|
|
28
19
|
*/
|
|
29
|
-
export function verifyWecomSignature(params
|
|
30
|
-
token: string;
|
|
31
|
-
timestamp: string;
|
|
32
|
-
nonce: string;
|
|
33
|
-
encrypt: string;
|
|
34
|
-
signature: string;
|
|
35
|
-
}): boolean {
|
|
20
|
+
export function verifyWecomSignature(params) {
|
|
36
21
|
const expected = computeWecomMsgSignature({
|
|
37
22
|
token: params.token,
|
|
38
23
|
timestamp: params.timestamp,
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
* WeCom XML 加解密辅助函数
|
|
3
3
|
* 用于 Agent 模式处理 XML 格式回调
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
5
|
/**
|
|
7
6
|
* 从 XML 密文中提取 Encrypt 字段
|
|
8
7
|
*/
|
|
9
|
-
export function extractEncryptFromXml(xml
|
|
8
|
+
export function extractEncryptFromXml(xml) {
|
|
10
9
|
const match = /<Encrypt><!\[CDATA\[(.*?)\]\]><\/Encrypt>/s.exec(xml);
|
|
11
10
|
if (!match?.[1]) {
|
|
12
11
|
// 尝试不带 CDATA 的格式
|
|
@@ -18,11 +17,10 @@ export function extractEncryptFromXml(xml: string): string {
|
|
|
18
17
|
}
|
|
19
18
|
return match[1];
|
|
20
19
|
}
|
|
21
|
-
|
|
22
20
|
/**
|
|
23
21
|
* 从 XML 中提取 ToUserName (CorpID)
|
|
24
22
|
*/
|
|
25
|
-
export function extractToUserNameFromXml(xml
|
|
23
|
+
export function extractToUserNameFromXml(xml) {
|
|
26
24
|
const match = /<ToUserName><!\[CDATA\[(.*?)\]\]><\/ToUserName>/s.exec(xml);
|
|
27
25
|
if (!match?.[1]) {
|
|
28
26
|
const altMatch = /<ToUserName>(.*?)<\/ToUserName>/s.exec(xml);
|
|
@@ -30,16 +28,10 @@ export function extractToUserNameFromXml(xml: string): string {
|
|
|
30
28
|
}
|
|
31
29
|
return match[1];
|
|
32
30
|
}
|
|
33
|
-
|
|
34
31
|
/**
|
|
35
32
|
* 构建加密 XML 响应
|
|
36
33
|
*/
|
|
37
|
-
export function buildEncryptedXmlResponse(params
|
|
38
|
-
encrypt: string;
|
|
39
|
-
signature: string;
|
|
40
|
-
timestamp: string;
|
|
41
|
-
nonce: string;
|
|
42
|
-
}): string {
|
|
34
|
+
export function buildEncryptedXmlResponse(params) {
|
|
43
35
|
return `<xml>
|
|
44
36
|
<Encrypt><![CDATA[${params.encrypt}]]></Encrypt>
|
|
45
37
|
<MsgSignature><![CDATA[${params.signature}]]></MsgSignature>
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* **decodeEncodingAESKey (解码 AES Key)**
|
|
4
|
+
*
|
|
5
|
+
* 将企业微信配置的 Base64 编码的 AES Key 解码为 Buffer。
|
|
6
|
+
* 包含补全 Padding 和长度校验 (必须32字节)。
|
|
7
|
+
*/
|
|
8
|
+
export function decodeEncodingAESKey(encodingAESKey) {
|
|
9
|
+
const trimmed = encodingAESKey.trim();
|
|
10
|
+
if (!trimmed)
|
|
11
|
+
throw new Error("encodingAESKey missing");
|
|
12
|
+
const withPadding = trimmed.endsWith("=") ? trimmed : `${trimmed}=`;
|
|
13
|
+
const key = Buffer.from(withPadding, "base64");
|
|
14
|
+
if (key.length !== 32) {
|
|
15
|
+
throw new Error(`invalid encodingAESKey (expected 32 bytes after base64 decode, got ${key.length})`);
|
|
16
|
+
}
|
|
17
|
+
return key;
|
|
18
|
+
}
|
|
19
|
+
// WeCom uses PKCS#7 padding with a block size of 32 bytes (not AES's 16-byte block).
|
|
20
|
+
// This is compatible with AES-CBC as 32 is a multiple of 16, but it requires manual padding/unpadding.
|
|
21
|
+
export const WECOM_PKCS7_BLOCK_SIZE = 32;
|
|
22
|
+
function pkcs7Pad(buf, blockSize) {
|
|
23
|
+
const mod = buf.length % blockSize;
|
|
24
|
+
const pad = mod === 0 ? blockSize : blockSize - mod;
|
|
25
|
+
const padByte = Buffer.from([pad]);
|
|
26
|
+
return Buffer.concat([buf, Buffer.alloc(pad, padByte[0])]);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* **pkcs7Unpad (去除 PKCS#7 填充)**
|
|
30
|
+
*
|
|
31
|
+
* 移除 AES 解密后的 PKCS#7 填充字节。
|
|
32
|
+
* 包含填充合法性校验。
|
|
33
|
+
*/
|
|
34
|
+
export function pkcs7Unpad(buf, blockSize) {
|
|
35
|
+
if (buf.length === 0)
|
|
36
|
+
throw new Error("invalid pkcs7 payload");
|
|
37
|
+
const pad = buf[buf.length - 1];
|
|
38
|
+
if (pad < 1 || pad > blockSize) {
|
|
39
|
+
throw new Error("invalid pkcs7 padding");
|
|
40
|
+
}
|
|
41
|
+
if (pad > buf.length) {
|
|
42
|
+
throw new Error("invalid pkcs7 payload");
|
|
43
|
+
}
|
|
44
|
+
// Best-effort validation (all padding bytes equal).
|
|
45
|
+
for (let i = 0; i < pad; i += 1) {
|
|
46
|
+
if (buf[buf.length - 1 - i] !== pad) {
|
|
47
|
+
throw new Error("invalid pkcs7 padding");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return buf.subarray(0, buf.length - pad);
|
|
51
|
+
}
|
|
52
|
+
function sha1Hex(input) {
|
|
53
|
+
return crypto.createHash("sha1").update(input).digest("hex");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* **computeWecomMsgSignature (计算消息签名)**
|
|
57
|
+
*
|
|
58
|
+
* 算法:sha1(sort(token, timestamp, nonce, encrypt_msg))
|
|
59
|
+
*/
|
|
60
|
+
export function computeWecomMsgSignature(params) {
|
|
61
|
+
const parts = [params.token, params.timestamp, params.nonce, params.encrypt]
|
|
62
|
+
.map((v) => String(v ?? ""))
|
|
63
|
+
.sort();
|
|
64
|
+
return sha1Hex(parts.join(""));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* **verifyWecomSignature (验证消息签名)**
|
|
68
|
+
*
|
|
69
|
+
* 比较计算出的签名与企业微信传入的签名是否一致。
|
|
70
|
+
*/
|
|
71
|
+
export function verifyWecomSignature(params) {
|
|
72
|
+
const expected = computeWecomMsgSignature({
|
|
73
|
+
token: params.token,
|
|
74
|
+
timestamp: params.timestamp,
|
|
75
|
+
nonce: params.nonce,
|
|
76
|
+
encrypt: params.encrypt,
|
|
77
|
+
});
|
|
78
|
+
return expected === params.signature;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* **decryptWecomEncrypted (解密企业微信消息)**
|
|
82
|
+
*
|
|
83
|
+
* 将企业微信的 AES 加密包解密为明文。
|
|
84
|
+
* 流程:
|
|
85
|
+
* 1. Base64 解码 AESKey 并获取 IV (前16字节)。
|
|
86
|
+
* 2. AES-CBC 解密。
|
|
87
|
+
* 3. 去除 PKCS#7 填充。
|
|
88
|
+
* 4. 拆解协议包结构: [16字节随机串][4字节长度][消息体][接收者ID]。
|
|
89
|
+
* 5. 校验接收者ID (ReceiveId)。
|
|
90
|
+
*/
|
|
91
|
+
export function decryptWecomEncrypted(params) {
|
|
92
|
+
const aesKey = decodeEncodingAESKey(params.encodingAESKey);
|
|
93
|
+
const iv = aesKey.subarray(0, 16);
|
|
94
|
+
const decipher = crypto.createDecipheriv("aes-256-cbc", aesKey, iv);
|
|
95
|
+
decipher.setAutoPadding(false);
|
|
96
|
+
const decryptedPadded = Buffer.concat([
|
|
97
|
+
decipher.update(Buffer.from(params.encrypt, "base64")),
|
|
98
|
+
decipher.final(),
|
|
99
|
+
]);
|
|
100
|
+
const decrypted = pkcs7Unpad(decryptedPadded, WECOM_PKCS7_BLOCK_SIZE);
|
|
101
|
+
if (decrypted.length < 20) {
|
|
102
|
+
throw new Error(`invalid decrypted payload (expected at least 20 bytes, got ${decrypted.length})`);
|
|
103
|
+
}
|
|
104
|
+
// 16 bytes random + 4 bytes network-order length + msg + receiveId (optional)
|
|
105
|
+
const msgLen = decrypted.readUInt32BE(16);
|
|
106
|
+
const msgStart = 20;
|
|
107
|
+
const msgEnd = msgStart + msgLen;
|
|
108
|
+
if (msgEnd > decrypted.length) {
|
|
109
|
+
throw new Error(`invalid decrypted msg length (msgEnd=${msgEnd}, payloadLength=${decrypted.length})`);
|
|
110
|
+
}
|
|
111
|
+
const msg = decrypted.subarray(msgStart, msgEnd).toString("utf8");
|
|
112
|
+
const receiveId = params.receiveId ?? "";
|
|
113
|
+
if (receiveId) {
|
|
114
|
+
const trailing = decrypted.subarray(msgEnd).toString("utf8");
|
|
115
|
+
if (trailing !== receiveId) {
|
|
116
|
+
throw new Error(`receiveId mismatch (expected "${receiveId}", got "${trailing}")`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return msg;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* **encryptWecomPlaintext (加密回复消息)**
|
|
123
|
+
*
|
|
124
|
+
* 将明文消息打包为企业微信的加密格式。
|
|
125
|
+
* 流程:
|
|
126
|
+
* 1. 构造协议包: [16字节随机串][4字节长度][消息体][接收者ID]。
|
|
127
|
+
* 2. PKCS#7 填充。
|
|
128
|
+
* 3. AES-CBC 加密。
|
|
129
|
+
* 4. 转 Base64。
|
|
130
|
+
*/
|
|
131
|
+
export function encryptWecomPlaintext(params) {
|
|
132
|
+
const aesKey = decodeEncodingAESKey(params.encodingAESKey);
|
|
133
|
+
const iv = aesKey.subarray(0, 16);
|
|
134
|
+
const random16 = crypto.randomBytes(16);
|
|
135
|
+
const msg = Buffer.from(params.plaintext ?? "", "utf8");
|
|
136
|
+
const msgLen = Buffer.alloc(4);
|
|
137
|
+
msgLen.writeUInt32BE(msg.length, 0);
|
|
138
|
+
const receiveId = Buffer.from(params.receiveId ?? "", "utf8");
|
|
139
|
+
const raw = Buffer.concat([random16, msgLen, msg, receiveId]);
|
|
140
|
+
const padded = pkcs7Pad(raw, WECOM_PKCS7_BLOCK_SIZE);
|
|
141
|
+
const cipher = crypto.createCipheriv("aes-256-cbc", aesKey, iv);
|
|
142
|
+
cipher.setAutoPadding(false);
|
|
143
|
+
const encrypted = Buffer.concat([cipher.update(padded), cipher.final()]);
|
|
144
|
+
return encrypted.toString("base64");
|
|
145
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function assertBotPrimaryTransport(account) {
|
|
2
|
+
if (account.primaryTransport === "ws" && !account.wsConfigured) {
|
|
3
|
+
throw new Error(`WeCom bot account "${account.accountId}" is missing bot.ws credentials.`);
|
|
4
|
+
}
|
|
5
|
+
if (account.primaryTransport === "webhook" && !account.webhookConfigured) {
|
|
6
|
+
throw new Error(`WeCom bot account "${account.accountId}" is missing bot.webhook credentials.`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function buildDedupKey(event) {
|
|
10
|
+
return `${event.accountId}:${event.transport}:${event.messageId}`;
|
|
11
|
+
}
|
|
12
|
+
export function resolveConversationKey(event) {
|
|
13
|
+
const conversation = event.conversation;
|
|
14
|
+
return [event.accountId, conversation.peerKind, conversation.peerId, conversation.senderId].join(":");
|
|
15
|
+
}
|
|
16
|
+
export function normalizeWecomAllowFromEntry(raw) {
|
|
17
|
+
return raw
|
|
18
|
+
.trim()
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/^wecom:/, "")
|
|
21
|
+
.replace(/^user:/, "")
|
|
22
|
+
.replace(/^userid:/, "");
|
|
23
|
+
}
|
|
24
|
+
export function isWecomSenderAllowed(senderUserId, allowFrom) {
|
|
25
|
+
const list = allowFrom.map((entry) => normalizeWecomAllowFromEntry(entry)).filter(Boolean);
|
|
26
|
+
if (list.includes("*"))
|
|
27
|
+
return true;
|
|
28
|
+
const normalizedSender = normalizeWecomAllowFromEntry(senderUserId);
|
|
29
|
+
if (!normalizedSender)
|
|
30
|
+
return false;
|
|
31
|
+
return list.includes(normalizedSender);
|
|
32
|
+
}
|