@openclaw-china/shared 2026.3.12 → 2026.3.16
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 +341 -270
- package/src/cli/china-setup.ts +159 -95
- package/src/cli/index.ts +2 -2
- package/src/cli/install-hint.ts +1 -0
- package/src/cron/index.ts +16 -16
- package/src/file/file-utils.test.ts +141 -141
- package/src/file/file-utils.ts +284 -284
- package/src/file/index.ts +10 -10
- package/src/index.ts +3 -3
- package/src/logger/index.ts +1 -1
- package/src/logger/logger.ts +51 -51
- package/src/media/index.ts +22 -22
- package/vitest.config.ts +8 -8
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" | "qqbot";
|
|
54
|
+
export type ChannelId = "dingtalk" | "feishu-china" | "wecom" | "wecom-app" | "wecom-kf" | "qqbot";
|
|
55
55
|
|
|
56
56
|
export type RegisterChinaSetupCliOptions = {
|
|
57
57
|
channels?: readonly ChannelId[];
|
|
@@ -77,6 +77,7 @@ const CHANNEL_ORDER: readonly ChannelId[] = [
|
|
|
77
77
|
"qqbot",
|
|
78
78
|
"wecom",
|
|
79
79
|
"wecom-app",
|
|
80
|
+
"wecom-kf",
|
|
80
81
|
"feishu-china",
|
|
81
82
|
];
|
|
82
83
|
const CHANNEL_DISPLAY_LABELS: Record<ChannelId, string> = {
|
|
@@ -84,6 +85,7 @@ const CHANNEL_DISPLAY_LABELS: Record<ChannelId, string> = {
|
|
|
84
85
|
"feishu-china": "Feishu(飞书)",
|
|
85
86
|
wecom: "WeCom(企业微信-智能机器人)",
|
|
86
87
|
"wecom-app": "WeCom App(自建应用-可接入微信)",
|
|
88
|
+
"wecom-kf": "WeCom KF(微信客服)",
|
|
87
89
|
qqbot: "QQBot(QQ 机器人)",
|
|
88
90
|
};
|
|
89
91
|
const CHANNEL_GUIDE_LINKS: Record<ChannelId, string> = {
|
|
@@ -91,6 +93,7 @@ const CHANNEL_GUIDE_LINKS: Record<ChannelId, string> = {
|
|
|
91
93
|
"feishu-china": "https://github.com/BytePioneer-AI/openclaw-china/blob/main/README.md",
|
|
92
94
|
wecom: `${GUIDES_BASE}/wecom/configuration.md`,
|
|
93
95
|
"wecom-app": `${GUIDES_BASE}/wecom-app/configuration.md`,
|
|
96
|
+
"wecom-kf": "https://github.com/BytePioneer-AI/openclaw-china/blob/main/extensions/wecom-kf/README.md",
|
|
94
97
|
qqbot: `${GUIDES_BASE}/qqbot/configuration.md`,
|
|
95
98
|
};
|
|
96
99
|
const CHINA_CLI_STATE_KEY = Symbol.for("@openclaw-china/china-cli-state");
|
|
@@ -266,19 +269,19 @@ function cloneConfig(cfg: ConfigRoot): ConfigRoot {
|
|
|
266
269
|
}
|
|
267
270
|
}
|
|
268
271
|
|
|
269
|
-
function getChannelConfig(cfg: ConfigRoot, channelId: ChannelId): ConfigRecord {
|
|
270
|
-
const channels = isRecord(cfg.channels) ? cfg.channels : {};
|
|
271
|
-
const existing = channels[channelId];
|
|
272
|
-
return isRecord(existing) ? existing : {};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function getGatewayAuthToken(cfg: ConfigRoot): string | undefined {
|
|
276
|
-
if (!isRecord(cfg.gateway)) {
|
|
277
|
-
return undefined;
|
|
278
|
-
}
|
|
279
|
-
const auth = isRecord(cfg.gateway.auth) ? cfg.gateway.auth : undefined;
|
|
280
|
-
return toTrimmedString(auth?.token);
|
|
281
|
-
}
|
|
272
|
+
function getChannelConfig(cfg: ConfigRoot, channelId: ChannelId): ConfigRecord {
|
|
273
|
+
const channels = isRecord(cfg.channels) ? cfg.channels : {};
|
|
274
|
+
const existing = channels[channelId];
|
|
275
|
+
return isRecord(existing) ? existing : {};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function getGatewayAuthToken(cfg: ConfigRoot): string | undefined {
|
|
279
|
+
if (!isRecord(cfg.gateway)) {
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
const auth = isRecord(cfg.gateway.auth) ? cfg.gateway.auth : undefined;
|
|
283
|
+
return toTrimmedString(auth?.token);
|
|
284
|
+
}
|
|
282
285
|
|
|
283
286
|
function getPreferredAccountConfig(channelCfg: ConfigRecord): ConfigRecord | undefined {
|
|
284
287
|
const accounts = channelCfg.accounts;
|
|
@@ -302,25 +305,25 @@ function getPreferredAccountConfig(channelCfg: ConfigRecord): ConfigRecord | und
|
|
|
302
305
|
return undefined;
|
|
303
306
|
}
|
|
304
307
|
|
|
305
|
-
function hasCredentialPair(channelCfg: ConfigRecord, firstKey: string, secondKey: string): boolean {
|
|
306
|
-
if (hasNonEmptyString(channelCfg[firstKey]) && hasNonEmptyString(channelCfg[secondKey])) {
|
|
307
|
-
return true;
|
|
308
|
-
}
|
|
309
|
-
const accountCfg = getPreferredAccountConfig(channelCfg);
|
|
310
|
-
return Boolean(
|
|
311
|
-
accountCfg &&
|
|
312
|
-
hasNonEmptyString(accountCfg[firstKey]) &&
|
|
313
|
-
hasNonEmptyString(accountCfg[secondKey])
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function hasTokenPair(channelCfg: ConfigRecord): boolean {
|
|
318
|
-
return hasCredentialPair(channelCfg, "token", "encodingAESKey");
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function hasWecomWsCredentialPair(channelCfg: ConfigRecord): boolean {
|
|
322
|
-
return hasCredentialPair(channelCfg, "botId", "secret");
|
|
323
|
-
}
|
|
308
|
+
function hasCredentialPair(channelCfg: ConfigRecord, firstKey: string, secondKey: string): boolean {
|
|
309
|
+
if (hasNonEmptyString(channelCfg[firstKey]) && hasNonEmptyString(channelCfg[secondKey])) {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
const accountCfg = getPreferredAccountConfig(channelCfg);
|
|
313
|
+
return Boolean(
|
|
314
|
+
accountCfg &&
|
|
315
|
+
hasNonEmptyString(accountCfg[firstKey]) &&
|
|
316
|
+
hasNonEmptyString(accountCfg[secondKey])
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function hasTokenPair(channelCfg: ConfigRecord): boolean {
|
|
321
|
+
return hasCredentialPair(channelCfg, "token", "encodingAESKey");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function hasWecomWsCredentialPair(channelCfg: ConfigRecord): boolean {
|
|
325
|
+
return hasCredentialPair(channelCfg, "botId", "secret");
|
|
326
|
+
}
|
|
324
327
|
|
|
325
328
|
function isChannelConfigured(cfg: ConfigRoot, channelId: ChannelId): boolean {
|
|
326
329
|
const channelCfg = getChannelConfig(cfg, channelId);
|
|
@@ -329,15 +332,22 @@ function isChannelConfigured(cfg: ConfigRoot, channelId: ChannelId): boolean {
|
|
|
329
332
|
return hasNonEmptyString(channelCfg.clientId) && hasNonEmptyString(channelCfg.clientSecret);
|
|
330
333
|
case "feishu-china":
|
|
331
334
|
return hasNonEmptyString(channelCfg.appId) && hasNonEmptyString(channelCfg.appSecret);
|
|
332
|
-
case "qqbot":
|
|
333
|
-
return hasNonEmptyString(channelCfg.appId) && hasNonEmptyString(channelCfg.clientSecret);
|
|
334
|
-
case "wecom":
|
|
335
|
-
return hasWecomWsCredentialPair(channelCfg);
|
|
336
|
-
case "wecom-app":
|
|
337
|
-
return hasTokenPair(channelCfg);
|
|
338
|
-
|
|
339
|
-
return
|
|
340
|
-
|
|
335
|
+
case "qqbot":
|
|
336
|
+
return hasNonEmptyString(channelCfg.appId) && hasNonEmptyString(channelCfg.clientSecret);
|
|
337
|
+
case "wecom":
|
|
338
|
+
return hasWecomWsCredentialPair(channelCfg);
|
|
339
|
+
case "wecom-app":
|
|
340
|
+
return hasTokenPair(channelCfg);
|
|
341
|
+
case "wecom-kf":
|
|
342
|
+
return (
|
|
343
|
+
hasNonEmptyString(channelCfg.corpId) &&
|
|
344
|
+
hasNonEmptyString(channelCfg.corpSecret) &&
|
|
345
|
+
hasNonEmptyString(channelCfg.token) &&
|
|
346
|
+
hasNonEmptyString(channelCfg.encodingAESKey)
|
|
347
|
+
);
|
|
348
|
+
default:
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
341
351
|
}
|
|
342
352
|
|
|
343
353
|
function withConfiguredSuffix(cfg: ConfigRoot, channelId: ChannelId): string {
|
|
@@ -464,10 +474,10 @@ class SetupPrompter {
|
|
|
464
474
|
}
|
|
465
475
|
}
|
|
466
476
|
|
|
467
|
-
async function configureDingtalk(prompter: SetupPrompter, cfg: ConfigRoot): Promise<ConfigRoot> {
|
|
468
|
-
section("配置 DingTalk(钉钉)");
|
|
469
|
-
showGuideLink("dingtalk");
|
|
470
|
-
const existing = getChannelConfig(cfg, "dingtalk");
|
|
477
|
+
async function configureDingtalk(prompter: SetupPrompter, cfg: ConfigRoot): Promise<ConfigRoot> {
|
|
478
|
+
section("配置 DingTalk(钉钉)");
|
|
479
|
+
showGuideLink("dingtalk");
|
|
480
|
+
const existing = getChannelConfig(cfg, "dingtalk");
|
|
471
481
|
|
|
472
482
|
const clientId = await prompter.askText({
|
|
473
483
|
label: "DingTalk clientId(AppKey)",
|
|
@@ -479,30 +489,30 @@ async function configureDingtalk(prompter: SetupPrompter, cfg: ConfigRoot): Prom
|
|
|
479
489
|
existingValue: toTrimmedString(existing.clientSecret),
|
|
480
490
|
required: true,
|
|
481
491
|
});
|
|
482
|
-
const enableAICard = await prompter.askConfirm(
|
|
483
|
-
"启用 AI Card 流式回复(推荐关闭,使用非流式)",
|
|
484
|
-
toBoolean(existing.enableAICard, false)
|
|
485
|
-
);
|
|
486
|
-
const patch: ConfigRecord = {
|
|
487
|
-
clientId,
|
|
488
|
-
clientSecret,
|
|
489
|
-
enableAICard,
|
|
490
|
-
};
|
|
491
|
-
|
|
492
|
-
if (enableAICard) {
|
|
493
|
-
const gatewayToken = await prompter.askSecret({
|
|
494
|
-
label: "OpenClaw Gateway Token(流式输出必需;留空则使用全局 gateway.auth.token)",
|
|
495
|
-
existingValue: toTrimmedString(existing.gatewayToken) ?? getGatewayAuthToken(cfg),
|
|
496
|
-
required: false,
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
if (gatewayToken.trim()) {
|
|
500
|
-
patch.gatewayToken = gatewayToken;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
return mergeChannelConfig(cfg, "dingtalk", patch);
|
|
505
|
-
}
|
|
492
|
+
const enableAICard = await prompter.askConfirm(
|
|
493
|
+
"启用 AI Card 流式回复(推荐关闭,使用非流式)",
|
|
494
|
+
toBoolean(existing.enableAICard, false)
|
|
495
|
+
);
|
|
496
|
+
const patch: ConfigRecord = {
|
|
497
|
+
clientId,
|
|
498
|
+
clientSecret,
|
|
499
|
+
enableAICard,
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
if (enableAICard) {
|
|
503
|
+
const gatewayToken = await prompter.askSecret({
|
|
504
|
+
label: "OpenClaw Gateway Token(流式输出必需;留空则使用全局 gateway.auth.token)",
|
|
505
|
+
existingValue: toTrimmedString(existing.gatewayToken) ?? getGatewayAuthToken(cfg),
|
|
506
|
+
required: false,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
if (gatewayToken.trim()) {
|
|
510
|
+
patch.gatewayToken = gatewayToken;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return mergeChannelConfig(cfg, "dingtalk", patch);
|
|
515
|
+
}
|
|
506
516
|
|
|
507
517
|
async function configureFeishu(prompter: SetupPrompter, cfg: ConfigRoot): Promise<ConfigRoot> {
|
|
508
518
|
section("配置 Feishu(飞书)");
|
|
@@ -530,31 +540,31 @@ async function configureFeishu(prompter: SetupPrompter, cfg: ConfigRoot): Promis
|
|
|
530
540
|
});
|
|
531
541
|
}
|
|
532
542
|
|
|
533
|
-
async function configureWecom(prompter: SetupPrompter, cfg: ConfigRoot): Promise<ConfigRoot> {
|
|
534
|
-
section("配置 WeCom(企业微信-智能机器人)");
|
|
535
|
-
showGuideLink("wecom");
|
|
536
|
-
const existing = getChannelConfig(cfg, "wecom");
|
|
537
|
-
clackNote("当前向导仅提供 WeCom ws 长连接配置。", "提示");
|
|
538
|
-
|
|
539
|
-
const botId = await prompter.askText({
|
|
540
|
-
label: "WeCom botId(ws 长连接)",
|
|
541
|
-
defaultValue: toTrimmedString(existing.botId),
|
|
542
|
-
required: true,
|
|
543
|
-
});
|
|
544
|
-
const secret = await prompter.askSecret({
|
|
545
|
-
label: "WeCom secret(ws 长连接)",
|
|
546
|
-
existingValue: toTrimmedString(existing.secret),
|
|
547
|
-
required: true,
|
|
548
|
-
});
|
|
549
|
-
return mergeChannelConfig(cfg, "wecom", {
|
|
550
|
-
mode: "ws",
|
|
551
|
-
botId,
|
|
552
|
-
secret,
|
|
553
|
-
webhookPath: undefined,
|
|
554
|
-
token: undefined,
|
|
555
|
-
encodingAESKey: undefined,
|
|
556
|
-
});
|
|
557
|
-
}
|
|
543
|
+
async function configureWecom(prompter: SetupPrompter, cfg: ConfigRoot): Promise<ConfigRoot> {
|
|
544
|
+
section("配置 WeCom(企业微信-智能机器人)");
|
|
545
|
+
showGuideLink("wecom");
|
|
546
|
+
const existing = getChannelConfig(cfg, "wecom");
|
|
547
|
+
clackNote("当前向导仅提供 WeCom ws 长连接配置。", "提示");
|
|
548
|
+
|
|
549
|
+
const botId = await prompter.askText({
|
|
550
|
+
label: "WeCom botId(ws 长连接)",
|
|
551
|
+
defaultValue: toTrimmedString(existing.botId),
|
|
552
|
+
required: true,
|
|
553
|
+
});
|
|
554
|
+
const secret = await prompter.askSecret({
|
|
555
|
+
label: "WeCom secret(ws 长连接)",
|
|
556
|
+
existingValue: toTrimmedString(existing.secret),
|
|
557
|
+
required: true,
|
|
558
|
+
});
|
|
559
|
+
return mergeChannelConfig(cfg, "wecom", {
|
|
560
|
+
mode: "ws",
|
|
561
|
+
botId,
|
|
562
|
+
secret,
|
|
563
|
+
webhookPath: undefined,
|
|
564
|
+
token: undefined,
|
|
565
|
+
encodingAESKey: undefined,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
558
568
|
|
|
559
569
|
async function configureWecomApp(prompter: SetupPrompter, cfg: ConfigRoot): Promise<ConfigRoot> {
|
|
560
570
|
section("配置 WeCom App(自建应用-可接入微信)");
|
|
@@ -638,6 +648,58 @@ async function configureWecomApp(prompter: SetupPrompter, cfg: ConfigRoot): Prom
|
|
|
638
648
|
return mergeChannelConfig(cfg, "wecom-app", patch);
|
|
639
649
|
}
|
|
640
650
|
|
|
651
|
+
async function configureWecomKf(prompter: SetupPrompter, cfg: ConfigRoot): Promise<ConfigRoot> {
|
|
652
|
+
section("配置 WeCom KF(微信客服)");
|
|
653
|
+
showGuideLink("wecom-kf");
|
|
654
|
+
const existing = getChannelConfig(cfg, "wecom-kf");
|
|
655
|
+
|
|
656
|
+
const webhookPath = await prompter.askText({
|
|
657
|
+
label: "Webhook 路径(默认 /wecom-kf)",
|
|
658
|
+
defaultValue: toTrimmedString(existing.webhookPath) ?? "/wecom-kf",
|
|
659
|
+
required: true,
|
|
660
|
+
});
|
|
661
|
+
const token = await prompter.askSecret({
|
|
662
|
+
label: "微信客服回调 Token",
|
|
663
|
+
existingValue: toTrimmedString(existing.token),
|
|
664
|
+
required: true,
|
|
665
|
+
});
|
|
666
|
+
const encodingAESKey = await prompter.askSecret({
|
|
667
|
+
label: "微信客服回调 EncodingAESKey",
|
|
668
|
+
existingValue: toTrimmedString(existing.encodingAESKey),
|
|
669
|
+
required: true,
|
|
670
|
+
});
|
|
671
|
+
const corpId = await prompter.askText({
|
|
672
|
+
label: "corpId",
|
|
673
|
+
defaultValue: toTrimmedString(existing.corpId),
|
|
674
|
+
required: true,
|
|
675
|
+
});
|
|
676
|
+
const corpSecret = await prompter.askSecret({
|
|
677
|
+
label: "微信客服 Secret",
|
|
678
|
+
existingValue: toTrimmedString(existing.corpSecret),
|
|
679
|
+
required: true,
|
|
680
|
+
});
|
|
681
|
+
const openKfId = await prompter.askText({
|
|
682
|
+
label: "open_kfid",
|
|
683
|
+
defaultValue: toTrimmedString(existing.openKfId),
|
|
684
|
+
required: true,
|
|
685
|
+
});
|
|
686
|
+
const welcomeText = await prompter.askText({
|
|
687
|
+
label: "欢迎语(可选)",
|
|
688
|
+
defaultValue: toTrimmedString(existing.welcomeText),
|
|
689
|
+
required: false,
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
return mergeChannelConfig(cfg, "wecom-kf", {
|
|
693
|
+
webhookPath,
|
|
694
|
+
token,
|
|
695
|
+
encodingAESKey,
|
|
696
|
+
corpId,
|
|
697
|
+
corpSecret,
|
|
698
|
+
openKfId,
|
|
699
|
+
welcomeText: welcomeText || undefined,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
|
|
641
703
|
async function configureQQBot(prompter: SetupPrompter, cfg: ConfigRoot): Promise<ConfigRoot> {
|
|
642
704
|
section("配置 QQBot(QQ 机器人)");
|
|
643
705
|
showGuideLink("qqbot");
|
|
@@ -702,6 +764,8 @@ async function configureSingleChannel(
|
|
|
702
764
|
return configureWecom(prompter, cfg);
|
|
703
765
|
case "wecom-app":
|
|
704
766
|
return configureWecomApp(prompter, cfg);
|
|
767
|
+
case "wecom-kf":
|
|
768
|
+
return configureWecomKf(prompter, cfg);
|
|
705
769
|
case "qqbot":
|
|
706
770
|
return configureQQBot(prompter, cfg);
|
|
707
771
|
default:
|
package/src/cli/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from "./china-setup.js";
|
|
2
|
-
export * from "./install-hint.js";
|
|
1
|
+
export * from "./china-setup.js";
|
|
2
|
+
export * from "./install-hint.js";
|
package/src/cli/install-hint.ts
CHANGED
package/src/cron/index.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
const CRON_HIDDEN_PROMPT = `
|
|
2
|
-
When creating or updating a cron task, always store a fixed delivery target in the job itself.
|
|
3
|
-
- Use the built-in cron tool (action=add/update). Do not run shell commands.
|
|
4
|
-
- Must use sessionTarget="isolated" for reminder jobs.
|
|
5
|
-
- payload.kind="agentTurn"
|
|
6
|
-
- payload.message must be plain user-visible reminder text only.
|
|
7
|
-
- You must encode runtime guardrails directly into payload.message so the cron run can follow them without extra context.
|
|
8
|
-
- Runtime guardrails to encode in payload.message:
|
|
9
|
-
- return plain text only
|
|
10
|
-
- never call any tool
|
|
11
|
-
- never call the message tool
|
|
12
|
-
- never send manually; delivery is handled by cron delivery settings
|
|
13
|
-
- Do not include tool directives, "NO_REPLY", or heartbeat markers in payload.message.
|
|
14
|
-
- Job name is never a message target.
|
|
15
|
-
- During cron run, must return plain text only and never call the message tool.
|
|
16
|
-
- Use top-level delivery with announce mode:
|
|
1
|
+
const CRON_HIDDEN_PROMPT = `
|
|
2
|
+
When creating or updating a cron task, always store a fixed delivery target in the job itself.
|
|
3
|
+
- Use the built-in cron tool (action=add/update). Do not run shell commands.
|
|
4
|
+
- Must use sessionTarget="isolated" for reminder jobs.
|
|
5
|
+
- payload.kind="agentTurn"
|
|
6
|
+
- payload.message must be plain user-visible reminder text only.
|
|
7
|
+
- You must encode runtime guardrails directly into payload.message so the cron run can follow them without extra context.
|
|
8
|
+
- Runtime guardrails to encode in payload.message:
|
|
9
|
+
- return plain text only
|
|
10
|
+
- never call any tool
|
|
11
|
+
- never call the message tool
|
|
12
|
+
- never send manually; delivery is handled by cron delivery settings
|
|
13
|
+
- Do not include tool directives, "NO_REPLY", or heartbeat markers in payload.message.
|
|
14
|
+
- Job name is never a message target.
|
|
15
|
+
- During cron run, must return plain text only and never call the message tool.
|
|
16
|
+
- Use top-level delivery with announce mode:
|
|
17
17
|
delivery.mode="announce"
|
|
18
18
|
delivery.channel=<OriginatingChannel> (example: "qqbot")
|
|
19
19
|
delivery.to=<OriginatingTo> (examples: "user:<openid>" / "group:<group_openid>")
|