@sunnoy/wecom 2.0.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,76 @@
1
+ /**
2
+ * WeCom self-built app callback media downloader.
3
+ *
4
+ * Downloads inbound media (image/voice/file) from WeCom via the
5
+ * Agent API `/cgi-bin/media/get` endpoint using the access token
6
+ * obtained from the self-built app credentials.
7
+ */
8
+
9
+ import path from "node:path";
10
+ import { logger } from "../logger.js";
11
+ import { getAccessToken } from "./agent-api.js";
12
+ import { wecomFetch } from "./http.js";
13
+ import { AGENT_API_ENDPOINTS, CALLBACK_MEDIA_DOWNLOAD_TIMEOUT_MS } from "./constants.js";
14
+
15
+ /**
16
+ * Download a WeCom media file (image / voice / file) by MediaId via the
17
+ * self-built app access token and save it through the core media runtime.
18
+ *
19
+ * @param {object} params
20
+ * @param {object} params.agent - { corpId, corpSecret, agentId }
21
+ * @param {string} params.mediaId - WeCom MediaId
22
+ * @param {"image"|"voice"|"file"} params.type - media type hint
23
+ * @param {object} params.runtime - OpenClaw runtime (for saveMediaBuffer)
24
+ * @param {object} params.config - OpenClaw config (for mediaMaxMb)
25
+ * @returns {Promise<{ path: string, contentType: string }>}
26
+ */
27
+ export async function downloadCallbackMedia({ agent, mediaId, type, runtime, config }) {
28
+ const token = await getAccessToken(agent);
29
+ const url = `${AGENT_API_ENDPOINTS.DOWNLOAD_MEDIA}?access_token=${encodeURIComponent(token)}&media_id=${encodeURIComponent(mediaId)}`;
30
+
31
+ const mediaMaxMb = config?.agents?.defaults?.mediaMaxMb ?? 5;
32
+ const maxBytes = mediaMaxMb * 1024 * 1024;
33
+
34
+ let response;
35
+ const controller = new AbortController();
36
+ const timeoutId = setTimeout(() => controller.abort(), CALLBACK_MEDIA_DOWNLOAD_TIMEOUT_MS);
37
+ try {
38
+ response = await wecomFetch(url, { signal: controller.signal });
39
+ } finally {
40
+ clearTimeout(timeoutId);
41
+ }
42
+
43
+ if (!response.ok) {
44
+ throw new Error(`WeCom media download failed: HTTP ${response.status} for mediaId=${mediaId}`);
45
+ }
46
+
47
+ const buffer = Buffer.from(await response.arrayBuffer());
48
+ const contentType =
49
+ response.headers.get("content-type") ||
50
+ (type === "image" ? "image/jpeg" : "application/octet-stream");
51
+
52
+ // Try to extract the filename from Content-Disposition
53
+ const disposition = response.headers.get("content-disposition") ?? "";
54
+ const filenameMatch = disposition.match(/filename[*\s]*=\s*(?:UTF-8''|")?([^";]+)/i);
55
+ const filename =
56
+ filenameMatch?.[1]?.trim() ||
57
+ (type === "image" ? `${mediaId}.jpg` : type === "voice" ? `${mediaId}.amr` : mediaId);
58
+
59
+ // Save via core media runtime when available
60
+ if (typeof runtime?.media?.saveMediaBuffer === "function") {
61
+ const saved = await runtime.media.saveMediaBuffer(buffer, contentType, "inbound", maxBytes, filename);
62
+ return { path: saved.path, contentType: saved.contentType };
63
+ }
64
+
65
+ // Fallback: write to OS temp dir
66
+ const { tmpdir } = await import("node:os");
67
+ const { writeFile } = await import("node:fs/promises");
68
+ const ext = path.extname(filename) || (type === "image" ? ".jpg" : ".bin");
69
+ const tempPath = path.join(
70
+ tmpdir(),
71
+ `wecom-cb-${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext}`,
72
+ );
73
+ await writeFile(tempPath, buffer);
74
+ logger.debug(`[CB] Media saved to temp path: ${tempPath}`);
75
+ return { path: tempPath, contentType };
76
+ }
@@ -1,6 +1,5 @@
1
1
  import crypto from "node:crypto";
