@openclaw-china/shared
Advanced tools
+1
-1
| { | ||
| "name": "@openclaw-china/shared", | ||
| "version": "2026.3.12", | ||
| "version": "2026.3.16", | ||
| "type": "module", | ||
@@ -5,0 +5,0 @@ "exports": { |
@@ -23,2 +23,3 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; | ||
| import { registerChinaSetupCli } from "./china-setup.js"; | ||
| import type { ChannelId } from "./china-setup.js"; | ||
@@ -66,5 +67,7 @@ type ActionHandler = () => void | Promise<void>; | ||
| async function runSetup(initialConfig: ConfigRoot): Promise<{ | ||
| async function runSetup( | ||
| initialConfig: ConfigRoot, | ||
| channels: readonly ChannelId[] = ["wecom"] | ||
| ): Promise<{ | ||
| writeConfigFile: ReturnType<typeof vi.fn>; | ||
| channels?: string[]; | ||
| }> { | ||
@@ -83,7 +86,7 @@ let registrar: | ||
| }, | ||
| registerCli: (nextRegistrar) => { | ||
| registrar = nextRegistrar; | ||
| registerCli: (nextRegistrar) => { | ||
| registrar = nextRegistrar; | ||
| }, | ||
| }, | ||
| }, | ||
| { channels: ["wecom"] } | ||
| { channels } | ||
| ); | ||
@@ -186,2 +189,70 @@ | ||
| describe("china setup wecom-kf", () => { | ||
| const stdinDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY"); | ||
| const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY"); | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| delete (globalThis as Record<PropertyKey, unknown>)[CLI_STATE_KEY]; | ||
| Object.defineProperty(process.stdin, "isTTY", { | ||
| configurable: true, | ||
| value: true, | ||
| }); | ||
| Object.defineProperty(process.stdout, "isTTY", { | ||
| configurable: true, | ||
| value: true, | ||
| }); | ||
| }); | ||
| afterEach(() => { | ||
| if (stdinDescriptor) { | ||
| Object.defineProperty(process.stdin, "isTTY", stdinDescriptor); | ||
| } | ||
| if (stdoutDescriptor) { | ||
| Object.defineProperty(process.stdout, "isTTY", stdoutDescriptor); | ||
| } | ||
| }); | ||
| it("stores wecom-kf callback and api credentials", async () => { | ||
| selectMock.mockResolvedValueOnce("wecom-kf"); | ||
| textMock | ||
| .mockResolvedValueOnce("/kf-hook") | ||
| .mockResolvedValueOnce("callback-token") | ||
| .mockResolvedValueOnce("encoding-aes-key") | ||
| .mockResolvedValueOnce("ww-test-corp") | ||
| .mockResolvedValueOnce("kf-secret") | ||
| .mockResolvedValueOnce("wk-test") | ||
| .mockResolvedValueOnce("你好,这里是 AI 客服"); | ||
| const { writeConfigFile } = await runSetup({}, ["wecom-kf"]); | ||
| expect(writeConfigFile).toHaveBeenCalledTimes(1); | ||
| const savedConfig = writeConfigFile.mock.calls[0]?.[0] as ConfigRoot; | ||
| const wecomKfConfig = savedConfig.channels?.["wecom-kf"]; | ||
| expect(wecomKfConfig?.enabled).toBe(true); | ||
| expect(wecomKfConfig?.webhookPath).toBe("/kf-hook"); | ||
| expect(wecomKfConfig?.token).toBe("callback-token"); | ||
| expect(wecomKfConfig?.encodingAESKey).toBe("encoding-aes-key"); | ||
| expect(wecomKfConfig?.corpId).toBe("ww-test-corp"); | ||
| expect(wecomKfConfig?.corpSecret).toBe("kf-secret"); | ||
| expect(wecomKfConfig?.openKfId).toBe("wk-test"); | ||
| expect(wecomKfConfig?.welcomeText).toBe("你好,这里是 AI 客服"); | ||
| const promptMessages = textMock.mock.calls.map((call) => { | ||
| const firstArg = call[0] as { message?: string } | undefined; | ||
| return firstArg?.message ?? ""; | ||
| }); | ||
| expect(promptMessages).toEqual([ | ||
| "Webhook 路径(默认 /wecom-kf)", | ||
| "微信客服回调 Token", | ||
| "微信客服回调 EncodingAESKey", | ||
| "corpId", | ||
| "微信客服 Secret", | ||
| "open_kfid", | ||
| "欢迎语(可选)", | ||
| ]); | ||
| }); | ||
| }); | ||
| describe("china setup dingtalk", () => { | ||
@@ -188,0 +259,0 @@ const stdinDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY"); |
@@ -54,3 +54,3 @@ import { | ||
| export type ChannelId = "dingtalk" | "feishu-china" | "wecom" | "wecom-app" | "qqbot"; | ||
| export type ChannelId = "dingtalk" | "feishu-china" | "wecom" | "wecom-app" | "wecom-kf" | "qqbot"; | ||
@@ -81,2 +81,3 @@ export type RegisterChinaSetupCliOptions = { | ||
| "wecom-app", | ||
| "wecom-kf", | ||
| "feishu-china", | ||
@@ -89,2 +90,3 @@ ]; | ||
| "wecom-app": "WeCom App(自建应用-可接入微信)", | ||
| "wecom-kf": "WeCom KF(微信客服)", | ||
| qqbot: "QQBot(QQ 机器人)", | ||
@@ -97,2 +99,3 @@ }; | ||
| "wecom-app": `${GUIDES_BASE}/wecom-app/configuration.md`, | ||
| "wecom-kf": "https://github.com/BytePioneer-AI/openclaw-china/blob/main/extensions/wecom-kf/README.md", | ||
| qqbot: `${GUIDES_BASE}/qqbot/configuration.md`, | ||
@@ -342,2 +345,9 @@ }; | ||
| return hasTokenPair(channelCfg); | ||
| case "wecom-kf": | ||
| return ( | ||
| hasNonEmptyString(channelCfg.corpId) && | ||
| hasNonEmptyString(channelCfg.corpSecret) && | ||
| hasNonEmptyString(channelCfg.token) && | ||
| hasNonEmptyString(channelCfg.encodingAESKey) | ||
| ); | ||
| default: | ||
@@ -646,2 +656,54 @@ return false; | ||
| async function configureWecomKf(prompter: SetupPrompter, cfg: ConfigRoot): Promise<ConfigRoot> { | ||
| section("配置 WeCom KF(微信客服)"); | ||
| showGuideLink("wecom-kf"); | ||
| const existing = getChannelConfig(cfg, "wecom-kf"); | ||
| const webhookPath = await prompter.askText({ | ||
| label: "Webhook 路径(默认 /wecom-kf)", | ||
| defaultValue: toTrimmedString(existing.webhookPath) ?? "/wecom-kf", | ||
| required: true, | ||
| }); | ||
| const token = await prompter.askSecret({ | ||
| label: "微信客服回调 Token", | ||
| existingValue: toTrimmedString(existing.token), | ||
| required: true, | ||
| }); | ||
| const encodingAESKey = await prompter.askSecret({ | ||
| label: "微信客服回调 EncodingAESKey", | ||
| existingValue: toTrimmedString(existing.encodingAESKey), | ||
| required: true, | ||
| }); | ||
| const corpId = await prompter.askText({ | ||
| label: "corpId", | ||
| defaultValue: toTrimmedString(existing.corpId), | ||
| required: true, | ||
| }); | ||
| const corpSecret = await prompter.askSecret({ | ||
| label: "微信客服 Secret", | ||
| existingValue: toTrimmedString(existing.corpSecret), | ||
| required: true, | ||
| }); | ||
| const openKfId = await prompter.askText({ | ||
| label: "open_kfid", | ||
| defaultValue: toTrimmedString(existing.openKfId), | ||
| required: true, | ||
| }); | ||
| const welcomeText = await prompter.askText({ | ||
| label: "欢迎语(可选)", | ||
| defaultValue: toTrimmedString(existing.welcomeText), | ||
| required: false, | ||
| }); | ||
| return mergeChannelConfig(cfg, "wecom-kf", { | ||
| webhookPath, | ||
| token, | ||
| encodingAESKey, | ||
| corpId, | ||
| corpSecret, | ||
| openKfId, | ||
| welcomeText: welcomeText || undefined, | ||
| }); | ||
| } | ||
| async function configureQQBot(prompter: SetupPrompter, cfg: ConfigRoot): Promise<ConfigRoot> { | ||
@@ -711,2 +773,4 @@ section("配置 QQBot(QQ 机器人)"); | ||
| return configureWecomApp(prompter, cfg); | ||
| case "wecom-kf": | ||
| return configureWecomKf(prompter, cfg); | ||
| case "qqbot": | ||
@@ -713,0 +777,0 @@ return configureQQBot(prompter, cfg); |
@@ -26,2 +26,3 @@ import type { ChannelId } from "./china-setup.js"; | ||
| "wecom-app", | ||
| "wecom-kf", | ||
| "qqbot", | ||
@@ -28,0 +29,0 @@ ]; |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
127824
4.56%3834
3.37%