From d684f4a10cdf42bd27298483baf6db74a447ad75 Mon Sep 17 00:00:00 2001 From: EthanHuo Date: Fri, 6 Nov 2026 17:20:09 +0800 Subject: [PATCH] feat: resolve the platform API key through an argc 8.1 value source, release 0.2.9 CLI and MCP read WBOT_API_KEY lazily from env, then the wbot auth set store (a custom Source), then the team test tenant key via latch only when WBOT_PLATFORM_URL targets the test origin. @schema lists the sources; a missing key fails with value_unavailable before any network request. --- bun.lock | 9 +- packages/wbot/CHANGELOG.md | 13 ++ packages/wbot/README.md | 2 + packages/wbot/package.json | 5 +- packages/wbot/wbot-agent-tools.bdd.md | 15 ++- packages/wbot/wbot-agent-tools.test.ts | 83 +++++++++++-- packages/wbot/wbot-cli.ts | 8 +- packages/wbot/wbot-config.ts | 116 ++++++++++-------- packages/wbot/wbot-mcp.ts | 16 ++- packages/wbot/wbot-public-entrypoint.test.ts | 20 ++- packages/wbot/wbot-values.ts | 34 +++++ .../claude/.claude-plugin/marketplace.json | 2 +- .../claude/wbot/.claude-plugin/plugin.json | 2 +- plugins/claude/wbot/.mcp.json | 2 +- plugins/codex/wbot/.codex-plugin/plugin.json | 2 +- plugins/codex/wbot/.mcp.json | 2 +- 16 files changed, 245 insertions(+), 86 deletions(-) create mode 100644 packages/wbot/wbot-values.ts diff --git a/bun.lock b/bun.lock index 26ec960..785881c 100644 --- a/bun.lock +++ b/bun.lock @@ -21,12 +21,13 @@ }, "packages/wbot": { "name": "@celados/wbot", - "version": "0.2.7", + "version": "0.2.8", "bin": { "wbot": "./dist/wbot.js", }, "devDependencies": { - "@celados/argc": "^8.0.0", + "@celados/argc": "^8.1.0", + "@celados/latch-source": "^0.1.0", "@modelcontextprotocol/sdk": "1.30.0", "@valibot/to-json-schema": "1.7.1", "valibot": "1.4.2", @@ -35,7 +36,9 @@ }, }, "packages": { - "@celados/argc": ["@celados/argc@8.0.0", "https://npm.celados.com/@celados/argc/-/@celados/argc-8.0.0.tgz", { "dependencies": { "yaml": "2.9.0" }, "peerDependencies": { "@standard-schema/spec": "^1.0.0" } }, "sha512-xVY1Dqta6Z5Fhx2QpDC4udfkC6903YOniosf0clmd034+Cvn56G968RIElW6n7BXw/oVTBBZTT+HnQInxImRtw=="], + "@celados/argc": ["@celados/argc@8.1.0", "https://npm.celados.com/@celados/argc/-/@celados/argc-8.1.0.tgz", { "dependencies": { "yaml": "2.9.0" }, "peerDependencies": { "@standard-schema/spec": "^1.0.0" } }, "sha512-tosY2BJY2orphzJKfb9LguW0vradVfKFv+G8lbDK03RBovLJdQYCZ5mPc2BulKp2+6jjQh/2CN1CqFdWxV3x1g=="], + + "@celados/latch-source": ["@celados/latch-source@0.1.0", "https://npm.celados.com/@celados/latch-source/-/@celados/latch-source-0.1.0.tgz", { "peerDependencies": { "@celados/argc": ">=8.1.0" } }, "sha512-RjPJV+DdgouKKr/DIMHIa/WMx62kzWezfbrnQgVQcbseSoZAb4cFV1LIgmKhzg1TflO6G5RHDtBk3mHzDUBqyQ=="], "@celados/wbot": ["@celados/wbot@workspace:packages/wbot"], diff --git a/packages/wbot/CHANGELOG.md b/packages/wbot/CHANGELOG.md index ce79d71..e0b6026 100644 --- a/packages/wbot/CHANGELOG.md +++ b/packages/wbot/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to the public wbot client are documented in this file. ## Unreleased +## 0.2.9 - 2026-11-06 + +### Changed + +- Update the CLI framework to `@celados/argc` 8.1.0. CLI and MCP resolve the API key through one + declared value source, lazily and only for commands that call the Platform: `WBOT_API_KEY`, then the + key saved by `wbot auth set`, then (only when `WBOT_PLATFORM_URL` is the test origin) the team test + Tenant key through latch. `wbot @schema` and `--help` list the sources under `values:`. +- A missing API key now fails with `error: DOMAIN_ERROR`, `code: value_unavailable` and a `tried:` list + (one entry per source, with reason and hint) instead of a `RUNTIME_ERROR` sentence. MCP tool errors + carry the same structured envelope. A damaged credentials file is reported as `invalid_credentials` + without echoing its content. + ## 0.2.8 - 2026-11-06 ### Changed diff --git a/packages/wbot/README.md b/packages/wbot/README.md index 052fb4e..d2f47de 100644 --- a/packages/wbot/README.md +++ b/packages/wbot/README.md @@ -18,6 +18,8 @@ wbot auth set `wbot auth set` reads the key without echoing it and stores it at `$XDG_CONFIG_HOME/wbot/credentials.json`, or `~/.config/wbot/credentials.json` when `XDG_CONFIG_HOME` is unset. For automation, set `WBOT_API_KEY`; it overrides the stored credential. `WBOT_PLATFORM_URL` is an optional development or self-hosted endpoint override. +The API key is resolved lazily, only by commands that call the Platform (`@schema`, `--help` and `auth set` never read it), from the first source that has one: `WBOT_API_KEY`, then the key saved by `wbot auth set`, then — only when `WBOT_PLATFORM_URL` is `https://wbot-api-test.celados.com` — the team's test Tenant key through [latch](https://github.com/celados/latch). `wbot @schema` lists these sources under `values:`. When none has a key, the command fails before any network request with `error: DOMAIN_ERROR`, `code: value_unavailable`, and one `tried:` entry per source with its reason and next step. + The default public API is `https://wbot-api-prod.celados.com`. Internal dogfood API keys created by the test deployment require `WBOT_PLATFORM_URL=https://wbot-api-test.celados.com`. This reuses `WBOT_API_KEY`, the local credentials file, and the same CLI and MCP schemas; there is no test-specific executable. The `wbot-cloud-prod.celados.com` and `wbot-cloud-test.celados.com` origins belong to Convex browser clients; they are not valid `WBOT_PLATFORM_URL` values. diff --git a/packages/wbot/package.json b/packages/wbot/package.json index ca695fd..23f0141 100644 --- a/packages/wbot/package.json +++ b/packages/wbot/package.json @@ -1,6 +1,6 @@ { "name": "@celados/wbot", - "version": "0.2.8", + "version": "0.2.9", "private": true, "description": "Agent-first conversation reading and authorized text and JPEG sending with read-only MCP tools", "license": "MIT", @@ -23,7 +23,8 @@ "build": "bun build ./wbot-cli.ts --target=bun --outfile=dist/wbot.js" }, "devDependencies": { - "@celados/argc": "^8.0.0", + "@celados/argc": "^8.1.0", + "@celados/latch-source": "^0.1.0", "@modelcontextprotocol/sdk": "1.30.0", "@valibot/to-json-schema": "1.7.1", "valibot": "1.4.2", diff --git a/packages/wbot/wbot-agent-tools.bdd.md b/packages/wbot/wbot-agent-tools.bdd.md index 38e7f0f..ddb6891 100644 --- a/packages/wbot/wbot-agent-tools.bdd.md +++ b/packages/wbot/wbot-agent-tools.bdd.md @@ -98,7 +98,7 @@ And 本地凭据文件保持不变 **场景 2.5:缺少凭据时在网络请求前失败** Given 环境变量和本地凭据文件都没有可用 API Key When Agent 通过 CLI 或 MCP 发起读取 -Then 动作在发出网络请求前失败 +Then 动作在发出网络请求前失败,错误为 `value_unavailable`,逐个列出尝试过的来源 And 错误指引用户运行 `wbot auth set` 或配置 `WBOT_API_KEY` And 错误不伪装成空会话列表 @@ -110,6 +110,19 @@ Then 动作在发出网络请求前失败 And 错误指引用户重新运行 `wbot auth set` And 错误不把文件内容输出给 Agent +**场景 2.8:未设置环境变量时复用 `wbot auth set` 保存的凭据** +Given 本地凭据文件已经保存一把 API Key +And 当前进程没有提供 `WBOT_API_KEY` +When Agent 通过 CLI 或 MCP 发起读取 +Then 本次调用使用本地凭据文件中的 API Key + +**场景 2.9:团队测试 key 不会流向生产调用** +Given 环境变量和本地凭据文件都没有可用 API Key +When Agent 以默认生产 endpoint 发起读取 +Then 团队 vault 中的测试 Tenant key 不参与解析,动作失败 +When 内部成员以 `WBOT_PLATFORM_URL=https://wbot-api-test.celados.com` 发起读取 +Then CLI 通过 latch 读取团队测试 Tenant key,无需渲染 `.env` 或保存本地凭据 + **场景 2.7:普通用户不需要配置 Platform URL** Given 用户使用公开发布的 wbot 工具 And 用户没有提供 endpoint 覆盖 diff --git a/packages/wbot/wbot-agent-tools.test.ts b/packages/wbot/wbot-agent-tools.test.ts index e2cfb2a..1732583 100644 --- a/packages/wbot/wbot-agent-tools.test.ts +++ b/packages/wbot/wbot-agent-tools.test.ts @@ -12,7 +12,11 @@ import { afterEach, describe, expect, test } from "vitest"; import type { PlatformClient } from "./platform-client"; -import { DEFAULT_WBOT_PLATFORM_URL, resolveWbotConfig } from "./wbot-config"; +import { + DEFAULT_WBOT_PLATFORM_URL, + TEST_WBOT_PLATFORM_URL, + resolveWbotPlatformUrl, +} from "./wbot-config"; import { createWbotMcpServer } from "./wbot-mcp"; type PluginMcpConfig = { @@ -76,16 +80,52 @@ describe("功能 2:CLI 与 MCP 复用安全的本机凭据", () => { test("场景 2.4:环境变量 API Key 覆盖本地凭据", async () => { const configRoot = await createTemporaryDirectory(); await writeCredentials(configRoot, "file-secret"); + const platform = await createRecordingPlatformServer(); - const config = await resolveWbotConfig({ + const result = await runCli(["conversations.list"], { WBOT_API_KEY: "environment-secret", + WBOT_PLATFORM_URL: platform.url, XDG_CONFIG_HOME: configRoot, }); - expect(config.apiKey).toBe("environment-secret"); + expect(result.exitCode, result.stderr).toBe(0); + expect(platform.authorizations).toEqual(["Bearer environment-secret"]); expect(await readCredentials(configRoot)).toContain("file-secret"); }); + test("场景 2.8:未设置环境变量时使用 wbot auth set 保存的凭据", async () => { + const configRoot = await createTemporaryDirectory(); + await writeCredentials(configRoot, "file-secret"); + const platform = await createRecordingPlatformServer(); + + const result = await runCli(["conversations.list"], { + WBOT_PLATFORM_URL: platform.url, + XDG_CONFIG_HOME: configRoot, + }); + + expect(result.exitCode, result.stderr).toBe(0); + expect(platform.authorizations).toEqual(["Bearer file-secret"]); + }); + + test("场景 2.9:团队 vault 中的测试 key 只在目标为测试服务时参与解析", async () => { + const configRoot = await createTemporaryDirectory(); + // NODE_ENV=test keeps the latch source from ever running the real vault. + const environment = { XDG_CONFIG_HOME: configRoot, NODE_ENV: "test" }; + + const production = await runCli(["conversations.list"], environment); + const testPlatform = await runCli(["conversations.list"], { + ...environment, + WBOT_PLATFORM_URL: TEST_WBOT_PLATFORM_URL, + }); + + expect(production.stderr).toContain("value_unavailable"); + expect(production.stderr).toContain("not_test_platform"); + expect(production.stderr).not.toContain("disabled_in_test"); + expect(testPlatform.stderr).toContain("value_unavailable"); + expect(testPlatform.stderr).toContain("disabled_in_test"); + expect(testPlatform.stderr).not.toContain("not_test_platform"); + }); + test("场景 2.5:缺少凭据时 CLI 在网络请求前失败", async () => { const configRoot = await createTemporaryDirectory(); const result = await runCli(["conversations.list"], { @@ -95,7 +135,8 @@ describe("功能 2:CLI 与 MCP 复用安全的本机凭据", () => { expect(result.exitCode).not.toBe(0); expect(result.stdout).toBe(""); - expect(result.stderr).toContain("RUNTIME_ERROR"); + expect(result.stderr).toContain("DOMAIN_ERROR"); + expect(result.stderr).toContain("value_unavailable"); expect(result.stderr).toContain("wbot auth set"); expect(result.stderr).toContain("WBOT_API_KEY"); }); @@ -115,15 +156,11 @@ describe("功能 2:CLI 与 MCP 复用安全的本机凭据", () => { expect(result.stderr).not.toContain(damagedSecret); }); - test("场景 2.7:默认使用生产 endpoint,并允许显式覆盖", async () => { - const defaultConfig = await resolveWbotConfig({ WBOT_API_KEY: "secret" }); - const overriddenConfig = await resolveWbotConfig({ - WBOT_API_KEY: "secret", - WBOT_PLATFORM_URL: "http://localhost:8787", - }); - - expect(defaultConfig.baseUrl).toBe(DEFAULT_WBOT_PLATFORM_URL); - expect(overriddenConfig.baseUrl).toBe("http://localhost:8787"); + test("场景 2.7:默认使用生产 endpoint,并允许显式覆盖", () => { + expect(resolveWbotPlatformUrl({})).toBe(DEFAULT_WBOT_PLATFORM_URL); + expect(resolveWbotPlatformUrl({ WBOT_PLATFORM_URL: "http://localhost:8787" })).toBe( + "http://localhost:8787", + ); }); test("场景 2.3:非交互 auth set 立即失败并指引环境变量", async () => { @@ -375,6 +412,26 @@ const createTemporaryDirectory = async () => { return path; }; +const createRecordingPlatformServer = () => + new Promise<{ url: string; authorizations: Array }>((resolve, reject) => { + const authorizations: Array = []; + const server = createServer((request, response) => { + authorizations.push(request.headers.authorization); + response.setHeader("Content-Type", "application/json"); + response.end(JSON.stringify({ items: [], nextCursor: "next", hasMore: false })); + }); + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + const address = server.address(); + if (address === null || typeof address === "string") { + reject(new Error("Test server did not expose a TCP address")); + return; + } + servers.push(server); + resolve({ url: `http://127.0.0.1:${address.port}`, authorizations }); + }); + }); + const credentialsPath = (configRoot: string) => join(configRoot, "wbot", "credentials.json"); const writeCredentials = async (configRoot: string, apiKey: string, validJson = true) => { diff --git a/packages/wbot/wbot-cli.ts b/packages/wbot/wbot-cli.ts index 09915ce..d1b29ff 100755 --- a/packages/wbot/wbot-cli.ts +++ b/packages/wbot/wbot-cli.ts @@ -11,8 +11,9 @@ import * as v from "valibot"; import type { WbotClient } from "./platform-client"; import packageJson from "./package.json" with { type: "json" }; -import { createWbotPlatformClientFromEnvironment, storeWbotApiKey } from "./wbot-config"; +import { storeWbotApiKey } from "./wbot-config"; import { runWbotMcpServer } from "./wbot-mcp"; +import { createWbotPlatformClient, wbotValues } from "./wbot-values"; const schema = toStandardJsonSchema; const nonEmptyString = v.pipe(v.string(), v.minLength(1)); @@ -124,11 +125,12 @@ const app = cli(commands, { name: "wbot", version: packageJson.version, description: "Read WeChat conversations and send authorized text or JPEG images through wbot", + values: wbotValues, }); export const runWbotCli = async () => { const callPlatform = async (operation: (client: WbotClient) => Promise) => - operation(await createWbotPlatformClientFromEnvironment(process.env)); + operation(await createWbotPlatformClient()); await app.run({ handlers: { @@ -201,7 +203,7 @@ export const runWbotCli = async () => { get: async (args) => JSON.stringify(await callPlatform((client) => client.getOutboundSend(args.input))), }, - mcp: runWbotMcpServer, + mcp: () => runWbotMcpServer(createWbotPlatformClient), }, }); }; diff --git a/packages/wbot/wbot-config.ts b/packages/wbot/wbot-config.ts index a3dce36..7902641 100644 --- a/packages/wbot/wbot-config.ts +++ b/packages/wbot/wbot-config.ts @@ -1,21 +1,15 @@ +import type { Source, SourceResult } from "@celados/argc/values"; + import { chmod, mkdir, readFile, writeFile } from "node:fs/promises"; import { homedir } from "node:os"; import { join } from "node:path"; import * as v from "valibot"; -import type { WbotClient } from "./platform-client"; - -import { createPlatformClient } from "./platform-client"; - export const DEFAULT_WBOT_PLATFORM_URL = "https://wbot-api-prod.celados.com"; +export const TEST_WBOT_PLATFORM_URL = "https://wbot-api-test.celados.com"; type WbotEnvironment = Record; -export type WbotConfig = { - apiKey: string; - baseUrl: string; -}; - const credentialsSchema = v.strictObject({ apiKey: v.pipe(v.string(), v.minLength(1)), }); @@ -25,17 +19,71 @@ export const wbotCredentialsPath = (environment: WbotEnvironment) => { return join(configRoot, "wbot", "credentials.json"); }; -export const resolveWbotConfig = async (environment: WbotEnvironment): Promise => { - const apiKey = - environment.WBOT_API_KEY === undefined || environment.WBOT_API_KEY.length === 0 - ? await readStoredApiKey(environment) - : environment.WBOT_API_KEY; - return { - apiKey, - baseUrl: environment.WBOT_PLATFORM_URL ?? DEFAULT_WBOT_PLATFORM_URL, - }; +export const resolveWbotPlatformUrl = (environment: WbotEnvironment = process.env) => + environment.WBOT_PLATFORM_URL ?? DEFAULT_WBOT_PLATFORM_URL; + +const isTestPlatform = (url: string) => { + try { + return new URL(url).origin === TEST_WBOT_PLATFORM_URL; + } catch { + return false; + } +}; + +/** The key saved by `wbot auth set`: the public, user-facing credential store. */ +export const storedCredentials: Source = { + label: "wbot auth set ($XDG_CONFIG_HOME/wbot/credentials.json)", + read: async (): Promise => { + let source: string; + try { + source = await readFile(wbotCredentialsPath(process.env), "utf8"); + } catch (error) { + return isMissingFileError(error) + ? { + found: false, + reason: "not configured", + code: "not_configured", + hint: "run `wbot auth set` in an interactive terminal, or set WBOT_API_KEY", + } + : { + found: false, + reason: "unreadable", + code: "unreadable_credentials", + hint: "run `wbot auth set` again, or set WBOT_API_KEY", + }; + } + try { + return { found: true, value: v.parse(credentialsSchema, JSON.parse(source)).apiKey }; + } catch { + // Never echo the file: a damaged file may still hold most of a key. + return { + found: false, + reason: "invalid credentials file", + code: "invalid_credentials", + hint: "run `wbot auth set` again, or set WBOT_API_KEY", + }; + } + }, }; +/** + * The team vault entry is the *test* tenant key. It is consulted only when the + * CLI targets the test platform, so a production call never silently receives + * a test key (and external users, who target production, never reach latch). + */ +export const onlyForTestPlatform = (source: Source): Source => ({ + label: `${source.label}, only when WBOT_PLATFORM_URL=${TEST_WBOT_PLATFORM_URL}`, + read: () => + isTestPlatform(resolveWbotPlatformUrl()) + ? source.read() + : { + found: false, + reason: "not the test platform", + code: "not_test_platform", + hint: `the vault entry is the test tenant key; it is used only with WBOT_PLATFORM_URL=${TEST_WBOT_PLATFORM_URL}`, + }, +}); + export const storeWbotApiKey = async (environment: WbotEnvironment, apiKey: string) => { if (apiKey.length === 0) throw new Error("wbot API key must be non-empty."); const path = wbotCredentialsPath(environment); @@ -45,37 +93,5 @@ export const storeWbotApiKey = async (environment: WbotEnvironment, apiKey: stri return path; }; -export const createWbotPlatformClientFromEnvironment = ( - environment: WbotEnvironment, -): Promise => - resolveWbotConfig(environment).then((config) => - createPlatformClient({ - baseUrl: config.baseUrl, - tenantApiKey: config.apiKey, - onDeprecationWarning: (warning) => { - process.stderr.write(`${warning}\n`); - }, - }), - ); - -const readStoredApiKey = async (environment: WbotEnvironment) => { - const path = wbotCredentialsPath(environment); - let source: string; - try { - source = await readFile(path, "utf8"); - } catch (error) { - if (isMissingFileError(error)) { - throw new Error("Missing wbot API key. Run `wbot auth set` or set WBOT_API_KEY."); - } - throw new Error("Unable to read wbot credentials. Run `wbot auth set` again."); - } - try { - const parsed = v.parse(credentialsSchema, JSON.parse(source)); - return parsed.apiKey; - } catch { - throw new Error("Invalid wbot credentials. Run `wbot auth set` again or set WBOT_API_KEY."); - } -}; - const isMissingFileError = (error: unknown) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT"; diff --git a/packages/wbot/wbot-mcp.ts b/packages/wbot/wbot-mcp.ts index e5a97d6..be8321a 100644 --- a/packages/wbot/wbot-mcp.ts +++ b/packages/wbot/wbot-mcp.ts @@ -5,7 +5,6 @@ import { z } from "zod"; import type { PlatformClient } from "./platform-client"; import packageJson from "./package.json" with { type: "json" }; -import { createWbotPlatformClientFromEnvironment } from "./wbot-config"; type PlatformClientSource = PlatformClient | (() => PlatformClient | Promise); @@ -63,8 +62,8 @@ export const createWbotMcpServer = (clientSource: PlatformClientSource) => { return server; }; -export const runWbotMcpServer = async () => { - const server = createWbotMcpServer(() => createWbotPlatformClientFromEnvironment(process.env)); +export const runWbotMcpServer = async (clientSource: PlatformClientSource) => { + const server = createWbotMcpServer(clientSource); await server.connect(new StdioServerTransport()); }; @@ -85,9 +84,18 @@ const callTool = async ( content: [ { type: "text" as const, - text: error instanceof Error ? error.message : "wbot request failed", + text: describeToolError(error), }, ], }; } }; + +// argc domain errors (e.g. `value_unavailable` for a missing API key) carry +// their structured cause in `envelope`; their message alone is "DOMAIN_ERROR". +const describeToolError = (error: unknown) => { + if (typeof error === "object" && error !== null && "envelope" in error) { + return JSON.stringify(error.envelope, null, 2); + } + return error instanceof Error ? error.message : "wbot request failed"; +}; diff --git a/packages/wbot/wbot-public-entrypoint.test.ts b/packages/wbot/wbot-public-entrypoint.test.ts index 22e2457..1dca335 100644 --- a/packages/wbot/wbot-public-entrypoint.test.ts +++ b/packages/wbot/wbot-public-entrypoint.test.ts @@ -10,7 +10,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { afterEach, describe, expect, test } from "vitest"; -import { DEFAULT_WBOT_PLATFORM_URL, resolveWbotConfig } from "./wbot-config"; +import { DEFAULT_WBOT_PLATFORM_URL, resolveWbotPlatformUrl } from "./wbot-config"; const packageRoot = new URL(".", import.meta.url).pathname; const cliEntry = join(packageRoot, "wbot-cli.ts"); @@ -60,6 +60,18 @@ describe("功能 1:外部用户只看到一个产品入口", () => { expect(result.stdout).not.toMatch(/operator/i); }); + test("场景 1.4:@schema 列出 API key 的来源,但不读取它们", async () => { + const result = await runCli(["@schema"]); + + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stdout).toMatch(/values:[\s\S]*WBOT_API_KEY/); + expect(result.stdout).toContain("env WBOT_API_KEY"); + expect(result.stdout).toContain("wbot auth set"); + expect(result.stdout).toContain( + "only when WBOT_PLATFORM_URL=https://wbot-api-test.celados.com", + ); + }); + test("场景 1.3:MCP host 通过 wbot mcp 启动服务", async () => { const fixture = await createStdioMcpFixture(); @@ -76,10 +88,8 @@ describe("功能 1:外部用户只看到一个产品入口", () => { }); describe("功能 2:环境选择不扩张公开命令面", () => { - test("场景 2.1:默认连接生产服务", async () => { - const config = await resolveWbotConfig(createFixture().environment); - - expect(config.baseUrl).toBe(DEFAULT_WBOT_PLATFORM_URL); + test("场景 2.1:默认连接生产服务", () => { + expect(resolveWbotPlatformUrl(createFixture().environment)).toBe(DEFAULT_WBOT_PLATFORM_URL); }); test("场景 2.2:内部 dogfood 显式连接测试服务", async () => { diff --git a/packages/wbot/wbot-values.ts b/packages/wbot/wbot-values.ts new file mode 100644 index 0000000..0fcb758 --- /dev/null +++ b/packages/wbot/wbot-values.ts @@ -0,0 +1,34 @@ +import { defineValueSource, env } from "@celados/argc/values"; +import { latch } from "@celados/latch-source"; +import * as v from "valibot"; + +import type { WbotClient } from "./platform-client"; + +import { createPlatformClient } from "./platform-client"; +import { onlyForTestPlatform, resolveWbotPlatformUrl, storedCredentials } from "./wbot-config"; + +export const wbotValues = defineValueSource({ + WBOT_API_KEY: { + schema: v.pipe(v.string(), v.minLength(1)), + description: "wbot Tenant API key", + get: [ + env("WBOT_API_KEY"), + storedCredentials, + onlyForTestPlatform( + latch("c0b64757-d366-4f25-a951-947d796e392e/password", { + item: "wbot / test / platform-tenant-key", + }), + ), + ], + }, +}); + +/** Resolves the key lazily: only commands that call the platform reach a source. */ +export const createWbotPlatformClient = async (): Promise => + createPlatformClient({ + baseUrl: resolveWbotPlatformUrl(), + tenantApiKey: await wbotValues.WBOT_API_KEY, + onDeprecationWarning: (warning) => { + process.stderr.write(`${warning}\n`); + }, + }); diff --git a/plugins/claude/.claude-plugin/marketplace.json b/plugins/claude/.claude-plugin/marketplace.json index be1fc4c..32a02fa 100644 --- a/plugins/claude/.claude-plugin/marketplace.json +++ b/plugins/claude/.claude-plugin/marketplace.json @@ -8,7 +8,7 @@ "name": "wbot", "source": "./wbot", "description": "Read authorized WeChat conversations through wbot.", - "version": "0.2.8" + "version": "0.2.9" } ] } diff --git a/plugins/claude/wbot/.claude-plugin/plugin.json b/plugins/claude/wbot/.claude-plugin/plugin.json index c175e3e..b484c47 100644 --- a/plugins/claude/wbot/.claude-plugin/plugin.json +++ b/plugins/claude/wbot/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "wbot", - "version": "0.2.8", + "version": "0.2.9", "description": "Read authorized WeChat conversations through wbot.", "author": { "name": "Celados" diff --git a/plugins/claude/wbot/.mcp.json b/plugins/claude/wbot/.mcp.json index 13a0d6a..060ca48 100644 --- a/plugins/claude/wbot/.mcp.json +++ b/plugins/claude/wbot/.mcp.json @@ -5,7 +5,7 @@ "args": [ "--bun", "--package", - "@celados/wbot@https://github.com/celados/wbot/releases/download/v0.2.8/wbot-0.2.8.tgz", + "@celados/wbot@https://github.com/celados/wbot/releases/download/v0.2.9/wbot-0.2.9.tgz", "wbot", "mcp" ] diff --git a/plugins/codex/wbot/.codex-plugin/plugin.json b/plugins/codex/wbot/.codex-plugin/plugin.json index e70e6cc..1203977 100644 --- a/plugins/codex/wbot/.codex-plugin/plugin.json +++ b/plugins/codex/wbot/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "wbot", - "version": "0.2.8", + "version": "0.2.9", "description": "Read authorized WeChat conversations through wbot.", "author": { "name": "Celados" diff --git a/plugins/codex/wbot/.mcp.json b/plugins/codex/wbot/.mcp.json index 13a0d6a..060ca48 100644 --- a/plugins/codex/wbot/.mcp.json +++ b/plugins/codex/wbot/.mcp.json @@ -5,7 +5,7 @@ "args": [ "--bun", "--package", - "@celados/wbot@https://github.com/celados/wbot/releases/download/v0.2.8/wbot-0.2.8.tgz", + "@celados/wbot@https://github.com/celados/wbot/releases/download/v0.2.9/wbot-0.2.9.tgz", "wbot", "mcp" ]