mirror of https://github.com/openclaw/openclaw.git
fix: honor feishu setup account writes
This commit is contained in:
parent
53612fa128
commit
17060ca124
|
|
@ -90,6 +90,56 @@ describe("feishu setup wizard", () => {
|
|||
}),
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
|
||||
it("writes selected-account credentials instead of overwriting the channel root", async () => {
|
||||
const prompter = createTestWizardPrompter({
|
||||
text: vi.fn(async ({ message }: { message: string }) => {
|
||||
if (message === "Enter Feishu App Secret") {
|
||||
return "work-secret"; // pragma: allowlist secret
|
||||
}
|
||||
if (message === "Enter Feishu App ID") {
|
||||
return "work-app";
|
||||
}
|
||||
if (message === "Group chat allowlist (chat_ids)") {
|
||||
return "";
|
||||
}
|
||||
throw new Error(`Unexpected prompt: ${message}`);
|
||||
}) as WizardPrompter["text"],
|
||||
select: vi.fn(
|
||||
async ({ initialValue }: { initialValue?: string }) => initialValue ?? "websocket",
|
||||
) as never,
|
||||
});
|
||||
|
||||
const result = await runSetupWizardConfigure({
|
||||
configure: feishuConfigure,
|
||||
cfg: {
|
||||
channels: {
|
||||
feishu: {
|
||||
appId: "top-level-app",
|
||||
appSecret: "top-level-secret", // pragma: allowlist secret
|
||||
accounts: {
|
||||
work: {
|
||||
appId: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
prompter,
|
||||
accountOverrides: {
|
||||
feishu: "work",
|
||||
},
|
||||
runtime: createNonExitingTypedRuntimeEnv<FeishuConfigureRuntime>(),
|
||||
});
|
||||
|
||||
expect(result.cfg.channels?.feishu?.appId).toBe("top-level-app");
|
||||
expect(result.cfg.channels?.feishu?.appSecret).toBe("top-level-secret");
|
||||
expect(result.cfg.channels?.feishu?.accounts?.work).toMatchObject({
|
||||
enabled: true,
|
||||
appId: "work-app",
|
||||
appSecret: "work-secret",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("feishu setup wizard status", () => {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
import {
|
||||
buildSingleChannelSecretPromptState,
|
||||
createTopLevelChannelAllowFromSetter,
|
||||
createTopLevelChannelDmPolicy,
|
||||
createTopLevelChannelGroupPolicySetter,
|
||||
createTopLevelChannelParsedAllowFromPrompt,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
hasConfiguredSecretInput,
|
||||
mergeAllowFromEntries,
|
||||
patchChannelConfigForAccount,
|
||||
patchTopLevelChannelConfigSection,
|
||||
promptSingleChannelSecretInput,
|
||||
splitSetupEntries,
|
||||
|
|
@ -22,13 +19,6 @@ import { feishuSetupAdapter } from "./setup-core.js";
|
|||
import type { FeishuConfig } from "./types.js";
|
||||
|
||||
const channel = "feishu" as const;
|
||||
const setFeishuAllowFrom = createTopLevelChannelAllowFromSetter({
|
||||
channel,
|
||||
});
|
||||
const setFeishuGroupPolicy = createTopLevelChannelGroupPolicySetter({
|
||||
channel,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
function normalizeString(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
|
|
@ -38,17 +28,49 @@ function normalizeString(value: unknown): string | undefined {
|
|||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
function setFeishuGroupAllowFrom(cfg: OpenClawConfig, groupAllowFrom: string[]): OpenClawConfig {
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
feishu: {
|
||||
...cfg.channels?.feishu,
|
||||
groupAllowFrom,
|
||||
},
|
||||
},
|
||||
};
|
||||
function getScopedFeishuConfig(cfg: OpenClawConfig, accountId: string): FeishuConfig {
|
||||
const feishuCfg = (cfg.channels?.feishu as FeishuConfig | undefined) ?? {};
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
return feishuCfg;
|
||||
}
|
||||
return (feishuCfg.accounts?.[accountId] as FeishuConfig | undefined) ?? {};
|
||||
}
|
||||
|
||||
function patchFeishuConfig(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
patch: Record<string, unknown>,
|
||||
): OpenClawConfig {
|
||||
return patchChannelConfigForAccount({
|
||||
cfg,
|
||||
channel,
|
||||
accountId,
|
||||
patch,
|
||||
});
|
||||
}
|
||||
|
||||
function setFeishuAllowFrom(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
allowFrom: string[],
|
||||
): OpenClawConfig {
|
||||
return patchFeishuConfig(cfg, accountId, { allowFrom });
|
||||
}
|
||||
|
||||
function setFeishuGroupPolicy(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
groupPolicy: "open" | "allowlist" | "disabled",
|
||||
): OpenClawConfig {
|
||||
return patchFeishuConfig(cfg, accountId, { groupPolicy });
|
||||
}
|
||||
|
||||
function setFeishuGroupAllowFrom(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
groupAllowFrom: string[],
|
||||
): OpenClawConfig {
|
||||
return patchFeishuConfig(cfg, accountId, { groupAllowFrom });
|
||||
}
|
||||
|
||||
function isFeishuConfigured(cfg: OpenClawConfig): boolean {
|
||||
|
|
@ -93,22 +115,34 @@ function isFeishuConfigured(cfg: OpenClawConfig): boolean {
|
|||
return topLevelConfigured || accountConfigured;
|
||||
}
|
||||
|
||||
const promptFeishuAllowFrom = createTopLevelChannelParsedAllowFromPrompt({
|
||||
channel,
|
||||
defaultAccountId: DEFAULT_ACCOUNT_ID,
|
||||
noteTitle: "Feishu allowlist",
|
||||
noteLines: [
|
||||
"Allowlist Feishu DMs by open_id or user_id.",
|
||||
"You can find user open_id in Feishu admin console or via API.",
|
||||
"Examples:",
|
||||
"- ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"- on_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
],
|
||||
message: "Feishu allowFrom (user open_ids)",
|
||||
placeholder: "ou_xxxxx, ou_yyyyy",
|
||||
parseEntries: (raw) => ({ entries: splitSetupEntries(raw) }),
|
||||
mergeEntries: ({ existing, parsed }) => mergeAllowFromEntries(existing, parsed),
|
||||
});
|
||||
async function promptFeishuAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId: string;
|
||||
prompter: Parameters<NonNullable<ChannelSetupDmPolicy["promptAllowFrom"]>>[0]["prompter"];
|
||||
}): Promise<OpenClawConfig> {
|
||||
const existingAllowFrom =
|
||||
resolveFeishuAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
}).config.allowFrom ?? [];
|
||||
await params.prompter.note(
|
||||
[
|
||||
"Allowlist Feishu DMs by open_id or user_id.",
|
||||
"You can find user open_id in Feishu admin console or via API.",
|
||||
"Examples:",
|
||||
"- ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"- on_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
].join("\n"),
|
||||
"Feishu allowlist",
|
||||
);
|
||||
const entry = await params.prompter.text({
|
||||
message: "Feishu allowFrom (user open_ids)",
|
||||
placeholder: "ou_xxxxx, ou_yyyyy",
|
||||
initialValue: existingAllowFrom.length > 0 ? existingAllowFrom.map(String).join(", ") : undefined,
|
||||
});
|
||||
const mergedAllowFrom = mergeAllowFromEntries(existingAllowFrom, splitSetupEntries(String(entry)));
|
||||
return setFeishuAllowFrom(params.cfg, params.accountId, mergedAllowFrom);
|
||||
}
|
||||
|
||||
async function noteFeishuCredentialHelp(
|
||||
prompter: Parameters<NonNullable<ChannelSetupWizard["finalize"]>>[0]["prompter"],
|
||||
|
|
@ -140,20 +174,51 @@ async function promptFeishuAppId(params: {
|
|||
).trim();
|
||||
}
|
||||
|
||||
const feishuDmPolicy: ChannelSetupDmPolicy = createTopLevelChannelDmPolicy({
|
||||
const feishuDmPolicy: ChannelSetupDmPolicy = {
|
||||
label: "Feishu",
|
||||
channel,
|
||||
policyKey: "channels.feishu.dmPolicy",
|
||||
allowFromKey: "channels.feishu.allowFrom",
|
||||
getCurrent: (cfg) => (cfg.channels?.feishu as FeishuConfig | undefined)?.dmPolicy ?? "pairing",
|
||||
promptAllowFrom: promptFeishuAllowFrom,
|
||||
});
|
||||
resolveConfigKeys: (_cfg, accountId) =>
|
||||
accountId && accountId !== DEFAULT_ACCOUNT_ID
|
||||
? {
|
||||
policyKey: `channels.feishu.accounts.${accountId}.dmPolicy`,
|
||||
allowFromKey: `channels.feishu.accounts.${accountId}.allowFrom`,
|
||||
}
|
||||
: {
|
||||
policyKey: "channels.feishu.dmPolicy",
|
||||
allowFromKey: "channels.feishu.allowFrom",
|
||||
},
|
||||
getCurrent: (cfg, accountId) =>
|
||||
resolveFeishuAccount({
|
||||
cfg,
|
||||
accountId: accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
}).config.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg, policy, accountId) => {
|
||||
const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
|
||||
const currentAllowFrom = resolveFeishuAccount({
|
||||
cfg,
|
||||
accountId: resolvedAccountId,
|
||||
}).config.allowFrom;
|
||||
return patchFeishuConfig(cfg, resolvedAccountId, {
|
||||
dmPolicy: policy,
|
||||
...(policy === "open" ? { allowFrom: mergeAllowFromEntries(currentAllowFrom, ["*"]) } : {}),
|
||||
});
|
||||
},
|
||||
promptAllowFrom: async ({ cfg, accountId, prompter }) =>
|
||||
await promptFeishuAllowFrom({
|
||||
cfg,
|
||||
accountId: accountId ?? DEFAULT_ACCOUNT_ID,
|
||||
prompter,
|
||||
}),
|
||||
};
|
||||
|
||||
export { feishuSetupAdapter } from "./setup-core.js";
|
||||
|
||||
export const feishuSetupWizard: ChannelSetupWizard = {
|
||||
channel,
|
||||
resolveAccountIdForConfigure: () => DEFAULT_ACCOUNT_ID,
|
||||
resolveAccountIdForConfigure: ({ accountOverride, defaultAccountId }) =>
|
||||
normalizeString(accountOverride) ?? defaultAccountId,
|
||||
resolveShouldPromptAccountIds: () => false,
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
|
|
@ -195,12 +260,23 @@ export const feishuSetupWizard: ChannelSetupWizard = {
|
|||
},
|
||||
},
|
||||
credentials: [],
|
||||
finalize: async ({ cfg, prompter, options }) => {
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
const resolved = inspectFeishuCredentials(feishuCfg);
|
||||
const hasConfigSecret = hasConfiguredSecretInput(feishuCfg?.appSecret);
|
||||
finalize: async ({ cfg, accountId, prompter, options }) => {
|
||||
const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
|
||||
const resolvedAccount = resolveFeishuAccount({ cfg, accountId: resolvedAccountId });
|
||||
const scopedConfig = getScopedFeishuConfig(cfg, resolvedAccountId);
|
||||
const resolved =
|
||||
resolvedAccount.configured && resolvedAccount.appId && resolvedAccount.appSecret
|
||||
? {
|
||||
appId: resolvedAccount.appId,
|
||||
appSecret: resolvedAccount.appSecret,
|
||||
encryptKey: resolvedAccount.encryptKey,
|
||||
verificationToken: resolvedAccount.verificationToken,
|
||||
domain: resolvedAccount.domain,
|
||||
}
|
||||
: null;
|
||||
const hasConfigSecret = hasConfiguredSecretInput(scopedConfig.appSecret);
|
||||
const hasConfigCreds = Boolean(
|
||||
typeof feishuCfg?.appId === "string" && feishuCfg.appId.trim() && hasConfigSecret,
|
||||
typeof scopedConfig.appId === "string" && scopedConfig.appId.trim() && hasConfigSecret,
|
||||
);
|
||||
const appSecretPromptState = buildSingleChannelSecretPromptState({
|
||||
accountConfigured: Boolean(resolved),
|
||||
|
|
@ -234,38 +310,28 @@ export const feishuSetupWizard: ChannelSetupWizard = {
|
|||
});
|
||||
|
||||
if (appSecretResult.action === "use-env") {
|
||||
next = patchTopLevelChannelConfigSection({
|
||||
cfg: next,
|
||||
channel,
|
||||
enabled: true,
|
||||
patch: {},
|
||||
}) as OpenClawConfig;
|
||||
next = patchFeishuConfig(next, resolvedAccountId, {});
|
||||
} else if (appSecretResult.action === "set") {
|
||||
appSecret = appSecretResult.value;
|
||||
appSecretProbeValue = appSecretResult.resolvedValue;
|
||||
appId = await promptFeishuAppId({
|
||||
prompter,
|
||||
initialValue:
|
||||
normalizeString(feishuCfg?.appId) ?? normalizeString(process.env.FEISHU_APP_ID),
|
||||
normalizeString(scopedConfig.appId) ?? normalizeString(process.env.FEISHU_APP_ID),
|
||||
});
|
||||
}
|
||||
|
||||
if (appId && appSecret) {
|
||||
next = patchTopLevelChannelConfigSection({
|
||||
cfg: next,
|
||||
channel,
|
||||
enabled: true,
|
||||
patch: {
|
||||
appId,
|
||||
appSecret,
|
||||
},
|
||||
}) as OpenClawConfig;
|
||||
next = patchFeishuConfig(next, resolvedAccountId, {
|
||||
appId,
|
||||
appSecret,
|
||||
});
|
||||
|
||||
try {
|
||||
const probe = await probeFeishu({
|
||||
appId,
|
||||
appSecret: appSecretProbeValue ?? undefined,
|
||||
domain: (next.channels?.feishu as FeishuConfig | undefined)?.domain,
|
||||
domain: resolveFeishuAccount({ cfg: next, accountId: resolvedAccountId }).domain,
|
||||
});
|
||||
if (probe.ok) {
|
||||
await prompter.note(
|
||||
|
|
@ -284,7 +350,8 @@ export const feishuSetupWizard: ChannelSetupWizard = {
|
|||
}
|
||||
|
||||
const currentMode =
|
||||
(next.channels?.feishu as FeishuConfig | undefined)?.connectionMode ?? "websocket";
|
||||
resolveFeishuAccount({ cfg: next, accountId: resolvedAccountId }).config.connectionMode ??
|
||||
"websocket";
|
||||
const connectionMode = (await prompter.select({
|
||||
message: "Feishu connection mode",
|
||||
options: [
|
||||
|
|
@ -293,15 +360,10 @@ export const feishuSetupWizard: ChannelSetupWizard = {
|
|||
],
|
||||
initialValue: currentMode,
|
||||
})) as "websocket" | "webhook";
|
||||
next = patchTopLevelChannelConfigSection({
|
||||
cfg: next,
|
||||
channel,
|
||||
patch: { connectionMode },
|
||||
}) as OpenClawConfig;
|
||||
next = patchFeishuConfig(next, resolvedAccountId, { connectionMode });
|
||||
|
||||
if (connectionMode === "webhook") {
|
||||
const currentVerificationToken = (next.channels?.feishu as FeishuConfig | undefined)
|
||||
?.verificationToken;
|
||||
const currentVerificationToken = getScopedFeishuConfig(next, resolvedAccountId).verificationToken;
|
||||
const verificationTokenResult = await promptSingleChannelSecretInput({
|
||||
cfg: next,
|
||||
prompter,
|
||||
|
|
@ -319,14 +381,12 @@ export const feishuSetupWizard: ChannelSetupWizard = {
|
|||
preferredEnvVar: "FEISHU_VERIFICATION_TOKEN",
|
||||
});
|
||||
if (verificationTokenResult.action === "set") {
|
||||
next = patchTopLevelChannelConfigSection({
|
||||
cfg: next,
|
||||
channel,
|
||||
patch: { verificationToken: verificationTokenResult.value },
|
||||
}) as OpenClawConfig;
|
||||
next = patchFeishuConfig(next, resolvedAccountId, {
|
||||
verificationToken: verificationTokenResult.value,
|
||||
});
|
||||
}
|
||||
|
||||
const currentEncryptKey = (next.channels?.feishu as FeishuConfig | undefined)?.encryptKey;
|
||||
const currentEncryptKey = getScopedFeishuConfig(next, resolvedAccountId).encryptKey;
|
||||
const encryptKeyResult = await promptSingleChannelSecretInput({
|
||||
cfg: next,
|
||||
prompter,
|
||||
|
|
@ -344,14 +404,12 @@ export const feishuSetupWizard: ChannelSetupWizard = {
|
|||
preferredEnvVar: "FEISHU_ENCRYPT_KEY",
|
||||
});
|
||||
if (encryptKeyResult.action === "set") {
|
||||
next = patchTopLevelChannelConfigSection({
|
||||
cfg: next,
|
||||
channel,
|
||||
patch: { encryptKey: encryptKeyResult.value },
|
||||
}) as OpenClawConfig;
|
||||
next = patchFeishuConfig(next, resolvedAccountId, {
|
||||
encryptKey: encryptKeyResult.value,
|
||||
});
|
||||
}
|
||||
|
||||
const currentWebhookPath = (next.channels?.feishu as FeishuConfig | undefined)?.webhookPath;
|
||||
const currentWebhookPath = getScopedFeishuConfig(next, resolvedAccountId).webhookPath;
|
||||
const webhookPath = String(
|
||||
await prompter.text({
|
||||
message: "Feishu webhook path",
|
||||
|
|
@ -359,14 +417,10 @@ export const feishuSetupWizard: ChannelSetupWizard = {
|
|||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
next = patchTopLevelChannelConfigSection({
|
||||
cfg: next,
|
||||
channel,
|
||||
patch: { webhookPath },
|
||||
}) as OpenClawConfig;
|
||||
next = patchFeishuConfig(next, resolvedAccountId, { webhookPath });
|
||||
}
|
||||
|
||||
const currentDomain = (next.channels?.feishu as FeishuConfig | undefined)?.domain ?? "feishu";
|
||||
const currentDomain = resolveFeishuAccount({ cfg: next, accountId: resolvedAccountId }).domain;
|
||||
const domain = await prompter.select({
|
||||
message: "Which Feishu domain?",
|
||||
options: [
|
||||
|
|
@ -375,11 +429,9 @@ export const feishuSetupWizard: ChannelSetupWizard = {
|
|||
],
|
||||
initialValue: currentDomain,
|
||||
});
|
||||
next = patchTopLevelChannelConfigSection({
|
||||
cfg: next,
|
||||
channel,
|
||||
patch: { domain: domain as "feishu" | "lark" },
|
||||
}) as OpenClawConfig;
|
||||
next = patchFeishuConfig(next, resolvedAccountId, {
|
||||
domain: domain as "feishu" | "lark",
|
||||
});
|
||||
|
||||
const groupPolicy = (await prompter.select({
|
||||
message: "Group chat policy",
|
||||
|
|
@ -388,12 +440,16 @@ export const feishuSetupWizard: ChannelSetupWizard = {
|
|||
{ value: "open", label: "Open - respond in all groups (requires mention)" },
|
||||
{ value: "disabled", label: "Disabled - don't respond in groups" },
|
||||
],
|
||||
initialValue: (next.channels?.feishu as FeishuConfig | undefined)?.groupPolicy ?? "allowlist",
|
||||
initialValue:
|
||||
resolveFeishuAccount({ cfg: next, accountId: resolvedAccountId }).config.groupPolicy ??
|
||||
"allowlist",
|
||||
})) as "allowlist" | "open" | "disabled";
|
||||
next = setFeishuGroupPolicy(next, groupPolicy);
|
||||
next = setFeishuGroupPolicy(next, resolvedAccountId, groupPolicy);
|
||||
|
||||
if (groupPolicy === "allowlist") {
|
||||
const existing = (next.channels?.feishu as FeishuConfig | undefined)?.groupAllowFrom ?? [];
|
||||
const existing =
|
||||
resolveFeishuAccount({ cfg: next, accountId: resolvedAccountId }).config.groupAllowFrom ??
|
||||
[];
|
||||
const entry = await prompter.text({
|
||||
message: "Group chat allowlist (chat_ids)",
|
||||
placeholder: "oc_xxxxx, oc_yyyyy",
|
||||
|
|
@ -402,7 +458,7 @@ export const feishuSetupWizard: ChannelSetupWizard = {
|
|||
if (entry) {
|
||||
const parts = splitSetupEntries(String(entry));
|
||||
if (parts.length > 0) {
|
||||
next = setFeishuGroupAllowFrom(next, parts);
|
||||
next = setFeishuGroupAllowFrom(next, resolvedAccountId, parts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue