@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.
@@ -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
- default:
339
- return false;
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";
@@ -23,6 +23,7 @@ const SUPPORTED_CHANNELS: readonly ChannelId[] = [
23
23
  "feishu-china",
24
24
  "wecom",
25
25
  "wecom-app",
26
+ "wecom-kf",
26
27
  "qqbot",
27
28
  ];
28
29
  const CHINA_INSTALL_HINT_SHOWN_KEY = Symbol.for("@openclaw-china/china-install-hint-shown");
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>")