@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
@@ -1,383 +0,0 @@
1
- /**
2
- * WeCom Agent API 客户端
3
- * 管理 AccessToken 缓存和 API 调用
4
- */
5
-
6
- import crypto from "node:crypto";
7
- import { API_ENDPOINTS, LIMITS } from "../types/constants.js";
8
- import type { ResolvedAgentAccount } from "../types/index.js";
9
- import { readResponseBodyAsBuffer, wecomFetch } from "../http.js";
10
- import { resolveWecomEgressProxyUrlFromNetwork } from "../config/index.js";
11
-
12
- /**
13
- * **TokenCache (AccessToken 缓存结构)**
14
- *
15
- * 用于缓存企业微信 API 调用所需的 AccessToken。
16
- * @property token 缓存的 Token 字符串
17
- * @property expiresAt 过期时间戳 (ms)
18
- * @property refreshPromise 当前正在进行的刷新 Promise (防止并发刷新)
19
- */
20
- type TokenCache = {
21
- token: string;
22
- expiresAt: number;
23
- refreshPromise: Promise<string> | null;
24
- };
25
-
26
- const tokenCaches = new Map<string, TokenCache>();
27
-
28
- function normalizeUploadFilename(filename: string): string {
29
- const trimmed = filename.trim();
30
- if (!trimmed) return "file.bin";
31
- const ext = trimmed.includes(".") ? `.${trimmed.split(".").pop()!.toLowerCase()}` : "";
32
- const base = ext ? trimmed.slice(0, -ext.length) : trimmed;
33
- const sanitizedBase = base
34
- .replace(/[^\x20-\x7e]/g, "_")
35
- .replace(/["\\\/;=]/g, "_")
36
- .replace(/\s+/g, "_")
37
- .replace(/_+/g, "_")
38
- .replace(/^_+|_+$/g, "");
39
- const safeBase = sanitizedBase || "file";
40
- const safeExt = ext.replace(/[^a-z0-9.]/g, "");
41
- return `${safeBase}${safeExt || ".bin"}`;
42
- }
43
-
44
- function guessUploadContentType(filename: string): string {
45
- const ext = filename.split(".").pop()?.toLowerCase() || "";
46
- const contentTypeMap: Record<string, string> = {
47
- // image
48
- jpg: "image/jpg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp", bmp: "image/bmp",
49
- // audio / video
50
- amr: "voice/amr", mp3: "audio/mpeg", wav: "audio/wav", m4a: "audio/mp4", ogg: "audio/ogg", mp4: "video/mp4", mov: "video/quicktime",
51
- // documents
52
- txt: "text/plain", md: "text/markdown", csv: "text/csv", tsv: "text/tab-separated-values", json: "application/json",
53
- xml: "application/xml", yaml: "application/yaml", yml: "application/yaml",
54
- pdf: "application/pdf", doc: "application/msword", docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
55
- xls: "application/vnd.ms-excel", xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
56
- ppt: "application/vnd.ms-powerpoint", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
57
- rtf: "application/rtf", odt: "application/vnd.oasis.opendocument.text",
58
- // archives
59
- zip: "application/zip", rar: "application/vnd.rar", "7z": "application/x-7z-compressed",
60
- gz: "application/gzip", tgz: "application/gzip", tar: "application/x-tar",
61
- };
62
- return contentTypeMap[ext] || "application/octet-stream";
63
- }
64
-
65
- function requireAgentId(agent: ResolvedAgentAccount): number {
66
- if (typeof agent.agentId === "number" && Number.isFinite(agent.agentId)) return agent.agentId;
67
- throw new Error(`wecom agent account=${agent.accountId} missing agentId; sending via cgi-bin/message/send requires agentId`);
68
- }
69
-
70
- /**
71
- * **getAccessToken (获取 AccessToken)**
72
- *
73
- * 获取企业微信 API 调用所需的 AccessToken。
74
- * 具备自动缓存和过期刷新机制。
75
- *
76
- * @param agent Agent 账号信息
77
- * @returns 有效的 AccessToken
78
- */
79
- export async function getAccessToken(agent: ResolvedAgentAccount): Promise<string> {
80
- const cacheKey = `${agent.corpId}:${String(agent.agentId ?? "na")}`;
81
- let cache = tokenCaches.get(cacheKey);
82
-
83
- if (!cache) {
84
- cache = { token: "", expiresAt: 0, refreshPromise: null };
85
- tokenCaches.set(cacheKey, cache);
86
- }
87
-
88
- const now = Date.now();
89
- if (cache.token && cache.expiresAt > now + LIMITS.TOKEN_REFRESH_BUFFER_MS) {
90
- return cache.token;
91
- }
92
-
93
- // 防止并发刷新
94
- if (cache.refreshPromise) {
95
- return cache.refreshPromise;
96
- }
97
-
98
- cache.refreshPromise = (async () => {
99
- try {
100
- const url = `${API_ENDPOINTS.GET_TOKEN}?corpid=${encodeURIComponent(agent.corpId)}&corpsecret=${encodeURIComponent(agent.corpSecret)}`;
101
- const res = await wecomFetch(url, undefined, { proxyUrl: resolveWecomEgressProxyUrlFromNetwork(agent.network), timeoutMs: LIMITS.REQUEST_TIMEOUT_MS });
102
- const json = await res.json() as { access_token?: string; expires_in?: number; errcode?: number; errmsg?: string };
103
-
104
- if (!json?.access_token) {
105
- throw new Error(`gettoken failed: ${json?.errcode} ${json?.errmsg}`);
106
- }
107
-
108
- cache!.token = json.access_token;
109
- cache!.expiresAt = Date.now() + (json.expires_in ?? 7200) * 1000;
110
- return cache!.token;
111
- } finally {
112
- cache!.refreshPromise = null;
113
- }
114
- })();
115
-
116
- return cache.refreshPromise;
117
- }
118
-
119
- /**
120
- * **sendText (发送文本消息)**
121
- *
122
- * 调用 `message/send` (Agent) 或 `appchat/send` (群聊) 发送文本。
123
- *
124
- * @param params.agent 发送方 Agent
125
- * @param params.toUser 接收用户 ID (单聊可选,可与 toParty/toTag 同时使用)
126
- * @param params.toParty 接收部门 ID (单聊可选)
127
- * @param params.toTag 接收标签 ID (单聊可选)
128
- * @param params.chatId 接收群 ID (群聊模式必填,互斥)
129
- * @param params.text 消息内容
130
- */
131
- export async function sendText(params: {
132
- agent: ResolvedAgentAccount;
133
- toUser?: string;
134
- toParty?: string;
135
- toTag?: string;
136
- chatId?: string;
137
- text: string;
138
- }): Promise<void> {
139
- const { agent, toUser, toParty, toTag, chatId, text } = params;
140
- const token = await getAccessToken(agent);
141
-
142
- const useChat = Boolean(chatId);
143
- const url = useChat
144
- ? `${API_ENDPOINTS.SEND_APPCHAT}?access_token=${encodeURIComponent(token)}`
145
- : `${API_ENDPOINTS.SEND_MESSAGE}?access_token=${encodeURIComponent(token)}`;
146
-
147
- const body = useChat
148
- ? { chatid: chatId, msgtype: "text", text: { content: text } }
149
- : {
150
- touser: toUser,
151
- toparty: toParty,
152
- totag: toTag,
153
- msgtype: "text",
154
- agentid: requireAgentId(agent),
155
- text: { content: text }
156
- };
157
-
158
- const res = await wecomFetch(url, {
159
- method: "POST",
160
- headers: { "Content-Type": "application/json" },
161
- body: JSON.stringify(body),
162
- }, { proxyUrl: resolveWecomEgressProxyUrlFromNetwork(agent.network), timeoutMs: LIMITS.REQUEST_TIMEOUT_MS });
163
- const json = await res.json() as {
164
- errcode?: number;
165
- errmsg?: string;
166
- invaliduser?: string;
167
- invalidparty?: string;
168
- invalidtag?: string;
169
- };
170
-
171
- if (json?.errcode !== 0) {
172
- throw new Error(`send failed: ${json?.errcode} ${json?.errmsg}`);
173
- }
174
-
175
- if (json?.invaliduser || json?.invalidparty || json?.invalidtag) {
176
- const details = [
177
- json.invaliduser ? `invaliduser=${json.invaliduser}` : "",
178
- json.invalidparty ? `invalidparty=${json.invalidparty}` : "",
179
- json.invalidtag ? `invalidtag=${json.invalidtag}` : ""
180
- ].filter(Boolean).join(", ");
181
- throw new Error(`send partial failure: ${details}`);
182
- }
183
- }
184
-
185
- /**
186
- * **uploadMedia (上传媒体文件)**
187
- *
188
- * 上传临时素材到企业微信。
189
- * 素材有效期为 3 天。
190
- *
191
- * @param params.type 媒体类型 (image, voice, video, file)
192
- * @param params.buffer 文件二进制数据
193
- * @param params.filename 文件名 (需包含正确扩展名)
194
- * @returns 媒体 ID (media_id)
195
- */
196
- export async function uploadMedia(params: {
197
- agent: ResolvedAgentAccount;
198
- type: "image" | "voice" | "video" | "file";
199
- buffer: Buffer;
200
- filename: string;
201
- }): Promise<string> {
202
- const { agent, type, buffer, filename } = params;
203
- const safeFilename = normalizeUploadFilename(filename);
204
- const token = await getAccessToken(agent);
205
- const proxyUrl = resolveWecomEgressProxyUrlFromNetwork(agent.network);
206
- // 添加 debug=1 参数获取更多错误信息
207
- const url = `${API_ENDPOINTS.UPLOAD_MEDIA}?access_token=${encodeURIComponent(token)}&type=${encodeURIComponent(type)}&debug=1`;
208
-
209
- // DEBUG: 输出上传信息
210
- console.log(`[wecom-upload] Uploading media: type=${type}, filename=${safeFilename}, size=${buffer.length} bytes`);
211
-
212
- const uploadOnce = async (fileContentType: string) => {
213
- // 手动构造 multipart/form-data 请求体
214
- // 企业微信要求包含 filename 和 filelength
215
- const boundary = `----WebKitFormBoundary${crypto.randomBytes(16).toString("hex")}`;
216
-
217
- const header = Buffer.from(
218
- `--${boundary}\r\n` +
219
- `Content-Disposition: form-data; name="media"; filename="${safeFilename}"; filelength=${buffer.length}\r\n` +
220
- `Content-Type: ${fileContentType}\r\n\r\n`
221
- );
222
- const footer = Buffer.from(`\r\n--${boundary}--\r\n`);
223
- const body = Buffer.concat([header, buffer, footer]);
224
-
225
- console.log(`[wecom-upload] Multipart body size=${body.length}, boundary=${boundary}, fileContentType=${fileContentType}`);
226
-
227
- const res = await wecomFetch(url, {
228
- method: "POST",
229
- headers: {
230
- "Content-Type": `multipart/form-data; boundary=${boundary}`,
231
- "Content-Length": String(body.length),
232
- },
233
- body: body,
234
- }, { proxyUrl, timeoutMs: LIMITS.REQUEST_TIMEOUT_MS });
235
- const json = await res.json() as { media_id?: string; errcode?: number; errmsg?: string };
236
- console.log(`[wecom-upload] Response:`, JSON.stringify(json));
237
- return json;
238
- };
239
-
240
- const preferredContentType = guessUploadContentType(safeFilename);
241
- let json = await uploadOnce(preferredContentType);
242
-
243
- // 某些文件类型在严格网关/企业微信校验下可能失败,回退到通用类型再试一次。
244
- if (!json?.media_id && preferredContentType !== "application/octet-stream") {
245
- console.warn(
246
- `[wecom-upload] Upload failed with ${preferredContentType}, retrying as application/octet-stream: ${json?.errcode} ${json?.errmsg}`,
247
- );
248
- json = await uploadOnce("application/octet-stream");
249
- }
250
-
251
- if (!json?.media_id) {
252
- throw new Error(`upload failed: ${json?.errcode} ${json?.errmsg}`);
253
- }
254
- return json.media_id;
255
- }
256
-
257
- /**
258
- * **sendMedia (发送媒体消息)**
259
- *
260
- * 发送图片、音频、视频或文件。需先通过 `uploadMedia` 获取 media_id。
261
- *
262
- * @param params.agent 发送方 Agent
263
- * @param params.toUser 接收用户 ID (单聊可选)
264
- * @param params.toParty 接收部门 ID (单聊可选)
265
- * @param params.toTag 接收标签 ID (单聊可选)
266
- * @param params.chatId 接收群 ID (群聊模式必填)
267
- * @param params.mediaId 媒体 ID
268
- * @param params.mediaType 媒体类型
269
- * @param params.title 视频标题 (可选)
270
- * @param params.description 视频描述 (可选)
271
- */
272
- export async function sendMedia(params: {
273
- agent: ResolvedAgentAccount;
274
- toUser?: string;
275
- toParty?: string;
276
- toTag?: string;
277
- chatId?: string;
278
- mediaId: string;
279
- mediaType: "image" | "voice" | "video" | "file";
280
- title?: string;
281
- description?: string;
282
- }): Promise<void> {
283
- const { agent, toUser, toParty, toTag, chatId, mediaId, mediaType, title, description } = params;
284
- const token = await getAccessToken(agent);
285
-
286
- const useChat = Boolean(chatId);
287
- const url = useChat
288
- ? `${API_ENDPOINTS.SEND_APPCHAT}?access_token=${encodeURIComponent(token)}`
289
- : `${API_ENDPOINTS.SEND_MESSAGE}?access_token=${encodeURIComponent(token)}`;
290
-
291
- const mediaPayload = mediaType === "video"
292
- ? { media_id: mediaId, title: title ?? "Video", description: description ?? "" }
293
- : { media_id: mediaId };
294
-
295
- const body = useChat
296
- ? { chatid: chatId, msgtype: mediaType, [mediaType]: mediaPayload }
297
- : {
298
- touser: toUser,
299
- toparty: toParty,
300
- totag: toTag,
301
- msgtype: mediaType,
302
- agentid: requireAgentId(agent),
303
- [mediaType]: mediaPayload
304
- };
305
-
306
- const res = await wecomFetch(url, {
307
- method: "POST",
308
- headers: { "Content-Type": "application/json" },
309
- body: JSON.stringify(body),
310
- }, { proxyUrl: resolveWecomEgressProxyUrlFromNetwork(agent.network), timeoutMs: LIMITS.REQUEST_TIMEOUT_MS });
311
- const json = await res.json() as {
312
- errcode?: number;
313
- errmsg?: string;
314
- invaliduser?: string;
315
- invalidparty?: string;
316
- invalidtag?: string;
317
- };
318
-
319
- if (json?.errcode !== 0) {
320
- throw new Error(`send ${mediaType} failed: ${json?.errcode} ${json?.errmsg}`);
321
- }
322
-
323
- if (json?.invaliduser || json?.invalidparty || json?.invalidtag) {
324
- const details = [
325
- json.invaliduser ? `invaliduser=${json.invaliduser}` : "",
326
- json.invalidparty ? `invalidparty=${json.invalidparty}` : "",
327
- json.invalidtag ? `invalidtag=${json.invalidtag}` : ""
328
- ].filter(Boolean).join(", ");
329
- throw new Error(`send ${mediaType} partial failure: ${details}`);
330
- }
331
- }
332
-
333
- /**
334
- * **downloadMedia (下载媒体文件)**
335
- *
336
- * 通过 media_id 从企业微信服务器下载临时素材。
337
- *
338
- * @returns { buffer, contentType }
339
- */
340
- export async function downloadMedia(params: {
341
- agent: ResolvedAgentAccount;
342
- mediaId: string;
343
- maxBytes?: number;
344
- }): Promise<{ buffer: Buffer; contentType: string; filename?: string }> {
345
- const { agent, mediaId } = params;
346
- const token = await getAccessToken(agent);
347
- const url = `${API_ENDPOINTS.DOWNLOAD_MEDIA}?access_token=${encodeURIComponent(token)}&media_id=${encodeURIComponent(mediaId)}`;
348
-
349
- const res = await wecomFetch(url, undefined, { proxyUrl: resolveWecomEgressProxyUrlFromNetwork(agent.network), timeoutMs: LIMITS.REQUEST_TIMEOUT_MS });
350
-
351
- if (!res.ok) {
352
- throw new Error(`download failed: ${res.status}`);
353
- }
354
-
355
- const contentType = res.headers.get("content-type") || "application/octet-stream";
356
- const disposition = res.headers.get("content-disposition") || "";
357
- const filename = (() => {
358
- // 兼容:filename="a.md" / filename=a.md / filename*=UTF-8''a%2Eb.md
359
- const mStar = disposition.match(/filename\*\s*=\s*([^;]+)/i);
360
- if (mStar) {
361
- const raw = mStar[1]!.trim().replace(/^"(.*)"$/, "$1");
362
- const parts = raw.split("''");
363
- const encoded = parts.length === 2 ? parts[1]! : raw;
364
- try {
365
- return decodeURIComponent(encoded);
366
- } catch {
367
- return encoded;
368
- }
369
- }
370
- const m = disposition.match(/filename\s*=\s*([^;]+)/i);
371
- if (!m) return undefined;
372
- return m[1]!.trim().replace(/^"(.*)"$/, "$1") || undefined;
373
- })();
374
-
375
- // 检查是否返回了错误 JSON
376
- if (contentType.includes("application/json")) {
377
- const json = await res.json() as { errcode?: number; errmsg?: string };
378
- throw new Error(`download failed: ${json?.errcode} ${json?.errmsg}`);
379
- }
380
-
381
- const buffer = await readResponseBodyAsBuffer(res, params.maxBytes);
382
- return { buffer, contentType, filename };
383
- }
@@ -1,136 +0,0 @@
1
-
2
- import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
3
- import type { ResolvedBotAccount } from "../types/index.js";
4
- import type { WecomBotInboundMessage as WecomInboundMessage } from "../types/index.js";
5
-
6
- /**
7
- * **WecomRuntimeEnv (运行时环境)**
8
- *
9
- * 包含基础的日志和错误报告接口,用于解耦对 PluginRuntime 的直接依赖。
10
- */
11
- export type WecomRuntimeEnv = {
12
- log?: (message: string) => void;
13
- error?: (message: string) => void;
14
- };
15
-
16
- /**
17
- * **WecomWebhookTarget (Webhook 目标上下文)**
18
- *
19
- * 描述一个注册的 Bot 接收端点。包含处理该端点所需的所有上下文信息。
20
- *
21
- * @property account 解析后的 Bot 账号信息 (Token, AESKey 等)
22
- * @property config 插件全局配置
23
- * @property runtime 运行时环境 (日志)
24
- * @property core OpenClaw 插件核心运行时
25
- * @property path 该 Target 注册的 Webhook 路径
26
- * @property statusSink 用于上报最后收发消息时间的回调
27
- */
28
- export type WecomWebhookTarget = {
29
- account: ResolvedBotAccount;
30
- config: OpenClawConfig;
31
- runtime: WecomRuntimeEnv;
32
- core: PluginRuntime;
33
- path: string;
34
- /** 反馈最后接收/发送时间 */
35
- statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
36
- };
37
-
38
- /**
39
- * **StreamState (流式会话状态)**
40
- *
41
- * 记录一个流式请求的生命周期状态。
42
- *
43
- * @property streamId 唯一会话 ID
44
- * @property msgid 关联的企业微信消息 ID (用于去重)
45
- * @property createdAt 创建时间
46
- * @property updatedAt 最后更新时间 (用于 Prune)
47
- * @property started 是否已开始处理 (Agent 已介入)
48
- * @property finished 是否已完成 (Agent 输出完毕或出错)
49
- * @property error 错误信息 (如有)
50
- * @property content 已积累的响应内容 (用于长轮询返回)
51
- * @property images 过程中生成的图片 (Base64 + MD5)
52
- */
53
- export type StreamState = {
54
- streamId: string;
55
- msgid?: string;
56
- /** 会话键(同一人同一会话,用于队列/批次) */
57
- conversationKey?: string;
58
- /** 批次键(conversationKey + 批次序号) */
59
- batchKey?: string;
60
- /** 触发者 userid(用于 Agent 私信兜底) */
61
- userId?: string;
62
- /** 会话类型(用于群聊兜底逻辑) */
63
- chatType?: "group" | "direct";
64
- /** 群聊 chatid(用于日志/提示,不用于 Agent 发群) */
65
- chatId?: string;
66
- /** 智能机器人 aibotid(用于 taskKey 生成与日志) */
67
- aibotid?: string;
68
- /** Bot 回调幂等键(用于最终交付幂等) */
69
- taskKey?: string;
70
- createdAt: number;
71
- updatedAt: number;
72
- started: boolean;
73
- finished: boolean;
74
- error?: string;
75
- content: string;
76
- images?: { base64: string; md5: string }[];
77
- /** 兜底模式(仅作为内部状态,不暴露给企微) */
78
- fallbackMode?: "media" | "timeout" | "error";
79
- /** 群内兜底提示是否已发送(用于防重复刷屏) */
80
- fallbackPromptSentAt?: number;
81
- /** Agent 私信最终交付是否已完成(用于防重复发送) */
82
- finalDeliveredAt?: number;
83
- /** 用于私信兜底的完整内容(不受 STREAM_MAX_BYTES 限制,但仍需上限保护) */
84
- dmContent?: string;
85
- /** 已通过 Agent 私信发送过的媒体标识(防重复发送附件) */
86
- agentMediaKeys?: string[];
87
- };
88
-
89
- /**
90
- * **PendingInbound (待处理/防抖消息)**
91
- *
92
- * 暂存在队列中的消息,等待防抖计时器结束进行聚合。
93
- *
94
- * @property streamId 预分配的流 ID
95
- * @property target 目标 Webhook 上下文
96
- * @property msg 原始消息对象 (如果聚合,通常指第一条)
97
- * @property contents 聚合的消息内容列表
98
- * @property media 附带的媒体文件 (如果有)
99
- * @property msgids 聚合的所有消息 ID (用于去重)
100
- * @property timeout 防抖定时器句柄
101
- */
102
- export type PendingInbound = {
103
- streamId: string;
104
- conversationKey: string;
105
- batchKey: string;
106
- target: WecomWebhookTarget;
107
- msg: WecomInboundMessage;
108
- contents: string[];
109
- media?: { buffer: Buffer; contentType: string; filename: string };
110
- msgids: string[];
111
- nonce: string;
112
- timestamp: string;
113
- timeout: ReturnType<typeof setTimeout> | null;
114
- /** 已到达防抖截止时间,但因前序批次仍在处理中而暂存 */
115
- readyToFlush?: boolean;
116
- createdAt: number;
117
- };
118
-
119
- /**
120
- * **ActiveReplyState (主动回复地址状态)**
121
- *
122
- * 存储企业微信回调中提供的 `response_url`,用于后续将流式响应转为主动推送(template_card)等。
123
- *
124
- * @property response_url 企业微信提供的回调回复 URL
125
- * @property proxyUrl 如果配置了代理,存储代理地址
126
- * @property createdAt 创建时间
127
- * @property usedAt 使用时间 (仅当 policy="once" 时有意义)
128
- * @property lastError 最后一次发送失败的错误信息
129
- */
130
- export type ActiveReplyState = {
131
- response_url: string;
132
- proxyUrl?: string;
133
- createdAt: number;
134
- usedAt?: number;
135
- lastError?: string;
136
- };