mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-24 07:01:49 +00:00
fix: resolve channel typing regressions
This commit is contained in:
@@ -8,6 +8,10 @@ type BlueBubblesConfigPatch = {
|
||||
};
|
||||
|
||||
type AccountEnabledMode = boolean | "preserve-or-true";
|
||||
type BlueBubblesAccountEntry = {
|
||||
enabled?: boolean;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
function normalizePatch(
|
||||
patch: BlueBubblesConfigPatch,
|
||||
@@ -51,7 +55,9 @@ export function applyBlueBubblesConnectionConfig(params: {
|
||||
};
|
||||
}
|
||||
|
||||
const currentAccount = params.cfg.channels?.bluebubbles?.accounts?.[params.accountId];
|
||||
const currentAccount = params.cfg.channels?.bluebubbles?.accounts?.[params.accountId] as
|
||||
| BlueBubblesAccountEntry
|
||||
| undefined;
|
||||
const enabled =
|
||||
params.accountEnabled === "preserve-or-true"
|
||||
? (currentAccount?.enabled ?? true)
|
||||
|
||||
@@ -27,7 +27,13 @@ describe("bluebubbles doctor", () => {
|
||||
expect(result.config.channels?.bluebubbles?.network).toEqual({
|
||||
dangerouslyAllowPrivateNetwork: true,
|
||||
});
|
||||
expect(result.config.channels?.bluebubbles?.accounts?.default?.network).toEqual({
|
||||
expect(
|
||||
(
|
||||
result.config.channels?.bluebubbles?.accounts?.default as {
|
||||
network?: { dangerouslyAllowPrivateNetwork?: boolean };
|
||||
}
|
||||
)?.network,
|
||||
).toEqual({
|
||||
dangerouslyAllowPrivateNetwork: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -201,8 +201,8 @@ export async function sendBlueBubblesMedia(params: {
|
||||
const maxBytes = resolveChannelMediaMaxBytes({
|
||||
cfg,
|
||||
resolveChannelLimitMb: ({ cfg, accountId }) =>
|
||||
cfg.channels?.bluebubbles?.accounts?.[accountId]?.mediaMaxMb ??
|
||||
cfg.channels?.bluebubbles?.mediaMaxMb,
|
||||
(cfg.channels?.bluebubbles?.accounts?.[accountId] as { mediaMaxMb?: number } | undefined)
|
||||
?.mediaMaxMb ?? cfg.channels?.bluebubbles?.mediaMaxMb,
|
||||
accountId,
|
||||
});
|
||||
const mediaLocalRoots = resolveMediaLocalRoots({ cfg, accountId });
|
||||
|
||||
@@ -21,8 +21,11 @@ export function setBlueBubblesDmPolicy(
|
||||
const existingAllowFrom =
|
||||
resolvedAccountId === "default"
|
||||
? cfg.channels?.bluebubbles?.allowFrom
|
||||
: (cfg.channels?.bluebubbles?.accounts?.[resolvedAccountId]?.allowFrom ??
|
||||
cfg.channels?.bluebubbles?.allowFrom);
|
||||
: ((
|
||||
cfg.channels?.bluebubbles?.accounts?.[resolvedAccountId] as
|
||||
| { allowFrom?: ReadonlyArray<string | number> }
|
||||
| undefined
|
||||
)?.allowFrom ?? cfg.channels?.bluebubbles?.allowFrom);
|
||||
return patchScopedAccountConfig({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
|
||||
@@ -36,6 +36,9 @@ async function createBlueBubblesConfigureAdapter() {
|
||||
docsPath: "/channels/bluebubbles",
|
||||
blurb: "iMessage via BlueBubbles",
|
||||
},
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "group"],
|
||||
},
|
||||
config: {
|
||||
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
|
||||
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
||||
@@ -213,8 +216,13 @@ describe("bluebubbles setup surface", () => {
|
||||
});
|
||||
|
||||
const next = blueBubblesSetupWizard.dmPolicy?.setPolicy(cfg, "open");
|
||||
const workAccount = next?.channels?.bluebubbles?.accounts?.work as
|
||||
| {
|
||||
dmPolicy?: string;
|
||||
}
|
||||
| undefined;
|
||||
expect(next?.channels?.bluebubbles?.dmPolicy).toBe("disabled");
|
||||
expect(next?.channels?.bluebubbles?.accounts?.work?.dmPolicy).toBe("open");
|
||||
expect(workAccount?.dmPolicy).toBe("open");
|
||||
});
|
||||
|
||||
it("uses configured defaultAccount when accountId is omitted in account resolution", async () => {
|
||||
@@ -294,12 +302,15 @@ describe("bluebubbles setup surface", () => {
|
||||
"work",
|
||||
);
|
||||
|
||||
const workAccount = next?.channels?.bluebubbles?.accounts?.work as
|
||||
| {
|
||||
dmPolicy?: string;
|
||||
allowFrom?: string[];
|
||||
}
|
||||
| undefined;
|
||||
expect(next?.channels?.bluebubbles?.dmPolicy).toBeUndefined();
|
||||
expect(next?.channels?.bluebubbles?.accounts?.work?.dmPolicy).toBe("open");
|
||||
expect(next?.channels?.bluebubbles?.accounts?.work?.allowFrom).toEqual([
|
||||
"user@example.com",
|
||||
"*",
|
||||
]);
|
||||
expect(workAccount?.dmPolicy).toBe("open");
|
||||
expect(workAccount?.allowFrom).toEqual(["user@example.com", "*"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -371,11 +371,17 @@ describe("feishu setup wizard status", () => {
|
||||
});
|
||||
|
||||
const next = feishuSetupWizard.dmPolicy?.setPolicy?.(cfg as never, "open");
|
||||
const workAccount = next?.channels?.feishu?.accounts?.work as
|
||||
| {
|
||||
dmPolicy?: string;
|
||||
allowFrom?: string[];
|
||||
}
|
||||
| undefined;
|
||||
|
||||
expect(next?.channels?.feishu?.dmPolicy).toBeUndefined();
|
||||
expect(next?.channels?.feishu?.allowFrom).toEqual(["ou_root"]);
|
||||
expect(next?.channels?.feishu?.accounts?.work?.dmPolicy).toBe("open");
|
||||
expect(next?.channels?.feishu?.accounts?.work?.allowFrom).toEqual(["ou_work", "*"]);
|
||||
expect(workAccount?.dmPolicy).toBe("open");
|
||||
expect(workAccount?.allowFrom).toEqual(["ou_work", "*"]);
|
||||
});
|
||||
|
||||
it("treats env SecretRef appId as not configured when env var is missing", async () => {
|
||||
|
||||
@@ -234,8 +234,13 @@ describe("line setup wizard", () => {
|
||||
});
|
||||
|
||||
const next = lineSetupWizard.dmPolicy?.setPolicy(cfg, "open");
|
||||
const workAccount = next?.channels?.line?.accounts?.work as
|
||||
| {
|
||||
dmPolicy?: string;
|
||||
}
|
||||
| undefined;
|
||||
expect(next?.channels?.line?.dmPolicy).toBe("disabled");
|
||||
expect(next?.channels?.line?.accounts?.work?.dmPolicy).toBe("open");
|
||||
expect(workAccount?.dmPolicy).toBe("open");
|
||||
});
|
||||
|
||||
it('writes open policy state to the named account and preserves inherited allowFrom with "*"', async () => {
|
||||
@@ -259,10 +264,16 @@ describe("line setup wizard", () => {
|
||||
"work",
|
||||
);
|
||||
|
||||
const workAccount = next?.channels?.line?.accounts?.work as
|
||||
| {
|
||||
dmPolicy?: string;
|
||||
allowFrom?: string[];
|
||||
}
|
||||
| undefined;
|
||||
expect(next?.channels?.line?.dmPolicy).toBeUndefined();
|
||||
expect(next?.channels?.line?.allowFrom).toEqual(["Uroot"]);
|
||||
expect(next?.channels?.line?.accounts?.work?.dmPolicy).toBe("open");
|
||||
expect(next?.channels?.line?.accounts?.work?.allowFrom).toEqual(["Uroot", "*"]);
|
||||
expect(workAccount?.dmPolicy).toBe("open");
|
||||
expect(workAccount?.allowFrom).toEqual(["Uroot", "*"]);
|
||||
});
|
||||
|
||||
it("uses configured defaultAccount for omitted setup configured state", async () => {
|
||||
|
||||
@@ -157,10 +157,24 @@ describe("matrix doctor", () => {
|
||||
} as never,
|
||||
});
|
||||
|
||||
expect(result.config.channels?.matrix?.groups?.["!ops:example.org"]).toEqual({
|
||||
const matrixConfig = result.config.channels?.matrix as
|
||||
| {
|
||||
groups?: Record<string, unknown>;
|
||||
accounts?: Record<string, unknown>;
|
||||
network?: { dangerouslyAllowPrivateNetwork?: boolean };
|
||||
}
|
||||
| undefined;
|
||||
const workAccount = matrixConfig?.accounts?.work as
|
||||
| {
|
||||
rooms?: Record<string, unknown>;
|
||||
network?: { dangerouslyAllowPrivateNetwork?: boolean };
|
||||
}
|
||||
| undefined;
|
||||
|
||||
expect(matrixConfig?.groups?.["!ops:example.org"]).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(result.config.channels?.matrix?.accounts?.work?.rooms?.["!legacy:example.org"]).toEqual({
|
||||
expect(workAccount?.rooms?.["!legacy:example.org"]).toEqual({
|
||||
enabled: false,
|
||||
});
|
||||
expect(result.changes).toEqual(
|
||||
@@ -193,10 +207,22 @@ describe("matrix doctor", () => {
|
||||
} as never,
|
||||
});
|
||||
|
||||
expect(result.config.channels?.matrix?.network).toEqual({
|
||||
const matrixConfig = result.config.channels?.matrix as
|
||||
| {
|
||||
accounts?: Record<string, unknown>;
|
||||
network?: { dangerouslyAllowPrivateNetwork?: boolean };
|
||||
}
|
||||
| undefined;
|
||||
const workAccount = matrixConfig?.accounts?.work as
|
||||
| {
|
||||
network?: { dangerouslyAllowPrivateNetwork?: boolean };
|
||||
}
|
||||
| undefined;
|
||||
|
||||
expect(matrixConfig?.network).toEqual({
|
||||
dangerouslyAllowPrivateNetwork: true,
|
||||
});
|
||||
expect(result.config.channels?.matrix?.accounts?.work?.network).toEqual({
|
||||
expect(workAccount?.network).toEqual({
|
||||
dangerouslyAllowPrivateNetwork: false,
|
||||
});
|
||||
expect(result.changes).toEqual(
|
||||
|
||||
@@ -18,10 +18,9 @@ export function resolveMatrixAckReactionConfig(params: {
|
||||
channel: "matrix",
|
||||
accountId: params.accountId ?? undefined,
|
||||
}).trim();
|
||||
const ackReactionScope =
|
||||
accountConfig.ackReactionScope ??
|
||||
const ackReactionScope = (accountConfig.ackReactionScope ??
|
||||
matrixConfig?.ackReactionScope ??
|
||||
params.cfg.messages?.ackReactionScope ??
|
||||
"group-mentions";
|
||||
"group-mentions") as MatrixAckReactionScope;
|
||||
return { ackReaction, ackReactionScope };
|
||||
}
|
||||
|
||||
@@ -62,12 +62,19 @@ describe("matrix onboarding", () => {
|
||||
|
||||
expect(result).not.toBe("skip");
|
||||
if (result !== "skip") {
|
||||
const opsAccount = result.cfg.channels?.["matrix"]?.accounts?.ops as
|
||||
| {
|
||||
enabled?: boolean;
|
||||
homeserver?: string;
|
||||
accessToken?: string;
|
||||
}
|
||||
| undefined;
|
||||
expect(result.accountId).toBe("ops");
|
||||
expect(result.cfg.channels?.["matrix"]?.accounts?.ops).toMatchObject({
|
||||
expect(opsAccount).toMatchObject({
|
||||
enabled: true,
|
||||
});
|
||||
expect(result.cfg.channels?.["matrix"]?.accounts?.ops?.homeserver).toBeUndefined();
|
||||
expect(result.cfg.channels?.["matrix"]?.accounts?.ops?.accessToken).toBeUndefined();
|
||||
expect(opsAccount?.homeserver).toBeUndefined();
|
||||
expect(opsAccount?.accessToken).toBeUndefined();
|
||||
}
|
||||
expect(
|
||||
confirmMessages.some((message) =>
|
||||
|
||||
@@ -441,7 +441,9 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = create
|
||||
resolveGatewayAuthBypassPaths: ({ cfg }) => {
|
||||
const base = cfg.channels?.mattermost;
|
||||
const callbackPaths = new Set(
|
||||
collectMattermostSlashCallbackPaths(base?.commands).filter(
|
||||
collectMattermostSlashCallbackPaths(
|
||||
base?.commands as Partial<MattermostSlashCommandConfig> | undefined,
|
||||
).filter(
|
||||
(path) =>
|
||||
path === "/api/channels/mattermost/command" ||
|
||||
path.startsWith("/api/channels/mattermost/"),
|
||||
|
||||
@@ -27,7 +27,13 @@ describe("mattermost doctor", () => {
|
||||
expect(result.config.channels?.mattermost?.network).toEqual({
|
||||
dangerouslyAllowPrivateNetwork: true,
|
||||
});
|
||||
expect(result.config.channels?.mattermost?.accounts?.work?.network).toEqual({
|
||||
expect(
|
||||
(
|
||||
result.config.channels?.mattermost?.accounts?.work as
|
||||
| { network?: Record<string, unknown> }
|
||||
| undefined
|
||||
)?.network,
|
||||
).toEqual({
|
||||
dangerouslyAllowPrivateNetwork: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,10 +13,15 @@ import {
|
||||
|
||||
function resolveRequireMentionForTest(params: MattermostRequireMentionResolverInput): boolean {
|
||||
const root = params.cfg.channels?.mattermost;
|
||||
const accountGroups = root?.accounts?.[params.accountId]?.groups;
|
||||
const accountGroups = (
|
||||
root?.accounts?.[params.accountId] as
|
||||
| { groups?: Record<string, { requireMention?: boolean }> }
|
||||
| undefined
|
||||
)?.groups;
|
||||
const groups = accountGroups ?? root?.groups;
|
||||
const groupConfig = params.groupId ? groups?.[params.groupId] : undefined;
|
||||
const defaultGroupConfig = groups?.["*"];
|
||||
const typedGroups = groups as Record<string, { requireMention?: boolean }> | undefined;
|
||||
const groupConfig = params.groupId ? typedGroups?.[params.groupId] : undefined;
|
||||
const defaultGroupConfig = typedGroups?.["*"];
|
||||
const configMention =
|
||||
typeof groupConfig?.requireMention === "boolean"
|
||||
? groupConfig.requireMention
|
||||
|
||||
@@ -240,7 +240,7 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
||||
changed = true;
|
||||
}
|
||||
const accountCleanup = clearAccountEntryFields({
|
||||
accounts: nextSection.accounts,
|
||||
accounts: nextSection.accounts as Record<string, object> | undefined,
|
||||
accountId,
|
||||
fields: ["botSecret"],
|
||||
});
|
||||
@@ -250,7 +250,7 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
||||
cleared = true;
|
||||
}
|
||||
if (accountCleanup.nextAccounts) {
|
||||
nextSection.accounts = accountCleanup.nextAccounts;
|
||||
nextSection.accounts = accountCleanup.nextAccounts as Record<string, unknown>;
|
||||
} else {
|
||||
delete nextSection.accounts;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,13 @@ describe("nextcloud-talk doctor", () => {
|
||||
expect(result.config.channels?.["nextcloud-talk"]?.network).toEqual({
|
||||
dangerouslyAllowPrivateNetwork: true,
|
||||
});
|
||||
expect(result.config.channels?.["nextcloud-talk"]?.accounts?.work?.network).toEqual({
|
||||
expect(
|
||||
(
|
||||
result.config.channels?.["nextcloud-talk"]?.accounts?.work as
|
||||
| { network?: Record<string, unknown> }
|
||||
| undefined
|
||||
)?.network,
|
||||
).toEqual({
|
||||
dangerouslyAllowPrivateNetwork: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -155,7 +155,10 @@ describe("nextcloud talk setup", () => {
|
||||
|
||||
const next = nextcloudTalkDmPolicy.setPolicy(base, "open");
|
||||
expect(next.channels?.["nextcloud-talk"]?.dmPolicy).toBe("disabled");
|
||||
expect(next.channels?.["nextcloud-talk"]?.accounts?.work?.dmPolicy).toBe("open");
|
||||
const workAccount = next.channels?.["nextcloud-talk"]?.accounts?.work as
|
||||
| { dmPolicy?: string; allowFrom?: Array<string | number> }
|
||||
| undefined;
|
||||
expect(workAccount?.dmPolicy).toBe("open");
|
||||
});
|
||||
|
||||
it('writes open DM policy to the named account and preserves inherited allowFrom with "*"', () => {
|
||||
@@ -178,8 +181,11 @@ describe("nextcloud talk setup", () => {
|
||||
);
|
||||
|
||||
expect(next.channels?.["nextcloud-talk"]?.dmPolicy).toBeUndefined();
|
||||
expect(next.channels?.["nextcloud-talk"]?.accounts?.work?.dmPolicy).toBe("open");
|
||||
expect(next.channels?.["nextcloud-talk"]?.accounts?.work?.allowFrom).toEqual(["alice", "*"]);
|
||||
const workAccount = next.channels?.["nextcloud-talk"]?.accounts?.work as
|
||||
| { dmPolicy?: string; allowFrom?: Array<string | number> }
|
||||
| undefined;
|
||||
expect(workAccount?.dmPolicy).toBe("open");
|
||||
expect(workAccount?.allowFrom).toEqual(["alice", "*"]);
|
||||
});
|
||||
|
||||
it("validates env/default-account constraints and applies config patches", () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
hasConfiguredSecretInput,
|
||||
normalizeSecretInputString,
|
||||
} from "openclaw/plugin-sdk/secret-input";
|
||||
import type { ChannelSetupDmPolicy, ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
|
||||
import type { ChannelSetupDmPolicy, ChannelSetupWizard, DmPolicy } from "openclaw/plugin-sdk/setup";
|
||||
import {
|
||||
createStandardChannelSetupStatus,
|
||||
createTopLevelChannelDmPolicy,
|
||||
@@ -89,7 +89,7 @@ const nostrDmPolicy: ChannelSetupDmPolicy = createTopLevelChannelDmPolicy({
|
||||
channel,
|
||||
policyKey: "channels.nostr.dmPolicy",
|
||||
allowFromKey: "channels.nostr.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.nostr?.dmPolicy ?? "pairing",
|
||||
getCurrent: (cfg) => (cfg.channels?.nostr?.dmPolicy as DmPolicy | undefined) ?? "pairing",
|
||||
promptAllowFrom: promptNostrAllowFrom,
|
||||
});
|
||||
|
||||
@@ -234,8 +234,8 @@ export const nostrSetupWizard: ChannelSetupWizard = {
|
||||
helpLines: ["Use ws:// or wss:// relay URLs.", "Leave blank to keep the default relay set."],
|
||||
currentValue: ({ cfg, accountId }) => {
|
||||
const account = resolveNostrAccount({ cfg, accountId });
|
||||
const relays =
|
||||
cfg.channels?.nostr?.relays && cfg.channels.nostr.relays.length > 0 ? account.relays : [];
|
||||
const configuredRelays = cfg.channels?.nostr?.relays as string[] | undefined;
|
||||
const relays = configuredRelays && configuredRelays.length > 0 ? account.relays : [];
|
||||
return relays.join(", ");
|
||||
},
|
||||
keepPrompt: (value) => `Relay URLs set (${value}). Keep them?`,
|
||||
|
||||
@@ -18,7 +18,7 @@ import type {
|
||||
|
||||
/** Extract the channel config from the full OpenClaw config object. */
|
||||
function getChannelConfig(cfg: OpenClawConfig): SynologyChatChannelConfig | undefined {
|
||||
return cfg?.channels?.["synology-chat"];
|
||||
return cfg?.channels?.["synology-chat"] as SynologyChatChannelConfig | undefined;
|
||||
}
|
||||
|
||||
function resolveImplicitAccountId(channelCfg: SynologyChatChannelConfig): string | undefined {
|
||||
|
||||
@@ -27,7 +27,13 @@ describe("tlon doctor", () => {
|
||||
expect(result.config.channels?.tlon?.network).toEqual({
|
||||
dangerouslyAllowPrivateNetwork: true,
|
||||
});
|
||||
expect(result.config.channels?.tlon?.accounts?.alt?.network).toEqual({
|
||||
expect(
|
||||
(
|
||||
result.config.channels?.tlon?.accounts?.alt as
|
||||
| { network?: Record<string, unknown> }
|
||||
| undefined
|
||||
)?.network,
|
||||
).toEqual({
|
||||
dangerouslyAllowPrivateNetwork: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -198,8 +198,11 @@ describe("setup surface helpers", () => {
|
||||
|
||||
// Should return config with username and clientId
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.cfg.channels?.twitch?.accounts?.default?.username).toBe("testbot");
|
||||
expect(result?.cfg.channels?.twitch?.accounts?.default?.clientId).toBe("test-client-id");
|
||||
const defaultAccount = result?.cfg.channels?.twitch?.accounts?.default as
|
||||
| { username?: string; clientId?: string }
|
||||
| undefined;
|
||||
expect(defaultAccount?.username).toBe("testbot");
|
||||
expect(defaultAccount?.clientId).toBe("test-client-id");
|
||||
});
|
||||
|
||||
it("writes env-token setup to the configured default account", async () => {
|
||||
@@ -226,8 +229,11 @@ describe("setup surface helpers", () => {
|
||||
{} as Parameters<typeof configureWithEnvToken>[5],
|
||||
);
|
||||
|
||||
expect(result?.cfg.channels?.twitch?.accounts?.secondary?.username).toBe("secondary-bot");
|
||||
expect(result?.cfg.channels?.twitch?.accounts?.secondary?.clientId).toBe("secondary-client");
|
||||
const secondaryAccount = result?.cfg.channels?.twitch?.accounts?.secondary as
|
||||
| { username?: string; clientId?: string }
|
||||
| undefined;
|
||||
expect(secondaryAccount?.username).toBe("secondary-bot");
|
||||
expect(secondaryAccount?.clientId).toBe("secondary-client");
|
||||
expect(result?.cfg.channels?.twitch?.accounts?.default).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,12 @@ import { resolveDefaultZaloAccountId, resolveZaloAccount } from "./accounts.js";
|
||||
|
||||
const channel = "zalo" as const;
|
||||
|
||||
type ZaloAccountSetupConfig = {
|
||||
enabled?: boolean;
|
||||
dmPolicy?: string;
|
||||
allowFrom?: Array<string | number> | ReadonlyArray<string | number>;
|
||||
};
|
||||
|
||||
export const zaloSetupAdapter = createPatchedAccountSetupAdapter({
|
||||
channelKey: channel,
|
||||
validateInput: createSetupInputPresenceValidator({
|
||||
@@ -78,6 +84,9 @@ export const zaloDmPolicy: ChannelSetupDmPolicy = {
|
||||
},
|
||||
};
|
||||
}
|
||||
const currentAccount = cfg.channels?.zalo?.accounts?.[resolvedAccountId] as
|
||||
| ZaloAccountSetupConfig
|
||||
| undefined;
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
@@ -88,8 +97,8 @@ export const zaloDmPolicy: ChannelSetupDmPolicy = {
|
||||
accounts: {
|
||||
...cfg.channels?.zalo?.accounts,
|
||||
[resolvedAccountId]: {
|
||||
...cfg.channels?.zalo?.accounts?.[resolvedAccountId],
|
||||
enabled: cfg.channels?.zalo?.accounts?.[resolvedAccountId]?.enabled ?? true,
|
||||
...currentAccount,
|
||||
enabled: currentAccount?.enabled ?? true,
|
||||
dmPolicy: policy,
|
||||
...(policy === "open"
|
||||
? { allowFrom: addWildcardAllowFrom(resolved.config.allowFrom) }
|
||||
|
||||
@@ -96,7 +96,10 @@ describe("zalo setup wizard", () => {
|
||||
|
||||
const next = zaloDmPolicy.setPolicy(cfg, "open");
|
||||
expect(next.channels?.zalo?.dmPolicy).toBe("disabled");
|
||||
expect(next.channels?.zalo?.accounts?.work?.dmPolicy).toBe("open");
|
||||
const workAccount = next.channels?.zalo?.accounts?.work as
|
||||
| { dmPolicy?: string; allowFrom?: Array<string | number> }
|
||||
| undefined;
|
||||
expect(workAccount?.dmPolicy).toBe("open");
|
||||
});
|
||||
|
||||
it('writes open policy state to the named account and preserves inherited allowFrom with "*"', () => {
|
||||
@@ -118,8 +121,11 @@ describe("zalo setup wizard", () => {
|
||||
);
|
||||
|
||||
expect(next.channels?.zalo?.dmPolicy).toBeUndefined();
|
||||
expect(next.channels?.zalo?.accounts?.work?.dmPolicy).toBe("open");
|
||||
expect(next.channels?.zalo?.accounts?.work?.allowFrom).toEqual(["123456789", "*"]);
|
||||
const workAccount = next.channels?.zalo?.accounts?.work as
|
||||
| { dmPolicy?: string; allowFrom?: Array<string | number> }
|
||||
| undefined;
|
||||
expect(workAccount?.dmPolicy).toBe("open");
|
||||
expect(workAccount?.allowFrom).toEqual(["123456789", "*"]);
|
||||
});
|
||||
|
||||
it("uses configured defaultAccount for omitted setup configured state", async () => {
|
||||
|
||||
@@ -19,6 +19,10 @@ const channel = "zalo" as const;
|
||||
|
||||
type UpdateMode = "polling" | "webhook";
|
||||
|
||||
type ZaloAccountSetupConfig = {
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
function setZaloUpdateMode(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
@@ -150,6 +154,9 @@ async function promptZaloAllowFrom(params: {
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
const currentAccount = cfg.channels?.zalo?.accounts?.[accountId] as
|
||||
| ZaloAccountSetupConfig
|
||||
| undefined;
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
@@ -160,8 +167,8 @@ async function promptZaloAllowFrom(params: {
|
||||
accounts: {
|
||||
...cfg.channels?.zalo?.accounts,
|
||||
[accountId]: {
|
||||
...cfg.channels?.zalo?.accounts?.[accountId],
|
||||
enabled: cfg.channels?.zalo?.accounts?.[accountId]?.enabled ?? true,
|
||||
...currentAccount,
|
||||
enabled: currentAccount?.enabled ?? true,
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: unique,
|
||||
},
|
||||
@@ -261,7 +268,9 @@ export const zaloSetupWizard: ChannelSetupWizard = {
|
||||
accounts: {
|
||||
...currentCfg.channels?.zalo?.accounts,
|
||||
[accountId]: {
|
||||
...currentCfg.channels?.zalo?.accounts?.[accountId],
|
||||
...(currentCfg.channels?.zalo?.accounts?.[accountId] as
|
||||
| Record<string, unknown>
|
||||
| undefined),
|
||||
enabled: true,
|
||||
botToken: value,
|
||||
},
|
||||
|
||||
@@ -35,7 +35,13 @@ describe("zalouser doctor", () => {
|
||||
expect(result.config.channels?.zalouser?.groups?.["group:trusted"]).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(result.config.channels?.zalouser?.accounts?.work?.groups?.["group:legacy"]).toEqual({
|
||||
expect(
|
||||
(
|
||||
result.config.channels?.zalouser?.accounts?.work as
|
||||
| { groups?: Record<string, unknown> }
|
||||
| undefined
|
||||
)?.groups?.["group:legacy"],
|
||||
).toEqual({
|
||||
enabled: false,
|
||||
});
|
||||
expect(result.changes).toEqual(
|
||||
|
||||
@@ -136,8 +136,9 @@ function installRuntime(params: {
|
||||
resolveRequireMention: vi.fn((input) => {
|
||||
const cfg = input.cfg as OpenClawConfig;
|
||||
const groupCfg = cfg.channels?.zalouser?.groups ?? {};
|
||||
const groupEntry = input.groupId ? groupCfg[input.groupId] : undefined;
|
||||
const defaultEntry = groupCfg["*"];
|
||||
const typedGroupCfg = groupCfg as Record<string, { requireMention?: boolean }>;
|
||||
const groupEntry = input.groupId ? typedGroupCfg[input.groupId] : undefined;
|
||||
const defaultEntry = typedGroupCfg["*"];
|
||||
if (typeof groupEntry?.requireMention === "boolean") {
|
||||
return groupEntry.requireMention;
|
||||
}
|
||||
|
||||
@@ -295,7 +295,10 @@ describe("zalouser setup wizard", () => {
|
||||
|
||||
const next = zalouserSetupWizard.dmPolicy?.setPolicy(cfg, "open");
|
||||
expect(next?.channels?.zalouser?.dmPolicy).toBe("disabled");
|
||||
expect(next?.channels?.zalouser?.accounts?.work?.dmPolicy).toBe("open");
|
||||
const workAccount = next?.channels?.zalouser?.accounts?.work as
|
||||
| { dmPolicy?: string; allowFrom?: Array<string | number> }
|
||||
| undefined;
|
||||
expect(workAccount?.dmPolicy).toBe("open");
|
||||
});
|
||||
|
||||
it('writes open policy state to the named account and preserves inherited allowFrom with "*"', () => {
|
||||
@@ -317,8 +320,11 @@ describe("zalouser setup wizard", () => {
|
||||
);
|
||||
|
||||
expect(next?.channels?.zalouser?.dmPolicy).toBeUndefined();
|
||||
expect(next?.channels?.zalouser?.accounts?.work?.dmPolicy).toBe("open");
|
||||
expect(next?.channels?.zalouser?.accounts?.work?.allowFrom).toEqual(["123456789", "*"]);
|
||||
const workAccount = next?.channels?.zalouser?.accounts?.work as
|
||||
| { dmPolicy?: string; allowFrom?: Array<string | number> }
|
||||
| undefined;
|
||||
expect(workAccount?.dmPolicy).toBe("open");
|
||||
expect(workAccount?.allowFrom).toEqual(["123456789", "*"]);
|
||||
});
|
||||
|
||||
it("shows the account-scoped current DM policy in quickstart notes", async () => {
|
||||
|
||||
@@ -406,7 +406,7 @@ describe("group policy warning builders", () => {
|
||||
cfg: {
|
||||
channels?: {
|
||||
defaults?: { groupPolicy?: "open" | "allowlist" | "disabled" };
|
||||
example?: unknown;
|
||||
example?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
channelLabel: string;
|
||||
@@ -505,7 +505,7 @@ describe("group policy warning builders", () => {
|
||||
|
||||
it("builds config-aware open-provider collectors", () => {
|
||||
const collectWarnings = createOpenProviderGroupPolicyWarningCollector<{
|
||||
cfg: { channels?: { example?: unknown } };
|
||||
cfg: { channels?: { example?: Record<string, unknown> } };
|
||||
configuredGroupPolicy?: "open" | "allowlist" | "disabled";
|
||||
}>({
|
||||
providerConfigPresent: (cfg) => cfg.channels?.example !== undefined,
|
||||
|
||||
@@ -57,8 +57,8 @@ export function createScopedChannelMediaMaxBytesResolver(channel: string) {
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
resolveChannelLimitMb: ({ cfg, accountId }) =>
|
||||
cfg.channels?.[channel]?.accounts?.[accountId]?.mediaMaxMb ??
|
||||
cfg.channels?.[channel]?.mediaMaxMb,
|
||||
(cfg.channels?.[channel]?.accounts?.[accountId] as { mediaMaxMb?: number } | undefined)
|
||||
?.mediaMaxMb ?? cfg.channels?.[channel]?.mediaMaxMb,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -692,8 +692,13 @@ describe("promptParsedAllowFromForAccount", () => {
|
||||
placeholder: "placeholder",
|
||||
parseEntries: (raw) =>
|
||||
parseSetupEntriesWithParser(raw, (entry) => ({ value: entry.toLowerCase() })),
|
||||
getExistingAllowFrom: ({ cfg, accountId }) =>
|
||||
cfg.channels?.bluebubbles?.accounts?.[accountId]?.allowFrom ?? [],
|
||||
getExistingAllowFrom: ({ cfg, accountId }) => [
|
||||
...((
|
||||
cfg.channels?.bluebubbles?.accounts?.[accountId] as
|
||||
| { allowFrom?: ReadonlyArray<string | number> }
|
||||
| undefined
|
||||
)?.allowFrom ?? []),
|
||||
],
|
||||
applyAllowFrom: ({ cfg, accountId, allowFrom }) =>
|
||||
patchChannelConfigForAccount({
|
||||
cfg,
|
||||
@@ -703,7 +708,13 @@ describe("promptParsedAllowFromForAccount", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
expect(next.channels?.bluebubbles?.accounts?.alt?.allowFrom).toEqual(["alice"]);
|
||||
expect(
|
||||
(
|
||||
next.channels?.bluebubbles?.accounts?.alt as
|
||||
| { allowFrom?: ReadonlyArray<string | number> }
|
||||
| undefined
|
||||
)?.allowFrom,
|
||||
).toEqual(["alice"]);
|
||||
expect(prompter.note).toHaveBeenCalledWith("line", "BlueBubbles allowlist");
|
||||
});
|
||||
|
||||
@@ -723,7 +734,7 @@ describe("promptParsedAllowFromForAccount", () => {
|
||||
message: "msg",
|
||||
placeholder: "placeholder",
|
||||
parseEntries: (raw) => ({ entries: [raw.trim()] }),
|
||||
getExistingAllowFrom: ({ cfg }) => cfg.channels?.nostr?.allowFrom ?? [],
|
||||
getExistingAllowFrom: ({ cfg }) => [...(cfg.channels?.nostr?.allowFrom ?? [])],
|
||||
mergeEntries: ({ existing, parsed }) => [...existing.map(String), ...parsed],
|
||||
applyAllowFrom: ({ cfg, allowFrom }) =>
|
||||
patchTopLevelChannelConfigSection({
|
||||
@@ -744,8 +755,13 @@ describe("createPromptParsedAllowFromForAccount", () => {
|
||||
message: "msg",
|
||||
placeholder: "placeholder",
|
||||
parseEntries: (raw) => ({ entries: [raw.trim().toLowerCase()] }),
|
||||
getExistingAllowFrom: ({ cfg, accountId }) =>
|
||||
cfg.channels?.bluebubbles?.accounts?.[accountId]?.allowFrom ?? [],
|
||||
getExistingAllowFrom: ({ cfg, accountId }) => [
|
||||
...((
|
||||
cfg.channels?.bluebubbles?.accounts?.[accountId] as
|
||||
| { allowFrom?: ReadonlyArray<string | number> }
|
||||
| undefined
|
||||
)?.allowFrom ?? []),
|
||||
],
|
||||
applyAllowFrom: ({ cfg, accountId, allowFrom }) =>
|
||||
patchChannelConfigForAccount({
|
||||
cfg,
|
||||
@@ -771,7 +787,13 @@ describe("createPromptParsedAllowFromForAccount", () => {
|
||||
prompter: prompter as any,
|
||||
});
|
||||
|
||||
expect(next.channels?.bluebubbles?.accounts?.work?.allowFrom).toEqual(["alice"]);
|
||||
expect(
|
||||
(
|
||||
next.channels?.bluebubbles?.accounts?.work as
|
||||
| { allowFrom?: ReadonlyArray<string | number> }
|
||||
| undefined
|
||||
)?.allowFrom,
|
||||
).toEqual(["alice"]);
|
||||
expect(prompter.note).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1267,7 +1289,7 @@ describe("setTopLevelChannelDmPolicyWithAllowFrom", () => {
|
||||
channel: "nextcloud-talk",
|
||||
dmPolicy: "open",
|
||||
getAllowFrom: (inputCfg) =>
|
||||
normalizeAllowFromEntries(inputCfg.channels?.["nextcloud-talk"]?.allowFrom ?? []),
|
||||
normalizeAllowFromEntries([...(inputCfg.channels?.["nextcloud-talk"]?.allowFrom ?? [])]),
|
||||
});
|
||||
expect(next.channels?.["nextcloud-talk"]?.allowFrom).toEqual(["alice", "*"]);
|
||||
});
|
||||
@@ -1339,7 +1361,7 @@ describe("patchNestedChannelConfigSection", () => {
|
||||
section: "dm",
|
||||
clearFields: ["allowFrom"],
|
||||
enabled: true,
|
||||
patch: { policy: "disabled" },
|
||||
patch: { policy: "disabled" as const },
|
||||
});
|
||||
|
||||
expect(next.channels?.matrix?.enabled).toBe(true);
|
||||
@@ -1355,7 +1377,13 @@ describe("createTopLevelChannelDmPolicy", () => {
|
||||
channel: "line",
|
||||
policyKey: "channels.line.dmPolicy",
|
||||
allowFromKey: "channels.line.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.line?.dmPolicy ?? "pairing",
|
||||
getCurrent: (cfg) =>
|
||||
(cfg.channels?.line?.dmPolicy as
|
||||
| "open"
|
||||
| "pairing"
|
||||
| "allowlist"
|
||||
| "disabled"
|
||||
| undefined) ?? "pairing",
|
||||
});
|
||||
|
||||
const next = dmPolicy.setPolicy(
|
||||
@@ -1445,7 +1473,12 @@ describe("createNestedChannelDmPolicy", () => {
|
||||
section: "dm",
|
||||
policyKey: "channels.matrix.dm.policy",
|
||||
allowFromKey: "channels.matrix.dm.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.matrix?.dm?.policy ?? "pairing",
|
||||
getCurrent: (cfg) =>
|
||||
(
|
||||
cfg.channels?.matrix?.dm as
|
||||
| { policy?: "open" | "pairing" | "allowlist" | "disabled" }
|
||||
| undefined
|
||||
)?.policy ?? "pairing",
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ export const promptAccountId: PromptAccountId = async (params: PromptAccountIdPa
|
||||
return normalized;
|
||||
};
|
||||
|
||||
export function addWildcardAllowFrom(allowFrom?: Array<string | number> | null): string[] {
|
||||
export function addWildcardAllowFrom(allowFrom?: ReadonlyArray<string | number> | null): string[] {
|
||||
const next = (allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
|
||||
if (!next.includes("*")) {
|
||||
next.push("*");
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
splitSetupEntries,
|
||||
} from "./setup-wizard-helpers.js";
|
||||
import type {
|
||||
ChannelSetupPlugin,
|
||||
ChannelSetupWizardAdapter,
|
||||
ChannelSetupConfigureContext,
|
||||
ChannelSetupDmPolicy,
|
||||
@@ -17,7 +18,6 @@ import type {
|
||||
ChannelSetupStatusContext,
|
||||
} from "./setup-wizard-types.js";
|
||||
import type { ChannelSetupInput } from "./types.core.js";
|
||||
import type { ChannelPlugin } from "./types.js";
|
||||
|
||||
export type ChannelSetupWizardStatus = {
|
||||
configuredLabel: string;
|
||||
@@ -282,7 +282,7 @@ export type ChannelSetupWizard = {
|
||||
onAccountRecorded?: ChannelSetupWizardAdapter["onAccountRecorded"];
|
||||
};
|
||||
|
||||
type ChannelSetupWizardPlugin = Pick<ChannelPlugin, "id" | "meta" | "config" | "setup">;
|
||||
type ChannelSetupWizardPlugin = ChannelSetupPlugin;
|
||||
|
||||
async function buildStatus(
|
||||
plugin: ChannelSetupWizardPlugin,
|
||||
|
||||
@@ -115,11 +115,15 @@ export type ChannelConfigAdapter<ResolvedAccount> = {
|
||||
enabled: boolean;
|
||||
}) => OpenClawConfig;
|
||||
deleteAccount?: (params: { cfg: OpenClawConfig; accountId: string }) => OpenClawConfig;
|
||||
isEnabled?: (account: ResolvedAccount, cfg: OpenClawConfig) => boolean;
|
||||
disabledReason?: (account: ResolvedAccount, cfg: OpenClawConfig) => string;
|
||||
isConfigured?: (account: ResolvedAccount, cfg: OpenClawConfig) => boolean | Promise<boolean>;
|
||||
unconfiguredReason?: (account: ResolvedAccount, cfg: OpenClawConfig) => string;
|
||||
describeAccount?: (account: ResolvedAccount, cfg: OpenClawConfig) => ChannelAccountSnapshot;
|
||||
isEnabled?: BivariantCallback<(account: ResolvedAccount, cfg: OpenClawConfig) => boolean>;
|
||||
disabledReason?: BivariantCallback<(account: ResolvedAccount, cfg: OpenClawConfig) => string>;
|
||||
isConfigured?: BivariantCallback<
|
||||
(account: ResolvedAccount, cfg: OpenClawConfig) => boolean | Promise<boolean>
|
||||
>;
|
||||
unconfiguredReason?: BivariantCallback<(account: ResolvedAccount, cfg: OpenClawConfig) => string>;
|
||||
describeAccount?: BivariantCallback<
|
||||
(account: ResolvedAccount, cfg: OpenClawConfig) => ChannelAccountSnapshot
|
||||
>;
|
||||
resolveAllowFrom?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
@@ -261,26 +265,28 @@ export type ChannelOutboundAdapter = {
|
||||
export type ChannelStatusAdapter<ResolvedAccount, Probe = unknown, Audit = unknown> = {
|
||||
defaultRuntime?: ChannelAccountSnapshot;
|
||||
skipStaleSocketHealthCheck?: boolean;
|
||||
buildChannelSummary?: (params: {
|
||||
account: ResolvedAccount;
|
||||
cfg: OpenClawConfig;
|
||||
defaultAccountId: string;
|
||||
snapshot: ChannelAccountSnapshot;
|
||||
}) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
||||
probeAccount?: (params: {
|
||||
account: ResolvedAccount;
|
||||
timeoutMs: number;
|
||||
cfg: OpenClawConfig;
|
||||
}) => Promise<Probe>;
|
||||
buildChannelSummary?: BivariantCallback<
|
||||
(params: {
|
||||
account: ResolvedAccount;
|
||||
cfg: OpenClawConfig;
|
||||
defaultAccountId: string;
|
||||
snapshot: ChannelAccountSnapshot;
|
||||
}) => Record<string, unknown> | Promise<Record<string, unknown>>
|
||||
>;
|
||||
probeAccount?: BivariantCallback<
|
||||
(params: { account: ResolvedAccount; timeoutMs: number; cfg: OpenClawConfig }) => Promise<Probe>
|
||||
>;
|
||||
formatCapabilitiesProbe?: BivariantCallback<
|
||||
(params: { probe: Probe }) => ChannelCapabilitiesDisplayLine[]
|
||||
>;
|
||||
auditAccount?: (params: {
|
||||
account: ResolvedAccount;
|
||||
timeoutMs: number;
|
||||
cfg: OpenClawConfig;
|
||||
probe?: Probe;
|
||||
}) => Promise<Audit>;
|
||||
auditAccount?: BivariantCallback<
|
||||
(params: {
|
||||
account: ResolvedAccount;
|
||||
timeoutMs: number;
|
||||
cfg: OpenClawConfig;
|
||||
probe?: Probe;
|
||||
}) => Promise<Audit>
|
||||
>;
|
||||
buildCapabilitiesDiagnostics?: BivariantCallback<
|
||||
(params: {
|
||||
account: ResolvedAccount;
|
||||
@@ -291,25 +297,31 @@ export type ChannelStatusAdapter<ResolvedAccount, Probe = unknown, Audit = unkno
|
||||
target?: string;
|
||||
}) => Promise<ChannelCapabilitiesDiagnostics | undefined>
|
||||
>;
|
||||
buildAccountSnapshot?: (params: {
|
||||
account: ResolvedAccount;
|
||||
cfg: OpenClawConfig;
|
||||
runtime?: ChannelAccountSnapshot;
|
||||
probe?: Probe;
|
||||
audit?: Audit;
|
||||
}) => ChannelAccountSnapshot | Promise<ChannelAccountSnapshot>;
|
||||
logSelfId?: (params: {
|
||||
account: ResolvedAccount;
|
||||
cfg: OpenClawConfig;
|
||||
runtime: RuntimeEnv;
|
||||
includeChannelPrefix?: boolean;
|
||||
}) => void;
|
||||
resolveAccountState?: (params: {
|
||||
account: ResolvedAccount;
|
||||
cfg: OpenClawConfig;
|
||||
configured: boolean;
|
||||
enabled: boolean;
|
||||
}) => ChannelAccountState;
|
||||
buildAccountSnapshot?: BivariantCallback<
|
||||
(params: {
|
||||
account: ResolvedAccount;
|
||||
cfg: OpenClawConfig;
|
||||
runtime?: ChannelAccountSnapshot;
|
||||
probe?: Probe;
|
||||
audit?: Audit;
|
||||
}) => ChannelAccountSnapshot | Promise<ChannelAccountSnapshot>
|
||||
>;
|
||||
logSelfId?: BivariantCallback<
|
||||
(params: {
|
||||
account: ResolvedAccount;
|
||||
cfg: OpenClawConfig;
|
||||
runtime: RuntimeEnv;
|
||||
includeChannelPrefix?: boolean;
|
||||
}) => void
|
||||
>;
|
||||
resolveAccountState?: BivariantCallback<
|
||||
(params: {
|
||||
account: ResolvedAccount;
|
||||
cfg: OpenClawConfig;
|
||||
configured: boolean;
|
||||
enabled: boolean;
|
||||
}) => ChannelAccountState
|
||||
>;
|
||||
collectStatusIssues?: (accounts: ChannelAccountSnapshot[]) => ChannelStatusIssue[];
|
||||
};
|
||||
|
||||
@@ -935,31 +947,35 @@ export type ChannelSecurityAdapter<ResolvedAccount = unknown> = {
|
||||
cfg: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}) => ChannelDoctorConfigMutation | Promise<ChannelDoctorConfigMutation>;
|
||||
resolveDmPolicy?: (
|
||||
ctx: ChannelSecurityContext<ResolvedAccount>,
|
||||
) => ChannelSecurityDmPolicy | null;
|
||||
collectWarnings?: (ctx: ChannelSecurityContext<ResolvedAccount>) => Promise<string[]> | string[];
|
||||
collectAuditFindings?: (
|
||||
ctx: ChannelSecurityContext<ResolvedAccount> & {
|
||||
sourceConfig: OpenClawConfig;
|
||||
orderedAccountIds: string[];
|
||||
hasExplicitAccountPath: boolean;
|
||||
},
|
||||
) =>
|
||||
| Promise<
|
||||
Array<{
|
||||
resolveDmPolicy?: BivariantCallback<
|
||||
(ctx: ChannelSecurityContext<ResolvedAccount>) => ChannelSecurityDmPolicy | null
|
||||
>;
|
||||
collectWarnings?: BivariantCallback<
|
||||
(ctx: ChannelSecurityContext<ResolvedAccount>) => Promise<string[]> | string[]
|
||||
>;
|
||||
collectAuditFindings?: BivariantCallback<
|
||||
(
|
||||
ctx: ChannelSecurityContext<ResolvedAccount> & {
|
||||
sourceConfig: OpenClawConfig;
|
||||
orderedAccountIds: string[];
|
||||
hasExplicitAccountPath: boolean;
|
||||
},
|
||||
) =>
|
||||
| Promise<
|
||||
Array<{
|
||||
checkId: string;
|
||||
severity: "info" | "warn" | "critical";
|
||||
title: string;
|
||||
detail: string;
|
||||
remediation?: string;
|
||||
}>
|
||||
>
|
||||
| Array<{
|
||||
checkId: string;
|
||||
severity: "info" | "warn" | "critical";
|
||||
title: string;
|
||||
detail: string;
|
||||
remediation?: string;
|
||||
}>
|
||||
>
|
||||
| Array<{
|
||||
checkId: string;
|
||||
severity: "info" | "warn" | "critical";
|
||||
title: string;
|
||||
detail: string;
|
||||
remediation?: string;
|
||||
}>;
|
||||
>;
|
||||
};
|
||||
|
||||
@@ -79,6 +79,10 @@ export type ChannelConfigSchema = {
|
||||
/** Full capability contract for a native channel plugin. */
|
||||
type ChannelPluginSetupWizard = ChannelSetupWizard | ChannelSetupWizardAdapter;
|
||||
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
export type ChannelPlugin<ResolvedAccount = any, Probe = unknown, Audit = unknown> = {
|
||||
// Omitted generic means "plugin with some account shape", not "plugin whose
|
||||
// account is literally Record<string, unknown>".
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
export type ChannelPlugin<ResolvedAccount = any, Probe = unknown, Audit = unknown> = {
|
||||
id: ChannelId;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { vi } from "vitest";
|
||||
import type { ChannelPluginCatalogEntry } from "../channels/plugins/catalog.js";
|
||||
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import type { ChannelsConfig } from "../config/types.channels.js";
|
||||
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
|
||||
export function createMockChannelSetupPluginInstallModule(
|
||||
@@ -71,7 +72,7 @@ export function createMSTeamsDeletePlugin(): ChannelPlugin {
|
||||
delete nextChannels.msteams;
|
||||
return {
|
||||
...cfg,
|
||||
channels: nextChannels,
|
||||
channels: nextChannels as ChannelsConfig,
|
||||
};
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -603,13 +603,25 @@ describe("legacy migrate nested channel enabled aliases", () => {
|
||||
expect(res.config?.channels?.matrix?.groups?.["!ops:example.org"]).toEqual({
|
||||
enabled: false,
|
||||
});
|
||||
expect(res.config?.channels?.matrix?.accounts?.work?.rooms?.["!legacy:example.org"]).toEqual({
|
||||
expect(
|
||||
(
|
||||
res.config?.channels?.matrix?.accounts?.work as
|
||||
| { rooms?: Record<string, unknown> }
|
||||
| undefined
|
||||
)?.rooms?.["!legacy:example.org"],
|
||||
).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(res.config?.channels?.zalouser?.groups?.["group:trusted"]).toEqual({
|
||||
enabled: false,
|
||||
});
|
||||
expect(res.config?.channels?.zalouser?.accounts?.work?.groups?.["group:legacy"]).toEqual({
|
||||
expect(
|
||||
(
|
||||
res.config?.channels?.zalouser?.accounts?.work as
|
||||
| { groups?: Record<string, unknown> }
|
||||
| undefined
|
||||
)?.groups?.["group:legacy"],
|
||||
).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,21 +26,42 @@ export type ChannelDefaultsConfig = {
|
||||
|
||||
export type ChannelModelByChannelConfig = Record<string, Record<string, string>>;
|
||||
|
||||
export type ExtensionNestedPolicyConfig = {
|
||||
policy?: string;
|
||||
allowFrom?: Array<string | number> | ReadonlyArray<string | number>;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* Base type for extension channel config sections.
|
||||
* Extensions can use this as a starting point for their channel config.
|
||||
*/
|
||||
export type ExtensionChannelConfig = {
|
||||
enabled?: boolean;
|
||||
allowFrom?: string | string[];
|
||||
allowFrom?: Array<string | number> | ReadonlyArray<string | number>;
|
||||
/** Default delivery target for CLI --deliver when no explicit --reply-to is provided. */
|
||||
defaultTo?: string;
|
||||
defaultTo?: string | number;
|
||||
/** Optional default account id when multiple accounts are configured. */
|
||||
defaultAccount?: string;
|
||||
dmPolicy?: string;
|
||||
groupPolicy?: GroupPolicy;
|
||||
contextVisibility?: ContextVisibilityMode;
|
||||
healthMonitor?: ChannelHealthMonitorConfig;
|
||||
dm?: ExtensionNestedPolicyConfig;
|
||||
network?: Record<string, unknown>;
|
||||
groups?: Record<string, unknown>;
|
||||
rooms?: Record<string, unknown>;
|
||||
mediaMaxMb?: number;
|
||||
callbackBaseUrl?: string;
|
||||
interactions?: { callbackBaseUrl?: string; [key: string]: unknown };
|
||||
execApprovals?: Record<string, unknown>;
|
||||
threadBindings?: {
|
||||
enabled?: boolean;
|
||||
spawnAcpSessions?: boolean;
|
||||
spawnSubagentSessions?: boolean;
|
||||
};
|
||||
spawnSubagentSessions?: boolean;
|
||||
dangerouslyAllowPrivateNetwork?: boolean;
|
||||
accounts?: Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
@@ -122,5 +122,5 @@ export const ChannelsSchema: z.ZodType<ChannelsConfig | undefined> = z
|
||||
.superRefine((value, ctx) => {
|
||||
addLegacyChannelAcpBindingIssues(value, ctx);
|
||||
})
|
||||
.transform((value, ctx) => normalizeBundledChannelConfigs(value, ctx))
|
||||
.transform((value, ctx) => normalizeBundledChannelConfigs(value as ChannelsConfig, ctx))
|
||||
.optional() as z.ZodType<ChannelsConfig | undefined>;
|
||||
|
||||
@@ -42,7 +42,11 @@ function isDiscordExecApprovalClientEnabledForTest(params: {
|
||||
const rootConfig = params.cfg.channels?.discord?.execApprovals;
|
||||
const accountConfig =
|
||||
accountId && accountId !== "default"
|
||||
? params.cfg.channels?.discordAccounts?.[accountId]?.execApprovals
|
||||
? (
|
||||
params.cfg.channels?.discordAccounts?.[accountId] as
|
||||
| { execApprovals?: { enabled?: boolean; approvers?: unknown[] } }
|
||||
| undefined
|
||||
)?.execApprovals
|
||||
: undefined;
|
||||
const config = accountConfig ?? rootConfig;
|
||||
return Boolean(config?.enabled && (config.approvers?.length ?? 0) > 0);
|
||||
@@ -56,7 +60,11 @@ function isTelegramExecApprovalClientEnabledForTest(params: {
|
||||
const rootConfig = params.cfg.channels?.telegram?.execApprovals;
|
||||
const accountConfig =
|
||||
accountId && accountId !== "default"
|
||||
? params.cfg.channels?.telegramAccounts?.[accountId]?.execApprovals
|
||||
? (
|
||||
params.cfg.channels?.telegramAccounts?.[accountId] as
|
||||
| { execApprovals?: { enabled?: boolean; approvers?: unknown[] } }
|
||||
| undefined
|
||||
)?.execApprovals
|
||||
: undefined;
|
||||
const config = accountConfig ?? rootConfig;
|
||||
return Boolean(config?.enabled && (config.approvers?.length ?? 0) > 0);
|
||||
|
||||
@@ -157,7 +157,10 @@ describe("secrets runtime snapshot matrix shadowing", () => {
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.matrix?.accounts?.ops?.password).toEqual({
|
||||
expect(
|
||||
(snapshot.config.channels?.matrix?.accounts?.ops as { password?: unknown } | undefined)
|
||||
?.password,
|
||||
).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_OPS_PASSWORD",
|
||||
@@ -331,7 +334,10 @@ describe("secrets runtime snapshot matrix shadowing", () => {
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.matrix?.accounts?.default?.password).toEqual({
|
||||
expect(
|
||||
(snapshot.config.channels?.matrix?.accounts?.default as { password?: unknown } | undefined)
|
||||
?.password,
|
||||
).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_DEFAULT_PASSWORD",
|
||||
|
||||
@@ -116,12 +116,11 @@ describe("secrets runtime snapshot nextcloud talk file precedence", () => {
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.["nextcloud-talk"]?.accounts?.work?.botSecret).toBe(
|
||||
"resolved-nextcloud-work-bot-secret",
|
||||
);
|
||||
expect(snapshot.config.channels?.["nextcloud-talk"]?.accounts?.work?.apiPassword).toBe(
|
||||
"resolved-nextcloud-work-api-password",
|
||||
);
|
||||
const workAccount = snapshot.config.channels?.["nextcloud-talk"]?.accounts?.work as
|
||||
| { botSecret?: unknown; apiPassword?: unknown }
|
||||
| undefined;
|
||||
expect(workAccount?.botSecret).toBe("resolved-nextcloud-work-bot-secret");
|
||||
expect(workAccount?.apiPassword).toBe("resolved-nextcloud-work-api-password");
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain(
|
||||
"channels.nextcloud-talk.accounts.work.botSecret",
|
||||
);
|
||||
|
||||
@@ -97,9 +97,10 @@ describe("secrets runtime snapshot zalo token activity", () => {
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.zalo?.accounts?.work?.botToken).toBe(
|
||||
"resolved-zalo-work-token",
|
||||
);
|
||||
expect(
|
||||
(snapshot.config.channels?.zalo?.accounts?.work as { botToken?: unknown } | undefined)
|
||||
?.botToken,
|
||||
).toBe("resolved-zalo-work-token");
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain(
|
||||
"channels.zalo.accounts.work.botToken",
|
||||
);
|
||||
@@ -153,9 +154,10 @@ describe("secrets runtime snapshot zalo token activity", () => {
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.zalo?.accounts?.default?.botToken).toBe(
|
||||
"resolved-zalo-default-token",
|
||||
);
|
||||
expect(
|
||||
(snapshot.config.channels?.zalo?.accounts?.default as { botToken?: unknown } | undefined)
|
||||
?.botToken,
|
||||
).toBe("resolved-zalo-default-token");
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain(
|
||||
"channels.zalo.accounts.default.botToken",
|
||||
);
|
||||
|
||||
@@ -53,9 +53,7 @@ function createSynologyChatAccount(params: {
|
||||
}): ResolvedSynologyChatAccount {
|
||||
const channel = params.cfg.channels?.["synology-chat"] ?? {};
|
||||
const accountConfig =
|
||||
params.accountId === "default"
|
||||
? channel
|
||||
: ((channel.accounts as Record<string, unknown> | undefined)?.[params.accountId] ?? {});
|
||||
params.accountId === "default" ? channel : (channel.accounts?.[params.accountId] ?? {});
|
||||
return {
|
||||
accountId: params.accountId,
|
||||
dangerouslyAllowNameMatching:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Centralized Vitest mock type for harness modules under `src/`.
|
||||
// Using an explicit named type avoids exporting inferred `vi.fn()` types that can trip TS2742.
|
||||
// Test harnesses need a permissive default callable shape so vi.fn() can stand in for many signatures.
|
||||
// Keep the callable bound permissive so explicit callback signatures remain assignable.
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
export type MockFn<T extends (...args: any[]) => unknown = (...args: any[]) => unknown> =
|
||||
export type MockFn<T extends (...args: any[]) => any = (...args: any[]) => any> =
|
||||
import("vitest").Mock<T>;
|
||||
|
||||
Reference in New Issue
Block a user