@sunnoy/wecom 2.1.0 → 2.2.1
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 +6 -2
- package/index.js +2 -0
- package/openclaw.plugin.json +3 -0
- package/package.json +5 -3
- package/skills/wecom-contact-lookup/SKILL.md +167 -0
- package/skills/wecom-doc-manager/SKILL.md +106 -0
- package/skills/wecom-doc-manager/references/api-create-doc.md +56 -0
- package/skills/wecom-doc-manager/references/api-edit-doc-content.md +68 -0
- package/skills/wecom-doc-manager/references/api-export-document.md +88 -0
- package/skills/wecom-edit-todo/SKILL.md +254 -0
- package/skills/wecom-get-todo-detail/SKILL.md +148 -0
- package/skills/wecom-get-todo-list/SKILL.md +132 -0
- package/skills/wecom-meeting-create/SKILL.md +163 -0
- package/skills/wecom-meeting-create/references/example-full.md +30 -0
- package/skills/wecom-meeting-create/references/example-reminder.md +46 -0
- package/skills/wecom-meeting-create/references/example-security.md +22 -0
- package/skills/wecom-meeting-manage/SKILL.md +141 -0
- package/skills/wecom-meeting-query/SKILL.md +335 -0
- package/skills/wecom-preflight/SKILL.md +103 -0
- package/skills/wecom-schedule/SKILL.md +164 -0
- package/skills/wecom-schedule/references/api-check-availability.md +56 -0
- package/skills/wecom-schedule/references/api-create-schedule.md +38 -0
- package/skills/wecom-schedule/references/api-get-schedule-detail.md +81 -0
- package/skills/wecom-schedule/references/api-update-schedule.md +30 -0
- package/skills/wecom-schedule/references/ref-reminders.md +24 -0
- package/skills/wecom-smartsheet-data/SKILL.md +76 -0
- package/skills/wecom-smartsheet-data/references/api-get-records.md +61 -0
- package/skills/wecom-smartsheet-data/references/cell-value-formats.md +120 -0
- package/skills/wecom-smartsheet-schema/SKILL.md +96 -0
- package/skills/wecom-smartsheet-schema/references/field-types.md +43 -0
- package/wecom/accounts.js +1 -0
- package/wecom/callback-inbound.js +133 -33
- package/wecom/channel-plugin.js +107 -125
- package/wecom/constants.js +83 -3
- package/wecom/mcp-config.js +146 -0
- package/wecom/mcp-tool.js +660 -0
- package/wecom/media-uploader.js +208 -0
- package/wecom/openclaw-compat.js +302 -0
- package/wecom/reqid-store.js +146 -0
- package/wecom/target.js +3 -2
- package/wecom/workspace-template.js +107 -21
- package/wecom/ws-monitor.js +778 -328
- package/image-processor.js +0 -175
|
@@ -41,10 +41,13 @@ import {
|
|
|
41
41
|
} from "./constants.js";
|
|
42
42
|
import { verifyCallbackSignature, decryptCallbackMessage } from "./callback-crypto.js";
|
|
43
43
|
import { downloadCallbackMedia } from "./callback-media.js";
|
|
44
|
+
import { assertPathInsideSandbox } from "./sandbox.js";
|
|
44
45
|
import {
|
|
45
46
|
buildInboundContext,
|
|
47
|
+
ensureDefaultSessionReasoningLevel,
|
|
46
48
|
resolveChannelCore,
|
|
47
49
|
normalizeReplyPayload,
|
|
50
|
+
normalizeReplyMediaUrlForLoad,
|
|
48
51
|
resolveReplyMediaLocalRoots,
|
|
49
52
|
} from "./ws-monitor.js";
|
|
50
53
|
|
|
@@ -169,19 +172,36 @@ async function loadLocalReplyMedia(mediaUrl, config, agentId, runtime) {
|
|
|
169
172
|
if (!normalized.startsWith("/") && !normalized.startsWith("sandbox:")) {
|
|
170
173
|
throw new Error(`Unsupported callback reply media URL scheme: ${mediaUrl}`);
|
|
171
174
|
}
|
|
175
|
+
const normalizedLocalPath = normalizeReplyMediaUrlForLoad(normalized, config, agentId);
|
|
176
|
+
if (!normalizedLocalPath) {
|
|
177
|
+
throw new Error(`Invalid callback reply media path: ${mediaUrl}`);
|
|
178
|
+
}
|
|
172
179
|
|
|
173
180
|
if (typeof runtime?.media?.loadWebMedia === "function") {
|
|
174
181
|
const localRoots = resolveReplyMediaLocalRoots(config, agentId);
|
|
175
|
-
const loaded = await runtime.media.loadWebMedia(
|
|
176
|
-
const filename = loaded.fileName || path.basename(
|
|
182
|
+
const loaded = await runtime.media.loadWebMedia(normalizedLocalPath, { localRoots });
|
|
183
|
+
const filename = loaded.fileName || path.basename(normalizedLocalPath) || "file";
|
|
177
184
|
return { buffer: loaded.buffer, filename, contentType: loaded.contentType || "" };
|
|
178
185
|
}
|
|
179
186
|
|
|
180
|
-
// Fallback when runtime.media is unavailable
|
|
187
|
+
// Fallback when runtime.media is unavailable — enforce local roots check manually
|
|
188
|
+
const localRoots = resolveReplyMediaLocalRoots(config, agentId);
|
|
189
|
+
const resolvedPath = path.resolve(normalizedLocalPath);
|
|
190
|
+
await assertPathInsideSandbox(resolvedPath, localRoots);
|
|
181
191
|
const { readFile } = await import("node:fs/promises");
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
192
|
+
const buffer = await readFile(resolvedPath);
|
|
193
|
+
return { buffer, filename: path.basename(resolvedPath) || "file", contentType: "" };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function resolveCallbackFinalText(accumulatedText, replyMediaUrls = []) {
|
|
197
|
+
const normalizedText = normalizeThinkingTags(String(accumulatedText ?? "").trim());
|
|
198
|
+
if (normalizedText) {
|
|
199
|
+
return normalizedText;
|
|
200
|
+
}
|
|
201
|
+
if (replyMediaUrls.length > 0) {
|
|
202
|
+
return "";
|
|
203
|
+
}
|
|
204
|
+
return "模型暂时无法响应,请稍后重试。";
|
|
185
205
|
}
|
|
186
206
|
|
|
187
207
|
// ---------------------------------------------------------------------------
|
|
@@ -347,20 +367,43 @@ async function processCallbackMessage({ parsedMsg, account, config, runtime }) {
|
|
|
347
367
|
});
|
|
348
368
|
ctxPayload.CommandAuthorized = commandAuthorized;
|
|
349
369
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
370
|
+
await ensureDefaultSessionReasoningLevel({
|
|
371
|
+
core,
|
|
372
|
+
storePath,
|
|
373
|
+
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
374
|
+
ctx: ctxPayload,
|
|
375
|
+
channelTag: "CB",
|
|
376
|
+
});
|
|
357
377
|
|
|
358
378
|
// --- Dispatch ---
|
|
359
|
-
const
|
|
379
|
+
const dispatchStartedAt = Date.now();
|
|
380
|
+
const logPerf = (event, extra = {}) => {
|
|
381
|
+
logger.info(`[CB:${account.accountId}] ${event}`, {
|
|
382
|
+
msgId,
|
|
383
|
+
senderId,
|
|
384
|
+
chatId,
|
|
385
|
+
routeAgentId: route.agentId,
|
|
386
|
+
sessionKey: route.sessionKey,
|
|
387
|
+
elapsedMs: Date.now() - dispatchStartedAt,
|
|
388
|
+
...extra,
|
|
389
|
+
});
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const state = {
|
|
393
|
+
accumulatedText: "",
|
|
394
|
+
replyMediaUrls: [],
|
|
395
|
+
deliveryCount: 0,
|
|
396
|
+
firstDeliveryAt: 0,
|
|
397
|
+
};
|
|
360
398
|
const streamId = `cb-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
361
399
|
|
|
362
400
|
const runDispatch = async () => {
|
|
363
401
|
try {
|
|
402
|
+
logPerf("dispatch_start", {
|
|
403
|
+
mediaCount: mediaList.length,
|
|
404
|
+
hasText: Boolean(effectiveText),
|
|
405
|
+
streamId,
|
|
406
|
+
});
|
|
364
407
|
await streamContext.run(
|
|
365
408
|
{ streamId, streamKey: peerId, agentId: route.agentId, accountId: account.accountId },
|
|
366
409
|
async () => {
|
|
@@ -370,8 +413,18 @@ async function processCallbackMessage({ parsedMsg, account, config, runtime }) {
|
|
|
370
413
|
// Disable block-streaming since Agent API replies are sent atomically
|
|
371
414
|
replyOptions: { disableBlockStreaming: true },
|
|
372
415
|
dispatcherOptions: {
|
|
373
|
-
deliver: async (payload) => {
|
|
416
|
+
deliver: async (payload, info = {}) => {
|
|
374
417
|
const normalized = normalizeReplyPayload(payload);
|
|
418
|
+
state.deliveryCount += 1;
|
|
419
|
+
if (!state.firstDeliveryAt) {
|
|
420
|
+
state.firstDeliveryAt = Date.now();
|
|
421
|
+
logPerf("first_reply_block_received", {
|
|
422
|
+
kind: info.kind ?? "unknown",
|
|
423
|
+
textLength: normalized.text.length,
|
|
424
|
+
mediaCount: normalized.mediaUrls.length,
|
|
425
|
+
deliveryCount: state.deliveryCount,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
375
428
|
state.accumulatedText += normalized.text;
|
|
376
429
|
for (const mediaUrl of normalized.mediaUrls) {
|
|
377
430
|
if (!state.replyMediaUrls.includes(mediaUrl)) {
|
|
@@ -387,7 +440,13 @@ async function processCallbackMessage({ parsedMsg, account, config, runtime }) {
|
|
|
387
440
|
},
|
|
388
441
|
);
|
|
389
442
|
|
|
390
|
-
|
|
443
|
+
logPerf("dispatch_returned", {
|
|
444
|
+
totalOutputChars: state.accumulatedText.length,
|
|
445
|
+
replyMediaCount: state.replyMediaUrls.length,
|
|
446
|
+
deliveryCount: state.deliveryCount,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const finalText = resolveCallbackFinalText(state.accumulatedText, state.replyMediaUrls);
|
|
391
450
|
|
|
392
451
|
if (!account.agentCredentials) {
|
|
393
452
|
logger.warn(`[CB:${account.accountId}] No agent credentials configured; callback reply skipped`);
|
|
@@ -397,24 +456,32 @@ async function processCallbackMessage({ parsedMsg, account, config, runtime }) {
|
|
|
397
456
|
const target = isGroupChat ? { chatId } : { toUser: senderId };
|
|
398
457
|
|
|
399
458
|
// Send reply text (chunked to stay within WeCom message size limits)
|
|
400
|
-
const chunks = splitTextByByteLimit(finalText, TEXT_CHUNK_LIMIT);
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
chunks: chunks.length,
|
|
406
|
-
totalLength: finalText.length,
|
|
407
|
-
preview: finalText.slice(0, 80),
|
|
408
|
-
});
|
|
409
|
-
for (const chunk of chunks) {
|
|
410
|
-
await agentSendText({
|
|
411
|
-
agent: account.agentCredentials,
|
|
412
|
-
...target,
|
|
413
|
-
text: chunk,
|
|
459
|
+
const chunks = finalText ? splitTextByByteLimit(finalText, TEXT_CHUNK_LIMIT) : [];
|
|
460
|
+
if (chunks.length > 0) {
|
|
461
|
+
logger.info(`[CB:${account.accountId}] → outbound`, {
|
|
462
|
+
senderId,
|
|
463
|
+
chatId,
|
|
414
464
|
format: account.agentReplyFormat,
|
|
465
|
+
chunks: chunks.length,
|
|
466
|
+
totalLength: finalText.length,
|
|
467
|
+
preview: finalText.slice(0, 80),
|
|
468
|
+
});
|
|
469
|
+
for (const chunk of chunks) {
|
|
470
|
+
await agentSendText({
|
|
471
|
+
agent: account.agentCredentials,
|
|
472
|
+
...target,
|
|
473
|
+
text: chunk,
|
|
474
|
+
format: account.agentReplyFormat,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
recordOutboundActivity({ accountId: account.accountId });
|
|
478
|
+
} else {
|
|
479
|
+
logger.info(`[CB:${account.accountId}] → outbound text skipped`, {
|
|
480
|
+
senderId,
|
|
481
|
+
chatId,
|
|
482
|
+
reason: state.replyMediaUrls.length > 0 ? "media_only_reply" : "empty_reply",
|
|
415
483
|
});
|
|
416
484
|
}
|
|
417
|
-
recordOutboundActivity({ accountId: account.accountId });
|
|
418
485
|
|
|
419
486
|
// Send any LLM-generated media (MEDIA:/FILE: directives in reply)
|
|
420
487
|
for (const mediaUrl of state.replyMediaUrls) {
|
|
@@ -443,8 +510,19 @@ async function processCallbackMessage({ parsedMsg, account, config, runtime }) {
|
|
|
443
510
|
logger.error(`[CB:${account.accountId}] Failed to send reply media: ${mediaError.message}`);
|
|
444
511
|
}
|
|
445
512
|
}
|
|
513
|
+
logPerf("dispatch_complete", {
|
|
514
|
+
totalOutputChars: state.accumulatedText.length,
|
|
515
|
+
replyMediaCount: state.replyMediaUrls.length,
|
|
516
|
+
deliveryCount: state.deliveryCount,
|
|
517
|
+
});
|
|
446
518
|
} catch (error) {
|
|
447
519
|
logger.error(`[CB:${account.accountId}] Dispatch error: ${error.message}`);
|
|
520
|
+
logPerf("dispatch_failed", {
|
|
521
|
+
error: error.message,
|
|
522
|
+
totalOutputChars: state.accumulatedText.length,
|
|
523
|
+
replyMediaCount: state.replyMediaUrls.length,
|
|
524
|
+
deliveryCount: state.deliveryCount,
|
|
525
|
+
});
|
|
448
526
|
if (account.agentCredentials) {
|
|
449
527
|
const target = isGroupChat ? { chatId } : { toUser: senderId };
|
|
450
528
|
try {
|
|
@@ -463,10 +541,27 @@ async function processCallbackMessage({ parsedMsg, account, config, runtime }) {
|
|
|
463
541
|
|
|
464
542
|
// Serialise per-sender to prevent concurrent replies to the same user
|
|
465
543
|
const lockKey = `${account.accountId}:${peerId}`;
|
|
544
|
+
const queuedAt = Date.now();
|
|
545
|
+
logPerf("dispatch_enqueued", { lockKey });
|
|
466
546
|
const previous = dispatchLocks.get(lockKey) ?? Promise.resolve();
|
|
467
|
-
const current = previous.then(
|
|
547
|
+
const current = previous.then(
|
|
548
|
+
async () => {
|
|
549
|
+
const queueWaitMs = Date.now() - queuedAt;
|
|
550
|
+
if (queueWaitMs >= 50) {
|
|
551
|
+
logPerf("dispatch_lock_acquired", { queueWaitMs });
|
|
552
|
+
}
|
|
553
|
+
return await runDispatch();
|
|
554
|
+
},
|
|
555
|
+
async () => {
|
|
556
|
+
const queueWaitMs = Date.now() - queuedAt;
|
|
557
|
+
if (queueWaitMs >= 50) {
|
|
558
|
+
logPerf("dispatch_lock_acquired", { queueWaitMs, previousFailed: true });
|
|
559
|
+
}
|
|
560
|
+
return await runDispatch();
|
|
561
|
+
},
|
|
562
|
+
);
|
|
468
563
|
dispatchLocks.set(lockKey, current);
|
|
469
|
-
current.finally(() => {
|
|
564
|
+
return await current.finally(() => {
|
|
470
565
|
if (dispatchLocks.get(lockKey) === current) {
|
|
471
566
|
dispatchLocks.delete(lockKey);
|
|
472
567
|
}
|
|
@@ -616,3 +711,8 @@ export function createCallbackHandler({ account, config, runtime }) {
|
|
|
616
711
|
return true;
|
|
617
712
|
};
|
|
618
713
|
}
|
|
714
|
+
|
|
715
|
+
export const callbackInboundTesting = {
|
|
716
|
+
loadLocalReplyMedia,
|
|
717
|
+
resolveCallbackFinalText,
|
|
718
|
+
};
|
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,8 @@ 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" } },
|
|
311
271
|
agent: {
|
|
312
272
|
type: "object",
|
|
313
273
|
additionalProperties: true,
|
|
@@ -381,7 +341,29 @@ export const wecomChannelPlugin = {
|
|
|
381
341
|
messaging: {
|
|
382
342
|
normalizeTarget: (target) => {
|
|
383
343
|
const trimmed = String(target ?? "").trim();
|
|
384
|
-
|
|
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;
|
|
385
367
|
},
|
|
386
368
|
targetResolver: {
|
|
387
369
|
looksLikeId: (value) => Boolean(String(value ?? "").trim()),
|
|
@@ -394,7 +376,14 @@ export const wecomChannelPlugin = {
|
|
|
394
376
|
listGroups: async () => [],
|
|
395
377
|
},
|
|
396
378
|
outbound: {
|
|
397
|
-
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
|
+
},
|
|
398
387
|
chunker: (text, limit) => resolveRuntimeTextChunker(text, limit),
|
|
399
388
|
textChunkLimit: TEXT_CHUNK_LIMIT,
|
|
400
389
|
sendText: async ({ cfg, to, text, accountId }) => {
|
|
@@ -437,10 +426,11 @@ export const wecomChannelPlugin = {
|
|
|
437
426
|
setOpenclawConfig(cfg);
|
|
438
427
|
const account = applyNetworkConfig(cfg, resolvedAccountId);
|
|
439
428
|
const target = resolveWecomTarget(to) ?? {};
|
|
440
|
-
const wsNoticeTarget = resolveWsNoticeTarget(target, to);
|
|
441
429
|
|
|
442
430
|
if (target.webhook) {
|
|
443
|
-
const preparedMedia = mediaUrl
|
|
431
|
+
const preparedMedia = mediaUrl
|
|
432
|
+
? await loadResolvedMedia(mediaUrl, { accountConfig: account?.config, mediaLocalRoots })
|
|
433
|
+
: undefined;
|
|
444
434
|
return sendViaWebhook({
|
|
445
435
|
cfg,
|
|
446
436
|
accountId: resolvedAccountId,
|
|
@@ -451,14 +441,6 @@ export const wecomChannelPlugin = {
|
|
|
451
441
|
});
|
|
452
442
|
}
|
|
453
443
|
|
|
454
|
-
const agentTarget =
|
|
455
|
-
target.toParty || target.toTag
|
|
456
|
-
? target
|
|
457
|
-
: target.chatId
|
|
458
|
-
? { chatId: target.chatId }
|
|
459
|
-
: { toUser: target.toUser || String(to).replace(/^wecom:/i, "") };
|
|
460
|
-
const preparedMedia = await loadResolvedMedia(mediaUrl, { mediaLocalRoots });
|
|
461
|
-
|
|
462
444
|
if (target.toParty || target.toTag) {
|
|
463
445
|
if (!account?.agentCredentials) {
|
|
464
446
|
throw new Error("Agent API is required for party/tag media delivery");
|
|
@@ -466,61 +448,63 @@ export const wecomChannelPlugin = {
|
|
|
466
448
|
return sendViaAgent({
|
|
467
449
|
cfg,
|
|
468
450
|
accountId: resolvedAccountId,
|
|
469
|
-
target
|
|
451
|
+
target,
|
|
470
452
|
text,
|
|
471
453
|
mediaUrl,
|
|
472
|
-
preparedMedia,
|
|
454
|
+
preparedMedia: await loadResolvedMedia(mediaUrl, { accountConfig: account?.config, mediaLocalRoots }),
|
|
473
455
|
});
|
|
474
456
|
}
|
|
475
457
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
cfg,
|
|
479
|
-
accountId: resolvedAccountId,
|
|
480
|
-
target: agentTarget,
|
|
481
|
-
text: wsNoticeTarget ? undefined : text,
|
|
482
|
-
mediaUrl,
|
|
483
|
-
preparedMedia,
|
|
484
|
-
});
|
|
458
|
+
const chatId = target.chatId || target.toUser || String(to).replace(/^wecom:/i, "");
|
|
459
|
+
const wsClient = getWsClient(resolvedAccountId);
|
|
485
460
|
|
|
486
|
-
|
|
461
|
+
let textAlreadySent = false;
|
|
462
|
+
if (wsClient?.isConnected && mediaUrl) {
|
|
463
|
+
if (text) {
|
|
487
464
|
try {
|
|
488
|
-
await
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
accountId: resolvedAccountId,
|
|
493
|
-
});
|
|
494
|
-
} catch (error) {
|
|
495
|
-
logger.warn(`[wecom] WS media notice failed, falling back to Agent text delivery: ${error.message}`);
|
|
496
|
-
if (text) {
|
|
497
|
-
await sendViaAgent({
|
|
498
|
-
cfg,
|
|
499
|
-
accountId: resolvedAccountId,
|
|
500
|
-
target: agentTarget,
|
|
501
|
-
text,
|
|
502
|
-
});
|
|
503
|
-
}
|
|
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}`);
|
|
504
469
|
}
|
|
505
470
|
}
|
|
506
471
|
|
|
507
|
-
|
|
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}`);
|
|
508
490
|
}
|
|
509
491
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
deliveredViaAgent: false,
|
|
518
|
-
}),
|
|
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,
|
|
519
499
|
accountId: resolvedAccountId,
|
|
500
|
+
target: agentTarget,
|
|
501
|
+
text: textAlreadySent ? undefined : text,
|
|
502
|
+
mediaUrl,
|
|
503
|
+
preparedMedia: await loadResolvedMedia(mediaUrl, { accountConfig: account?.config, mediaLocalRoots }),
|
|
520
504
|
});
|
|
521
505
|
}
|
|
522
506
|
|
|
523
|
-
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");
|
|
524
508
|
},
|
|
525
509
|
},
|
|
526
510
|
status: {
|
|
@@ -649,6 +633,4 @@ export const wecomChannelPlugin = {
|
|
|
649
633
|
},
|
|
650
634
|
};
|
|
651
635
|
|
|
652
|
-
export const wecomChannelPluginTesting = {
|
|
653
|
-
buildUnsupportedMediaNotice,
|
|
654
|
-
};
|
|
636
|
+
export const wecomChannelPluginTesting = {};
|