@mocrane/wecom 2026.2.5 → 2026.2.27
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/assets/01.image.jpg +0 -0
- package/assets/02.image.jpg +0 -0
- package/assets/03.agent.page.png +0 -0
- package/assets/03.bot.page.png +0 -0
- package/assets/link-me.jpg +0 -0
- package/package.json +2 -1
- package/src/agent/handler.ts +21 -0
- package/src/config/schema.ts +17 -0
- package/src/dynamic-agent.ts +178 -0
- package/src/monitor.ts +21 -0
- package/src/types/config.ts +14 -0
- package/README.md +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mocrane/wecom",
|
|
3
|
-
"version": "2026.2.
|
|
3
|
+
"version": "2026.2.27",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw WeCom (WeChat Work) intelligent bot channel plugin",
|
|
6
6
|
"main": "index.ts",
|
|
7
7
|
"files": [
|
|
8
8
|
"index.ts",
|
|
9
9
|
"src/",
|
|
10
|
+
"assets/",
|
|
10
11
|
"openclaw.plugin.json",
|
|
11
12
|
"clawdbot.plugin.json",
|
|
12
13
|
"README.md",
|
package/src/agent/handler.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { getWecomRuntime } from "../runtime.js";
|
|
|
17
17
|
import type { WecomAgentInboundMessage } from "../types/index.js";
|
|
18
18
|
import { buildWecomUnauthorizedCommandPrompt, resolveWecomCommandAuthorization } from "../shared/command-auth.js";
|
|
19
19
|
import { resolveWecomMediaMaxBytes } from "../config/index.js";
|
|
20
|
+
import { generateAgentId, shouldUseDynamicAgent, ensureDynamicAgentListed } from "../dynamic-agent.js";
|
|
20
21
|
|
|
21
22
|
/** 错误提示信息 */
|
|
22
23
|
const ERROR_HELP = "";
|
|
@@ -442,6 +443,26 @@ async function processAgentMessage(params: {
|
|
|
442
443
|
peer: { kind: isGroup ? "group" : "dm", id: peerId },
|
|
443
444
|
});
|
|
444
445
|
|
|
446
|
+
// ===== 动态 Agent 路由注入 =====
|
|
447
|
+
const useDynamicAgent = shouldUseDynamicAgent({
|
|
448
|
+
chatType: isGroup ? "group" : "dm",
|
|
449
|
+
senderId: fromUser,
|
|
450
|
+
config,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
if (useDynamicAgent) {
|
|
454
|
+
const targetAgentId = generateAgentId(
|
|
455
|
+
isGroup ? "group" : "dm",
|
|
456
|
+
peerId
|
|
457
|
+
);
|
|
458
|
+
route.agentId = targetAgentId;
|
|
459
|
+
route.sessionKey = `agent:${targetAgentId}:${isGroup ? "group" : "dm"}:${peerId}`;
|
|
460
|
+
// 异步添加到 agents.list(不阻塞)
|
|
461
|
+
ensureDynamicAgentListed(targetAgentId, core).catch(() => {});
|
|
462
|
+
log?.(`[wecom-agent] dynamic agent routing: ${targetAgentId}, sessionKey=${route.sessionKey}`);
|
|
463
|
+
}
|
|
464
|
+
// ===== 动态 Agent 路由注入结束 =====
|
|
465
|
+
|
|
445
466
|
// 构建上下文
|
|
446
467
|
const fromLabel = isGroup ? `group:${peerId}` : `user:${fromUser}`;
|
|
447
468
|
const storePath = core.channel.session.resolveStorePath(config.session?.store, {
|
package/src/config/schema.ts
CHANGED
|
@@ -92,6 +92,22 @@ const agentSchema = z.object({
|
|
|
92
92
|
dm: dmSchema,
|
|
93
93
|
}).optional();
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* **dynamicAgentsSchema (动态 Agent 配置)**
|
|
97
|
+
*
|
|
98
|
+
* 控制是否按用户/群组自动创建独立 Agent 实例。
|
|
99
|
+
* @property enabled - 是否启用动态 Agent
|
|
100
|
+
* @property dmCreateAgent - 私聊是否为每个用户创建独立 Agent
|
|
101
|
+
* @property groupEnabled - 群聊是否启用动态 Agent
|
|
102
|
+
* @property adminUsers - 管理员列表(绕过动态路由)
|
|
103
|
+
*/
|
|
104
|
+
const dynamicAgentsSchema = z.object({
|
|
105
|
+
enabled: z.boolean().optional(),
|
|
106
|
+
dmCreateAgent: z.boolean().optional(),
|
|
107
|
+
groupEnabled: z.boolean().optional(),
|
|
108
|
+
adminUsers: z.array(z.string()).optional(),
|
|
109
|
+
}).optional();
|
|
110
|
+
|
|
95
111
|
/** 顶层 WeCom 配置 Schema */
|
|
96
112
|
export const WecomConfigSchema = z.object({
|
|
97
113
|
enabled: z.boolean().optional(),
|
|
@@ -99,6 +115,7 @@ export const WecomConfigSchema = z.object({
|
|
|
99
115
|
agent: agentSchema,
|
|
100
116
|
media: mediaSchema,
|
|
101
117
|
network: networkSchema,
|
|
118
|
+
dynamicAgents: dynamicAgentsSchema,
|
|
102
119
|
});
|
|
103
120
|
|
|
104
121
|
export type WecomConfigInput = z.infer<typeof WecomConfigSchema>;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **动态 Agent 路由模块**
|
|
3
|
+
*
|
|
4
|
+
* 为每个用户/群组自动生成独立的 Agent ID,实现会话隔离。
|
|
5
|
+
* 参考: openclaw-plugin-wecom/dynamic-agent.js
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
9
|
+
|
|
10
|
+
export interface DynamicAgentConfig {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
dmCreateAgent: boolean;
|
|
13
|
+
groupEnabled: boolean;
|
|
14
|
+
adminUsers: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* **getDynamicAgentConfig (读取动态 Agent 配置)**
|
|
19
|
+
*
|
|
20
|
+
* 从全局配置中读取动态 Agent 配置,提供默认值。
|
|
21
|
+
*/
|
|
22
|
+
export function getDynamicAgentConfig(config: OpenClawConfig): DynamicAgentConfig {
|
|
23
|
+
const dynamicAgents = (config as { channels?: { wecom?: { dynamicAgents?: Partial<DynamicAgentConfig> } } })?.channels?.wecom?.dynamicAgents;
|
|
24
|
+
return {
|
|
25
|
+
enabled: dynamicAgents?.enabled ?? false,
|
|
26
|
+
dmCreateAgent: dynamicAgents?.dmCreateAgent ?? true,
|
|
27
|
+
groupEnabled: dynamicAgents?.groupEnabled ?? true,
|
|
28
|
+
adminUsers: dynamicAgents?.adminUsers ?? [],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* **generateAgentId (生成动态 Agent ID)**
|
|
34
|
+
*
|
|
35
|
+
* 根据聊天类型和对端 ID 生成确定性的 Agent ID。
|
|
36
|
+
* 格式: wecom-{type}-{sanitizedPeerId}
|
|
37
|
+
* - type: dm | group
|
|
38
|
+
* - sanitizedPeerId: 小写,非字母数字下划线横线替换为下划线
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* generateAgentId("dm", "ZhangSan") // "wecom-dm-zhangsan"
|
|
42
|
+
* generateAgentId("group", "wr123456") // "wecom-group-wr123456"
|
|
43
|
+
*/
|
|
44
|
+
export function generateAgentId(chatType: "dm" | "group", peerId: string): string {
|
|
45
|
+
const sanitized = String(peerId)
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
.replace(/[^a-z0-9_-]/g, "_");
|
|
48
|
+
return `wecom-${chatType}-${sanitized}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* **shouldUseDynamicAgent (检查是否使用动态 Agent)**
|
|
53
|
+
*
|
|
54
|
+
* 根据配置和发送者信息判断是否应使用动态 Agent。
|
|
55
|
+
* 管理员(adminUsers)始终绕过动态路由,使用主 Agent。
|
|
56
|
+
*/
|
|
57
|
+
export function shouldUseDynamicAgent(params: {
|
|
58
|
+
chatType: "dm" | "group";
|
|
59
|
+
senderId: string;
|
|
60
|
+
config: OpenClawConfig;
|
|
61
|
+
}): boolean {
|
|
62
|
+
const { chatType, senderId, config } = params;
|
|
63
|
+
const dynamicConfig = getDynamicAgentConfig(config);
|
|
64
|
+
|
|
65
|
+
if (!dynamicConfig.enabled) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 管理员绕过动态路由
|
|
70
|
+
const sender = String(senderId).trim().toLowerCase();
|
|
71
|
+
const isAdmin = dynamicConfig.adminUsers.some(
|
|
72
|
+
(admin) => admin.trim().toLowerCase() === sender
|
|
73
|
+
);
|
|
74
|
+
if (isAdmin) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (chatType === "group") {
|
|
79
|
+
return dynamicConfig.groupEnabled;
|
|
80
|
+
}
|
|
81
|
+
return dynamicConfig.dmCreateAgent;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 内存中已确保的 Agent ID(避免重复写入)
|
|
86
|
+
*/
|
|
87
|
+
const ensuredDynamicAgentIds = new Set<string>();
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 写入队列(避免并发冲突)
|
|
91
|
+
*/
|
|
92
|
+
let ensureDynamicAgentWriteQueue: Promise<void> = Promise.resolve();
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 将 Agent ID 插入 agents.list(如果不存在)
|
|
96
|
+
*/
|
|
97
|
+
function upsertAgentIdOnlyEntry(cfg: Record<string, unknown>, agentId: string): boolean {
|
|
98
|
+
if (!cfg.agents || typeof cfg.agents !== "object") {
|
|
99
|
+
cfg.agents = {};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const agentsObj = cfg.agents as Record<string, unknown>;
|
|
103
|
+
const currentList: Array<{ id: string }> = Array.isArray(agentsObj.list) ? agentsObj.list as Array<{ id: string }> : [];
|
|
104
|
+
const existingIds = new Set(
|
|
105
|
+
currentList
|
|
106
|
+
.map((entry) => entry?.id?.trim().toLowerCase())
|
|
107
|
+
.filter((id): id is string => Boolean(id))
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
let changed = false;
|
|
111
|
+
const nextList = [...currentList];
|
|
112
|
+
|
|
113
|
+
// 首次创建时保留 main 作为默认
|
|
114
|
+
if (nextList.length === 0) {
|
|
115
|
+
nextList.push({ id: "main" });
|
|
116
|
+
existingIds.add("main");
|
|
117
|
+
changed = true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!existingIds.has(agentId.toLowerCase())) {
|
|
121
|
+
nextList.push({ id: agentId });
|
|
122
|
+
changed = true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (changed) {
|
|
126
|
+
agentsObj.list = nextList;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return changed;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* **ensureDynamicAgentListed (确保动态 Agent 已添加到 agents.list)**
|
|
134
|
+
*
|
|
135
|
+
* 将动态生成的 Agent ID 添加到 OpenClaw 配置中的 agents.list。
|
|
136
|
+
* 特性:
|
|
137
|
+
* - 幂等:使用内存 Set 避免重复写入
|
|
138
|
+
* - 串行:使用 Promise 队列避免并发冲突
|
|
139
|
+
* - 异步:不阻塞消息处理流程
|
|
140
|
+
*/
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
142
|
+
export async function ensureDynamicAgentListed(agentId: string, runtime: any): Promise<void> {
|
|
143
|
+
const normalizedId = String(agentId).trim().toLowerCase();
|
|
144
|
+
if (!normalizedId) return;
|
|
145
|
+
if (ensuredDynamicAgentIds.has(normalizedId)) return;
|
|
146
|
+
|
|
147
|
+
const configRuntime = runtime?.config;
|
|
148
|
+
if (!configRuntime?.loadConfig || !configRuntime?.writeConfigFile) return;
|
|
149
|
+
|
|
150
|
+
ensureDynamicAgentWriteQueue = ensureDynamicAgentWriteQueue
|
|
151
|
+
.then(async () => {
|
|
152
|
+
if (ensuredDynamicAgentIds.has(normalizedId)) return;
|
|
153
|
+
|
|
154
|
+
const latestConfig = configRuntime.loadConfig!();
|
|
155
|
+
if (!latestConfig || typeof latestConfig !== "object") return;
|
|
156
|
+
|
|
157
|
+
const changed = upsertAgentIdOnlyEntry(latestConfig as Record<string, unknown>, normalizedId);
|
|
158
|
+
if (changed) {
|
|
159
|
+
await configRuntime.writeConfigFile!(latestConfig as unknown);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
ensuredDynamicAgentIds.add(normalizedId);
|
|
163
|
+
})
|
|
164
|
+
.catch((err) => {
|
|
165
|
+
console.warn(`[wecom] 动态 Agent 添加失败: ${normalizedId}`, err);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
await ensureDynamicAgentWriteQueue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* **resetEnsuredCache (重置已确保缓存)**
|
|
173
|
+
*
|
|
174
|
+
* 主要用于测试场景,重置内存中的缓存状态。
|
|
175
|
+
*/
|
|
176
|
+
export function resetEnsuredCache(): void {
|
|
177
|
+
ensuredDynamicAgentIds.clear();
|
|
178
|
+
}
|
package/src/monitor.ts
CHANGED
|
@@ -28,6 +28,7 @@ import axios from "axios";
|
|
|
28
28
|
import type { WecomRuntimeEnv, WecomWebhookTarget, StreamState, PendingInbound, ActiveReplyState } from "./monitor/types.js";
|
|
29
29
|
import { monitorState, LIMITS } from "./monitor/state.js";
|
|
30
30
|
import { buildWecomUnauthorizedCommandPrompt, resolveWecomCommandAuthorization } from "./shared/command-auth.js";
|
|
31
|
+
import { generateAgentId, shouldUseDynamicAgent, ensureDynamicAgentListed } from "./dynamic-agent.js";
|
|
31
32
|
|
|
32
33
|
// Global State
|
|
33
34
|
monitorState.streamStore.setFlushHandler((pending) => void flushPending(pending));
|
|
@@ -927,6 +928,26 @@ async function startAgentForStream(params: {
|
|
|
927
928
|
peer: { kind: chatType === "group" ? "group" : "dm", id: chatId },
|
|
928
929
|
});
|
|
929
930
|
|
|
931
|
+
// ===== 动态 Agent 路由注入 =====
|
|
932
|
+
const useDynamicAgent = shouldUseDynamicAgent({
|
|
933
|
+
chatType: chatType === "group" ? "group" : "dm",
|
|
934
|
+
senderId: userid,
|
|
935
|
+
config,
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
if (useDynamicAgent) {
|
|
939
|
+
const targetAgentId = generateAgentId(
|
|
940
|
+
chatType === "group" ? "group" : "dm",
|
|
941
|
+
chatId
|
|
942
|
+
);
|
|
943
|
+
route.agentId = targetAgentId;
|
|
944
|
+
route.sessionKey = `agent:${targetAgentId}:${chatType === "group" ? "group" : "dm"}:${chatId}`;
|
|
945
|
+
// 异步添加到 agents.list(不阻塞)
|
|
946
|
+
ensureDynamicAgentListed(targetAgentId, core).catch(() => {});
|
|
947
|
+
logVerbose(target, `dynamic agent routing: ${targetAgentId}, sessionKey=${route.sessionKey}`);
|
|
948
|
+
}
|
|
949
|
+
// ===== 动态 Agent 路由注入结束 =====
|
|
950
|
+
|
|
930
951
|
logVerbose(target, `starting agent processing (streamId=${streamId}, agentId=${route.agentId}, peerKind=${chatType}, peerId=${chatId})`);
|
|
931
952
|
logVerbose(target, `启动 Agent 处理: streamId=${streamId} 路由=${route.agentId} 类型=${chatType} ID=${chatId}`);
|
|
932
953
|
|
package/src/types/config.ts
CHANGED
|
@@ -70,6 +70,18 @@ export type WecomAgentConfig = {
|
|
|
70
70
|
dm?: WecomDmConfig;
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
/** 动态 Agent 配置 */
|
|
74
|
+
export type WecomDynamicAgentsConfig = {
|
|
75
|
+
/** 是否启用动态 Agent */
|
|
76
|
+
enabled?: boolean;
|
|
77
|
+
/** 私聊:是否为每个用户创建独立 Agent */
|
|
78
|
+
dmCreateAgent?: boolean;
|
|
79
|
+
/** 群聊:是否启用动态 Agent */
|
|
80
|
+
groupEnabled?: boolean;
|
|
81
|
+
/** 管理员列表(绕过动态路由,使用主 Agent) */
|
|
82
|
+
adminUsers?: string[];
|
|
83
|
+
};
|
|
84
|
+
|
|
73
85
|
/**
|
|
74
86
|
* 顶层 WeCom 配置
|
|
75
87
|
* 通过 bot / agent 字段隐式指定模式
|
|
@@ -85,4 +97,6 @@ export type WecomConfig = {
|
|
|
85
97
|
media?: WecomMediaConfig;
|
|
86
98
|
/** 网络配置 */
|
|
87
99
|
network?: WecomNetworkConfig;
|
|
100
|
+
/** 动态 Agent 配置 */
|
|
101
|
+
dynamicAgents?: WecomDynamicAgentsConfig;
|
|
88
102
|
};
|
package/README.md
DELETED
|
File without changes
|