@mocrane/wecom 2026.2.5
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 +21 -0
- package/README.md +0 -0
- package/clawdbot.plugin.json +10 -0
- package/index.ts +28 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +81 -0
- package/src/accounts.ts +72 -0
- package/src/agent/api-client.ts +336 -0
- package/src/agent/handler.ts +566 -0
- package/src/agent/index.ts +12 -0
- package/src/channel.ts +259 -0
- package/src/config/accounts.ts +99 -0
- package/src/config/index.ts +12 -0
- package/src/config/media.ts +14 -0
- package/src/config/network.ts +16 -0
- package/src/config/schema.ts +104 -0
- package/src/config-schema.ts +41 -0
- package/src/crypto/aes.ts +108 -0
- package/src/crypto/index.ts +24 -0
- package/src/crypto/signature.ts +43 -0
- package/src/crypto/xml.ts +49 -0
- package/src/crypto.test.ts +32 -0
- package/src/crypto.ts +176 -0
- package/src/http.ts +102 -0
- package/src/media.test.ts +55 -0
- package/src/media.ts +55 -0
- package/src/monitor/state.queue.test.ts +185 -0
- package/src/monitor/state.ts +514 -0
- package/src/monitor/types.ts +136 -0
- package/src/monitor.active.test.ts +239 -0
- package/src/monitor.integration.test.ts +207 -0
- package/src/monitor.ts +1802 -0
- package/src/monitor.webhook.test.ts +311 -0
- package/src/onboarding.ts +472 -0
- package/src/outbound.test.ts +143 -0
- package/src/outbound.ts +200 -0
- package/src/runtime.ts +14 -0
- package/src/shared/command-auth.ts +101 -0
- package/src/shared/index.ts +5 -0
- package/src/shared/xml-parser.test.ts +30 -0
- package/src/shared/xml-parser.ts +183 -0
- package/src/target.ts +80 -0
- package/src/types/account.ts +76 -0
- package/src/types/config.ts +88 -0
- package/src/types/constants.ts +42 -0
- package/src/types/global.d.ts +9 -0
- package/src/types/index.ts +38 -0
- package/src/types/message.ts +185 -0
- package/src/types.ts +159 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeCom 配置向导 (Onboarding)
|
|
3
|
+
* 支持 Bot、Agent 和双模式同时启动的交互式配置流程
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ChannelOnboardingAdapter,
|
|
8
|
+
ChannelOnboardingDmPolicy,
|
|
9
|
+
OpenClawConfig,
|
|
10
|
+
WizardPrompter,
|
|
11
|
+
} from "openclaw/plugin-sdk";
|
|
12
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
|
|
13
|
+
import type { WecomConfig, WecomBotConfig, WecomAgentConfig, WecomDmConfig } from "./types/index.js";
|
|
14
|
+
|
|
15
|
+
const channel = "wecom" as const;
|
|
16
|
+
|
|
17
|
+
type WecomMode = "bot" | "agent" | "both";
|
|
18
|
+
|
|
19
|
+
// ============================================================
|
|
20
|
+
// 辅助函数
|
|
21
|
+
// ============================================================
|
|
22
|
+
|
|
23
|
+
function getWecomConfig(cfg: OpenClawConfig): WecomConfig | undefined {
|
|
24
|
+
return cfg.channels?.wecom as WecomConfig | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function setWecomEnabled(cfg: OpenClawConfig, enabled: boolean): OpenClawConfig {
|
|
28
|
+
return {
|
|
29
|
+
...cfg,
|
|
30
|
+
channels: {
|
|
31
|
+
...cfg.channels,
|
|
32
|
+
wecom: {
|
|
33
|
+
...(cfg.channels?.wecom ?? {}),
|
|
34
|
+
enabled,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
} as OpenClawConfig;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function setWecomBotConfig(cfg: OpenClawConfig, bot: WecomBotConfig): OpenClawConfig {
|
|
41
|
+
return {
|
|
42
|
+
...cfg,
|
|
43
|
+
channels: {
|
|
44
|
+
...cfg.channels,
|
|
45
|
+
wecom: {
|
|
46
|
+
...(cfg.channels?.wecom ?? {}),
|
|
47
|
+
enabled: true,
|
|
48
|
+
bot,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
} as OpenClawConfig;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function setWecomAgentConfig(cfg: OpenClawConfig, agent: WecomAgentConfig): OpenClawConfig {
|
|
55
|
+
return {
|
|
56
|
+
...cfg,
|
|
57
|
+
channels: {
|
|
58
|
+
...cfg.channels,
|
|
59
|
+
wecom: {
|
|
60
|
+
...(cfg.channels?.wecom ?? {}),
|
|
61
|
+
enabled: true,
|
|
62
|
+
agent,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
} as OpenClawConfig;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function setGatewayBindLan(cfg: OpenClawConfig): OpenClawConfig {
|
|
69
|
+
return {
|
|
70
|
+
...cfg,
|
|
71
|
+
gateway: {
|
|
72
|
+
...(cfg.gateway ?? {}),
|
|
73
|
+
bind: "lan",
|
|
74
|
+
},
|
|
75
|
+
} as OpenClawConfig;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function setWecomDmPolicy(
|
|
79
|
+
cfg: OpenClawConfig,
|
|
80
|
+
mode: "bot" | "agent",
|
|
81
|
+
dm: WecomDmConfig,
|
|
82
|
+
): OpenClawConfig {
|
|
83
|
+
const wecom = getWecomConfig(cfg) ?? {};
|
|
84
|
+
if (mode === "bot") {
|
|
85
|
+
return {
|
|
86
|
+
...cfg,
|
|
87
|
+
channels: {
|
|
88
|
+
...cfg.channels,
|
|
89
|
+
wecom: {
|
|
90
|
+
...wecom,
|
|
91
|
+
bot: {
|
|
92
|
+
...wecom.bot,
|
|
93
|
+
dm,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
} as OpenClawConfig;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
...cfg,
|
|
101
|
+
channels: {
|
|
102
|
+
...cfg.channels,
|
|
103
|
+
wecom: {
|
|
104
|
+
...wecom,
|
|
105
|
+
agent: {
|
|
106
|
+
...wecom.agent,
|
|
107
|
+
dm,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
} as OpenClawConfig;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ============================================================
|
|
115
|
+
// 欢迎与引导
|
|
116
|
+
// ============================================================
|
|
117
|
+
|
|
118
|
+
async function showWelcome(prompter: WizardPrompter): Promise<void> {
|
|
119
|
+
await prompter.note(
|
|
120
|
+
[
|
|
121
|
+
"🚀 欢迎使用企业微信(WeCom)接入向导",
|
|
122
|
+
"本插件支持「智能体 Bot」与「自建应用 Agent」双模式并行。",
|
|
123
|
+
].join("\n"),
|
|
124
|
+
"WeCom 配置向导",
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ============================================================
|
|
129
|
+
// 模式选择
|
|
130
|
+
// ============================================================
|
|
131
|
+
|
|
132
|
+
async function promptMode(prompter: WizardPrompter): Promise<WecomMode> {
|
|
133
|
+
const choice = await prompter.select({
|
|
134
|
+
message: "请选择您要配置的接入模式:",
|
|
135
|
+
options: [
|
|
136
|
+
{
|
|
137
|
+
value: "bot",
|
|
138
|
+
label: "Bot 模式 (智能机器人)",
|
|
139
|
+
hint: "回调速度快,支持流式占位符,适合日常对话",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
value: "agent",
|
|
143
|
+
label: "Agent 模式 (自建应用)",
|
|
144
|
+
hint: "功能最全,支持 API 主动推送、发送文件/视频、交互卡片",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
value: "both",
|
|
148
|
+
label: "双模式 (Bot + Agent 同时启用)",
|
|
149
|
+
hint: "推荐:Bot 用于快速对话,Agent 用于主动推送和媒体发送",
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
initialValue: "both",
|
|
153
|
+
});
|
|
154
|
+
return choice as WecomMode;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ============================================================
|
|
158
|
+
// Bot 模式配置
|
|
159
|
+
// ============================================================
|
|
160
|
+
|
|
161
|
+
async function configureBotMode(
|
|
162
|
+
cfg: OpenClawConfig,
|
|
163
|
+
prompter: WizardPrompter,
|
|
164
|
+
): Promise<OpenClawConfig> {
|
|
165
|
+
await prompter.note(
|
|
166
|
+
[
|
|
167
|
+
"正在配置 Bot 模式...",
|
|
168
|
+
"",
|
|
169
|
+
"💡 操作指南: 请在企微后台【管理工具 -> 智能机器人】开启 API 模式。",
|
|
170
|
+
"🔗 回调 URL: https://您的域名/wecom/bot",
|
|
171
|
+
"",
|
|
172
|
+
"请先在后台填入回调 URL,然后获取以下信息。",
|
|
173
|
+
].join("\n"),
|
|
174
|
+
"Bot 模式配置",
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const token = String(
|
|
178
|
+
await prompter.text({
|
|
179
|
+
message: "请输入 Token:",
|
|
180
|
+
validate: (value: string | undefined) => (value?.trim() ? undefined : "Token 不能为空"),
|
|
181
|
+
}),
|
|
182
|
+
).trim();
|
|
183
|
+
|
|
184
|
+
const encodingAESKey = String(
|
|
185
|
+
await prompter.text({
|
|
186
|
+
message: "请输入 EncodingAESKey:",
|
|
187
|
+
validate: (value: string | undefined) => {
|
|
188
|
+
const v = value?.trim() ?? "";
|
|
189
|
+
if (!v) return "EncodingAESKey 不能为空";
|
|
190
|
+
if (v.length !== 43) return "EncodingAESKey 应为 43 个字符";
|
|
191
|
+
return undefined;
|
|
192
|
+
},
|
|
193
|
+
}),
|
|
194
|
+
).trim();
|
|
195
|
+
|
|
196
|
+
const streamPlaceholder = await prompter.text({
|
|
197
|
+
message: "流式占位符 (可选):",
|
|
198
|
+
placeholder: "正在思考...",
|
|
199
|
+
initialValue: "正在思考...",
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const welcomeText = await prompter.text({
|
|
203
|
+
message: "欢迎语 (可选):",
|
|
204
|
+
placeholder: "你好!我是 AI 助手",
|
|
205
|
+
initialValue: "你好!我是 AI 助手",
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const botConfig: WecomBotConfig = {
|
|
209
|
+
token,
|
|
210
|
+
encodingAESKey,
|
|
211
|
+
streamPlaceholderContent: streamPlaceholder?.trim() || undefined,
|
|
212
|
+
welcomeText: welcomeText?.trim() || undefined,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
return setWecomBotConfig(cfg, botConfig);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ============================================================
|
|
219
|
+
// Agent 模式配置
|
|
220
|
+
// ============================================================
|
|
221
|
+
|
|
222
|
+
async function configureAgentMode(
|
|
223
|
+
cfg: OpenClawConfig,
|
|
224
|
+
prompter: WizardPrompter,
|
|
225
|
+
): Promise<OpenClawConfig> {
|
|
226
|
+
await prompter.note(
|
|
227
|
+
[
|
|
228
|
+
"正在配置 Agent 模式...",
|
|
229
|
+
"",
|
|
230
|
+
"💡 操作指南: 请在企微后台【应用管理 -> 自建应用】创建应用。",
|
|
231
|
+
].join("\n"),
|
|
232
|
+
"Agent 模式配置",
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const corpId = String(
|
|
236
|
+
await prompter.text({
|
|
237
|
+
message: "请输入 CorpID (企业ID):",
|
|
238
|
+
validate: (value: string | undefined) => (value?.trim() ? undefined : "CorpID 不能为空"),
|
|
239
|
+
}),
|
|
240
|
+
).trim();
|
|
241
|
+
|
|
242
|
+
const agentIdStr = String(
|
|
243
|
+
await prompter.text({
|
|
244
|
+
message: "请输入 AgentID (应用ID):",
|
|
245
|
+
validate: (value: string | undefined) => {
|
|
246
|
+
const v = value?.trim() ?? "";
|
|
247
|
+
if (!v) return "AgentID 不能为空";
|
|
248
|
+
if (!/^\d+$/.test(v)) return "AgentID 应为数字";
|
|
249
|
+
return undefined;
|
|
250
|
+
},
|
|
251
|
+
}),
|
|
252
|
+
).trim();
|
|
253
|
+
const agentId = Number(agentIdStr);
|
|
254
|
+
|
|
255
|
+
const corpSecret = String(
|
|
256
|
+
await prompter.text({
|
|
257
|
+
message: "请输入 Secret (应用密钥):",
|
|
258
|
+
validate: (value: string | undefined) => (value?.trim() ? undefined : "Secret 不能为空"),
|
|
259
|
+
}),
|
|
260
|
+
).trim();
|
|
261
|
+
|
|
262
|
+
await prompter.note(
|
|
263
|
+
[
|
|
264
|
+
"💡 操作指南: 请在自建应用详情页进入【接收消息 -> 设置API接收】。",
|
|
265
|
+
"🔗 回调 URL: https://您的域名/wecom/agent",
|
|
266
|
+
"",
|
|
267
|
+
"请先在后台填入回调 URL,然后获取以下信息。",
|
|
268
|
+
].join("\n"),
|
|
269
|
+
"回调配置",
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const token = String(
|
|
273
|
+
await prompter.text({
|
|
274
|
+
message: "请输入 Token (回调令牌):",
|
|
275
|
+
validate: (value: string | undefined) => (value?.trim() ? undefined : "Token 不能为空"),
|
|
276
|
+
}),
|
|
277
|
+
).trim();
|
|
278
|
+
|
|
279
|
+
const encodingAESKey = String(
|
|
280
|
+
await prompter.text({
|
|
281
|
+
message: "请输入 EncodingAESKey (回调加密密钥):",
|
|
282
|
+
validate: (value: string | undefined) => {
|
|
283
|
+
const v = value?.trim() ?? "";
|
|
284
|
+
if (!v) return "EncodingAESKey 不能为空";
|
|
285
|
+
if (v.length !== 43) return "EncodingAESKey 应为 43 个字符";
|
|
286
|
+
return undefined;
|
|
287
|
+
},
|
|
288
|
+
}),
|
|
289
|
+
).trim();
|
|
290
|
+
|
|
291
|
+
const welcomeText = await prompter.text({
|
|
292
|
+
message: "欢迎语 (可选):",
|
|
293
|
+
placeholder: "欢迎使用智能助手",
|
|
294
|
+
initialValue: "欢迎使用智能助手",
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const agentConfig: WecomAgentConfig = {
|
|
298
|
+
corpId,
|
|
299
|
+
corpSecret,
|
|
300
|
+
agentId,
|
|
301
|
+
token,
|
|
302
|
+
encodingAESKey,
|
|
303
|
+
welcomeText: welcomeText?.trim() || undefined,
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
return setWecomAgentConfig(cfg, agentConfig);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ============================================================
|
|
310
|
+
// DM 策略配置
|
|
311
|
+
// ============================================================
|
|
312
|
+
|
|
313
|
+
async function promptDmPolicy(
|
|
314
|
+
cfg: OpenClawConfig,
|
|
315
|
+
prompter: WizardPrompter,
|
|
316
|
+
modes: ("bot" | "agent")[],
|
|
317
|
+
): Promise<OpenClawConfig> {
|
|
318
|
+
const policyChoice = await prompter.select({
|
|
319
|
+
message: "请选择私聊 (DM) 访问策略:",
|
|
320
|
+
options: [
|
|
321
|
+
{ value: "pairing", label: "配对模式", hint: "推荐:安全,未知用户需授权" },
|
|
322
|
+
{ value: "allowlist", label: "白名单模式", hint: "仅允许特定 UserID" },
|
|
323
|
+
{ value: "open", label: "开放模式", hint: "任何人可发起" },
|
|
324
|
+
{ value: "disabled", label: "禁用私聊", hint: "不接受私聊消息" },
|
|
325
|
+
],
|
|
326
|
+
initialValue: "pairing",
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const policy = policyChoice as "pairing" | "allowlist" | "open" | "disabled";
|
|
330
|
+
let allowFrom: string[] | undefined;
|
|
331
|
+
|
|
332
|
+
if (policy === "allowlist") {
|
|
333
|
+
const allowFromStr = String(
|
|
334
|
+
await prompter.text({
|
|
335
|
+
message: "请输入白名单 UserID (多个用逗号分隔):",
|
|
336
|
+
placeholder: "user1,user2",
|
|
337
|
+
validate: (value: string | undefined) => (value?.trim() ? undefined : "请输入至少一个 UserID"),
|
|
338
|
+
}),
|
|
339
|
+
).trim();
|
|
340
|
+
allowFrom = allowFromStr.split(",").map((s) => s.trim()).filter(Boolean);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const dm: WecomDmConfig = { policy, allowFrom };
|
|
344
|
+
|
|
345
|
+
let result = cfg;
|
|
346
|
+
for (const mode of modes) {
|
|
347
|
+
result = setWecomDmPolicy(result, mode, dm);
|
|
348
|
+
}
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ============================================================
|
|
353
|
+
// 配置汇总
|
|
354
|
+
// ============================================================
|
|
355
|
+
|
|
356
|
+
async function showSummary(cfg: OpenClawConfig, prompter: WizardPrompter): Promise<void> {
|
|
357
|
+
const wecom = getWecomConfig(cfg);
|
|
358
|
+
const lines: string[] = ["✅ 配置已保存!", ""];
|
|
359
|
+
|
|
360
|
+
if (wecom?.bot?.token) {
|
|
361
|
+
lines.push("📱 Bot 模式: 已配置");
|
|
362
|
+
lines.push(` 回调 URL: https://您的域名/wecom/bot`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (wecom?.agent?.corpId) {
|
|
366
|
+
lines.push("🏢 Agent 模式: 已配置");
|
|
367
|
+
lines.push(` 回调 URL: https://您的域名/wecom/agent`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
lines.push("");
|
|
371
|
+
lines.push("⚠️ 请确保您已在企微后台填写了正确的回调 URL,");
|
|
372
|
+
lines.push(" 并点击了后台的『保存』按钮完成验证。");
|
|
373
|
+
|
|
374
|
+
await prompter.note(lines.join("\n"), "配置完成");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ============================================================
|
|
378
|
+
// DM Policy Adapter
|
|
379
|
+
// ============================================================
|
|
380
|
+
|
|
381
|
+
const dmPolicy: ChannelOnboardingDmPolicy = {
|
|
382
|
+
label: "WeCom",
|
|
383
|
+
channel,
|
|
384
|
+
policyKey: "channels.wecom.bot.dm.policy",
|
|
385
|
+
allowFromKey: "channels.wecom.bot.dm.allowFrom",
|
|
386
|
+
getCurrent: (cfg: OpenClawConfig) => {
|
|
387
|
+
const wecom = getWecomConfig(cfg);
|
|
388
|
+
return (wecom?.bot?.dm?.policy ?? "pairing") as "pairing";
|
|
389
|
+
},
|
|
390
|
+
setPolicy: (cfg: OpenClawConfig, policy: "pairing" | "allowlist" | "open" | "disabled") => {
|
|
391
|
+
return setWecomDmPolicy(cfg, "bot", { policy });
|
|
392
|
+
},
|
|
393
|
+
promptAllowFrom: async ({ cfg, prompter }: { cfg: OpenClawConfig; prompter: WizardPrompter }) => {
|
|
394
|
+
const allowFromStr = String(
|
|
395
|
+
await prompter.text({
|
|
396
|
+
message: "请输入白名单 UserID:",
|
|
397
|
+
validate: (value: string | undefined) => (value?.trim() ? undefined : "请输入 UserID"),
|
|
398
|
+
}),
|
|
399
|
+
).trim();
|
|
400
|
+
const allowFrom = allowFromStr.split(",").map((s) => s.trim()).filter(Boolean);
|
|
401
|
+
return setWecomDmPolicy(cfg, "bot", { policy: "allowlist", allowFrom });
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// ============================================================
|
|
406
|
+
// Onboarding Adapter
|
|
407
|
+
// ============================================================
|
|
408
|
+
|
|
409
|
+
export const wecomOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
410
|
+
channel,
|
|
411
|
+
dmPolicy,
|
|
412
|
+
getStatus: async ({ cfg }: { cfg: OpenClawConfig }) => {
|
|
413
|
+
const wecom = getWecomConfig(cfg);
|
|
414
|
+
const botConfigured = Boolean(wecom?.bot?.token && wecom?.bot?.encodingAESKey);
|
|
415
|
+
const agentConfigured = Boolean(
|
|
416
|
+
wecom?.agent?.corpId && wecom?.agent?.corpSecret && wecom?.agent?.agentId,
|
|
417
|
+
);
|
|
418
|
+
const configured = botConfigured || agentConfigured;
|
|
419
|
+
|
|
420
|
+
const statusParts: string[] = [];
|
|
421
|
+
if (botConfigured) statusParts.push("Bot ✓");
|
|
422
|
+
if (agentConfigured) statusParts.push("Agent ✓");
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
channel,
|
|
426
|
+
configured,
|
|
427
|
+
statusLines: [
|
|
428
|
+
`WeCom: ${configured ? statusParts.join(" + ") : "需要配置"}`,
|
|
429
|
+
],
|
|
430
|
+
selectionHint: configured
|
|
431
|
+
? `configured · ${statusParts.join(" + ")}`
|
|
432
|
+
: "enterprise-ready · dual-mode",
|
|
433
|
+
quickstartScore: configured ? 1 : 8,
|
|
434
|
+
};
|
|
435
|
+
},
|
|
436
|
+
configure: async ({ cfg, prompter }: { cfg: OpenClawConfig; prompter: WizardPrompter }) => {
|
|
437
|
+
// 1. 欢迎
|
|
438
|
+
await showWelcome(prompter);
|
|
439
|
+
|
|
440
|
+
// 2. 模式选择
|
|
441
|
+
const mode = await promptMode(prompter);
|
|
442
|
+
|
|
443
|
+
let next = cfg;
|
|
444
|
+
const configuredModes: ("bot" | "agent")[] = [];
|
|
445
|
+
|
|
446
|
+
// 3. 配置 Bot
|
|
447
|
+
if (mode === "bot" || mode === "both") {
|
|
448
|
+
next = await configureBotMode(next, prompter);
|
|
449
|
+
configuredModes.push("bot");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// 4. 配置 Agent
|
|
453
|
+
if (mode === "agent" || mode === "both") {
|
|
454
|
+
next = await configureAgentMode(next, prompter);
|
|
455
|
+
configuredModes.push("agent");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// 5. DM 策略
|
|
459
|
+
next = await promptDmPolicy(next, prompter, configuredModes);
|
|
460
|
+
|
|
461
|
+
// 6. 启用通道
|
|
462
|
+
next = setWecomEnabled(next, true);
|
|
463
|
+
|
|
464
|
+
// 7. 设置 gateway.bind 为 lan(允许外部访问回调)
|
|
465
|
+
next = setGatewayBindLan(next);
|
|
466
|
+
|
|
467
|
+
// 8. 汇总
|
|
468
|
+
await showSummary(next, prompter);
|
|
469
|
+
|
|
470
|
+
return { cfg: next, accountId: DEFAULT_ACCOUNT_ID };
|
|
471
|
+
},
|
|
472
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("./agent/api-client.js", () => ({
|
|
4
|
+
sendText: vi.fn(),
|
|
5
|
+
sendMedia: vi.fn(),
|
|
6
|
+
uploadMedia: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
describe("wecomOutbound", () => {
|
|
10
|
+
it("does not crash when called with core outbound params", async () => {
|
|
11
|
+
const { wecomOutbound } = await import("./outbound.js");
|
|
12
|
+
await expect(
|
|
13
|
+
wecomOutbound.sendMedia({
|
|
14
|
+
cfg: {},
|
|
15
|
+
to: "wr-test-chat",
|
|
16
|
+
text: "caption",
|
|
17
|
+
mediaUrl: "https://example.com/media.png",
|
|
18
|
+
} as any),
|
|
19
|
+
).rejects.toThrow(/Agent mode/i);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("routes sendText to agent chatId/userid", async () => {
|
|
23
|
+
const { wecomOutbound } = await import("./outbound.js");
|
|
24
|
+
const api = await import("./agent/api-client.js");
|
|
25
|
+
const now = vi.spyOn(Date, "now").mockReturnValue(123);
|
|
26
|
+
(api.sendText as any).mockResolvedValue(undefined);
|
|
27
|
+
|
|
28
|
+
const cfg = {
|
|
29
|
+
channels: {
|
|
30
|
+
wecom: {
|
|
31
|
+
enabled: true,
|
|
32
|
+
agent: {
|
|
33
|
+
corpId: "corp",
|
|
34
|
+
corpSecret: "secret",
|
|
35
|
+
agentId: 1000002,
|
|
36
|
+
token: "token",
|
|
37
|
+
encodingAESKey: "aes",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Chat ID (wr/wc) is intentionally NOT supported for Agent outbound.
|
|
44
|
+
await expect(wecomOutbound.sendText({ cfg, to: "wr123", text: "hello" } as any)).rejects.toThrow(
|
|
45
|
+
/不支持向群 chatId 发送/,
|
|
46
|
+
);
|
|
47
|
+
expect(api.sendText).not.toHaveBeenCalled();
|
|
48
|
+
|
|
49
|
+
// Test: User ID (Default)
|
|
50
|
+
const userResult = await wecomOutbound.sendText({
|
|
51
|
+
cfg,
|
|
52
|
+
to: "userid123",
|
|
53
|
+
text: "hi",
|
|
54
|
+
} as any);
|
|
55
|
+
expect(api.sendText).toHaveBeenCalledWith(
|
|
56
|
+
expect.objectContaining({
|
|
57
|
+
chatId: undefined,
|
|
58
|
+
toUser: "userid123",
|
|
59
|
+
toParty: undefined,
|
|
60
|
+
toTag: undefined,
|
|
61
|
+
text: "hi",
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
expect(userResult.messageId).toBe("agent-123");
|
|
65
|
+
|
|
66
|
+
(api.sendText as any).mockClear();
|
|
67
|
+
|
|
68
|
+
// Test: User ID explicit
|
|
69
|
+
await wecomOutbound.sendText({ cfg, to: "user:zhangsan", text: "hi" } as any);
|
|
70
|
+
expect(api.sendText).toHaveBeenCalledWith(
|
|
71
|
+
expect.objectContaining({ toUser: "zhangsan", toParty: undefined }),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
(api.sendText as any).mockClear();
|
|
75
|
+
|
|
76
|
+
// Test: Party ID (Numeric)
|
|
77
|
+
await wecomOutbound.sendText({ cfg, to: "1001", text: "hi party" } as any);
|
|
78
|
+
expect(api.sendText).toHaveBeenCalledWith(
|
|
79
|
+
expect.objectContaining({ toUser: undefined, toParty: "1001" }),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
(api.sendText as any).mockClear();
|
|
83
|
+
|
|
84
|
+
// Test: Party ID Explicit
|
|
85
|
+
await wecomOutbound.sendText({ cfg, to: "party:2002", text: "hi party 2" } as any);
|
|
86
|
+
expect(api.sendText).toHaveBeenCalledWith(
|
|
87
|
+
expect.objectContaining({ toUser: undefined, toParty: "2002" }),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
(api.sendText as any).mockClear();
|
|
91
|
+
|
|
92
|
+
// Test: Tag ID Explicit
|
|
93
|
+
await wecomOutbound.sendText({ cfg, to: "tag:1", text: "hi tag" } as any);
|
|
94
|
+
expect(api.sendText).toHaveBeenCalledWith(
|
|
95
|
+
expect.objectContaining({ toUser: undefined, toTag: "1" }),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
now.mockRestore();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("suppresses /new ack for bot sessions but not agent sessions", async () => {
|
|
102
|
+
const { wecomOutbound } = await import("./outbound.js");
|
|
103
|
+
const api = await import("./agent/api-client.js");
|
|
104
|
+
const now = vi.spyOn(Date, "now").mockReturnValue(456);
|
|
105
|
+
(api.sendText as any).mockResolvedValue(undefined);
|
|
106
|
+
(api.sendText as any).mockClear();
|
|
107
|
+
|
|
108
|
+
const cfg = {
|
|
109
|
+
channels: {
|
|
110
|
+
wecom: {
|
|
111
|
+
enabled: true,
|
|
112
|
+
agent: {
|
|
113
|
+
corpId: "corp",
|
|
114
|
+
corpSecret: "secret",
|
|
115
|
+
agentId: 1000002,
|
|
116
|
+
token: "token",
|
|
117
|
+
encodingAESKey: "aes",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const ack = "✅ New session started · model: openai-codex/gpt-5.2";
|
|
124
|
+
|
|
125
|
+
// Bot 会话(wecom:...)应抑制,避免私信回执
|
|
126
|
+
const r1 = await wecomOutbound.sendText({ cfg, to: "wecom:userid123", text: ack } as any);
|
|
127
|
+
expect(api.sendText).not.toHaveBeenCalled();
|
|
128
|
+
expect(r1.messageId).toBe("suppressed-456");
|
|
129
|
+
|
|
130
|
+
(api.sendText as any).mockClear();
|
|
131
|
+
|
|
132
|
+
// Agent 会话(wecom-agent:...)允许发送回执
|
|
133
|
+
await wecomOutbound.sendText({ cfg, to: "wecom-agent:userid123", text: ack } as any);
|
|
134
|
+
expect(api.sendText).toHaveBeenCalledWith(
|
|
135
|
+
expect.objectContaining({
|
|
136
|
+
toUser: "userid123",
|
|
137
|
+
text: "✅ 已开启新会话(模型:openai-codex/gpt-5.2)",
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
now.mockRestore();
|
|
142
|
+
});
|
|
143
|
+
});
|