2
2
  import { basename } from "node:path";
3
- import { readFile } from "node:fs/promises";
4
3
  import {
5
4
  buildBaseAccountStatusSnapshot,
6
5
  buildBaseChannelStatusSummary,
@@ -23,9 +22,10 @@ import { agentSendMedia, agentSendText, agentUploadMedia } from "./agent-api.js"
23
22
  import { setConfigProxyUrl, wecomFetch } from "./http.js";
24
23
  import { wecomOnboardingAdapter } from "./onboarding.js";
25
24
  import { getAccountTelemetry, recordOutboundActivity } from "./runtime-telemetry.js";
26
- import { getRuntime, setOpenclawConfig } from "./state.js";
25
+ import { getOpenclawConfig, getRuntime, setOpenclawConfig } from "./state.js";
27
26
  import { resolveWecomTarget } from "./target.js";
28
27
  import { webhookSendFile, webhookSendImage, webhookSendMarkdown, webhookUploadFile } from "./webhook-bot.js";
28
+ import { loadOutboundMediaFromUrl as loadOutboundMediaFromUrlCompat } from "./openclaw-compat.js";
29
29
  import {
30
30
  CHANNEL_ID,
31
31
  DEFAULT_ACCOUNT_ID,
@@ -34,7 +34,10 @@ import {
34
34
  getWebhookBotSendUrl,
35
35
  setApiBaseUrl,
36
36
  } from "./constants.js";
37
+ import { uploadAndSendMedia } from "./media-uploader.js";
38
+ import { getExtendedMediaLocalRoots } from "./openclaw-compat.js";
37
39
  import { sendWsMessage, startWsMonitor } from "./ws-monitor.js";
40
+ import { getWsClient } from "./ws-state.js";
38
41
 
39
42
  function normalizePairingEntry(entry) {
40
43
  return String(entry ?? "")
@@ -79,40 +82,32 @@ function normalizeMediaPath(mediaUrl) {
79
82
  return value;
80
83
  }
81
84
 
82
- async function loadMediaPayload(mediaUrl, { mediaLocalRoots } = {}) {
85
+ async function loadMediaPayload(mediaUrl, { accountConfig, mediaLocalRoots } = {}) {
83
86
  const normalized = normalizeMediaPath(mediaUrl);
84
- if (normalized.startsWith("/")) {
85
- // Prefer core's loadWebMedia with sandbox enforcement when available.
86
- const runtime = getRuntime();
87
- if (typeof runtime?.media?.loadWebMedia === "function" && Array.isArray(mediaLocalRoots) && mediaLocalRoots.length > 0) {
88
- const loaded = await runtime.media.loadWebMedia(normalized, { localRoots: mediaLocalRoots });
89
- return {
90
- buffer: loaded.buffer,
91
- filename: loaded.fileName || basename(normalized) || "file",
92
- contentType: loaded.contentType || "",
93
- };
94
- }
95
- const buffer = await readFile(normalized);
96
- return {
97
- buffer,
98
- filename: basename(normalized) || "file",
99
- contentType: "",
100
- };
101
- }
87
+ let runtime = null;
88
+ try {
89
+ runtime = getRuntime();
90
+ } catch {}
91
+
92
+ const loaded = await loadOutboundMediaFromUrlCompat(normalized, {
93
+ accountConfig,
94
+ fetchImpl: wecomFetch,
95
+ mediaLocalRoots,
96
+ runtimeLoadMedia:
97
+ typeof runtime?.media?.loadWebMedia === "function"
98
+ ? (path, options) => runtime.media.loadWebMedia(path, options)
99
+ : undefined,
100
+ });
102
101
 
103
- const response = await wecomFetch(normalized);
104
- if (!response.ok) {
105
- throw new Error(`failed to download media: ${response.status}`);
106
- }
107
102
  return {
108
- buffer: Buffer.from(await response.arrayBuffer()),
109
- filename: basename(new URL(normalized).pathname) || "file",
110
- contentType: response.headers.get("content-type") || "",
103
+ buffer: loaded.buffer,
104
+ filename: loaded.fileName || basename(normalized) || "file",
105
+ contentType: loaded.contentType || "",
111
106
  };
112
107
  }
113
108
 
114
- async function loadResolvedMedia(mediaUrl, { mediaLocalRoots } = {}) {
115
- const media = await loadMediaPayload(mediaUrl, { mediaLocalRoots });
109
+ async function loadResolvedMedia(mediaUrl, { accountConfig, mediaLocalRoots } = {}) {
110
+ const media = await loadMediaPayload(mediaUrl, { accountConfig, mediaLocalRoots });
116
111
  return {
117
112
  ...media,
118
113
  mediaType: resolveAgentMediaType(media.filename, media.contentType),
@@ -134,45 +129,6 @@ export function resolveAgentMediaTypeFromFilename(filename) {
134
129
  return resolveAgentMediaType(filename, "");
135
130
  }
136
131
 
137
- function resolveWsNoticeTarget(target, rawTo) {
138
- if (target?.webhook || target?.toParty || target?.toTag) {
139
- return null;
140
- }
141
- const fallback = String(rawTo ?? "").trim();
142
- return target?.chatId || target?.toUser || fallback || null;
143
- }
144
-
145
- function buildUnsupportedMediaNotice({ text, mediaType, deliveredViaAgent }) {
146
- let notice;
147
- if (mediaType === "file") {
148
- notice = deliveredViaAgent
149
- ? "由于当前企业微信bot不支持给用户发送文件,文件通过自建应用发送。"
150
- : "由于当前企业微信bot不支持给用户发送文件,且当前未配置自建应用发送渠道。";
151
- } else if (mediaType === "image") {
152
- notice = deliveredViaAgent
153
- ? "由于当前企业微信bot不支持直接发送图片,图片通过自建应用发送。"
154
- : "由于当前企业微信bot不支持直接发送图片,且当前未配置自建应用发送渠道。";
155
- } else {
156
- notice = deliveredViaAgent
157
- ? "由于当前企业微信bot不支持直接发送媒体,媒体通过自建应用发送。"
158
- : "由于当前企业微信bot不支持直接发送媒体,且当前未配置自建应用发送渠道。";
159
- }
160
-
161
- return [text, notice].filter(Boolean).join("\n\n");
162
- }
163
-
164
- async function sendUnsupportedMediaNoticeViaWs({ to, text, mediaType, accountId }) {
165
- return sendWsMessage({
166
- to,
167
- content: buildUnsupportedMediaNotice({
168
- text,
169
- mediaType,
170
- deliveredViaAgent: true,
171
- }),
172
- accountId,
173
- });
174
- }
175
-
176
132
  function resolveOutboundAccountId(cfg, accountId) {
177
133
  return accountId || resolveDefaultAccountId(cfg);
178
134
  }
@@ -199,7 +155,8 @@ async function sendViaWebhook({ cfg, accountId, webhookName, text, mediaUrl, pre
199
155
  return { channel: CHANNEL_ID, messageId: `wecom-webhook-${Date.now()}` };
200
156
  }
201
157
 
202
- const { buffer, filename, mediaType } = preparedMedia ?? (await loadResolvedMedia(mediaUrl));
158
+ const { buffer, filename, mediaType } =
159
+ preparedMedia ?? (await loadResolvedMedia(mediaUrl, { accountConfig: account?.config }));
203
160
 
204
161
  if (text) {
205
162
  await webhookSendMarkdown({ url, content: text });
@@ -237,7 +194,8 @@ async function sendViaAgent({ cfg, accountId, target, text, mediaUrl, preparedMe
237
194
  return { channel: CHANNEL_ID, messageId: `wecom-agent-${Date.now()}` };
238
195
  }
239
196
 
240
- const { buffer, filename, mediaType } = preparedMedia ?? (await loadResolvedMedia(mediaUrl));
197
+ const { buffer, filename, mediaType } =
198
+ preparedMedia ?? (await loadResolvedMedia(mediaUrl, { accountConfig: resolveAccount(cfg, accountId)?.config }));
241
199
  const mediaId = await agentUploadMedia({
242
200
  agent,
243
201
  type: mediaType,
@@ -308,6 +266,24 @@ export const wecomChannelPlugin = {
308
266
  allowFrom: { type: "array", items: { type: "string" } },
309
267
  groupPolicy: { enum: ["open", "allowlist", "disabled"] },
310
268
  groupAllowFrom: { type: "array", items: { type: "string" } },
269
+ deliveryMode: { enum: ["direct", "gateway"] },
270
+ mediaLocalRoots: { type: "array", items: { type: "string" } },
271
+ agent: {
272
+ type: "object",
273
+ additionalProperties: true,
274
+ properties: {
275
+ replyFormat: { enum: ["text", "markdown"] },
276
+ callback: {
277
+ type: "object",
278
+ additionalProperties: false,
279
+ properties: {
280
+ token: { type: "string" },
281
+ encodingAESKey: { type: "string" },
282
+ path: { type: "string" },
283
+ },
284
+ },
285
+ },
286
+ },
311
287
  },
312
288
  },
313
289
  uiHints: {
@@ -316,6 +292,10 @@ export const wecomChannelPlugin = {
316
292
  websocketUrl: { label: "WebSocket URL", placeholder: DEFAULT_WS_URL },
317
293
  welcomeMessage: { label: "Welcome Message" },
318
294
  "agent.corpSecret": { sensitive: true, label: "Application Secret" },
295
+ "agent.replyFormat": { label: "Reply Format", placeholder: "text" },
296
+ "agent.callback.token": { label: "Callback Token" },
297
+ "agent.callback.encodingAESKey": { label: "Callback Encoding AES Key", sensitive: true },
298
+ "agent.callback.path": { label: "Callback Path", placeholder: "/api/channels/wecom/callback" },
319
299
  },
320
300
  },
321
301
  config: {
@@ -324,7 +304,8 @@ export const wecomChannelPlugin = {
324
304
  defaultAccountId: (cfg) => resolveDefaultAccountId(cfg),
325
305
  setAccountEnabled: ({ cfg, accountId, enabled }) => updateAccountConfig(cfg, accountId, { enabled }),
326
306
  deleteAccount: ({ cfg, accountId }) => deleteAccountConfig(cfg, accountId),
327
- isConfigured: (account) => Boolean(account.botId && account.secret),
307
+ isConfigured: (account) =>
308
+ Boolean((account.botId && account.secret) || account.callbackConfigured),
328
309
  describeAccount,
329
310
  resolveAllowFrom: ({ cfg, accountId }) => resolveAllowFromForAccount(cfg, accountId),
330
311
  formatAllowFrom: ({ allowFrom }) => normalizeAllowFromEntries(allowFrom.map((entry) => String(entry))),
@@ -360,7 +341,29 @@ export const wecomChannelPlugin = {
360
341
  messaging: {
361
342
  normalizeTarget: (target) => {
362
343
  const trimmed = String(target ?? "").trim();
363
- return trimmed || undefined;
344
+ if (!trimmed) {
345
+ return undefined;
346
+ }
347
+ const resolved = resolveWecomTarget(trimmed);
348
+ if (!resolved) {
349
+ return undefined;
350
+ }
351
+ if (resolved.webhook) {
352
+ return `webhook:${resolved.webhook}`;
353
+ }
354
+ if (resolved.toParty) {
355
+ return `party:${resolved.toParty}`;
356
+ }
357
+ if (resolved.toTag) {
358
+ return `tag:${resolved.toTag}`;
359
+ }
360
+ if (resolved.chatId) {
361
+ return `chat:${resolved.chatId}`;
362
+ }
363
+ if (resolved.toUser) {
364
+ return `user:${resolved.toUser}`;
365
+ }
366
+ return trimmed;
364
367
  },
365
368
  targetResolver: {
366
369
  looksLikeId: (value) => Boolean(String(value ?? "").trim()),
@@ -373,7 +376,14 @@ export const wecomChannelPlugin = {
373
376
  listGroups: async () => [],
374
377
  },
375
378
  outbound: {
376
- deliveryMode: "direct",
379
+ get deliveryMode() {
380
+ try {
381
+ const cfg = getOpenclawConfig();
382
+ const mode = cfg?.channels?.wecom?.deliveryMode;
383
+ if (mode === "direct" || mode === "gateway") return mode;
384
+ } catch {}
385
+ return "gateway";
386
+ },
377
387
  chunker: (text, limit) => resolveRuntimeTextChunker(text, limit),
378
388
  textChunkLimit: TEXT_CHUNK_LIMIT,
379
389
  sendText: async ({ cfg, to, text, accountId }) => {
@@ -416,10 +426,11 @@ export const wecomChannelPlugin = {
416
426
  setOpenclawConfig(cfg);
417
427
  const account = applyNetworkConfig(cfg, resolvedAccountId);
418
428
  const target = resolveWecomTarget(to) ?? {};
419
- const wsNoticeTarget = resolveWsNoticeTarget(target, to);
420
429
 
421
430
  if (target.webhook) {
422
- const preparedMedia = mediaUrl ? await loadResolvedMedia(mediaUrl, { mediaLocalRoots }) : undefined;
431
+ const preparedMedia = mediaUrl
432
+ ? await loadResolvedMedia(mediaUrl, { accountConfig: account?.config, mediaLocalRoots })
433
+ : undefined;
423
434
  return sendViaWebhook({
424
435
  cfg,
425
436
  accountId: resolvedAccountId,
@@ -430,14 +441,6 @@ export const wecomChannelPlugin = {
430
441
  });
431
442
  }
432
443
 
433
- const agentTarget =
434
- target.toParty || target.toTag
435
- ? target
436
- : target.chatId
437
- ? { chatId: target.chatId }
438
- : { toUser: target.toUser || String(to).replace(/^wecom:/i, "") };
439
- const preparedMedia = await loadResolvedMedia(mediaUrl, { mediaLocalRoots });
440
-
441
444
  if (target.toParty || target.toTag) {
442
445
  if (!account?.agentCredentials) {
443
446
  throw new Error("Agent API is required for party/tag media delivery");
@@ -445,61 +448,63 @@ export const wecomChannelPlugin = {
445
448
  return sendViaAgent({
446
449
  cfg,
447
450
  accountId: resolvedAccountId,
448
- target: agentTarget,
451
+ target,
449
452
  text,
450
453
  mediaUrl,
451
- preparedMedia,
454
+ preparedMedia: await loadResolvedMedia(mediaUrl, { accountConfig: account?.config, mediaLocalRoots }),
452
455
  });
453
456
  }
454
457
 
455
- if (account?.agentCredentials) {
456
- const agentResult = await sendViaAgent({
457
- cfg,
458
- accountId: resolvedAccountId,
459
- target: agentTarget,
460
- text: wsNoticeTarget ? undefined : text,
461
- mediaUrl,
462
- preparedMedia,
463
- });
458
+ const chatId = target.chatId || target.toUser || String(to).replace(/^wecom:/i, "");
459
+ const wsClient = getWsClient(resolvedAccountId);
464
460
 
465
- if (wsNoticeTarget) {
461
+ let textAlreadySent = false;
462
+ if (wsClient?.isConnected && mediaUrl) {
463
+ if (text) {
466
464
  try {
467
- await sendUnsupportedMediaNoticeViaWs({
468
- to: wsNoticeTarget,
469
- text,
470
- mediaType: preparedMedia.mediaType,
471
- accountId: resolvedAccountId,
472
- });
473
- } catch (error) {
474
- logger.warn(`[wecom] WS media notice failed, falling back to Agent text delivery: ${error.message}`);
475
- if (text) {
476
- await sendViaAgent({
477
- cfg,
478
- accountId: resolvedAccountId,
479
- target: agentTarget,
480
- text,
481
- });
482
- }
465
+ await sendWsMessage({ to: chatId, content: text, accountId: resolvedAccountId });
466
+ textAlreadySent = true;
467
+ } catch (textErr) {
468
+ logger.warn(`[wecom] WS text send failed before media upload: ${textErr.message}`);
483
469
  }
484
470
  }
485
471
 
486
- return agentResult;
472
+ const extendedRoots = await getExtendedMediaLocalRoots({
473
+ accountConfig: account?.config,
474
+ mediaLocalRoots,
475
+ });
476
+ const result = await uploadAndSendMedia({
477
+ wsClient,
478
+ mediaUrl,
479
+ chatId,
480
+ mediaLocalRoots: extendedRoots,
481
+ log: (...args) => logger.info(...args),
482
+ errorLog: (...args) => logger.error(...args),
483
+ });
484
+
485
+ if (result.ok) {
486
+ recordOutboundActivity({ accountId: resolvedAccountId });
487
+ return { channel: CHANNEL_ID, messageId: result.messageId, chatId };
488
+ }
489
+ logger.warn(`[wecom] WS media upload failed, falling back: ${result.error || result.rejectReason}`);
487
490
  }
488
491
 
489
- if (wsNoticeTarget) {
490
- logger.warn("[wecom] Agent API is not configured for unsupported WS media; sending notice only");
491
- return sendWsMessage({
492
- to: wsNoticeTarget,
493
- content: buildUnsupportedMediaNotice({
494
- text,
495
- mediaType: preparedMedia.mediaType,
496
- deliveredViaAgent: false,
497
- }),
492
+ const agentTarget = target.chatId
493
+ ? { chatId: target.chatId }
494
+ : { toUser: target.toUser || String(to).replace(/^wecom:/i, "") };
495
+
496
+ if (account?.agentCredentials) {
497
+ return sendViaAgent({
498
+ cfg,
498
499
  accountId: resolvedAccountId,
500
+ target: agentTarget,
501
+ text: textAlreadySent ? undefined : text,
502
+ mediaUrl,
503
+ preparedMedia: await loadResolvedMedia(mediaUrl, { accountConfig: account?.config, mediaLocalRoots }),
499
504
  });
500
505
  }
501
506
 
502
- throw new Error("Agent API is not configured for unsupported WeCom media delivery");
507
+ throw new Error("No media delivery channel available: WS upload failed and Agent API is not configured");
503
508
  },
504
509
  },
505
510
  status: {
@@ -628,6 +633,4 @@ export const wecomChannelPlugin = {
628
633
  },
629
634
  };
630
635
 
631
- export const wecomChannelPluginTesting = {
632
- buildUnsupportedMediaNotice,
633
- };
636
+ export const wecomChannelPluginTesting = {};
@@ -26,11 +26,87 @@ export const REQID_FLUSH_DEBOUNCE_MS = 1_000;
26
26
  export const PENDING_REPLY_TTL_MS = 5 * 60 * 1000;
27
27
  export const PENDING_REPLY_MAX_SIZE = 50;
28
28
 
29
+ export const IMAGE_MAX_BYTES = 10 * 1024 * 1024;
30
+ export const VIDEO_MAX_BYTES = 10 * 1024 * 1024;
31
+ export const VOICE_MAX_BYTES = 2 * 1024 * 1024;
32
+ export const FILE_MAX_BYTES = 20 * 1024 * 1024;
33
+ export const ABSOLUTE_MAX_BYTES = FILE_MAX_BYTES;
34
+
29
35
  export const DEFAULT_MEDIA_MAX_MB = 5;
30
36
  export const TEXT_CHUNK_LIMIT = 4000;
31
- export const DEFAULT_WELCOME_MESSAGE = ["你好,我是 AI 助手。", "", "可用命令:", "/new", "/compact", "/help", "/status"].join(
32
- "\n",
33
- );
37
+ export const DEFAULT_WELCOME_MESSAGES = [
38
+ [
39
+ "新的一天,元气满满!🌞",
40
+ "",
41
+ "你可以通过斜杠指令管理会话:",
42
+ "/new 新建对话",
43
+ "/compact 压缩对话",
44
+ "/help 帮助",
45
+ "/status 查看状态",
46
+ "/reasoning stream 打开思考动画",
47
+ ].join("\n"),
48
+ [
49
+ "终于唤醒我啦,我已经准备就绪!😄",
50
+ "",
51
+ "试试这些常用指令:",
52
+ "/new 新建对话",
53
+ "/compact 压缩对话",
54
+ "/help 帮助",
55
+ "/status 查看状态",
56
+ "/reasoning stream 打开思考动画",
57
+ ].join("\n"),
58
+ [
59
+ "欢迎回来,准备开始今天的工作吧!✨",
60
+ "",
61
+ "会话管理指令:",
62
+ "/new 新建对话",
63
+ "/compact 压缩对话",
64
+ "/help 帮助",
65
+ "/status 查看状态",
66
+ "/reasoning stream 打开思考动画",
67
+ ].join("\n"),
68
+ [
69
+ "嗨,我已经在线!🤖",
70
+ "",
71
+ "你可以先试试这些命令:",
72
+ "/new 新建对话",
73
+ "/compact 压缩对话",
74
+ "/help 帮助",
75
+ "/status 查看状态",
76
+ "/reasoning stream 打开思考动画",
77
+ ].join("\n"),
78
+ [
79
+ "今天也一起高效开工吧!🚀",
80
+ "",
81
+ "先来看看这些指令:",
82
+ "/new 新建对话",
83
+ "/compact 压缩对话",
84
+ "/help 帮助",
85
+ "/status 查看状态",
86
+ "/reasoning stream 打开思考动画",
87
+ ].join("\n"),
88
+ [
89
+ "叮咚,你的数字助手已就位!🎉",
90
+ "",
91
+ "常用操作给你备好了:",
92
+ "/new 新建对话",
93
+ "/compact 压缩对话",
94
+ "/help 帮助",
95
+ "/status 查看状态",
96
+ "/reasoning stream 打开思考动画",
97
+ ].join("\n"),
98
+ [
99
+ "灵感加载完成,随时可以开聊!💡",
100
+ "",
101
+ "你可以这样开始:",
102
+ "/new 新建对话",
103
+ "/compact 压缩对话",
104
+ "/help 帮助",
105
+ "/status 查看状态",
106
+ "/reasoning stream 打开思考动画",
107
+ ].join("\n"),
108
+ ];
109
+ export const DEFAULT_WELCOME_MESSAGE = DEFAULT_WELCOME_MESSAGES[0];
34
110
 
35
111
  export const MEDIA_CACHE_DIR = join(process.env.HOME || "/tmp", ".openclaw", "media", "wecom");
36
112
 
@@ -90,6 +166,11 @@ export const TOKEN_REFRESH_BUFFER_MS = 60 * 1000;
90
166
  export const AGENT_API_REQUEST_TIMEOUT_MS = 15 * 1000;
91
167
  export const MAX_REQUEST_BODY_SIZE = 1024 * 1024;
92
168
 
169
+ // Callback (self-built app HTTP inbound) constants
170
+ export const CALLBACK_INBOUND_MAX_BODY_BYTES = 1 * 1024 * 1024;
171
+ export const CALLBACK_MEDIA_DOWNLOAD_TIMEOUT_MS = 30_000;
172
+ export const CALLBACK_TIMESTAMP_TOLERANCE_S = 300;
173
+
93
174
  export function getWebhookBotSendUrl() {
94
175
  return `${resolveApiBaseUrl()}/cgi-bin/webhook/send`;
95
176
  }