@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.
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.5",
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",
@@ -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, {
@@ -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
 
@@ -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