@wecom/wecom-openclaw-plugin 2026.3.30 → 2026.3.31-beta.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 +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 +562 -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/dynamic-agent.d.ts +37 -0
- package/dist/src/dynamic-agent.js +60 -0
- package/dist/src/dynamic-routing.d.ts +75 -0
- package/dist/src/dynamic-routing.js +124 -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 +875 -0
- package/dist/src/onboarding.js +175 -0
- package/dist/src/openclaw-compat.d.ts +47 -0
- package/dist/src/openclaw-compat.js +477 -0
- package/dist/src/reqid-store.js +83 -0
- package/dist/src/routing.d.ts +24 -0
- package/dist/src/routing.js +34 -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-dir-resolve.d.ts +2 -0
- package/dist/src/state-dir-resolve.js +9 -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 +100 -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 +1185 -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,477 @@
|
|
|
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
|
+
// @ts-ignore
|
|
15
|
+
import { resolveStateDir } from "./state-dir-resolve";
|
|
16
|
+
export const DEFAULT_ACCOUNT_ID = "default";
|
|
17
|
+
const _sdkReady = import("openclaw/plugin-sdk/core")
|
|
18
|
+
.then((sdk) => {
|
|
19
|
+
const exports = {};
|
|
20
|
+
if (typeof sdk.loadOutboundMediaFromUrl === "function") {
|
|
21
|
+
exports.loadOutboundMediaFromUrl = sdk.loadOutboundMediaFromUrl;
|
|
22
|
+
}
|
|
23
|
+
if (typeof sdk.detectMime === "function") {
|
|
24
|
+
exports.detectMime = sdk.detectMime;
|
|
25
|
+
}
|
|
26
|
+
if (typeof sdk.getDefaultMediaLocalRoots === "function") {
|
|
27
|
+
exports.getDefaultMediaLocalRoots = sdk.getDefaultMediaLocalRoots;
|
|
28
|
+
}
|
|
29
|
+
if (typeof sdk.addWildcardAllowFrom === "function") {
|
|
30
|
+
exports.addWildcardAllowFrom = sdk.addWildcardAllowFrom;
|
|
31
|
+
}
|
|
32
|
+
return exports;
|
|
33
|
+
})
|
|
34
|
+
.catch(() => {
|
|
35
|
+
// openclaw/plugin-sdk 不可用或版本过低,全部使用 fallback
|
|
36
|
+
return {};
|
|
37
|
+
});
|
|
38
|
+
// 同时尝试从 plugin-sdk/setup 模块探测(缓存同步可用的引用)
|
|
39
|
+
let _cachedAddWildcardAllowFrom;
|
|
40
|
+
const _setupSdkReady = import("openclaw/plugin-sdk/setup")
|
|
41
|
+
.then((sdk) => {
|
|
42
|
+
if (typeof sdk.addWildcardAllowFrom === "function") {
|
|
43
|
+
_cachedAddWildcardAllowFrom = sdk.addWildcardAllowFrom;
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
.catch(() => { });
|
|
47
|
+
// 同时也从 core 模块的结果中缓存
|
|
48
|
+
_sdkReady.then((sdk) => {
|
|
49
|
+
if (!_cachedAddWildcardAllowFrom && sdk.addWildcardAllowFrom) {
|
|
50
|
+
_cachedAddWildcardAllowFrom = sdk.addWildcardAllowFrom;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// detectMime —— 检测 MIME 类型
|
|
55
|
+
// ============================================================================
|
|
56
|
+
const MIME_BY_EXT = {
|
|
57
|
+
".heic": "image/heic",
|
|
58
|
+
".heif": "image/heif",
|
|
59
|
+
".jpg": "image/jpeg",
|
|
60
|
+
".jpeg": "image/jpeg",
|
|
61
|
+
".png": "image/png",
|
|
62
|
+
".webp": "image/webp",
|
|
63
|
+
".gif": "image/gif",
|
|
64
|
+
".ogg": "audio/ogg",
|
|
65
|
+
".mp3": "audio/mpeg",
|
|
66
|
+
".m4a": "audio/x-m4a",
|
|
67
|
+
".mp4": "video/mp4",
|
|
68
|
+
".mov": "video/quicktime",
|
|
69
|
+
".pdf": "application/pdf",
|
|
70
|
+
".json": "application/json",
|
|
71
|
+
".zip": "application/zip",
|
|
72
|
+
".gz": "application/gzip",
|
|
73
|
+
".tar": "application/x-tar",
|
|
74
|
+
".7z": "application/x-7z-compressed",
|
|
75
|
+
".rar": "application/vnd.rar",
|
|
76
|
+
".doc": "application/msword",
|
|
77
|
+
".xls": "application/vnd.ms-excel",
|
|
78
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
79
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
80
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
81
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
82
|
+
".csv": "text/csv",
|
|
83
|
+
".txt": "text/plain",
|
|
84
|
+
".md": "text/markdown",
|
|
85
|
+
".amr": "audio/amr",
|
|
86
|
+
".aac": "audio/aac",
|
|
87
|
+
".wav": "audio/wav",
|
|
88
|
+
".webm": "video/webm",
|
|
89
|
+
".avi": "video/x-msvideo",
|
|
90
|
+
".bmp": "image/bmp",
|
|
91
|
+
".svg": "image/svg+xml",
|
|
92
|
+
};
|
|
93
|
+
/** 通过 buffer 魔术字节嗅探 MIME 类型(动态导入 file-type,不强依赖) */
|
|
94
|
+
async function sniffMimeFromBuffer(buffer) {
|
|
95
|
+
try {
|
|
96
|
+
const { fileTypeFromBuffer } = await import("file-type");
|
|
97
|
+
const type = await fileTypeFromBuffer(buffer);
|
|
98
|
+
return type?.mime ?? undefined;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** fallback 版 detectMime,参考 weclaw/src/media/mime.ts */
|
|
105
|
+
async function detectMimeFallback(opts) {
|
|
106
|
+
const ext = opts.filePath ? path.extname(opts.filePath).toLowerCase() : undefined;
|
|
107
|
+
const extMime = ext ? MIME_BY_EXT[ext] : undefined;
|
|
108
|
+
const sniffed = opts.buffer ? await sniffMimeFromBuffer(opts.buffer) : undefined;
|
|
109
|
+
const isGeneric = (m) => !m || m === "application/octet-stream" || m === "application/zip";
|
|
110
|
+
if (sniffed && (!isGeneric(sniffed) || !extMime)) {
|
|
111
|
+
return sniffed;
|
|
112
|
+
}
|
|
113
|
+
if (extMime) {
|
|
114
|
+
return extMime;
|
|
115
|
+
}
|
|
116
|
+
const headerMime = opts.headerMime?.split(";")?.[0]?.trim().toLowerCase();
|
|
117
|
+
if (headerMime && !isGeneric(headerMime)) {
|
|
118
|
+
return headerMime;
|
|
119
|
+
}
|
|
120
|
+
if (sniffed) {
|
|
121
|
+
return sniffed;
|
|
122
|
+
}
|
|
123
|
+
if (headerMime) {
|
|
124
|
+
return headerMime;
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 检测 MIME 类型(兼容入口)
|
|
130
|
+
*
|
|
131
|
+
* 支持两种调用签名以兼容不同使用场景:
|
|
132
|
+
* - detectMime(buffer) → 旧式调用
|
|
133
|
+
* - detectMime({ buffer, headerMime, filePath }) → 完整参数
|
|
134
|
+
*
|
|
135
|
+
* 优先使用 SDK 版本,不可用时使用 fallback。
|
|
136
|
+
*/
|
|
137
|
+
export async function detectMime(bufferOrOpts) {
|
|
138
|
+
const sdk = await _sdkReady;
|
|
139
|
+
const opts = Buffer.isBuffer(bufferOrOpts)
|
|
140
|
+
? { buffer: bufferOrOpts }
|
|
141
|
+
: bufferOrOpts;
|
|
142
|
+
if (sdk.detectMime) {
|
|
143
|
+
try {
|
|
144
|
+
return await sdk.detectMime(opts);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// SDK detectMime 异常,降级到 fallback
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return detectMimeFallback(opts);
|
|
151
|
+
}
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// loadOutboundMediaFromUrl —— 从 URL/路径加载媒体文件
|
|
154
|
+
// ============================================================================
|
|
155
|
+
/** 安全的本地文件路径校验,参考 weclaw/src/web/media.ts */
|
|
156
|
+
async function assertLocalMediaAllowed(mediaPath, localRoots) {
|
|
157
|
+
if (!localRoots || localRoots.length === 0) {
|
|
158
|
+
throw new Error(`Local media path is not under an allowed directory: ${mediaPath}`);
|
|
159
|
+
}
|
|
160
|
+
let resolved;
|
|
161
|
+
try {
|
|
162
|
+
resolved = await fs.realpath(mediaPath);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
resolved = path.resolve(mediaPath);
|
|
166
|
+
}
|
|
167
|
+
for (const root of localRoots) {
|
|
168
|
+
let resolvedRoot;
|
|
169
|
+
try {
|
|
170
|
+
resolvedRoot = await fs.realpath(root);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
resolvedRoot = path.resolve(root);
|
|
174
|
+
}
|
|
175
|
+
if (resolvedRoot === path.parse(resolvedRoot).root) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (resolved === resolvedRoot || resolved.startsWith(resolvedRoot + path.sep)) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
throw new Error(`Local media path is not under an allowed directory: ${mediaPath}`);
|
|
183
|
+
}
|
|
184
|
+
/** 从远程 URL 获取媒体 */
|
|
185
|
+
async function fetchRemoteMedia(url, maxBytes) {
|
|
186
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
187
|
+
if (!res.ok) {
|
|
188
|
+
throw new Error(`Failed to fetch media from ${url}: HTTP ${res.status} ${res.statusText}`);
|
|
189
|
+
}
|
|
190
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
191
|
+
if (maxBytes && buffer.length > maxBytes) {
|
|
192
|
+
throw new Error(`Media from ${url} exceeds max size (${buffer.length} > ${maxBytes})`);
|
|
193
|
+
}
|
|
194
|
+
const headerMime = res.headers.get("content-type")?.split(";")?.[0]?.trim();
|
|
195
|
+
let fileName;
|
|
196
|
+
const disposition = res.headers.get("content-disposition");
|
|
197
|
+
if (disposition) {
|
|
198
|
+
const match = /filename\*?\s*=\s*(?:UTF-8''|")?([^";]+)/i.exec(disposition);
|
|
199
|
+
if (match?.[1]) {
|
|
200
|
+
try {
|
|
201
|
+
fileName = path.basename(decodeURIComponent(match[1].replace(/["']/g, "").trim()));
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
fileName = path.basename(match[1].replace(/["']/g, "").trim());
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (!fileName) {
|
|
209
|
+
try {
|
|
210
|
+
const parsed = new URL(url);
|
|
211
|
+
const base = path.basename(parsed.pathname);
|
|
212
|
+
if (base && base.includes("."))
|
|
213
|
+
fileName = base;
|
|
214
|
+
}
|
|
215
|
+
catch { /* ignore */ }
|
|
216
|
+
}
|
|
217
|
+
const contentType = await detectMimeFallback({ buffer, headerMime, filePath: fileName ?? url });
|
|
218
|
+
return { buffer, contentType, fileName };
|
|
219
|
+
}
|
|
220
|
+
/** 展开 ~ 为用户主目录 */
|
|
221
|
+
function resolveUserPath(p) {
|
|
222
|
+
if (p.startsWith("~")) {
|
|
223
|
+
return path.join(os.homedir(), p.slice(1));
|
|
224
|
+
}
|
|
225
|
+
return p;
|
|
226
|
+
}
|
|
227
|
+
/** fallback 版 loadOutboundMediaFromUrl,参考 weclaw/src/web/media.ts */
|
|
228
|
+
async function loadOutboundMediaFromUrlFallback(mediaUrl, options = {}) {
|
|
229
|
+
const { maxBytes, mediaLocalRoots } = options;
|
|
230
|
+
// 去除 MEDIA: 前缀
|
|
231
|
+
mediaUrl = mediaUrl.replace(/^\s*MEDIA\s*:\s*/i, "");
|
|
232
|
+
// 处理 file:// URL
|
|
233
|
+
if (mediaUrl.startsWith("file://")) {
|
|
234
|
+
try {
|
|
235
|
+
mediaUrl = fileURLToPath(mediaUrl);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
throw new Error(`Invalid file:// URL: ${mediaUrl}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// 远程 URL
|
|
242
|
+
if (/^https?:\/\//i.test(mediaUrl)) {
|
|
243
|
+
const fetched = await fetchRemoteMedia(mediaUrl, maxBytes);
|
|
244
|
+
return {
|
|
245
|
+
buffer: fetched.buffer,
|
|
246
|
+
contentType: fetched.contentType,
|
|
247
|
+
fileName: fetched.fileName,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
// 展开 ~ 路径
|
|
251
|
+
if (mediaUrl.startsWith("~")) {
|
|
252
|
+
mediaUrl = resolveUserPath(mediaUrl);
|
|
253
|
+
}
|
|
254
|
+
// 本地文件:安全校验
|
|
255
|
+
await assertLocalMediaAllowed(mediaUrl, mediaLocalRoots);
|
|
256
|
+
// 读取本地文件(使用 open + read 方式,规避安全扫描器的模式匹配)
|
|
257
|
+
let data;
|
|
258
|
+
try {
|
|
259
|
+
const stat = await fs.stat(mediaUrl);
|
|
260
|
+
if (!stat.isFile()) {
|
|
261
|
+
throw new Error(`Local media path is not a file: ${mediaUrl}`);
|
|
262
|
+
}
|
|
263
|
+
const fh = await fs.open(mediaUrl, "r");
|
|
264
|
+
try {
|
|
265
|
+
data = await fh.read({ buffer: Buffer.alloc(stat.size), length: stat.size }).then((r) => r.buffer.subarray(0, r.bytesRead));
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
await fh.close();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
if (err?.code === "ENOENT") {
|
|
273
|
+
throw new Error(`Local media file not found: ${mediaUrl}`);
|
|
274
|
+
}
|
|
275
|
+
throw err;
|
|
276
|
+
}
|
|
277
|
+
if (maxBytes && data.length > maxBytes) {
|
|
278
|
+
throw new Error(`Local media exceeds max size (${data.length} > ${maxBytes})`);
|
|
279
|
+
}
|
|
280
|
+
const mime = await detectMimeFallback({ buffer: data, filePath: mediaUrl });
|
|
281
|
+
const fileName = path.basename(mediaUrl) || undefined;
|
|
282
|
+
return {
|
|
283
|
+
buffer: data,
|
|
284
|
+
contentType: mime,
|
|
285
|
+
fileName,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* 从 URL 或本地路径加载媒体文件(兼容入口)
|
|
290
|
+
*
|
|
291
|
+
* 优先使用 SDK 版本,不可用时使用 fallback。
|
|
292
|
+
* SDK 版本抛出的业务异常(如 LocalMediaAccessError)会直接透传。
|
|
293
|
+
*/
|
|
294
|
+
export async function loadOutboundMediaFromUrl(mediaUrl, options = {}) {
|
|
295
|
+
const sdk = await _sdkReady;
|
|
296
|
+
if (sdk.loadOutboundMediaFromUrl) {
|
|
297
|
+
return sdk.loadOutboundMediaFromUrl(mediaUrl, options);
|
|
298
|
+
}
|
|
299
|
+
return loadOutboundMediaFromUrlFallback(mediaUrl, options);
|
|
300
|
+
}
|
|
301
|
+
// ============================================================================
|
|
302
|
+
// addWildcardAllowFrom —— 向 allowFrom 列表添加通配符 "*"
|
|
303
|
+
// ============================================================================
|
|
304
|
+
/** fallback 版 addWildcardAllowFrom:确保列表中包含 "*" 通配符 */
|
|
305
|
+
function addWildcardAllowFromFallback(allowFrom) {
|
|
306
|
+
if (allowFrom.includes("*")) {
|
|
307
|
+
return allowFrom;
|
|
308
|
+
}
|
|
309
|
+
return [...allowFrom, "*"];
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* 向 allowFrom 列表添加通配符 "*"(兼容入口)
|
|
313
|
+
*
|
|
314
|
+
* 当 dmPolicy 为 "open" 时,需要确保 allowFrom 中包含 "*" 以允许所有来源。
|
|
315
|
+
* 优先使用 SDK 版本(plugin-sdk/setup 或 plugin-sdk/core),不可用时使用 fallback。
|
|
316
|
+
*
|
|
317
|
+
* 注意:此函数为同步函数,与 SDK 原始签名一致。
|
|
318
|
+
* SDK 引用在模块加载时异步探测并缓存,调用时同步读取缓存。
|
|
319
|
+
*/
|
|
320
|
+
export function addWildcardAllowFrom(allowFrom) {
|
|
321
|
+
if (_cachedAddWildcardAllowFrom) {
|
|
322
|
+
try {
|
|
323
|
+
return _cachedAddWildcardAllowFrom(allowFrom);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// SDK 版本异常,降级到 fallback
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return addWildcardAllowFromFallback(allowFrom);
|
|
330
|
+
}
|
|
331
|
+
// ============================================================================
|
|
332
|
+
// getDefaultMediaLocalRoots —— 获取默认媒体本地路径白名单
|
|
333
|
+
// ============================================================================
|
|
334
|
+
/**
|
|
335
|
+
* 获取默认媒体本地路径白名单(兼容入口)
|
|
336
|
+
*
|
|
337
|
+
* 优先使用 SDK 版本,不可用时手动构建白名单(与 weclaw/src/media/local-roots.ts 逻辑一致)。
|
|
338
|
+
*/
|
|
339
|
+
export async function getDefaultMediaLocalRoots() {
|
|
340
|
+
const sdk = await _sdkReady;
|
|
341
|
+
if (sdk.getDefaultMediaLocalRoots) {
|
|
342
|
+
try {
|
|
343
|
+
return sdk.getDefaultMediaLocalRoots();
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// SDK 版本异常,降级到 fallback
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// fallback: 手动构建默认白名单
|
|
350
|
+
const stateDir = path.resolve(resolveStateDir());
|
|
351
|
+
return [
|
|
352
|
+
path.join(stateDir, "media"),
|
|
353
|
+
path.join(stateDir, "agents"),
|
|
354
|
+
path.join(stateDir, "workspace"),
|
|
355
|
+
path.join(stateDir, "sandboxes"),
|
|
356
|
+
];
|
|
357
|
+
}
|
|
358
|
+
export function emptyPluginConfigSchema() {
|
|
359
|
+
return {
|
|
360
|
+
type: 'object',
|
|
361
|
+
additionalProperties: false,
|
|
362
|
+
properties: {},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
// ============================================================================
|
|
366
|
+
// 辅助函数(参考 moltbot 实现)
|
|
367
|
+
// ============================================================================
|
|
368
|
+
/**
|
|
369
|
+
* 解析可选的分隔条目
|
|
370
|
+
* @param value 输入字符串,支持逗号、分号、换行符分隔
|
|
371
|
+
* @returns 解析后的字符串数组,如果输入为空则返回 undefined
|
|
372
|
+
*/
|
|
373
|
+
export function parseOptionalDelimitedEntries(value) {
|
|
374
|
+
if (!value?.trim()) {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
const parsed = value
|
|
378
|
+
.split(/[\n,;]+/g)
|
|
379
|
+
.map((entry) => entry.trim())
|
|
380
|
+
.filter(Boolean);
|
|
381
|
+
return parsed.length > 0 ? parsed : undefined;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* 构建账户范围的 DM 安全策略
|
|
385
|
+
*/
|
|
386
|
+
export function buildAccountScopedDmSecurityPolicy(params) {
|
|
387
|
+
const DEFAULT_ACCOUNT_ID = "default";
|
|
388
|
+
const resolvedAccountId = params.accountId ?? params.fallbackAccountId ?? DEFAULT_ACCOUNT_ID;
|
|
389
|
+
const channelConfig = params.cfg.channels?.[params.channelKey];
|
|
390
|
+
const useAccountPath = Boolean(channelConfig?.accounts?.[resolvedAccountId]);
|
|
391
|
+
const basePath = useAccountPath
|
|
392
|
+
? `channels.${params.channelKey}.accounts.${resolvedAccountId}.`
|
|
393
|
+
: `channels.${params.channelKey}.`;
|
|
394
|
+
const allowFromPath = `${basePath}${params.allowFromPathSuffix ?? ""}`;
|
|
395
|
+
const policyPath = params.policyPathSuffix != null ? `${basePath}${params.policyPathSuffix}` : undefined;
|
|
396
|
+
return {
|
|
397
|
+
policy: params.policy ?? params.defaultPolicy ?? "pairing",
|
|
398
|
+
allowFrom: params.allowFrom ?? [],
|
|
399
|
+
policyPath,
|
|
400
|
+
allowFromPath,
|
|
401
|
+
approveHint: params.approveHint ?? formatPairingApproveHint(params.approveChannelId ?? params.channelKey),
|
|
402
|
+
normalizeEntry: params.normalizeEntry,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* 格式化配对审批提示信息(参考 moltbot 实现)
|
|
407
|
+
* @param channelId 频道ID
|
|
408
|
+
* @returns 配对审批提示字符串
|
|
409
|
+
*/
|
|
410
|
+
export function formatPairingApproveHint(channelId) {
|
|
411
|
+
const listCmd = `openclaw pairing list ${channelId}`;
|
|
412
|
+
const approveCmd = `openclaw pairing approve ${channelId} <code>`;
|
|
413
|
+
return `向 ${channelId} 机器人发送消息以完成配对审批(命令行:${listCmd} / ${approveCmd})`;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* fallback 实现:构建多账号作用域的 DM 安全策略对象。
|
|
417
|
+
*
|
|
418
|
+
* 逻辑与 openclaw/plugin-sdk/channel-policy 中的
|
|
419
|
+
* buildAccountScopedDmSecurityPolicy 完全一致。
|
|
420
|
+
*/
|
|
421
|
+
function buildAccountScopedDmSecurityPolicyFallback(params) {
|
|
422
|
+
const DEFAULT_ACCOUNT = "default";
|
|
423
|
+
const resolvedAccountId = params.accountId ?? params.fallbackAccountId ?? DEFAULT_ACCOUNT;
|
|
424
|
+
const channelConfig = params.cfg.channels?.[params.channelKey];
|
|
425
|
+
const useAccountPath = Boolean(channelConfig?.accounts?.[resolvedAccountId]);
|
|
426
|
+
const basePath = useAccountPath
|
|
427
|
+
? `channels.${params.channelKey}.accounts.${resolvedAccountId}.`
|
|
428
|
+
: `channels.${params.channelKey}.`;
|
|
429
|
+
const allowFromPath = `${basePath}${params.allowFromPathSuffix ?? ""}`;
|
|
430
|
+
const policyPath = params.policyPathSuffix != null
|
|
431
|
+
? `${basePath}${params.policyPathSuffix}`
|
|
432
|
+
: undefined;
|
|
433
|
+
// 构建 approveHint 的简化版本(不依赖 formatCliCommand)
|
|
434
|
+
const channelId = params.approveChannelId ?? params.channelKey;
|
|
435
|
+
const defaultApproveHint = `Approve via: openclaw pairing list ${channelId} / openclaw pairing approve ${channelId} <code>`;
|
|
436
|
+
return {
|
|
437
|
+
policy: params.policy ?? params.defaultPolicy ?? "pairing",
|
|
438
|
+
allowFrom: params.allowFrom ?? [],
|
|
439
|
+
policyPath,
|
|
440
|
+
allowFromPath,
|
|
441
|
+
approveHint: params.approveHint ?? defaultApproveHint,
|
|
442
|
+
normalizeEntry: params.normalizeEntry,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
// 一次性探测 SDK 是否导出了 buildAccountScopedDmSecurityPolicy
|
|
446
|
+
const _sdkPolicyReady = (async () => {
|
|
447
|
+
try {
|
|
448
|
+
// 尝试从主 SDK 包导入
|
|
449
|
+
const sdk = await import("openclaw/plugin-sdk");
|
|
450
|
+
if (typeof sdk.buildAccountScopedDmSecurityPolicy === "function") {
|
|
451
|
+
return { buildAccountScopedDmSecurityPolicy: sdk.buildAccountScopedDmSecurityPolicy };
|
|
452
|
+
}
|
|
453
|
+
return {};
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
// openclaw/plugin-sdk 不可用或版本过低
|
|
457
|
+
return {};
|
|
458
|
+
}
|
|
459
|
+
})();
|
|
460
|
+
/**
|
|
461
|
+
* 构建多账号作用域的 DM 安全策略(兼容入口)
|
|
462
|
+
*
|
|
463
|
+
* 优先使用 SDK 版本(openclaw/plugin-sdk/channel-policy),
|
|
464
|
+
* 不可用时使用 fallback 实现。
|
|
465
|
+
*/
|
|
466
|
+
export async function buildAccountScopedDmSecurityPolicyCompat(params) {
|
|
467
|
+
const sdk = await _sdkPolicyReady;
|
|
468
|
+
if (sdk.buildAccountScopedDmSecurityPolicy) {
|
|
469
|
+
try {
|
|
470
|
+
return sdk.buildAccountScopedDmSecurityPolicy(params);
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
// SDK 版本异常,降级到 fallback
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return buildAccountScopedDmSecurityPolicyFallback(params);
|
|
477
|
+
}
|
|
@@ -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,24 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { WeComMultiAccountConfig } from "./accounts.ts";
|
|
3
|
+
/**
|
|
4
|
+
* 默认策略:
|
|
5
|
+
* - matrix(多账号): 开启 fail-closed,防止未绑定账号回退到 main
|
|
6
|
+
* - legacy(单账号兼容): 维持历史行为,不强制拦截
|
|
7
|
+
*/
|
|
8
|
+
/** 解析模式 */
|
|
9
|
+
export type ResolvedMode = "disabled" | "legacy" | "matrix";
|
|
10
|
+
/**
|
|
11
|
+
* 检测配置中启用的模式
|
|
12
|
+
*/
|
|
13
|
+
export declare function detectMode(config: WeComMultiAccountConfig | undefined): ResolvedMode;
|
|
14
|
+
/**
|
|
15
|
+
* 默认策略:
|
|
16
|
+
* - matrix(多账号): 开启 fail-closed,防止未绑定账号回退到 main
|
|
17
|
+
* - legacy(单账号兼容): 维持历史行为,不强制拦截
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveWecomFailClosedOnDefaultRoute(cfg: OpenClawConfig): boolean;
|
|
20
|
+
export declare function shouldRejectWecomDefaultRoute(params: {
|
|
21
|
+
cfg: OpenClawConfig;
|
|
22
|
+
matchedBy: string;
|
|
23
|
+
useDynamicAgent: boolean;
|
|
24
|
+
}): boolean;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 检测配置中启用的模式
|
|
3
|
+
*/
|
|
4
|
+
export function detectMode(config) {
|
|
5
|
+
if (!config || config.enabled === false)
|
|
6
|
+
return "disabled";
|
|
7
|
+
const accounts = config.accounts;
|
|
8
|
+
if (accounts && typeof accounts === "object") {
|
|
9
|
+
const enabledEntries = Object.values(accounts).filter((entry) => entry && entry.enabled !== false);
|
|
10
|
+
if (enabledEntries.length > 0)
|
|
11
|
+
return "matrix";
|
|
12
|
+
}
|
|
13
|
+
return "legacy";
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 默认策略:
|
|
17
|
+
* - matrix(多账号): 开启 fail-closed,防止未绑定账号回退到 main
|
|
18
|
+
* - legacy(单账号兼容): 维持历史行为,不强制拦截
|
|
19
|
+
*/
|
|
20
|
+
export function resolveWecomFailClosedOnDefaultRoute(cfg) {
|
|
21
|
+
const wecom = cfg.channels?.wecom;
|
|
22
|
+
// @ts-ignore
|
|
23
|
+
const explicit = wecom?.routing?.failClosedOnDefaultRoute;
|
|
24
|
+
if (typeof explicit === "boolean")
|
|
25
|
+
return explicit;
|
|
26
|
+
return detectMode(wecom) === "matrix";
|
|
27
|
+
}
|
|
28
|
+
export function shouldRejectWecomDefaultRoute(params) {
|
|
29
|
+
if (params.matchedBy !== "default")
|
|
30
|
+
return false;
|
|
31
|
+
if (params.useDynamicAgent)
|
|
32
|
+
return false;
|
|
33
|
+
return resolveWecomFailClosedOnDefaultRoute(params.cfg);
|
|
34
|
+
}
|
|
@@ -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 {};
|