@sunnoy/wecom 1.5.1 → 1.6.2
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 +25 -6
- package/dynamic-agent.js +5 -1
- package/index.js +12 -10
- package/package.json +1 -1
- package/wecom/accounts.js +5 -4
- package/wecom/agent-inbound.js +8 -2
- package/wecom/channel-plugin.js +16 -44
- package/wecom/inbound-processor.js +9 -2
- package/wecom/outbound-delivery.js +68 -10
- package/wecom/workspace-template.js +16 -18
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
### 动态 Agent 与隔离
|
|
22
22
|
- **动态 Agent 管理**: 默认按"每个私聊用户 / 每个群聊"自动创建独立 Agent。每个 Agent 拥有独立的工作区与对话上下文,实现更强的数据隔离。
|
|
23
23
|
- **群聊深度集成**: 支持群聊消息解析,可通过 @提及(At-mention)精准触发机器人响应。
|
|
24
|
-
- **管理员用户**:
|
|
24
|
+
- **管理员用户**: 可配置管理员列表,默认绕过指令白名单;可选开启“绕过动态 Agent 路由”。
|
|
25
25
|
- **指令白名单**: 内置常用指令支持(如 `/new`、`/status`),并提供指令白名单配置功能。
|
|
26
26
|
|
|
27
27
|
### 多媒体支持
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
|
|
38
38
|
## 前置要求
|
|
39
39
|
|
|
40
|
-
- 已安装 [OpenClaw](https://github.com/openclaw/openclaw) (版本 2026.
|
|
40
|
+
- 已安装 [OpenClaw](https://github.com/openclaw/openclaw) (版本 2026.3.2+)
|
|
41
41
|
- 企业微信管理后台权限,可创建智能机器人应用或自建应用
|
|
42
42
|
- 可从企业微信访问的服务器地址(HTTP/HTTPS)
|
|
43
43
|
|
|
@@ -124,6 +124,9 @@ npm run test:e2e
|
|
|
124
124
|
"enabled": true,
|
|
125
125
|
"allowlist": ["/new", "/status", "/help", "/compact"]
|
|
126
126
|
},
|
|
127
|
+
"network": {
|
|
128
|
+
"egressProxyUrl": "http://your-proxy-host:8080"
|
|
129
|
+
},
|
|
127
130
|
"agent": {
|
|
128
131
|
"corpId": "企业 CorpID",
|
|
129
132
|
"corpSecret": "应用 Secret",
|
|
@@ -149,7 +152,7 @@ npm run test:e2e
|
|
|
149
152
|
| `plugins.entries.wecom.enabled` | boolean | 是 | 启用插件 |
|
|
150
153
|
| `channels.wecom.token` | string | 是* | 企业微信机器人 Token (*Bot 模式必填) |
|
|
151
154
|
| `channels.wecom.encodingAesKey` | string | 是* | 消息加密密钥(43 位)(*Bot 模式必填) |
|
|
152
|
-
| `channels.wecom.adminUsers` | array | 否 | 管理员用户 ID
|
|
155
|
+
| `channels.wecom.adminUsers` | array | 否 | 管理员用户 ID 列表(绕过指令白名单) |
|
|
153
156
|
| `channels.wecom.commands.enabled` | boolean | 否 | 是否启用指令白名单过滤(默认 true) |
|
|
154
157
|
| `channels.wecom.commands.allowlist` | array | 否 | 允许的指令白名单 |
|
|
155
158
|
|
|
@@ -160,6 +163,7 @@ npm run test:e2e
|
|
|
160
163
|
| 配置项 | 类型 | 必填 | 说明 |
|
|
161
164
|
|--------|------|------|------|
|
|
162
165
|
| `channels.wecom.dynamicAgents.enabled` | boolean | 否 | 是否启用动态 Agent(默认 true) |
|
|
166
|
+
| `channels.wecom.dynamicAgents.adminBypass` | boolean | 否 | 管理员是否跳过动态 Agent 路由(默认 false) |
|
|
163
167
|
| `channels.wecom.dm.createAgentOnFirstMessage` | boolean | 否 | 私聊时为每个用户创建独立 Agent(默认 true) |
|
|
164
168
|
| `channels.wecom.groupChat.enabled` | boolean | 否 | 是否启用群聊处理(默认 true) |
|
|
165
169
|
| `channels.wecom.groupChat.requireMention` | boolean | 否 | 群聊是否必须 @ 提及才响应(默认 true) |
|
|
@@ -186,6 +190,15 @@ npm run test:e2e
|
|
|
186
190
|
| `channels.wecom.agent.token` | string | 是 | 回调 Token (用于验证签名) |
|
|
187
191
|
| `channels.wecom.agent.encodingAesKey` | string | 是 | 回调 EncodingAESKey (43 位) |
|
|
188
192
|
|
|
193
|
+
#### 网络代理配置 (可选)
|
|
194
|
+
|
|
195
|
+
用于 Agent / Webhook 等外发请求走固定出口代理(适用于企业微信固定 IP 白名单场景)。
|
|
196
|
+
|
|
197
|
+
| 配置项 | 类型 | 必填 | 说明 |
|
|
198
|
+
|--------|------|------|------|
|
|
199
|
+
| `channels.wecom.network.egressProxyUrl` | string | 否 | 外发 HTTP(S) 代理地址,例如 `http://proxy:8080` |
|
|
200
|
+
| `WECOM_EGRESS_PROXY_URL` | env | 否 | 环境变量方式配置代理,优先级高于 `channels.wecom.network.egressProxyUrl` |
|
|
201
|
+
|
|
189
202
|
#### Webhook 配置 (可选)
|
|
190
203
|
|
|
191
204
|
配置 Webhook Bot 用于群通知:
|
|
@@ -283,13 +296,16 @@ Webhook Bot 用于向群聊发送通知消息。
|
|
|
283
296
|
|
|
284
297
|
## 管理员用户
|
|
285
298
|
|
|
286
|
-
|
|
299
|
+
管理员用户默认可以绕过指令白名单限制。若希望管理员用户同时跳过动态 Agent 路由(直接路由到主 Agent),可开启 `dynamicAgents.adminBypass`。
|
|
287
300
|
|
|
288
301
|
```json
|
|
289
302
|
{
|
|
290
303
|
"channels": {
|
|
291
304
|
"wecom": {
|
|
292
|
-
"adminUsers": ["user1", "user2"]
|
|
305
|
+
"adminUsers": ["user1", "user2"],
|
|
306
|
+
"dynamicAgents": {
|
|
307
|
+
"adminBypass": true
|
|
308
|
+
}
|
|
293
309
|
}
|
|
294
310
|
}
|
|
295
311
|
}
|
|
@@ -310,7 +326,7 @@ Webhook Bot 用于向群聊发送通知消息。
|
|
|
310
326
|
- **多账号群聊**: `wecom-<accountId>-group-<chatId>`
|
|
311
327
|
2. OpenClaw 自动创建/复用对应的 Agent 工作区
|
|
312
328
|
3. 每个用户/群聊拥有独立的对话历史和上下文
|
|
313
|
-
4.
|
|
329
|
+
4. 管理员用户默认参与动态路由;当 `dynamicAgents.adminBypass=true` 时跳过动态路由,直接使用主 Agent
|
|
314
330
|
|
|
315
331
|
### 高级配置
|
|
316
332
|
|
|
@@ -338,6 +354,7 @@ Webhook Bot 用于向群聊发送通知消息。
|
|
|
338
354
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
339
355
|
|--------|------|--------|------|
|
|
340
356
|
| `dynamicAgents.enabled` | boolean | `true` | 是否启用动态 Agent |
|
|
357
|
+
| `dynamicAgents.adminBypass` | boolean | `false` | 管理员是否跳过动态 Agent 路由 |
|
|
341
358
|
| `dm.createAgentOnFirstMessage` | boolean | `true` | 私聊使用动态 Agent |
|
|
342
359
|
| `groupChat.enabled` | boolean | `true` | 启用群聊处理 |
|
|
343
360
|
| `groupChat.requireMention` | boolean | `true` | 群聊必须 @ 提及才响应 |
|
|
@@ -372,6 +389,7 @@ Webhook Bot 用于向群聊发送通知消息。
|
|
|
372
389
|
"token": "Bot1 的 Token",
|
|
373
390
|
"encodingAesKey": "Bot1 的 EncodingAESKey",
|
|
374
391
|
"adminUsers": ["admin1"],
|
|
392
|
+
"workspaceTemplate": "/path/to/bot1-template",
|
|
375
393
|
"agent": {
|
|
376
394
|
"corpId": "企业 CorpID",
|
|
377
395
|
"corpSecret": "Bot1 应用 Secret",
|
|
@@ -405,6 +423,7 @@ Webhook Bot 用于向群聊发送通知消息。
|
|
|
405
423
|
| 完全兼容 | 旧的单账号配置(`token` 直接写在 `wecom` 下)自动识别为 `default` 账号,无需修改 |
|
|
406
424
|
| Webhook 路径 | 自动按账号分配:`/webhooks/wecom/bot1`、`/webhooks/wecom/bot2` |
|
|
407
425
|
| Agent 回调路径 | 自动按账号分配:`/webhooks/app/bot1`、`/webhooks/app/bot2` |
|
|
426
|
+
| 工作区模板 | 支持按账号自定义:`channels.wecom.<accountId>.workspaceTemplate`(覆盖全局配置) |
|
|
408
427
|
| 动态 Agent ID | 按账号隔离:`wecom-bot1-dm-{userId}`、`wecom-bot2-group-{chatId}` |
|
|
409
428
|
| 冲突检测 | 启动时自动检测重复的 Token 或 Agent ID,避免消息路由错乱 |
|
|
410
429
|
|
package/dynamic-agent.js
CHANGED
|
@@ -47,17 +47,21 @@ export function getDynamicAgentConfig(config) {
|
|
|
47
47
|
groupEnabled: wecom.groupChat?.enabled !== false,
|
|
48
48
|
groupRequireMention: wecom.groupChat?.requireMention !== false,
|
|
49
49
|
groupMentionPatterns: wecom.groupChat?.mentionPatterns || ["@"],
|
|
50
|
+
adminBypass: wecom.dynamicAgents?.adminBypass === true,
|
|
50
51
|
};
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
55
|
* Decide whether this message context should route to a dynamic agent.
|
|
55
56
|
*/
|
|
56
|
-
export function shouldUseDynamicAgent({ chatType, config }) {
|
|
57
|
+
export function shouldUseDynamicAgent({ chatType, config, senderIsAdmin = false }) {
|
|
57
58
|
const dynamicConfig = getDynamicAgentConfig(config);
|
|
58
59
|
if (!dynamicConfig.enabled) {
|
|
59
60
|
return false;
|
|
60
61
|
}
|
|
62
|
+
if (senderIsAdmin && dynamicConfig.adminBypass) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
61
65
|
if (chatType === "group") {
|
|
62
66
|
return dynamicConfig.groupEnabled;
|
|
63
67
|
}
|
package/index.js
CHANGED
|
@@ -38,16 +38,18 @@ const plugin = {
|
|
|
38
38
|
api.registerChannel({ plugin: wecomChannelPlugin });
|
|
39
39
|
logger.info("WeCom channel registered");
|
|
40
40
|
|
|
41
|
-
// Register HTTP
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
// Register webhook HTTP route with auth: "plugin" so gateway does NOT
|
|
42
|
+
// enforce Bearer-token auth. WeCom callbacks use msg_signature verification
|
|
43
|
+
// which the plugin handles internally.
|
|
44
|
+
// OpenClaw 3.2 removed registerHttpHandler; use registerHttpRoute with
|
|
45
|
+
// auth: "plugin" + match: "prefix" to handle all /webhooks/* paths.
|
|
46
|
+
api.registerHttpRoute({
|
|
47
|
+
path: "/webhooks",
|
|
48
|
+
handler: wecomHttpHandler,
|
|
49
|
+
auth: "plugin",
|
|
50
|
+
match: "prefix",
|
|
51
|
+
});
|
|
52
|
+
logger.info("WeCom HTTP route registered (auth: plugin, match: prefix)");
|
|
51
53
|
},
|
|
52
54
|
};
|
|
53
55
|
|
package/package.json
CHANGED
package/wecom/accounts.js
CHANGED
|
@@ -79,16 +79,17 @@ function buildAccount(accountId, accountCfg) {
|
|
|
79
79
|
agent?.corpId && agent?.corpSecret && agent?.agentId && agent?.token && agent?.encodingAesKey,
|
|
80
80
|
);
|
|
81
81
|
|
|
82
|
+
const hasBotTokens = Boolean(accountCfg?.token && accountCfg?.encodingAesKey);
|
|
83
|
+
const defaultPath = accountId === DEFAULT_ACCOUNT_ID ? "/webhooks/wecom" : `/webhooks/wecom/${accountId}`;
|
|
84
|
+
|
|
82
85
|
return {
|
|
83
86
|
accountId,
|
|
84
87
|
name: accountCfg?.name || accountId,
|
|
85
88
|
enabled: accountCfg?.enabled !== false,
|
|
86
|
-
configured:
|
|
89
|
+
configured: hasBotTokens || agentConfigured,
|
|
87
90
|
token: accountCfg?.token || "",
|
|
88
91
|
encodingAesKey: accountCfg?.encodingAesKey || "",
|
|
89
|
-
webhookPath:
|
|
90
|
-
accountCfg?.webhookPath ||
|
|
91
|
-
(accountId === DEFAULT_ACCOUNT_ID ? "/webhooks/wecom" : `/webhooks/wecom/${accountId}`),
|
|
92
|
+
webhookPath: accountCfg?.webhookPath || (hasBotTokens ? defaultPath : ""),
|
|
92
93
|
config: accountCfg || {},
|
|
93
94
|
agentConfigured,
|
|
94
95
|
agentInboundConfigured,
|
package/wecom/agent-inbound.js
CHANGED
|
@@ -292,12 +292,18 @@ async function processAgentMessage({
|
|
|
292
292
|
|
|
293
293
|
const dynamicConfig = getDynamicAgentConfig(accountCfg);
|
|
294
294
|
const targetAgentId =
|
|
295
|
-
dynamicConfig.enabled
|
|
295
|
+
dynamicConfig.enabled
|
|
296
|
+
&& shouldUseDynamicAgent({
|
|
297
|
+
chatType: peerKind,
|
|
298
|
+
config: accountCfg,
|
|
299
|
+
senderIsAdmin,
|
|
300
|
+
})
|
|
296
301
|
? generateAgentId(peerKind, peerId, accountId)
|
|
297
302
|
: null;
|
|
298
303
|
|
|
304
|
+
const templateDir = accountCfg?.workspaceTemplate || config?.channels?.wecom?.workspaceTemplate;
|
|
299
305
|
if (targetAgentId) {
|
|
300
|
-
await ensureDynamicAgentListed(targetAgentId);
|
|
306
|
+
await ensureDynamicAgentListed(targetAgentId, templateDir);
|
|
301
307
|
logger.debug("[agent-inbound] dynamic agent", { agentId: targetAgentId, peerId });
|
|
302
308
|
}
|
|
303
309
|
|
package/wecom/channel-plugin.js
CHANGED
|
@@ -13,7 +13,7 @@ import { resolveWecomTarget } from "./target.js";
|
|
|
13
13
|
import { webhookSendImage, webhookSendText, webhookUploadFile, webhookSendFile } from "./webhook-bot.js";
|
|
14
14
|
import { normalizeWebhookPath, registerWebhookTarget } from "./webhook-targets.js";
|
|
15
15
|
import { wecomFetch, setConfigProxyUrl } from "./http.js";
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
|
|
18
18
|
const AGENT_IMAGE_EXTS = new Set(["jpg", "jpeg", "png", "gif", "bmp"]);
|
|
19
19
|
|
|
@@ -92,6 +92,11 @@ export const wecomChannelPlugin = {
|
|
|
92
92
|
description: "Enable per-user/per-group agent isolation",
|
|
93
93
|
default: true,
|
|
94
94
|
},
|
|
95
|
+
adminBypass: {
|
|
96
|
+
type: "boolean",
|
|
97
|
+
description: "When true, adminUsers bypass dynamic agent routing and use the default route",
|
|
98
|
+
default: false,
|
|
99
|
+
},
|
|
95
100
|
},
|
|
96
101
|
},
|
|
97
102
|
dm: {
|
|
@@ -720,36 +725,21 @@ export const wecomChannelPlugin = {
|
|
|
720
725
|
});
|
|
721
726
|
}
|
|
722
727
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
// Register HTTP route with OpenClaw route framework.
|
|
730
|
-
// Uses registerPluginHttpRoute (new API in OpenClaw 2026.3.2+) for explicit
|
|
731
|
-
// path-based routing. Falls back gracefully when the SDK is unavailable
|
|
732
|
-
// (older OpenClaw uses the legacy wildcard handler registered in index.js).
|
|
733
|
-
let unregisterBotRoute;
|
|
734
|
-
const botPath = account.webhookPath || "/webhooks/wecom";
|
|
735
|
-
try {
|
|
736
|
-
const { registerPluginHttpRoute } = await import("openclaw/plugin-sdk");
|
|
737
|
-
unregisterBotRoute = registerPluginHttpRoute({
|
|
728
|
+
let unregister;
|
|
729
|
+
const botPath = account.webhookPath;
|
|
730
|
+
if (botPath) {
|
|
731
|
+
unregister = registerWebhookTarget({
|
|
738
732
|
path: botPath,
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
log: (msg) => logger.info(msg),
|
|
742
|
-
handler: createWecomRouteHandler(normalizeWebhookPath(botPath)),
|
|
733
|
+
account,
|
|
734
|
+
config: ctx.cfg,
|
|
743
735
|
});
|
|
744
|
-
logger.info("WeCom Bot
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
logger.debug("registerPluginHttpRoute unavailable, using legacy handler", { path: botPath });
|
|
736
|
+
logger.info("WeCom Bot webhook path active", { path: botPath });
|
|
737
|
+
} else {
|
|
738
|
+
logger.debug("No Bot webhook path for this account, skipping", { accountId: account.accountId });
|
|
748
739
|
}
|
|
749
740
|
|
|
750
741
|
// Register Agent inbound webhook if agent inbound is fully configured.
|
|
751
742
|
let unregisterAgent;
|
|
752
|
-
let unregisterAgentRoute;
|
|
753
743
|
// Per-account agent path: /webhooks/app for default, /webhooks/app/{accountId} for others.
|
|
754
744
|
const agentInboundPath = account.accountId === DEFAULT_ACCOUNT_ID
|
|
755
745
|
? "/webhooks/app"
|
|
@@ -778,22 +768,6 @@ export const wecomChannelPlugin = {
|
|
|
778
768
|
config: ctx.cfg,
|
|
779
769
|
});
|
|
780
770
|
logger.info("WeCom Agent inbound webhook registered", { path: agentInboundPath });
|
|
781
|
-
|
|
782
|
-
// Register agent inbound HTTP route (new API).
|
|
783
|
-
try {
|
|
784
|
-
const { registerPluginHttpRoute } = await import("openclaw/plugin-sdk");
|
|
785
|
-
unregisterAgentRoute = registerPluginHttpRoute({
|
|
786
|
-
path: agentInboundPath,
|
|
787
|
-
pluginId: "wecom",
|
|
788
|
-
accountId: account.accountId,
|
|
789
|
-
source: "agent-inbound",
|
|
790
|
-
log: (msg) => logger.info(msg),
|
|
791
|
-
handler: createWecomRouteHandler(normalizeWebhookPath(agentInboundPath)),
|
|
792
|
-
});
|
|
793
|
-
logger.info("WeCom Agent inbound HTTP route registered", { path: agentInboundPath });
|
|
794
|
-
} catch {
|
|
795
|
-
logger.debug("registerPluginHttpRoute unavailable for agent inbound, using legacy handler");
|
|
796
|
-
}
|
|
797
771
|
}
|
|
798
772
|
}
|
|
799
773
|
|
|
@@ -804,10 +778,8 @@ export const wecomChannelPlugin = {
|
|
|
804
778
|
clearTimeout(buf.timer);
|
|
805
779
|
}
|
|
806
780
|
messageBuffers.clear();
|
|
807
|
-
unregister();
|
|
808
|
-
if (unregisterBotRoute) unregisterBotRoute();
|
|
781
|
+
if (unregister) unregister();
|
|
809
782
|
if (unregisterAgent) unregisterAgent();
|
|
810
|
-
if (unregisterAgentRoute) unregisterAgentRoute();
|
|
811
783
|
};
|
|
812
784
|
|
|
813
785
|
// Backward compatibility: older runtime may not pass abortSignal.
|
|
@@ -241,12 +241,19 @@ export async function processInboundMessage({
|
|
|
241
241
|
|
|
242
242
|
// Compute deterministic agent target for this conversation.
|
|
243
243
|
const targetAgentId =
|
|
244
|
-
dynamicConfig.enabled
|
|
244
|
+
dynamicConfig.enabled
|
|
245
|
+
&& shouldUseDynamicAgent({
|
|
246
|
+
chatType: peerKind,
|
|
247
|
+
config: account.config,
|
|
248
|
+
senderIsAdmin,
|
|
249
|
+
})
|
|
245
250
|
? generateAgentId(peerKind, peerId, account.accountId)
|
|
246
251
|
: null;
|
|
247
252
|
|
|
253
|
+
// Resolve template directory: per-account or global.
|
|
254
|
+
const templateDir = account.config?.workspaceTemplate || config?.channels?.wecom?.workspaceTemplate;
|
|
248
255
|
if (targetAgentId) {
|
|
249
|
-
await ensureDynamicAgentListed(targetAgentId);
|
|
256
|
+
await ensureDynamicAgentListed(targetAgentId, templateDir);
|
|
250
257
|
logger.debug("Using dynamic agent", { agentId: targetAgentId, chatType: peerKind, peerId });
|
|
251
258
|
} else if (senderIsAdmin) {
|
|
252
259
|
logger.debug("Admin user, dynamic agent disabled for this chat type; falling back to default route", {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readFile,
|
|
2
|
-
import { basename,
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { basename, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { logger } from "../logger.js";
|
|
4
4
|
import { streamManager } from "../stream-manager.js";
|
|
5
5
|
import { agentSendText, agentUploadMedia, agentSendMedia } from "./agent-api.js";
|
|
@@ -19,10 +19,36 @@ const WECOM_MIN_FILE_SIZE = 5;
|
|
|
19
19
|
* ~/.openclaw/workspace-{agentId} on the host. Any path starting with
|
|
20
20
|
* /workspace/ is transparently rewritten when an agentId is available.
|
|
21
21
|
*/
|
|
22
|
+
export function resolveWorkspaceHostPathSafe({ workspaceDir, workspacePath }) {
|
|
23
|
+
const relativePath = String(workspacePath || "").replace(/^\/workspace\/?/, "");
|
|
24
|
+
if (!relativePath) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const hostPath = resolve(workspaceDir, relativePath);
|
|
29
|
+
const rel = relative(workspaceDir, hostPath);
|
|
30
|
+
const escapesWorkspace = rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel);
|
|
31
|
+
if (escapesWorkspace) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return hostPath;
|
|
35
|
+
}
|
|
36
|
+
|
|
22
37
|
function resolveHostPath(filePath, effectiveAgentId) {
|
|
23
38
|
if (effectiveAgentId && filePath.startsWith("/workspace/")) {
|
|
24
|
-
const
|
|
25
|
-
const hostPath =
|
|
39
|
+
const workspaceDir = resolveAgentWorkspaceDirLocal(effectiveAgentId);
|
|
40
|
+
const hostPath = resolveWorkspaceHostPathSafe({
|
|
41
|
+
workspaceDir,
|
|
42
|
+
workspacePath: filePath,
|
|
43
|
+
});
|
|
44
|
+
if (!hostPath) {
|
|
45
|
+
logger.warn("Rejected unsafe /workspace/ path outside workspace", {
|
|
46
|
+
sandbox: filePath,
|
|
47
|
+
workspaceDir,
|
|
48
|
+
agentId: effectiveAgentId,
|
|
49
|
+
});
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
26
52
|
logger.debug("Resolved sandbox path to host path", { sandbox: filePath, host: hostPath });
|
|
27
53
|
return hostPath;
|
|
28
54
|
}
|
|
@@ -110,6 +136,12 @@ export async function deliverWecomReply({ payload, senderId, streamId, agentId }
|
|
|
110
136
|
for (const media of mediaMatches) {
|
|
111
137
|
// Resolve /workspace/ sandbox paths to host-side paths.
|
|
112
138
|
const resolvedMediaPath = resolveHostPath(media.path, effectiveAgentId);
|
|
139
|
+
if (!resolvedMediaPath) {
|
|
140
|
+
processedText = processedText
|
|
141
|
+
.replace(media.fullMatch, "⚠️ 检测到不安全的 /workspace/ 路径,已拒绝发送")
|
|
142
|
+
.trim();
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
113
145
|
const mediaExt = resolvedMediaPath.split(".").pop()?.toLowerCase() || "";
|
|
114
146
|
if (mediaImageExts.has(mediaExt)) {
|
|
115
147
|
// Image: queue for delivery when stream finishes.
|
|
@@ -177,6 +209,15 @@ export async function deliverWecomReply({ payload, senderId, streamId, agentId }
|
|
|
177
209
|
}
|
|
178
210
|
// Resolve /workspace/ sandbox paths to host-side paths.
|
|
179
211
|
absPath = resolveHostPath(absPath, effectiveAgentId);
|
|
212
|
+
if (!absPath) {
|
|
213
|
+
const unsafeHint = "⚠️ 检测到不安全的 /workspace/ 路径,已拒绝发送";
|
|
214
|
+
if (streamId && streamManager.hasStream(streamId)) {
|
|
215
|
+
streamManager.appendStream(streamId, `\n\n${unsafeHint}`);
|
|
216
|
+
} else {
|
|
217
|
+
processedText = processedText ? `${processedText}\n\n${unsafeHint}` : unsafeHint;
|
|
218
|
+
}
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
180
221
|
|
|
181
222
|
const isLocal = absPath.startsWith("/");
|
|
182
223
|
const mediaFilename = isLocal ? basename(absPath) : (basename(new URL(mediaPath).pathname) || "file");
|
|
@@ -298,19 +339,36 @@ export async function deliverWecomReply({ payload, senderId, streamId, agentId }
|
|
|
298
339
|
const imageExtsAuto = new Set(["jpg", "jpeg", "png", "gif", "bmp", "webp"]);
|
|
299
340
|
|
|
300
341
|
for (const wsPath of detectedPaths) {
|
|
301
|
-
// /workspace/foo.pdf → hostDir/foo.pdf
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
342
|
+
// /workspace/foo.pdf → hostDir/foo.pdf (with traversal guard)
|
|
343
|
+
const hostPath = resolveWorkspaceHostPathSafe({
|
|
344
|
+
workspaceDir,
|
|
345
|
+
workspacePath: wsPath,
|
|
346
|
+
});
|
|
347
|
+
if (!hostPath) {
|
|
348
|
+
processedText = processedText.replace(wsPath, "⚠️ 检测到不安全的 /workspace/ 路径,已拒绝发送");
|
|
349
|
+
logger.warn("Auto-detect: rejected unsafe /workspace/ path", {
|
|
350
|
+
streamId,
|
|
351
|
+
wsPath,
|
|
352
|
+
workspaceDir,
|
|
353
|
+
});
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
305
356
|
const filename = basename(hostPath);
|
|
306
357
|
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
|
307
358
|
|
|
308
359
|
// Skip image files — they are handled by the stream msg_item mechanism.
|
|
309
360
|
if (imageExtsAuto.has(ext)) continue;
|
|
310
361
|
|
|
311
|
-
// Check
|
|
362
|
+
// Check the path exists on host and is a regular file.
|
|
312
363
|
try {
|
|
313
|
-
await
|
|
364
|
+
const st = await stat(hostPath);
|
|
365
|
+
if (!st.isFile()) {
|
|
366
|
+
logger.debug("Auto-detect: path is not a regular file, skipping", {
|
|
367
|
+
wsPath,
|
|
368
|
+
hostPath,
|
|
369
|
+
});
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
314
372
|
} catch {
|
|
315
373
|
logger.debug("Auto-detect: workspace file not found on host, skipping", {
|
|
316
374
|
wsPath,
|
|
@@ -36,9 +36,13 @@ export function getWorkspaceTemplateDir(config) {
|
|
|
36
36
|
* Copy template files into a newly created agent's workspace directory.
|
|
37
37
|
* Only copies files that don't already exist (writeFileIfMissing semantics).
|
|
38
38
|
* Silently skips if workspaceTemplate is not configured or directory is missing.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} agentId
|
|
41
|
+
* @param {object} config - OpenClaw config
|
|
42
|
+
* @param {string} [overrideTemplateDir] - Optional per-account template directory
|
|
39
43
|
*/
|
|
40
|
-
export function seedAgentWorkspace(agentId, config) {
|
|
41
|
-
const templateDir = getWorkspaceTemplateDir(config);
|
|
44
|
+
export function seedAgentWorkspace(agentId, config, overrideTemplateDir) {
|
|
45
|
+
const templateDir = overrideTemplateDir || getWorkspaceTemplateDir(config);
|
|
42
46
|
if (!templateDir) {
|
|
43
47
|
return;
|
|
44
48
|
}
|
|
@@ -114,7 +118,7 @@ export function upsertAgentIdOnlyEntry(cfg, agentId) {
|
|
|
114
118
|
return changed;
|
|
115
119
|
}
|
|
116
120
|
|
|
117
|
-
export async function ensureDynamicAgentListed(agentId) {
|
|
121
|
+
export async function ensureDynamicAgentListed(agentId, templateDir) {
|
|
118
122
|
const normalizedId = String(agentId || "")
|
|
119
123
|
.trim()
|
|
120
124
|
.toLowerCase();
|
|
@@ -128,28 +132,22 @@ export async function ensureDynamicAgentListed(agentId) {
|
|
|
128
132
|
return;
|
|
129
133
|
}
|
|
130
134
|
|
|
131
|
-
const queue = getEnsureDynamicAgentWriteQueue()
|
|
135
|
+
const queue = (getEnsureDynamicAgentWriteQueue() || Promise.resolve())
|
|
132
136
|
.then(async () => {
|
|
133
|
-
const
|
|
134
|
-
if (!
|
|
137
|
+
const openclawConfig = getOpenclawConfig();
|
|
138
|
+
if (!openclawConfig || typeof openclawConfig !== "object") {
|
|
135
139
|
return;
|
|
136
140
|
}
|
|
137
141
|
|
|
138
|
-
|
|
142
|
+
// Upsert into memory only. Writing to config file is dangerous and can wipe user settings.
|
|
143
|
+
const changed = upsertAgentIdOnlyEntry(openclawConfig, normalizedId);
|
|
139
144
|
if (changed) {
|
|
140
|
-
|
|
141
|
-
logger.info("WeCom: dynamic agent added to agents.list", { agentId: normalizedId });
|
|
145
|
+
logger.info("WeCom: dynamic agent added to in-memory agents.list", { agentId: normalizedId });
|
|
142
146
|
}
|
|
147
|
+
|
|
143
148
|
// Always attempt seeding so recreated/cleaned dynamic agents can recover
|
|
144
|
-
// template files
|
|
145
|
-
seedAgentWorkspace(normalizedId,
|
|
146
|
-
|
|
147
|
-
// Keep runtime in-memory config aligned to avoid stale reads in this process.
|
|
148
|
-
const openclawConfig = getOpenclawConfig();
|
|
149
|
-
if (openclawConfig && typeof openclawConfig === "object") {
|
|
150
|
-
upsertAgentIdOnlyEntry(openclawConfig, normalizedId);
|
|
151
|
-
setOpenclawConfig(openclawConfig);
|
|
152
|
-
}
|
|
149
|
+
// template files.
|
|
150
|
+
seedAgentWorkspace(normalizedId, openclawConfig, templateDir);
|
|
153
151
|
|
|
154
152
|
getEnsuredDynamicAgentIds().add(normalizedId);
|
|
155
153
|
})
|