@openclaw-china/shared 2026.3.18 → 2026.3.20
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/package.json +1 -1
- package/src/cli/china-setup.test.ts +179 -61
- package/src/cli/china-setup.ts +108 -11
- package/src/cli/install-hint.ts +1 -0
package/package.json
CHANGED
|
@@ -44,6 +44,31 @@ type ConfigRoot = {
|
|
|
44
44
|
|
|
45
45
|
const CLI_STATE_KEY = Symbol.for("@openclaw-china/china-cli-state");
|
|
46
46
|
|
|
47
|
+
function setupTTYMocks(): () => void {
|
|
48
|
+
const stdinDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
|
|
49
|
+
const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
|
50
|
+
|
|
51
|
+
vi.clearAllMocks();
|
|
52
|
+
delete (globalThis as Record<PropertyKey, unknown>)[CLI_STATE_KEY];
|
|
53
|
+
Object.defineProperty(process.stdin, "isTTY", {
|
|
54
|
+
configurable: true,
|
|
55
|
+
value: true,
|
|
56
|
+
});
|
|
57
|
+
Object.defineProperty(process.stdout, "isTTY", {
|
|
58
|
+
configurable: true,
|
|
59
|
+
value: true,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
if (stdinDescriptor) {
|
|
64
|
+
Object.defineProperty(process.stdin, "isTTY", stdinDescriptor);
|
|
65
|
+
}
|
|
66
|
+
if (stdoutDescriptor) {
|
|
67
|
+
Object.defineProperty(process.stdout, "isTTY", stdoutDescriptor);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
47
72
|
function createCommandNode(): CommandNode {
|
|
48
73
|
const node: CommandNode = {
|
|
49
74
|
children: new Map<string, CommandNode>(),
|
|
@@ -103,29 +128,14 @@ async function runSetup(
|
|
|
103
128
|
}
|
|
104
129
|
|
|
105
130
|
describe("china setup wecom", () => {
|
|
106
|
-
|
|
107
|
-
const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
|
131
|
+
let restoreTTY: (() => void) | undefined;
|
|
108
132
|
|
|
109
133
|
beforeEach(() => {
|
|
110
|
-
|
|
111
|
-
delete (globalThis as Record<PropertyKey, unknown>)[CLI_STATE_KEY];
|
|
112
|
-
Object.defineProperty(process.stdin, "isTTY", {
|
|
113
|
-
configurable: true,
|
|
114
|
-
value: true,
|
|
115
|
-
});
|
|
116
|
-
Object.defineProperty(process.stdout, "isTTY", {
|
|
117
|
-
configurable: true,
|
|
118
|
-
value: true,
|
|
119
|
-
});
|
|
134
|
+
restoreTTY = setupTTYMocks();
|
|
120
135
|
});
|
|
121
136
|
|
|
122
137
|
afterEach(() => {
|
|
123
|
-
|
|
124
|
-
Object.defineProperty(process.stdin, "isTTY", stdinDescriptor);
|
|
125
|
-
}
|
|
126
|
-
if (stdoutDescriptor) {
|
|
127
|
-
Object.defineProperty(process.stdout, "isTTY", stdoutDescriptor);
|
|
128
|
-
}
|
|
138
|
+
restoreTTY?.();
|
|
129
139
|
});
|
|
130
140
|
|
|
131
141
|
it("stores ws-only credentials for wecom setup", async () => {
|
|
@@ -183,42 +193,146 @@ describe("china setup wecom", () => {
|
|
|
183
193
|
});
|
|
184
194
|
});
|
|
185
195
|
|
|
196
|
+
describe("china setup wechat-mp", () => {
|
|
197
|
+
let restoreTTY: (() => void) | undefined;
|
|
198
|
+
|
|
199
|
+
beforeEach(() => {
|
|
200
|
+
restoreTTY = setupTTYMocks();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
afterEach(() => {
|
|
204
|
+
restoreTTY?.();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("stores wechat-mp callback and account config", async () => {
|
|
208
|
+
selectMock
|
|
209
|
+
.mockResolvedValueOnce("wechat-mp")
|
|
210
|
+
.mockResolvedValueOnce("safe")
|
|
211
|
+
.mockResolvedValueOnce("passive");
|
|
212
|
+
confirmMock.mockResolvedValueOnce(true); // renderMarkdown enabled (default)
|
|
213
|
+
textMock
|
|
214
|
+
.mockResolvedValueOnce("/wechat-mp")
|
|
215
|
+
.mockResolvedValueOnce("wx-test-appid")
|
|
216
|
+
.mockResolvedValueOnce("wx-test-secret")
|
|
217
|
+
.mockResolvedValueOnce("callback-token")
|
|
218
|
+
.mockResolvedValueOnce("encoding-aes-key")
|
|
219
|
+
.mockResolvedValueOnce("欢迎关注");
|
|
220
|
+
|
|
221
|
+
const { writeConfigFile } = await runSetup({}, ["wechat-mp"]);
|
|
222
|
+
|
|
223
|
+
expect(writeConfigFile).toHaveBeenCalledTimes(1);
|
|
224
|
+
const savedConfig = writeConfigFile.mock.calls[0]?.[0] as ConfigRoot;
|
|
225
|
+
const wechatMpConfig = savedConfig.channels?.["wechat-mp"];
|
|
226
|
+
|
|
227
|
+
expect(wechatMpConfig?.enabled).toBe(true);
|
|
228
|
+
expect(wechatMpConfig?.webhookPath).toBe("/wechat-mp");
|
|
229
|
+
expect(wechatMpConfig?.appId).toBe("wx-test-appid");
|
|
230
|
+
expect(wechatMpConfig?.appSecret).toBe("wx-test-secret");
|
|
231
|
+
expect(wechatMpConfig?.token).toBe("callback-token");
|
|
232
|
+
expect(wechatMpConfig?.encodingAESKey).toBe("encoding-aes-key");
|
|
233
|
+
expect(wechatMpConfig?.messageMode).toBe("safe");
|
|
234
|
+
expect(wechatMpConfig?.replyMode).toBe("passive");
|
|
235
|
+
expect(wechatMpConfig?.welcomeText).toBe("欢迎关注");
|
|
236
|
+
expect(wechatMpConfig?.renderMarkdown).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("stores activeDeliveryMode when replyMode is active", async () => {
|
|
240
|
+
selectMock
|
|
241
|
+
.mockResolvedValueOnce("wechat-mp")
|
|
242
|
+
.mockResolvedValueOnce("safe")
|
|
243
|
+
.mockResolvedValueOnce("active")
|
|
244
|
+
.mockResolvedValueOnce("split");
|
|
245
|
+
textMock
|
|
246
|
+
.mockResolvedValueOnce("/wechat-mp-active")
|
|
247
|
+
.mockResolvedValueOnce("wx-active-appid")
|
|
248
|
+
.mockResolvedValueOnce("wx-active-secret")
|
|
249
|
+
.mockResolvedValueOnce("active-token")
|
|
250
|
+
.mockResolvedValueOnce("active-aes-key")
|
|
251
|
+
.mockResolvedValueOnce("welcome");
|
|
252
|
+
|
|
253
|
+
const { writeConfigFile } = await runSetup({}, ["wechat-mp"]);
|
|
254
|
+
|
|
255
|
+
expect(writeConfigFile).toHaveBeenCalledTimes(1);
|
|
256
|
+
const savedConfig = writeConfigFile.mock.calls[0]?.[0] as ConfigRoot;
|
|
257
|
+
const wechatMpConfig = savedConfig.channels?.["wechat-mp"];
|
|
258
|
+
|
|
259
|
+
expect(wechatMpConfig?.enabled).toBe(true);
|
|
260
|
+
expect(wechatMpConfig?.replyMode).toBe("active");
|
|
261
|
+
expect(wechatMpConfig?.activeDeliveryMode).toBe("split");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("stores renderMarkdown when explicitly disabled", async () => {
|
|
265
|
+
selectMock
|
|
266
|
+
.mockResolvedValueOnce("wechat-mp")
|
|
267
|
+
.mockResolvedValueOnce("safe")
|
|
268
|
+
.mockResolvedValueOnce("active")
|
|
269
|
+
.mockResolvedValueOnce("merged");
|
|
270
|
+
confirmMock.mockResolvedValueOnce(false); // Disable renderMarkdown
|
|
271
|
+
textMock
|
|
272
|
+
.mockResolvedValueOnce("/wechat-mp-no-md")
|
|
273
|
+
.mockResolvedValueOnce("wx-no-md-appid")
|
|
274
|
+
.mockResolvedValueOnce("wx-no-md-secret")
|
|
275
|
+
.mockResolvedValueOnce("no-md-token")
|
|
276
|
+
.mockResolvedValueOnce("no-md-aes-key")
|
|
277
|
+
.mockResolvedValueOnce("welcome");
|
|
278
|
+
|
|
279
|
+
const { writeConfigFile } = await runSetup({}, ["wechat-mp"]);
|
|
280
|
+
|
|
281
|
+
expect(writeConfigFile).toHaveBeenCalledTimes(1);
|
|
282
|
+
const savedConfig = writeConfigFile.mock.calls[0]?.[0] as ConfigRoot;
|
|
283
|
+
const wechatMpConfig = savedConfig.channels?.["wechat-mp"];
|
|
284
|
+
|
|
285
|
+
expect(wechatMpConfig?.enabled).toBe(true);
|
|
286
|
+
expect(wechatMpConfig?.activeDeliveryMode).toBe("merged");
|
|
287
|
+
expect(wechatMpConfig?.renderMarkdown).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("defaults renderMarkdown to true when not explicitly disabled", async () => {
|
|
291
|
+
selectMock
|
|
292
|
+
.mockResolvedValueOnce("wechat-mp")
|
|
293
|
+
.mockResolvedValueOnce("safe")
|
|
294
|
+
.mockResolvedValueOnce("passive");
|
|
295
|
+
confirmMock.mockResolvedValueOnce(true); // Keep renderMarkdown enabled (default)
|
|
296
|
+
textMock
|
|
297
|
+
.mockResolvedValueOnce("/wechat-mp-default-md")
|
|
298
|
+
.mockResolvedValueOnce("wx-default-appid")
|
|
299
|
+
.mockResolvedValueOnce("wx-default-secret")
|
|
300
|
+
.mockResolvedValueOnce("default-token")
|
|
301
|
+
.mockResolvedValueOnce("default-aes-key")
|
|
302
|
+
.mockResolvedValueOnce("welcome");
|
|
303
|
+
|
|
304
|
+
const { writeConfigFile } = await runSetup({}, ["wechat-mp"]);
|
|
305
|
+
|
|
306
|
+
expect(writeConfigFile).toHaveBeenCalledTimes(1);
|
|
307
|
+
const savedConfig = writeConfigFile.mock.calls[0]?.[0] as ConfigRoot;
|
|
308
|
+
const wechatMpConfig = savedConfig.channels?.["wechat-mp"];
|
|
309
|
+
|
|
310
|
+
expect(wechatMpConfig?.enabled).toBe(true);
|
|
311
|
+
// setup writes the value explicitly, even when it's the default true
|
|
312
|
+
expect(wechatMpConfig?.renderMarkdown).toBe(true);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
186
316
|
describe("china setup wecom-kf", () => {
|
|
187
|
-
|
|
188
|
-
const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
|
317
|
+
let restoreTTY: (() => void) | undefined;
|
|
189
318
|
|
|
190
319
|
beforeEach(() => {
|
|
191
|
-
|
|
192
|
-
delete (globalThis as Record<PropertyKey, unknown>)[CLI_STATE_KEY];
|
|
193
|
-
Object.defineProperty(process.stdin, "isTTY", {
|
|
194
|
-
configurable: true,
|
|
195
|
-
value: true,
|
|
196
|
-
});
|
|
197
|
-
Object.defineProperty(process.stdout, "isTTY", {
|
|
198
|
-
configurable: true,
|
|
199
|
-
value: true,
|
|
200
|
-
});
|
|
320
|
+
restoreTTY = setupTTYMocks();
|
|
201
321
|
});
|
|
202
322
|
|
|
203
323
|
afterEach(() => {
|
|
204
|
-
|
|
205
|
-
Object.defineProperty(process.stdin, "isTTY", stdinDescriptor);
|
|
206
|
-
}
|
|
207
|
-
if (stdoutDescriptor) {
|
|
208
|
-
Object.defineProperty(process.stdout, "isTTY", stdoutDescriptor);
|
|
209
|
-
}
|
|
324
|
+
restoreTTY?.();
|
|
210
325
|
});
|
|
211
326
|
|
|
212
|
-
it("stores wecom-kf callback
|
|
327
|
+
it("stores only the initial wecom-kf callback setup fields", async () => {
|
|
213
328
|
selectMock.mockResolvedValueOnce("wecom-kf");
|
|
214
329
|
textMock
|
|
215
330
|
.mockResolvedValueOnce("/kf-hook")
|
|
216
331
|
.mockResolvedValueOnce("callback-token")
|
|
217
332
|
.mockResolvedValueOnce("encoding-aes-key")
|
|
218
333
|
.mockResolvedValueOnce("ww-test-corp")
|
|
219
|
-
.mockResolvedValueOnce("kf-secret")
|
|
220
334
|
.mockResolvedValueOnce("wk-test")
|
|
221
|
-
.mockResolvedValueOnce("
|
|
335
|
+
.mockResolvedValueOnce("");
|
|
222
336
|
|
|
223
337
|
const { writeConfigFile } = await runSetup({}, ["wecom-kf"]);
|
|
224
338
|
|
|
@@ -231,9 +345,12 @@ describe("china setup wecom-kf", () => {
|
|
|
231
345
|
expect(wecomKfConfig?.token).toBe("callback-token");
|
|
232
346
|
expect(wecomKfConfig?.encodingAESKey).toBe("encoding-aes-key");
|
|
233
347
|
expect(wecomKfConfig?.corpId).toBe("ww-test-corp");
|
|
234
|
-
expect(wecomKfConfig?.corpSecret).toBe("kf-secret");
|
|
235
348
|
expect(wecomKfConfig?.openKfId).toBe("wk-test");
|
|
236
|
-
expect(wecomKfConfig?.
|
|
349
|
+
expect(wecomKfConfig?.corpSecret).toBeUndefined();
|
|
350
|
+
expect(wecomKfConfig?.apiBaseUrl).toBeUndefined();
|
|
351
|
+
expect(wecomKfConfig?.welcomeText).toBeUndefined();
|
|
352
|
+
expect(wecomKfConfig?.dmPolicy).toBeUndefined();
|
|
353
|
+
expect(wecomKfConfig?.allowFrom).toBeUndefined();
|
|
237
354
|
|
|
238
355
|
const promptMessages = textMock.mock.calls.map((call) => {
|
|
239
356
|
const firstArg = call[0] as { message?: string } | undefined;
|
|
@@ -244,37 +361,38 @@ describe("china setup wecom-kf", () => {
|
|
|
244
361
|
"微信客服回调 Token",
|
|
245
362
|
"微信客服回调 EncodingAESKey",
|
|
246
363
|
"corpId",
|
|
247
|
-
"微信客服 Secret",
|
|
248
364
|
"open_kfid",
|
|
249
|
-
"
|
|
365
|
+
"微信客服 Secret(最后填写;首次接入可先留空)",
|
|
250
366
|
]);
|
|
367
|
+
|
|
368
|
+
const noteMessages = noteMock.mock.calls.map((call) => {
|
|
369
|
+
const firstArg = call[0] as string | undefined;
|
|
370
|
+
return firstArg ?? "";
|
|
371
|
+
});
|
|
372
|
+
expect(
|
|
373
|
+
noteMessages.some((message) =>
|
|
374
|
+
message.includes(
|
|
375
|
+
"配置文档:https://github.com/BytePioneer-AI/openclaw-china/tree/main/doc/guides/wecom-kf/configuration.md"
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
).toBe(true);
|
|
379
|
+
expect(
|
|
380
|
+
noteMessages.some((message) =>
|
|
381
|
+
message.includes("corpSecret 会作为最后一个参数询问;首次接入可先留空,待回调 URL 校验通过并点击“开始使用”后再补")
|
|
382
|
+
)
|
|
383
|
+
).toBe(true);
|
|
251
384
|
});
|
|
252
385
|
});
|
|
253
386
|
|
|
254
387
|
describe("china setup dingtalk", () => {
|
|
255
|
-
|
|
256
|
-
const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
|
388
|
+
let restoreTTY: (() => void) | undefined;
|
|
257
389
|
|
|
258
390
|
beforeEach(() => {
|
|
259
|
-
|
|
260
|
-
delete (globalThis as Record<PropertyKey, unknown>)[CLI_STATE_KEY];
|
|
261
|
-
Object.defineProperty(process.stdin, "isTTY", {
|
|
262
|
-
configurable: true,
|
|
263
|
-
value: true,
|
|
264
|
-
});
|
|
265
|
-
Object.defineProperty(process.stdout, "isTTY", {
|
|
266
|
-
configurable: true,
|
|
267
|
-
value: true,
|
|
268
|
-
});
|
|
391
|
+
restoreTTY = setupTTYMocks();
|
|
269
392
|
});
|
|
270
393
|
|
|
271
394
|
afterEach(() => {
|
|
272
|
-
|
|
273
|
-
Object.defineProperty(process.stdin, "isTTY", stdinDescriptor);
|
|
274
|
-
}
|
|
275
|
-
if (stdoutDescriptor) {
|
|
276
|
-
Object.defineProperty(process.stdout, "isTTY", stdoutDescriptor);
|
|
277
|
-
}
|
|
395
|
+
restoreTTY?.();
|
|
278
396
|
});
|
|
279
397
|
|
|
280
398
|
it("stores gateway token when dingtalk AI Card streaming is enabled", async () => {
|
package/src/cli/china-setup.ts
CHANGED
|
@@ -51,7 +51,7 @@ type ConfigRoot = {
|
|
|
51
51
|
[key: string]: unknown;
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
export type ChannelId = "dingtalk" | "feishu-china" | "wecom" | "wecom-app" | "wecom-kf" | "qqbot";
|
|
54
|
+
export type ChannelId = "dingtalk" | "feishu-china" | "wecom" | "wecom-app" | "wecom-kf" | "qqbot" | "wechat-mp";
|
|
55
55
|
|
|
56
56
|
export type RegisterChinaSetupCliOptions = {
|
|
57
57
|
channels?: readonly ChannelId[];
|
|
@@ -78,6 +78,7 @@ const CHANNEL_ORDER: readonly ChannelId[] = [
|
|
|
78
78
|
"wecom",
|
|
79
79
|
"wecom-app",
|
|
80
80
|
"wecom-kf",
|
|
81
|
+
"wechat-mp",
|
|
81
82
|
"feishu-china",
|
|
82
83
|
];
|
|
83
84
|
const CHANNEL_DISPLAY_LABELS: Record<ChannelId, string> = {
|
|
@@ -86,6 +87,7 @@ const CHANNEL_DISPLAY_LABELS: Record<ChannelId, string> = {
|
|
|
86
87
|
wecom: "WeCom(企业微信-智能机器人)",
|
|
87
88
|
"wecom-app": "WeCom App(自建应用-可接入微信)",
|
|
88
89
|
"wecom-kf": "WeCom KF(微信客服)",
|
|
90
|
+
"wechat-mp": "WeChat MP(微信公众号)",
|
|
89
91
|
qqbot: "QQBot(QQ 机器人)",
|
|
90
92
|
};
|
|
91
93
|
const CHANNEL_GUIDE_LINKS: Record<ChannelId, string> = {
|
|
@@ -94,6 +96,7 @@ const CHANNEL_GUIDE_LINKS: Record<ChannelId, string> = {
|
|
|
94
96
|
wecom: `${GUIDES_BASE}/wecom/configuration.md`,
|
|
95
97
|
"wecom-app": `${GUIDES_BASE}/wecom-app/configuration.md`,
|
|
96
98
|
"wecom-kf": "https://github.com/BytePioneer-AI/openclaw-china/blob/main/extensions/wecom-kf/README.md",
|
|
99
|
+
"wechat-mp": `${GUIDES_BASE}/wechat-mp/configuration.md`,
|
|
97
100
|
qqbot: `${GUIDES_BASE}/qqbot/configuration.md`,
|
|
98
101
|
};
|
|
99
102
|
const CHINA_CLI_STATE_KEY = Symbol.for("@openclaw-china/china-cli-state");
|
|
@@ -341,10 +344,11 @@ function isChannelConfigured(cfg: ConfigRoot, channelId: ChannelId): boolean {
|
|
|
341
344
|
case "wecom-kf":
|
|
342
345
|
return (
|
|
343
346
|
hasNonEmptyString(channelCfg.corpId) &&
|
|
344
|
-
hasNonEmptyString(channelCfg.corpSecret) &&
|
|
345
347
|
hasNonEmptyString(channelCfg.token) &&
|
|
346
348
|
hasNonEmptyString(channelCfg.encodingAESKey)
|
|
347
349
|
);
|
|
350
|
+
case "wechat-mp":
|
|
351
|
+
return hasNonEmptyString(channelCfg.appId) && hasNonEmptyString(channelCfg.token);
|
|
348
352
|
default:
|
|
349
353
|
return false;
|
|
350
354
|
}
|
|
@@ -652,6 +656,15 @@ async function configureWecomKf(prompter: SetupPrompter, cfg: ConfigRoot): Promi
|
|
|
652
656
|
section("配置 WeCom KF(微信客服)");
|
|
653
657
|
showGuideLink("wecom-kf");
|
|
654
658
|
const existing = getChannelConfig(cfg, "wecom-kf");
|
|
659
|
+
clackNote(
|
|
660
|
+
[
|
|
661
|
+
"向导顺序:webhookPath / token / encodingAESKey / corpId / open_kfid / corpSecret",
|
|
662
|
+
"基础必填:corpId / token / encodingAESKey / open_kfid",
|
|
663
|
+
"corpSecret 会作为最后一个参数询问;首次接入可先留空,待回调 URL 校验通过并点击“开始使用”后再补",
|
|
664
|
+
"webhookPath 默认值:/wecom-kf",
|
|
665
|
+
].join("\n"),
|
|
666
|
+
"参数说明"
|
|
667
|
+
);
|
|
655
668
|
|
|
656
669
|
const webhookPath = await prompter.askText({
|
|
657
670
|
label: "Webhook 路径(默认 /wecom-kf)",
|
|
@@ -673,19 +686,14 @@ async function configureWecomKf(prompter: SetupPrompter, cfg: ConfigRoot): Promi
|
|
|
673
686
|
defaultValue: toTrimmedString(existing.corpId),
|
|
674
687
|
required: true,
|
|
675
688
|
});
|
|
676
|
-
const corpSecret = await prompter.askSecret({
|
|
677
|
-
label: "微信客服 Secret",
|
|
678
|
-
existingValue: toTrimmedString(existing.corpSecret),
|
|
679
|
-
required: true,
|
|
680
|
-
});
|
|
681
689
|
const openKfId = await prompter.askText({
|
|
682
690
|
label: "open_kfid",
|
|
683
691
|
defaultValue: toTrimmedString(existing.openKfId),
|
|
684
692
|
required: true,
|
|
685
693
|
});
|
|
686
|
-
const
|
|
687
|
-
label: "
|
|
688
|
-
|
|
694
|
+
const corpSecret = await prompter.askSecret({
|
|
695
|
+
label: "微信客服 Secret(最后填写;首次接入可先留空)",
|
|
696
|
+
existingValue: toTrimmedString(existing.corpSecret),
|
|
689
697
|
required: false,
|
|
690
698
|
});
|
|
691
699
|
|
|
@@ -694,8 +702,95 @@ async function configureWecomKf(prompter: SetupPrompter, cfg: ConfigRoot): Promi
|
|
|
694
702
|
token,
|
|
695
703
|
encodingAESKey,
|
|
696
704
|
corpId,
|
|
697
|
-
corpSecret,
|
|
698
705
|
openKfId,
|
|
706
|
+
corpSecret: corpSecret || undefined,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function configureWechatMp(prompter: SetupPrompter, cfg: ConfigRoot): Promise<ConfigRoot> {
|
|
711
|
+
section("配置 WeChat MP(微信公众号)");
|
|
712
|
+
showGuideLink("wechat-mp");
|
|
713
|
+
const existing = getChannelConfig(cfg, "wechat-mp");
|
|
714
|
+
|
|
715
|
+
const webhookPath = await prompter.askText({
|
|
716
|
+
label: "Webhook 路径(默认 /wechat-mp)",
|
|
717
|
+
defaultValue: toTrimmedString(existing.webhookPath) ?? "/wechat-mp",
|
|
718
|
+
required: true,
|
|
719
|
+
});
|
|
720
|
+
const appId = await prompter.askText({
|
|
721
|
+
label: "公众号 appId",
|
|
722
|
+
defaultValue: toTrimmedString(existing.appId),
|
|
723
|
+
required: true,
|
|
724
|
+
});
|
|
725
|
+
const appSecret = await prompter.askSecret({
|
|
726
|
+
label: "公众号 appSecret(主动发送需要)",
|
|
727
|
+
existingValue: toTrimmedString(existing.appSecret),
|
|
728
|
+
required: false,
|
|
729
|
+
});
|
|
730
|
+
const token = await prompter.askSecret({
|
|
731
|
+
label: "服务器配置 token",
|
|
732
|
+
existingValue: toTrimmedString(existing.token),
|
|
733
|
+
required: true,
|
|
734
|
+
});
|
|
735
|
+
const messageMode = await prompter.askSelect<"plain" | "safe" | "compat">(
|
|
736
|
+
"消息加解密模式",
|
|
737
|
+
[
|
|
738
|
+
{ value: "plain", label: "plain(明文)" },
|
|
739
|
+
{ value: "safe", label: "safe(安全模式)" },
|
|
740
|
+
{ value: "compat", label: "compat(兼容模式)" },
|
|
741
|
+
],
|
|
742
|
+
(toTrimmedString(existing.messageMode) as "plain" | "safe" | "compat" | undefined) ?? "safe"
|
|
743
|
+
);
|
|
744
|
+
let encodingAESKey = toTrimmedString(existing.encodingAESKey);
|
|
745
|
+
if (messageMode !== "plain") {
|
|
746
|
+
encodingAESKey = await prompter.askSecret({
|
|
747
|
+
label: "EncodingAESKey(safe/compat 必填)",
|
|
748
|
+
existingValue: encodingAESKey,
|
|
749
|
+
required: true,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
const replyMode = await prompter.askSelect<"passive" | "active">(
|
|
753
|
+
"回复模式",
|
|
754
|
+
[
|
|
755
|
+
{ value: "passive", label: "passive(5 秒内被动回复)" },
|
|
756
|
+
{ value: "active", label: "active(客服消息主动发送)" },
|
|
757
|
+
],
|
|
758
|
+
(toTrimmedString(existing.replyMode) as "passive" | "active" | undefined) ?? "passive"
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
let activeDeliveryMode: "merged" | "split" | undefined;
|
|
762
|
+
if (replyMode === "active") {
|
|
763
|
+
activeDeliveryMode = await prompter.askSelect<"merged" | "split">(
|
|
764
|
+
"主动发送模式(activeDeliveryMode)",
|
|
765
|
+
[
|
|
766
|
+
{ value: "split", label: "split(逐块发送,推荐)" },
|
|
767
|
+
{ value: "merged", label: "merged(合并后单次发送)" },
|
|
768
|
+
],
|
|
769
|
+
(toTrimmedString(existing.activeDeliveryMode) as "merged" | "split" | undefined) ?? "split"
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const renderMarkdown = await prompter.askConfirm(
|
|
774
|
+
"启用 Markdown 渲染(推荐开启)",
|
|
775
|
+
toBoolean(existing.renderMarkdown, true)
|
|
776
|
+
);
|
|
777
|
+
|
|
778
|
+
const welcomeText = await prompter.askText({
|
|
779
|
+
label: "欢迎语(可选)",
|
|
780
|
+
defaultValue: toTrimmedString(existing.welcomeText),
|
|
781
|
+
required: false,
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
return mergeChannelConfig(cfg, "wechat-mp", {
|
|
785
|
+
webhookPath,
|
|
786
|
+
appId,
|
|
787
|
+
appSecret: appSecret || undefined,
|
|
788
|
+
token,
|
|
789
|
+
encodingAESKey: messageMode === "plain" ? undefined : encodingAESKey,
|
|
790
|
+
messageMode,
|
|
791
|
+
replyMode,
|
|
792
|
+
activeDeliveryMode,
|
|
793
|
+
renderMarkdown,
|
|
699
794
|
welcomeText: welcomeText || undefined,
|
|
700
795
|
});
|
|
701
796
|
}
|
|
@@ -766,6 +861,8 @@ async function configureSingleChannel(
|
|
|
766
861
|
return configureWecomApp(prompter, cfg);
|
|
767
862
|
case "wecom-kf":
|
|
768
863
|
return configureWecomKf(prompter, cfg);
|
|
864
|
+
case "wechat-mp":
|
|
865
|
+
return configureWechatMp(prompter, cfg);
|
|
769
866
|
case "qqbot":
|
|
770
867
|
return configureQQBot(prompter, cfg);
|
|
771
868
|
default:
|
package/src/cli/install-hint.ts
CHANGED