@mocrane/wecom 2026.2.27 → 2026.3.8-4
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/LICENSE +4 -18
- package/README.md +572 -0
- package/assets/01.bot-add.png +0 -0
- package/assets/01.bot-setp2.png +0 -0
- package/assets/02.agent.add.png +0 -0
- package/assets/02.agent.api-set.png +0 -0
- package/assets/register.png +0 -0
- package/changelog/v2.2.28.md +70 -0
- package/changelog/v2.3.2.md +28 -0
- package/changelog/v2.3.4.md +20 -0
- package/index.ts +11 -3
- package/package.json +4 -2
- package/src/accounts.ts +17 -55
- package/src/agent/api-client.ts +84 -37
- package/src/agent/api-client.upload.test.ts +110 -0
- package/src/agent/handler.event-filter.test.ts +50 -0
- package/src/agent/handler.ts +147 -145
- package/src/channel.config.test.ts +147 -0
- package/src/channel.lifecycle.test.ts +252 -0
- package/src/channel.ts +132 -141
- package/src/config/accounts.resolve.test.ts +38 -0
- package/src/config/accounts.ts +267 -25
- package/src/config/index.ts +6 -0
- package/src/config/network.ts +9 -5
- package/src/config/routing.test.ts +88 -0
- package/src/config/routing.ts +26 -0
- package/src/config/schema.ts +41 -6
- package/src/config-schema.ts +5 -41
- package/src/dynamic-agent.account-scope.test.ts +17 -0
- package/src/dynamic-agent.ts +13 -13
- package/src/gateway-monitor.ts +260 -0
- package/src/http.ts +16 -2
- package/src/media.test.ts +28 -1
- package/src/media.ts +59 -1
- package/src/monitor/state.queue.test.ts +1 -1
- package/src/monitor/state.ts +1 -1
- package/src/monitor/types.ts +5 -1
- package/src/monitor.active.test.ts +15 -9
- package/src/monitor.inbound-filter.test.ts +63 -0
- package/src/monitor.integration.test.ts +4 -2
- package/src/monitor.ts +982 -134
- package/src/monitor.webhook.test.ts +381 -3
- package/src/onboarding.ts +379 -54
- package/src/outbound.test.ts +130 -0
- package/src/outbound.ts +82 -9
- package/src/shared/command-auth.ts +4 -2
- package/src/shared/xml-parser.test.ts +21 -1
- package/src/shared/xml-parser.ts +18 -0
- package/src/types/account.ts +54 -16
- package/src/types/config.ts +50 -6
- package/src/types/constants.ts +7 -3
- package/src/types/index.ts +3 -0
- package/src/types.ts +29 -147
- package/src/ws-adapter.ts +481 -0
package/src/outbound.test.ts
CHANGED
|
@@ -19,6 +19,38 @@ describe("wecomOutbound", () => {
|
|
|
19
19
|
).rejects.toThrow(/Agent mode/i);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
it("throws explicit error when outbound accountId does not exist", async () => {
|
|
23
|
+
const { wecomOutbound } = await import("./outbound.js");
|
|
24
|
+
const cfg = {
|
|
25
|
+
channels: {
|
|
26
|
+
wecom: {
|
|
27
|
+
enabled: true,
|
|
28
|
+
defaultAccount: "acct-a",
|
|
29
|
+
accounts: {
|
|
30
|
+
"acct-a": {
|
|
31
|
+
enabled: true,
|
|
32
|
+
agent: {
|
|
33
|
+
corpId: "corp-a",
|
|
34
|
+
corpSecret: "secret-a",
|
|
35
|
+
agentId: 10001,
|
|
36
|
+
token: "token-a",
|
|
37
|
+
encodingAESKey: "aes-a",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
await expect(
|
|
45
|
+
wecomOutbound.sendText({
|
|
46
|
+
cfg,
|
|
47
|
+
accountId: "acct-missing",
|
|
48
|
+
to: "user:zhangsan",
|
|
49
|
+
text: "hello",
|
|
50
|
+
} as any),
|
|
51
|
+
).rejects.toThrow(/account "acct-missing" not found/i);
|
|
52
|
+
});
|
|
53
|
+
|
|
22
54
|
it("routes sendText to agent chatId/userid", async () => {
|
|
23
55
|
const { wecomOutbound } = await import("./outbound.js");
|
|
24
56
|
const api = await import("./agent/api-client.js");
|
|
@@ -140,4 +172,102 @@ describe("wecomOutbound", () => {
|
|
|
140
172
|
|
|
141
173
|
now.mockRestore();
|
|
142
174
|
});
|
|
175
|
+
|
|
176
|
+
it("uses account-scoped agent config in matrix mode", async () => {
|
|
177
|
+
const { wecomOutbound } = await import("./outbound.js");
|
|
178
|
+
const api = await import("./agent/api-client.js");
|
|
179
|
+
(api.sendText as any).mockResolvedValue(undefined);
|
|
180
|
+
(api.sendText as any).mockClear();
|
|
181
|
+
|
|
182
|
+
const cfg = {
|
|
183
|
+
channels: {
|
|
184
|
+
wecom: {
|
|
185
|
+
enabled: true,
|
|
186
|
+
defaultAccount: "acct-a",
|
|
187
|
+
accounts: {
|
|
188
|
+
"acct-a": {
|
|
189
|
+
enabled: true,
|
|
190
|
+
agent: {
|
|
191
|
+
corpId: "corp-a",
|
|
192
|
+
corpSecret: "secret-a",
|
|
193
|
+
agentId: 10001,
|
|
194
|
+
token: "token-a",
|
|
195
|
+
encodingAESKey: "aes-a",
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
"acct-b": {
|
|
199
|
+
enabled: true,
|
|
200
|
+
agent: {
|
|
201
|
+
corpId: "corp-b",
|
|
202
|
+
corpSecret: "secret-b",
|
|
203
|
+
agentId: 10002,
|
|
204
|
+
token: "token-b",
|
|
205
|
+
encodingAESKey: "aes-b",
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
await wecomOutbound.sendText({
|
|
214
|
+
cfg,
|
|
215
|
+
accountId: "acct-b",
|
|
216
|
+
to: "user:lisi",
|
|
217
|
+
text: "hello b",
|
|
218
|
+
} as any);
|
|
219
|
+
expect(api.sendText).toHaveBeenCalledWith(
|
|
220
|
+
expect.objectContaining({
|
|
221
|
+
toUser: "lisi",
|
|
222
|
+
agent: expect.objectContaining({
|
|
223
|
+
accountId: "acct-b",
|
|
224
|
+
agentId: 10002,
|
|
225
|
+
corpId: "corp-b",
|
|
226
|
+
}),
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("rejects outbound when target account has matrix conflict", async () => {
|
|
232
|
+
const { wecomOutbound } = await import("./outbound.js");
|
|
233
|
+
const cfg = {
|
|
234
|
+
channels: {
|
|
235
|
+
wecom: {
|
|
236
|
+
enabled: true,
|
|
237
|
+
defaultAccount: "acct-a",
|
|
238
|
+
accounts: {
|
|
239
|
+
"acct-a": {
|
|
240
|
+
enabled: true,
|
|
241
|
+
agent: {
|
|
242
|
+
corpId: "corp-shared",
|
|
243
|
+
corpSecret: "secret-a",
|
|
244
|
+
agentId: 10001,
|
|
245
|
+
token: "token-a",
|
|
246
|
+
encodingAESKey: "aes-a",
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
"acct-b": {
|
|
250
|
+
enabled: true,
|
|
251
|
+
agent: {
|
|
252
|
+
corpId: "corp-shared",
|
|
253
|
+
corpSecret: "secret-b",
|
|
254
|
+
agentId: 10001,
|
|
255
|
+
token: "token-b",
|
|
256
|
+
encodingAESKey: "aes-b",
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
await expect(
|
|
265
|
+
wecomOutbound.sendText({
|
|
266
|
+
cfg,
|
|
267
|
+
accountId: "acct-b",
|
|
268
|
+
to: "user:lisi",
|
|
269
|
+
text: "hello",
|
|
270
|
+
} as any),
|
|
271
|
+
).rejects.toThrow(/duplicate wecom agent identity/i);
|
|
272
|
+
});
|
|
143
273
|
});
|
package/src/outbound.ts
CHANGED
|
@@ -1,20 +1,50 @@
|
|
|
1
1
|
import type { ChannelOutboundAdapter, ChannelOutboundContext } from "openclaw/plugin-sdk";
|
|
2
2
|
|
|
3
3
|
import { sendText as sendAgentText, sendMedia as sendAgentMedia, uploadMedia } from "./agent/api-client.js";
|
|
4
|
-
import { resolveWecomAccounts } from "./config/index.js";
|
|
4
|
+
import { resolveWecomAccount, resolveWecomAccountConflict, resolveWecomAccounts } from "./config/index.js";
|
|
5
5
|
import { getWecomRuntime } from "./runtime.js";
|
|
6
|
+
import { getWsClient, waitForWsConnection } from "./ws-adapter.js";
|
|
6
7
|
|
|
7
8
|
import { resolveWecomTarget } from "./target.js";
|
|
8
9
|
|
|
9
|
-
function resolveAgentConfigOrThrow(
|
|
10
|
-
|
|
10
|
+
function resolveAgentConfigOrThrow(params: {
|
|
11
|
+
cfg: ChannelOutboundContext["cfg"];
|
|
12
|
+
accountId?: string | null;
|
|
13
|
+
}) {
|
|
14
|
+
const resolvedAccounts = resolveWecomAccounts(params.cfg);
|
|
15
|
+
const conflictAccountId = params.accountId?.trim() || resolvedAccounts.defaultAccountId;
|
|
16
|
+
const conflict = resolveWecomAccountConflict({
|
|
17
|
+
cfg: params.cfg,
|
|
18
|
+
accountId: conflictAccountId,
|
|
19
|
+
});
|
|
20
|
+
if (conflict) {
|
|
21
|
+
throw new Error(conflict.message);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const requestedAccountId = params.accountId?.trim();
|
|
25
|
+
if (requestedAccountId) {
|
|
26
|
+
if (!resolvedAccounts.accounts[requestedAccountId]) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`WeCom outbound account "${requestedAccountId}" not found. Configure channels.wecom.accounts.${requestedAccountId} or use an existing accountId.`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const account = resolveWecomAccount({
|
|
33
|
+
cfg: params.cfg,
|
|
34
|
+
accountId: params.accountId,
|
|
35
|
+
}).agent;
|
|
11
36
|
if (!account?.configured) {
|
|
12
37
|
throw new Error(
|
|
13
|
-
|
|
38
|
+
`WeCom outbound requires Agent mode for account=${params.accountId ?? "default"}. Configure channels.wecom.accounts.<accountId>.agent (or legacy channels.wecom.agent).`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (typeof account.agentId !== "number" || !Number.isFinite(account.agentId)) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`WeCom outbound requires channels.wecom.accounts.<accountId>.agent.agentId (or legacy channels.wecom.agent.agentId) for account=${params.accountId ?? account.accountId}.`,
|
|
14
44
|
);
|
|
15
45
|
}
|
|
16
46
|
// 注意:不要在日志里输出 corpSecret 等敏感信息
|
|
17
|
-
console.log(`[wecom-outbound] Using agent config: corpId=${account.corpId}, agentId=${account.agentId}`);
|
|
47
|
+
console.log(`[wecom-outbound] Using agent config: accountId=${account.accountId}, corpId=${account.corpId}, agentId=${account.agentId}`);
|
|
18
48
|
return account;
|
|
19
49
|
}
|
|
20
50
|
|
|
@@ -29,10 +59,47 @@ export const wecomOutbound: ChannelOutboundAdapter = {
|
|
|
29
59
|
return [text];
|
|
30
60
|
}
|
|
31
61
|
},
|
|
32
|
-
sendText: async ({ cfg, to, text }: ChannelOutboundContext) => {
|
|
62
|
+
sendText: async ({ cfg, to, text, accountId }: ChannelOutboundContext) => {
|
|
33
63
|
// signal removed - not supported in current SDK
|
|
34
64
|
|
|
35
|
-
|
|
65
|
+
// ── Bot WebSocket outbound 独立路径 ──
|
|
66
|
+
// WS Bot 完全独立收发,不与 Agent 组成双模,失败时直接抛错而非 fallthrough
|
|
67
|
+
const resolvedAccount = resolveWecomAccount({ cfg, accountId });
|
|
68
|
+
const botAccount = resolvedAccount.bot;
|
|
69
|
+
if (botAccount?.connectionMode === 'websocket' && botAccount.configured) {
|
|
70
|
+
const wsClient = getWsClient(botAccount.accountId);
|
|
71
|
+
const wsTarget = resolveWecomTarget(to);
|
|
72
|
+
const chatid = wsTarget?.touser || wsTarget?.chatid;
|
|
73
|
+
|
|
74
|
+
// 如果目标是 Agent 会话(wecom-agent:),跳过 WS Bot,走 Agent outbound
|
|
75
|
+
const rawTo = typeof to === "string" ? to.trim().toLowerCase() : "";
|
|
76
|
+
if (!rawTo.startsWith("wecom-agent:")) {
|
|
77
|
+
if (!wsClient?.isConnected) {
|
|
78
|
+
console.log(`[wecom-outbound] Bot WS 未连接,等待重连... (accountId=${botAccount.accountId})`);
|
|
79
|
+
const reconnected = await waitForWsConnection(botAccount.accountId, 10_000);
|
|
80
|
+
if (!reconnected) {
|
|
81
|
+
throw new Error(`[wecom-outbound] Bot WS 等待重连超时,无法发送消息 (accountId=${botAccount.accountId})`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!chatid) {
|
|
85
|
+
throw new Error(`[wecom-outbound] Bot WS 无法解析目标 chatid (to=${String(to)})`);
|
|
86
|
+
}
|
|
87
|
+
// 重连后重新获取 client(可能是新实例)
|
|
88
|
+
const activeClient = getWsClient(botAccount.accountId);
|
|
89
|
+
if (!activeClient?.isConnected) {
|
|
90
|
+
throw new Error(`[wecom-outbound] Bot WS 重连后仍不可用 (accountId=${botAccount.accountId})`);
|
|
91
|
+
}
|
|
92
|
+
await activeClient.sendMessage(chatid, {
|
|
93
|
+
msgtype: 'markdown',
|
|
94
|
+
markdown: { content: text },
|
|
95
|
+
});
|
|
96
|
+
console.log(`[wecom-outbound] Sent text via Bot WS to chatid=${chatid} (len=${text.length})`);
|
|
97
|
+
return { channel: "wecom", messageId: `ws-bot-${Date.now()}`, timestamp: Date.now() };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Agent outbound(Webhook Bot 双模 / Agent 独立)──
|
|
102
|
+
const agent = resolveAgentConfigOrThrow({ cfg, accountId });
|
|
36
103
|
const target = resolveWecomTarget(to);
|
|
37
104
|
if (!target) {
|
|
38
105
|
throw new Error("WeCom outbound requires a target (userid, partyid, tagid or chatid).");
|
|
@@ -98,10 +165,10 @@ export const wecomOutbound: ChannelOutboundAdapter = {
|
|
|
98
165
|
timestamp: Date.now(),
|
|
99
166
|
};
|
|
100
167
|
},
|
|
101
|
-
sendMedia: async ({ cfg, to, text, mediaUrl }: ChannelOutboundContext) => {
|
|
168
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, accountId }: ChannelOutboundContext) => {
|
|
102
169
|
// signal removed - not supported in current SDK
|
|
103
170
|
|
|
104
|
-
const agent = resolveAgentConfigOrThrow(cfg);
|
|
171
|
+
const agent = resolveAgentConfigOrThrow({ cfg, accountId });
|
|
105
172
|
const target = resolveWecomTarget(to);
|
|
106
173
|
if (!target) {
|
|
107
174
|
throw new Error("WeCom outbound requires a target (userid, partyid, tagid or chatid).");
|
|
@@ -149,6 +216,12 @@ export const wecomOutbound: ChannelOutboundAdapter = {
|
|
|
149
216
|
amr: "audio/amr", mp4: "video/mp4", pdf: "application/pdf", doc: "application/msword",
|
|
150
217
|
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
151
218
|
xls: "application/vnd.ms-excel", xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
219
|
+
ppt: "application/vnd.ms-powerpoint", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
220
|
+
txt: "text/plain", csv: "text/csv", tsv: "text/tab-separated-values", md: "text/markdown", json: "application/json",
|
|
221
|
+
xml: "application/xml", yaml: "application/yaml", yml: "application/yaml",
|
|
222
|
+
zip: "application/zip", rar: "application/vnd.rar", "7z": "application/x-7z-compressed",
|
|
223
|
+
tar: "application/x-tar", gz: "application/gzip", tgz: "application/gzip",
|
|
224
|
+
rtf: "application/rtf", odt: "application/vnd.oasis.opendocument.text",
|
|
152
225
|
};
|
|
153
226
|
contentType = mimeTypes[ext] || "application/octet-stream";
|
|
154
227
|
console.log(`[wecom-outbound] Reading local file: ${mediaUrl}, ext=${ext}, contentType=${contentType}`);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { WecomAgentConfig, WecomBotConfig } from "../types/index.js";
|
|
4
|
+
|
|
5
|
+
type WecomCommandAuthAccountConfig = Pick<WecomBotConfig, "dm"> | Pick<WecomAgentConfig, "dm">;
|
|
4
6
|
|
|
5
7
|
function normalizeWecomAllowFromEntry(raw: string): string {
|
|
6
8
|
return raw
|
|
@@ -22,7 +24,7 @@ function isWecomSenderAllowed(senderUserId: string, allowFrom: string[]): boolea
|
|
|
22
24
|
export async function resolveWecomCommandAuthorization(params: {
|
|
23
25
|
core: PluginRuntime;
|
|
24
26
|
cfg: OpenClawConfig;
|
|
25
|
-
accountConfig:
|
|
27
|
+
accountConfig: WecomCommandAuthAccountConfig;
|
|
26
28
|
rawBody: string;
|
|
27
29
|
senderUserId: string;
|
|
28
30
|
}): Promise<{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
2
|
|
|
3
|
-
import { extractContent, extractMediaId, extractMsgId } from "./xml-parser.js";
|
|
3
|
+
import { extractContent, extractFromUser, extractMediaId, extractMsgId, parseXml } from "./xml-parser.js";
|
|
4
4
|
|
|
5
5
|
describe("wecom xml-parser", () => {
|
|
6
6
|
test("extractContent is robust to non-string Content", () => {
|
|
@@ -27,4 +27,24 @@ describe("wecom xml-parser", () => {
|
|
|
27
27
|
const msg: any = { MsgId: 123456789 };
|
|
28
28
|
expect(extractMsgId(msg)).toBe("123456789");
|
|
29
29
|
});
|
|
30
|
+
|
|
31
|
+
test("parseXml preserves leading zero userid in FromUserName", () => {
|
|
32
|
+
const xml = `
|
|
33
|
+
<xml>
|
|
34
|
+
<FromUserName><![CDATA[0254571]]></FromUserName>
|
|
35
|
+
</xml>
|
|
36
|
+
`;
|
|
37
|
+
const msg = parseXml(xml);
|
|
38
|
+
expect(extractFromUser(msg)).toBe("0254571");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("parseXml preserves 64-bit MsgId as string", () => {
|
|
42
|
+
const xml = `
|
|
43
|
+
<xml>
|
|
44
|
+
<MsgId>1234567890123456</MsgId>
|
|
45
|
+
</xml>
|
|
46
|
+
`;
|
|
47
|
+
const msg = parseXml(xml);
|
|
48
|
+
expect(extractMsgId(msg)).toBe("1234567890123456");
|
|
49
|
+
});
|
|
30
50
|
});
|
package/src/shared/xml-parser.ts
CHANGED
|
@@ -10,6 +10,8 @@ const xmlParser = new XMLParser({
|
|
|
10
10
|
ignoreAttributes: false,
|
|
11
11
|
trimValues: true,
|
|
12
12
|
processEntities: false,
|
|
13
|
+
parseTagValue: false,
|
|
14
|
+
parseAttributeValue: false,
|
|
13
15
|
});
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -72,6 +74,22 @@ export function extractChatId(msg: WecomAgentInboundMessage): string | undefined
|
|
|
72
74
|
return msg.ChatId ? String(msg.ChatId) : undefined;
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
/**
|
|
78
|
+
* 从 XML 中提取 AgentID(兼容 AgentID/agentid 等大小写)
|
|
79
|
+
*/
|
|
80
|
+
export function extractAgentId(msg: WecomAgentInboundMessage): string | number | undefined {
|
|
81
|
+
const raw =
|
|
82
|
+
(msg as any).AgentID ??
|
|
83
|
+
(msg as any).AgentId ??
|
|
84
|
+
(msg as any).agentid ??
|
|
85
|
+
(msg as any).agentId;
|
|
86
|
+
if (raw == null) return undefined;
|
|
87
|
+
if (typeof raw === "string") return raw.trim() || undefined;
|
|
88
|
+
if (typeof raw === "number") return raw;
|
|
89
|
+
const asString = String(raw).trim();
|
|
90
|
+
return asString || undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
75
93
|
/**
|
|
76
94
|
* 从 XML 中提取消息内容
|
|
77
95
|
*/
|
package/src/types/account.ts
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
* WeCom 账号类型定义
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
WecomBotConfig,
|
|
7
|
+
WecomAgentConfig,
|
|
8
|
+
WecomDmConfig,
|
|
9
|
+
WecomNetworkConfig,
|
|
10
|
+
WecomAccountConfig,
|
|
11
|
+
} from "./config.js";
|
|
6
12
|
|
|
7
13
|
/**
|
|
8
14
|
* 解析后的 Bot 账号
|
|
@@ -14,9 +20,9 @@ export type ResolvedBotAccount = {
|
|
|
14
20
|
enabled: boolean;
|
|
15
21
|
/** 是否配置完整 */
|
|
16
22
|
configured: boolean;
|
|
17
|
-
/** 回调 Token */
|
|
23
|
+
/** 回调 Token (webhook 模式) */
|
|
18
24
|
token: string;
|
|
19
|
-
/** 回调加密密钥 */
|
|
25
|
+
/** 回调加密密钥 (webhook 模式) */
|
|
20
26
|
encodingAESKey: string;
|
|
21
27
|
/** 接收者 ID */
|
|
22
28
|
receiveId: string;
|
|
@@ -24,6 +30,15 @@ export type ResolvedBotAccount = {
|
|
|
24
30
|
config: WecomBotConfig;
|
|
25
31
|
/** 网络配置(来自 channels.wecom.network) */
|
|
26
32
|
network?: WecomNetworkConfig;
|
|
33
|
+
|
|
34
|
+
// --- 长链接模式 (WebSocket) ---
|
|
35
|
+
|
|
36
|
+
/** 连接模式 */
|
|
37
|
+
connectionMode: 'webhook' | 'websocket';
|
|
38
|
+
/** 机器人 BotID(websocket 模式下有值) */
|
|
39
|
+
botId?: string;
|
|
40
|
+
/** 机器人 Secret(websocket 模式下有值) */
|
|
41
|
+
secret?: string;
|
|
27
42
|
};
|
|
28
43
|
|
|
29
44
|
/**
|
|
@@ -40,8 +55,8 @@ export type ResolvedAgentAccount = {
|
|
|
40
55
|
corpId: string;
|
|
41
56
|
/** 应用 Secret */
|
|
42
57
|
corpSecret: string;
|
|
43
|
-
/** 应用 ID (
|
|
44
|
-
agentId
|
|
58
|
+
/** 应用 ID (数字,可选) */
|
|
59
|
+
agentId?: number;
|
|
45
60
|
/** 回调 Token */
|
|
46
61
|
token: string;
|
|
47
62
|
/** 回调加密密钥 */
|
|
@@ -52,23 +67,46 @@ export type ResolvedAgentAccount = {
|
|
|
52
67
|
network?: WecomNetworkConfig;
|
|
53
68
|
};
|
|
54
69
|
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
*/
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
|
|
70
|
+
/** Matrix/Legacy 的统一账号解析结果 */
|
|
71
|
+
export type ResolvedWecomAccount = {
|
|
72
|
+
/** 账号 ID(用于 bindings.match.accountId) */
|
|
73
|
+
accountId: string;
|
|
74
|
+
/** 展示名称 */
|
|
75
|
+
name?: string;
|
|
76
|
+
/** 是否启用 */
|
|
77
|
+
enabled: boolean;
|
|
78
|
+
/** 是否具备至少一种可用能力(bot/agent) */
|
|
79
|
+
configured: boolean;
|
|
80
|
+
/** 原始账号配置(Matrix 条目或 Legacy 聚合) */
|
|
81
|
+
config: WecomAccountConfig;
|
|
82
|
+
/** Bot 能力 */
|
|
83
|
+
bot?: ResolvedBotAccount;
|
|
84
|
+
/** Agent 能力 */
|
|
85
|
+
agent?: ResolvedAgentAccount;
|
|
63
86
|
};
|
|
64
87
|
|
|
88
|
+
/** 解析模式 */
|
|
89
|
+
export type ResolvedMode = "disabled" | "legacy" | "matrix";
|
|
90
|
+
|
|
65
91
|
/**
|
|
66
|
-
*
|
|
92
|
+
* 已解析的模式状态
|
|
67
93
|
*/
|
|
68
94
|
export type ResolvedWecomAccounts = {
|
|
69
|
-
/**
|
|
95
|
+
/** 当前模式 */
|
|
96
|
+
mode: ResolvedMode;
|
|
97
|
+
/** 默认账号 ID */
|
|
98
|
+
defaultAccountId: string;
|
|
99
|
+
/** 账号集合(Legacy 下仅 default) */
|
|
100
|
+
accounts: Record<string, ResolvedWecomAccount>;
|
|
101
|
+
/**
|
|
102
|
+
* 向后兼容:默认账号的 bot(历史调用点仍可读取)。
|
|
103
|
+
* Matrix 下等价于 defaultAccountId 对应账号的 bot。
|
|
104
|
+
*/
|
|
70
105
|
bot?: ResolvedBotAccount;
|
|
71
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* 向后兼容:默认账号的 agent(历史调用点仍可读取)。
|
|
108
|
+
* Matrix 下等价于 defaultAccountId 对应账号的 agent。
|
|
109
|
+
*/
|
|
72
110
|
agent?: ResolvedAgentAccount;
|
|
73
111
|
};
|
|
74
112
|
|
package/src/types/config.ts
CHANGED
|
@@ -30,15 +30,33 @@ export type WecomNetworkConfig = {
|
|
|
30
30
|
egressProxyUrl?: string;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
/** 路由行为配置 */
|
|
34
|
+
export type WecomRoutingConfig = {
|
|
35
|
+
/**
|
|
36
|
+
* 当路由未命中 bindings(matchedBy=default)时是否拒绝继续处理。
|
|
37
|
+
* - true: fail-closed(推荐于多账号)
|
|
38
|
+
* - false: 允许回退默认 agent(历史兼容)
|
|
39
|
+
*/
|
|
40
|
+
failClosedOnDefaultRoute?: boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
33
43
|
/**
|
|
34
44
|
* Bot 模式配置 (智能体)
|
|
35
45
|
* 用于接收 JSON 格式回调 + 流式回复
|
|
36
46
|
*/
|
|
37
47
|
export type WecomBotConfig = {
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
|
|
48
|
+
/** 智能机器人 ID(用于 Matrix 模式二次身份确认,webhook 模式) */
|
|
49
|
+
aibotid?: string;
|
|
50
|
+
/** 回调 Token (企微后台生成,webhook 模式必填) */
|
|
51
|
+
token?: string;
|
|
52
|
+
/** 回调加密密钥 (企微后台生成,webhook 模式必填) */
|
|
53
|
+
encodingAESKey?: string;
|
|
54
|
+
/**
|
|
55
|
+
* BotId 列表(可选,用于审计与告警)。
|
|
56
|
+
* - 回调路由优先由 URL + 签名决定;botIds 不参与强制拦截。
|
|
57
|
+
* - 当解密后的 aibotid 不在 botIds 中时,仅记录告警日志。
|
|
58
|
+
*/
|
|
59
|
+
botIds?: string[];
|
|
42
60
|
/** 接收者 ID (可选,用于解密校验) */
|
|
43
61
|
receiveId?: string;
|
|
44
62
|
/** 流式消息占位符 */
|
|
@@ -47,6 +65,15 @@ export type WecomBotConfig = {
|
|
|
47
65
|
welcomeText?: string;
|
|
48
66
|
/** DM 策略 */
|
|
49
67
|
dm?: WecomDmConfig;
|
|
68
|
+
|
|
69
|
+
// --- 长链接模式 (WebSocket) ---
|
|
70
|
+
|
|
71
|
+
/** 连接模式:webhook(默认)或 websocket */
|
|
72
|
+
connectionMode?: 'webhook' | 'websocket';
|
|
73
|
+
/** 机器人 BotID(websocket 模式必填,企微后台获取) */
|
|
74
|
+
botId?: string;
|
|
75
|
+
/** 机器人 Secret(websocket 模式必填,企微后台获取) */
|
|
76
|
+
secret?: string;
|
|
50
77
|
};
|
|
51
78
|
|
|
52
79
|
/**
|
|
@@ -58,8 +85,8 @@ export type WecomAgentConfig = {
|
|
|
58
85
|
corpId: string;
|
|
59
86
|
/** 应用 Secret */
|
|
60
87
|
corpSecret: string;
|
|
61
|
-
/** 应用 ID */
|
|
62
|
-
agentId
|
|
88
|
+
/** 应用 ID(可选;不填时可接收回调,但主动发送需具备该字段) */
|
|
89
|
+
agentId?: number | string;
|
|
63
90
|
/** 回调 Token (企微后台「设置API接收」) */
|
|
64
91
|
token: string;
|
|
65
92
|
/** 回调加密密钥 (企微后台「设置API接收」) */
|
|
@@ -93,10 +120,27 @@ export type WecomConfig = {
|
|
|
93
120
|
bot?: WecomBotConfig;
|
|
94
121
|
/** Agent 模式配置 (自建应用) */
|
|
95
122
|
agent?: WecomAgentConfig;
|
|
123
|
+
/**
|
|
124
|
+
* 多账号配置(每个账号可包含 bot + agent,作为一组)。
|
|
125
|
+
* accountId 用于与 OpenClaw `bindings[].match.accountId` 对齐,从而把不同 WeCom 账号路由到不同 OpenClaw agent。
|
|
126
|
+
*/
|
|
127
|
+
accounts?: Record<string, WecomAccountConfig>;
|
|
128
|
+
/** 默认账号(可选) */
|
|
129
|
+
defaultAccount?: string;
|
|
96
130
|
/** 媒体处理配置 */
|
|
97
131
|
media?: WecomMediaConfig;
|
|
98
132
|
/** 网络配置 */
|
|
99
133
|
network?: WecomNetworkConfig;
|
|
134
|
+
/** 路由配置 */
|
|
135
|
+
routing?: WecomRoutingConfig;
|
|
100
136
|
/** 动态 Agent 配置 */
|
|
101
137
|
dynamicAgents?: WecomDynamicAgentsConfig;
|
|
102
138
|
};
|
|
139
|
+
|
|
140
|
+
/** Matrix 账号条目 */
|
|
141
|
+
export type WecomAccountConfig = {
|
|
142
|
+
enabled?: boolean;
|
|
143
|
+
name?: string;
|
|
144
|
+
bot?: WecomBotConfig;
|
|
145
|
+
agent?: WecomAgentConfig;
|
|
146
|
+
};
|
package/src/types/constants.ts
CHANGED
|
@@ -4,12 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
/** 固定 Webhook 路径 */
|
|
6
6
|
export const WEBHOOK_PATHS = {
|
|
7
|
-
/** Bot
|
|
7
|
+
/** Bot 模式历史兼容路径(不再维护) */
|
|
8
8
|
BOT: "/wecom",
|
|
9
|
-
/** Bot
|
|
9
|
+
/** Bot 模式历史备用兼容路径(不再维护) */
|
|
10
10
|
BOT_ALT: "/wecom/bot",
|
|
11
|
-
/** Agent
|
|
11
|
+
/** Agent 模式历史兼容路径(不再维护) */
|
|
12
12
|
AGENT: "/wecom/agent",
|
|
13
|
+
/** Bot 模式推荐路径前缀 */
|
|
14
|
+
BOT_PLUGIN: "/plugins/wecom/bot",
|
|
15
|
+
/** Agent 模式推荐路径前缀 */
|
|
16
|
+
AGENT_PLUGIN: "/plugins/wecom/agent",
|
|
13
17
|
} as const;
|
|
14
18
|
|
|
15
19
|
/** 企业微信 API 端点 */
|
package/src/types/index.ts
CHANGED
|
@@ -7,9 +7,11 @@ export * from "./constants.js";
|
|
|
7
7
|
|
|
8
8
|
// 配置类型
|
|
9
9
|
export type {
|
|
10
|
+
WecomAccountConfig,
|
|
10
11
|
WecomDmConfig,
|
|
11
12
|
WecomMediaConfig,
|
|
12
13
|
WecomNetworkConfig,
|
|
14
|
+
WecomRoutingConfig,
|
|
13
15
|
WecomBotConfig,
|
|
14
16
|
WecomAgentConfig,
|
|
15
17
|
WecomConfig,
|
|
@@ -17,6 +19,7 @@ export type {
|
|
|
17
19
|
|
|
18
20
|
// 账号类型
|
|
19
21
|
export type {
|
|
22
|
+
ResolvedWecomAccount,
|
|
20
23
|
ResolvedBotAccount,
|
|
21
24
|
ResolvedAgentAccount,
|
|
22
25
|
ResolvedMode,
|