@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.
- package/README.md +89 -9
- package/index.js +16 -0
- package/openclaw.plugin.json +3 -0
- package/package.json +5 -3
- package/skills/wecom-doc/SKILL.md +363 -0
- package/skills/wecom-doc/references/doc-api.md +224 -0
- package/wecom/accounts.js +19 -0
- package/wecom/agent-api.js +7 -6
- package/wecom/callback-crypto.js +80 -0
- package/wecom/callback-inbound.js +718 -0
- package/wecom/callback-media.js +76 -0
- package/wecom/channel-plugin.js +129 -126
- package/wecom/constants.js +84 -3
- package/wecom/mcp-config.js +146 -0
- package/wecom/media-uploader.js +208 -0
- package/wecom/openclaw-compat.js +302 -0
- package/wecom/reqid-store.js +146 -0
- package/wecom/workspace-template.js +107 -21
- package/wecom/ws-monitor.js +687 -326
- package/image-processor.js +0 -175
|
@@ -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
|
+
}
|
package/wecom/channel-plugin.js
CHANGED
|
@@ -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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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:
|
|
109
|
-
filename: basename(
|
|
110
|
-
contentType:
|
|
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 } =
|
|
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 } =
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
456
|
-
|
|
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
|
-
|
|
461
|
+
let textAlreadySent = false;
|
|
462
|
+
if (wsClient?.isConnected && mediaUrl) {
|
|
463
|
+
if (text) {
|
|
466
464
|
try {
|
|
467
|
-
await
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
|
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 = {};
|
package/wecom/constants.js
CHANGED
|
@@ -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
|
|
32
|
-
|
|
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
|
}
|