@dingxiang-me/openclaw-wechat 2.0.0 → 2.1.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/CHANGELOG.md +22 -0
- package/README.en.md +37 -32
- package/README.md +54 -68
- package/openclaw.plugin.json +7 -5
- package/package.json +4 -1
- package/src/core.js +12 -4
- package/src/wecom/agent-inbound-guards.js +40 -17
- package/src/wecom/api-client-send-text.js +43 -20
- package/src/wecom/bot-inbound-content.js +14 -6
- package/src/wecom/bot-inbound-executor-helpers.js +33 -7
- package/src/wecom/bot-inbound-executor.js +7 -1
- package/src/wecom/bot-inbound-guards.js +47 -22
- package/src/wecom/bot-long-connection-manager.js +16 -4
- package/src/wecom/bot-webhook-dispatch.js +2 -0
- package/src/wecom/channel-plugin.js +22 -0
- package/src/wecom/command-handlers.js +6 -0
- package/src/wecom/command-status-text.js +69 -1
- package/src/wecom/outbound-delivery.js +0 -3
- package/src/wecom/outbound-webhook-sender.js +39 -16
- package/src/wecom/pairing.js +188 -0
- package/src/wecom/plugin-constants.js +1 -1
- package/src/wecom/target-utils.js +41 -5
- package/src/wecom/voice-transcription-process.js +65 -3
- package/src/wecom/voice-transcription.js +3 -2
- package/src/wecom/webhook-adapter-normalize.js +29 -0
- package/src/wecom/webhook-adapter.js +294 -59
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { normalizeWecomAllowFromEntry } from "../core.js";
|
|
2
|
+
|
|
3
|
+
function normalizeAccountId(accountId) {
|
|
4
|
+
const normalized = String(accountId ?? "default").trim().toLowerCase();
|
|
5
|
+
return normalized || "default";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function uniqueAllowFrom(values = []) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
for (const value of Array.isArray(values) ? values : []) {
|
|
12
|
+
const normalized = normalizeWecomAllowFromEntry(value);
|
|
13
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
14
|
+
seen.add(normalized);
|
|
15
|
+
out.push(normalized);
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getPairingRuntime(api) {
|
|
21
|
+
const pairing = api?.runtime?.channel?.pairing;
|
|
22
|
+
if (!pairing || typeof pairing !== "object") return null;
|
|
23
|
+
if (typeof pairing.readAllowFromStore !== "function") return null;
|
|
24
|
+
if (typeof pairing.upsertPairingRequest !== "function") return null;
|
|
25
|
+
if (typeof pairing.buildPairingReply !== "function") return null;
|
|
26
|
+
return pairing;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function matchesAllowFrom({ allowFrom = [], senderId = "", isWecomSenderAllowed } = {}) {
|
|
30
|
+
const normalizedAllowFrom = Array.isArray(allowFrom) ? allowFrom : [];
|
|
31
|
+
if (normalizedAllowFrom.includes("*")) return true;
|
|
32
|
+
if (typeof isWecomSenderAllowed !== "function") return false;
|
|
33
|
+
return isWecomSenderAllowed({
|
|
34
|
+
senderId,
|
|
35
|
+
allowFrom: normalizedAllowFrom,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function readWecomPairingAllowFromStore({ api, accountId = "default" } = {}) {
|
|
40
|
+
const pairing = getPairingRuntime(api);
|
|
41
|
+
if (!pairing) return [];
|
|
42
|
+
try {
|
|
43
|
+
const storeEntries = await pairing.readAllowFromStore({
|
|
44
|
+
channel: "wecom",
|
|
45
|
+
accountId: normalizeAccountId(accountId),
|
|
46
|
+
});
|
|
47
|
+
return uniqueAllowFrom(storeEntries);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
api?.logger?.warn?.(`wecom: failed to read pairing store: ${String(err?.message || err)}`);
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function resolveWecomDirectMessageAccess({
|
|
55
|
+
api,
|
|
56
|
+
accountId = "default",
|
|
57
|
+
dmPolicy = {},
|
|
58
|
+
allowFromPolicy = {},
|
|
59
|
+
normalizedFromUser = "",
|
|
60
|
+
isAdminUser = false,
|
|
61
|
+
isWecomSenderAllowed,
|
|
62
|
+
} = {}) {
|
|
63
|
+
const mode = String(dmPolicy?.mode ?? "open").trim().toLowerCase() || "open";
|
|
64
|
+
const normalizedSender = normalizeWecomAllowFromEntry(normalizedFromUser);
|
|
65
|
+
const configuredAllowFrom = uniqueAllowFrom(dmPolicy?.allowFrom);
|
|
66
|
+
const baseAllowFrom = Array.isArray(allowFromPolicy?.allowFrom) ? allowFromPolicy.allowFrom : [];
|
|
67
|
+
const senderAllowedByBasePolicy =
|
|
68
|
+
isAdminUser ||
|
|
69
|
+
matchesAllowFrom({
|
|
70
|
+
senderId: normalizedSender,
|
|
71
|
+
allowFrom: baseAllowFrom,
|
|
72
|
+
isWecomSenderAllowed,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (mode === "deny") {
|
|
76
|
+
return {
|
|
77
|
+
decision: "block",
|
|
78
|
+
reason: "dm-deny",
|
|
79
|
+
rejectText: dmPolicy?.rejectMessage || "当前渠道私聊已关闭,请联系管理员。",
|
|
80
|
+
configuredAllowFrom,
|
|
81
|
+
effectiveAllowFrom: configuredAllowFrom,
|
|
82
|
+
storeAllowFrom: [],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (mode === "open") {
|
|
87
|
+
return {
|
|
88
|
+
decision: senderAllowedByBasePolicy ? "allow" : "block",
|
|
89
|
+
reason: senderAllowedByBasePolicy ? "dm-open" : "allowFrom",
|
|
90
|
+
rejectText: senderAllowedByBasePolicy
|
|
91
|
+
? ""
|
|
92
|
+
: allowFromPolicy?.rejectMessage || "当前账号未授权,请联系管理员。",
|
|
93
|
+
configuredAllowFrom,
|
|
94
|
+
effectiveAllowFrom: configuredAllowFrom,
|
|
95
|
+
storeAllowFrom: [],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (mode === "allowlist") {
|
|
100
|
+
const allowed =
|
|
101
|
+
isAdminUser ||
|
|
102
|
+
matchesAllowFrom({
|
|
103
|
+
senderId: normalizedSender,
|
|
104
|
+
allowFrom: configuredAllowFrom,
|
|
105
|
+
isWecomSenderAllowed,
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
decision: senderAllowedByBasePolicy && allowed ? "allow" : "block",
|
|
109
|
+
reason: !senderAllowedByBasePolicy ? "allowFrom" : allowed ? "dm-allowlist" : "dm-allowlist-blocked",
|
|
110
|
+
rejectText: !senderAllowedByBasePolicy
|
|
111
|
+
? allowFromPolicy?.rejectMessage || "当前账号未授权,请联系管理员。"
|
|
112
|
+
: dmPolicy?.rejectMessage || "当前私聊账号未授权,请联系管理员。",
|
|
113
|
+
configuredAllowFrom,
|
|
114
|
+
effectiveAllowFrom: configuredAllowFrom,
|
|
115
|
+
storeAllowFrom: [],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!senderAllowedByBasePolicy) {
|
|
120
|
+
return {
|
|
121
|
+
decision: "block",
|
|
122
|
+
reason: "allowFrom",
|
|
123
|
+
rejectText: allowFromPolicy?.rejectMessage || "当前账号未授权,请联系管理员。",
|
|
124
|
+
configuredAllowFrom,
|
|
125
|
+
effectiveAllowFrom: configuredAllowFrom,
|
|
126
|
+
storeAllowFrom: [],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const storeAllowFrom = await readWecomPairingAllowFromStore({
|
|
131
|
+
api,
|
|
132
|
+
accountId,
|
|
133
|
+
});
|
|
134
|
+
const effectiveAllowFrom = uniqueAllowFrom([...configuredAllowFrom, ...storeAllowFrom]);
|
|
135
|
+
const allowed =
|
|
136
|
+
isAdminUser ||
|
|
137
|
+
matchesAllowFrom({
|
|
138
|
+
senderId: normalizedSender,
|
|
139
|
+
allowFrom: effectiveAllowFrom,
|
|
140
|
+
isWecomSenderAllowed,
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
decision: allowed ? "allow" : "pairing",
|
|
144
|
+
reason: allowed ? "dm-pairing-approved" : "dm-pairing",
|
|
145
|
+
rejectText: allowed ? "" : dmPolicy?.rejectMessage || "当前私聊需先完成配对审批。",
|
|
146
|
+
configuredAllowFrom,
|
|
147
|
+
effectiveAllowFrom,
|
|
148
|
+
storeAllowFrom,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function issueWecomPairingChallenge({
|
|
153
|
+
api,
|
|
154
|
+
accountId = "default",
|
|
155
|
+
fromUser = "",
|
|
156
|
+
normalizedFromUser = "",
|
|
157
|
+
sendPairingReply,
|
|
158
|
+
} = {}) {
|
|
159
|
+
const pairing = getPairingRuntime(api);
|
|
160
|
+
const senderId = normalizeWecomAllowFromEntry(normalizedFromUser || fromUser);
|
|
161
|
+
if (!pairing || !senderId || typeof sendPairingReply !== "function") {
|
|
162
|
+
return { created: false, unsupported: true };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { code, created } = await pairing.upsertPairingRequest({
|
|
166
|
+
channel: "wecom",
|
|
167
|
+
accountId: normalizeAccountId(accountId),
|
|
168
|
+
id: senderId,
|
|
169
|
+
meta: {
|
|
170
|
+
name: String(fromUser ?? "").trim() || undefined,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
if (!created) {
|
|
174
|
+
return { created: false, code };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const replyText = pairing.buildPairingReply({
|
|
178
|
+
channel: "wecom",
|
|
179
|
+
idLine: `Your WeCom user id: ${senderId}`,
|
|
180
|
+
code,
|
|
181
|
+
});
|
|
182
|
+
try {
|
|
183
|
+
await sendPairingReply(replyText);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
api?.logger?.warn?.(`wecom: pairing reply failed: ${String(err?.message || err)}`);
|
|
186
|
+
}
|
|
187
|
+
return { created: true, code, replyText };
|
|
188
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const MAX_REQUEST_BODY_SIZE = 1024 * 1024;
|
|
2
|
-
export const PLUGIN_VERSION = "2.
|
|
2
|
+
export const PLUGIN_VERSION = "2.1.0";
|
|
3
3
|
export const WECOM_TEMP_DIR_NAME = "openclaw-wechat";
|
|
4
4
|
export const WECOM_TEMP_FILE_RETENTION_MS = 30 * 60 * 1000;
|
|
5
5
|
export const WECOM_MIN_FILE_SIZE = 5;
|
|
@@ -3,13 +3,38 @@ export function createWecomTargetResolver({ resolveWecomTarget } = {}) {
|
|
|
3
3
|
throw new Error("createWecomTargetResolver: resolveWecomTarget is required");
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
function readString(value) {
|
|
7
|
+
return String(value ?? "").trim();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function pickFirstString(...values) {
|
|
11
|
+
for (const value of values) {
|
|
12
|
+
const normalized = readString(value);
|
|
13
|
+
if (normalized) return normalized;
|
|
14
|
+
}
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
function normalizeWecomResolvedTarget(rawTarget) {
|
|
7
19
|
if (rawTarget && typeof rawTarget === "object") {
|
|
8
|
-
const toUser =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
20
|
+
const toUser = pickFirstString(
|
|
21
|
+
rawTarget.toUser,
|
|
22
|
+
rawTarget.userId,
|
|
23
|
+
rawTarget.userid,
|
|
24
|
+
rawTarget.user,
|
|
25
|
+
rawTarget.username,
|
|
26
|
+
);
|
|
27
|
+
const toParty = pickFirstString(
|
|
28
|
+
rawTarget.toParty,
|
|
29
|
+
rawTarget.partyId,
|
|
30
|
+
rawTarget.partyid,
|
|
31
|
+
rawTarget.deptId,
|
|
32
|
+
rawTarget.deptid,
|
|
33
|
+
rawTarget.departmentId,
|
|
34
|
+
);
|
|
35
|
+
const toTag = pickFirstString(rawTarget.toTag, rawTarget.tagId, rawTarget.tagid);
|
|
36
|
+
const chatId = pickFirstString(rawTarget.chatId, rawTarget.chatid, rawTarget.groupId, rawTarget.groupid);
|
|
37
|
+
const webhook = pickFirstString(rawTarget.webhook, rawTarget.webhookId, rawTarget.webhookTarget);
|
|
13
38
|
if (toUser || toParty || toTag || chatId || webhook) {
|
|
14
39
|
return {
|
|
15
40
|
...(toUser ? { toUser } : {}),
|
|
@@ -19,6 +44,17 @@ export function createWecomTargetResolver({ resolveWecomTarget } = {}) {
|
|
|
19
44
|
...(webhook ? { webhook } : {}),
|
|
20
45
|
};
|
|
21
46
|
}
|
|
47
|
+
const nestedTarget = pickFirstString(
|
|
48
|
+
rawTarget.to,
|
|
49
|
+
rawTarget.target,
|
|
50
|
+
rawTarget.value,
|
|
51
|
+
rawTarget.address,
|
|
52
|
+
rawTarget.rawTarget,
|
|
53
|
+
);
|
|
54
|
+
if (nestedTarget) {
|
|
55
|
+
const resolvedNestedTarget = resolveWecomTarget(nestedTarget);
|
|
56
|
+
return resolvedNestedTarget && typeof resolvedNestedTarget === "object" ? resolvedNestedTarget : null;
|
|
57
|
+
}
|
|
22
58
|
}
|
|
23
59
|
const resolved = resolveWecomTarget(rawTarget);
|
|
24
60
|
return resolved && typeof resolved === "object" ? resolved : null;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { constants as fsConstants } from "node:fs";
|
|
3
|
+
import { access } from "node:fs/promises";
|
|
4
|
+
import { delimiter } from "node:path";
|
|
2
5
|
|
|
3
6
|
function assertFunction(name, value) {
|
|
4
7
|
if (typeof value !== "function") {
|
|
@@ -9,6 +12,8 @@ function assertFunction(name, value) {
|
|
|
9
12
|
export function createVoiceTranscriptionProcessRuntime({
|
|
10
13
|
runProcessWithTimeoutImpl,
|
|
11
14
|
checkCommandAvailableImpl,
|
|
15
|
+
processEnv = process.env,
|
|
16
|
+
accessImpl = access,
|
|
12
17
|
} = {}) {
|
|
13
18
|
const ffmpegPathCheckCache = {
|
|
14
19
|
checked: false,
|
|
@@ -16,6 +21,50 @@ export function createVoiceTranscriptionProcessRuntime({
|
|
|
16
21
|
};
|
|
17
22
|
const commandPathCheckCache = new Map();
|
|
18
23
|
|
|
24
|
+
function readString(value) {
|
|
25
|
+
return String(value ?? "").trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function uniqueStrings(values) {
|
|
29
|
+
return Array.from(
|
|
30
|
+
new Set(
|
|
31
|
+
values
|
|
32
|
+
.map((value) => readString(value))
|
|
33
|
+
.filter(Boolean),
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function listCandidateCommandPaths(command) {
|
|
39
|
+
const normalizedCommand = readString(command);
|
|
40
|
+
if (!normalizedCommand) return [];
|
|
41
|
+
if (normalizedCommand.includes("/")) {
|
|
42
|
+
return [normalizedCommand];
|
|
43
|
+
}
|
|
44
|
+
const homeDir = readString(processEnv?.HOME);
|
|
45
|
+
const pathDirs = readString(processEnv?.PATH)
|
|
46
|
+
.split(delimiter)
|
|
47
|
+
.map((entry) => readString(entry))
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
const pythonUserBins = homeDir
|
|
50
|
+
? ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"].map(
|
|
51
|
+
(version) => `${homeDir}/Library/Python/${version}/bin`,
|
|
52
|
+
)
|
|
53
|
+
: [];
|
|
54
|
+
const searchDirs = uniqueStrings([
|
|
55
|
+
...pathDirs,
|
|
56
|
+
homeDir ? `${homeDir}/.local/bin` : "",
|
|
57
|
+
homeDir ? `${homeDir}/bin` : "",
|
|
58
|
+
...pythonUserBins,
|
|
59
|
+
"/usr/local/bin",
|
|
60
|
+
"/opt/homebrew/bin",
|
|
61
|
+
]);
|
|
62
|
+
return uniqueStrings([
|
|
63
|
+
normalizedCommand,
|
|
64
|
+
...searchDirs.map((dir) => `${dir}/${normalizedCommand}`),
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
|
|
19
68
|
function runProcessWithTimeout({ command, args, timeoutMs = 15000, allowNonZeroExitCode = false }) {
|
|
20
69
|
if (typeof runProcessWithTimeoutImpl === "function") {
|
|
21
70
|
return runProcessWithTimeoutImpl({ command, args, timeoutMs, allowNonZeroExitCode });
|
|
@@ -72,6 +121,16 @@ export function createVoiceTranscriptionProcessRuntime({
|
|
|
72
121
|
if (commandPathCheckCache.has(normalized)) {
|
|
73
122
|
return commandPathCheckCache.get(normalized);
|
|
74
123
|
}
|
|
124
|
+
if (normalized.includes("/")) {
|
|
125
|
+
try {
|
|
126
|
+
await accessImpl(normalized, fsConstants.X_OK);
|
|
127
|
+
commandPathCheckCache.set(normalized, true);
|
|
128
|
+
return true;
|
|
129
|
+
} catch {
|
|
130
|
+
commandPathCheckCache.set(normalized, false);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
75
134
|
try {
|
|
76
135
|
await runProcessWithTimeout({
|
|
77
136
|
command: normalized,
|
|
@@ -101,15 +160,18 @@ export function createVoiceTranscriptionProcessRuntime({
|
|
|
101
160
|
function listLocalWhisperCommandCandidates({ voiceConfig } = {}) {
|
|
102
161
|
const provider = String(voiceConfig?.provider ?? "").trim().toLowerCase();
|
|
103
162
|
const explicitCommand = String(voiceConfig?.command ?? "").trim();
|
|
104
|
-
const
|
|
163
|
+
const fallbackCommandNames =
|
|
105
164
|
provider === "local-whisper"
|
|
106
165
|
? ["whisper"]
|
|
107
166
|
: provider === "local-whisper-cli"
|
|
108
167
|
? ["whisper-cli"]
|
|
109
168
|
: [];
|
|
110
|
-
const
|
|
169
|
+
const commandNames = explicitCommand
|
|
170
|
+
? uniqueStrings([explicitCommand, ...fallbackCommandNames])
|
|
171
|
+
: uniqueStrings(fallbackCommandNames);
|
|
172
|
+
const candidates = uniqueStrings(commandNames.flatMap((command) => listCandidateCommandPaths(command)));
|
|
111
173
|
|
|
112
|
-
if (
|
|
174
|
+
if (commandNames.length === 0) {
|
|
113
175
|
return {
|
|
114
176
|
provider,
|
|
115
177
|
explicitCommand,
|
|
@@ -29,6 +29,7 @@ export function createWecomVoiceTranscriber({
|
|
|
29
29
|
const processRuntime = createVoiceTranscriptionProcessRuntime({
|
|
30
30
|
runProcessWithTimeoutImpl,
|
|
31
31
|
checkCommandAvailableImpl,
|
|
32
|
+
processEnv,
|
|
32
33
|
});
|
|
33
34
|
const {
|
|
34
35
|
runProcessWithTimeout,
|
|
@@ -206,7 +207,7 @@ export function createWecomVoiceTranscriber({
|
|
|
206
207
|
if (voiceConfig.requireModelPath !== false && !voiceConfig.modelPath) {
|
|
207
208
|
throw new Error("voiceTranscription.modelPath is required for local-whisper-cli (or set requireModelPath=false)");
|
|
208
209
|
}
|
|
209
|
-
return transcribeWithWhisperCli({
|
|
210
|
+
return await transcribeWithWhisperCli({
|
|
210
211
|
command,
|
|
211
212
|
modelPath: voiceConfig.modelPath,
|
|
212
213
|
audioPath,
|
|
@@ -217,7 +218,7 @@ export function createWecomVoiceTranscriber({
|
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
if (provider === "local-whisper") {
|
|
220
|
-
return transcribeWithWhisperPython({
|
|
221
|
+
return await transcribeWithWhisperPython({
|
|
221
222
|
command,
|
|
222
223
|
model: voiceConfig.model,
|
|
223
224
|
audioPath,
|
|
@@ -18,6 +18,25 @@ export function dedupeUrlList(urls) {
|
|
|
18
18
|
return out;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function dedupeMediaEntries(entries) {
|
|
22
|
+
const seen = new Map();
|
|
23
|
+
for (const rawEntry of Array.isArray(entries) ? entries : []) {
|
|
24
|
+
if (!rawEntry || typeof rawEntry !== "object") continue;
|
|
25
|
+
const url = normalizeToken(rawEntry.url);
|
|
26
|
+
if (!url) continue;
|
|
27
|
+
const aesKey = normalizeToken(rawEntry.aesKey);
|
|
28
|
+
const existing = seen.get(url);
|
|
29
|
+
if (!existing) {
|
|
30
|
+
seen.set(url, { url, aesKey });
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (!existing.aesKey && aesKey) {
|
|
34
|
+
seen.set(url, { url, aesKey });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return Array.from(seen.values());
|
|
38
|
+
}
|
|
39
|
+
|
|
21
40
|
export function collectWecomBotImageUrls(imageLike) {
|
|
22
41
|
return dedupeUrlList([
|
|
23
42
|
imageLike?.url,
|
|
@@ -28,6 +47,16 @@ export function collectWecomBotImageUrls(imageLike) {
|
|
|
28
47
|
]);
|
|
29
48
|
}
|
|
30
49
|
|
|
50
|
+
export function collectWecomBotImageEntries(imageLike) {
|
|
51
|
+
const aesKey = normalizeToken(imageLike?.aeskey || imageLike?.aes_key || imageLike?.aesKey);
|
|
52
|
+
return dedupeMediaEntries(
|
|
53
|
+
collectWecomBotImageUrls(imageLike).map((url) => ({
|
|
54
|
+
url,
|
|
55
|
+
aesKey,
|
|
56
|
+
})),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
31
60
|
export function normalizeWecomBotOutboundMediaUrls(payload = {}) {
|
|
32
61
|
return dedupeUrlList([
|
|
33
62
|
payload?.mediaUrl,
|