@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
package/src/context-store.ts
DELETED
|
@@ -1,297 +0,0 @@
|
|
|
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
|
-
|
|
9
|
-
import { randomUUID } from "node:crypto";
|
|
10
|
-
import fs from "node:fs";
|
|
11
|
-
import path from "node:path";
|
|
12
|
-
|
|
13
|
-
// Simple logger
|
|
14
|
-
const logger = {
|
|
15
|
-
info: (...args: unknown[]) => console.log('[wecom-context]', ...args),
|
|
16
|
-
warn: (...args: unknown[]) => console.warn('[wecom-context]', ...args),
|
|
17
|
-
debug: (...args: unknown[]) => process.env.DEBUG && console.log('[wecom-context]', ...args),
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
type PeerKind = "direct" | "group";
|
|
21
|
-
|
|
22
|
-
type StoredPeerContext = {
|
|
23
|
-
contextToken: string;
|
|
24
|
-
peerKind: PeerKind;
|
|
25
|
-
lastSeen: number;
|
|
26
|
-
upstreamCorpId?: string;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
type ResolvedPeerContext = StoredPeerContext & {
|
|
30
|
-
accountId: string;
|
|
31
|
-
peerId: string;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// In-memory store: accountId -> peerId -> context info
|
|
35
|
-
const peerContextStore = new Map<string, Map<string, StoredPeerContext>>();
|
|
36
|
-
|
|
37
|
-
// Reverse lookup: peerId -> accountId (for routing outbound)
|
|
38
|
-
const peerToAccountMap = new Map<string, string>();
|
|
39
|
-
const contextTokenToPeerMap = new Map<string, ResolvedPeerContext>();
|
|
40
|
-
|
|
41
|
-
function resolveStateDir(): string {
|
|
42
|
-
return process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME || "/tmp", ".openclaw");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function resolveContextFilePath(accountId: string): string {
|
|
46
|
-
return path.join(
|
|
47
|
-
resolveStateDir(),
|
|
48
|
-
"wecom",
|
|
49
|
-
"context",
|
|
50
|
-
`${accountId}.json`
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Persist peer contexts for an account to disk */
|
|
55
|
-
function persistContexts(accountId: string): void {
|
|
56
|
-
const peerMap = peerContextStore.get(accountId);
|
|
57
|
-
if (!peerMap) return;
|
|
58
|
-
|
|
59
|
-
const data: Record<string, StoredPeerContext> = {};
|
|
60
|
-
for (const [peerId, info] of peerMap) {
|
|
61
|
-
data[peerId] = info;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const filePath = resolveContextFilePath(accountId);
|
|
65
|
-
try {
|
|
66
|
-
const dir = path.dirname(filePath);
|
|
67
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
68
|
-
fs.writeFileSync(filePath, JSON.stringify(data, null, 0), "utf-8");
|
|
69
|
-
} catch (err) {
|
|
70
|
-
logger.warn?.(`persistContexts: failed to write ${filePath}: ${String(err)}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function normalizeContextToken(value: unknown): string | undefined {
|
|
75
|
-
const token = typeof value === "string" ? value.trim() : "";
|
|
76
|
-
return token || undefined;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function normalizePeerKind(value: unknown): PeerKind {
|
|
80
|
-
return value === "group" ? "group" : "direct";
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function normalizeOptional(value: unknown): string | undefined {
|
|
84
|
-
const normalized = typeof value === "string" ? value.trim() : "";
|
|
85
|
-
return normalized || undefined;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function findStoredPeerContext(accountId: string, peerId: string): StoredPeerContext | undefined {
|
|
89
|
-
const peerMap = peerContextStore.get(accountId);
|
|
90
|
-
if (!peerMap) return undefined;
|
|
91
|
-
|
|
92
|
-
const exact = peerMap.get(peerId);
|
|
93
|
-
if (exact) return exact;
|
|
94
|
-
|
|
95
|
-
const normalizedPeerId = peerId.trim().toLowerCase();
|
|
96
|
-
for (const [storedPeerId, info] of peerMap) {
|
|
97
|
-
if (storedPeerId.trim().toLowerCase() === normalizedPeerId) {
|
|
98
|
-
return info;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return undefined;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function registerPeerContext(accountId: string, peerId: string, info: StoredPeerContext): void {
|
|
106
|
-
let peerMap = peerContextStore.get(accountId);
|
|
107
|
-
if (!peerMap) {
|
|
108
|
-
peerMap = new Map();
|
|
109
|
-
peerContextStore.set(accountId, peerMap);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const previous = peerMap.get(peerId);
|
|
113
|
-
if (previous?.contextToken && previous.contextToken !== info.contextToken) {
|
|
114
|
-
contextTokenToPeerMap.delete(previous.contextToken);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
peerMap.set(peerId, info);
|
|
118
|
-
peerToAccountMap.set(peerId, accountId);
|
|
119
|
-
contextTokenToPeerMap.set(info.contextToken, {
|
|
120
|
-
accountId,
|
|
121
|
-
peerId,
|
|
122
|
-
...info,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function resolveStoredPeerContext(
|
|
127
|
-
accountId: string,
|
|
128
|
-
peerId: string,
|
|
129
|
-
params: {
|
|
130
|
-
contextToken?: string;
|
|
131
|
-
peerKind?: PeerKind;
|
|
132
|
-
lastSeen?: number;
|
|
133
|
-
upstreamCorpId?: string;
|
|
134
|
-
},
|
|
135
|
-
): StoredPeerContext {
|
|
136
|
-
const existing = findStoredPeerContext(accountId, peerId);
|
|
137
|
-
return {
|
|
138
|
-
contextToken:
|
|
139
|
-
normalizeContextToken(params.contextToken) ??
|
|
140
|
-
existing?.contextToken ??
|
|
141
|
-
randomUUID(),
|
|
142
|
-
peerKind: params.peerKind ?? existing?.peerKind ?? "direct",
|
|
143
|
-
lastSeen: params.lastSeen ?? Date.now(),
|
|
144
|
-
...(normalizeOptional(params.upstreamCorpId) || existing?.upstreamCorpId
|
|
145
|
-
? { upstreamCorpId: normalizeOptional(params.upstreamCorpId) ?? existing?.upstreamCorpId }
|
|
146
|
-
: {}),
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Restore persisted peer contexts for an account */
|
|
151
|
-
export function restorePeerContexts(accountId: string): void {
|
|
152
|
-
const filePath = resolveContextFilePath(accountId);
|
|
153
|
-
try {
|
|
154
|
-
if (!fs.existsSync(filePath)) return;
|
|
155
|
-
const raw = fs.readFileSync(filePath, "utf-8");
|
|
156
|
-
const data = JSON.parse(raw) as Record<
|
|
157
|
-
string,
|
|
158
|
-
{
|
|
159
|
-
contextToken?: string;
|
|
160
|
-
peerKind?: string;
|
|
161
|
-
lastSeen?: number;
|
|
162
|
-
upstreamCorpId?: string;
|
|
163
|
-
}
|
|
164
|
-
>;
|
|
165
|
-
|
|
166
|
-
const peerMap = new Map<string, StoredPeerContext>();
|
|
167
|
-
let count = 0;
|
|
168
|
-
let mutated = false;
|
|
169
|
-
for (const [peerId, info] of Object.entries(data)) {
|
|
170
|
-
const normalized: StoredPeerContext = {
|
|
171
|
-
contextToken: normalizeContextToken(info?.contextToken) ?? randomUUID(),
|
|
172
|
-
peerKind: normalizePeerKind(info?.peerKind),
|
|
173
|
-
lastSeen:
|
|
174
|
-
typeof info?.lastSeen === "number" && Number.isFinite(info.lastSeen)
|
|
175
|
-
? info.lastSeen
|
|
176
|
-
: Date.now(),
|
|
177
|
-
...(normalizeOptional(info?.upstreamCorpId)
|
|
178
|
-
? { upstreamCorpId: normalizeOptional(info?.upstreamCorpId) }
|
|
179
|
-
: {}),
|
|
180
|
-
};
|
|
181
|
-
peerMap.set(peerId, normalized);
|
|
182
|
-
peerToAccountMap.set(peerId, accountId);
|
|
183
|
-
contextTokenToPeerMap.set(normalized.contextToken, {
|
|
184
|
-
accountId,
|
|
185
|
-
peerId,
|
|
186
|
-
...normalized,
|
|
187
|
-
});
|
|
188
|
-
if (
|
|
189
|
-
normalized.contextToken !== info?.contextToken ||
|
|
190
|
-
normalized.peerKind !== info?.peerKind ||
|
|
191
|
-
normalized.lastSeen !== info?.lastSeen ||
|
|
192
|
-
normalized.upstreamCorpId !== normalizeOptional(info?.upstreamCorpId)
|
|
193
|
-
) {
|
|
194
|
-
mutated = true;
|
|
195
|
-
}
|
|
196
|
-
count++;
|
|
197
|
-
}
|
|
198
|
-
peerContextStore.set(accountId, peerMap);
|
|
199
|
-
if (mutated) {
|
|
200
|
-
persistContexts(accountId);
|
|
201
|
-
}
|
|
202
|
-
logger.info?.(`restorePeerContexts: restored ${count} peers for account=${accountId}`);
|
|
203
|
-
} catch (err) {
|
|
204
|
-
logger.warn?.(`restorePeerContexts: failed to read ${filePath}: ${String(err)}`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** Store context for a peer (called on inbound message) */
|
|
209
|
-
export function setPeerContext(
|
|
210
|
-
accountId: string,
|
|
211
|
-
peerId: string,
|
|
212
|
-
options?: {
|
|
213
|
-
contextToken?: string;
|
|
214
|
-
peerKind?: PeerKind;
|
|
215
|
-
lastSeen?: number;
|
|
216
|
-
upstreamCorpId?: string;
|
|
217
|
-
},
|
|
218
|
-
): string {
|
|
219
|
-
const resolved = resolveStoredPeerContext(accountId, peerId, options ?? {});
|
|
220
|
-
registerPeerContext(accountId, peerId, resolved);
|
|
221
|
-
|
|
222
|
-
// Persist to disk (debounced would be better, but simple for now)
|
|
223
|
-
persistContexts(accountId);
|
|
224
|
-
|
|
225
|
-
logger.debug?.(
|
|
226
|
-
`setPeerContext: accountId=${accountId} peerId=${peerId} token=${resolved.contextToken} kind=${resolved.peerKind}`,
|
|
227
|
-
);
|
|
228
|
-
return resolved.contextToken;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/** Get the accountId that has an active session with a peer */
|
|
232
|
-
export function getAccountIdByPeer(peerId: string): string | undefined {
|
|
233
|
-
return peerToAccountMap.get(peerId);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/** Get the most recent peerId for an account (for proactive push) */
|
|
237
|
-
export function getRecentPeerForAccount(accountId: string, maxAgeMs = 30 * 60 * 1000): string | undefined {
|
|
238
|
-
const peerMap = peerContextStore.get(accountId);
|
|
239
|
-
if (!peerMap) return undefined;
|
|
240
|
-
|
|
241
|
-
let mostRecent: { peerId: string; lastSeen: number } | undefined;
|
|
242
|
-
|
|
243
|
-
for (const [peerId, info] of peerMap) {
|
|
244
|
-
if (Date.now() - info.lastSeen > maxAgeMs) continue;
|
|
245
|
-
if (!mostRecent || info.lastSeen > mostRecent.lastSeen) {
|
|
246
|
-
mostRecent = { peerId, lastSeen: info.lastSeen };
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return mostRecent?.peerId;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/** Get context token for a peer */
|
|
254
|
-
export function getPeerContextToken(accountId: string, peerId: string): string | undefined {
|
|
255
|
-
return findStoredPeerContext(accountId, peerId)?.contextToken;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export function getPeerUpstreamCorpId(accountId: string, peerId: string): string | undefined {
|
|
259
|
-
return findStoredPeerContext(accountId, peerId)?.upstreamCorpId;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/** Resolve a peer context from a context token. */
|
|
263
|
-
export function getPeerContextByToken(contextToken: string): ResolvedPeerContext | undefined {
|
|
264
|
-
return contextTokenToPeerMap.get(contextToken);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/** Resolve accountId from a context token. */
|
|
268
|
-
export function getAccountIdByContextToken(contextToken: string): string | undefined {
|
|
269
|
-
return contextTokenToPeerMap.get(contextToken)?.accountId;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/** Check if we have an active session for routing */
|
|
273
|
-
export function hasActiveSession(accountId: string, peerId: string, maxAgeMs = 30 * 60 * 1000): boolean {
|
|
274
|
-
const info = findStoredPeerContext(accountId, peerId);
|
|
275
|
-
if (!info) return false;
|
|
276
|
-
|
|
277
|
-
return Date.now() - info.lastSeen < maxAgeMs;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/** Clear all contexts for an account */
|
|
281
|
-
export function clearPeerContexts(accountId: string): void {
|
|
282
|
-
const peerMap = peerContextStore.get(accountId);
|
|
283
|
-
if (peerMap) {
|
|
284
|
-
for (const [peerId, info] of peerMap) {
|
|
285
|
-
peerToAccountMap.delete(peerId);
|
|
286
|
-
contextTokenToPeerMap.delete(info.contextToken);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
peerContextStore.delete(accountId);
|
|
290
|
-
|
|
291
|
-
const filePath = resolveContextFilePath(accountId);
|
|
292
|
-
try {
|
|
293
|
-
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
294
|
-
} catch (err) {
|
|
295
|
-
logger.warn?.(`clearPeerContexts: failed to remove ${filePath}`);
|
|
296
|
-
}
|
|
297
|
-
}
|
package/src/crypto/index.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WeCom 加解密模块导出
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
// AES 加解密
|
|
6
|
-
export {
|
|
7
|
-
decodeEncodingAESKey,
|
|
8
|
-
pkcs7Unpad,
|
|
9
|
-
decryptWecomEncrypted,
|
|
10
|
-
encryptWecomPlaintext,
|
|
11
|
-
} from "./aes.js";
|
|
12
|
-
|
|
13
|
-
// 签名验证
|
|
14
|
-
export {
|
|
15
|
-
computeWecomMsgSignature,
|
|
16
|
-
verifyWecomSignature,
|
|
17
|
-
} from "./signature.js";
|
|
18
|
-
|
|
19
|
-
// XML 辅助
|
|
20
|
-
export {
|
|
21
|
-
extractEncryptFromXml,
|
|
22
|
-
extractToUserNameFromXml,
|
|
23
|
-
buildEncryptedXmlResponse,
|
|
24
|
-
} from "./xml.js";
|
package/src/crypto.test.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { computeWecomMsgSignature, decryptWecomEncrypted, encryptWecomPlaintext } from "./crypto.js";
|
|
4
|
-
|
|
5
|
-
describe("wecom crypto", () => {
|
|
6
|
-
it("round-trips plaintext", () => {
|
|
7
|
-
const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"; // 43 chars base64 (plus '=' padding)
|
|
8
|
-
const plaintext = JSON.stringify({ hello: "world" });
|
|
9
|
-
const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext });
|
|
10
|
-
const decrypted = decryptWecomEncrypted({ encodingAESKey, receiveId: "", encrypt });
|
|
11
|
-
expect(decrypted).toBe(plaintext);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("pads correctly when raw length is a multiple of 32", () => {
|
|
15
|
-
const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
|
|
16
|
-
// raw length = 20 + plaintext.length + receiveId.length; choose plaintext length % 32 === 12
|
|
17
|
-
const plaintext = "x".repeat(12);
|
|
18
|
-
const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext });
|
|
19
|
-
const decrypted = decryptWecomEncrypted({ encodingAESKey, receiveId: "", encrypt });
|
|
20
|
-
expect(decrypted).toBe(plaintext);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("computes sha1 msg signature", () => {
|
|
24
|
-
const sig = computeWecomMsgSignature({
|
|
25
|
-
token: "token",
|
|
26
|
-
timestamp: "123",
|
|
27
|
-
nonce: "456",
|
|
28
|
-
encrypt: "ENCRYPT",
|
|
29
|
-
});
|
|
30
|
-
expect(sig).toMatch(/^[a-f0-9]{40}$/);
|
|
31
|
-
});
|
|
32
|
-
});
|
package/src/crypto.ts
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* **decodeEncodingAESKey (解码 AES Key)**
|
|
5
|
-
*
|
|
6
|
-
* 将企业微信配置的 Base64 编码的 AES Key 解码为 Buffer。
|
|
7
|
-
* 包含补全 Padding 和长度校验 (必须32字节)。
|
|
8
|
-
*/
|
|
9
|
-
export function decodeEncodingAESKey(encodingAESKey: string): Buffer {
|
|
10
|
-
const trimmed = encodingAESKey.trim();
|
|
11
|
-
if (!trimmed) 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
|
-
|
|
20
|
-
// WeCom uses PKCS#7 padding with a block size of 32 bytes (not AES's 16-byte block).
|
|
21
|
-
// This is compatible with AES-CBC as 32 is a multiple of 16, but it requires manual padding/unpadding.
|
|
22
|
-
export const WECOM_PKCS7_BLOCK_SIZE = 32;
|
|
23
|
-
|
|
24
|
-
function pkcs7Pad(buf: Buffer, blockSize: number): Buffer {
|
|
25
|
-
const mod = buf.length % blockSize;
|
|
26
|
-
const pad = mod === 0 ? blockSize : blockSize - mod;
|
|
27
|
-
const padByte = Buffer.from([pad]);
|
|
28
|
-
return Buffer.concat([buf, Buffer.alloc(pad, padByte[0]!)]);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* **pkcs7Unpad (去除 PKCS#7 填充)**
|
|
33
|
-
*
|
|
34
|
-
* 移除 AES 解密后的 PKCS#7 填充字节。
|
|
35
|
-
* 包含填充合法性校验。
|
|
36
|
-
*/
|
|
37
|
-
export function pkcs7Unpad(buf: Buffer, blockSize: number): Buffer {
|
|
38
|
-
if (buf.length === 0) throw new Error("invalid pkcs7 payload");
|
|
39
|
-
const pad = buf[buf.length - 1]!;
|
|
40
|
-
if (pad < 1 || pad > blockSize) {
|
|
41
|
-
throw new Error("invalid pkcs7 padding");
|
|
42
|
-
}
|
|
43
|
-
if (pad > buf.length) {
|
|
44
|
-
throw new Error("invalid pkcs7 payload");
|
|
45
|
-
}
|
|
46
|
-
// Best-effort validation (all padding bytes equal).
|
|
47
|
-
for (let i = 0; i < pad; i += 1) {
|
|
48
|
-
if (buf[buf.length - 1 - i] !== pad) {
|
|
49
|
-
throw new Error("invalid pkcs7 padding");
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return buf.subarray(0, buf.length - pad);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function sha1Hex(input: string): string {
|
|
56
|
-
return crypto.createHash("sha1").update(input).digest("hex");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* **computeWecomMsgSignature (计算消息签名)**
|
|
61
|
-
*
|
|
62
|
-
* 算法:sha1(sort(token, timestamp, nonce, encrypt_msg))
|
|
63
|
-
*/
|
|
64
|
-
export function computeWecomMsgSignature(params: {
|
|
65
|
-
token: string;
|
|
66
|
-
timestamp: string;
|
|
67
|
-
nonce: string;
|
|
68
|
-
encrypt: string;
|
|
69
|
-
}): string {
|
|
70
|
-
const parts = [params.token, params.timestamp, params.nonce, params.encrypt]
|
|
71
|
-
.map((v) => String(v ?? ""))
|
|
72
|
-
.sort();
|
|
73
|
-
return sha1Hex(parts.join(""));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* **verifyWecomSignature (验证消息签名)**
|
|
78
|
-
*
|
|
79
|
-
* 比较计算出的签名与企业微信传入的签名是否一致。
|
|
80
|
-
*/
|
|
81
|
-
export function verifyWecomSignature(params: {
|
|
82
|
-
token: string;
|
|
83
|
-
timestamp: string;
|
|
84
|
-
nonce: string;
|
|
85
|
-
encrypt: string;
|
|
86
|
-
signature: string;
|
|
87
|
-
}): boolean {
|
|
88
|
-
const expected = computeWecomMsgSignature({
|
|
89
|
-
token: params.token,
|
|
90
|
-
timestamp: params.timestamp,
|
|
91
|
-
nonce: params.nonce,
|
|
92
|
-
encrypt: params.encrypt,
|
|
93
|
-
});
|
|
94
|
-
return expected === params.signature;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* **decryptWecomEncrypted (解密企业微信消息)**
|
|
99
|
-
*
|
|
100
|
-
* 将企业微信的 AES 加密包解密为明文。
|
|
101
|
-
* 流程:
|
|
102
|
-
* 1. Base64 解码 AESKey 并获取 IV (前16字节)。
|
|
103
|
-
* 2. AES-CBC 解密。
|
|
104
|
-
* 3. 去除 PKCS#7 填充。
|
|
105
|
-
* 4. 拆解协议包结构: [16字节随机串][4字节长度][消息体][接收者ID]。
|
|
106
|
-
* 5. 校验接收者ID (ReceiveId)。
|
|
107
|
-
*/
|
|
108
|
-
export function decryptWecomEncrypted(params: {
|
|
109
|
-
encodingAESKey: string;
|
|
110
|
-
receiveId?: string;
|
|
111
|
-
encrypt: string;
|
|
112
|
-
}): string {
|
|
113
|
-
const aesKey = decodeEncodingAESKey(params.encodingAESKey);
|
|
114
|
-
const iv = aesKey.subarray(0, 16);
|
|
115
|
-
const decipher = crypto.createDecipheriv("aes-256-cbc", aesKey, iv);
|
|
116
|
-
decipher.setAutoPadding(false);
|
|
117
|
-
const decryptedPadded = Buffer.concat([
|
|
118
|
-
decipher.update(Buffer.from(params.encrypt, "base64")),
|
|
119
|
-
decipher.final(),
|
|
120
|
-
]);
|
|
121
|
-
const decrypted = pkcs7Unpad(decryptedPadded, WECOM_PKCS7_BLOCK_SIZE);
|
|
122
|
-
|
|
123
|
-
if (decrypted.length < 20) {
|
|
124
|
-
throw new Error(`invalid decrypted payload (expected at least 20 bytes, got ${decrypted.length})`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// 16 bytes random + 4 bytes network-order length + msg + receiveId (optional)
|
|
128
|
-
const msgLen = decrypted.readUInt32BE(16);
|
|
129
|
-
const msgStart = 20;
|
|
130
|
-
const msgEnd = msgStart + msgLen;
|
|
131
|
-
if (msgEnd > decrypted.length) {
|
|
132
|
-
throw new Error(`invalid decrypted msg length (msgEnd=${msgEnd}, payloadLength=${decrypted.length})`);
|
|
133
|
-
}
|
|
134
|
-
const msg = decrypted.subarray(msgStart, msgEnd).toString("utf8");
|
|
135
|
-
|
|
136
|
-
const receiveId = params.receiveId ?? "";
|
|
137
|
-
if (receiveId) {
|
|
138
|
-
const trailing = decrypted.subarray(msgEnd).toString("utf8");
|
|
139
|
-
if (trailing !== receiveId) {
|
|
140
|
-
throw new Error(`receiveId mismatch (expected "${receiveId}", got "${trailing}")`);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return msg;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* **encryptWecomPlaintext (加密回复消息)**
|
|
149
|
-
*
|
|
150
|
-
* 将明文消息打包为企业微信的加密格式。
|
|
151
|
-
* 流程:
|
|
152
|
-
* 1. 构造协议包: [16字节随机串][4字节长度][消息体][接收者ID]。
|
|
153
|
-
* 2. PKCS#7 填充。
|
|
154
|
-
* 3. AES-CBC 加密。
|
|
155
|
-
* 4. 转 Base64。
|
|
156
|
-
*/
|
|
157
|
-
export function encryptWecomPlaintext(params: {
|
|
158
|
-
encodingAESKey: string;
|
|
159
|
-
receiveId?: string;
|
|
160
|
-
plaintext: string;
|
|
161
|
-
}): string {
|
|
162
|
-
const aesKey = decodeEncodingAESKey(params.encodingAESKey);
|
|
163
|
-
const iv = aesKey.subarray(0, 16);
|
|
164
|
-
const random16 = crypto.randomBytes(16);
|
|
165
|
-
const msg = Buffer.from(params.plaintext ?? "", "utf8");
|
|
166
|
-
const msgLen = Buffer.alloc(4);
|
|
167
|
-
msgLen.writeUInt32BE(msg.length, 0);
|
|
168
|
-
const receiveId = Buffer.from(params.receiveId ?? "", "utf8");
|
|
169
|
-
|
|
170
|
-
const raw = Buffer.concat([random16, msgLen, msg, receiveId]);
|
|
171
|
-
const padded = pkcs7Pad(raw, WECOM_PKCS7_BLOCK_SIZE);
|
|
172
|
-
const cipher = crypto.createCipheriv("aes-256-cbc", aesKey, iv);
|
|
173
|
-
cipher.setAutoPadding(false);
|
|
174
|
-
const encrypted = Buffer.concat([cipher.update(padded), cipher.final()]);
|
|
175
|
-
return encrypted.toString("base64");
|
|
176
|
-
}
|
package/src/domain/models.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { DeliveryTask, RawFrameReference, ReplyContext, TransportSessionSnapshot, UnifiedInboundEvent } from "../types/index.js";
|
|
2
|
-
|
|
3
|
-
export type WecomConversation = UnifiedInboundEvent["conversation"];
|
|
4
|
-
export type WecomRawEnvelope = RawFrameReference;
|
|
5
|
-
export type WecomReplyContext = ReplyContext;
|
|
6
|
-
export type WecomTransportSession = TransportSessionSnapshot;
|
|
7
|
-
export type WecomDeliveryTask = DeliveryTask;
|
package/src/domain/policies.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import type { ResolvedBotAccount, UnifiedInboundEvent } from "../types/index.js";
|
|
2
|
-
|
|
3
|
-
export function assertBotPrimaryTransport(account: ResolvedBotAccount): void {
|
|
4
|
-
if (account.primaryTransport === "ws" && !account.wsConfigured) {
|
|
5
|
-
throw new Error(`WeCom bot account "${account.accountId}" is missing bot.ws credentials.`);
|
|
6
|
-
}
|
|
7
|
-
if (account.primaryTransport === "webhook" && !account.webhookConfigured) {
|
|
8
|
-
throw new Error(`WeCom bot account "${account.accountId}" is missing bot.webhook credentials.`);
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function buildDedupKey(event: UnifiedInboundEvent): string {
|
|
13
|
-
return `${event.accountId}:${event.transport}:${event.messageId}`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function resolveConversationKey(event: UnifiedInboundEvent): string {
|
|
17
|
-
const conversation = event.conversation;
|
|
18
|
-
return [event.accountId, conversation.peerKind, conversation.peerId, conversation.senderId].join(":");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function normalizeWecomAllowFromEntry(raw: string): string {
|
|
22
|
-
return raw
|
|
23
|
-
.trim()
|
|
24
|
-
.toLowerCase()
|
|
25
|
-
.replace(/^wecom:/, "")
|
|
26
|
-
.replace(/^user:/, "")
|
|
27
|
-
.replace(/^userid:/, "");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function isWecomSenderAllowed(senderUserId: string, allowFrom: string[]): boolean {
|
|
31
|
-
const list = allowFrom.map((entry) => normalizeWecomAllowFromEntry(entry)).filter(Boolean);
|
|
32
|
-
if (list.includes("*")) return true;
|
|
33
|
-
const normalizedSender = normalizeWecomAllowFromEntry(senderUserId);
|
|
34
|
-
if (!normalizedSender) return false;
|
|
35
|
-
return list.includes(normalizedSender);
|
|
36
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { generateAgentId } from "./dynamic-agent.js";
|
|
4
|
-
|
|
5
|
-
describe("generateAgentId account scoping", () => {
|
|
6
|
-
it("generates different ids for same peer across accounts", () => {
|
|
7
|
-
const a = generateAgentId("dm", "zhangsan", "acct-a");
|
|
8
|
-
const b = generateAgentId("dm", "zhangsan", "acct-b");
|
|
9
|
-
expect(a).toBe("wecom-acct-a-dm-zhangsan");
|
|
10
|
-
expect(b).toBe("wecom-acct-b-dm-zhangsan");
|
|
11
|
-
expect(a).not.toBe(b);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("falls back to default account scope when accountId is omitted", () => {
|
|
15
|
-
expect(generateAgentId("group", "wr123456")).toBe("wecom-default-group-wr123456");
|
|
16
|
-
});
|
|
17
|
-
});
|