@yanhaidao/wecom 2.3.4 → 2.3.9

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.
Files changed (108) hide show
  1. package/README.md +213 -339
  2. package/assets/03.bot.page.png +0 -0
  3. package/changelog/v2.3.9.md +22 -0
  4. package/compat-single-account.md +32 -2
  5. package/index.ts +5 -5
  6. package/package.json +8 -7
  7. package/src/agent/api-client.upload.test.ts +1 -2
  8. package/src/agent/handler.ts +82 -9
  9. package/src/agent/index.ts +1 -1
  10. package/src/app/account-runtime.ts +245 -0
  11. package/src/app/bootstrap.ts +29 -0
  12. package/src/app/index.ts +31 -0
  13. package/src/capability/agent/delivery-service.ts +79 -0
  14. package/src/capability/agent/fallback-policy.ts +13 -0
  15. package/src/capability/agent/index.ts +3 -0
  16. package/src/capability/agent/ingress-service.ts +38 -0
  17. package/src/capability/bot/dispatch-config.ts +47 -0
  18. package/src/capability/bot/fallback-delivery.ts +178 -0
  19. package/src/capability/bot/index.ts +1 -0
  20. package/src/capability/bot/local-path-delivery.ts +215 -0
  21. package/src/capability/bot/service.ts +56 -0
  22. package/src/capability/bot/stream-delivery.ts +379 -0
  23. package/src/capability/bot/stream-finalizer.ts +120 -0
  24. package/src/capability/bot/stream-orchestrator.ts +352 -0
  25. package/src/capability/bot/types.ts +8 -0
  26. package/src/capability/index.ts +2 -0
  27. package/src/channel.lifecycle.test.ts +9 -6
  28. package/src/channel.meta.test.ts +12 -0
  29. package/src/channel.ts +48 -21
  30. package/src/config/accounts.ts +223 -283
  31. package/src/config/derived-paths.test.ts +111 -0
  32. package/src/config/derived-paths.ts +41 -0
  33. package/src/config/index.ts +10 -12
  34. package/src/config/runtime-config.ts +46 -0
  35. package/src/config/schema.ts +59 -102
  36. package/src/domain/models.ts +7 -0
  37. package/src/domain/policies.ts +36 -0
  38. package/src/dynamic-agent.ts +6 -0
  39. package/src/gateway-monitor.ts +43 -93
  40. package/src/http.ts +23 -2
  41. package/src/monitor/limits.ts +7 -0
  42. package/src/monitor/state.ts +28 -508
  43. package/src/monitor.active.test.ts +3 -3
  44. package/src/monitor.integration.test.ts +0 -1
  45. package/src/monitor.ts +64 -2603
  46. package/src/monitor.webhook.test.ts +127 -42
  47. package/src/observability/audit-log.ts +48 -0
  48. package/src/observability/legacy-operational-event-store.ts +36 -0
  49. package/src/observability/raw-envelope-log.ts +28 -0
  50. package/src/observability/status-registry.ts +13 -0
  51. package/src/observability/transport-session-view.ts +14 -0
  52. package/src/onboarding.test.ts +219 -0
  53. package/src/onboarding.ts +88 -71
  54. package/src/outbound.test.ts +5 -5
  55. package/src/outbound.ts +18 -66
  56. package/src/runtime/dispatcher.ts +52 -0
  57. package/src/runtime/index.ts +4 -0
  58. package/src/runtime/outbound-intent.ts +4 -0
  59. package/src/runtime/reply-orchestrator.test.ts +38 -0
  60. package/src/runtime/reply-orchestrator.ts +55 -0
  61. package/src/runtime/routing-bridge.ts +19 -0
  62. package/src/runtime/session-manager.ts +76 -0
  63. package/src/runtime.ts +7 -14
  64. package/src/shared/command-auth.ts +1 -17
  65. package/src/shared/media-service.ts +36 -0
  66. package/src/shared/media-types.ts +5 -0
  67. package/src/store/active-reply-store.ts +42 -0
  68. package/src/store/interfaces.ts +11 -0
  69. package/src/store/memory-store.ts +43 -0
  70. package/src/store/stream-batch-store.ts +350 -0
  71. package/src/target.ts +28 -0
  72. package/src/transport/agent-api/client.ts +44 -0
  73. package/src/transport/agent-api/core.ts +367 -0
  74. package/src/transport/agent-api/delivery.ts +41 -0
  75. package/src/transport/agent-api/media-upload.ts +11 -0
  76. package/src/transport/agent-api/reply.ts +39 -0
  77. package/src/transport/agent-callback/http-handler.ts +47 -0
  78. package/src/transport/agent-callback/inbound.ts +5 -0
  79. package/src/transport/agent-callback/reply.ts +13 -0
  80. package/src/transport/agent-callback/request-handler.ts +244 -0
  81. package/src/transport/agent-callback/session.ts +23 -0
  82. package/src/transport/bot-webhook/active-reply.ts +36 -0
  83. package/src/transport/bot-webhook/http-handler.ts +48 -0
  84. package/src/transport/bot-webhook/inbound-normalizer.ts +371 -0
  85. package/src/transport/bot-webhook/inbound.ts +5 -0
  86. package/src/transport/bot-webhook/message-shape.ts +89 -0
  87. package/src/transport/bot-webhook/protocol.ts +148 -0
  88. package/src/transport/bot-webhook/reply.ts +15 -0
  89. package/src/transport/bot-webhook/request-handler.ts +394 -0
  90. package/src/transport/bot-webhook/session.ts +23 -0
  91. package/src/transport/bot-ws/inbound.ts +109 -0
  92. package/src/transport/bot-ws/reply.ts +48 -0
  93. package/src/transport/bot-ws/sdk-adapter.ts +180 -0
  94. package/src/transport/bot-ws/session.ts +28 -0
  95. package/src/transport/http/common.ts +109 -0
  96. package/src/transport/http/registry.ts +92 -0
  97. package/src/transport/http/request-handler.ts +84 -0
  98. package/src/transport/index.ts +14 -0
  99. package/src/types/account.ts +56 -91
  100. package/src/types/config.ts +59 -112
  101. package/src/types/constants.ts +20 -35
  102. package/src/types/events.ts +21 -0
  103. package/src/types/index.ts +14 -38
  104. package/src/types/legacy-stream.ts +50 -0
  105. package/src/types/runtime-context.ts +28 -0
  106. package/src/types/runtime.ts +161 -0
  107. package/src/agent/api-client.ts +0 -383
  108. package/src/monitor/types.ts +0 -136
