@openclaw-china/shared 2026.3.16 → 2026.3.19

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.16",
3
+ "version": "2026.3.19",
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,68 @@ 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
+ textMock
213
+ .mockResolvedValueOnce("/wechat-mp")
214
+ .mockResolvedValueOnce("wx-test-appid")
215
+ .mockResolvedValueOnce("wx-test-secret")
216
+ .mockResolvedValueOnce("callback-token")
217
+ .mockResolvedValueOnce("encoding-aes-key")
218
+ .mockResolvedValueOnce("欢迎关注");
219
+
220
+ const { writeConfigFile } = await runSetup({}, ["wechat-mp"]);
221
+
222
+ expect(writeConfigFile).toHaveBeenCalledTimes(1);
223
+ const savedConfig = writeConfigFile.mock.calls[0]?.[0] as ConfigRoot;
224
+ const wechatMpConfig = savedConfig.channels?.["wechat-mp"];
225
+
226
+ expect(wechatMpConfig?.enabled).toBe(true);
227
+ expect(wechatMpConfig?.webhookPath).toBe("/wechat-mp");
228
+ expect(wechatMpConfig?.appId).toBe("wx-test-appid");
229
+ expect(wechatMpConfig?.appSecret).toBe("wx-test-secret");
230
+ expect(wechatMpConfig?.token).toBe("callback-token");
231
+ expect(wechatMpConfig?.encodingAESKey).toBe("encoding-aes-key");
232
+ expect(wechatMpConfig?.messageMode).toBe("safe");
233
+ expect(wechatMpConfig?.replyMode).toBe("passive");
234
+ expect(wechatMpConfig?.welcomeText).toBe("欢迎关注");
235
+ });
236
+ });
237
+
186
238
  describe("china setup wecom-kf", () => {
187
- const stdinDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
188
- const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
239
+ let restoreTTY: (() => void) | undefined;
189
240
 
190
241
  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
- });
242
+ restoreTTY = setupTTYMocks();
201
243
  });
202
244
 
203
245
  afterEach(() => {
204
- if (stdinDescriptor) {
205
- Object.defineProperty(process.stdin, "isTTY", stdinDescriptor);
206
- }
207
- if (stdoutDescriptor) {
208
- Object.defineProperty(process.stdout, "isTTY", stdoutDescriptor);
209
- }
246
+ restoreTTY?.();
210
247
  });
211
248
 
212
- it("stores wecom-kf callback and api credentials", async () => {
249
+ it("stores only the initial wecom-kf callback setup fields", async () => {
213
250
  selectMock.mockResolvedValueOnce("wecom-kf");
214
251
  textMock
215
252
  .mockResolvedValueOnce("/kf-hook")
216
253
  .mockResolvedValueOnce("callback-token")
217
254
  .mockResolvedValueOnce("encoding-aes-key")
218
255
  .mockResolvedValueOnce("ww-test-corp")
219
- .mockResolvedValueOnce("kf-secret")
220
256
  .mockResolvedValueOnce("wk-test")
221
- .mockResolvedValueOnce("你好,这里是 AI 客服");
257
+ .mockResolvedValueOnce("");
222
258
 
223
259
  const { writeConfigFile } = await runSetup({}, ["wecom-kf"]);
224
260
 
@@ -231,9 +267,12 @@ describe("china setup wecom-kf", () => {
231
267
  expect(wecomKfConfig?.token).toBe("callback-token");
232
268
  expect(wecomKfConfig?.encodingAESKey).toBe("encoding-aes-key");
233
269
  expect(wecomKfConfig?.corpId).toBe("ww-test-corp");
234
- expect(wecomKfConfig?.corpSecret).toBe("kf-secret");
235
270
  expect(wecomKfConfig?.openKfId).toBe("wk-test");
236
- expect(wecomKfConfig?.welcomeText).toBe("你好,这里是 AI 客服");
271
+ expect(wecomKfConfig?.corpSecret).toBeUndefined();
272
+ expect(wecomKfConfig?.apiBaseUrl).toBeUndefined();
273
+ expect(wecomKfConfig?.welcomeText).toBeUndefined();
274
+ expect(wecomKfConfig?.dmPolicy).toBeUndefined();
275
+ expect(wecomKfConfig?.allowFrom).toBeUndefined();
237
276
 
238
277
  const promptMessages = textMock.mock.calls.map((call) => {
239
278
  const firstArg = call[0] as { message?: string } | undefined;
@@ -244,37 +283,38 @@ describe("china setup wecom-kf", () => {
244
283
  "微信客服回调 Token",
245
284
  "微信客服回调 EncodingAESKey",
246
285
  "corpId",
247
- "微信客服 Secret",
248
286
  "open_kfid",
249
- "欢迎语(可选)",
287
+ "微信客服 Secret(最后填写;首次接入可先留空)",
250
288
  ]);
289
+
290
+ const noteMessages = noteMock.mock.calls.map((call) => {
291
+ const firstArg = call[0] as string | undefined;
292
+ return firstArg ?? "";
293
+ });
294
+ expect(
295
+ noteMessages.some((message) =>
296
+ message.includes(
297
+ "配置文档:https://github.com/BytePioneer-AI/openclaw-china/tree/main/doc/guides/wecom-kf/configuration.md"
298
+ )
299
+ )
300
+ ).toBe(true);
301
+ expect(
302
+ noteMessages.some((message) =>
303
+ message.includes("corpSecret 会作为最后一个参数询问;首次接入可先留空,待回调 URL 校验通过并点击“开始使用”后再补")
304
+ )
305
+ ).toBe(true);
251
306
  });
252
307
  });
253
308
 
254
309
  describe("china setup dingtalk", () => {
255
- const stdinDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
256
- const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
310
+ let restoreTTY: (() => void) | undefined;
257
311
 
258
312
  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
- });
313
+ restoreTTY = setupTTYMocks();
269
314
  });
270
315
 
271
316
  afterEach(() => {
272
- if (stdinDescriptor) {
273
- Object.defineProperty(process.stdin, "isTTY", stdinDescriptor);
274
- }
275
- if (stdoutDescriptor) {
276
- Object.defineProperty(process.stdout, "isTTY", stdoutDescriptor);
277
- }
317
+ restoreTTY?.();
278
318
  });
279
319
 
280
320
  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,75 @@ 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
+ const welcomeText = await prompter.askText({
761
+ label: "欢迎语(可选)",
762
+ defaultValue: toTrimmedString(existing.welcomeText),
763
+ required: false,
764
+ });
765
+
766
+ return mergeChannelConfig(cfg, "wechat-mp", {
767
+ webhookPath,
768
+ appId,
769
+ appSecret: appSecret || undefined,
770
+ token,
771
+ encodingAESKey: messageMode === "plain" ? undefined : encodingAESKey,
772
+ messageMode,
773
+ replyMode,
699
774
  welcomeText: welcomeText || undefined,
700
775
  });
701
776
  }
@@ -766,6 +841,8 @@ async function configureSingleChannel(
766
841
  return configureWecomApp(prompter, cfg);
767
842
  case "wecom-kf":
768
843
  return configureWecomKf(prompter, cfg);
844
+ case "wechat-mp":
845
+ return configureWechatMp(prompter, cfg);
769
846
  case "qqbot":
770
847
  return configureQQBot(prompter, cfg);
771
848
  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");