@wecom/wecom-openclaw-plugin 2026.3.30 → 2026.4.1-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.
Files changed (143) hide show
  1. package/dist/cjs/index.js +45 -0
  2. package/dist/cjs/index.js.map +1 -0
  3. package/dist/cjs/src/channel.js +339 -0
  4. package/dist/cjs/src/channel.js.map +1 -0
  5. package/dist/cjs/src/const.js +137 -0
  6. package/dist/cjs/src/const.js.map +1 -0
  7. package/dist/cjs/src/dm-policy.js +89 -0
  8. package/dist/cjs/src/dm-policy.js.map +1 -0
  9. package/dist/cjs/src/group-policy.js +133 -0
  10. package/dist/cjs/src/group-policy.js.map +1 -0
  11. package/dist/cjs/src/mcp/interceptors/biz-error.js +70 -0
  12. package/dist/cjs/src/mcp/interceptors/biz-error.js.map +1 -0
  13. package/dist/cjs/src/mcp/interceptors/index.js +73 -0
  14. package/dist/cjs/src/mcp/interceptors/index.js.map +1 -0
  15. package/dist/cjs/src/mcp/interceptors/msg-media.js +159 -0
  16. package/dist/cjs/src/mcp/interceptors/msg-media.js.map +1 -0
  17. package/dist/cjs/src/mcp/interceptors/smartpage-create.js +157 -0
  18. package/dist/cjs/src/mcp/interceptors/smartpage-create.js.map +1 -0
  19. package/dist/cjs/src/mcp/interceptors/smartpage-export.js +93 -0
  20. package/dist/cjs/src/mcp/interceptors/smartpage-export.js.map +1 -0
  21. package/dist/cjs/src/mcp/schema.js +104 -0
  22. package/dist/cjs/src/mcp/schema.js.map +1 -0
  23. package/dist/cjs/src/mcp/tool.js +208 -0
  24. package/dist/cjs/src/mcp/tool.js.map +1 -0
  25. package/dist/cjs/src/mcp/transport.js +417 -0
  26. package/dist/cjs/src/mcp/transport.js.map +1 -0
  27. package/dist/cjs/src/media-handler.js +129 -0
  28. package/dist/cjs/src/media-handler.js.map +1 -0
  29. package/dist/cjs/src/media-uploader.js +284 -0
  30. package/dist/cjs/src/media-uploader.js.map +1 -0
  31. package/dist/cjs/src/message-parser.js +134 -0
  32. package/dist/cjs/src/message-parser.js.map +1 -0
  33. package/dist/cjs/src/message-sender.js +89 -0
  34. package/dist/cjs/src/message-sender.js.map +1 -0
  35. package/dist/cjs/src/monitor.js +991 -0
  36. package/dist/cjs/src/monitor.js.map +1 -0
  37. package/dist/cjs/src/onboarding.js +182 -0
  38. package/dist/cjs/src/onboarding.js.map +1 -0
  39. package/dist/cjs/src/openclaw-compat.js +406 -0
  40. package/dist/cjs/src/openclaw-compat.js.map +1 -0
  41. package/dist/cjs/src/reqid-store.js +88 -0
  42. package/dist/cjs/src/reqid-store.js.map +1 -0
  43. package/dist/cjs/src/runtime.js +16 -0
  44. package/dist/cjs/src/runtime.js.map +1 -0
  45. package/dist/cjs/src/state-manager.js +158 -0
  46. package/dist/cjs/src/state-manager.js.map +1 -0
  47. package/dist/cjs/src/template-card-parser.js +641 -0
  48. package/dist/cjs/src/template-card-parser.js.map +1 -0
  49. package/dist/cjs/src/timeout.js +42 -0
  50. package/dist/cjs/src/timeout.js.map +1 -0
  51. package/dist/cjs/src/utils.js +60 -0
  52. package/dist/cjs/src/utils.js.map +1 -0
  53. package/dist/cjs/src/version.js +26 -0
  54. package/dist/cjs/src/version.js.map +1 -0
  55. package/dist/esm/index.js +41 -0
  56. package/dist/esm/index.js.map +1 -0
  57. package/dist/esm/src/channel.js +337 -0
  58. package/dist/esm/src/channel.js.map +1 -0
  59. package/dist/esm/src/const.js +110 -0
  60. package/dist/esm/src/const.js.map +1 -0
  61. package/dist/esm/src/dm-policy.js +87 -0
  62. package/dist/esm/src/dm-policy.js.map +1 -0
  63. package/dist/esm/src/group-policy.js +130 -0
  64. package/dist/esm/src/group-policy.js.map +1 -0
  65. package/dist/esm/src/mcp/interceptors/biz-error.js +68 -0
  66. package/dist/esm/src/mcp/interceptors/biz-error.js.map +1 -0
  67. package/dist/esm/src/mcp/interceptors/index.js +70 -0
  68. package/dist/esm/src/mcp/interceptors/index.js.map +1 -0
  69. package/dist/esm/src/mcp/interceptors/msg-media.js +137 -0
  70. package/dist/esm/src/mcp/interceptors/msg-media.js.map +1 -0
  71. package/dist/esm/src/mcp/interceptors/smartpage-create.js +136 -0
  72. package/dist/esm/src/mcp/interceptors/smartpage-create.js.map +1 -0
  73. package/dist/esm/src/mcp/interceptors/smartpage-export.js +91 -0
  74. package/dist/esm/src/mcp/interceptors/smartpage-export.js.map +1 -0
  75. package/dist/esm/src/mcp/schema.js +102 -0
  76. package/dist/esm/src/mcp/schema.js.map +1 -0
  77. package/dist/esm/src/mcp/tool.js +206 -0
  78. package/dist/esm/src/mcp/tool.js.map +1 -0
  79. package/dist/esm/src/mcp/transport.js +411 -0
  80. package/dist/esm/src/mcp/transport.js.map +1 -0
  81. package/dist/esm/src/media-handler.js +126 -0
  82. package/dist/esm/src/media-handler.js.map +1 -0
  83. package/dist/esm/src/media-uploader.js +279 -0
  84. package/dist/esm/src/media-uploader.js.map +1 -0
  85. package/dist/esm/src/message-parser.js +132 -0
  86. package/dist/esm/src/message-parser.js.map +1 -0
  87. package/dist/esm/src/message-sender.js +85 -0
  88. package/dist/esm/src/message-sender.js.map +1 -0
  89. package/dist/esm/src/monitor.js +963 -0
  90. package/dist/esm/src/monitor.js.map +1 -0
  91. package/dist/esm/src/onboarding.js +179 -0
  92. package/dist/esm/src/onboarding.js.map +1 -0
  93. package/dist/esm/src/openclaw-compat.js +377 -0
  94. package/dist/esm/src/openclaw-compat.js.map +1 -0
  95. package/dist/esm/src/reqid-store.js +86 -0
  96. package/dist/esm/src/reqid-store.js.map +1 -0
  97. package/dist/esm/src/runtime.js +13 -0
  98. package/dist/esm/src/runtime.js.map +1 -0
  99. package/dist/esm/src/state-manager.js +148 -0
  100. package/dist/esm/src/state-manager.js.map +1 -0
  101. package/dist/esm/src/template-card-parser.js +638 -0
  102. package/dist/esm/src/template-card-parser.js.map +1 -0
  103. package/dist/esm/src/timeout.js +39 -0
  104. package/dist/esm/src/timeout.js.map +1 -0
  105. package/dist/esm/src/utils.js +56 -0
  106. package/dist/esm/src/utils.js.map +1 -0
  107. package/dist/esm/src/version.js +23 -0
  108. package/dist/esm/src/version.js.map +1 -0
  109. package/dist/esm/types/index.d.ts +9 -0
  110. package/package.json +3 -3
  111. package/dist/index.cjs.js +0 -4953
  112. package/dist/index.cjs.js.map +0 -1
  113. package/dist/index.esm.js +0 -4926
  114. package/dist/index.esm.js.map +0 -1
  115. /package/dist/{src → esm/types/src}/channel.d.ts +0 -0
  116. /package/dist/{src → esm/types/src}/const.d.ts +0 -0
  117. /package/dist/{src → esm/types/src}/dm-policy.d.ts +0 -0
  118. /package/dist/{src → esm/types/src}/group-policy.d.ts +0 -0
  119. /package/dist/{src → esm/types/src}/interface.d.ts +0 -0
  120. /package/dist/{src → esm/types/src}/mcp/index.d.ts +0 -0
  121. /package/dist/{src → esm/types/src}/mcp/interceptors/biz-error.d.ts +0 -0
  122. /package/dist/{src → esm/types/src}/mcp/interceptors/index.d.ts +0 -0
  123. /package/dist/{src → esm/types/src}/mcp/interceptors/msg-media.d.ts +0 -0
  124. /package/dist/{src → esm/types/src}/mcp/interceptors/smartpage-create.d.ts +0 -0
  125. /package/dist/{src → esm/types/src}/mcp/interceptors/smartpage-export.d.ts +0 -0
  126. /package/dist/{src → esm/types/src}/mcp/interceptors/types.d.ts +0 -0
  127. /package/dist/{src → esm/types/src}/mcp/schema.d.ts +0 -0
  128. /package/dist/{src → esm/types/src}/mcp/tool.d.ts +0 -0
  129. /package/dist/{src → esm/types/src}/mcp/transport.d.ts +0 -0
  130. /package/dist/{src → esm/types/src}/media-handler.d.ts +0 -0
  131. /package/dist/{src → esm/types/src}/media-uploader.d.ts +0 -0
  132. /package/dist/{src → esm/types/src}/message-parser.d.ts +0 -0
  133. /package/dist/{src → esm/types/src}/message-sender.d.ts +0 -0
  134. /package/dist/{src → esm/types/src}/monitor.d.ts +0 -0
  135. /package/dist/{src → esm/types/src}/onboarding.d.ts +0 -0
  136. /package/dist/{src → esm/types/src}/openclaw-compat.d.ts +0 -0
  137. /package/dist/{src → esm/types/src}/reqid-store.d.ts +0 -0
  138. /package/dist/{src → esm/types/src}/runtime.d.ts +0 -0
  139. /package/dist/{src → esm/types/src}/state-manager.d.ts +0 -0
  140. /package/dist/{src → esm/types/src}/template-card-parser.d.ts +0 -0
  141. /package/dist/{src → esm/types/src}/timeout.d.ts +0 -0
  142. /package/dist/{src → esm/types/src}/utils.d.ts +0 -0
  143. /package/dist/{src → esm/types/src}/version.d.ts +0 -0
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ var runtime = require('./runtime.js');
4
+ var _const = require('./const.js');
5
+ var messageSender = require('./message-sender.js');
6
+ var groupPolicy = require('./group-policy.js');
7
+
8
+ /**
9
+ * 企业微信 DM(私聊)访问控制模块
10
+ *
11
+ * 负责私聊策略检查、配对流程
12
+ */
13
+ // ============================================================================
14
+ // 公开 API
15
+ // ============================================================================
16
+ /**
17
+ * 检查 DM Policy 访问控制
18
+ * @returns 检查结果,包含是否允许继续处理
19
+ */
20
+ async function checkDmPolicy(params) {
21
+ const { senderId, isGroup, account, wsClient, frame, runtime: runtime$1 } = params;
22
+ const core = runtime.getWeComRuntime();
23
+ // 群聊消息不检查 DM Policy
24
+ if (isGroup) {
25
+ return { allowed: true };
26
+ }
27
+ const dmPolicy = account.config.dmPolicy ?? "open";
28
+ const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
29
+ // 如果 dmPolicy 是 disabled,直接拒绝
30
+ if (dmPolicy === "disabled") {
31
+ runtime$1.log?.(`[WeCom] Blocked DM from ${senderId} (dmPolicy=disabled)`);
32
+ return { allowed: false };
33
+ }
34
+ // 如果是 open 模式,允许所有人
35
+ if (dmPolicy === "open") {
36
+ return { allowed: true };
37
+ }
38
+ // OpenClaw <= 2026.2.19 signature: readAllowFromStore(channel, env?, accountId?)
39
+ const oldStoreAllowFrom = await core.channel.pairing.readAllowFromStore('wecom', undefined, account.accountId).catch(() => []);
40
+ // Compatibility fallback for newer OpenClaw implementations.
41
+ const newStoreAllowFrom = await core.channel.pairing
42
+ .readAllowFromStore({ channel: _const.CHANNEL_ID, accountId: account.accountId })
43
+ .catch(() => []);
44
+ // 检查发送者是否在允许列表中
45
+ const storeAllowFrom = [...oldStoreAllowFrom, ...newStoreAllowFrom];
46
+ const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom];
47
+ const senderAllowedResult = groupPolicy.isSenderAllowed(senderId, effectiveAllowFrom);
48
+ if (senderAllowedResult) {
49
+ return { allowed: true };
50
+ }
51
+ // 处理未授权用户
52
+ if (dmPolicy === "pairing") {
53
+ const { code, created } = await core.channel.pairing.upsertPairingRequest({
54
+ channel: _const.CHANNEL_ID,
55
+ id: senderId,
56
+ accountId: account.accountId,
57
+ meta: { name: senderId },
58
+ });
59
+ if (created) {
60
+ runtime$1.log?.(`[WeCom] Pairing request created for sender=${senderId}`);
61
+ try {
62
+ await messageSender.sendWeComReply({
63
+ wsClient,
64
+ frame,
65
+ text: core.channel.pairing.buildPairingReply({
66
+ channel: _const.CHANNEL_ID,
67
+ idLine: `您的企业微信用户ID: ${senderId}`,
68
+ code,
69
+ }),
70
+ runtime: runtime$1,
71
+ finish: true,
72
+ });
73
+ }
74
+ catch (err) {
75
+ runtime$1.error?.(`[WeCom] Failed to send pairing reply to ${senderId}: ${String(err)}`);
76
+ }
77
+ }
78
+ else {
79
+ runtime$1.log?.(`[WeCom] Pairing request already exists for sender=${senderId}`);
80
+ }
81
+ return { allowed: false, pairingSent: created };
82
+ }
83
+ // allowlist 模式:直接拒绝未授权用户
84
+ runtime$1.log?.(`[WeCom] Blocked unauthorized sender ${senderId} (dmPolicy=${dmPolicy})`);
85
+ return { allowed: false };
86
+ }
87
+
88
+ exports.checkDmPolicy = checkDmPolicy;
89
+ //# sourceMappingURL=dm-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dm-policy.js","sources":["../../../../../src/dm-policy.ts"],"sourcesContent":["/**\n * 企业微信 DM(私聊)访问控制模块\n *\n * 负责私聊策略检查、配对流程\n */\n\nimport type { RuntimeEnv } from \"openclaw/plugin-sdk/runtime-env\";\nimport type { WSClient, WsFrame } from \"@wecom/aibot-node-sdk\";\nimport { getWeComRuntime } from \"./runtime.js\";\nimport { CHANNEL_ID } from \"./const.js\";\nimport type { ResolvedWeComAccount } from \"./utils.js\";\nimport { sendWeComReply } from \"./message-sender.js\";\nimport { isSenderAllowed } from \"./group-policy.js\";\n\n// ============================================================================\n// 检查结果类型\n// ============================================================================\n\n/**\n * DM Policy 检查结果\n */\nexport interface DmPolicyCheckResult {\n /** 是否允许继续处理消息 */\n allowed: boolean;\n /** 是否已发送配对消息(仅在 pairing 模式下) */\n pairingSent?: boolean;\n}\n\n// ============================================================================\n// 公开 API\n// ============================================================================\n\n/**\n * 检查 DM Policy 访问控制\n * @returns 检查结果,包含是否允许继续处理\n */\nexport async function checkDmPolicy(params: {\n senderId: string;\n isGroup: boolean;\n account: ResolvedWeComAccount;\n wsClient: WSClient;\n frame: WsFrame;\n runtime: RuntimeEnv;\n}): Promise<DmPolicyCheckResult> {\n const { senderId, isGroup, account, wsClient, frame, runtime } = params;\n const core = getWeComRuntime();\n\n // 群聊消息不检查 DM Policy\n if (isGroup) {\n return { allowed: true };\n }\n\n const dmPolicy = account.config.dmPolicy ?? \"open\";\n const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));\n\n // 如果 dmPolicy 是 disabled,直接拒绝\n if (dmPolicy === \"disabled\") {\n runtime.log?.(`[WeCom] Blocked DM from ${senderId} (dmPolicy=disabled)`);\n return { allowed: false };\n }\n\n // 如果是 open 模式,允许所有人\n if (dmPolicy === \"open\") {\n return { allowed: true };\n }\n\n // OpenClaw <= 2026.2.19 signature: readAllowFromStore(channel, env?, accountId?)\n const oldStoreAllowFrom = await core.channel.pairing.readAllowFromStore('wecom', undefined, account.accountId).catch(() => []);;\n // Compatibility fallback for newer OpenClaw implementations.\n const newStoreAllowFrom = await core.channel.pairing\n .readAllowFromStore({ channel: CHANNEL_ID, accountId: account.accountId })\n .catch(() => []);\n\n // 检查发送者是否在允许列表中\n const storeAllowFrom = [...oldStoreAllowFrom, ...newStoreAllowFrom];\n\n const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom];\n const senderAllowedResult = isSenderAllowed(senderId, effectiveAllowFrom);\n\n if (senderAllowedResult) {\n return { allowed: true };\n }\n\n // 处理未授权用户\n if (dmPolicy === \"pairing\") {\n const { code, created } = await core.channel.pairing.upsertPairingRequest({\n channel: CHANNEL_ID,\n id: senderId,\n accountId: account.accountId,\n meta: { name: senderId },\n });\n\n if (created) {\n runtime.log?.(`[WeCom] Pairing request created for sender=${senderId}`);\n try {\n await sendWeComReply({\n wsClient,\n frame,\n text: core.channel.pairing.buildPairingReply({\n channel: CHANNEL_ID,\n idLine: `您的企业微信用户ID: ${senderId}`,\n code,\n }),\n runtime,\n finish: true,\n });\n } catch (err) {\n runtime.error?.(`[WeCom] Failed to send pairing reply to ${senderId}: ${String(err)}`);\n }\n } else {\n runtime.log?.(`[WeCom] Pairing request already exists for sender=${senderId}`);\n }\n return { allowed: false, pairingSent: created };\n }\n\n // allowlist 模式:直接拒绝未授权用户\n runtime.log?.(`[WeCom] Blocked unauthorized sender ${senderId} (dmPolicy=${dmPolicy})`);\n return { allowed: false };\n}\n"],"names":["runtime","getWeComRuntime","CHANNEL_ID","isSenderAllowed","sendWeComReply"],"mappings":"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"}
@@ -0,0 +1,133 @@
1
+ 'use strict';
2
+
3
+ var _const = require('./const.js');
4
+
5
+ /**
6
+ * 企业微信群组访问控制模块
7
+ *
8
+ * 负责群组策略检查(groupPolicy、群组白名单、群内发送者白名单)
9
+ */
10
+ // ============================================================================
11
+ // 内部辅助函数
12
+ // ============================================================================
13
+ /**
14
+ * 解析企业微信群组配置
15
+ */
16
+ function resolveWeComGroupConfig(params) {
17
+ const groups = params.cfg?.groups ?? {};
18
+ const wildcard = groups["*"];
19
+ const groupId = params.groupId?.trim();
20
+ if (!groupId) {
21
+ return undefined;
22
+ }
23
+ const direct = groups[groupId];
24
+ if (direct) {
25
+ return direct;
26
+ }
27
+ const lowered = groupId.toLowerCase();
28
+ const matchKey = Object.keys(groups).find((key) => key.toLowerCase() === lowered);
29
+ if (matchKey) {
30
+ return groups[matchKey];
31
+ }
32
+ return wildcard;
33
+ }
34
+ /**
35
+ * 检查群组是否在允许列表中
36
+ */
37
+ function isWeComGroupAllowed(params) {
38
+ const { groupPolicy } = params;
39
+ if (groupPolicy === "disabled") {
40
+ return false;
41
+ }
42
+ if (groupPolicy === "open") {
43
+ return true;
44
+ }
45
+ // allowlist 模式:检查群组是否在允许列表中
46
+ const normalizedAllowFrom = params.allowFrom.map((entry) => String(entry).replace(new RegExp(`^${_const.CHANNEL_ID}:`, "i"), "").trim());
47
+ if (normalizedAllowFrom.includes("*")) {
48
+ return true;
49
+ }
50
+ const normalizedGroupId = params.groupId.trim();
51
+ return normalizedAllowFrom.some((entry) => entry === normalizedGroupId || entry.toLowerCase() === normalizedGroupId.toLowerCase());
52
+ }
53
+ /**
54
+ * 检查群组内发送者是否在允许列表中
55
+ */
56
+ function isGroupSenderAllowed(params) {
57
+ const { senderId, groupId, wecomConfig } = params;
58
+ const groupConfig = resolveWeComGroupConfig({
59
+ cfg: wecomConfig,
60
+ groupId,
61
+ });
62
+ const perGroupSenderAllowFrom = (groupConfig?.allowFrom ?? []).map((v) => String(v));
63
+ if (perGroupSenderAllowFrom.length === 0) {
64
+ return true;
65
+ }
66
+ if (perGroupSenderAllowFrom.includes("*")) {
67
+ return true;
68
+ }
69
+ return perGroupSenderAllowFrom.some((entry) => {
70
+ const normalized = entry.replace(new RegExp(`^${_const.CHANNEL_ID}:`, "i"), "").trim();
71
+ return normalized === senderId || normalized === `user:${senderId}`;
72
+ });
73
+ }
74
+ // ============================================================================
75
+ // 公开 API
76
+ // ============================================================================
77
+ /**
78
+ * 检查群组策略访问控制
79
+ * @returns 检查结果,包含是否允许继续处理
80
+ */
81
+ function checkGroupPolicy(params) {
82
+ const { chatId, senderId, account, config, runtime } = params;
83
+ const wecomConfig = (config.channels?.[_const.CHANNEL_ID] ?? {});
84
+ const defaultGroupPolicy = config.channels?.[_const.CHANNEL_ID]?.groupPolicy;
85
+ const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "open";
86
+ // const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
87
+ // providerConfigPresent: config.channels?.[CHANNEL_ID] !== undefined,
88
+ // groupPolicy: wecomConfig.groupPolicy,
89
+ // defaultGroupPolicy,
90
+ // });
91
+ // warnMissingProviderGroupPolicyFallbackOnce({
92
+ // providerMissingFallbackApplied,
93
+ // providerKey: CHANNEL_ID,
94
+ // accountId: account.accountId,
95
+ // log: (msg) => runtime.log?.(msg),
96
+ // });
97
+ const groupAllowFrom = wecomConfig.groupAllowFrom ?? [];
98
+ const groupAllowed = isWeComGroupAllowed({
99
+ groupPolicy,
100
+ allowFrom: groupAllowFrom,
101
+ groupId: chatId,
102
+ });
103
+ if (!groupAllowed) {
104
+ runtime.log?.(`[WeCom] Group ${chatId} not allowed (groupPolicy=${groupPolicy})`);
105
+ return { allowed: false };
106
+ }
107
+ const senderAllowed = isGroupSenderAllowed({
108
+ senderId,
109
+ groupId: chatId,
110
+ wecomConfig,
111
+ });
112
+ if (!senderAllowed) {
113
+ runtime.log?.(`[WeCom] Sender ${senderId} not in group ${chatId} sender allowlist`);
114
+ return { allowed: false };
115
+ }
116
+ return { allowed: true };
117
+ }
118
+ /**
119
+ * 检查发送者是否在允许列表中(通用)
120
+ */
121
+ function isSenderAllowed(senderId, allowFrom) {
122
+ if (allowFrom.includes("*")) {
123
+ return true;
124
+ }
125
+ return allowFrom.some((entry) => {
126
+ const normalized = entry.replace(new RegExp(`^${_const.CHANNEL_ID}:`, "i"), "").trim();
127
+ return normalized === senderId || normalized === `user:${senderId}`;
128
+ });
129
+ }
130
+
131
+ exports.checkGroupPolicy = checkGroupPolicy;
132
+ exports.isSenderAllowed = isSenderAllowed;
133
+ //# sourceMappingURL=group-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group-policy.js","sources":["../../../../../src/group-policy.ts"],"sourcesContent":["/**\n * 企业微信群组访问控制模块\n *\n * 负责群组策略检查(groupPolicy、群组白名单、群内发送者白名单)\n */\n\nimport type { OpenClawConfig } from \"openclaw/plugin-sdk/core\";\nimport type { RuntimeEnv } from \"openclaw/plugin-sdk/runtime-env\";\nimport { CHANNEL_ID } from \"./const.js\";\nimport type { ResolvedWeComAccount, WeComConfig, WeComGroupConfig } from \"./utils.js\";\n\n// ============================================================================\n// 检查结果类型\n// ============================================================================\n\n/**\n * 群组策略检查结果\n */\nexport interface GroupPolicyCheckResult {\n /** 是否允许继续处理消息 */\n allowed: boolean;\n}\n\n// ============================================================================\n// 内部辅助函数\n// ============================================================================\n\n/**\n * 解析企业微信群组配置\n */\nfunction resolveWeComGroupConfig(params: {\n cfg?: WeComConfig;\n groupId?: string | null;\n}): WeComGroupConfig | undefined {\n const groups = params.cfg?.groups ?? {};\n const wildcard = groups[\"*\"];\n const groupId = params.groupId?.trim();\n if (!groupId) {\n return undefined;\n }\n\n const direct = groups[groupId];\n if (direct) {\n return direct;\n }\n\n const lowered = groupId.toLowerCase();\n const matchKey = Object.keys(groups).find((key) => key.toLowerCase() === lowered);\n if (matchKey) {\n return groups[matchKey];\n }\n return wildcard;\n}\n\n/**\n * 检查群组是否在允许列表中\n */\nfunction isWeComGroupAllowed(params: {\n groupPolicy: \"open\" | \"allowlist\" | \"disabled\";\n allowFrom: Array<string | number>;\n groupId: string;\n}): boolean {\n const { groupPolicy } = params;\n if (groupPolicy === \"disabled\") {\n return false;\n }\n if (groupPolicy === \"open\") {\n return true;\n }\n // allowlist 模式:检查群组是否在允许列表中\n const normalizedAllowFrom = params.allowFrom.map((entry) =>\n String(entry).replace(new RegExp(`^${CHANNEL_ID}:`, \"i\"), \"\").trim()\n );\n if (normalizedAllowFrom.includes(\"*\")) {\n return true;\n }\n const normalizedGroupId = params.groupId.trim();\n return normalizedAllowFrom.some(\n (entry) => entry === normalizedGroupId || entry.toLowerCase() === normalizedGroupId.toLowerCase()\n );\n}\n\n/**\n * 检查群组内发送者是否在允许列表中\n */\nfunction isGroupSenderAllowed(params: {\n senderId: string;\n groupId: string;\n wecomConfig: WeComConfig;\n}): boolean {\n const { senderId, groupId, wecomConfig } = params;\n\n const groupConfig = resolveWeComGroupConfig({\n cfg: wecomConfig,\n groupId,\n });\n\n const perGroupSenderAllowFrom = (groupConfig?.allowFrom ?? []).map((v) => String(v));\n\n if (perGroupSenderAllowFrom.length === 0) {\n return true;\n }\n\n if (perGroupSenderAllowFrom.includes(\"*\")) {\n return true;\n }\n\n return perGroupSenderAllowFrom.some((entry) => {\n const normalized = entry.replace(new RegExp(`^${CHANNEL_ID}:`, \"i\"), \"\").trim();\n return normalized === senderId || normalized === `user:${senderId}`;\n });\n}\n\n// ============================================================================\n// 公开 API\n// ============================================================================\n\n/**\n * 检查群组策略访问控制\n * @returns 检查结果,包含是否允许继续处理\n */\nexport function checkGroupPolicy(params: {\n chatId: string;\n senderId: string;\n account: ResolvedWeComAccount;\n config: OpenClawConfig;\n runtime: RuntimeEnv;\n}): GroupPolicyCheckResult {\n const { chatId, senderId, account, config, runtime } = params;\n const wecomConfig = (config.channels?.[CHANNEL_ID] ?? {}) as WeComConfig;\n\n const defaultGroupPolicy = config.channels?.[CHANNEL_ID]?.groupPolicy;\n const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? \"open\"\n // const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({\n // providerConfigPresent: config.channels?.[CHANNEL_ID] !== undefined,\n // groupPolicy: wecomConfig.groupPolicy,\n // defaultGroupPolicy,\n // });\n\n // warnMissingProviderGroupPolicyFallbackOnce({\n // providerMissingFallbackApplied,\n // providerKey: CHANNEL_ID,\n // accountId: account.accountId,\n // log: (msg) => runtime.log?.(msg),\n // });\n\n const groupAllowFrom = wecomConfig.groupAllowFrom ?? [];\n const groupAllowed = isWeComGroupAllowed({\n groupPolicy,\n allowFrom: groupAllowFrom,\n groupId: chatId,\n });\n\n if (!groupAllowed) {\n runtime.log?.(\n `[WeCom] Group ${chatId} not allowed (groupPolicy=${groupPolicy})`,\n );\n return { allowed: false };\n }\n\n const senderAllowed = isGroupSenderAllowed({\n senderId,\n groupId: chatId,\n wecomConfig,\n });\n\n if (!senderAllowed) {\n runtime.log?.(\n `[WeCom] Sender ${senderId} not in group ${chatId} sender allowlist`,\n );\n return { allowed: false };\n }\n\n return { allowed: true };\n}\n\n/**\n * 检查发送者是否在允许列表中(通用)\n */\nexport function isSenderAllowed(senderId: string, allowFrom: string[]): boolean {\n if (allowFrom.includes(\"*\")) {\n return true;\n }\n return allowFrom.some((entry) => {\n const normalized = entry.replace(new RegExp(`^${CHANNEL_ID}:`, \"i\"), \"\").trim();\n return normalized === senderId || normalized === `user:${senderId}`;\n });\n}\n"],"names":["CHANNEL_ID"],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ var transport = require('../transport.js');
4
+
5
+ /**
6
+ * 业务错误码检查拦截器
7
+ *
8
+ * 检查 tools/call 返回结果中是否包含需要清理缓存的业务错误码。
9
+ * MCP Server 可能在正常的 JSON-RPC 响应中返回业务层错误,
10
+ * 这些错误被包裹在 result.content[].text 中,需要解析后判断。
11
+ *
12
+ * 此拦截器对所有 call 调用生效。
13
+ */
14
+ // ============================================================================
15
+ // 常量
16
+ // ============================================================================
17
+ /**
18
+ * 需要触发缓存清理的业务错误码集合
19
+ *
20
+ * 这些错误码出现在 MCP 工具调用返回的 content 文本中(业务层面),
21
+ * 与 JSON-RPC 层面的错误码不同,需要在此处额外检测。
22
+ *
23
+ * - 850002: 机器人未被授权使用对应能力,需清理缓存以便下次重新拉取配置
24
+ */
25
+ const BIZ_CACHE_CLEAR_ERROR_CODES = new Set([850002]);
26
+ // ============================================================================
27
+ // 拦截器实现
28
+ // ============================================================================
29
+ const bizErrorInterceptor = {
30
+ name: "biz-error",
31
+ /** 对所有 call 调用生效 */
32
+ match: () => true,
33
+ /** 检查返回结果中的业务错误码,必要时清理缓存 */
34
+ afterCall(ctx, result) {
35
+ checkBizErrorAndClearCache(result, ctx.category);
36
+ // 不修改 result,透传给下一个拦截器
37
+ return result;
38
+ },
39
+ };
40
+ // ============================================================================
41
+ // 内部实现
42
+ // ============================================================================
43
+ /**
44
+ * 检查 tools/call 的返回结果中是否包含需要清理缓存的业务错误码
45
+ */
46
+ function checkBizErrorAndClearCache(result, category) {
47
+ if (!result || typeof result !== "object")
48
+ return;
49
+ const { content } = result;
50
+ if (!Array.isArray(content))
51
+ return;
52
+ for (const item of content) {
53
+ if (item.type !== "text" || !item.text)
54
+ continue;
55
+ try {
56
+ const parsed = JSON.parse(item.text);
57
+ if (typeof parsed.errcode === "number" && BIZ_CACHE_CLEAR_ERROR_CODES.has(parsed.errcode)) {
58
+ console.log(`[mcp] 检测到业务错误码 ${parsed.errcode} (category="${category}"),清理缓存`);
59
+ transport.clearCategoryCache(category);
60
+ return;
61
+ }
62
+ }
63
+ catch {
64
+ // text 不是 JSON 格式,跳过
65
+ }
66
+ }
67
+ }
68
+
69
+ exports.bizErrorInterceptor = bizErrorInterceptor;
70
+ //# sourceMappingURL=biz-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"biz-error.js","sources":["../../../../../../../src/mcp/interceptors/biz-error.ts"],"sourcesContent":["/**\n * 业务错误码检查拦截器\n *\n * 检查 tools/call 返回结果中是否包含需要清理缓存的业务错误码。\n * MCP Server 可能在正常的 JSON-RPC 响应中返回业务层错误,\n * 这些错误被包裹在 result.content[].text 中,需要解析后判断。\n *\n * 此拦截器对所有 call 调用生效。\n */\n\nimport { clearCategoryCache } from \"../transport.js\";\nimport type { CallInterceptor, CallContext } from \"./types.js\";\n\n// ============================================================================\n// 常量\n// ============================================================================\n\n/**\n * 需要触发缓存清理的业务错误码集合\n *\n * 这些错误码出现在 MCP 工具调用返回的 content 文本中(业务层面),\n * 与 JSON-RPC 层面的错误码不同,需要在此处额外检测。\n *\n * - 850002: 机器人未被授权使用对应能力,需清理缓存以便下次重新拉取配置\n */\nconst BIZ_CACHE_CLEAR_ERROR_CODES = new Set([850002]);\n\n// ============================================================================\n// 拦截器实现\n// ============================================================================\n\nexport const bizErrorInterceptor: CallInterceptor = {\n name: \"biz-error\",\n\n /** 对所有 call 调用生效 */\n match: () => true,\n\n /** 检查返回结果中的业务错误码,必要时清理缓存 */\n afterCall(ctx: CallContext, result: unknown): unknown {\n checkBizErrorAndClearCache(result, ctx.category);\n // 不修改 result,透传给下一个拦截器\n return result;\n },\n};\n\n// ============================================================================\n// 内部实现\n// ============================================================================\n\n/**\n * 检查 tools/call 的返回结果中是否包含需要清理缓存的业务错误码\n */\nfunction checkBizErrorAndClearCache(result: unknown, category: string): void {\n if (!result || typeof result !== \"object\") return;\n\n const { content } = result as { content?: Array<{ type: string; text?: string }> };\n if (!Array.isArray(content)) return;\n\n for (const item of content) {\n if (item.type !== \"text\" || !item.text) continue;\n try {\n const parsed = JSON.parse(item.text) as Record<string, unknown>;\n if (typeof parsed.errcode === \"number\" && BIZ_CACHE_CLEAR_ERROR_CODES.has(parsed.errcode)) {\n console.log(`[mcp] 检测到业务错误码 ${parsed.errcode} (category=\"${category}\"),清理缓存`);\n clearCategoryCache(category);\n return;\n }\n } catch {\n // text 不是 JSON 格式,跳过\n }\n }\n}\n"],"names":["clearCategoryCache"],"mappings":";;;;AAAA;;;;;;;;AAQG;AAKH;AACA;AACA;AAEA;;;;;;;AAOG;AACH,MAAM,2BAA2B,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD;AACA;AACA;AAEO,MAAM,mBAAmB,GAAoB;AAClD,IAAA,IAAI,EAAE,WAAW;;AAGjB,IAAA,KAAK,EAAE,MAAM,IAAI;;IAGjB,SAAS,CAAC,GAAgB,EAAE,MAAe,EAAA;AACzC,QAAA,0BAA0B,CAAC,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC;;AAEhD,QAAA,OAAO,MAAM;IACf,CAAC;;AAGH;AACA;AACA;AAEA;;AAEG;AACH,SAAS,0BAA0B,CAAC,MAAe,EAAE,QAAgB,EAAA;AACnE,IAAA,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE;AAE3C,IAAA,MAAM,EAAE,OAAO,EAAE,GAAG,MAA8D;AAClF,IAAA,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE;AAE7B,IAAA,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE;QAC1B,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI;YAAE;AACxC,QAAA,IAAI;YACF,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAA4B;AAC/D,YAAA,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,IAAI,2BAA2B,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE;gBACzF,OAAO,CAAC,GAAG,CAAC,CAAA,eAAA,EAAkB,MAAM,CAAC,OAAO,CAAA,YAAA,EAAe,QAAQ,CAAA,OAAA,CAAS,CAAC;gBAC7EA,4BAAkB,CAAC,QAAQ,CAAC;gBAC5B;YACF;QACF;AAAE,QAAA,MAAM;;QAER;IACF;AACF;;;;"}
@@ -0,0 +1,73 @@
1
+ 'use strict';
2
+
3
+ var bizError = require('./biz-error.js');
4
+ var msgMedia = require('./msg-media.js');
5
+ var smartpageCreate = require('./smartpage-create.js');
6
+ var smartpageExport = require('./smartpage-export.js');
7
+
8
+ /**
9
+ * MCP call 拦截器注册表与调度入口
10
+ *
11
+ * 所有 call 拦截器在此注册,按注册顺序执行。
12
+ * 新增拦截器只需:
13
+ * 1. 在 interceptors/ 目录下新建文件,实现 CallInterceptor 接口
14
+ * 2. 在下方 interceptors 数组中注册
15
+ *
16
+ * tool.ts 的 handleCall 无需任何改动。
17
+ */
18
+ // ============================================================================
19
+ // 拦截器注册表(按注册顺序执行)
20
+ // ============================================================================
21
+ const interceptors = [
22
+ bizError.bizErrorInterceptor, // 业务错误码检查(所有 call 生效)
23
+ msgMedia.mediaInterceptor, // get_msg_media base64 拦截
24
+ smartpageCreate.smartpageCreateInterceptor, // smartpage_create 本地文件读取
25
+ smartpageExport.smartpageExportInterceptor, // smartpage_get_export_result content → 本地文件
26
+ ];
27
+ /**
28
+ * 收集匹配的 beforeCall 配置,合并后返回
29
+ *
30
+ * 合并策略:
31
+ * - timeoutMs: 取所有拦截器返回值中的最大值
32
+ * - args: 后注册的拦截器覆盖前者(一般同一调用只有一个拦截器会返回 args)
33
+ */
34
+ async function resolveBeforeCall(ctx) {
35
+ let mergedTimeoutMs;
36
+ let mergedArgs;
37
+ for (const interceptor of interceptors) {
38
+ if (!interceptor.match(ctx) || !interceptor.beforeCall)
39
+ continue;
40
+ const opts = await interceptor.beforeCall(ctx);
41
+ if (opts?.timeoutMs !== undefined) {
42
+ mergedTimeoutMs = mergedTimeoutMs === undefined
43
+ ? opts.timeoutMs
44
+ : Math.max(mergedTimeoutMs, opts.timeoutMs);
45
+ }
46
+ if (opts?.args !== undefined) {
47
+ mergedArgs = opts.args;
48
+ }
49
+ }
50
+ return {
51
+ options: mergedTimeoutMs !== undefined ? { timeoutMs: mergedTimeoutMs } : undefined,
52
+ args: mergedArgs,
53
+ };
54
+ }
55
+ /**
56
+ * 依次执行匹配的 afterCall 拦截器,管道式传递 result
57
+ *
58
+ * 前一个拦截器的返回值作为下一个拦截器的输入。
59
+ * 拦截器若不需要修改 result,应原样返回。
60
+ */
61
+ async function runAfterCall(ctx, result) {
62
+ let current = result;
63
+ for (const interceptor of interceptors) {
64
+ if (!interceptor.match(ctx) || !interceptor.afterCall)
65
+ continue;
66
+ current = await interceptor.afterCall(ctx, current);
67
+ }
68
+ return current;
69
+ }
70
+
71
+ exports.resolveBeforeCall = resolveBeforeCall;
72
+ exports.runAfterCall = runAfterCall;
73
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../../../../../../src/mcp/interceptors/index.ts"],"sourcesContent":["/**\n * MCP call 拦截器注册表与调度入口\n *\n * 所有 call 拦截器在此注册,按注册顺序执行。\n * 新增拦截器只需:\n * 1. 在 interceptors/ 目录下新建文件,实现 CallInterceptor 接口\n * 2. 在下方 interceptors 数组中注册\n *\n * tool.ts 的 handleCall 无需任何改动。\n */\n\nimport { bizErrorInterceptor } from \"./biz-error.js\";\nimport { mediaInterceptor } from \"./msg-media.js\";\nimport { smartpageCreateInterceptor } from \"./smartpage-create.js\";\nimport { smartpageExportInterceptor } from \"./smartpage-export.js\";\nimport type { CallContext, CallInterceptor, BeforeCallOptions } from \"./types.js\";\nimport type { SendJsonRpcOptions } from \"../transport.js\";\n\nexport type { CallContext, CallInterceptor, BeforeCallOptions } from \"./types.js\";\n\n// ============================================================================\n// 拦截器注册表(按注册顺序执行)\n// ============================================================================\n\nconst interceptors: CallInterceptor[] = [\n bizErrorInterceptor, // 业务错误码检查(所有 call 生效)\n mediaInterceptor, // get_msg_media base64 拦截\n smartpageCreateInterceptor, // smartpage_create 本地文件读取\n smartpageExportInterceptor, // smartpage_get_export_result content → 本地文件\n];\n\n// ============================================================================\n// 调度 API\n// ============================================================================\n\n/** resolveBeforeCall 的返回值 */\nexport interface ResolvedBeforeCall {\n /** 合并后的 sendJsonRpc 选项(如超时时间) */\n options?: SendJsonRpcOptions;\n /** 替换后的 args(如从本地文件读取的请求体) */\n args?: Record<string, unknown>;\n}\n\n/**\n * 收集匹配的 beforeCall 配置,合并后返回\n *\n * 合并策略:\n * - timeoutMs: 取所有拦截器返回值中的最大值\n * - args: 后注册的拦截器覆盖前者(一般同一调用只有一个拦截器会返回 args)\n */\nexport async function resolveBeforeCall(ctx: CallContext): Promise<ResolvedBeforeCall> {\n let mergedTimeoutMs: number | undefined;\n let mergedArgs: Record<string, unknown> | undefined;\n\n for (const interceptor of interceptors) {\n if (!interceptor.match(ctx) || !interceptor.beforeCall) continue;\n\n const opts = await interceptor.beforeCall(ctx);\n if (opts?.timeoutMs !== undefined) {\n mergedTimeoutMs = mergedTimeoutMs === undefined\n ? opts.timeoutMs\n : Math.max(mergedTimeoutMs, opts.timeoutMs);\n }\n if (opts?.args !== undefined) {\n mergedArgs = opts.args;\n }\n }\n\n return {\n options: mergedTimeoutMs !== undefined ? { timeoutMs: mergedTimeoutMs } : undefined,\n args: mergedArgs,\n };\n}\n\n/**\n * 依次执行匹配的 afterCall 拦截器,管道式传递 result\n *\n * 前一个拦截器的返回值作为下一个拦截器的输入。\n * 拦截器若不需要修改 result,应原样返回。\n */\nexport async function runAfterCall(ctx: CallContext, result: unknown): Promise<unknown> {\n let current = result;\n\n for (const interceptor of interceptors) {\n if (!interceptor.match(ctx) || !interceptor.afterCall) continue;\n current = await interceptor.afterCall(ctx, current);\n }\n\n return current;\n}\n"],"names":["bizErrorInterceptor","mediaInterceptor","smartpageCreateInterceptor","smartpageExportInterceptor"],"mappings":";;;;;;;AAAA;;;;;;;;;AASG;AAWH;AACA;AACA;AAEA,MAAM,YAAY,GAAsB;AACtC,IAAAA,4BAAmB;AACnB,IAAAC,yBAAgB;AAChB,IAAAC,0CAA0B;AAC1B,IAAAC,0CAA0B;CAC3B;AAcD;;;;;;AAMG;AACI,eAAe,iBAAiB,CAAC,GAAgB,EAAA;AACtD,IAAA,IAAI,eAAmC;AACvC,IAAA,IAAI,UAA+C;AAEnD,IAAA,KAAK,MAAM,WAAW,IAAI,YAAY,EAAE;QACtC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,UAAU;YAAE;QAExD,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,UAAU,CAAC,GAAG,CAAC;AAC9C,QAAA,IAAI,IAAI,EAAE,SAAS,KAAK,SAAS,EAAE;YACjC,eAAe,GAAG,eAAe,KAAK;kBAClC,IAAI,CAAC;kBACL,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,SAAS,CAAC;QAC/C;AACA,QAAA,IAAI,IAAI,EAAE,IAAI,KAAK,SAAS,EAAE;AAC5B,YAAA,UAAU,GAAG,IAAI,CAAC,IAAI;QACxB;IACF;IAEA,OAAO;AACL,QAAA,OAAO,EAAE,eAAe,KAAK,SAAS,GAAG,EAAE,SAAS,EAAE,eAAe,EAAE,GAAG,SAAS;AACnF,QAAA,IAAI,EAAE,UAAU;KACjB;AACH;AAEA;;;;;AAKG;AACI,eAAe,YAAY,CAAC,GAAgB,EAAE,MAAe,EAAA;IAClE,IAAI,OAAO,GAAG,MAAM;AAEpB,IAAA,KAAK,MAAM,WAAW,IAAI,YAAY,EAAE;QACtC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS;YAAE;QACvD,OAAO,GAAG,MAAM,WAAW,CAAC,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC;IACrD;AAEA,IAAA,OAAO,OAAO;AAChB;;;;;"}
@@ -0,0 +1,159 @@
1
+ 'use strict';
2
+
3
+ var fs = require('node:fs/promises');
4
+ var path = require('node:path');
5
+ var transport = require('../transport.js');
6
+ var runtime = require('../../runtime.js');
7
+ var openclawCompat = require('../../openclaw-compat.js');
8
+
9
+ function _interopNamespaceDefault(e) {
10
+ var n = Object.create(null);
11
+ if (e) {
12
+ Object.keys(e).forEach(function (k) {
13
+ if (k !== 'default') {
14
+ var d = Object.getOwnPropertyDescriptor(e, k);
15
+ Object.defineProperty(n, k, d.get ? d : {
16
+ enumerable: true,
17
+ get: function () { return e[k]; }
18
+ });
19
+ }
20
+ });
21
+ }
22
+ n.default = e;
23
+ return Object.freeze(n);
24
+ }
25
+
26
+ var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
27
+ var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
28
+
29
+ /**
30
+ * get_msg_media 响应拦截器
31
+ *
32
+ * 核心逻辑:
33
+ * 1. beforeCall: 设置延长的超时时间(120s),因为 base64 数据可达 ~27MB
34
+ * 2. afterCall: 从 MCP result 的 content[].text 中提取 base64_data,
35
+ * 解码为 Buffer 并通过 saveMediaBuffer 保存到本地媒体目录,
36
+ * 替换响应中的 base64_data 为 local_path,避免大模型被 base64 数据消耗 token
37
+ */
38
+ // ============================================================================
39
+ // 拦截器实现
40
+ // ============================================================================
41
+ const mediaInterceptor = {
42
+ name: "media",
43
+ /** 仅对 get_msg_media 方法生效 */
44
+ match: (ctx) => ctx.method === "get_msg_media",
45
+ /** 设置延长的超时时间 */
46
+ beforeCall() {
47
+ return { timeoutMs: transport.MEDIA_DOWNLOAD_TIMEOUT_MS };
48
+ },
49
+ /** 拦截响应:base64 → 本地文件 */
50
+ async afterCall(ctx, result) {
51
+ return interceptMediaResponse(result);
52
+ },
53
+ };
54
+ // ============================================================================
55
+ // 内部实现
56
+ // ============================================================================
57
+ /**
58
+ * 拦截 get_msg_media 的 MCP 响应
59
+ *
60
+ * 1. 从 MCP result 的 content[].text 中提取业务 JSON
61
+ * 2. 提取 media_item.base64_data,解码为 Buffer
62
+ * 3. 通过 openclaw SDK 的 saveMediaBuffer 保存到本地媒体目录
63
+ * 4. 替换响应:移除 base64_data,加入 local_path
64
+ *
65
+ * 这样大模型只看到轻量的文件路径信息,不会被 base64 数据消耗 token。
66
+ */
67
+ async function interceptMediaResponse(result) {
68
+ const t0 = performance.now();
69
+ // 1. 提取 MCP result 中的 content 数组
70
+ const content = result?.content;
71
+ if (!Array.isArray(content))
72
+ return result;
73
+ const textItem = content.find((c) => c.type === "text" && typeof c.text === "string");
74
+ if (!textItem)
75
+ return result;
76
+ // 2. 解析业务 JSON
77
+ let bizData;
78
+ try {
79
+ bizData = JSON.parse(textItem.text);
80
+ }
81
+ catch {
82
+ // 非 JSON 格式,原样返回
83
+ return result;
84
+ }
85
+ // 3. 校验业务响应:errcode !== 0 或无 media_item 时原样返回
86
+ if (bizData.errcode !== 0)
87
+ return result;
88
+ const mediaItem = bizData.media_item;
89
+ if (!mediaItem || typeof mediaItem.base64_data !== "string")
90
+ return result;
91
+ const base64Data = mediaItem.base64_data;
92
+ const mediaName = mediaItem.name;
93
+ const mediaType = mediaItem.type;
94
+ const mediaId = mediaItem.media_id;
95
+ const tParsed = performance.now();
96
+ // 4. 解码 base64 → Buffer
97
+ const buffer = Buffer.from(base64Data, "base64");
98
+ const tDecoded = performance.now();
99
+ // 5. 检测 contentType,并通过 saveMediaBuffer 保存到本地媒体目录
100
+ const contentType = await openclawCompat.detectMime({ buffer, filePath: mediaName }) ?? "application/octet-stream";
101
+ const tMimeDetected = performance.now();
102
+ // 企业微信聊天记录附件可达 20MB(文件消息上限),
103
+ // 而 saveMediaBuffer 默认 maxBytes 为 5MB(针对 outbound 场景),
104
+ // 此处显式放宽到 20MB 以支持大文件下载。
105
+ const INBOUND_MAX_BYTES = 20 * 1024 * 1024; // 20MB
106
+ const core = runtime.getWeComRuntime();
107
+ const saved = await core.channel.media.saveMediaBuffer(buffer, contentType, "inbound", INBOUND_MAX_BYTES, // maxBytes: 放宽到 20MB,匹配企业微信文件消息上限
108
+ mediaName);
109
+ // 5.1 补偿:核心库 EXT_BY_MIME 可能缺少某些格式映射(如 audio/amr),
110
+ // 导致保存的文件没有后缀。此处检测并修复。
111
+ const MIME_EXT_PATCH = {
112
+ "audio/amr": ".amr",
113
+ };
114
+ const patchExt = MIME_EXT_PATCH[contentType];
115
+ if (patchExt && !path__namespace.extname(saved.path)) {
116
+ const newPath = saved.path + patchExt;
117
+ try {
118
+ await fs__namespace.rename(saved.path, newPath);
119
+ saved.path = newPath;
120
+ }
121
+ catch {
122
+ // rename 失败不影响主流程,文件仍可用
123
+ }
124
+ }
125
+ const tSaved = performance.now();
126
+ // 6. 构造精简响应,移除 base64_data,加入本地路径
127
+ const newBizData = {
128
+ errcode: 0,
129
+ errmsg: "ok",
130
+ media_item: {
131
+ media_id: mediaId,
132
+ name: mediaName ?? saved.path.split("/").pop(),
133
+ type: mediaType,
134
+ local_path: saved.path,
135
+ size: buffer.length,
136
+ content_type: saved.contentType,
137
+ },
138
+ };
139
+ const tEnd = performance.now();
140
+ // 耗时日志:各环节耗时(ms)
141
+ console.log(`[mcp] get_msg_media 拦截成功: media_id=${mediaId ?? "unknown"}, ` +
142
+ `type=${mediaType ?? "unknown"}, size=${buffer.length}, saved=${saved.path}\n` +
143
+ ` ⏱ 耗时明细 (总 ${(tEnd - t0).toFixed(1)}ms):\n` +
144
+ ` 解析响应 JSON: ${(tParsed - t0).toFixed(1)}ms\n` +
145
+ ` base64 解码: ${(tDecoded - tParsed).toFixed(1)}ms (${(base64Data.length / 1024).toFixed(0)}KB base64 → ${(buffer.length / 1024).toFixed(0)}KB buffer)\n` +
146
+ ` MIME 检测: ${(tMimeDetected - tDecoded).toFixed(1)}ms (${contentType})\n` +
147
+ ` saveMediaBuffer: ${(tSaved - tMimeDetected).toFixed(1)}ms\n` +
148
+ ` 构造响应: ${(tEnd - tSaved).toFixed(1)}ms`);
149
+ // 7. 返回修改后的 MCP result 结构
150
+ return {
151
+ content: [{
152
+ type: "text",
153
+ text: JSON.stringify(newBizData),
154
+ }],
155
+ };
156
+ }
157
+
158
+ exports.mediaInterceptor = mediaInterceptor;
159
+ //# sourceMappingURL=msg-media.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"msg-media.js","sources":["../../../../../../../src/mcp/interceptors/msg-media.ts"],"sourcesContent":["/**\n * get_msg_media 响应拦截器\n *\n * 核心逻辑:\n * 1. beforeCall: 设置延长的超时时间(120s),因为 base64 数据可达 ~27MB\n * 2. afterCall: 从 MCP result 的 content[].text 中提取 base64_data,\n * 解码为 Buffer 并通过 saveMediaBuffer 保存到本地媒体目录,\n * 替换响应中的 base64_data 为 local_path,避免大模型被 base64 数据消耗 token\n */\n\nimport * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { MEDIA_DOWNLOAD_TIMEOUT_MS } from \"../transport.js\";\nimport { getWeComRuntime } from \"../../runtime.js\";\nimport { detectMime } from \"../../openclaw-compat.js\";\nimport type { CallInterceptor, CallContext } from \"./types.js\";\n\n// ============================================================================\n// 拦截器实现\n// ============================================================================\n\nexport const mediaInterceptor: CallInterceptor = {\n name: \"media\",\n\n /** 仅对 get_msg_media 方法生效 */\n match: (ctx: CallContext) => ctx.method === \"get_msg_media\",\n\n /** 设置延长的超时时间 */\n beforeCall() {\n return { timeoutMs: MEDIA_DOWNLOAD_TIMEOUT_MS };\n },\n\n /** 拦截响应:base64 → 本地文件 */\n async afterCall(ctx: CallContext, result: unknown): Promise<unknown> {\n return interceptMediaResponse(result);\n },\n};\n\n// ============================================================================\n// 内部实现\n// ============================================================================\n\n/**\n * 拦截 get_msg_media 的 MCP 响应\n *\n * 1. 从 MCP result 的 content[].text 中提取业务 JSON\n * 2. 提取 media_item.base64_data,解码为 Buffer\n * 3. 通过 openclaw SDK 的 saveMediaBuffer 保存到本地媒体目录\n * 4. 替换响应:移除 base64_data,加入 local_path\n *\n * 这样大模型只看到轻量的文件路径信息,不会被 base64 数据消耗 token。\n */\nasync function interceptMediaResponse(result: unknown): Promise<unknown> {\n const t0 = performance.now();\n\n // 1. 提取 MCP result 中的 content 数组\n const content = (result as Record<string, unknown>)?.content;\n if (!Array.isArray(content)) return result;\n\n const textItem = content.find(\n (c: Record<string, unknown>) => c.type === \"text\" && typeof c.text === \"string\",\n ) as { type: string; text: string } | undefined;\n if (!textItem) return result;\n\n // 2. 解析业务 JSON\n let bizData: Record<string, unknown>;\n try {\n bizData = JSON.parse(textItem.text) as Record<string, unknown>;\n } catch {\n // 非 JSON 格式,原样返回\n return result;\n }\n\n // 3. 校验业务响应:errcode !== 0 或无 media_item 时原样返回\n if (bizData.errcode !== 0) return result;\n\n const mediaItem = bizData.media_item as Record<string, unknown> | undefined;\n if (!mediaItem || typeof mediaItem.base64_data !== \"string\") return result;\n\n const base64Data = mediaItem.base64_data as string;\n const mediaName = mediaItem.name as string | undefined;\n const mediaType = mediaItem.type as string | undefined;\n const mediaId = mediaItem.media_id as string | undefined;\n\n const tParsed = performance.now();\n\n // 4. 解码 base64 → Buffer\n const buffer = Buffer.from(base64Data, \"base64\");\n const tDecoded = performance.now();\n\n // 5. 检测 contentType,并通过 saveMediaBuffer 保存到本地媒体目录\n const contentType = await detectMime({ buffer, filePath: mediaName }) ?? \"application/octet-stream\";\n const tMimeDetected = performance.now();\n\n // 企业微信聊天记录附件可达 20MB(文件消息上限),\n // 而 saveMediaBuffer 默认 maxBytes 为 5MB(针对 outbound 场景),\n // 此处显式放宽到 20MB 以支持大文件下载。\n const INBOUND_MAX_BYTES = 20 * 1024 * 1024; // 20MB\n\n const core = getWeComRuntime();\n const saved = await core.channel.media.saveMediaBuffer(\n buffer,\n contentType,\n \"inbound\",\n INBOUND_MAX_BYTES, // maxBytes: 放宽到 20MB,匹配企业微信文件消息上限\n mediaName, // originalFilename: 保留原始文件名\n );\n\n // 5.1 补偿:核心库 EXT_BY_MIME 可能缺少某些格式映射(如 audio/amr),\n // 导致保存的文件没有后缀。此处检测并修复。\n const MIME_EXT_PATCH: Record<string, string> = {\n \"audio/amr\": \".amr\",\n };\n const patchExt = MIME_EXT_PATCH[contentType];\n if (patchExt && !path.extname(saved.path)) {\n const newPath = saved.path + patchExt;\n try {\n await fs.rename(saved.path, newPath);\n saved.path = newPath;\n } catch {\n // rename 失败不影响主流程,文件仍可用\n }\n }\n\n const tSaved = performance.now();\n\n // 6. 构造精简响应,移除 base64_data,加入本地路径\n const newBizData = {\n errcode: 0,\n errmsg: \"ok\",\n media_item: {\n media_id: mediaId,\n name: mediaName ?? saved.path.split(\"/\").pop(),\n type: mediaType,\n local_path: saved.path,\n size: buffer.length,\n content_type: saved.contentType,\n },\n };\n\n const tEnd = performance.now();\n\n // 耗时日志:各环节耗时(ms)\n console.log(\n `[mcp] get_msg_media 拦截成功: media_id=${mediaId ?? \"unknown\"}, ` +\n `type=${mediaType ?? \"unknown\"}, size=${buffer.length}, saved=${saved.path}\\n` +\n ` ⏱ 耗时明细 (总 ${(tEnd - t0).toFixed(1)}ms):\\n` +\n ` 解析响应 JSON: ${(tParsed - t0).toFixed(1)}ms\\n` +\n ` base64 解码: ${(tDecoded - tParsed).toFixed(1)}ms (${(base64Data.length / 1024).toFixed(0)}KB base64 → ${(buffer.length / 1024).toFixed(0)}KB buffer)\\n` +\n ` MIME 检测: ${(tMimeDetected - tDecoded).toFixed(1)}ms (${contentType})\\n` +\n ` saveMediaBuffer: ${(tSaved - tMimeDetected).toFixed(1)}ms\\n` +\n ` 构造响应: ${(tEnd - tSaved).toFixed(1)}ms`,\n );\n\n // 7. 返回修改后的 MCP result 结构\n return {\n content: [{\n type: \"text\" as const,\n text: JSON.stringify(newBizData),\n }],\n };\n}\n"],"names":["MEDIA_DOWNLOAD_TIMEOUT_MS","detectMime","getWeComRuntime","path","fs"],"mappings":"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"}