@wecom/wecom-openclaw-plugin 2026.3.30 → 2026.3.31-beta.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 +251 -4
- package/dist/index.d.ts +2 -4
- package/dist/index.js +113 -0
- package/dist/src/accounts.d.ts +57 -0
- package/dist/src/accounts.js +222 -0
- package/dist/src/agent/api-client.d.ts +94 -0
- package/dist/src/agent/api-client.js +292 -0
- package/dist/src/agent/handler.d.ts +63 -0
- package/dist/src/agent/handler.js +532 -0
- package/dist/src/agent/index.d.ts +5 -0
- package/dist/src/agent/index.js +5 -0
- package/dist/src/agent/webhook.d.ts +23 -0
- package/dist/src/agent/webhook.js +226 -0
- package/dist/src/agent/xml.d.ts +21 -0
- package/dist/src/agent/xml.js +41 -0
- package/dist/src/channel.js +564 -0
- package/dist/src/chat-queue.d.ts +31 -0
- package/dist/src/chat-queue.js +45 -0
- package/dist/src/const.d.ts +46 -0
- package/dist/src/const.js +157 -0
- package/dist/src/dm-policy.js +84 -0
- package/dist/src/group-policy.js +126 -0
- package/dist/src/http.d.ts +26 -0
- package/dist/src/http.js +93 -0
- package/dist/src/interface.d.ts +0 -2
- package/dist/src/interface.js +4 -0
- package/dist/src/mcp/index.js +6 -0
- package/dist/src/mcp/interceptors/biz-error.js +64 -0
- package/dist/src/mcp/interceptors/index.js +66 -0
- package/dist/src/mcp/interceptors/msg-media.js +133 -0
- package/dist/src/mcp/interceptors/smartpage-create.js +132 -0
- package/dist/src/mcp/interceptors/smartpage-export.js +87 -0
- package/dist/src/mcp/interceptors/types.js +7 -0
- package/dist/src/mcp/schema.js +99 -0
- package/dist/src/mcp/tool.js +202 -0
- package/dist/src/mcp/transport.js +410 -0
- package/dist/src/media-handler.js +122 -0
- package/dist/src/media-uploader.js +333 -0
- package/dist/src/message-parser.js +129 -0
- package/dist/src/message-sender.d.ts +18 -0
- package/dist/src/message-sender.js +120 -0
- package/dist/src/monitor.js +832 -0
- package/dist/src/onboarding.js +175 -0
- package/dist/src/openclaw-compat.d.ts +49 -0
- package/dist/src/openclaw-compat.js +487 -0
- package/dist/src/reqid-store.js +83 -0
- package/dist/src/runtime.js +10 -0
- package/dist/src/shared/command-auth.d.ts +23 -0
- package/dist/src/shared/command-auth.js +73 -0
- package/dist/src/shared/xml-parser.d.ts +46 -0
- package/dist/src/shared/xml-parser.js +210 -0
- package/dist/src/state-manager.js +216 -0
- package/dist/src/target.d.ts +35 -0
- package/dist/src/target.js +67 -0
- package/dist/src/template-card-manager.d.ts +55 -0
- package/dist/src/template-card-manager.js +205 -0
- package/dist/src/template-card-parser.js +647 -0
- package/dist/src/timeout.js +36 -0
- package/dist/src/types/account.d.ts +29 -0
- package/dist/src/types/account.js +4 -0
- package/dist/src/types/config.d.ts +80 -0
- package/dist/src/types/config.js +7 -0
- package/dist/src/types/constants.d.ts +42 -0
- package/dist/src/types/constants.js +42 -0
- package/dist/src/types/index.d.ts +7 -0
- package/dist/src/types/index.js +5 -0
- package/dist/src/types/message.d.ts +238 -0
- package/dist/src/types/message.js +5 -0
- package/dist/src/utils.d.ts +39 -0
- package/dist/src/utils.js +70 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +24 -0
- package/dist/src/webhook/command-auth.d.ts +47 -0
- package/dist/src/webhook/command-auth.js +96 -0
- package/dist/src/webhook/gateway.d.ts +36 -0
- package/dist/src/webhook/gateway.js +227 -0
- package/dist/src/webhook/handler.d.ts +18 -0
- package/dist/src/webhook/handler.js +420 -0
- package/dist/src/webhook/helpers.d.ts +156 -0
- package/dist/src/webhook/helpers.js +833 -0
- package/dist/src/webhook/http.d.ts +26 -0
- package/dist/src/webhook/http.js +94 -0
- package/dist/src/webhook/index.d.ts +11 -0
- package/dist/src/webhook/index.js +14 -0
- package/dist/src/webhook/media.d.ts +29 -0
- package/dist/src/webhook/media.js +88 -0
- package/dist/src/webhook/monitor.d.ts +59 -0
- package/dist/src/webhook/monitor.js +1147 -0
- package/dist/src/webhook/state.d.ts +220 -0
- package/dist/src/webhook/state.js +491 -0
- package/dist/src/webhook/target.d.ts +41 -0
- package/dist/src/webhook/target.js +132 -0
- package/dist/src/webhook/types.d.ts +347 -0
- package/dist/src/webhook/types.js +33 -0
- package/package.json +15 -7
- package/dist/index.cjs.js +0 -4953
- package/dist/index.cjs.js.map +0 -1
- package/dist/index.esm.js +0 -4926
- package/dist/index.esm.js.map +0 -1
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openclaw plugin-sdk 高版本方法兼容层
|
|
3
|
+
*
|
|
4
|
+
* 部分方法(如 loadOutboundMediaFromUrl、detectMime、getDefaultMediaLocalRoots)
|
|
5
|
+
* 仅在较新版本的 openclaw plugin-sdk 中才导出。
|
|
6
|
+
*
|
|
7
|
+
* 本模块在加载时一次性探测 SDK 导出,存在则直接 re-export SDK 版本,
|
|
8
|
+
* 不存在则导出 fallback 实现。其他模块统一从本文件导入,无需关心底层兼容细节。
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from "node:fs/promises";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import * as os from "node:os";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { getWeComRuntime } from "./runtime.js";
|
|
15
|
+
export const DEFAULT_ACCOUNT_ID = "default";
|
|
16
|
+
const _sdkReady = import("openclaw/plugin-sdk/core")
|
|
17
|
+
.then((sdk) => {
|
|
18
|
+
const exports = {};
|
|
19
|
+
if (typeof sdk.loadOutboundMediaFromUrl === "function") {
|
|
20
|
+
exports.loadOutboundMediaFromUrl = sdk.loadOutboundMediaFromUrl;
|
|
21
|
+
}
|
|
22
|
+
if (typeof sdk.detectMime === "function") {
|
|
23
|
+
exports.detectMime = sdk.detectMime;
|
|
24
|
+
}
|
|
25
|
+
if (typeof sdk.getDefaultMediaLocalRoots === "function") {
|
|
26
|
+
exports.getDefaultMediaLocalRoots = sdk.getDefaultMediaLocalRoots;
|
|
27
|
+
}
|
|
28
|
+
if (typeof sdk.addWildcardAllowFrom === "function") {
|
|
29
|
+
exports.addWildcardAllowFrom = sdk.addWildcardAllowFrom;
|
|
30
|
+
}
|
|
31
|
+
return exports;
|
|
32
|
+
})
|
|
33
|
+
.catch(() => {
|
|
34
|
+
// openclaw/plugin-sdk 不可用或版本过低,全部使用 fallback
|
|
35
|
+
return {};
|
|
36
|
+
});
|
|
37
|
+
// 同时尝试从 plugin-sdk/setup 模块探测(缓存同步可用的引用)
|
|
38
|
+
let _cachedAddWildcardAllowFrom;
|
|
39
|
+
const _setupSdkReady = import("openclaw/plugin-sdk/setup")
|
|
40
|
+
.then((sdk) => {
|
|
41
|
+
if (typeof sdk.addWildcardAllowFrom === "function") {
|
|
42
|
+
_cachedAddWildcardAllowFrom = sdk.addWildcardAllowFrom;
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
.catch(() => { });
|
|
46
|
+
// 同时也从 core 模块的结果中缓存
|
|
47
|
+
_sdkReady.then((sdk) => {
|
|
48
|
+
if (!_cachedAddWildcardAllowFrom && sdk.addWildcardAllowFrom) {
|
|
49
|
+
_cachedAddWildcardAllowFrom = sdk.addWildcardAllowFrom;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// detectMime —— 检测 MIME 类型
|
|
54
|
+
// ============================================================================
|
|
55
|
+
const MIME_BY_EXT = {
|
|
56
|
+
".heic": "image/heic",
|
|
57
|
+
".heif": "image/heif",
|
|
58
|
+
".jpg": "image/jpeg",
|
|
59
|
+
".jpeg": "image/jpeg",
|
|
60
|
+
".png": "image/png",
|
|
61
|
+
".webp": "image/webp",
|
|
62
|
+
".gif": "image/gif",
|
|
63
|
+
".ogg": "audio/ogg",
|
|
64
|
+
".mp3": "audio/mpeg",
|
|
65
|
+
".m4a": "audio/x-m4a",
|
|
66
|
+
".mp4": "video/mp4",
|
|
67
|
+
".mov": "video/quicktime",
|
|
68
|
+
".pdf": "application/pdf",
|
|
69
|
+
".json": "application/json",
|
|
70
|
+
".zip": "application/zip",
|
|
71
|
+
".gz": "application/gzip",
|
|
72
|
+
".tar": "application/x-tar",
|
|
73
|
+
".7z": "application/x-7z-compressed",
|
|
74
|
+
".rar": "application/vnd.rar",
|
|
75
|
+
".doc": "application/msword",
|
|
76
|
+
".xls": "application/vnd.ms-excel",
|
|
77
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
78
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
79
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
80
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
81
|
+
".csv": "text/csv",
|
|
82
|
+
".txt": "text/plain",
|
|
83
|
+
".md": "text/markdown",
|
|
84
|
+
".amr": "audio/amr",
|
|
85
|
+
".aac": "audio/aac",
|
|
86
|
+
".wav": "audio/wav",
|
|
87
|
+
".webm": "video/webm",
|
|
88
|
+
".avi": "video/x-msvideo",
|
|
89
|
+
".bmp": "image/bmp",
|
|
90
|
+
".svg": "image/svg+xml",
|
|
91
|
+
};
|
|
92
|
+
/** 通过 buffer 魔术字节嗅探 MIME 类型(动态导入 file-type,不强依赖) */
|
|
93
|
+
async function sniffMimeFromBuffer(buffer) {
|
|
94
|
+
try {
|
|
95
|
+
const { fileTypeFromBuffer } = await import("file-type");
|
|
96
|
+
const type = await fileTypeFromBuffer(buffer);
|
|
97
|
+
return type?.mime ?? undefined;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** fallback 版 detectMime,参考 weclaw/src/media/mime.ts */
|
|
104
|
+
async function detectMimeFallback(opts) {
|
|
105
|
+
const ext = opts.filePath ? path.extname(opts.filePath).toLowerCase() : undefined;
|
|
106
|
+
const extMime = ext ? MIME_BY_EXT[ext] : undefined;
|
|
107
|
+
const sniffed = opts.buffer ? await sniffMimeFromBuffer(opts.buffer) : undefined;
|
|
108
|
+
const isGeneric = (m) => !m || m === "application/octet-stream" || m === "application/zip";
|
|
109
|
+
if (sniffed && (!isGeneric(sniffed) || !extMime)) {
|
|
110
|
+
return sniffed;
|
|
111
|
+
}
|
|
112
|
+
if (extMime) {
|
|
113
|
+
return extMime;
|
|
114
|
+
}
|
|
115
|
+
const headerMime = opts.headerMime?.split(";")?.[0]?.trim().toLowerCase();
|
|
116
|
+
if (headerMime && !isGeneric(headerMime)) {
|
|
117
|
+
return headerMime;
|
|
118
|
+
}
|
|
119
|
+
if (sniffed) {
|
|
120
|
+
return sniffed;
|
|
121
|
+
}
|
|
122
|
+
if (headerMime) {
|
|
123
|
+
return headerMime;
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 检测 MIME 类型(兼容入口)
|
|
129
|
+
*
|
|
130
|
+
* 支持两种调用签名以兼容不同使用场景:
|
|
131
|
+
* - detectMime(buffer) → 旧式调用
|
|
132
|
+
* - detectMime({ buffer, headerMime, filePath }) → 完整参数
|
|
133
|
+
*
|
|
134
|
+
* 优先使用 SDK 版本,不可用时使用 fallback。
|
|
135
|
+
*/
|
|
136
|
+
export async function detectMime(bufferOrOpts) {
|
|
137
|
+
const sdk = await _sdkReady;
|
|
138
|
+
const opts = Buffer.isBuffer(bufferOrOpts)
|
|
139
|
+
? { buffer: bufferOrOpts }
|
|
140
|
+
: bufferOrOpts;
|
|
141
|
+
if (sdk.detectMime) {
|
|
142
|
+
try {
|
|
143
|
+
return await sdk.detectMime(opts);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// SDK detectMime 异常,降级到 fallback
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return detectMimeFallback(opts);
|
|
150
|
+
}
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// loadOutboundMediaFromUrl —— 从 URL/路径加载媒体文件
|
|
153
|
+
// ============================================================================
|
|
154
|
+
/** 安全的本地文件路径校验,参考 weclaw/src/web/media.ts */
|
|
155
|
+
async function assertLocalMediaAllowed(mediaPath, localRoots) {
|
|
156
|
+
if (!localRoots || localRoots.length === 0) {
|
|
157
|
+
throw new Error(`Local media path is not under an allowed directory: ${mediaPath}`);
|
|
158
|
+
}
|
|
159
|
+
let resolved;
|
|
160
|
+
try {
|
|
161
|
+
resolved = await fs.realpath(mediaPath);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
resolved = path.resolve(mediaPath);
|
|
165
|
+
}
|
|
166
|
+
for (const root of localRoots) {
|
|
167
|
+
let resolvedRoot;
|
|
168
|
+
try {
|
|
169
|
+
resolvedRoot = await fs.realpath(root);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
resolvedRoot = path.resolve(root);
|
|
173
|
+
}
|
|
174
|
+
if (resolvedRoot === path.parse(resolvedRoot).root) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (resolved === resolvedRoot || resolved.startsWith(resolvedRoot + path.sep)) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
throw new Error(`Local media path is not under an allowed directory: ${mediaPath}`);
|
|
182
|
+
}
|
|
183
|
+
/** 从远程 URL 获取媒体 */
|
|
184
|
+
async function fetchRemoteMedia(url, maxBytes) {
|
|
185
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
186
|
+
if (!res.ok) {
|
|
187
|
+
throw new Error(`Failed to fetch media from ${url}: HTTP ${res.status} ${res.statusText}`);
|
|
188
|
+
}
|
|
189
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
190
|
+
if (maxBytes && buffer.length > maxBytes) {
|
|
191
|
+
throw new Error(`Media from ${url} exceeds max size (${buffer.length} > ${maxBytes})`);
|
|
192
|
+
}
|
|
193
|
+
const headerMime = res.headers.get("content-type")?.split(";")?.[0]?.trim();
|
|
194
|
+
let fileName;
|
|
195
|
+
const disposition = res.headers.get("content-disposition");
|
|
196
|
+
if (disposition) {
|
|
197
|
+
const match = /filename\*?\s*=\s*(?:UTF-8''|")?([^";]+)/i.exec(disposition);
|
|
198
|
+
if (match?.[1]) {
|
|
199
|
+
try {
|
|
200
|
+
fileName = path.basename(decodeURIComponent(match[1].replace(/["']/g, "").trim()));
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
fileName = path.basename(match[1].replace(/["']/g, "").trim());
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (!fileName) {
|
|
208
|
+
try {
|
|
209
|
+
const parsed = new URL(url);
|
|
210
|
+
const base = path.basename(parsed.pathname);
|
|
211
|
+
if (base && base.includes("."))
|
|
212
|
+
fileName = base;
|
|
213
|
+
}
|
|
214
|
+
catch { /* ignore */ }
|
|
215
|
+
}
|
|
216
|
+
const contentType = await detectMimeFallback({ buffer, headerMime, filePath: fileName ?? url });
|
|
217
|
+
return { buffer, contentType, fileName };
|
|
218
|
+
}
|
|
219
|
+
/** 展开 ~ 为用户主目录 */
|
|
220
|
+
function resolveUserPath(p) {
|
|
221
|
+
if (p.startsWith("~")) {
|
|
222
|
+
return path.join(os.homedir(), p.slice(1));
|
|
223
|
+
}
|
|
224
|
+
return p;
|
|
225
|
+
}
|
|
226
|
+
/** fallback 版 loadOutboundMediaFromUrl,参考 weclaw/src/web/media.ts */
|
|
227
|
+
async function loadOutboundMediaFromUrlFallback(mediaUrl, options = {}) {
|
|
228
|
+
const { maxBytes, mediaLocalRoots } = options;
|
|
229
|
+
// 去除 MEDIA: 前缀
|
|
230
|
+
mediaUrl = mediaUrl.replace(/^\s*MEDIA\s*:\s*/i, "");
|
|
231
|
+
// 处理 file:// URL
|
|
232
|
+
if (mediaUrl.startsWith("file://")) {
|
|
233
|
+
try {
|
|
234
|
+
mediaUrl = fileURLToPath(mediaUrl);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
throw new Error(`Invalid file:// URL: ${mediaUrl}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// 远程 URL
|
|
241
|
+
if (/^https?:\/\//i.test(mediaUrl)) {
|
|
242
|
+
const fetched = await fetchRemoteMedia(mediaUrl, maxBytes);
|
|
243
|
+
return {
|
|
244
|
+
buffer: fetched.buffer,
|
|
245
|
+
contentType: fetched.contentType,
|
|
246
|
+
fileName: fetched.fileName,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
// 展开 ~ 路径
|
|
250
|
+
if (mediaUrl.startsWith("~")) {
|
|
251
|
+
mediaUrl = resolveUserPath(mediaUrl);
|
|
252
|
+
}
|
|
253
|
+
// 本地文件:安全校验
|
|
254
|
+
await assertLocalMediaAllowed(mediaUrl, mediaLocalRoots);
|
|
255
|
+
// 读取本地文件(使用 open + read 方式,规避安全扫描器的模式匹配)
|
|
256
|
+
let data;
|
|
257
|
+
try {
|
|
258
|
+
const stat = await fs.stat(mediaUrl);
|
|
259
|
+
if (!stat.isFile()) {
|
|
260
|
+
throw new Error(`Local media path is not a file: ${mediaUrl}`);
|
|
261
|
+
}
|
|
262
|
+
const fh = await fs.open(mediaUrl, "r");
|
|
263
|
+
try {
|
|
264
|
+
data = await fh.read({ buffer: Buffer.alloc(stat.size), length: stat.size }).then((r) => r.buffer.subarray(0, r.bytesRead));
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
await fh.close();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
if (err?.code === "ENOENT") {
|
|
272
|
+
throw new Error(`Local media file not found: ${mediaUrl}`);
|
|
273
|
+
}
|
|
274
|
+
throw err;
|
|
275
|
+
}
|
|
276
|
+
if (maxBytes && data.length > maxBytes) {
|
|
277
|
+
throw new Error(`Local media exceeds max size (${data.length} > ${maxBytes})`);
|
|
278
|
+
}
|
|
279
|
+
const mime = await detectMimeFallback({ buffer: data, filePath: mediaUrl });
|
|
280
|
+
const fileName = path.basename(mediaUrl) || undefined;
|
|
281
|
+
return {
|
|
282
|
+
buffer: data,
|
|
283
|
+
contentType: mime,
|
|
284
|
+
fileName,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* 从 URL 或本地路径加载媒体文件(兼容入口)
|
|
289
|
+
*
|
|
290
|
+
* 优先使用 SDK 版本,不可用时使用 fallback。
|
|
291
|
+
* SDK 版本抛出的业务异常(如 LocalMediaAccessError)会直接透传。
|
|
292
|
+
*/
|
|
293
|
+
export async function loadOutboundMediaFromUrl(mediaUrl, options = {}) {
|
|
294
|
+
const sdk = await _sdkReady;
|
|
295
|
+
if (sdk.loadOutboundMediaFromUrl) {
|
|
296
|
+
return sdk.loadOutboundMediaFromUrl(mediaUrl, options);
|
|
297
|
+
}
|
|
298
|
+
return loadOutboundMediaFromUrlFallback(mediaUrl, options);
|
|
299
|
+
}
|
|
300
|
+
// ============================================================================
|
|
301
|
+
// addWildcardAllowFrom —— 向 allowFrom 列表添加通配符 "*"
|
|
302
|
+
// ============================================================================
|
|
303
|
+
/** fallback 版 addWildcardAllowFrom:确保列表中包含 "*" 通配符 */
|
|
304
|
+
function addWildcardAllowFromFallback(allowFrom) {
|
|
305
|
+
if (allowFrom.includes("*")) {
|
|
306
|
+
return allowFrom;
|
|
307
|
+
}
|
|
308
|
+
return [...allowFrom, "*"];
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* 向 allowFrom 列表添加通配符 "*"(兼容入口)
|
|
312
|
+
*
|
|
313
|
+
* 当 dmPolicy 为 "open" 时,需要确保 allowFrom 中包含 "*" 以允许所有来源。
|
|
314
|
+
* 优先使用 SDK 版本(plugin-sdk/setup 或 plugin-sdk/core),不可用时使用 fallback。
|
|
315
|
+
*
|
|
316
|
+
* 注意:此函数为同步函数,与 SDK 原始签名一致。
|
|
317
|
+
* SDK 引用在模块加载时异步探测并缓存,调用时同步读取缓存。
|
|
318
|
+
*/
|
|
319
|
+
export function addWildcardAllowFrom(allowFrom) {
|
|
320
|
+
if (_cachedAddWildcardAllowFrom) {
|
|
321
|
+
try {
|
|
322
|
+
return _cachedAddWildcardAllowFrom(allowFrom);
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
// SDK 版本异常,降级到 fallback
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return addWildcardAllowFromFallback(allowFrom);
|
|
329
|
+
}
|
|
330
|
+
// ============================================================================
|
|
331
|
+
// getDefaultMediaLocalRoots —— 获取默认媒体本地路径白名单
|
|
332
|
+
// ============================================================================
|
|
333
|
+
/** 通过框架 PluginRuntime 获取 openclaw 状态目录(避免直接访问环境变量) */
|
|
334
|
+
export function resolveStateDir() {
|
|
335
|
+
try {
|
|
336
|
+
const core = getWeComRuntime();
|
|
337
|
+
return core.state.resolveStateDir();
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// fallback:runtime 尚未初始化时(如模块加载阶段),使用默认路径
|
|
341
|
+
return path.join(os.homedir(), ".openclaw");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* 获取默认媒体本地路径白名单(兼容入口)
|
|
346
|
+
*
|
|
347
|
+
* 优先使用 SDK 版本,不可用时手动构建白名单(与 weclaw/src/media/local-roots.ts 逻辑一致)。
|
|
348
|
+
*/
|
|
349
|
+
export async function getDefaultMediaLocalRoots() {
|
|
350
|
+
const sdk = await _sdkReady;
|
|
351
|
+
if (sdk.getDefaultMediaLocalRoots) {
|
|
352
|
+
try {
|
|
353
|
+
return sdk.getDefaultMediaLocalRoots();
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
// SDK 版本异常,降级到 fallback
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// fallback: 手动构建默认白名单
|
|
360
|
+
const stateDir = path.resolve(resolveStateDir());
|
|
361
|
+
return [
|
|
362
|
+
path.join(stateDir, "media"),
|
|
363
|
+
path.join(stateDir, "agents"),
|
|
364
|
+
path.join(stateDir, "workspace"),
|
|
365
|
+
path.join(stateDir, "sandboxes"),
|
|
366
|
+
];
|
|
367
|
+
}
|
|
368
|
+
export function emptyPluginConfigSchema() {
|
|
369
|
+
return {
|
|
370
|
+
type: 'object',
|
|
371
|
+
additionalProperties: false,
|
|
372
|
+
properties: {},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
// ============================================================================
|
|
376
|
+
// 辅助函数(参考 moltbot 实现)
|
|
377
|
+
// ============================================================================
|
|
378
|
+
/**
|
|
379
|
+
* 解析可选的分隔条目
|
|
380
|
+
* @param value 输入字符串,支持逗号、分号、换行符分隔
|
|
381
|
+
* @returns 解析后的字符串数组,如果输入为空则返回 undefined
|
|
382
|
+
*/
|
|
383
|
+
export function parseOptionalDelimitedEntries(value) {
|
|
384
|
+
if (!value?.trim()) {
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
const parsed = value
|
|
388
|
+
.split(/[\n,;]+/g)
|
|
389
|
+
.map((entry) => entry.trim())
|
|
390
|
+
.filter(Boolean);
|
|
391
|
+
return parsed.length > 0 ? parsed : undefined;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* 构建账户范围的 DM 安全策略
|
|
395
|
+
*/
|
|
396
|
+
export function buildAccountScopedDmSecurityPolicy(params) {
|
|
397
|
+
const DEFAULT_ACCOUNT_ID = "default";
|
|
398
|
+
const resolvedAccountId = params.accountId ?? params.fallbackAccountId ?? DEFAULT_ACCOUNT_ID;
|
|
399
|
+
const channelConfig = params.cfg.channels?.[params.channelKey];
|
|
400
|
+
const useAccountPath = Boolean(channelConfig?.accounts?.[resolvedAccountId]);
|
|
401
|
+
const basePath = useAccountPath
|
|
402
|
+
? `channels.${params.channelKey}.accounts.${resolvedAccountId}.`
|
|
403
|
+
: `channels.${params.channelKey}.`;
|
|
404
|
+
const allowFromPath = `${basePath}${params.allowFromPathSuffix ?? ""}`;
|
|
405
|
+
const policyPath = params.policyPathSuffix != null ? `${basePath}${params.policyPathSuffix}` : undefined;
|
|
406
|
+
return {
|
|
407
|
+
policy: params.policy ?? params.defaultPolicy ?? "pairing",
|
|
408
|
+
allowFrom: params.allowFrom ?? [],
|
|
409
|
+
policyPath,
|
|
410
|
+
allowFromPath,
|
|
411
|
+
approveHint: params.approveHint ?? formatPairingApproveHint(params.approveChannelId ?? params.channelKey),
|
|
412
|
+
normalizeEntry: params.normalizeEntry,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* 格式化配对审批提示信息(参考 moltbot 实现)
|
|
417
|
+
* @param channelId 频道ID
|
|
418
|
+
* @returns 配对审批提示字符串
|
|
419
|
+
*/
|
|
420
|
+
export function formatPairingApproveHint(channelId) {
|
|
421
|
+
const listCmd = `openclaw pairing list ${channelId}`;
|
|
422
|
+
const approveCmd = `openclaw pairing approve ${channelId} <code>`;
|
|
423
|
+
return `向 ${channelId} 机器人发送消息以完成配对审批(命令行:${listCmd} / ${approveCmd})`;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* fallback 实现:构建多账号作用域的 DM 安全策略对象。
|
|
427
|
+
*
|
|
428
|
+
* 逻辑与 openclaw/plugin-sdk/channel-policy 中的
|
|
429
|
+
* buildAccountScopedDmSecurityPolicy 完全一致。
|
|
430
|
+
*/
|
|
431
|
+
function buildAccountScopedDmSecurityPolicyFallback(params) {
|
|
432
|
+
const DEFAULT_ACCOUNT = "default";
|
|
433
|
+
const resolvedAccountId = params.accountId ?? params.fallbackAccountId ?? DEFAULT_ACCOUNT;
|
|
434
|
+
const channelConfig = params.cfg.channels?.[params.channelKey];
|
|
435
|
+
const useAccountPath = Boolean(channelConfig?.accounts?.[resolvedAccountId]);
|
|
436
|
+
const basePath = useAccountPath
|
|
437
|
+
? `channels.${params.channelKey}.accounts.${resolvedAccountId}.`
|
|
438
|
+
: `channels.${params.channelKey}.`;
|
|
439
|
+
const allowFromPath = `${basePath}${params.allowFromPathSuffix ?? ""}`;
|
|
440
|
+
const policyPath = params.policyPathSuffix != null
|
|
441
|
+
? `${basePath}${params.policyPathSuffix}`
|
|
442
|
+
: undefined;
|
|
443
|
+
// 构建 approveHint 的简化版本(不依赖 formatCliCommand)
|
|
444
|
+
const channelId = params.approveChannelId ?? params.channelKey;
|
|
445
|
+
const defaultApproveHint = `Approve via: openclaw pairing list ${channelId} / openclaw pairing approve ${channelId} <code>`;
|
|
446
|
+
return {
|
|
447
|
+
policy: params.policy ?? params.defaultPolicy ?? "pairing",
|
|
448
|
+
allowFrom: params.allowFrom ?? [],
|
|
449
|
+
policyPath,
|
|
450
|
+
allowFromPath,
|
|
451
|
+
approveHint: params.approveHint ?? defaultApproveHint,
|
|
452
|
+
normalizeEntry: params.normalizeEntry,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
// 一次性探测 SDK 是否导出了 buildAccountScopedDmSecurityPolicy
|
|
456
|
+
const _sdkPolicyReady = (async () => {
|
|
457
|
+
try {
|
|
458
|
+
// 尝试从主 SDK 包导入
|
|
459
|
+
const sdk = await import("openclaw/plugin-sdk");
|
|
460
|
+
if (typeof sdk.buildAccountScopedDmSecurityPolicy === "function") {
|
|
461
|
+
return { buildAccountScopedDmSecurityPolicy: sdk.buildAccountScopedDmSecurityPolicy };
|
|
462
|
+
}
|
|
463
|
+
return {};
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
// openclaw/plugin-sdk 不可用或版本过低
|
|
467
|
+
return {};
|
|
468
|
+
}
|
|
469
|
+
})();
|
|
470
|
+
/**
|
|
471
|
+
* 构建多账号作用域的 DM 安全策略(兼容入口)
|
|
472
|
+
*
|
|
473
|
+
* 优先使用 SDK 版本(openclaw/plugin-sdk/channel-policy),
|
|
474
|
+
* 不可用时使用 fallback 实现。
|
|
475
|
+
*/
|
|
476
|
+
export async function buildAccountScopedDmSecurityPolicyCompat(params) {
|
|
477
|
+
const sdk = await _sdkPolicyReady;
|
|
478
|
+
if (sdk.buildAccountScopedDmSecurityPolicy) {
|
|
479
|
+
try {
|
|
480
|
+
return sdk.buildAccountScopedDmSecurityPolicy(params);
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
// SDK 版本异常,降级到 fallback
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return buildAccountScopedDmSecurityPolicyFallback(params);
|
|
487
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// 类型定义
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// 常量
|
|
6
|
+
// ============================================================================
|
|
7
|
+
const DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 天
|
|
8
|
+
const DEFAULT_MEMORY_MAX_SIZE = 200;
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// 核心实现
|
|
11
|
+
// ============================================================================
|
|
12
|
+
export function createPersistentReqIdStore(accountId, options) {
|
|
13
|
+
const ttlMs = options?.ttlMs ?? DEFAULT_TTL_MS;
|
|
14
|
+
const memoryMaxSize = options?.memoryMaxSize ?? DEFAULT_MEMORY_MAX_SIZE;
|
|
15
|
+
// 内存层:chatId → ReqIdEntry
|
|
16
|
+
const memory = new Map();
|
|
17
|
+
// ========== 内部辅助函数 ==========
|
|
18
|
+
/** 检查条目是否过期 */
|
|
19
|
+
function isExpired(entry, now) {
|
|
20
|
+
return ttlMs > 0 && now - entry.ts >= ttlMs;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 内存容量控制:淘汰最旧的条目。
|
|
24
|
+
* 利用 Map 的插入顺序 + touch(先 delete 再 set) 实现类 LRU 效果。
|
|
25
|
+
*/
|
|
26
|
+
function pruneMemory() {
|
|
27
|
+
if (memory.size <= memoryMaxSize)
|
|
28
|
+
return;
|
|
29
|
+
const sorted = [...memory.entries()].sort((a, b) => a[1].ts - b[1].ts);
|
|
30
|
+
const toRemove = sorted.slice(0, memory.size - memoryMaxSize);
|
|
31
|
+
for (const [key] of toRemove) {
|
|
32
|
+
memory.delete(key);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// ========== 公开 API ==========
|
|
36
|
+
function set(chatId, reqId) {
|
|
37
|
+
const entry = { reqId, ts: Date.now() };
|
|
38
|
+
// touch:先删再设,保持 Map 插入顺序(类 LRU)
|
|
39
|
+
memory.delete(chatId);
|
|
40
|
+
memory.set(chatId, entry);
|
|
41
|
+
pruneMemory();
|
|
42
|
+
}
|
|
43
|
+
async function get(chatId) {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
// 仅查内存
|
|
46
|
+
const memEntry = memory.get(chatId);
|
|
47
|
+
if (memEntry && !isExpired(memEntry, now)) {
|
|
48
|
+
return memEntry.reqId;
|
|
49
|
+
}
|
|
50
|
+
if (memEntry) {
|
|
51
|
+
memory.delete(chatId); // 过期则删除
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
function getSync(chatId) {
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
const entry = memory.get(chatId);
|
|
58
|
+
if (entry && !isExpired(entry, now)) {
|
|
59
|
+
return entry.reqId;
|
|
60
|
+
}
|
|
61
|
+
if (entry) {
|
|
62
|
+
memory.delete(chatId);
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
function del(chatId) {
|
|
67
|
+
memory.delete(chatId);
|
|
68
|
+
}
|
|
69
|
+
function clearMemory() {
|
|
70
|
+
memory.clear();
|
|
71
|
+
}
|
|
72
|
+
function memorySize() {
|
|
73
|
+
return memory.size;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
set,
|
|
77
|
+
get,
|
|
78
|
+
getSync,
|
|
79
|
+
delete: del,
|
|
80
|
+
clearMemory,
|
|
81
|
+
memorySize,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { WecomAgentConfig, WecomBotConfig } from "../types/index.js";
|
|
3
|
+
type WecomCommandAuthAccountConfig = Pick<WecomBotConfig, "dmPolicy" | "allowFrom"> | Pick<WecomAgentConfig, "dmPolicy" | "allowFrom">;
|
|
4
|
+
export declare function resolveWecomCommandAuthorization(params: {
|
|
5
|
+
core: PluginRuntime;
|
|
6
|
+
cfg: OpenClawConfig;
|
|
7
|
+
accountConfig: WecomCommandAuthAccountConfig;
|
|
8
|
+
rawBody: string;
|
|
9
|
+
senderUserId: string;
|
|
10
|
+
}): Promise<{
|
|
11
|
+
shouldComputeAuth: boolean;
|
|
12
|
+
dmPolicy: "pairing" | "allowlist" | "open" | "disabled";
|
|
13
|
+
senderAllowed: boolean;
|
|
14
|
+
authorizerConfigured: boolean;
|
|
15
|
+
commandAuthorized: boolean | undefined;
|
|
16
|
+
effectiveAllowFrom: string[];
|
|
17
|
+
}>;
|
|
18
|
+
export declare function buildWecomUnauthorizedCommandPrompt(params: {
|
|
19
|
+
senderUserId: string;
|
|
20
|
+
dmPolicy: "pairing" | "allowlist" | "open" | "disabled";
|
|
21
|
+
scope: "bot" | "agent";
|
|
22
|
+
}): string;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
function normalizeWecomAllowFromEntry(raw) {
|
|
2
|
+
return raw
|
|
3
|
+
.trim()
|
|
4
|
+
.toLowerCase()
|
|
5
|
+
.replace(/^wecom:/, "")
|
|
6
|
+
.replace(/^user:/, "")
|
|
7
|
+
.replace(/^userid:/, "");
|
|
8
|
+
}
|
|
9
|
+
function isWecomSenderAllowed(senderUserId, allowFrom) {
|
|
10
|
+
const list = allowFrom.map((entry) => normalizeWecomAllowFromEntry(entry)).filter(Boolean);
|
|
11
|
+
if (list.includes("*"))
|
|
12
|
+
return true;
|
|
13
|
+
const normalizedSender = normalizeWecomAllowFromEntry(senderUserId);
|
|
14
|
+
if (!normalizedSender)
|
|
15
|
+
return false;
|
|
16
|
+
return list.includes(normalizedSender);
|
|
17
|
+
}
|
|
18
|
+
export async function resolveWecomCommandAuthorization(params) {
|
|
19
|
+
const { core, cfg, accountConfig, rawBody, senderUserId } = params;
|
|
20
|
+
const dmPolicy = (accountConfig.dmPolicy ?? "pairing");
|
|
21
|
+
const configAllowFrom = (accountConfig.allowFrom ?? []).map((v) => String(v));
|
|
22
|
+
const shouldComputeAuth = core.channel.commands.shouldComputeCommandAuthorized(rawBody, cfg);
|
|
23
|
+
// WeCom channel currently does NOT support the `openclaw pairing` CLI workflow
|
|
24
|
+
// ("Channel wecom does not support pairing"). So we must not rely on pairing
|
|
25
|
+
// store approvals for command authorization here.
|
|
26
|
+
//
|
|
27
|
+
// Policy semantics:
|
|
28
|
+
// - open: commands are allowed for everyone by default (unless higher-level access-groups deny).
|
|
29
|
+
// - allowlist: commands require allowFrom entries.
|
|
30
|
+
// - pairing: treated the same as allowlist for WeCom (since pairing CLI is unsupported).
|
|
31
|
+
const effectiveAllowFrom = dmPolicy === "open" ? ["*"] : configAllowFrom;
|
|
32
|
+
const senderAllowed = isWecomSenderAllowed(senderUserId, effectiveAllowFrom);
|
|
33
|
+
const allowAllConfigured = effectiveAllowFrom.some((entry) => normalizeWecomAllowFromEntry(entry) === "*");
|
|
34
|
+
const authorizerConfigured = allowAllConfigured || effectiveAllowFrom.length > 0;
|
|
35
|
+
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
|
36
|
+
const commandAuthorized = shouldComputeAuth
|
|
37
|
+
? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
|
|
38
|
+
useAccessGroups,
|
|
39
|
+
authorizers: [{ configured: authorizerConfigured, allowed: senderAllowed }],
|
|
40
|
+
})
|
|
41
|
+
: undefined;
|
|
42
|
+
return {
|
|
43
|
+
shouldComputeAuth,
|
|
44
|
+
dmPolicy,
|
|
45
|
+
senderAllowed,
|
|
46
|
+
authorizerConfigured,
|
|
47
|
+
commandAuthorized,
|
|
48
|
+
effectiveAllowFrom,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function buildWecomUnauthorizedCommandPrompt(params) {
|
|
52
|
+
const user = params.senderUserId || "unknown";
|
|
53
|
+
const policy = params.dmPolicy;
|
|
54
|
+
const scopeLabel = params.scope === "bot" ? "Bot(智能机器人)" : "Agent(自建应用)";
|
|
55
|
+
const dmPrefix = params.scope === "bot" ? "channels.wecom.bot" : "channels.wecom.agent";
|
|
56
|
+
const allowCmd = (value) => `openclaw config set ${dmPrefix}.allowFrom '${value}'`;
|
|
57
|
+
const policyCmd = (value) => `openclaw config set ${dmPrefix}.dmPolicy "${value}"`;
|
|
58
|
+
if (policy === "disabled") {
|
|
59
|
+
return [
|
|
60
|
+
`无权限执行命令(${scopeLabel} 已禁用:dmPolicy=disabled)`,
|
|
61
|
+
`触发者:${user}`,
|
|
62
|
+
`管理员:${policyCmd("open")}(全放开)或 ${policyCmd("allowlist")}(白名单)`,
|
|
63
|
+
].join("\n");
|
|
64
|
+
}
|
|
65
|
+
// WeCom 不支持 pairing CLI,因此这里统一给出“open / allowlist”两种明确的配置指令
|
|
66
|
+
return [
|
|
67
|
+
`无权限执行命令(入口:${scopeLabel},userid:${user})`,
|
|
68
|
+
`管理员全放开:${policyCmd("open")}`,
|
|
69
|
+
`管理员放行该用户:${policyCmd("allowlist")}`,
|
|
70
|
+
`然后设置白名单:${allowCmd(JSON.stringify([user]))}`,
|
|
71
|
+
`如果仍被拦截:检查 commands.useAccessGroups/访问组`,
|
|
72
|
+
].join("\n");
|
|
73
|
+
}
|