@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 +1 -1
- package/src/cli/china-setup.test.ts +101 -61
- package/src/cli/china-setup.ts +88 -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,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
|
-
|
|
188
|
-
const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
|
239
|
+
let restoreTTY: (() => void) | undefined;
|
|
189
240
|
|
|
190
241
|
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
|
-
});
|
|
242
|
+
restoreTTY = setupTTYMocks();
|
|
201
243
|
});
|
|
202
244
|
|
|
203
245
|
afterEach(() => {
|
|
204
|
-
|
|
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
|
|
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("
|
|
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?.
|
|
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
|
-
|
|
256
|
-
const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
|
310
|
+
let restoreTTY: (() => void) | undefined;
|
|
257
311
|
|
|
258
312
|
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
|
-
});
|
|
313
|
+
restoreTTY = setupTTYMocks();
|
|
269
314
|
});
|
|
270
315
|
|
|
271
316
|
afterEach(() => {
|
|
272
|
-
|
|
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 () => {
|
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,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:
|
package/src/cli/install-hint.ts
CHANGED