@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw-china/shared",
3
- "version": "2026.3.18",
3
+ "version": "2026.3.20",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
@@ -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
- const stdinDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
107
- const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
131
+ let restoreTTY: (() => void) | undefined;
108
132
 
109
133
  beforeEach(() => {
110
- vi.clearAllMocks();
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
- if (stdinDescriptor) {
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
- const stdinDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
188
- const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
317
+ let restoreTTY: (() => void) | undefined;
189
318
 
190
319
  beforeEach(() => {
191
- vi.clearAllMocks();
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
- if (stdinDescriptor) {
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 and api credentials", async () => {
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("你好,这里是 AI 客服");
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?.welcomeText).toBe("你好,这里是 AI 客服");
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
- const stdinDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
256
- const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
388
+ let restoreTTY: (() => void) | undefined;
257
389
 
258
390
  beforeEach(() => {
259
- vi.clearAllMocks();
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
- if (stdinDescriptor) {
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 () => {
@@ -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 welcomeText = await prompter.askText({
687
- label: "欢迎语(可选)",
688
- defaultValue: toTrimmedString(existing.welcomeText),
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:
@@ -24,6 +24,7 @@ const SUPPORTED_CHANNELS: readonly ChannelId[] = [
24
24
  "wecom",
25
25
  "wecom-app",
26
26
  "wecom-kf",
27
+ "wechat-mp",
27
28
  "qqbot",
28
29
  ];
29
30
  const CHINA_INSTALL_HINT_SHOWN_KEY = Symbol.for("@openclaw-china/china-install-hint-shown");