@wecom/wecom-openclaw-plugin 2026.4.2 → 2026.4.3
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 +395 -10
- package/dist/index.d.ts +2 -4
- package/dist/index.js +58 -0
- package/dist/src/accounts.d.ts +57 -0
- package/dist/src/accounts.js +222 -0
- package/dist/src/agent/api-client.d.ts +94 -0
- package/dist/src/agent/api-client.js +292 -0
- package/dist/src/agent/handler.d.ts +63 -0
- package/dist/src/agent/handler.js +551 -0
- package/dist/src/agent/index.d.ts +5 -0
- package/dist/src/agent/index.js +5 -0
- package/dist/src/agent/webhook.d.ts +24 -0
- package/dist/src/agent/webhook.js +228 -0
- package/dist/src/agent/xml.d.ts +21 -0
- package/dist/src/agent/xml.js +41 -0
- package/dist/src/channel.js +579 -0
- package/dist/src/chat-queue.d.ts +31 -0
- package/dist/src/chat-queue.js +45 -0
- package/dist/{esm/types/src → src}/const.d.ts +46 -0
- package/dist/{esm/src → src}/const.js +77 -30
- package/dist/{esm/src → src}/dm-policy.js +6 -9
- package/dist/src/dynamic-agent.d.ts +37 -0
- package/dist/src/dynamic-agent.js +60 -0
- package/dist/src/dynamic-routing.d.ts +65 -0
- package/dist/src/dynamic-routing.js +58 -0
- package/dist/{esm/src → src}/group-policy.js +8 -22
- package/dist/src/http.d.ts +26 -0
- package/dist/src/http.js +93 -0
- package/dist/{esm/types/src → src}/interface.d.ts +0 -2
- package/dist/src/interface.js +4 -0
- package/dist/src/mcp/index.js +6 -0
- package/dist/{esm/src → src}/mcp/interceptors/biz-error.js +2 -6
- package/dist/{esm/src → src}/mcp/interceptors/index.js +6 -10
- package/dist/{esm/src → src}/mcp/interceptors/msg-media.js +6 -10
- package/dist/{esm/src → src}/mcp/interceptors/smartpage-create.js +2 -6
- package/dist/{esm/src → src}/mcp/interceptors/smartpage-export.js +2 -6
- package/dist/src/mcp/interceptors/types.js +7 -0
- package/dist/{esm/src → src}/mcp/schema.js +1 -4
- package/dist/{esm/src → src}/mcp/tool.js +4 -8
- package/dist/{esm/src → src}/mcp/transport.js +15 -16
- package/dist/{esm/src → src}/media-handler.js +6 -10
- package/dist/{esm/src → src}/media-uploader.js +64 -10
- package/dist/{esm/src → src}/message-parser.js +1 -4
- package/dist/{esm/types/src → src}/message-sender.d.ts +18 -0
- package/dist/{esm/src → src}/message-sender.js +45 -11
- package/dist/{esm/src → src}/monitor.js +145 -292
- package/dist/{esm/src → src}/onboarding.js +22 -26
- package/dist/{esm/types/src → src}/openclaw-compat.d.ts +49 -0
- package/dist/{esm/src → src}/openclaw-compat.js +133 -27
- package/dist/{esm/src → src}/reqid-store.js +4 -7
- package/dist/{esm/src → src}/runtime.js +2 -5
- package/dist/src/shared/command-auth.d.ts +23 -0
- package/dist/src/shared/command-auth.js +73 -0
- package/dist/src/shared/xml-parser.d.ts +46 -0
- package/dist/src/shared/xml-parser.js +210 -0
- package/dist/{esm/src → src}/state-dir-resolve.js +3 -7
- package/dist/{esm/src → src}/state-manager.js +84 -16
- package/dist/src/target.d.ts +35 -0
- package/dist/src/target.js +67 -0
- package/dist/src/template-card-manager.d.ts +55 -0
- package/dist/src/template-card-manager.js +205 -0
- package/dist/{esm/src → src}/template-card-parser.js +17 -8
- package/dist/{esm/src → src}/timeout.js +2 -5
- package/dist/src/types/account.d.ts +29 -0
- package/dist/src/types/account.js +4 -0
- package/dist/src/types/config.d.ts +91 -0
- package/dist/src/types/config.js +7 -0
- package/dist/src/types/constants.d.ts +42 -0
- package/dist/src/types/constants.js +42 -0
- package/dist/src/types/index.d.ts +7 -0
- package/dist/src/types/index.js +5 -0
- package/dist/src/types/message.d.ts +238 -0
- package/dist/src/types/message.js +5 -0
- package/dist/{esm/types/src → src}/utils.d.ts +41 -0
- package/dist/{esm/src → src}/utils.js +24 -10
- package/dist/src/version.d.ts +2 -0
- package/dist/src/version.js +24 -0
- package/dist/src/webhook/command-auth.d.ts +47 -0
- package/dist/src/webhook/command-auth.js +96 -0
- package/dist/src/webhook/gateway.d.ts +36 -0
- package/dist/src/webhook/gateway.js +235 -0
- package/dist/src/webhook/handler.d.ts +18 -0
- package/dist/src/webhook/handler.js +420 -0
- package/dist/src/webhook/helpers.d.ts +156 -0
- package/dist/src/webhook/helpers.js +833 -0
- package/dist/src/webhook/http.d.ts +26 -0
- package/dist/src/webhook/http.js +94 -0
- package/dist/src/webhook/index.d.ts +11 -0
- package/dist/src/webhook/index.js +14 -0
- package/dist/src/webhook/media.d.ts +29 -0
- package/dist/src/webhook/media.js +88 -0
- package/dist/src/webhook/monitor.d.ts +59 -0
- package/dist/src/webhook/monitor.js +1161 -0
- package/dist/src/webhook/state.d.ts +220 -0
- package/dist/src/webhook/state.js +491 -0
- package/dist/src/webhook/target.d.ts +41 -0
- package/dist/src/webhook/target.js +132 -0
- package/dist/src/webhook/types.d.ts +347 -0
- package/dist/src/webhook/types.js +33 -0
- package/dist/src/webhook/video-frame.d.ts +13 -0
- package/dist/src/webhook/video-frame.js +30 -0
- package/package.json +15 -7
- package/dist/cjs/index.js +0 -45
- package/dist/cjs/index.js.map +0 -1
- package/dist/cjs/src/channel.js +0 -339
- package/dist/cjs/src/channel.js.map +0 -1
- package/dist/cjs/src/const.js +0 -137
- package/dist/cjs/src/const.js.map +0 -1
- package/dist/cjs/src/dm-policy.js +0 -89
- package/dist/cjs/src/dm-policy.js.map +0 -1
- package/dist/cjs/src/group-policy.js +0 -133
- package/dist/cjs/src/group-policy.js.map +0 -1
- package/dist/cjs/src/mcp/interceptors/biz-error.js +0 -70
- package/dist/cjs/src/mcp/interceptors/biz-error.js.map +0 -1
- package/dist/cjs/src/mcp/interceptors/index.js +0 -73
- package/dist/cjs/src/mcp/interceptors/index.js.map +0 -1
- package/dist/cjs/src/mcp/interceptors/msg-media.js +0 -159
- package/dist/cjs/src/mcp/interceptors/msg-media.js.map +0 -1
- package/dist/cjs/src/mcp/interceptors/smartpage-create.js +0 -157
- package/dist/cjs/src/mcp/interceptors/smartpage-create.js.map +0 -1
- package/dist/cjs/src/mcp/interceptors/smartpage-export.js +0 -93
- package/dist/cjs/src/mcp/interceptors/smartpage-export.js.map +0 -1
- package/dist/cjs/src/mcp/schema.js +0 -104
- package/dist/cjs/src/mcp/schema.js.map +0 -1
- package/dist/cjs/src/mcp/tool.js +0 -208
- package/dist/cjs/src/mcp/tool.js.map +0 -1
- package/dist/cjs/src/mcp/transport.js +0 -417
- package/dist/cjs/src/mcp/transport.js.map +0 -1
- package/dist/cjs/src/media-handler.js +0 -129
- package/dist/cjs/src/media-handler.js.map +0 -1
- package/dist/cjs/src/media-uploader.js +0 -284
- package/dist/cjs/src/media-uploader.js.map +0 -1
- package/dist/cjs/src/message-parser.js +0 -134
- package/dist/cjs/src/message-parser.js.map +0 -1
- package/dist/cjs/src/message-sender.js +0 -89
- package/dist/cjs/src/message-sender.js.map +0 -1
- package/dist/cjs/src/monitor.js +0 -991
- package/dist/cjs/src/monitor.js.map +0 -1
- package/dist/cjs/src/onboarding.js +0 -182
- package/dist/cjs/src/onboarding.js.map +0 -1
- package/dist/cjs/src/openclaw-compat.js +0 -400
- package/dist/cjs/src/openclaw-compat.js.map +0 -1
- package/dist/cjs/src/reqid-store.js +0 -88
- package/dist/cjs/src/reqid-store.js.map +0 -1
- package/dist/cjs/src/runtime.js +0 -16
- package/dist/cjs/src/runtime.js.map +0 -1
- package/dist/cjs/src/state-dir-resolve.js +0 -35
- package/dist/cjs/src/state-dir-resolve.js.map +0 -1
- package/dist/cjs/src/state-manager.js +0 -158
- package/dist/cjs/src/state-manager.js.map +0 -1
- package/dist/cjs/src/template-card-parser.js +0 -641
- package/dist/cjs/src/template-card-parser.js.map +0 -1
- package/dist/cjs/src/timeout.js +0 -42
- package/dist/cjs/src/timeout.js.map +0 -1
- package/dist/cjs/src/utils.js +0 -60
- package/dist/cjs/src/utils.js.map +0 -1
- package/dist/cjs/src/version.js +0 -26
- package/dist/cjs/src/version.js.map +0 -1
- package/dist/esm/index.js +0 -41
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/src/channel.js +0 -337
- package/dist/esm/src/channel.js.map +0 -1
- package/dist/esm/src/const.js.map +0 -1
- package/dist/esm/src/dm-policy.js.map +0 -1
- package/dist/esm/src/group-policy.js.map +0 -1
- package/dist/esm/src/mcp/interceptors/biz-error.js.map +0 -1
- package/dist/esm/src/mcp/interceptors/index.js.map +0 -1
- package/dist/esm/src/mcp/interceptors/msg-media.js.map +0 -1
- package/dist/esm/src/mcp/interceptors/smartpage-create.js.map +0 -1
- package/dist/esm/src/mcp/interceptors/smartpage-export.js.map +0 -1
- package/dist/esm/src/mcp/schema.js.map +0 -1
- package/dist/esm/src/mcp/tool.js.map +0 -1
- package/dist/esm/src/mcp/transport.js.map +0 -1
- package/dist/esm/src/media-handler.js.map +0 -1
- package/dist/esm/src/media-uploader.js.map +0 -1
- package/dist/esm/src/message-parser.js.map +0 -1
- package/dist/esm/src/message-sender.js.map +0 -1
- package/dist/esm/src/monitor.js.map +0 -1
- package/dist/esm/src/onboarding.js.map +0 -1
- package/dist/esm/src/openclaw-compat.js.map +0 -1
- package/dist/esm/src/reqid-store.js.map +0 -1
- package/dist/esm/src/runtime.js.map +0 -1
- package/dist/esm/src/state-dir-resolve.js.map +0 -1
- package/dist/esm/src/state-manager.js.map +0 -1
- package/dist/esm/src/template-card-parser.js.map +0 -1
- package/dist/esm/src/timeout.js.map +0 -1
- package/dist/esm/src/utils.js.map +0 -1
- package/dist/esm/src/version.js +0 -23
- package/dist/esm/src/version.js.map +0 -1
- package/dist/esm/types/index.d.ts +0 -9
- package/dist/esm/types/src/version.d.ts +0 -2
- /package/dist/{esm/types/src → src}/channel.d.ts +0 -0
- /package/dist/{esm/types/src → src}/dm-policy.d.ts +0 -0
- /package/dist/{esm/types/src → src}/group-policy.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/index.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/interceptors/biz-error.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/interceptors/index.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/interceptors/msg-media.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/interceptors/smartpage-create.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/interceptors/smartpage-export.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/interceptors/types.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/schema.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/tool.d.ts +0 -0
- /package/dist/{esm/types/src → src}/mcp/transport.d.ts +0 -0
- /package/dist/{esm/types/src → src}/media-handler.d.ts +0 -0
- /package/dist/{esm/types/src → src}/media-uploader.d.ts +0 -0
- /package/dist/{esm/types/src → src}/message-parser.d.ts +0 -0
- /package/dist/{esm/types/src → src}/monitor.d.ts +0 -0
- /package/dist/{esm/types/src → src}/onboarding.d.ts +0 -0
- /package/dist/{esm/types/src → src}/reqid-store.d.ts +0 -0
- /package/dist/{esm/types/src → src}/runtime.d.ts +0 -0
- /package/dist/{esm/types/src → src}/state-dir-resolve.d.ts +0 -0
- /package/dist/{esm/types/src → src}/state-manager.d.ts +0 -0
- /package/dist/{esm/types/src → src}/template-card-parser.d.ts +0 -0
- /package/dist/{esm/types/src → src}/timeout.d.ts +0 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
2
|
+
import { CHANNEL_ID } from "./const.js";
|
|
3
|
+
import { DefaultWsUrl } from "./utils.js";
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// 账号列举
|
|
6
|
+
// ============================================================================
|
|
7
|
+
/**
|
|
8
|
+
* 列出 accounts 字段中配置的所有账号 ID(已 normalize)。
|
|
9
|
+
*/
|
|
10
|
+
function listConfiguredAccountIds(cfg) {
|
|
11
|
+
const accounts = cfg.channels?.[CHANNEL_ID]?.accounts;
|
|
12
|
+
if (!accounts || typeof accounts !== "object") {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
return Object.keys(accounts).filter(Boolean).map(normalizeAccountId);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 判断是否为多账号模式(即配置中存在 accounts 字段)。
|
|
19
|
+
* 用于区分单账号/多账号模式的分支判断,替代 `accountId === DEFAULT_ACCOUNT_ID` 的不可靠判断。
|
|
20
|
+
*/
|
|
21
|
+
export function hasMultiAccounts(cfg) {
|
|
22
|
+
return listConfiguredAccountIds(cfg).length > 0;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 列出所有企业微信账号 ID。
|
|
26
|
+
* 如果没有 accounts 字段,返回 [DEFAULT_ACCOUNT_ID] 以向后兼容。
|
|
27
|
+
*/
|
|
28
|
+
export function listWeComAccountIds(cfg) {
|
|
29
|
+
const ids = listConfiguredAccountIds(cfg);
|
|
30
|
+
if (ids.length === 0) {
|
|
31
|
+
// 向后兼容:未配置 accounts 时使用默认账号
|
|
32
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
33
|
+
}
|
|
34
|
+
return [...ids].sort((a, b) => a.localeCompare(b));
|
|
35
|
+
}
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// 默认账号解析
|
|
38
|
+
// ============================================================================
|
|
39
|
+
/**
|
|
40
|
+
* 解析默认账号 ID。
|
|
41
|
+
*
|
|
42
|
+
* 优先级:
|
|
43
|
+
* 1. 显式设置的 defaultAccount
|
|
44
|
+
* 2. 包含 DEFAULT_ACCOUNT_ID 的账号列表
|
|
45
|
+
* 3. 字母序第一个账号
|
|
46
|
+
*/
|
|
47
|
+
export function resolveDefaultWeComAccountId(cfg) {
|
|
48
|
+
const wecomConfig = cfg.channels?.[CHANNEL_ID];
|
|
49
|
+
const preferred = wecomConfig?.defaultAccount?.trim();
|
|
50
|
+
if (preferred) {
|
|
51
|
+
return normalizeAccountId(preferred);
|
|
52
|
+
}
|
|
53
|
+
const ids = listWeComAccountIds(cfg);
|
|
54
|
+
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
55
|
+
return DEFAULT_ACCOUNT_ID;
|
|
56
|
+
}
|
|
57
|
+
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
58
|
+
}
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// 配置合并
|
|
61
|
+
// ============================================================================
|
|
62
|
+
/**
|
|
63
|
+
* 合并顶层配置与账号级配置(账号级覆盖顶层)。
|
|
64
|
+
*
|
|
65
|
+
* 顶层字段(如 dmPolicy、allowFrom)作为所有账号的默认值,
|
|
66
|
+
* accounts.xxx 中的字段会覆盖顶层的同名字段。
|
|
67
|
+
* 对于 groups 等嵌套 Record 对象,使用深层合并(账号级条目覆盖同 key,但不丢失基础配置中的其他 key)。
|
|
68
|
+
*/
|
|
69
|
+
function mergeWeComAccountConfig(cfg, accountId) {
|
|
70
|
+
const wecomConfig = cfg.channels?.[CHANNEL_ID];
|
|
71
|
+
// 提取基础配置(排除 accounts 和 defaultAccount 字段避免递归)
|
|
72
|
+
const { accounts: _ignored, defaultAccount: _da, ...base } = wecomConfig ?? {};
|
|
73
|
+
// 查找账号级覆盖(支持 normalize 后的 key 匹配)
|
|
74
|
+
const account = findAccountConfig(wecomConfig?.accounts, accountId);
|
|
75
|
+
// 深层合并:对 groups 做嵌套合并,其余字段用账号级覆盖
|
|
76
|
+
const { groups: baseGroups, ...baseRest } = base;
|
|
77
|
+
const { groups: accountGroups, ...accountRest } = account;
|
|
78
|
+
const mergedGroups = baseGroups || accountGroups
|
|
79
|
+
? { ...baseGroups, ...accountGroups }
|
|
80
|
+
: undefined;
|
|
81
|
+
return {
|
|
82
|
+
...baseRest,
|
|
83
|
+
...accountRest,
|
|
84
|
+
...(mergedGroups !== undefined ? { groups: mergedGroups } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 在 accounts Record 中按 normalize 后的 accountId 查找配置。
|
|
89
|
+
* 避免因大小写差异导致找不到账号。
|
|
90
|
+
*/
|
|
91
|
+
function findAccountConfig(accounts, accountId) {
|
|
92
|
+
if (!accounts)
|
|
93
|
+
return {};
|
|
94
|
+
// 精确匹配优先
|
|
95
|
+
if (accounts[accountId])
|
|
96
|
+
return accounts[accountId];
|
|
97
|
+
// normalize 后匹配
|
|
98
|
+
const normalized = normalizeAccountId(accountId);
|
|
99
|
+
for (const [key, value] of Object.entries(accounts)) {
|
|
100
|
+
if (normalizeAccountId(key) === normalized)
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// 账号解析
|
|
107
|
+
// ============================================================================
|
|
108
|
+
/**
|
|
109
|
+
* 解析单个企业微信账号的完整配置。
|
|
110
|
+
*
|
|
111
|
+
* 支持:
|
|
112
|
+
* - 显式指定 accountId → 使用该 accountId
|
|
113
|
+
* - 未指定 → 使用默认账号
|
|
114
|
+
* - 单账号模式(无 accounts 字段) → 直接读取顶层配置
|
|
115
|
+
*/
|
|
116
|
+
export function resolveWeComAccountMulti(params) {
|
|
117
|
+
const hasExplicitId = typeof params.accountId === "string" && params.accountId.trim() !== "";
|
|
118
|
+
const accountId = hasExplicitId
|
|
119
|
+
? normalizeAccountId(params.accountId)
|
|
120
|
+
: resolveDefaultWeComAccountId(params.cfg);
|
|
121
|
+
const wecomConfig = params.cfg.channels?.[CHANNEL_ID];
|
|
122
|
+
// 顶层 enabled 状态
|
|
123
|
+
const baseEnabled = wecomConfig?.enabled !== false;
|
|
124
|
+
// 合并配置
|
|
125
|
+
const merged = mergeWeComAccountConfig(params.cfg, accountId);
|
|
126
|
+
// 账号级 enabled 状态
|
|
127
|
+
const accountEnabled = merged.enabled !== false;
|
|
128
|
+
// 解析 Agent 子配置
|
|
129
|
+
const agentCfg = merged.agent;
|
|
130
|
+
let agent;
|
|
131
|
+
if (agentCfg?.corpId && agentCfg?.corpSecret && agentCfg?.token && agentCfg?.encodingAESKey) {
|
|
132
|
+
agent = {
|
|
133
|
+
accountId,
|
|
134
|
+
enabled: baseEnabled && accountEnabled,
|
|
135
|
+
configured: true,
|
|
136
|
+
corpId: agentCfg.corpId,
|
|
137
|
+
corpSecret: agentCfg.corpSecret,
|
|
138
|
+
agentId: typeof agentCfg.agentId === "string" ? Number(agentCfg.agentId) || undefined : agentCfg.agentId,
|
|
139
|
+
token: agentCfg.token,
|
|
140
|
+
encodingAESKey: agentCfg.encodingAESKey,
|
|
141
|
+
config: agentCfg,
|
|
142
|
+
network: merged.network,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
accountId,
|
|
147
|
+
name: merged.name ?? "企业微信",
|
|
148
|
+
enabled: baseEnabled && accountEnabled,
|
|
149
|
+
websocketUrl: merged.websocketUrl || DefaultWsUrl,
|
|
150
|
+
botId: merged.botId ?? "",
|
|
151
|
+
secret: merged.secret ?? "",
|
|
152
|
+
sendThinkingMessage: merged.sendThinkingMessage ?? true,
|
|
153
|
+
config: merged,
|
|
154
|
+
agent,
|
|
155
|
+
token: merged.token ?? "",
|
|
156
|
+
encodingAESKey: merged.encodingAESKey ?? "",
|
|
157
|
+
receiveId: merged.receiveId ?? "",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// ============================================================================
|
|
161
|
+
// 批量查询
|
|
162
|
+
// ============================================================================
|
|
163
|
+
/**
|
|
164
|
+
* 列出所有已启用且已配置凭据的账号。
|
|
165
|
+
*/
|
|
166
|
+
export function listEnabledWeComAccounts(cfg) {
|
|
167
|
+
return listWeComAccountIds(cfg)
|
|
168
|
+
.map((accountId) => resolveWeComAccountMulti({ cfg, accountId }))
|
|
169
|
+
.filter((account) => {
|
|
170
|
+
if (!account.enabled)
|
|
171
|
+
return false;
|
|
172
|
+
const hasBotCredentials = Boolean(account.botId?.trim() && account.secret?.trim());
|
|
173
|
+
const hasAgentCredentials = Boolean(account.agent?.configured);
|
|
174
|
+
return hasBotCredentials || hasAgentCredentials;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
// ============================================================================
|
|
178
|
+
// 配置写入(多账号感知)
|
|
179
|
+
// ============================================================================
|
|
180
|
+
/**
|
|
181
|
+
* 写入企业微信账户配置(自动区分单账号/多账号模式)。
|
|
182
|
+
*
|
|
183
|
+
* - 单账号模式(无 accounts 字段):写入顶层 channels.wecom
|
|
184
|
+
* - 多账号模式:写入 channels.wecom.accounts[accountId]
|
|
185
|
+
*
|
|
186
|
+
* @param cfg 当前全局配置
|
|
187
|
+
* @param updates 要写入的部分配置字段
|
|
188
|
+
* @param accountId 目标账号 ID(默认写入默认账号)
|
|
189
|
+
*/
|
|
190
|
+
export function setWeComAccountMulti(cfg, updates, accountId) {
|
|
191
|
+
const resolvedAccountId = accountId ?? resolveDefaultWeComAccountId(cfg);
|
|
192
|
+
const isMulti = hasMultiAccounts(cfg);
|
|
193
|
+
if (!isMulti) {
|
|
194
|
+
// 单账号模式:合并到顶层
|
|
195
|
+
const existing = (cfg.channels?.[CHANNEL_ID] ?? {});
|
|
196
|
+
const merged = { ...existing, ...updates };
|
|
197
|
+
return {
|
|
198
|
+
...cfg,
|
|
199
|
+
channels: {
|
|
200
|
+
...cfg.channels,
|
|
201
|
+
[CHANNEL_ID]: merged,
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
// 多账号模式:合并到 accounts[accountId]
|
|
206
|
+
const wecomConfig = (cfg.channels?.[CHANNEL_ID] ?? {});
|
|
207
|
+
const existingAccount = wecomConfig.accounts?.[resolvedAccountId] ?? {};
|
|
208
|
+
const mergedAccount = { ...existingAccount, ...updates };
|
|
209
|
+
return {
|
|
210
|
+
...cfg,
|
|
211
|
+
channels: {
|
|
212
|
+
...cfg.channels,
|
|
213
|
+
[CHANNEL_ID]: {
|
|
214
|
+
...wecomConfig,
|
|
215
|
+
accounts: {
|
|
216
|
+
...wecomConfig.accounts,
|
|
217
|
+
[resolvedAccountId]: mergedAccount,
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeCom Agent API 客户端
|
|
3
|
+
* 管理 AccessToken 缓存和 API 调用
|
|
4
|
+
*/
|
|
5
|
+
import type { ResolvedAgentAccount } from "../types/index.js";
|
|
6
|
+
/**
|
|
7
|
+
* **getAccessToken (获取 AccessToken)**
|
|
8
|
+
*
|
|
9
|
+
* 获取企业微信 API 调用所需的 AccessToken。
|
|
10
|
+
* 具备自动缓存和过期刷新机制。
|
|
11
|
+
*
|
|
12
|
+
* @param agent Agent 账号信息
|
|
13
|
+
* @returns 有效的 AccessToken
|
|
14
|
+
*/
|
|
15
|
+
export declare function getAccessToken(agent: ResolvedAgentAccount): Promise<string>;
|
|
16
|
+
/**
|
|
17
|
+
* **sendText (发送文本消息)**
|
|
18
|
+
*
|
|
19
|
+
* 调用 `message/send` (Agent) 或 `appchat/send` (群聊) 发送文本。
|
|
20
|
+
*
|
|
21
|
+
* @param params.agent 发送方 Agent
|
|
22
|
+
* @param params.toUser 接收用户 ID (单聊可选,可与 toParty/toTag 同时使用)
|
|
23
|
+
* @param params.toParty 接收部门 ID (单聊可选)
|
|
24
|
+
* @param params.toTag 接收标签 ID (单聊可选)
|
|
25
|
+
* @param params.chatId 接收群 ID (群聊模式必填,互斥)
|
|
26
|
+
* @param params.text 消息内容
|
|
27
|
+
*/
|
|
28
|
+
export declare function sendText(params: {
|
|
29
|
+
agent: ResolvedAgentAccount;
|
|
30
|
+
toUser?: string;
|
|
31
|
+
toParty?: string;
|
|
32
|
+
toTag?: string;
|
|
33
|
+
chatId?: string;
|
|
34
|
+
text: string;
|
|
35
|
+
}): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* **uploadMedia (上传媒体文件)**
|
|
38
|
+
*
|
|
39
|
+
* 上传临时素材到企业微信。
|
|
40
|
+
* 素材有效期为 3 天。
|
|
41
|
+
*
|
|
42
|
+
* @param params.type 媒体类型 (image, voice, video, file)
|
|
43
|
+
* @param params.buffer 文件二进制数据
|
|
44
|
+
* @param params.filename 文件名 (需包含正确扩展名)
|
|
45
|
+
* @returns 媒体 ID (media_id)
|
|
46
|
+
*/
|
|
47
|
+
export declare function uploadMedia(params: {
|
|
48
|
+
agent: ResolvedAgentAccount;
|
|
49
|
+
type: "image" | "voice" | "video" | "file";
|
|
50
|
+
buffer: Buffer;
|
|
51
|
+
filename: string;
|
|
52
|
+
}): Promise<string>;
|
|
53
|
+
/**
|
|
54
|
+
* **sendMedia (发送媒体消息)**
|
|
55
|
+
*
|
|
56
|
+
* 发送图片、音频、视频或文件。需先通过 `uploadMedia` 获取 media_id。
|
|
57
|
+
*
|
|
58
|
+
* @param params.agent 发送方 Agent
|
|
59
|
+
* @param params.toUser 接收用户 ID (单聊可选)
|
|
60
|
+
* @param params.toParty 接收部门 ID (单聊可选)
|
|
61
|
+
* @param params.toTag 接收标签 ID (单聊可选)
|
|
62
|
+
* @param params.chatId 接收群 ID (群聊模式必填)
|
|
63
|
+
* @param params.mediaId 媒体 ID
|
|
64
|
+
* @param params.mediaType 媒体类型
|
|
65
|
+
* @param params.title 视频标题 (可选)
|
|
66
|
+
* @param params.description 视频描述 (可选)
|
|
67
|
+
*/
|
|
68
|
+
export declare function sendMedia(params: {
|
|
69
|
+
agent: ResolvedAgentAccount;
|
|
70
|
+
toUser?: string;
|
|
71
|
+
toParty?: string;
|
|
72
|
+
toTag?: string;
|
|
73
|
+
chatId?: string;
|
|
74
|
+
mediaId: string;
|
|
75
|
+
mediaType: "image" | "voice" | "video" | "file";
|
|
76
|
+
title?: string;
|
|
77
|
+
description?: string;
|
|
78
|
+
}): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* **downloadMedia (下载媒体文件)**
|
|
81
|
+
*
|
|
82
|
+
* 通过 media_id 从企业微信服务器下载临时素材。
|
|
83
|
+
*
|
|
84
|
+
* @returns { buffer, contentType }
|
|
85
|
+
*/
|
|
86
|
+
export declare function downloadMedia(params: {
|
|
87
|
+
agent: ResolvedAgentAccount;
|
|
88
|
+
mediaId: string;
|
|
89
|
+
maxBytes?: number;
|
|
90
|
+
}): Promise<{
|
|
91
|
+
buffer: Buffer;
|
|
92
|
+
contentType: string;
|
|
93
|
+
filename?: string;
|
|
94
|
+
}>;
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeCom Agent API 客户端
|
|
3
|
+
* 管理 AccessToken 缓存和 API 调用
|
|
4
|
+
*/
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import { API_ENDPOINTS, LIMITS } from "../types/constants.js";
|
|
7
|
+
import { readResponseBodyAsBuffer, wecomFetch } from "../http.js";
|
|
8
|
+
function resolveWecomEgressProxyUrlFromNetwork(network) {
|
|
9
|
+
return network?.egressProxyUrl;
|
|
10
|
+
}
|
|
11
|
+
const tokenCaches = new Map();
|
|
12
|
+
function normalizeUploadFilename(filename) {
|
|
13
|
+
const trimmed = filename.trim();
|
|
14
|
+
if (!trimmed)
|
|
15
|
+
return "file.bin";
|
|
16
|
+
const ext = trimmed.includes(".") ? `.${trimmed.split(".").pop().toLowerCase()}` : "";
|
|
17
|
+
const base = ext ? trimmed.slice(0, -ext.length) : trimmed;
|
|
18
|
+
const sanitizedBase = base
|
|
19
|
+
.replace(/[^\x20-\x7e]/g, "_")
|
|
20
|
+
.replace(/["\\\/;=]/g, "_")
|
|
21
|
+
.replace(/\s+/g, "_")
|
|
22
|
+
.replace(/_+/g, "_")
|
|
23
|
+
.replace(/^_+|_+$/g, "");
|
|
24
|
+
const safeBase = sanitizedBase || "file";
|
|
25
|
+
const safeExt = ext.replace(/[^a-z0-9.]/g, "");
|
|
26
|
+
return `${safeBase}${safeExt || ".bin"}`;
|
|
27
|
+
}
|
|
28
|
+
function guessUploadContentType(filename) {
|
|
29
|
+
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
|
30
|
+
const contentTypeMap = {
|
|
31
|
+
// image
|
|
32
|
+
jpg: "image/jpg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp", bmp: "image/bmp",
|
|
33
|
+
// audio / video
|
|
34
|
+
amr: "voice/amr", mp3: "audio/mpeg", wav: "audio/wav", m4a: "audio/mp4", ogg: "audio/ogg", mp4: "video/mp4", mov: "video/quicktime",
|
|
35
|
+
// documents
|
|
36
|
+
txt: "text/plain", md: "text/markdown", csv: "text/csv", tsv: "text/tab-separated-values", json: "application/json",
|
|
37
|
+
xml: "application/xml", yaml: "application/yaml", yml: "application/yaml",
|
|
38
|
+
pdf: "application/pdf", doc: "application/msword", docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
39
|
+
xls: "application/vnd.ms-excel", xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
40
|
+
ppt: "application/vnd.ms-powerpoint", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
41
|
+
rtf: "application/rtf", odt: "application/vnd.oasis.opendocument.text",
|
|
42
|
+
// archives
|
|
43
|
+
zip: "application/zip", rar: "application/vnd.rar", "7z": "application/x-7z-compressed",
|
|
44
|
+
gz: "application/gzip", tgz: "application/gzip", tar: "application/x-tar",
|
|
45
|
+
};
|
|
46
|
+
return contentTypeMap[ext] || "application/octet-stream";
|
|
47
|
+
}
|
|
48
|
+
function requireAgentId(agent) {
|
|
49
|
+
if (typeof agent.agentId === "number" && Number.isFinite(agent.agentId))
|
|
50
|
+
return agent.agentId;
|
|
51
|
+
throw new Error(`wecom agent account=${agent.accountId} missing agentId; sending via cgi-bin/message/send requires agentId`);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* **getAccessToken (获取 AccessToken)**
|
|
55
|
+
*
|
|
56
|
+
* 获取企业微信 API 调用所需的 AccessToken。
|
|
57
|
+
* 具备自动缓存和过期刷新机制。
|
|
58
|
+
*
|
|
59
|
+
* @param agent Agent 账号信息
|
|
60
|
+
* @returns 有效的 AccessToken
|
|
61
|
+
*/
|
|
62
|
+
export async function getAccessToken(agent) {
|
|
63
|
+
const cacheKey = `${agent.corpId}:${String(agent.agentId ?? "na")}`;
|
|
64
|
+
let cache = tokenCaches.get(cacheKey);
|
|
65
|
+
if (!cache) {
|
|
66
|
+
cache = { token: "", expiresAt: 0, refreshPromise: null };
|
|
67
|
+
tokenCaches.set(cacheKey, cache);
|
|
68
|
+
}
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
if (cache.token && cache.expiresAt > now + LIMITS.TOKEN_REFRESH_BUFFER_MS) {
|
|
71
|
+
return cache.token;
|
|
72
|
+
}
|
|
73
|
+
// 防止并发刷新
|
|
74
|
+
if (cache.refreshPromise) {
|
|
75
|
+
return cache.refreshPromise;
|
|
76
|
+
}
|
|
77
|
+
cache.refreshPromise = (async () => {
|
|
78
|
+
try {
|
|
79
|
+
const url = `${API_ENDPOINTS.GET_TOKEN}?corpid=${encodeURIComponent(agent.corpId)}&corpsecret=${encodeURIComponent(agent.corpSecret)}`;
|
|
80
|
+
const res = await wecomFetch(url, undefined, { proxyUrl: resolveWecomEgressProxyUrlFromNetwork(agent.network), timeoutMs: LIMITS.REQUEST_TIMEOUT_MS });
|
|
81
|
+
const json = await res.json();
|
|
82
|
+
if (!json?.access_token) {
|
|
83
|
+
throw new Error(`gettoken failed: ${json?.errcode} ${json?.errmsg}`);
|
|
84
|
+
}
|
|
85
|
+
cache.token = json.access_token;
|
|
86
|
+
cache.expiresAt = Date.now() + (json.expires_in ?? 7200) * 1000;
|
|
87
|
+
return cache.token;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
cache.refreshPromise = null;
|
|
91
|
+
}
|
|
92
|
+
})();
|
|
93
|
+
return cache.refreshPromise;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* **sendText (发送文本消息)**
|
|
97
|
+
*
|
|
98
|
+
* 调用 `message/send` (Agent) 或 `appchat/send` (群聊) 发送文本。
|
|
99
|
+
*
|
|
100
|
+
* @param params.agent 发送方 Agent
|
|
101
|
+
* @param params.toUser 接收用户 ID (单聊可选,可与 toParty/toTag 同时使用)
|
|
102
|
+
* @param params.toParty 接收部门 ID (单聊可选)
|
|
103
|
+
* @param params.toTag 接收标签 ID (单聊可选)
|
|
104
|
+
* @param params.chatId 接收群 ID (群聊模式必填,互斥)
|
|
105
|
+
* @param params.text 消息内容
|
|
106
|
+
*/
|
|
107
|
+
export async function sendText(params) {
|
|
108
|
+
const { agent, toUser, toParty, toTag, chatId, text } = params;
|
|
109
|
+
const token = await getAccessToken(agent);
|
|
110
|
+
const useChat = Boolean(chatId);
|
|
111
|
+
const url = useChat
|
|
112
|
+
? `${API_ENDPOINTS.SEND_APPCHAT}?access_token=${encodeURIComponent(token)}`
|
|
113
|
+
: `${API_ENDPOINTS.SEND_MESSAGE}?access_token=${encodeURIComponent(token)}`;
|
|
114
|
+
const body = useChat
|
|
115
|
+
? { chatid: chatId, msgtype: "text", text: { content: text } }
|
|
116
|
+
: {
|
|
117
|
+
touser: toUser,
|
|
118
|
+
toparty: toParty,
|
|
119
|
+
totag: toTag,
|
|
120
|
+
msgtype: "text",
|
|
121
|
+
agentid: requireAgentId(agent),
|
|
122
|
+
text: { content: text }
|
|
123
|
+
};
|
|
124
|
+
const res = await wecomFetch(url, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: { "Content-Type": "application/json" },
|
|
127
|
+
body: JSON.stringify(body),
|
|
128
|
+
}, { proxyUrl: resolveWecomEgressProxyUrlFromNetwork(agent.network), timeoutMs: LIMITS.REQUEST_TIMEOUT_MS });
|
|
129
|
+
const json = await res.json();
|
|
130
|
+
if (json?.errcode !== 0) {
|
|
131
|
+
throw new Error(`send failed: ${json?.errcode} ${json?.errmsg}`);
|
|
132
|
+
}
|
|
133
|
+
if (json?.invaliduser || json?.invalidparty || json?.invalidtag) {
|
|
134
|
+
const details = [
|
|
135
|
+
json.invaliduser ? `invaliduser=${json.invaliduser}` : "",
|
|
136
|
+
json.invalidparty ? `invalidparty=${json.invalidparty}` : "",
|
|
137
|
+
json.invalidtag ? `invalidtag=${json.invalidtag}` : ""
|
|
138
|
+
].filter(Boolean).join(", ");
|
|
139
|
+
throw new Error(`send partial failure: ${details}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* **uploadMedia (上传媒体文件)**
|
|
144
|
+
*
|
|
145
|
+
* 上传临时素材到企业微信。
|
|
146
|
+
* 素材有效期为 3 天。
|
|
147
|
+
*
|
|
148
|
+
* @param params.type 媒体类型 (image, voice, video, file)
|
|
149
|
+
* @param params.buffer 文件二进制数据
|
|
150
|
+
* @param params.filename 文件名 (需包含正确扩展名)
|
|
151
|
+
* @returns 媒体 ID (media_id)
|
|
152
|
+
*/
|
|
153
|
+
export async function uploadMedia(params) {
|
|
154
|
+
const { agent, type, buffer, filename } = params;
|
|
155
|
+
const safeFilename = normalizeUploadFilename(filename);
|
|
156
|
+
const token = await getAccessToken(agent);
|
|
157
|
+
const proxyUrl = resolveWecomEgressProxyUrlFromNetwork(agent.network);
|
|
158
|
+
// 添加 debug=1 参数获取更多错误信息
|
|
159
|
+
const url = `${API_ENDPOINTS.UPLOAD_MEDIA}?access_token=${encodeURIComponent(token)}&type=${encodeURIComponent(type)}&debug=1`;
|
|
160
|
+
// DEBUG: 输出上传信息
|
|
161
|
+
console.log(`[wecom-upload] Uploading media: type=${type}, filename=${safeFilename}, size=${buffer.length} bytes`);
|
|
162
|
+
const uploadOnce = async (fileContentType) => {
|
|
163
|
+
// 手动构造 multipart/form-data 请求体
|
|
164
|
+
// 企业微信要求包含 filename 和 filelength
|
|
165
|
+
const boundary = `----WebKitFormBoundary${crypto.randomBytes(16).toString("hex")}`;
|
|
166
|
+
const header = Buffer.from(`--${boundary}\r\n` +
|
|
167
|
+
`Content-Disposition: form-data; name="media"; filename="${safeFilename}"; filelength=${buffer.length}\r\n` +
|
|
168
|
+
`Content-Type: ${fileContentType}\r\n\r\n`);
|
|
169
|
+
const footer = Buffer.from(`\r\n--${boundary}--\r\n`);
|
|
170
|
+
const body = Buffer.concat([header, buffer, footer]);
|
|
171
|
+
console.log(`[wecom-upload] Multipart body size=${body.length}, boundary=${boundary}, fileContentType=${fileContentType}`);
|
|
172
|
+
const res = await wecomFetch(url, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: {
|
|
175
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
176
|
+
"Content-Length": String(body.length),
|
|
177
|
+
},
|
|
178
|
+
body: body,
|
|
179
|
+
}, { proxyUrl, timeoutMs: LIMITS.REQUEST_TIMEOUT_MS });
|
|
180
|
+
const json = await res.json();
|
|
181
|
+
console.log(`[wecom-upload] Response:`, JSON.stringify(json));
|
|
182
|
+
return json;
|
|
183
|
+
};
|
|
184
|
+
const preferredContentType = guessUploadContentType(safeFilename);
|
|
185
|
+
let json = await uploadOnce(preferredContentType);
|
|
186
|
+
// 某些文件类型在严格网关/企业微信校验下可能失败,回退到通用类型再试一次。
|
|
187
|
+
if (!json?.media_id && preferredContentType !== "application/octet-stream") {
|
|
188
|
+
console.warn(`[wecom-upload] Upload failed with ${preferredContentType}, retrying as application/octet-stream: ${json?.errcode} ${json?.errmsg}`);
|
|
189
|
+
json = await uploadOnce("application/octet-stream");
|
|
190
|
+
}
|
|
191
|
+
if (!json?.media_id) {
|
|
192
|
+
throw new Error(`upload failed: ${json?.errcode} ${json?.errmsg}`);
|
|
193
|
+
}
|
|
194
|
+
return json.media_id;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* **sendMedia (发送媒体消息)**
|
|
198
|
+
*
|
|
199
|
+
* 发送图片、音频、视频或文件。需先通过 `uploadMedia` 获取 media_id。
|
|
200
|
+
*
|
|
201
|
+
* @param params.agent 发送方 Agent
|
|
202
|
+
* @param params.toUser 接收用户 ID (单聊可选)
|
|
203
|
+
* @param params.toParty 接收部门 ID (单聊可选)
|
|
204
|
+
* @param params.toTag 接收标签 ID (单聊可选)
|
|
205
|
+
* @param params.chatId 接收群 ID (群聊模式必填)
|
|
206
|
+
* @param params.mediaId 媒体 ID
|
|
207
|
+
* @param params.mediaType 媒体类型
|
|
208
|
+
* @param params.title 视频标题 (可选)
|
|
209
|
+
* @param params.description 视频描述 (可选)
|
|
210
|
+
*/
|
|
211
|
+
export async function sendMedia(params) {
|
|
212
|
+
const { agent, toUser, toParty, toTag, chatId, mediaId, mediaType, title, description } = params;
|
|
213
|
+
const token = await getAccessToken(agent);
|
|
214
|
+
const useChat = Boolean(chatId);
|
|
215
|
+
const url = useChat
|
|
216
|
+
? `${API_ENDPOINTS.SEND_APPCHAT}?access_token=${encodeURIComponent(token)}`
|
|
217
|
+
: `${API_ENDPOINTS.SEND_MESSAGE}?access_token=${encodeURIComponent(token)}`;
|
|
218
|
+
const mediaPayload = mediaType === "video"
|
|
219
|
+
? { media_id: mediaId, title: title ?? "Video", description: description ?? "" }
|
|
220
|
+
: { media_id: mediaId };
|
|
221
|
+
const body = useChat
|
|
222
|
+
? { chatid: chatId, msgtype: mediaType, [mediaType]: mediaPayload }
|
|
223
|
+
: {
|
|
224
|
+
touser: toUser,
|
|
225
|
+
toparty: toParty,
|
|
226
|
+
totag: toTag,
|
|
227
|
+
msgtype: mediaType,
|
|
228
|
+
agentid: requireAgentId(agent),
|
|
229
|
+
[mediaType]: mediaPayload
|
|
230
|
+
};
|
|
231
|
+
const res = await wecomFetch(url, {
|
|
232
|
+
method: "POST",
|
|
233
|
+
headers: { "Content-Type": "application/json" },
|
|
234
|
+
body: JSON.stringify(body),
|
|
235
|
+
}, { proxyUrl: resolveWecomEgressProxyUrlFromNetwork(agent.network), timeoutMs: LIMITS.REQUEST_TIMEOUT_MS });
|
|
236
|
+
const json = await res.json();
|
|
237
|
+
if (json?.errcode !== 0) {
|
|
238
|
+
throw new Error(`send ${mediaType} failed: ${json?.errcode} ${json?.errmsg}`);
|
|
239
|
+
}
|
|
240
|
+
if (json?.invaliduser || json?.invalidparty || json?.invalidtag) {
|
|
241
|
+
const details = [
|
|
242
|
+
json.invaliduser ? `invaliduser=${json.invaliduser}` : "",
|
|
243
|
+
json.invalidparty ? `invalidparty=${json.invalidparty}` : "",
|
|
244
|
+
json.invalidtag ? `invalidtag=${json.invalidtag}` : ""
|
|
245
|
+
].filter(Boolean).join(", ");
|
|
246
|
+
throw new Error(`send ${mediaType} partial failure: ${details}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* **downloadMedia (下载媒体文件)**
|
|
251
|
+
*
|
|
252
|
+
* 通过 media_id 从企业微信服务器下载临时素材。
|
|
253
|
+
*
|
|
254
|
+
* @returns { buffer, contentType }
|
|
255
|
+
*/
|
|
256
|
+
export async function downloadMedia(params) {
|
|
257
|
+
const { agent, mediaId } = params;
|
|
258
|
+
const token = await getAccessToken(agent);
|
|
259
|
+
const url = `${API_ENDPOINTS.DOWNLOAD_MEDIA}?access_token=${encodeURIComponent(token)}&media_id=${encodeURIComponent(mediaId)}`;
|
|
260
|
+
const res = await wecomFetch(url, undefined, { proxyUrl: resolveWecomEgressProxyUrlFromNetwork(agent.network), timeoutMs: LIMITS.REQUEST_TIMEOUT_MS });
|
|
261
|
+
if (!res.ok) {
|
|
262
|
+
throw new Error(`download failed: ${res.status}`);
|
|
263
|
+
}
|
|
264
|
+
const contentType = res.headers.get("content-type") || "application/octet-stream";
|
|
265
|
+
const disposition = res.headers.get("content-disposition") || "";
|
|
266
|
+
const filename = (() => {
|
|
267
|
+
// 兼容:filename="a.md" / filename=a.md / filename*=UTF-8''a%2Eb.md
|
|
268
|
+
const mStar = disposition.match(/filename\*\s*=\s*([^;]+)/i);
|
|
269
|
+
if (mStar) {
|
|
270
|
+
const raw = mStar[1].trim().replace(/^"(.*)"$/, "$1");
|
|
271
|
+
const parts = raw.split("''");
|
|
272
|
+
const encoded = parts.length === 2 ? parts[1] : raw;
|
|
273
|
+
try {
|
|
274
|
+
return decodeURIComponent(encoded);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return encoded;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const m = disposition.match(/filename\s*=\s*([^;]+)/i);
|
|
281
|
+
if (!m)
|
|
282
|
+
return undefined;
|
|
283
|
+
return m[1].trim().replace(/^"(.*)"$/, "$1") || undefined;
|
|
284
|
+
})();
|
|
285
|
+
// 检查是否返回了错误 JSON
|
|
286
|
+
if (contentType.includes("application/json")) {
|
|
287
|
+
const json = await res.json();
|
|
288
|
+
throw new Error(`download failed: ${json?.errcode} ${json?.errmsg}`);
|
|
289
|
+
}
|
|
290
|
+
const buffer = await readResponseBodyAsBuffer(res, params.maxBytes);
|
|
291
|
+
return { buffer, contentType, filename };
|
|
292
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeCom Agent Webhook 处理器
|
|
3
|
+
* 处理 XML 格式回调
|
|
4
|
+
*/
|
|
5
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
6
|
+
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
7
|
+
import type { ResolvedAgentAccount } from "../types/index.js";
|
|
8
|
+
import type { WecomAgentInboundMessage } from "../types/index.js";
|
|
9
|
+
/**
|
|
10
|
+
* **AgentWebhookParams (Webhook 处理器参数)**
|
|
11
|
+
*
|
|
12
|
+
* 传递给 Agent Webhook 处理函数的上下文参数集合。
|
|
13
|
+
* @property req Node.js 原始请求对象
|
|
14
|
+
* @property res Node.js 原始响应对象
|
|
15
|
+
* @property agent 解析后的 Agent 账号信息
|
|
16
|
+
* @property config 全局插件配置
|
|
17
|
+
* @property core OpenClaw 插件运行时
|
|
18
|
+
* @property log 可选日志输出函数
|
|
19
|
+
* @property error 可选错误输出函数
|
|
20
|
+
*/
|
|
21
|
+
export type AgentWebhookParams = {
|
|
22
|
+
req: IncomingMessage;
|
|
23
|
+
res: ServerResponse;
|
|
24
|
+
/**
|
|
25
|
+
* 上游已完成验签/解密时传入,避免重复协议处理。
|
|
26
|
+
* 仅用于 POST 消息回调流程。
|
|
27
|
+
*/
|
|
28
|
+
verifiedPost?: {
|
|
29
|
+
timestamp: string;
|
|
30
|
+
nonce: string;
|
|
31
|
+
signature: string;
|
|
32
|
+
encrypted: string;
|
|
33
|
+
decrypted: string;
|
|
34
|
+
parsed: WecomAgentInboundMessage;
|
|
35
|
+
};
|
|
36
|
+
agent: ResolvedAgentAccount;
|
|
37
|
+
config: OpenClawConfig;
|
|
38
|
+
core: PluginRuntime;
|
|
39
|
+
log?: (msg: string) => void;
|
|
40
|
+
error?: (msg: string) => void;
|
|
41
|
+
};
|
|
42
|
+
export type AgentInboundProcessDecision = {
|
|
43
|
+
shouldProcess: boolean;
|
|
44
|
+
reason: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* 仅允许“用户意图消息”进入 AI 会话。
|
|
48
|
+
* - event 回调(如 enter_agent/subscribe)不应触发会话与自动回复
|
|
49
|
+
* - 系统发送者(sys)不应触发会话与自动回复
|
|
50
|
+
* - 缺失发送者时默认丢弃,避免写入异常会话
|
|
51
|
+
*/
|
|
52
|
+
export declare function shouldProcessAgentInboundMessage(params: {
|
|
53
|
+
msgType: string;
|
|
54
|
+
fromUser: string;
|
|
55
|
+
eventType?: string;
|
|
56
|
+
}): AgentInboundProcessDecision;
|
|
57
|
+
/**
|
|
58
|
+
* **handleAgentWebhook (Agent Webhook 入口)**
|
|
59
|
+
*
|
|
60
|
+
* 统一处理 Agent 模式的 POST 消息回调请求。
|
|
61
|
+
* URL 验证与验签/解密由 monitor 层统一处理后再调用本函数。
|
|
62
|
+
*/
|
|
63
|
+
export declare function handleAgentWebhook(params: AgentWebhookParams): Promise<boolean>;
|