/**
* WeCom 配置向导 (Onboarding)
* 支持 Bot、Agent 和双模式同时启动的交互式配置流程
*/
import type {
ChannelOnboardingAdapter,
ChannelOnboardingDmPolicy,
OpenClawConfig,
WizardPrompter,
} from "openclaw/plugin-sdk";
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
import type { WecomConfig, WecomBotConfig, WecomAgentConfig, WecomDmConfig } from "./types/index.js";
const channel = "wecom" as const;
type WecomMode = "bot" | "agent" | "both";
// ============================================================
// 辅助函数
// ============================================================
function getWecomConfig(cfg: OpenClawConfig): WecomConfig | undefined {
return cfg.channels?.wecom as WecomConfig | undefined;
}
function setWecomEnabled(cfg: OpenClawConfig, enabled: boolean): OpenClawConfig {
return {
...cfg,
channels: {
...cfg.channels,
wecom: {
...(cfg.channels?.wecom ?? {}),
enabled,
},
},
} as OpenClawConfig;
}
function setWecomBotConfig(cfg: OpenClawConfig, bot: WecomBotConfig): OpenClawConfig {
return {
...cfg,
channels: {
...cfg.channels,
wecom: {
...(cfg.channels?.wecom ?? {}),
enabled: true,
bot,
},
},
} as OpenClawConfig;
}
function setWecomAgentConfig(cfg: OpenClawConfig, agent: WecomAgentConfig): OpenClawConfig {
return {
...cfg,
channels: {
...cfg.channels,
wecom: {
...(cfg.channels?.wecom ?? {}),
enabled: true,
agent,
},
},
} as OpenClawConfig;
}
function setGatewayBindLan(cfg: OpenClawConfig): OpenClawConfig {
return {
...cfg,
gateway: {
...(cfg.gateway ?? {}),
bind: "lan",
},
} as OpenClawConfig;
}
function setWecomDmPolicy(
cfg: OpenClawConfig,
mode: "bot" | "agent",
dm: WecomDmConfig,
): OpenClawConfig {
const wecom = getWecomConfig(cfg) ?? {};
if (mode === "bot") {
return {
...cfg,
channels: {
...cfg.channels,
wecom: {
...wecom,
bot: {
...wecom.bot,
dm,
},
},
},
} as OpenClawConfig;
}
return {
...cfg,
channels: {
...cfg.channels,
wecom: {
...wecom,
agent: {
...wecom.agent,
dm,
},
},
},
} as OpenClawConfig;
}
// ============================================================
// 欢迎与引导
// ============================================================
async function showWelcome(prompter: WizardPrompter): Promise<void> {
await prompter.note(
[
"🚀 欢迎使用企业微信(WeCom)接入向导",
"本插件支持「智能体 Bot」与「自建应用 Agent」双模式并行。",
].join("\n"),
"WeCom 配置向导",
);
}
// ============================================================
// 模式选择
// ============================================================
async function promptMode(prompter: WizardPrompter): Promise<WecomMode> {
const choice = await prompter.select({
message: "请选择您要配置的接入模式:",
options: [
{
value: "bot",
label: "Bot 模式 (智能机器人)",
hint: "回调速度快,支持流式占位符,适合日常对话",
},
{
value: "agent",
label: "Agent 模式 (自建应用)",
hint: "功能最全,支持 API 主动推送、发送文件/视频、交互卡片",
},
{
value: "both",
label: "双模式 (Bot + Agent 同时启用)",
hint: "推荐:Bot 用于快速对话,Agent 用于主动推送和媒体发送",
},
],
initialValue: "both",
});
return choice as WecomMode;
}
// ============================================================
// Bot 模式配置
// ============================================================
async function configureBotMode(
cfg: OpenClawConfig,
prompter: WizardPrompter,
): Promise<OpenClawConfig> {
await prompter.note(
[
"正在配置 Bot 模式...",
"",
"💡 操作指南: 请在企微后台【管理工具 -> 智能机器人】开启 API 模式。",
"🔗 回调 URL: https://您的域名/wecom/bot",
"",
"请先在后台填入回调 URL,然后获取以下信息。",
].join("\n"),
"Bot 模式配置",
);
const token = String(
await prompter.text({
message: "请输入 Token:",
validate: (value: string | undefined) => (value?.trim() ? undefined : "Token 不能为空"),
}),
).trim();
const encodingAESKey = String(
await prompter.text({
message: "请输入 EncodingAESKey:",
validate: (value: string | undefined) => {
const v = value?.trim() ?? "";
if (!v) return "EncodingAESKey 不能为空";
if (v.length !== 43) return "EncodingAESKey 应为 43 个字符";
return undefined;
},
}),
).trim();
const streamPlaceholder = await prompter.text({
message: "流式占位符 (可选):",
placeholder: "正在思考...",
initialValue: "正在思考...",
});
const welcomeText = await prompter.text({
message: "欢迎语 (可选):",
placeholder: "你好!我是 AI 助手",
initialValue: "你好!我是 AI 助手",
});
const botConfig: WecomBotConfig = {
token,
encodingAESKey,
streamPlaceholderContent: streamPlaceholder?.trim() || undefined,
welcomeText: welcomeText?.trim() || undefined,
};
return setWecomBotConfig(cfg, botConfig);
}
// ============================================================
// Agent 模式配置
// ============================================================
async function configureAgentMode(
cfg: OpenClawConfig,
prompter: WizardPrompter,
): Promise<OpenClawConfig> {
await prompter.note(
[
"正在配置 Agent 模式...",
"",
"💡 操作指南: 请在企微后台【应用管理 -> 自建应用】创建应用。",
].join("\n"),
"Agent 模式配置",
);
const corpId = String(
await prompter.text({
message: "请输入 CorpID (企业ID):",
validate: (value: string | undefined) => (value?.trim() ? undefined : "CorpID 不能为空"),
}),
).trim();
const agentIdStr = String(
await prompter.text({
message: "请输入 AgentID (应用ID):",
validate: (value: string | undefined) => {
const v = value?.trim() ?? "";
if (!v) return "AgentID 不能为空";
if (!/^\d+$/.test(v)) return "AgentID 应为数字";
return undefined;
},
}),
).trim();
const agentId = Number(agentIdStr);
const corpSecret = String(
await prompter.text({
message: "请输入 Secret (应用密钥):",
validate: (value: string | undefined) => (value?.trim() ? undefined : "Secret 不能为空"),
}),
).trim();
await prompter.note(
[
"💡 操作指南: 请在自建应用详情页进入【接收消息 -> 设置API接收】。",
"🔗 回调 URL: https://您的域名/wecom/agent",
"",
"请先在后台填入回调 URL,然后获取以下信息。",
].join("\n"),
"回调配置",
);
const token = String(
await prompter.text({
message: "请输入 Token (回调令牌):",
validate: (value: string | undefined) => (value?.trim() ? undefined : "Token 不能为空"),
}),
).trim();
const encodingAESKey = String(
await prompter.text({
message: "请输入 EncodingAESKey (回调加密密钥):",
validate: (value: string | undefined) => {
const v = value?.trim() ?? "";
if (!v) return "EncodingAESKey 不能为空";
if (v.length !== 43) return "EncodingAESKey 应为 43 个字符";
return undefined;
},
}),
).trim();
const welcomeText = await prompter.text({
message: "欢迎语 (可选):",
placeholder: "欢迎使用智能助手",
initialValue: "欢迎使用智能助手",
});
const agentConfig: WecomAgentConfig = {
corpId,
corpSecret,
agentId,
token,
encodingAESKey,
welcomeText: welcomeText?.trim() || undefined,
};
return setWecomAgentConfig(cfg, agentConfig);
}
// ============================================================
// DM 策略配置
// ============================================================
async function promptDmPolicy(
cfg: OpenClawConfig,
prompter: WizardPrompter,
modes: ("bot" | "agent")[],
): Promise<OpenClawConfig> {
const policyChoice = await prompter.select({
message: "请选择私聊 (DM) 访问策略:",
options: [
{ value: "pairing", label: "配对模式", hint: "推荐:安全,未知用户需授权" },
{ value: "allowlist", label: "白名单模式", hint: "仅允许特定 UserID" },
{ value: "open", label: "开放模式", hint: "任何人可发起" },
{ value: "disabled", label: "禁用私聊", hint: "不接受私聊消息" },
],
initialValue: "pairing",
});
const policy = policyChoice as "pairing" | "allowlist" | "open" | "disabled";
let allowFrom: string[] | undefined;
if (policy === "allowlist") {
const allowFromStr = String(
await prompter.text({
message: "请输入白名单 UserID (多个用逗号分隔):",
placeholder: "user1,user2",
validate: (value: string | undefined) => (value?.trim() ? undefined : "请输入至少一个 UserID"),
}),
).trim();
allowFrom = allowFromStr.split(",").map((s) => s.trim()).filter(Boolean);
}
const dm: WecomDmConfig = { policy, allowFrom };
let result = cfg;
for (const mode of modes) {
result = setWecomDmPolicy(result, mode, dm);
}
return result;
}
// ============================================================
// 配置汇总
// ============================================================
async function showSummary(cfg: OpenClawConfig, prompter: WizardPrompter): Promise<void> {
const wecom = getWecomConfig(cfg);
const lines: string[] = ["✅ 配置已保存!", ""];
if (wecom?.bot?.token) {
lines.push("📱 Bot 模式: 已配置");
lines.push(` 回调 URL: https://您的域名/wecom/bot`);
}
if (wecom?.agent?.corpId) {
lines.push("🏢 Agent 模式: 已配置");
lines.push(` 回调 URL: https://您的域名/wecom/agent`);
}
lines.push("");
lines.push("⚠️ 请确保您已在企微后台填写了正确的回调 URL,");
lines.push(" 并点击了后台的『保存』按钮完成验证。");
await prompter.note(lines.join("\n"), "配置完成");
}
// ============================================================
// DM Policy Adapter
// ============================================================
const dmPolicy: ChannelOnboardingDmPolicy = {
label: "WeCom",
channel,
policyKey: "channels.wecom.bot.dm.policy",
allowFromKey: "channels.wecom.bot.dm.allowFrom",
getCurrent: (cfg: OpenClawConfig) => {
const wecom = getWecomConfig(cfg);
return (wecom?.bot?.dm?.policy ?? "pairing") as "pairing";
},
setPolicy: (cfg: OpenClawConfig, policy: "pairing" | "allowlist" | "open" | "disabled") => {
return setWecomDmPolicy(cfg, "bot", { policy });
},
promptAllowFrom: async ({ cfg, prompter }: { cfg: OpenClawConfig; prompter: WizardPrompter }) => {
const allowFromStr = String(
await prompter.text({
message: "请输入白名单 UserID:",
validate: (value: string | undefined) => (value?.trim() ? undefined : "请输入 UserID"),
}),
).trim();
const allowFrom = allowFromStr.split(",").map((s) => s.trim()).filter(Boolean);
return setWecomDmPolicy(cfg, "bot", { policy: "allowlist", allowFrom });
},
};
// ============================================================
// Onboarding Adapter
// ============================================================
export const wecomOnboardingAdapter: ChannelOnboardingAdapter = {
channel,
dmPolicy,
getStatus: async ({ cfg }: { cfg: OpenClawConfig }) => {
const wecom = getWecomConfig(cfg);
const botConfigured = Boolean(wecom?.bot?.token && wecom?.bot?.encodingAESKey);
const agentConfigured = Boolean(
wecom?.agent?.corpId && wecom?.agent?.corpSecret && wecom?.agent?.agentId,
);
const configured = botConfigured || agentConfigured;
const statusParts: string[] = [];
if (botConfigured) statusParts.push("Bot ✓");
if (agentConfigured) statusParts.push("Agent ✓");
return {
channel,
configured,
statusLines: [
`WeCom: ${configured ? statusParts.join(" + ") : "需要配置"}`,
],
selectionHint: configured
? `configured · ${statusParts.join(" + ")}`
: "enterprise-ready · dual-mode",
quickstartScore: configured ? 1 : 8,
};
},
configure: async ({ cfg, prompter }: { cfg: OpenClawConfig; prompter: WizardPrompter }) => {
// 1. 欢迎
await showWelcome(prompter);
// 2. 模式选择
const mode = await promptMode(prompter);
let next = cfg;
const configuredModes: ("bot" | "agent")[] = [];
// 3. 配置 Bot
if (mode === "bot" || mode === "both") {
next = await configureBotMode(next, prompter);
configuredModes.push("bot");
}
// 4. 配置 Agent
if (mode === "agent" || mode === "both") {
next = await configureAgentMode(next, prompter);
configuredModes.push("agent");
}
// 5. DM 策略
next = await promptDmPolicy(next, prompter, configuredModes);
// 6. 启用通道
next = setWecomEnabled(next, true);
// 7. 设置 gateway.bind 为 lan(允许外部访问回调)
next = setGatewayBindLan(next);
// 8. 汇总
await showSummary(next, prompter);
return { cfg: next, accountId: DEFAULT_ACCOUNT_ID };
},
};