@@ -0,0 +1,244 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+
3
+ import { decryptWecomEncrypted, verifyWecomSignature } from "../../crypto.js";
4
+ import { extractEncryptFromXml } from "../../crypto/xml.js";
5
+ import { getWecomRuntime } from "../../runtime.js";
6
+ import { handleAgentWebhook } from "../../agent/index.js";
7
+ import { extractAgentId, parseXml } from "../../shared/xml-parser.js";
8
+ import { LIMITS as WECOM_LIMITS } from "../../types/constants.js";
9
+ import type { AgentWebhookTarget } from "../http/registry.js";
10
+ import {
11
+ logRouteFailure,
12
+ readTextBody,
13
+ resolveQueryParams,
14
+ resolveSignatureParam,
15
+ type RouteFailureReason,
16
+ writeRouteFailure,
17
+ } from "../http/common.js";
18
+
19
+ const ERROR_HELP = "\n\n遇到问题?联系作者: YanHaidao (微信: YanHaidao)";
20
+
21
+ function truncateForLog(raw: string, maxChars = 600): string {
22
+ const compact = raw.replace(/\s+/g, " ").trim();
23
+ if (compact.length <= maxChars) return compact;
24
+ return `${compact.slice(0, maxChars)}...(truncated)`;
25
+ }
26
+
27
+ function buildParsedAgentSummary(parsed: ReturnType<typeof parseXml>): string {
28
+ const data = parsed as Record<string, unknown>;
29
+ const msgType = String(data.MsgType ?? "").trim() || "unknown";
30
+ const fromUser = String(data.FromUserName ?? "").trim() || "N/A";
31
+ const toUser = String(data.ToUserName ?? "").trim() || "N/A";
32
+ const event = String(data.Event ?? "").trim() || "N/A";
33
+ const msgId = String(data.MsgId ?? "").trim() || "N/A";
34
+ const chatId = String(data.ChatId ?? data.chatid ?? "").trim() || "N/A";
35
+ const agentId = String(data.AgentID ?? "").trim() || "N/A";
36
+ return `msgType=${msgType} from=${fromUser} to=${toUser} event=${event} msgId=${msgId} chatId=${chatId} agentId=${agentId}`;
37
+ }
38
+
39
+ function normalizeAgentIdValue(value: unknown): number | undefined {
40
+ if (typeof value === "number" && Number.isFinite(value)) return value;
41
+ const raw = String(value ?? "").trim();
42
+ if (!raw) return undefined;
43
+ const parsed = Number(raw);
44
+ return Number.isFinite(parsed) ? parsed : undefined;
45
+ }
46
+
47
+ export async function handleAgentCallbackRequest(params: {
48
+ req: IncomingMessage;
49
+ res: ServerResponse;
50
+ path: string;
51
+ reqId: string;
52
+ targets: AgentWebhookTarget[];
53
+ }): Promise<boolean> {
54
+ const { req, res, path, reqId, targets } = params;
55
+ if (targets.length === 0) {
56
+ console.error(
57
+ `[wecom] inbound(agent): reqId=${reqId} path=${path} no_registered_target availableTargets=0`,
58
+ );
59
+ res.statusCode = 404;
60
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
61
+ res.end(`agent not configured for path=${path} - Agent 模式未配置或回调路径错误,请运行 openclaw onboarding${ERROR_HELP}`);
62
+ return true;
63
+ }
64
+
65
+ const query = resolveQueryParams(req);
66
+ const timestamp = query.get("timestamp") ?? "";
67
+ const nonce = query.get("nonce") ?? "";
68
+ const signature = resolveSignatureParam(query);
69
+ const hasSig = Boolean(signature);
70
+ const remote = req.socket?.remoteAddress ?? "unknown";
71
+
72
+ if (req.method === "GET") {
73
+ const echostr = query.get("echostr") ?? "";
74
+ const signatureMatches = targets.filter((target) =>
75
+ verifyWecomSignature({
76
+ token: target.agent.token,
77
+ timestamp,
78
+ nonce,
79
+ encrypt: echostr,
80
+ signature,
81
+ }),
82
+ );
83
+ if (signatureMatches.length !== 1) {
84
+ const reason: RouteFailureReason =
85
+ signatureMatches.length === 0 ? "wecom_account_not_found" : "wecom_account_conflict";
86
+ const candidateIds = (signatureMatches.length > 0 ? signatureMatches : targets).map((target) => target.agent.accountId);
87
+ logRouteFailure({
88
+ reqId,
89
+ path,
90
+ method: "GET",
91
+ reason,
92
+ candidateAccountIds: candidateIds,
93
+ });
94
+ writeRouteFailure(
95
+ res,
96
+ reason,
97
+ reason === "wecom_account_conflict"
98
+ ? "Agent callback account conflict: multiple accounts matched signature."
99
+ : "Agent callback account not found: signature verification failed.",
100
+ );
101
+ return true;
102
+ }
103
+ const selected = signatureMatches[0]!;
104
+ try {
105
+ const plain = decryptWecomEncrypted({
106
+ encodingAESKey: selected.agent.encodingAESKey,
107
+ receiveId: selected.agent.corpId,
108
+ encrypt: echostr,
109
+ });
110
+ res.statusCode = 200;
111
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
112
+ res.end(plain);
113
+ return true;
114
+ } catch {
115
+ res.statusCode = 400;
116
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
117
+ res.end(`decrypt failed - 解密失败,请检查 EncodingAESKey${ERROR_HELP}`);
118
+ return true;
119
+ }
120
+ }
121
+
122
+ if (req.method !== "POST") {
123
+ return false;
124
+ }
125
+
126
+ const rawBody = await readTextBody(req, WECOM_LIMITS.MAX_REQUEST_BODY_SIZE);
127
+ if (!rawBody.ok) {
128
+ res.statusCode = 400;
129
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
130
+ res.end(rawBody.error || "invalid payload");
131
+ return true;
132
+ }
133
+
134
+ console.log(
135
+ `[wecom] inbound(agent): reqId=${reqId} path=${path} rawXmlBytes=${Buffer.byteLength(rawBody.value, "utf8")} rawPreview=${JSON.stringify(truncateForLog(rawBody.value))}`,
136
+ );
137
+
138
+ let encrypted = "";
139
+ try {
140
+ encrypted = extractEncryptFromXml(rawBody.value);
141
+ } catch {
142
+ res.statusCode = 400;
143
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
144
+ res.end(`invalid xml - 缺少 Encrypt 字段${ERROR_HELP}`);
145
+ return true;
146
+ }
147
+
148
+ console.log(
149
+ `[wecom] inbound(agent): reqId=${reqId} path=${path} encryptedLen=${encrypted.length}`,
150
+ );
151
+
152
+ const signatureMatches = targets.filter((target) =>
153
+ verifyWecomSignature({
154
+ token: target.agent.token,
155
+ timestamp,
156
+ nonce,
157
+ encrypt: encrypted,
158
+ signature,
159
+ }),
160
+ );
161
+ if (signatureMatches.length !== 1) {
162
+ const reason: RouteFailureReason =
163
+ signatureMatches.length === 0 ? "wecom_account_not_found" : "wecom_account_conflict";
164
+ const candidateIds = (signatureMatches.length > 0 ? signatureMatches : targets).map((target) => target.agent.accountId);
165
+ logRouteFailure({
166
+ reqId,
167
+ path,
168
+ method: "POST",
169
+ reason,
170
+ candidateAccountIds: candidateIds,
171
+ });
172
+ writeRouteFailure(
173
+ res,
174
+ reason,
175
+ reason === "wecom_account_conflict"
176
+ ? "Agent callback account conflict: multiple accounts matched signature."
177
+ : "Agent callback account not found: signature verification failed.",
178
+ );
179
+ return true;
180
+ }
181
+
182
+ const selected = signatureMatches[0]!;
183
+ let decrypted = "";
184
+ let parsed: ReturnType<typeof parseXml> | null = null;
185
+ try {
186
+ decrypted = decryptWecomEncrypted({
187
+ encodingAESKey: selected.agent.encodingAESKey,
188
+ receiveId: selected.agent.corpId,
189
+ encrypt: encrypted,
190
+ });
191
+ parsed = parseXml(decrypted);
192
+ } catch {
193
+ res.statusCode = 400;
194
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
195
+ res.end(`decrypt failed - 解密失败,请检查 EncodingAESKey${ERROR_HELP}`);
196
+ return true;
197
+ }
198
+ if (!parsed) {
199
+ res.statusCode = 400;
200
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
201
+ res.end(`invalid xml - XML 解析失败${ERROR_HELP}`);
202
+ return true;
203
+ }
204
+
205
+ selected.runtimeEnv.log?.(
206
+ `[wecom] inbound(agent): reqId=${reqId} accountId=${selected.agent.accountId} decryptedBytes=${Buffer.byteLength(decrypted, "utf8")} parsed=${buildParsedAgentSummary(parsed)} decryptedPreview=${JSON.stringify(truncateForLog(decrypted))}`,
207
+ );
208
+
209
+ const inboundAgentId = normalizeAgentIdValue(extractAgentId(parsed));
210
+ if (
211
+ inboundAgentId !== undefined &&
212
+ selected.agent.agentId !== undefined &&
213
+ inboundAgentId !== selected.agent.agentId
214
+ ) {
215
+ selected.runtimeEnv.error?.(
216
+ `[wecom] inbound(agent): reqId=${reqId} accountId=${selected.agent.accountId} agentId_mismatch expected=${selected.agent.agentId} actual=${inboundAgentId}`,
217
+ );
218
+ }
219
+
220
+ const core = getWecomRuntime();
221
+ selected.runtimeEnv.log?.(
222
+ `[wecom] inbound(agent): reqId=${reqId} method=${req.method ?? "UNKNOWN"} remote=${remote} timestamp=${timestamp ? "yes" : "no"} nonce=${nonce ? "yes" : "no"} msg_signature=${hasSig ? "yes" : "no"} accountId=${selected.agent.accountId}`,
223
+ );
224
+ selected.touchTransportSession?.({ lastInboundAt: Date.now(), running: true });
225
+ return handleAgentWebhook({
226
+ req,
227
+ res,
228
+ verifiedPost: {
229
+ timestamp,
230
+ nonce,
231
+ signature,
232
+ encrypted,
233
+ decrypted,
234
+ parsed,
235
+ },
236
+ agent: selected.agent,
237
+ config: selected.config,
238
+ core,
239
+ log: selected.runtimeEnv.log,
240
+ error: selected.runtimeEnv.error,
241
+ auditSink: selected.auditSink,
242
+ touchTransportSession: selected.touchTransportSession,
243
+ });
244
+ }
@@ -0,0 +1,23 @@
1
+ import type { TransportSessionSnapshot } from "../../types/index.js";
2
+
3
+ export function createAgentCallbackSessionSnapshot(params: {
4
+ accountId: string;
5
+ running: boolean;
6
+ lastInboundAt?: number;
7
+ lastOutboundAt?: number;
8
+ lastError?: string;
9
+ }): TransportSessionSnapshot {
10
+ return {
11
+ accountId: params.accountId,
12
+ transport: "agent-callback",
13
+ running: params.running,
14
+ ownerId: `${params.accountId}:agent-callback`,
15
+ connected: params.running,
16
+ authenticated: true,
17
+ lastConnectedAt: params.running ? Date.now() : undefined,
18
+ lastDisconnectedAt: params.running ? undefined : Date.now(),
19
+ lastInboundAt: params.lastInboundAt,
20
+ lastOutboundAt: params.lastOutboundAt,
21
+ lastError: params.lastError,
22
+ };
23
+ }
@@ -0,0 +1,36 @@
1
+ import { wecomFetch } from "../../http.js";
2
+ import { LIMITS, monitorState } from "../../monitor/state.js";
3
+
4
+ const activeReplyStore = monitorState.activeReplyStore;
5
+
6
+ export function storeActiveReply(streamId: string, responseUrl?: string, proxyUrl?: string): void {
7
+ activeReplyStore.store(streamId, responseUrl, proxyUrl);
8
+ }
9
+
10
+ export function getActiveReplyUrl(streamId: string): string | undefined {
11
+ return activeReplyStore.getUrl(streamId);
12
+ }
13
+
14
+ export async function useActiveReplyOnce(
15
+ streamId: string,
16
+ fn: (params: { responseUrl: string; proxyUrl?: string }) => Promise<void>,
17
+ ): Promise<void> {
18
+ return activeReplyStore.use(streamId, fn);
19
+ }
20
+
21
+ export async function sendActiveMessage(streamId: string, content: string): Promise<void> {
22
+ await useActiveReplyOnce(streamId, async ({ responseUrl, proxyUrl }) => {
23
+ const res = await wecomFetch(
24
+ responseUrl,
25
+ {
26
+ method: "POST",
27
+ headers: { "Content-Type": "application/json" },
28
+ body: JSON.stringify({ msgtype: "text", text: { content } }),
29
+ },
30
+ { proxyUrl, timeoutMs: LIMITS.REQUEST_TIMEOUT_MS },
31
+ );
32
+ if (!res.ok) {
33
+ throw new Error(`active send failed: ${res.status}`);
34
+ }
35
+ });
36
+ }
@@ -0,0 +1,48 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
2
+
3
+ import type { WecomRuntimeEnv } from "../../types/runtime-context.js";
4
+ import type { ResolvedBotAccount } from "../../types/index.js";
5
+ import type { WecomAccountRuntime } from "../../app/account-runtime.js";
6
+ import { resolveBotWebhookPaths } from "./inbound.js";
7
+ import { createBotWebhookSessionSnapshot } from "./session.js";
8
+ import { registerWecomWebhookTarget } from "../http/registry.js";
9
+
10
+ export function startBotWebhookTransport(params: {
11
+ account: ResolvedBotAccount;
12
+ cfg: OpenClawConfig;
13
+ runtime: WecomAccountRuntime;
14
+ runtimeEnv: WecomRuntimeEnv;
15
+ }): { paths: string[]; stop: () => void } {
16
+ const paths = resolveBotWebhookPaths(params.account.accountId);
17
+ params.runtime.updateTransportSession(
18
+ createBotWebhookSessionSnapshot({
19
+ accountId: params.account.accountId,
20
+ running: true,
21
+ }),
22
+ );
23
+ const unregisters = paths.map((path) =>
24
+ registerWecomWebhookTarget({
25
+ account: params.account,
26
+ config: params.cfg,
27
+ runtime: params.runtimeEnv,
28
+ core: params.runtime.core,
29
+ path,
30
+ touchTransportSession: (patch) => params.runtime.touchTransportSession("bot-webhook", patch),
31
+ auditSink: (event) => params.runtime.recordOperationalIssue(event),
32
+ }),
33
+ );
34
+ return {
35
+ paths,
36
+ stop: () => {
37
+ for (const unregister of unregisters) {
38
+ unregister();
39
+ }
40
+ params.runtime.updateTransportSession(
41
+ createBotWebhookSessionSnapshot({
42
+ accountId: params.account.accountId,
43
+ running: false,
44
+ }),
45
+ );
46
+ },
47
+ };
48
+ }