From ba95d43e3ce1d961febd14131b28cfcefae75dc1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 24 Mar 2026 10:03:58 -0700 Subject: [PATCH] refactor: split feishu runtime and inspect secret resolution --- extensions/feishu/src/accounts.test.ts | 86 ++++++ extensions/feishu/src/accounts.ts | 266 ++++++++++++------ extensions/feishu/src/bot.card-action.test.ts | 3 +- extensions/feishu/src/bot.ts | 4 +- extensions/feishu/src/card-action.ts | 4 +- extensions/feishu/src/channel.ts | 10 +- extensions/feishu/src/media.test.ts | 1 + extensions/feishu/src/media.ts | 8 +- extensions/feishu/src/monitor.ts | 7 +- extensions/feishu/src/pins.ts | 8 +- extensions/feishu/src/reactions.ts | 4 +- .../feishu/src/reply-dispatcher.test.ts | 5 +- extensions/feishu/src/reply-dispatcher.ts | 4 +- extensions/feishu/src/send-target.test.ts | 1 + extensions/feishu/src/send-target.ts | 4 +- extensions/feishu/src/send.test.ts | 1 + extensions/feishu/src/send.ts | 10 +- extensions/feishu/src/setup-surface.ts | 10 +- extensions/feishu/src/tool-account.ts | 4 +- extensions/feishu/src/typing.ts | 6 +- 20 files changed, 320 insertions(+), 126 deletions(-) diff --git a/extensions/feishu/src/accounts.test.ts b/extensions/feishu/src/accounts.test.ts index 0e800a818f1..6f0392c8dc7 100644 --- a/extensions/feishu/src/accounts.test.ts +++ b/extensions/feishu/src/accounts.test.ts @@ -1,9 +1,12 @@ import { describe, expect, it } from "vitest"; import { + FeishuSecretRefUnavailableError, + inspectFeishuCredentials, resolveDefaultFeishuAccountId, resolveDefaultFeishuAccountSelection, resolveFeishuAccount, resolveFeishuCredentials, + resolveFeishuRuntimeAccount, } from "./accounts.js"; import type { FeishuConfig } from "./types.js"; @@ -169,6 +172,18 @@ describe("resolveFeishuCredentials", () => { expect(creds).toBeNull(); }); + it("supports explicit inspect mode for unresolved SecretRefs", () => { + const creds = resolveFeishuCredentials( + asConfig({ + appId: "cli_123", + appSecret: { source: "file", provider: "default", id: "path/to/secret" } as never, + }), + { mode: "inspect" }, + ); + + expect(creds).toBeNull(); + }); + it("throws unresolved SecretRef error when env SecretRef points to missing env var", () => { const key = "FEISHU_APP_SECRET_MISSING_TEST"; withEnvVar(key, undefined, () => { @@ -274,6 +289,24 @@ describe("resolveFeishuCredentials", () => { domain: "feishu", }); }); + + it("keeps required credentials when optional event SecretRefs are unresolved in inspect mode", () => { + const creds = inspectFeishuCredentials( + asConfig({ + appId: "cli_123", + appSecret: "secret_456", + verificationToken: { source: "file", provider: "default", id: "path/to/token" } as never, + }), + ); + + expect(creds).toEqual({ + appId: "cli_123", + appSecret: "secret_456", // pragma: allowlist secret + encryptKey: undefined, + verificationToken: undefined, + domain: "feishu", + }); + }); }); describe("resolveFeishuAccount", () => { @@ -348,6 +381,59 @@ describe("resolveFeishuAccount", () => { expect(account.appSecret).toBeUndefined(); }); + it("keeps account configured when optional event SecretRefs are unresolved in inspect mode", () => { + const account = resolveFeishuAccount({ + cfg: { + channels: { + feishu: { + accounts: { + main: { + appId: "cli_123", + appSecret: "secret_456", + verificationToken: { + source: "file", + provider: "default", + id: "path/to/token", + }, + } as never, + }, + }, + }, + } as never, + accountId: "main", + }); + + expect(account.configured).toBe(true); + expect(account.appSecret).toBe("secret_456"); + expect(account.verificationToken).toBeUndefined(); + }); + + it("throws typed SecretRef errors in runtime account resolution", () => { + let caught: unknown; + try { + resolveFeishuRuntimeAccount({ + cfg: { + channels: { + feishu: { + accounts: { + main: { + appId: "cli_123", + appSecret: { source: "file", provider: "default", id: "path/to/secret" }, + } as never, + }, + }, + }, + } as never, + accountId: "main", + }); + } catch (error) { + caught = error; + } + + expect(caught).toBeInstanceOf(FeishuSecretRefUnavailableError); + expect((caught as Error).message).toMatch(/channels\.feishu\.appSecret: unresolved SecretRef/i); + }); + it("does not throw when account name is non-string", () => { expect(() => resolveFeishuAccount({ diff --git a/extensions/feishu/src/accounts.ts b/extensions/feishu/src/accounts.ts index bf3631f595d..c25ef543212 100644 --- a/extensions/feishu/src/accounts.ts +++ b/extensions/feishu/src/accounts.ts @@ -5,8 +5,8 @@ import { normalizeOptionalAccountId, resolveMergedAccountConfig, } from "openclaw/plugin-sdk/account-resolution"; +import { coerceSecretRef } from "openclaw/plugin-sdk/config-runtime"; import type { ClawdbotConfig } from "../runtime-api.js"; -import { normalizeResolvedSecretInputString, normalizeSecretInputString } from "./secret-input.js"; import type { FeishuConfig, FeishuAccountConfig, @@ -25,11 +25,125 @@ const { export { listFeishuAccountIds }; -function isUnresolvedSecretRefError(error: unknown): boolean { - if (!(error instanceof Error)) { - return false; +type FeishuCredentialResolutionMode = "inspect" | "strict"; +type FeishuResolvedSecretRef = NonNullable>; + +function normalizeString(value: unknown): string | undefined { + if (typeof value !== "string") { + return undefined; } - return /unresolved SecretRef/i.test(error.message); + const trimmed = value.trim(); + return trimmed ? trimmed : undefined; +} + +function formatSecretRefLabel(ref: FeishuResolvedSecretRef): string { + return `${ref.source}:${ref.provider}:${ref.id}`; +} + +export class FeishuSecretRefUnavailableError extends Error { + path: string; + + constructor(path: string, ref: FeishuResolvedSecretRef) { + super( + `${path}: unresolved SecretRef "${formatSecretRefLabel(ref)}". ` + + "Resolve this command against an active gateway runtime snapshot before reading it.", + ); + this.name = "FeishuSecretRefUnavailableError"; + this.path = path; + } +} + +export function isFeishuSecretRefUnavailableError( + error: unknown, +): error is FeishuSecretRefUnavailableError { + return error instanceof FeishuSecretRefUnavailableError; +} + +function resolveFeishuSecretLike(params: { + value: unknown; + path: string; + mode: FeishuCredentialResolutionMode; + allowEnvSecretRefRead?: boolean; +}): string | undefined { + const asString = normalizeString(params.value); + if (asString) { + return asString; + } + + const ref = coerceSecretRef(params.value); + if (!ref) { + return undefined; + } + + if (params.mode === "inspect") { + if (params.allowEnvSecretRefRead && ref.source === "env") { + const envValue = normalizeString(process.env[ref.id]); + if (envValue) { + return envValue; + } + } + return undefined; + } + + throw new FeishuSecretRefUnavailableError(params.path, ref); +} + +function resolveFeishuBaseCredentials( + cfg: FeishuConfig | undefined, + mode: FeishuCredentialResolutionMode, +): { + appId: string; + appSecret: string; + domain: FeishuDomain; +} | null { + const appId = resolveFeishuSecretLike({ + value: cfg?.appId, + path: "channels.feishu.appId", + mode, + allowEnvSecretRefRead: true, + }); + const appSecret = resolveFeishuSecretLike({ + value: cfg?.appSecret, + path: "channels.feishu.appSecret", + mode, + allowEnvSecretRefRead: true, + }); + + if (!appId || !appSecret) { + return null; + } + + return { + appId, + appSecret, + domain: cfg?.domain ?? "feishu", + }; +} + +function resolveFeishuEventSecrets( + cfg: FeishuConfig | undefined, + mode: FeishuCredentialResolutionMode, +): { + encryptKey?: string; + verificationToken?: string; +} { + return { + encryptKey: + (cfg?.connectionMode ?? "websocket") === "webhook" + ? resolveFeishuSecretLike({ + value: cfg?.encryptKey, + path: "channels.feishu.encryptKey", + mode, + allowEnvSecretRefRead: true, + }) + : normalizeString(cfg?.encryptKey), + verificationToken: resolveFeishuSecretLike({ + value: cfg?.verificationToken, + path: "channels.feishu.verificationToken", + mode, + allowEnvSecretRefRead: true, + }), + }; } /** @@ -94,7 +208,10 @@ export function resolveFeishuCredentials(cfg?: FeishuConfig): { } | null; export function resolveFeishuCredentials( cfg: FeishuConfig | undefined, - options: { allowUnresolvedSecretRef?: boolean }, + options: { + mode?: FeishuCredentialResolutionMode; + allowUnresolvedSecretRef?: boolean; + }, ): { appId: string; appSecret: string; @@ -104,7 +221,10 @@ export function resolveFeishuCredentials( } | null; export function resolveFeishuCredentials( cfg?: FeishuConfig, - options?: { allowUnresolvedSecretRef?: boolean }, + options?: { + mode?: FeishuCredentialResolutionMode; + allowUnresolvedSecretRef?: boolean; + }, ): { appId: string; appSecret: string; @@ -112,68 +232,28 @@ export function resolveFeishuCredentials( verificationToken?: string; domain: FeishuDomain; } | null { - const normalizeString = (value: unknown): string | undefined => { - if (typeof value !== "string") { - return undefined; - } - const trimmed = value.trim(); - return trimmed ? trimmed : undefined; - }; - - const resolveSecretLike = (value: unknown, path: string): string | undefined => { - const asString = normalizeString(value); - if (asString) { - return asString; - } - - // In relaxed/setup paths only: allow direct env SecretRef reads for UX. - // Default resolution path must preserve unresolved-ref diagnostics/policy semantics. - if (options?.allowUnresolvedSecretRef && typeof value === "object" && value !== null) { - const rec = value as Record; - const source = normalizeString(rec.source)?.toLowerCase(); - const id = normalizeString(rec.id); - if (source === "env" && id) { - const envValue = normalizeString(process.env[id]); - if (envValue) { - return envValue; - } - } - } - - if (options?.allowUnresolvedSecretRef) { - return normalizeSecretInputString(value); - } - return normalizeResolvedSecretInputString({ value, path }); - }; - - const appId = resolveSecretLike(cfg?.appId, "channels.feishu.appId"); - const appSecret = resolveSecretLike(cfg?.appSecret, "channels.feishu.appSecret"); - - if (!appId || !appSecret) { + const mode = options?.mode ?? (options?.allowUnresolvedSecretRef ? "inspect" : "strict"); + const base = resolveFeishuBaseCredentials(cfg, mode); + if (!base) { return null; } - const connectionMode = cfg?.connectionMode ?? "websocket"; + const eventSecrets = resolveFeishuEventSecrets(cfg, mode); + return { - appId, - appSecret, - encryptKey: - connectionMode === "webhook" - ? resolveSecretLike(cfg?.encryptKey, "channels.feishu.encryptKey") - : normalizeString(cfg?.encryptKey), - verificationToken: resolveSecretLike( - cfg?.verificationToken, - "channels.feishu.verificationToken", - ), - domain: cfg?.domain ?? "feishu", + ...base, + ...eventSecrets, }; } -/** - * Resolve a complete Feishu account with merged config. - */ -export function resolveFeishuAccount(params: { +export function inspectFeishuCredentials(cfg?: FeishuConfig) { + return resolveFeishuCredentials(cfg, { mode: "inspect" }); +} + +function buildResolvedFeishuAccount(params: { cfg: ClawdbotConfig; accountId?: string | null; + baseMode: FeishuCredentialResolutionMode; + eventSecretMode: FeishuCredentialResolutionMode; }): ResolvedFeishuAccount { const hasExplicitAccountId = typeof params.accountId === "string" && params.accountId.trim() !== ""; @@ -188,44 +268,62 @@ export function resolveFeishuAccount(params: { : (defaultSelection?.source ?? "fallback"); const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined; - // Base enabled state (top-level) const baseEnabled = feishuCfg?.enabled !== false; - - // Merge configs const merged = mergeFeishuAccountConfig(params.cfg, accountId); - - // Account-level enabled state const accountEnabled = merged.enabled !== false; const enabled = baseEnabled && accountEnabled; - - // Resolve credentials from merged config. - // CLI startup can parse config before gateway-backed SecretRef resolution is available. - // Treat unresolved refs as "not configured" here instead of crashing plugin load. - let creds: ReturnType = null; - try { - creds = resolveFeishuCredentials(merged); - } catch (error) { - if (!isUnresolvedSecretRefError(error)) { - throw error; - } - } + const baseCreds = resolveFeishuBaseCredentials(merged, params.baseMode); + const eventSecrets = resolveFeishuEventSecrets(merged, params.eventSecretMode); const accountName = (merged as FeishuAccountConfig).name; return { accountId, selectionSource, enabled, - configured: Boolean(creds), + configured: Boolean(baseCreds), name: typeof accountName === "string" ? accountName.trim() || undefined : undefined, - appId: creds?.appId, - appSecret: creds?.appSecret, - encryptKey: creds?.encryptKey, - verificationToken: creds?.verificationToken, - domain: creds?.domain ?? "feishu", + appId: baseCreds?.appId, + appSecret: baseCreds?.appSecret, + encryptKey: eventSecrets.encryptKey, + verificationToken: eventSecrets.verificationToken, + domain: baseCreds?.domain ?? "feishu", config: merged, }; } +/** + * Resolve a read-only Feishu account snapshot for CLI/config surfaces. + * Unresolved SecretRefs are treated as unavailable instead of throwing. + */ +export function resolveFeishuAccount(params: { + cfg: ClawdbotConfig; + accountId?: string | null; +}): ResolvedFeishuAccount { + return buildResolvedFeishuAccount({ + ...params, + baseMode: "inspect", + eventSecretMode: "inspect", + }); +} + +/** + * Resolve a runtime Feishu account. + * Required app credentials stay strict; event-only secrets can be required by callers. + */ +export function resolveFeishuRuntimeAccount( + params: { + cfg: ClawdbotConfig; + accountId?: string | null; + }, + options?: { requireEventSecrets?: boolean }, +): ResolvedFeishuAccount { + return buildResolvedFeishuAccount({ + ...params, + baseMode: "strict", + eventSecretMode: options?.requireEventSecrets ? "strict" : "inspect", + }); +} + /** * List all enabled and configured accounts. */ diff --git a/extensions/feishu/src/bot.card-action.test.ts b/extensions/feishu/src/bot.card-action.test.ts index 0dd3cf8730c..dd2ad6da910 100644 --- a/extensions/feishu/src/bot.card-action.test.ts +++ b/extensions/feishu/src/bot.card-action.test.ts @@ -11,9 +11,10 @@ import { FEISHU_APPROVAL_REQUEST_ACTION, } from "./card-ux-approval.js"; -// Mock resolveFeishuAccount +// Mock account resolution vi.mock("./accounts.js", () => ({ resolveFeishuAccount: vi.fn().mockReturnValue({ accountId: "mock-account" }), + resolveFeishuRuntimeAccount: vi.fn().mockReturnValue({ accountId: "mock-account" }), })); // Mock bot.js to verify handleFeishuMessage call diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index 63b898a23fb..9c73ee4b6e5 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -20,7 +20,7 @@ import { resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce, } from "../runtime-api.js"; -import { resolveFeishuAccount } from "./accounts.js"; +import { resolveFeishuRuntimeAccount } from "./accounts.js"; import { checkBotMentioned, normalizeFeishuCommandProbeBody, @@ -240,7 +240,7 @@ export async function handleFeishuMessage(params: { } = params; // Resolve account with merged config - const account = resolveFeishuAccount({ cfg, accountId }); + const account = resolveFeishuRuntimeAccount({ cfg, accountId }); const feishuCfg = account.config; const log = runtime?.log ?? console.log; diff --git a/extensions/feishu/src/card-action.ts b/extensions/feishu/src/card-action.ts index 6b1b542010e..f01a8a5b906 100644 --- a/extensions/feishu/src/card-action.ts +++ b/extensions/feishu/src/card-action.ts @@ -1,5 +1,5 @@ import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js"; -import { resolveFeishuAccount } from "./accounts.js"; +import { resolveFeishuRuntimeAccount } from "./accounts.js"; import { handleFeishuMessage, type FeishuMessageEvent } from "./bot.js"; import { decodeFeishuCardAction, buildFeishuCardActionTextFallback } from "./card-interaction.js"; import { @@ -174,7 +174,7 @@ export async function handleFeishuCardAction(params: { accountId?: string; }): Promise { const { cfg, event, runtime, accountId } = params; - const account = resolveFeishuAccount({ cfg, accountId }); + const account = resolveFeishuRuntimeAccount({ cfg, accountId }); const log = runtime?.log ?? console.log; const decoded = decodeFeishuCardAction({ event }); const claimedToken = beginFeishuCardActionToken({ diff --git a/extensions/feishu/src/channel.ts b/extensions/feishu/src/channel.ts index 13111a4e92e..fe6ad5dda51 100644 --- a/extensions/feishu/src/channel.ts +++ b/extensions/feishu/src/channel.ts @@ -33,8 +33,9 @@ import { } from "../runtime-api.js"; import type { ChannelMessageActionName } from "../runtime-api.js"; import { + inspectFeishuCredentials, resolveFeishuAccount, - resolveFeishuCredentials, + resolveFeishuRuntimeAccount, listFeishuAccountIds, listEnabledFeishuAccounts, resolveDefaultFeishuAccountId, @@ -100,7 +101,7 @@ function describeFeishuMessageTool({ >[0]): ChannelMessageToolDiscovery { const enabled = cfg.channels?.feishu?.enabled !== false && - Boolean(resolveFeishuCredentials(cfg.channels?.feishu as FeishuConfig | undefined)); + Boolean(inspectFeishuCredentials(cfg.channels?.feishu as FeishuConfig | undefined)); if (listEnabledFeishuAccounts(cfg).length === 0) { return { actions: [], @@ -977,7 +978,10 @@ export const feishuPlugin: ChannelPlugin { const { monitorFeishuProvider } = await import("./monitor.js"); - const account = resolveFeishuAccount({ cfg: ctx.cfg, accountId: ctx.accountId }); + const account = resolveFeishuRuntimeAccount( + { cfg: ctx.cfg, accountId: ctx.accountId }, + { requireEventSecrets: true }, + ); const port = account.config?.webhookPort ?? null; ctx.setStatus({ accountId: ctx.accountId, port }); ctx.log?.info( diff --git a/extensions/feishu/src/media.test.ts b/extensions/feishu/src/media.test.ts index 67ea2c1b77f..77dd7fdadc0 100644 --- a/extensions/feishu/src/media.test.ts +++ b/extensions/feishu/src/media.test.ts @@ -24,6 +24,7 @@ vi.mock("./client.js", () => ({ vi.mock("./accounts.js", () => ({ resolveFeishuAccount: resolveFeishuAccountMock, + resolveFeishuRuntimeAccount: resolveFeishuAccountMock, })); vi.mock("./targets.js", () => ({ diff --git a/extensions/feishu/src/media.ts b/extensions/feishu/src/media.ts index a55ed6774e6..ba9048859df 100644 --- a/extensions/feishu/src/media.ts +++ b/extensions/feishu/src/media.ts @@ -3,7 +3,7 @@ import path from "path"; import { Readable } from "stream"; import { mediaKindFromMime } from "openclaw/plugin-sdk/media-runtime"; import { withTempDownloadPath, type ClawdbotConfig } from "../runtime-api.js"; -import { resolveFeishuAccount } from "./accounts.js"; +import { resolveFeishuRuntimeAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { normalizeFeishuExternalKey } from "./external-keys.js"; import { getFeishuRuntime } from "./runtime.js"; @@ -24,10 +24,10 @@ export type DownloadMessageResourceResult = { }; function createConfiguredFeishuMediaClient(params: { cfg: ClawdbotConfig; accountId?: string }): { - account: ReturnType; + account: ReturnType; client: ReturnType; } { - const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId }); + const account = resolveFeishuRuntimeAccount({ cfg: params.cfg, accountId: params.accountId }); if (!account.configured) { throw new Error(`Feishu account "${account.accountId}" not configured`); } @@ -547,7 +547,7 @@ export async function sendMediaFeishu(params: { accountId, mediaLocalRoots, } = params; - const account = resolveFeishuAccount({ cfg, accountId }); + const account = resolveFeishuRuntimeAccount({ cfg, accountId }); if (!account.configured) { throw new Error(`Feishu account "${account.accountId}" not configured`); } diff --git a/extensions/feishu/src/monitor.ts b/extensions/feishu/src/monitor.ts index 67be9c259f6..3613a1c55c1 100644 --- a/extensions/feishu/src/monitor.ts +++ b/extensions/feishu/src/monitor.ts @@ -1,5 +1,5 @@ import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js"; -import { listEnabledFeishuAccounts, resolveFeishuAccount } from "./accounts.js"; +import { listEnabledFeishuAccounts, resolveFeishuRuntimeAccount } from "./accounts.js"; import { monitorSingleAccount, resolveReactionSyntheticEvent, @@ -37,7 +37,10 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi const log = opts.runtime?.log ?? console.log; if (opts.accountId) { - const account = resolveFeishuAccount({ cfg, accountId: opts.accountId }); + const account = resolveFeishuRuntimeAccount( + { cfg, accountId: opts.accountId }, + { requireEventSecrets: true }, + ); if (!account.enabled || !account.configured) { throw new Error(`Feishu account "${opts.accountId}" not configured or disabled`); } diff --git a/extensions/feishu/src/pins.ts b/extensions/feishu/src/pins.ts index d5bf82aaeb8..3b72adf4c0e 100644 --- a/extensions/feishu/src/pins.ts +++ b/extensions/feishu/src/pins.ts @@ -1,5 +1,5 @@ import type { ClawdbotConfig } from "../runtime-api.js"; -import { resolveFeishuAccount } from "./accounts.js"; +import { resolveFeishuRuntimeAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; export type FeishuPin = { @@ -37,7 +37,7 @@ export async function createPinFeishu(params: { messageId: string; accountId?: string; }): Promise { - const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId }); + const account = resolveFeishuRuntimeAccount({ cfg: params.cfg, accountId: params.accountId }); if (!account.configured) { throw new Error(`Feishu account "${account.accountId}" not configured`); } @@ -57,7 +57,7 @@ export async function removePinFeishu(params: { messageId: string; accountId?: string; }): Promise { - const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId }); + const account = resolveFeishuRuntimeAccount({ cfg: params.cfg, accountId: params.accountId }); if (!account.configured) { throw new Error(`Feishu account "${account.accountId}" not configured`); } @@ -80,7 +80,7 @@ export async function listPinsFeishu(params: { pageToken?: string; accountId?: string; }): Promise<{ chatId: string; pins: FeishuPin[]; hasMore: boolean; pageToken?: string }> { - const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId }); + const account = resolveFeishuRuntimeAccount({ cfg: params.cfg, accountId: params.accountId }); if (!account.configured) { throw new Error(`Feishu account "${account.accountId}" not configured`); } diff --git a/extensions/feishu/src/reactions.ts b/extensions/feishu/src/reactions.ts index b6d7f328b70..b977c00781e 100644 --- a/extensions/feishu/src/reactions.ts +++ b/extensions/feishu/src/reactions.ts @@ -1,5 +1,5 @@ import type { ClawdbotConfig } from "../runtime-api.js"; -import { resolveFeishuAccount } from "./accounts.js"; +import { resolveFeishuRuntimeAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; export type FeishuReaction = { @@ -10,7 +10,7 @@ export type FeishuReaction = { }; function resolveConfiguredFeishuClient(params: { cfg: ClawdbotConfig; accountId?: string }) { - const account = resolveFeishuAccount(params); + const account = resolveFeishuRuntimeAccount(params); if (!account.configured) { throw new Error(`Feishu account "${account.accountId}" not configured`); } diff --git a/extensions/feishu/src/reply-dispatcher.test.ts b/extensions/feishu/src/reply-dispatcher.test.ts index c7b2f9af28b..656ee8b3928 100644 --- a/extensions/feishu/src/reply-dispatcher.test.ts +++ b/extensions/feishu/src/reply-dispatcher.test.ts @@ -13,7 +13,10 @@ const addTypingIndicatorMock = vi.hoisted(() => vi.fn(async () => ({ messageId: const removeTypingIndicatorMock = vi.hoisted(() => vi.fn(async () => {})); const streamingInstances = vi.hoisted(() => [] as any[]); -vi.mock("./accounts.js", () => ({ resolveFeishuAccount: resolveFeishuAccountMock })); +vi.mock("./accounts.js", () => ({ + resolveFeishuAccount: resolveFeishuAccountMock, + resolveFeishuRuntimeAccount: resolveFeishuAccountMock, +})); vi.mock("./runtime.js", () => ({ getFeishuRuntime: getFeishuRuntimeMock })); vi.mock("./send.js", () => ({ sendMessageFeishu: sendMessageFeishuMock, diff --git a/extensions/feishu/src/reply-dispatcher.ts b/extensions/feishu/src/reply-dispatcher.ts index 6ab7184c8e8..131c0d3ca41 100644 --- a/extensions/feishu/src/reply-dispatcher.ts +++ b/extensions/feishu/src/reply-dispatcher.ts @@ -12,7 +12,7 @@ import { type ReplyPayload, type RuntimeEnv, } from "../runtime-api.js"; -import { resolveFeishuAccount } from "./accounts.js"; +import { resolveFeishuRuntimeAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { sendMediaFeishu } from "./media.js"; import type { MentionTarget } from "./mention.js"; @@ -110,7 +110,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP const sendReplyToMessageId = skipReplyToInMessages ? undefined : replyToMessageId; const threadReplyMode = threadReply === true; const effectiveReplyInThread = threadReplyMode ? true : replyInThread; - const account = resolveFeishuAccount({ cfg, accountId }); + const account = resolveFeishuRuntimeAccount({ cfg, accountId }); const prefixContext = createReplyPrefixContext({ cfg, agentId }); let typingState: TypingIndicatorState | null = null; diff --git a/extensions/feishu/src/send-target.test.ts b/extensions/feishu/src/send-target.test.ts index d435d95267a..ad2e9326f27 100644 --- a/extensions/feishu/src/send-target.test.ts +++ b/extensions/feishu/src/send-target.test.ts @@ -7,6 +7,7 @@ const createFeishuClientMock = vi.hoisted(() => vi.fn()); vi.mock("./accounts.js", () => ({ resolveFeishuAccount: resolveFeishuAccountMock, + resolveFeishuRuntimeAccount: resolveFeishuAccountMock, })); vi.mock("./client.js", () => ({ diff --git a/extensions/feishu/src/send-target.ts b/extensions/feishu/src/send-target.ts index b0a3d6793c8..322333b3bad 100644 --- a/extensions/feishu/src/send-target.ts +++ b/extensions/feishu/src/send-target.ts @@ -1,5 +1,5 @@ import type { ClawdbotConfig } from "../runtime-api.js"; -import { resolveFeishuAccount } from "./accounts.js"; +import { resolveFeishuRuntimeAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js"; @@ -9,7 +9,7 @@ export function resolveFeishuSendTarget(params: { accountId?: string; }) { const target = params.to.trim(); - const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId }); + const account = resolveFeishuRuntimeAccount({ cfg: params.cfg, accountId: params.accountId }); if (!account.configured) { throw new Error(`Feishu account "${account.accountId}" not configured`); } diff --git a/extensions/feishu/src/send.test.ts b/extensions/feishu/src/send.test.ts index a7af456068d..80ff108612e 100644 --- a/extensions/feishu/src/send.test.ts +++ b/extensions/feishu/src/send.test.ts @@ -28,6 +28,7 @@ vi.mock("./client.js", () => ({ vi.mock("./accounts.js", () => ({ resolveFeishuAccount: mockResolveFeishuAccount, + resolveFeishuRuntimeAccount: mockResolveFeishuAccount, })); vi.mock("./runtime.js", () => ({ diff --git a/extensions/feishu/src/send.ts b/extensions/feishu/src/send.ts index 7ea5395010c..0ac34cadeff 100644 --- a/extensions/feishu/src/send.ts +++ b/extensions/feishu/src/send.ts @@ -1,5 +1,5 @@ import type { ClawdbotConfig } from "../runtime-api.js"; -import { resolveFeishuAccount } from "./accounts.js"; +import { resolveFeishuRuntimeAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import type { MentionTarget } from "./mention.js"; import { buildMentionedMessage, buildMentionedCardContent } from "./mention.js"; @@ -286,7 +286,7 @@ export async function getMessageFeishu(params: { accountId?: string; }): Promise { const { cfg, messageId, accountId } = params; - const account = resolveFeishuAccount({ cfg, accountId }); + const account = resolveFeishuRuntimeAccount({ cfg, accountId }); if (!account.configured) { throw new Error(`Feishu account "${account.accountId}" not configured`); } @@ -343,7 +343,7 @@ export async function listFeishuThreadMessages(params: { accountId?: string; }): Promise { const { cfg, threadId, currentMessageId, rootMessageId, limit = 20, accountId } = params; - const account = resolveFeishuAccount({ cfg, accountId }); + const account = resolveFeishuRuntimeAccount({ cfg, accountId }); if (!account.configured) { throw new Error(`Feishu account "${account.accountId}" not configured`); } @@ -506,7 +506,7 @@ export async function editMessageFeishu(params: { accountId?: string; }): Promise<{ messageId: string; contentType: "post" | "interactive" }> { const { cfg, messageId, text, card, accountId } = params; - const account = resolveFeishuAccount({ cfg, accountId }); + const account = resolveFeishuRuntimeAccount({ cfg, accountId }); if (!account.configured) { throw new Error(`Feishu account "${account.accountId}" not configured`); } @@ -558,7 +558,7 @@ export async function updateCardFeishu(params: { accountId?: string; }): Promise { const { cfg, messageId, card, accountId } = params; - const account = resolveFeishuAccount({ cfg, accountId }); + const account = resolveFeishuRuntimeAccount({ cfg, accountId }); if (!account.configured) { throw new Error(`Feishu account "${account.accountId}" not configured`); } diff --git a/extensions/feishu/src/setup-surface.ts b/extensions/feishu/src/setup-surface.ts index 1a7d51a7d9d..cba06e4d1d8 100644 --- a/extensions/feishu/src/setup-surface.ts +++ b/extensions/feishu/src/setup-surface.ts @@ -16,7 +16,7 @@ import { type OpenClawConfig, type SecretInput, } from "openclaw/plugin-sdk/setup"; -import { listFeishuAccountIds, resolveFeishuCredentials } from "./accounts.js"; +import { inspectFeishuCredentials, listFeishuAccountIds } from "./accounts.js"; import { probeFeishu } from "./probe.js"; import { feishuSetupAdapter } from "./setup-core.js"; import type { FeishuConfig } from "./types.js"; @@ -165,9 +165,7 @@ export const feishuSetupWizard: ChannelSetupWizard = { resolveConfigured: ({ cfg }) => isFeishuConfigured(cfg), resolveStatusLines: async ({ cfg, configured }) => { const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined; - const resolvedCredentials = resolveFeishuCredentials(feishuCfg, { - allowUnresolvedSecretRef: true, - }); + const resolvedCredentials = inspectFeishuCredentials(feishuCfg); let probeResult = null; if (configured && resolvedCredentials) { try { @@ -186,9 +184,7 @@ export const feishuSetupWizard: ChannelSetupWizard = { credentials: [], finalize: async ({ cfg, prompter, options }) => { const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined; - const resolved = resolveFeishuCredentials(feishuCfg, { - allowUnresolvedSecretRef: true, - }); + const resolved = inspectFeishuCredentials(feishuCfg); const hasConfigSecret = hasConfiguredSecretInput(feishuCfg?.appSecret); const hasConfigCreds = Boolean( typeof feishuCfg?.appId === "string" && feishuCfg.appId.trim() && hasConfigSecret, diff --git a/extensions/feishu/src/tool-account.ts b/extensions/feishu/src/tool-account.ts index dff71b424dc..d884b938688 100644 --- a/extensions/feishu/src/tool-account.ts +++ b/extensions/feishu/src/tool-account.ts @@ -1,6 +1,6 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; import type { OpenClawPluginApi } from "../runtime-api.js"; -import { resolveFeishuAccount } from "./accounts.js"; +import { resolveFeishuRuntimeAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { resolveToolsConfig } from "./tools-config.js"; import type { FeishuToolsConfig, ResolvedFeishuAccount } from "./types.js"; @@ -29,7 +29,7 @@ export function resolveFeishuToolAccount(params: { if (!params.api.config) { throw new Error("Feishu config unavailable"); } - return resolveFeishuAccount({ + return resolveFeishuRuntimeAccount({ cfg: params.api.config, accountId: normalizeOptionalAccountId(params.executeParams?.accountId) ?? diff --git a/extensions/feishu/src/typing.ts b/extensions/feishu/src/typing.ts index d3f25297a79..86c63e4c9b7 100644 --- a/extensions/feishu/src/typing.ts +++ b/extensions/feishu/src/typing.ts @@ -1,5 +1,5 @@ import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js"; -import { resolveFeishuAccount } from "./accounts.js"; +import { resolveFeishuRuntimeAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { getFeishuRuntime } from "./runtime.js"; @@ -107,7 +107,7 @@ export async function addTypingIndicator(params: { runtime?: RuntimeEnv; }): Promise { const { cfg, messageId, accountId, runtime } = params; - const account = resolveFeishuAccount({ cfg, accountId }); + const account = resolveFeishuRuntimeAccount({ cfg, accountId }); if (!account.configured) { return { messageId, reactionId: null }; } @@ -168,7 +168,7 @@ export async function removeTypingIndicator(params: { return; } - const account = resolveFeishuAccount({ cfg, accountId }); + const account = resolveFeishuRuntimeAccount({ cfg, accountId }); if (!account.configured) { return; }