@dingxiang-me/openclaw-wechat 2.0.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -0
- package/README.en.md +204 -32
- package/README.md +234 -63
- package/docs/channels/wecom.md +137 -1
- package/openclaw.plugin.json +694 -10
- package/package.json +207 -4
- package/scripts/wecom-agent-selfcheck.mjs +775 -0
- package/scripts/wecom-bot-longconn-probe.mjs +582 -0
- package/scripts/wecom-bot-selfcheck.mjs +952 -0
- package/scripts/wecom-callback-matrix.mjs +224 -0
- package/scripts/wecom-doctor.mjs +1407 -0
- package/scripts/wecom-e2e-scenario.mjs +333 -0
- package/scripts/wecom-migrate.mjs +261 -0
- package/scripts/wecom-quickstart.mjs +1824 -0
- package/scripts/wecom-release-check.mjs +232 -0
- package/scripts/wecom-remote-e2e.mjs +310 -0
- package/scripts/wecom-selfcheck.mjs +1255 -0
- package/scripts/wecom-smoke.sh +74 -0
- package/src/core/delivery-router.js +21 -0
- package/src/core.js +631 -34
- package/src/wecom/account-config-core.js +27 -1
- package/src/wecom/account-config.js +19 -2
- package/src/wecom/agent-dispatch-executor.js +11 -0
- package/src/wecom/agent-dispatch-handlers.js +61 -8
- package/src/wecom/agent-inbound-guards.js +63 -16
- package/src/wecom/agent-inbound-processor.js +34 -2
- package/src/wecom/agent-late-reply-runtime.js +30 -2
- package/src/wecom/agent-text-sender.js +2 -0
- package/src/wecom/api-client-core.js +27 -19
- package/src/wecom/api-client-media.js +16 -7
- package/src/wecom/api-client-send-text.js +4 -0
- package/src/wecom/api-client-send-typed.js +4 -1
- package/src/wecom/api-client-senders.js +41 -3
- package/src/wecom/api-client.js +1 -0
- package/src/wecom/bot-dispatch-fallback.js +18 -3
- package/src/wecom/bot-dispatch-handlers.js +47 -10
- package/src/wecom/bot-inbound-dispatch-runtime.js +3 -0
- package/src/wecom/bot-inbound-executor-helpers.js +11 -1
- package/src/wecom/bot-inbound-executor.js +25 -1
- package/src/wecom/bot-inbound-guards.js +78 -23
- package/src/wecom/bot-long-connection-manager.js +4 -4
- package/src/wecom/channel-config-schema.js +132 -0
- package/src/wecom/channel-plugin.js +370 -7
- package/src/wecom/command-handlers.js +107 -10
- package/src/wecom/command-status-text.js +275 -1
- package/src/wecom/doc-client.js +7 -1
- package/src/wecom/inbound-content-handler-file-video-link.js +4 -0
- package/src/wecom/inbound-content-handler-image-voice.js +6 -0
- package/src/wecom/inbound-content.js +5 -0
- package/src/wecom/installer-api.js +910 -0
- package/src/wecom/media-download.js +2 -2
- package/src/wecom/migration-diagnostics.js +816 -0
- package/src/wecom/network-config.js +91 -0
- package/src/wecom/observability-metrics.js +9 -3
- package/src/wecom/outbound-agent-delivery.js +313 -0
- package/src/wecom/outbound-agent-media-sender.js +37 -7
- package/src/wecom/outbound-agent-push.js +1 -0
- package/src/wecom/outbound-delivery.js +129 -12
- package/src/wecom/outbound-stream-msg-item.js +25 -2
- package/src/wecom/outbound-webhook-delivery.js +19 -0
- package/src/wecom/outbound-webhook-media.js +30 -6
- package/src/wecom/pairing.js +188 -0
- package/src/wecom/pending-reply-manager.js +143 -0
- package/src/wecom/plugin-account-policy-services.js +26 -0
- package/src/wecom/plugin-base-services.js +58 -0
- package/src/wecom/plugin-constants.js +1 -1
- package/src/wecom/plugin-delivery-inbound-services.js +25 -0
- package/src/wecom/plugin-processing-deps.js +7 -0
- package/src/wecom/plugin-route-runtime-deps.js +1 -0
- package/src/wecom/plugin-services.js +87 -0
- package/src/wecom/policy-resolvers.js +93 -20
- package/src/wecom/quickstart-metadata.js +1247 -0
- package/src/wecom/reasoning-visibility.js +104 -0
- package/src/wecom/register-runtime.js +10 -0
- package/src/wecom/reliable-delivery-persistence.js +138 -0
- package/src/wecom/reliable-delivery.js +642 -0
- package/src/wecom/reply-output-policy.js +171 -0
- package/src/wecom/text-inbound-scheduler.js +6 -1
- package/src/wecom/workspace-auto-sender.js +2 -0
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { normalizeAccountId } from "./account-config-core.js";
|
|
5
|
+
import { collectWecomMigrationDiagnostics } from "./migration-diagnostics.js";
|
|
6
|
+
import {
|
|
7
|
+
buildWecomQuickstartSetupPlan,
|
|
8
|
+
WECOM_DOCTOR_COMMAND,
|
|
9
|
+
WECOM_QUICKSTART_DEFAULT_GROUP_PROFILE,
|
|
10
|
+
WECOM_QUICKSTART_MIGRATION_COMMAND,
|
|
11
|
+
WECOM_QUICKSTART_RECOMMENDED_MODE,
|
|
12
|
+
WECOM_QUICKSTART_SETUP_COMMAND,
|
|
13
|
+
WECOM_QUICKSTART_WIZARD_COMMAND,
|
|
14
|
+
} from "./quickstart-metadata.js";
|
|
15
|
+
|
|
16
|
+
export const WECOM_PLUGIN_ENTRY_ID = "openclaw-wechat";
|
|
17
|
+
export const WECOM_PLUGIN_NPM_SPEC = "@dingxiang-me/openclaw-wechat";
|
|
18
|
+
export const WECOM_INSTALLER_NPM_SPEC = "@dingxiang-me/openclaw-wecom-cli";
|
|
19
|
+
export const WECOM_INSTALLER_COMMAND = "npx -y @dingxiang-me/openclaw-wecom-cli install";
|
|
20
|
+
export const WECOM_INSTALLER_SOURCE_OPTIONS = Object.freeze([
|
|
21
|
+
"auto",
|
|
22
|
+
"official-wecom",
|
|
23
|
+
"sunnoy-wecom",
|
|
24
|
+
"legacy-openclaw-wechat",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
function uniqueStrings(values = []) {
|
|
28
|
+
return Array.from(
|
|
29
|
+
new Set(
|
|
30
|
+
values
|
|
31
|
+
.map((item) => String(item ?? "").trim())
|
|
32
|
+
.filter(Boolean),
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function uniquePaths(values = []) {
|
|
38
|
+
return uniqueStrings(values);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function pickFirstNonEmptyString(...values) {
|
|
42
|
+
for (const value of values) {
|
|
43
|
+
const trimmed = String(value ?? "").trim();
|
|
44
|
+
if (trimmed) return trimmed;
|
|
45
|
+
}
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function dedupeActions(actions = []) {
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const action of actions) {
|
|
53
|
+
const id = String(action?.id ?? "").trim();
|
|
54
|
+
if (!id || seen.has(id)) continue;
|
|
55
|
+
seen.add(id);
|
|
56
|
+
out.push(action);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function dedupeCheckItems(items = []) {
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const item of items) {
|
|
65
|
+
const id = String(item?.id ?? "").trim();
|
|
66
|
+
if (!id || seen.has(id)) continue;
|
|
67
|
+
seen.add(id);
|
|
68
|
+
out.push(item);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function detectInstallerAccountCapabilities(config = {}, accountId = "default") {
|
|
74
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
75
|
+
const channelConfig = asObject(config?.channels?.wecom);
|
|
76
|
+
const configuredDefaultAccountId = normalizeAccountId(channelConfig?.defaultAccount ?? "default");
|
|
77
|
+
const accountConfig =
|
|
78
|
+
normalizedAccountId === "default"
|
|
79
|
+
? mergeDeep(channelConfig, asObject(channelConfig?.accounts?.[configuredDefaultAccountId]))
|
|
80
|
+
: asObject(channelConfig?.accounts?.[normalizedAccountId] ?? channelConfig?.[normalizedAccountId]);
|
|
81
|
+
const legacyAgent = asObject(accountConfig?.agent);
|
|
82
|
+
const bot = asObject(accountConfig?.bot);
|
|
83
|
+
const longConnection = asObject(bot?.longConnection);
|
|
84
|
+
const hasAgent =
|
|
85
|
+
Boolean(pickFirstNonEmptyString(accountConfig?.corpId, legacyAgent?.corpId)) &&
|
|
86
|
+
Boolean(pickFirstNonEmptyString(accountConfig?.corpSecret, legacyAgent?.corpSecret)) &&
|
|
87
|
+
Number.isFinite(Number(accountConfig?.agentId ?? legacyAgent?.agentId));
|
|
88
|
+
const hasBotLongConnection =
|
|
89
|
+
Boolean(pickFirstNonEmptyString(longConnection?.botId, longConnection?.botid, accountConfig?.botId, accountConfig?.botid)) &&
|
|
90
|
+
Boolean(pickFirstNonEmptyString(longConnection?.secret, accountConfig?.secret));
|
|
91
|
+
const hasBotWebhook =
|
|
92
|
+
Boolean(pickFirstNonEmptyString(bot?.token, bot?.callbackToken, accountConfig?.token)) &&
|
|
93
|
+
Boolean(pickFirstNonEmptyString(bot?.encodingAesKey, bot?.callbackAesKey, accountConfig?.encodingAesKey));
|
|
94
|
+
const hasGroupAllowlist =
|
|
95
|
+
String(accountConfig?.groupPolicy ?? accountConfig?.groupChat?.policy ?? "").trim().toLowerCase() === "allowlist" ||
|
|
96
|
+
(Array.isArray(accountConfig?.groupAllowFrom) && accountConfig.groupAllowFrom.length > 0);
|
|
97
|
+
return {
|
|
98
|
+
hasAgent,
|
|
99
|
+
hasBotLongConnection,
|
|
100
|
+
hasBotWebhook,
|
|
101
|
+
hasBot: hasBotLongConnection || hasBotWebhook,
|
|
102
|
+
hasGroupAllowlist,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildInstallerSourceCheckOrder({ source = "fresh", capabilities = {}, selectedMode = "bot_long_connection" } = {}) {
|
|
107
|
+
const checks = [];
|
|
108
|
+
const pushCheck = (id, title, detail) => {
|
|
109
|
+
checks.push({
|
|
110
|
+
id,
|
|
111
|
+
title,
|
|
112
|
+
detail,
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
pushCheck(
|
|
117
|
+
"doctor_offline",
|
|
118
|
+
"先跑离线 doctor",
|
|
119
|
+
"先验证安装结构、迁移结果和本地插件布局,不依赖公网回调或网络出口。",
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (source === "official-wecom") {
|
|
123
|
+
if (capabilities.hasBot || selectedMode !== "agent_callback") {
|
|
124
|
+
pushCheck("bot_selfcheck", "检查 Bot 基础配置", "优先确认扁平 Bot 配置已迁到当前结构。");
|
|
125
|
+
}
|
|
126
|
+
if (capabilities.hasBotLongConnection || selectedMode !== "agent_callback") {
|
|
127
|
+
pushCheck("bot_longconn_probe", "探测 Bot 长连接", "在真正收发消息前,先验证长连接握手和代理链路。");
|
|
128
|
+
}
|
|
129
|
+
if (capabilities.hasAgent || selectedMode !== "bot_long_connection") {
|
|
130
|
+
pushCheck("agent_selfcheck", "检查 Agent 配置", "如果来源里同时带了 Agent 能力,再确认 corpId/corpSecret/agentId。");
|
|
131
|
+
pushCheck("channel_selfcheck", "检查综合回包能力", "最后确认 WeCom 总体 readiness 和当前账号摘要。");
|
|
132
|
+
}
|
|
133
|
+
return dedupeCheckItems(checks);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (source === "sunnoy-wecom") {
|
|
137
|
+
if (capabilities.hasBot || selectedMode !== "agent_callback") {
|
|
138
|
+
pushCheck("bot_selfcheck", "检查 Bot 基础配置", "先确认 Bot 兼容字段已迁到当前结构。");
|
|
139
|
+
}
|
|
140
|
+
if (capabilities.hasAgent || selectedMode !== "bot_long_connection") {
|
|
141
|
+
pushCheck("agent_selfcheck", "检查 Agent 配置", "确认 Agent 兼容字段和主动发送能力都已保留。");
|
|
142
|
+
}
|
|
143
|
+
if (capabilities.hasBotLongConnection || selectedMode !== "agent_callback") {
|
|
144
|
+
pushCheck("bot_longconn_probe", "探测 Bot 长连接", "sunnoy 来源常带代理/出网配置,优先验证长连接网络。");
|
|
145
|
+
}
|
|
146
|
+
pushCheck("doctor_online", "最后跑联网 doctor", "把代理、apiBaseUrl、公网回调和真实网络探测一起验证。");
|
|
147
|
+
return dedupeCheckItems(checks);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (source === "legacy-openclaw-wechat") {
|
|
151
|
+
if (capabilities.hasAgent || selectedMode !== "bot_long_connection") {
|
|
152
|
+
pushCheck("agent_selfcheck", "检查 Agent 配置", "legacy 来源常含 agent.* 兼容块,先确认 Agent 已迁正。");
|
|
153
|
+
}
|
|
154
|
+
if (capabilities.hasBot || selectedMode !== "agent_callback") {
|
|
155
|
+
pushCheck("bot_selfcheck", "检查 Bot 基础配置", "确认旧版 Bot 字段已并到当前 bot.longConnection / bot.* 结构。");
|
|
156
|
+
}
|
|
157
|
+
if (capabilities.hasBotLongConnection || selectedMode === "bot_long_connection" || selectedMode === "hybrid") {
|
|
158
|
+
pushCheck("bot_longconn_probe", "探测 Bot 长连接", "如果保留了 Bot 长连接,再额外验证 websocket 侧是否正常。");
|
|
159
|
+
}
|
|
160
|
+
pushCheck("channel_selfcheck", "检查综合回包能力", "最后确认当前账号在迁移后仍能收、回、发。");
|
|
161
|
+
return dedupeCheckItems(checks);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (source === "mixed-source") {
|
|
165
|
+
if (capabilities.hasBot || selectedMode !== "agent_callback") {
|
|
166
|
+
pushCheck("bot_selfcheck", "检查 Bot 基础配置", "混合来源先分别确认 Bot 侧字段是否已经收口。");
|
|
167
|
+
}
|
|
168
|
+
if (capabilities.hasAgent || selectedMode !== "bot_long_connection") {
|
|
169
|
+
pushCheck("agent_selfcheck", "检查 Agent 配置", "混合来源还要单独确认 Agent 兼容字段没有漏迁。");
|
|
170
|
+
}
|
|
171
|
+
pushCheck("channel_selfcheck", "检查综合回包能力", "在继续落盘或上线前,先跑一次综合自检确认总体状态。");
|
|
172
|
+
return dedupeCheckItems(checks);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (selectedMode === "agent_callback") {
|
|
176
|
+
pushCheck("agent_selfcheck", "检查 Agent 配置", "确认 callback、Agent API 和当前账号配置都完整。");
|
|
177
|
+
pushCheck("channel_selfcheck", "检查综合回包能力", "最后确认当前账号 readiness。");
|
|
178
|
+
return dedupeCheckItems(checks);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (selectedMode === "hybrid") {
|
|
182
|
+
pushCheck("agent_selfcheck", "检查 Agent 配置", "先确认 Agent 主动发送和 callback 路径。");
|
|
183
|
+
pushCheck("bot_selfcheck", "检查 Bot 基础配置", "再确认 Bot 对话入口和 webhook/长连接结构。");
|
|
184
|
+
pushCheck("bot_longconn_probe", "探测 Bot 长连接", "如果启用了长连接,最后做一次 websocket 握手验证。");
|
|
185
|
+
pushCheck("channel_selfcheck", "检查综合回包能力", "最后确认双通道综合状态。");
|
|
186
|
+
return dedupeCheckItems(checks);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
pushCheck("bot_selfcheck", "检查 Bot 基础配置", "确认 Bot 长连接必填项、webhook 配置和当前账号摘要。");
|
|
190
|
+
pushCheck("bot_longconn_probe", "探测 Bot 长连接", "在真正发消息前先验证 websocket 握手。");
|
|
191
|
+
return dedupeCheckItems(checks);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildInstallerSourceRepairDefaults({ source = "fresh" } = {}) {
|
|
195
|
+
if (source === "official-wecom") {
|
|
196
|
+
return {
|
|
197
|
+
doctorFixMode: "auto",
|
|
198
|
+
preserveNetworkCompatibility: false,
|
|
199
|
+
removeLegacyFieldAliases: true,
|
|
200
|
+
preferOfflineDoctor: true,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (source === "sunnoy-wecom") {
|
|
204
|
+
return {
|
|
205
|
+
doctorFixMode: "confirm",
|
|
206
|
+
preserveNetworkCompatibility: true,
|
|
207
|
+
removeLegacyFieldAliases: true,
|
|
208
|
+
preferOfflineDoctor: false,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (source === "legacy-openclaw-wechat") {
|
|
212
|
+
return {
|
|
213
|
+
doctorFixMode: "auto",
|
|
214
|
+
preserveNetworkCompatibility: true,
|
|
215
|
+
removeLegacyFieldAliases: true,
|
|
216
|
+
preferOfflineDoctor: true,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (source === "mixed-source") {
|
|
220
|
+
return {
|
|
221
|
+
doctorFixMode: "confirm",
|
|
222
|
+
preserveNetworkCompatibility: true,
|
|
223
|
+
removeLegacyFieldAliases: false,
|
|
224
|
+
preferOfflineDoctor: true,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
doctorFixMode: "off",
|
|
229
|
+
preserveNetworkCompatibility: true,
|
|
230
|
+
removeLegacyFieldAliases: false,
|
|
231
|
+
preferOfflineDoctor: true,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function resolveInstallerSourceProfile({
|
|
236
|
+
requestedSource = "auto",
|
|
237
|
+
detectedSource = "fresh",
|
|
238
|
+
requestedMode = WECOM_QUICKSTART_RECOMMENDED_MODE,
|
|
239
|
+
requestedGroupProfile = WECOM_QUICKSTART_DEFAULT_GROUP_PROFILE,
|
|
240
|
+
requestedDmMode = "pairing",
|
|
241
|
+
modeExplicit = false,
|
|
242
|
+
groupProfileExplicit = false,
|
|
243
|
+
dmModeExplicit = false,
|
|
244
|
+
config = {},
|
|
245
|
+
accountId = "default",
|
|
246
|
+
} = {}) {
|
|
247
|
+
const source = String(requestedSource === "auto" ? detectedSource : requestedSource || detectedSource || "fresh");
|
|
248
|
+
const capabilities = detectInstallerAccountCapabilities(config, accountId);
|
|
249
|
+
const recommendedMode = capabilities.hasAgent && capabilities.hasBot
|
|
250
|
+
? "hybrid"
|
|
251
|
+
: capabilities.hasAgent
|
|
252
|
+
? "agent_callback"
|
|
253
|
+
: "bot_long_connection";
|
|
254
|
+
const selectedMode = modeExplicit ? requestedMode : recommendedMode;
|
|
255
|
+
const recommendedGroupProfile = capabilities.hasGroupAllowlist ? "allowlist_template" : WECOM_QUICKSTART_DEFAULT_GROUP_PROFILE;
|
|
256
|
+
const selectedGroupProfile = groupProfileExplicit ? requestedGroupProfile : recommendedGroupProfile;
|
|
257
|
+
const selectedDmMode = dmModeExplicit ? requestedDmMode : "pairing";
|
|
258
|
+
const checkOrder = buildInstallerSourceCheckOrder({
|
|
259
|
+
source,
|
|
260
|
+
capabilities,
|
|
261
|
+
selectedMode,
|
|
262
|
+
});
|
|
263
|
+
const repairDefaults = buildInstallerSourceRepairDefaults({ source });
|
|
264
|
+
const notes = [];
|
|
265
|
+
if (!modeExplicit && selectedMode !== requestedMode) {
|
|
266
|
+
notes.push(`未显式指定 mode,安装器按 ${source} 来源和现有能力选择了 ${selectedMode}。`);
|
|
267
|
+
}
|
|
268
|
+
if (!groupProfileExplicit && selectedGroupProfile !== requestedGroupProfile) {
|
|
269
|
+
notes.push(`检测到群白名单相关配置,默认切换到 ${selectedGroupProfile} 模板。`);
|
|
270
|
+
}
|
|
271
|
+
if (source === "official-wecom") {
|
|
272
|
+
notes.push("官方插件来源默认优先保留 Bot 长连接接入路径。");
|
|
273
|
+
} else if (source === "sunnoy-wecom") {
|
|
274
|
+
notes.push("sunnoy 来源会优先保留现有网络兼容字段和双通道能力。");
|
|
275
|
+
} else if (source === "legacy-openclaw-wechat") {
|
|
276
|
+
notes.push("legacy 来源会优先匹配旧版 agent/bot 组合能力,再决定安装模式。");
|
|
277
|
+
}
|
|
278
|
+
if (repairDefaults.doctorFixMode === "auto") {
|
|
279
|
+
notes.push("该来源默认允许 installer/doctor 自动应用本地迁移 patch。");
|
|
280
|
+
} else if (repairDefaults.doctorFixMode === "confirm") {
|
|
281
|
+
notes.push("该来源默认先保守输出迁移建议,不会直接附带 doctor --fix。");
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
source,
|
|
285
|
+
capabilities,
|
|
286
|
+
requestedMode,
|
|
287
|
+
requestedGroupProfile,
|
|
288
|
+
requestedDmMode,
|
|
289
|
+
recommendedMode,
|
|
290
|
+
recommendedGroupProfile,
|
|
291
|
+
recommendedDmMode: "pairing",
|
|
292
|
+
selectedMode,
|
|
293
|
+
selectedGroupProfile,
|
|
294
|
+
selectedDmMode,
|
|
295
|
+
checkOrder,
|
|
296
|
+
checkOrderSummary: checkOrder.map((item) => item.title).join(" -> "),
|
|
297
|
+
repairDefaults,
|
|
298
|
+
modeDerived: modeExplicit !== true,
|
|
299
|
+
groupProfileDerived: groupProfileExplicit !== true,
|
|
300
|
+
dmModeDerived: dmModeExplicit !== true,
|
|
301
|
+
notes,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function asObject(value) {
|
|
306
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function deepClone(value) {
|
|
310
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function mergeDeep(base, patch) {
|
|
314
|
+
if (Array.isArray(patch)) return patch.slice();
|
|
315
|
+
if (!patch || typeof patch !== "object") return patch;
|
|
316
|
+
const out = { ...asObject(base) };
|
|
317
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
318
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
319
|
+
out[key] = mergeDeep(asObject(base?.[key]), value);
|
|
320
|
+
} else if (Array.isArray(value)) {
|
|
321
|
+
out[key] = value.slice();
|
|
322
|
+
} else {
|
|
323
|
+
out[key] = value;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function valuesEqual(left, right) {
|
|
330
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function collectChangedPaths(baseValue, patchValue, prefix = "", out = []) {
|
|
334
|
+
if (Array.isArray(patchValue)) {
|
|
335
|
+
if (!valuesEqual(baseValue, patchValue) && prefix) out.push(prefix);
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
if (!patchValue || typeof patchValue !== "object") {
|
|
339
|
+
if (!valuesEqual(baseValue, patchValue) && prefix) out.push(prefix);
|
|
340
|
+
return out;
|
|
341
|
+
}
|
|
342
|
+
for (const [key, value] of Object.entries(patchValue)) {
|
|
343
|
+
const nextPrefix = prefix ? `${prefix}.${key}` : key;
|
|
344
|
+
const nextBase = baseValue && typeof baseValue === "object" ? baseValue[key] : undefined;
|
|
345
|
+
collectChangedPaths(nextBase, value, nextPrefix, out);
|
|
346
|
+
}
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function deleteNestedPath(target, dottedPath = "") {
|
|
351
|
+
const parts = String(dottedPath ?? "").split(".").filter(Boolean);
|
|
352
|
+
if (parts.length === 0) return;
|
|
353
|
+
const stack = [];
|
|
354
|
+
let cursor = target;
|
|
355
|
+
for (const part of parts.slice(0, -1)) {
|
|
356
|
+
if (!cursor || typeof cursor !== "object" || Array.isArray(cursor)) return;
|
|
357
|
+
stack.push([cursor, part]);
|
|
358
|
+
cursor = cursor[part];
|
|
359
|
+
}
|
|
360
|
+
if (!cursor || typeof cursor !== "object" || Array.isArray(cursor)) return;
|
|
361
|
+
delete cursor[parts.at(-1)];
|
|
362
|
+
for (let index = stack.length - 1; index >= 0; index -= 1) {
|
|
363
|
+
const [parent, key] = stack[index];
|
|
364
|
+
const child = parent?.[key];
|
|
365
|
+
if (child && typeof child === "object" && !Array.isArray(child) && Object.keys(child).length === 0) {
|
|
366
|
+
delete parent[key];
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function buildMigratedConfig(baseConfig = {}, patch = {}, legacyFields = []) {
|
|
372
|
+
const merged = mergeDeep(baseConfig, patch);
|
|
373
|
+
for (const field of Array.isArray(legacyFields) ? legacyFields : []) {
|
|
374
|
+
deleteNestedPath(merged, field?.path);
|
|
375
|
+
}
|
|
376
|
+
return merged;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function expandHome(p) {
|
|
380
|
+
if (!p) return p;
|
|
381
|
+
if (p === "~") return os.homedir();
|
|
382
|
+
if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
|
|
383
|
+
return p;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function buildBackupPath(configPath) {
|
|
387
|
+
return `${configPath}.bak-${Date.now()}`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function splitPathSegments(input) {
|
|
391
|
+
return String(input ?? "")
|
|
392
|
+
.split(".")
|
|
393
|
+
.flatMap((segment) => {
|
|
394
|
+
const parts = [];
|
|
395
|
+
const re = /([^[\]]+)|\[(\d+)\]/g;
|
|
396
|
+
let matched = null;
|
|
397
|
+
while ((matched = re.exec(segment))) {
|
|
398
|
+
if (matched[1]) parts.push(matched[1]);
|
|
399
|
+
else if (matched[2]) parts.push(Number.parseInt(matched[2], 10));
|
|
400
|
+
}
|
|
401
|
+
return parts;
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function getAtPath(value, pathText) {
|
|
406
|
+
let current = value;
|
|
407
|
+
for (const segment of splitPathSegments(pathText)) {
|
|
408
|
+
if (current == null) return undefined;
|
|
409
|
+
current = current?.[segment];
|
|
410
|
+
}
|
|
411
|
+
return current;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function setAtPath(target, pathText, nextValue) {
|
|
415
|
+
const segments = splitPathSegments(pathText);
|
|
416
|
+
if (segments.length === 0) return;
|
|
417
|
+
let current = target;
|
|
418
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
419
|
+
const segment = segments[index];
|
|
420
|
+
const nextSegment = segments[index + 1];
|
|
421
|
+
const existing = current?.[segment];
|
|
422
|
+
if (existing == null || typeof existing !== "object") {
|
|
423
|
+
current[segment] = typeof nextSegment === "number" ? [] : {};
|
|
424
|
+
}
|
|
425
|
+
current = current[segment];
|
|
426
|
+
}
|
|
427
|
+
current[segments.at(-1)] = nextValue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function normalizeGroupAllow(groupAllow) {
|
|
431
|
+
if (Array.isArray(groupAllow)) {
|
|
432
|
+
return groupAllow.map((item) => String(item ?? "").trim()).filter(Boolean);
|
|
433
|
+
}
|
|
434
|
+
return String(groupAllow ?? "")
|
|
435
|
+
.split(",")
|
|
436
|
+
.map((item) => item.trim())
|
|
437
|
+
.filter(Boolean);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function resolveStarterAccountConfig(starterConfig, accountId = "default") {
|
|
441
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
442
|
+
const channelConfig = asObject(starterConfig?.channels?.wecom);
|
|
443
|
+
if (normalizedAccountId === "default") return channelConfig;
|
|
444
|
+
if (!channelConfig.accounts || typeof channelConfig.accounts !== "object") {
|
|
445
|
+
channelConfig.accounts = {};
|
|
446
|
+
}
|
|
447
|
+
if (!channelConfig.accounts[normalizedAccountId] || typeof channelConfig.accounts[normalizedAccountId] !== "object") {
|
|
448
|
+
channelConfig.accounts[normalizedAccountId] = {};
|
|
449
|
+
}
|
|
450
|
+
return channelConfig.accounts[normalizedAccountId];
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function resolveMigrationAccountPatch(configPatch = {}, accountId = "default") {
|
|
454
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
455
|
+
const channelPatch = asObject(configPatch?.channels?.wecom);
|
|
456
|
+
const accountPatches = asObject(channelPatch?.accounts);
|
|
457
|
+
return asObject(accountPatches?.[normalizedAccountId]);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function mergeAccountPatchIntoStarterConfig(starterConfig, accountId = "default", accountPatch = {}) {
|
|
461
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
462
|
+
const channelConfig = asObject(starterConfig?.channels?.wecom);
|
|
463
|
+
if (normalizedAccountId === "default") {
|
|
464
|
+
starterConfig.channels.wecom = mergeDeep(channelConfig, accountPatch);
|
|
465
|
+
return starterConfig;
|
|
466
|
+
}
|
|
467
|
+
const targetAccountConfig = resolveStarterAccountConfig(starterConfig, normalizedAccountId);
|
|
468
|
+
channelConfig.accounts[normalizedAccountId] = mergeDeep(targetAccountConfig, accountPatch);
|
|
469
|
+
return starterConfig;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function buildPluginEnablePatch() {
|
|
473
|
+
return {
|
|
474
|
+
plugins: {
|
|
475
|
+
enabled: true,
|
|
476
|
+
allow: [WECOM_PLUGIN_ENTRY_ID],
|
|
477
|
+
entries: {
|
|
478
|
+
[WECOM_PLUGIN_ENTRY_ID]: {
|
|
479
|
+
enabled: true,
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function applyInstallerValuesToStarterConfig(starterConfig, accountId = "default", values = {}) {
|
|
487
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
488
|
+
const channelConfig = asObject(starterConfig?.channels?.wecom);
|
|
489
|
+
const targetAccountConfig = resolveStarterAccountConfig(starterConfig, normalizedAccountId);
|
|
490
|
+
|
|
491
|
+
if (values.corpId) targetAccountConfig.corpId = String(values.corpId).trim();
|
|
492
|
+
if (values.corpSecret) targetAccountConfig.corpSecret = String(values.corpSecret).trim();
|
|
493
|
+
if (values.agentId != null && values.agentId !== "") targetAccountConfig.agentId = Number(values.agentId);
|
|
494
|
+
if (values.callbackToken) targetAccountConfig.callbackToken = String(values.callbackToken).trim();
|
|
495
|
+
if (values.callbackAesKey) targetAccountConfig.callbackAesKey = String(values.callbackAesKey).trim();
|
|
496
|
+
if (values.webhookPath) targetAccountConfig.webhookPath = String(values.webhookPath).trim();
|
|
497
|
+
if (values.apiBaseUrl) targetAccountConfig.apiBaseUrl = String(values.apiBaseUrl).trim();
|
|
498
|
+
if (values.outboundProxy) targetAccountConfig.outboundProxy = String(values.outboundProxy).trim();
|
|
499
|
+
|
|
500
|
+
if (values.botId || values.botSecret) {
|
|
501
|
+
channelConfig.bot = asObject(channelConfig.bot);
|
|
502
|
+
channelConfig.bot.enabled = true;
|
|
503
|
+
channelConfig.bot.longConnection = asObject(channelConfig.bot.longConnection);
|
|
504
|
+
channelConfig.bot.longConnection.enabled = true;
|
|
505
|
+
if (values.botId) channelConfig.bot.longConnection.botId = String(values.botId).trim();
|
|
506
|
+
if (values.botSecret) channelConfig.bot.longConnection.secret = String(values.botSecret).trim();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (normalizedAccountId !== "default") {
|
|
510
|
+
targetAccountConfig.bot = asObject(targetAccountConfig.bot);
|
|
511
|
+
targetAccountConfig.bot.longConnection = asObject(targetAccountConfig.bot.longConnection);
|
|
512
|
+
if (values.botId) {
|
|
513
|
+
targetAccountConfig.bot.enabled = true;
|
|
514
|
+
targetAccountConfig.bot.longConnection.enabled = true;
|
|
515
|
+
targetAccountConfig.bot.longConnection.botId = String(values.botId).trim();
|
|
516
|
+
}
|
|
517
|
+
if (values.botSecret) {
|
|
518
|
+
targetAccountConfig.bot.enabled = true;
|
|
519
|
+
targetAccountConfig.bot.longConnection.enabled = true;
|
|
520
|
+
targetAccountConfig.bot.longConnection.secret = String(values.botSecret).trim();
|
|
521
|
+
}
|
|
522
|
+
if (values.botWebhookToken) {
|
|
523
|
+
targetAccountConfig.bot.token = String(values.botWebhookToken).trim();
|
|
524
|
+
}
|
|
525
|
+
if (values.botEncodingAesKey) {
|
|
526
|
+
targetAccountConfig.bot.encodingAesKey = String(values.botEncodingAesKey).trim();
|
|
527
|
+
}
|
|
528
|
+
if (values.botWebhookPath) {
|
|
529
|
+
targetAccountConfig.bot.webhookPath = String(values.botWebhookPath).trim();
|
|
530
|
+
}
|
|
531
|
+
} else {
|
|
532
|
+
if (values.botWebhookToken) {
|
|
533
|
+
channelConfig.bot = asObject(channelConfig.bot);
|
|
534
|
+
channelConfig.bot.enabled = true;
|
|
535
|
+
channelConfig.bot.token = String(values.botWebhookToken).trim();
|
|
536
|
+
}
|
|
537
|
+
if (values.botEncodingAesKey) {
|
|
538
|
+
channelConfig.bot = asObject(channelConfig.bot);
|
|
539
|
+
channelConfig.bot.enabled = true;
|
|
540
|
+
channelConfig.bot.encodingAesKey = String(values.botEncodingAesKey).trim();
|
|
541
|
+
}
|
|
542
|
+
if (values.botWebhookPath) {
|
|
543
|
+
channelConfig.bot = asObject(channelConfig.bot);
|
|
544
|
+
channelConfig.bot.enabled = true;
|
|
545
|
+
channelConfig.bot.webhookPath = String(values.botWebhookPath).trim();
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return starterConfig;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function resolveRemainingPlaceholders(placeholders = [], starterConfig = {}) {
|
|
553
|
+
return placeholders.filter((item) => valuesEqual(getAtPath(starterConfig, item.path), item.currentValue));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function buildWecomInstallerMigrationGuide({
|
|
557
|
+
requestedSource = "auto",
|
|
558
|
+
detectedSource = "unknown",
|
|
559
|
+
effectiveSource = "unknown",
|
|
560
|
+
sourceSummary = "",
|
|
561
|
+
sourceSignals = [],
|
|
562
|
+
sourceMismatch = false,
|
|
563
|
+
} = {}) {
|
|
564
|
+
const source = String(detectedSource || effectiveSource || "unknown");
|
|
565
|
+
const title =
|
|
566
|
+
source === "official-wecom"
|
|
567
|
+
? "官方 WeCom 插件迁移"
|
|
568
|
+
: source === "sunnoy-wecom"
|
|
569
|
+
? "sunnoy WeCom 插件迁移"
|
|
570
|
+
: source === "legacy-openclaw-wechat"
|
|
571
|
+
? "旧版 OpenClaw-Wechat 迁移"
|
|
572
|
+
: source === "mixed-source"
|
|
573
|
+
? "混合来源 WeCom 迁移"
|
|
574
|
+
: source === "native-openclaw-wechat"
|
|
575
|
+
? "当前已是原生 OpenClaw-Wechat 布局"
|
|
576
|
+
: source === "fresh"
|
|
577
|
+
? "首次安装"
|
|
578
|
+
: "WeCom 配置迁移";
|
|
579
|
+
const notes = [];
|
|
580
|
+
if (source === "official-wecom") {
|
|
581
|
+
notes.push("已识别官方插件常见的扁平 Bot 配置写法,安装器会归一化到当前 bot/accounts 结构。");
|
|
582
|
+
} else if (source === "sunnoy-wecom") {
|
|
583
|
+
notes.push("已识别 sunnoy 风格兼容字段,安装器会保留网络与 Bot 能力并归一化到当前结构。");
|
|
584
|
+
} else if (source === "legacy-openclaw-wechat") {
|
|
585
|
+
notes.push("已识别旧版 OpenClaw-Wechat 兼容字段,安装器会迁移 legacy agent/dynamic/layout 配置。");
|
|
586
|
+
} else if (source === "mixed-source") {
|
|
587
|
+
notes.push("当前配置混合了多种来源的兼容字段,建议优先审阅迁移 patch 再继续落盘。");
|
|
588
|
+
} else if (source === "native-openclaw-wechat") {
|
|
589
|
+
notes.push("当前配置已经是原生布局,安装器主要补插件启用与 starter config。");
|
|
590
|
+
} else if (source === "fresh") {
|
|
591
|
+
notes.push("当前未发现需要迁移的旧配置,将直接按 quickstart 生成 starter config。");
|
|
592
|
+
}
|
|
593
|
+
if (sourceMismatch) {
|
|
594
|
+
notes.push(`你指定了 ${requestedSource},但实际检测更接近 ${detectedSource},请在落盘前确认来源判断。`);
|
|
595
|
+
}
|
|
596
|
+
if (String(sourceSummary).trim()) {
|
|
597
|
+
notes.push(String(sourceSummary).trim());
|
|
598
|
+
}
|
|
599
|
+
const legacyFieldPaths = uniqueStrings(sourceSignals.map((item) => item?.path));
|
|
600
|
+
const signalKinds = uniqueStrings(sourceSignals.map((item) => item?.kind));
|
|
601
|
+
return {
|
|
602
|
+
source,
|
|
603
|
+
requestedSource,
|
|
604
|
+
effectiveSource,
|
|
605
|
+
title,
|
|
606
|
+
summary: String(sourceSummary || "").trim() || title,
|
|
607
|
+
notes,
|
|
608
|
+
legacyFieldPaths,
|
|
609
|
+
signalKinds,
|
|
610
|
+
recommendedCommand: WECOM_QUICKSTART_MIGRATION_COMMAND,
|
|
611
|
+
doctorFixCommand: `${WECOM_DOCTOR_COMMAND} --fix`,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function buildWecomInstallerActions({
|
|
616
|
+
setupPlan,
|
|
617
|
+
diagnostics,
|
|
618
|
+
guide,
|
|
619
|
+
sourceProfile,
|
|
620
|
+
configPatch,
|
|
621
|
+
canAutoFix = false,
|
|
622
|
+
} = {}) {
|
|
623
|
+
const actions = [];
|
|
624
|
+
const migrationSource = String(diagnostics?.migrationSource ?? guide?.source ?? "unknown");
|
|
625
|
+
const migrationState = String(diagnostics?.migrationState ?? "");
|
|
626
|
+
|
|
627
|
+
if (guide?.title) {
|
|
628
|
+
actions.push({
|
|
629
|
+
id: "review-installer-migration-source",
|
|
630
|
+
kind: "review_migration",
|
|
631
|
+
title: guide.title,
|
|
632
|
+
detail: guide.summary || guide.title,
|
|
633
|
+
paths: uniquePaths(guide.legacyFieldPaths ?? []),
|
|
634
|
+
command: WECOM_QUICKSTART_MIGRATION_COMMAND,
|
|
635
|
+
recommended: migrationSource !== "fresh" && migrationSource !== "native-openclaw-wechat",
|
|
636
|
+
blocking: migrationState === "mixed_layout",
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (sourceProfile?.modeDerived && sourceProfile?.selectedMode) {
|
|
641
|
+
actions.push({
|
|
642
|
+
id: "review-installer-selected-mode",
|
|
643
|
+
kind: "review_setup_profile",
|
|
644
|
+
title: "确认安装器自动选择的接入模式",
|
|
645
|
+
detail: `当前将按 ${sourceProfile.selectedMode} 生成 starter config。${sourceProfile.notes?.join(" ") || ""}`.trim(),
|
|
646
|
+
paths: [],
|
|
647
|
+
command: WECOM_INSTALLER_COMMAND,
|
|
648
|
+
recommended: true,
|
|
649
|
+
blocking: false,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (Array.isArray(sourceProfile?.checkOrder) && sourceProfile.checkOrder.length > 0) {
|
|
654
|
+
actions.push({
|
|
655
|
+
id: "review-installer-check-order",
|
|
656
|
+
kind: "review_checks",
|
|
657
|
+
title: "确认来源专属检查顺序",
|
|
658
|
+
detail: sourceProfile.checkOrderSummary || "请按安装器给出的检查顺序继续验证。",
|
|
659
|
+
paths: [],
|
|
660
|
+
command: WECOM_INSTALLER_COMMAND,
|
|
661
|
+
recommended: true,
|
|
662
|
+
blocking: false,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (guide?.requestedSource && guide?.requestedSource !== "auto" && guide?.requestedSource !== guide?.source) {
|
|
667
|
+
actions.push({
|
|
668
|
+
id: "review-installer-source-mismatch",
|
|
669
|
+
kind: "review_migration",
|
|
670
|
+
title: "确认迁移来源判断",
|
|
671
|
+
detail: `当前检测到 ${guide.source},但安装参数指定了 ${guide.requestedSource}。`,
|
|
672
|
+
paths: uniquePaths(guide.legacyFieldPaths ?? []),
|
|
673
|
+
command: WECOM_QUICKSTART_MIGRATION_COMMAND,
|
|
674
|
+
recommended: true,
|
|
675
|
+
blocking: true,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (Array.isArray(diagnostics?.recommendedActions)) {
|
|
680
|
+
for (const action of diagnostics.recommendedActions) {
|
|
681
|
+
actions.push({
|
|
682
|
+
...action,
|
|
683
|
+
id: `migration:${action.id}`,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (canAutoFix) {
|
|
689
|
+
actions.push({
|
|
690
|
+
id: "installer-run-doctor-fix",
|
|
691
|
+
kind: "apply_patch",
|
|
692
|
+
title: `对${guide?.title || "当前 WeCom 配置"}执行 doctor --fix`,
|
|
693
|
+
detail: "在 starter config 写入后,继续应用本地 migration/plugin patch 并重跑 doctor。",
|
|
694
|
+
paths: uniquePaths([
|
|
695
|
+
...(guide?.legacyFieldPaths ?? []),
|
|
696
|
+
...Object.keys(asObject(configPatch?.channels?.wecom)).map((key) => `channels.wecom.${key}`),
|
|
697
|
+
]),
|
|
698
|
+
command: `${WECOM_DOCTOR_COMMAND} --fix`,
|
|
699
|
+
recommended: true,
|
|
700
|
+
blocking: false,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (Array.isArray(setupPlan?.actions)) {
|
|
705
|
+
for (const action of setupPlan.actions) {
|
|
706
|
+
actions.push({
|
|
707
|
+
...action,
|
|
708
|
+
id: `setup:${action.id}`,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return dedupeActions(actions);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
export function buildWecomPluginInstallCommand({ openclawBin = "openclaw", npmSpec = WECOM_PLUGIN_NPM_SPEC } = {}) {
|
|
717
|
+
return {
|
|
718
|
+
bin: String(openclawBin || "openclaw").trim() || "openclaw",
|
|
719
|
+
args: ["plugins", "install", String(npmSpec || WECOM_PLUGIN_NPM_SPEC).trim() || WECOM_PLUGIN_NPM_SPEC],
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export function buildWecomInstallerPlan({
|
|
724
|
+
mode = WECOM_QUICKSTART_RECOMMENDED_MODE,
|
|
725
|
+
accountId = "default",
|
|
726
|
+
from = "auto",
|
|
727
|
+
dmMode = "pairing",
|
|
728
|
+
modeExplicit = false,
|
|
729
|
+
dmModeExplicit = false,
|
|
730
|
+
groupProfile = WECOM_QUICKSTART_DEFAULT_GROUP_PROFILE,
|
|
731
|
+
groupProfileExplicit = false,
|
|
732
|
+
groupChatId = "",
|
|
733
|
+
groupAllow = [],
|
|
734
|
+
currentConfig = {},
|
|
735
|
+
values = {},
|
|
736
|
+
} = {}) {
|
|
737
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
738
|
+
const normalizedGroupAllow = normalizeGroupAllow(groupAllow);
|
|
739
|
+
const requestedSource = String(from ?? "auto").trim().toLowerCase() || "auto";
|
|
740
|
+
if (!WECOM_INSTALLER_SOURCE_OPTIONS.includes(requestedSource)) {
|
|
741
|
+
throw new Error(
|
|
742
|
+
`Unsupported installer source: ${requestedSource}. Expected one of ${WECOM_INSTALLER_SOURCE_OPTIONS.join(", ")}`,
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
const diagnostics = collectWecomMigrationDiagnostics({
|
|
746
|
+
config: currentConfig,
|
|
747
|
+
accountId: normalizedAccountId,
|
|
748
|
+
});
|
|
749
|
+
const detectedSource = String(diagnostics?.migrationSource ?? "fresh");
|
|
750
|
+
const effectiveSource = requestedSource === "auto" ? detectedSource : requestedSource;
|
|
751
|
+
const migratedConfigForSelection = buildMigratedConfig(
|
|
752
|
+
currentConfig,
|
|
753
|
+
diagnostics?.configPatch ?? {},
|
|
754
|
+
diagnostics?.detectedLegacyFields ?? [],
|
|
755
|
+
);
|
|
756
|
+
const sourceProfile = resolveInstallerSourceProfile({
|
|
757
|
+
requestedSource,
|
|
758
|
+
detectedSource,
|
|
759
|
+
requestedMode: mode,
|
|
760
|
+
requestedGroupProfile: groupProfile,
|
|
761
|
+
requestedDmMode: dmMode,
|
|
762
|
+
modeExplicit,
|
|
763
|
+
groupProfileExplicit,
|
|
764
|
+
dmModeExplicit,
|
|
765
|
+
config: migratedConfigForSelection,
|
|
766
|
+
accountId: normalizedAccountId,
|
|
767
|
+
});
|
|
768
|
+
const setupPlan = buildWecomQuickstartSetupPlan({
|
|
769
|
+
mode: sourceProfile.selectedMode,
|
|
770
|
+
accountId: normalizedAccountId,
|
|
771
|
+
dmMode: sourceProfile.selectedDmMode,
|
|
772
|
+
groupProfile: sourceProfile.selectedGroupProfile,
|
|
773
|
+
groupChatId,
|
|
774
|
+
groupAllow: normalizedGroupAllow,
|
|
775
|
+
currentConfig,
|
|
776
|
+
});
|
|
777
|
+
const sourceMismatch =
|
|
778
|
+
requestedSource !== "auto" &&
|
|
779
|
+
!["fresh", "unknown", "mixed-source", requestedSource].includes(detectedSource);
|
|
780
|
+
const starterConfig = deepClone(setupPlan.starterConfig);
|
|
781
|
+
const migrationAccountPatch = resolveMigrationAccountPatch(diagnostics?.configPatch, normalizedAccountId);
|
|
782
|
+
if (Object.keys(migrationAccountPatch).length > 0) {
|
|
783
|
+
mergeAccountPatchIntoStarterConfig(starterConfig, normalizedAccountId, migrationAccountPatch);
|
|
784
|
+
}
|
|
785
|
+
const migratedDefaultAccount =
|
|
786
|
+
normalizedAccountId !== "default"
|
|
787
|
+
? String(diagnostics?.configPatch?.channels?.wecom?.defaultAccount ?? "").trim()
|
|
788
|
+
: "";
|
|
789
|
+
if (migratedDefaultAccount) {
|
|
790
|
+
starterConfig.channels.wecom.defaultAccount = migratedDefaultAccount;
|
|
791
|
+
}
|
|
792
|
+
applyInstallerValuesToStarterConfig(starterConfig, normalizedAccountId, values);
|
|
793
|
+
const remainingPlaceholders = resolveRemainingPlaceholders(setupPlan.placeholders, starterConfig);
|
|
794
|
+
const configPatch = mergeDeep(
|
|
795
|
+
buildPluginEnablePatch(),
|
|
796
|
+
mergeDeep(diagnostics?.configPatch ?? {}, starterConfig),
|
|
797
|
+
);
|
|
798
|
+
const canAutoFix =
|
|
799
|
+
Boolean(diagnostics?.configPatch) &&
|
|
800
|
+
!["fresh", "native-openclaw-wechat", "unknown"].includes(effectiveSource);
|
|
801
|
+
const migrationGuide = buildWecomInstallerMigrationGuide({
|
|
802
|
+
requestedSource,
|
|
803
|
+
detectedSource,
|
|
804
|
+
effectiveSource,
|
|
805
|
+
sourceSummary: diagnostics?.migrationSourceSummary ?? "",
|
|
806
|
+
sourceSignals: diagnostics?.migrationSourceSignals ?? [],
|
|
807
|
+
sourceMismatch,
|
|
808
|
+
});
|
|
809
|
+
const actions = buildWecomInstallerActions({
|
|
810
|
+
setupPlan,
|
|
811
|
+
diagnostics,
|
|
812
|
+
guide: migrationGuide,
|
|
813
|
+
sourceProfile,
|
|
814
|
+
configPatch,
|
|
815
|
+
canAutoFix,
|
|
816
|
+
});
|
|
817
|
+
return {
|
|
818
|
+
...setupPlan,
|
|
819
|
+
accountId: normalizedAccountId,
|
|
820
|
+
source: requestedSource,
|
|
821
|
+
groupAllow: normalizedGroupAllow,
|
|
822
|
+
requestedMode: mode,
|
|
823
|
+
requestedGroupProfile: groupProfile,
|
|
824
|
+
requestedDmMode: dmMode,
|
|
825
|
+
placeholdersBefore: setupPlan.placeholders,
|
|
826
|
+
placeholders: remainingPlaceholders,
|
|
827
|
+
starterConfig,
|
|
828
|
+
configPatch,
|
|
829
|
+
sourceProfile,
|
|
830
|
+
migration: {
|
|
831
|
+
requestedSource,
|
|
832
|
+
detectedSource,
|
|
833
|
+
effectiveSource,
|
|
834
|
+
sourceSummary: diagnostics?.migrationSourceSummary ?? "",
|
|
835
|
+
sourceSignals: diagnostics?.migrationSourceSignals ?? [],
|
|
836
|
+
sourceMismatch,
|
|
837
|
+
canAutoFix,
|
|
838
|
+
legacyFields: diagnostics?.detectedLegacyFields ?? [],
|
|
839
|
+
guide: migrationGuide,
|
|
840
|
+
diagnostics,
|
|
841
|
+
},
|
|
842
|
+
actions,
|
|
843
|
+
installer: {
|
|
844
|
+
pluginEntryId: WECOM_PLUGIN_ENTRY_ID,
|
|
845
|
+
pluginNpmSpec: WECOM_PLUGIN_NPM_SPEC,
|
|
846
|
+
installerNpmSpec: WECOM_INSTALLER_NPM_SPEC,
|
|
847
|
+
installerCommand: WECOM_INSTALLER_COMMAND,
|
|
848
|
+
quickstartCommand: WECOM_QUICKSTART_SETUP_COMMAND,
|
|
849
|
+
migrateCommand: WECOM_QUICKSTART_MIGRATION_COMMAND,
|
|
850
|
+
doctorCommand: WECOM_DOCTOR_COMMAND,
|
|
851
|
+
wizardCommand: WECOM_QUICKSTART_WIZARD_COMMAND,
|
|
852
|
+
},
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
export async function loadWecomInstallerConfig(configPath) {
|
|
857
|
+
const resolvedPath = path.resolve(expandHome(configPath));
|
|
858
|
+
try {
|
|
859
|
+
const raw = await readFile(resolvedPath, "utf8");
|
|
860
|
+
return {
|
|
861
|
+
exists: true,
|
|
862
|
+
configPath: resolvedPath,
|
|
863
|
+
config: JSON.parse(raw),
|
|
864
|
+
};
|
|
865
|
+
} catch (err) {
|
|
866
|
+
if (err?.code === "ENOENT") {
|
|
867
|
+
return {
|
|
868
|
+
exists: false,
|
|
869
|
+
configPath: resolvedPath,
|
|
870
|
+
config: {},
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
throw err;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
export async function applyWecomInstallerConfigPatch(configPath, patch, legacyFields = []) {
|
|
878
|
+
const loaded = await loadWecomInstallerConfig(configPath);
|
|
879
|
+
const merged = buildMigratedConfig(loaded.config, patch, legacyFields);
|
|
880
|
+
const changedPaths = collectChangedPaths(loaded.config, merged);
|
|
881
|
+
const backupPath = loaded.exists ? buildBackupPath(loaded.configPath) : null;
|
|
882
|
+
await mkdir(path.dirname(loaded.configPath), { recursive: true });
|
|
883
|
+
if (loaded.exists) {
|
|
884
|
+
await writeFile(backupPath, `${JSON.stringify(loaded.config, null, 2)}\n`, "utf8");
|
|
885
|
+
}
|
|
886
|
+
await writeFile(loaded.configPath, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
887
|
+
return {
|
|
888
|
+
applied: true,
|
|
889
|
+
existed: loaded.exists,
|
|
890
|
+
configPath: loaded.configPath,
|
|
891
|
+
backupPath,
|
|
892
|
+
changedPaths,
|
|
893
|
+
merged,
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
export function previewWecomInstallerChangedPaths(currentConfig = {}, patch = {}, legacyFields = []) {
|
|
898
|
+
return collectChangedPaths(asObject(currentConfig), buildMigratedConfig(asObject(currentConfig), patch, legacyFields));
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
export function applyWecomInstallerValuesForPreview(starterConfig = {}, accountId = "default", values = {}) {
|
|
902
|
+
const copy = deepClone(starterConfig);
|
|
903
|
+
applyInstallerValuesToStarterConfig(copy, accountId, values);
|
|
904
|
+
return copy;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
export function setWecomInstallerValueAtPath(target, pathText, value) {
|
|
908
|
+
setAtPath(target, pathText, value);
|
|
909
|
+
return target;
|
|
910
|
+
}
|